Not for Sale! Εκρήξεις και particle systems
Αναρτήθηκε από τον/την Κώστας Αναγνώστου στο Μαρτίου 4, 2011
Έχει περάσει πολύς καιρός από το προηγούμενο άρθρο στη σειρά tutorial ανάπτυξης του Not for Sale! Και ενώ ο κώδικας για το συγκεκριμένο άρθρο είναι έτοιμος εδώ και καιρό, δεν μπορούσα να βρω χρόνο για να γράψω το κείμενο. Ούτε τώρα βρήκα, αλλά για να μην κάθεται ο κώδικας τον ανεβάζω με μερικά σύντομα σχόλια για την λειτουργία του.
Σήμερα θα ασχοληθούμε με τα συστήματα σωματιδίων (particle systems) τα οποία είναι ένας εύκολος και ευέλικτος τρόπος να δημιουργήσουμε πολλών διαφορετικών ειδών εφέ σε ένα παιχνίδι. Είχα κάνει μια σύντομη αναφορά στα συστήματα σωματιδίων σε ένα παλιό άρθρο για την OpenGL, η λογική στην οποία θα βασιστούμε είναι ίδια και στην συγκεκριμένη περίπτωση.
Σωματίδιο (particle) ονομάζουμε μια «οντότητα» που χαρακτηρίζεται από διάφορες παραμέτρους, κατ’ελάχιστον μια θέση στο χώρο, το χρόνο ζωής και ένα διάνυσμα ταχύτητας. Σε ένα σωματίδιο μπορούμε να αποδώσουμε φυσικές παραμέτρους όπως βαρύτητα, επιβράδυνση, μέγεθος, διαφάνεια. Επιπλέον μπορούμε να απεικονίσουμε μια υφή σε αυτό και να το φωτίσουμε με τα φώτα της σκηνής. Αν θεωρήσουμε έναν πεπερασμένο αριθμό τέτοιων σωματιδίων και μια πηγή που τα εκπέμπει τότε έχουμε ένα σύστημα σωματιδίων (particle system).
Στα πλαίσια του παιχνιδιού θα αναπαραστήσουμε ένα σωματίδιο με μια class Particle που θα περιέχει τα παρακάτω δεδομένα
class Particle
{
Rectangle rectangle;
float rotationSpeed;
float rotation;
Vector2 velocity;
float timeToLive;
float elapsedTimeToLive;
float startSize;
float endSize;
bool active;
Vector2 origin;
Texture2D texture;
Color color;
}
Στην ουσία ορίζουμε το Rectangle της υφής του σωματιδίου (που καθορίζει θέση και μέγεθος), μια υφή Texture2D, ταχύτητα Velocity, περιστροφή Rotation, χρόνο ζωής timeToLive κλπ. Επιπλέον ορίζουμε και τις μεθόδους Reset, Update και Render που καθορίζουν την συμπεριφορά του κάθε particle:
class Particle
{
//data
public bool Active
{
get { return active; }
}
public Particle(Texture2D texture)
{
rectangle = new Rectangle(0, 0, 1, 1);
this.texture = texture;
active = false;
//set origin to sprite centre for correct rotation
origin = new Vector2(texture.Width / 2, texture.Height / 2);
}
public void Reset(Vector2 position, Vector2 velocity, float startSize, float endSize, float rotationSpeed, Color color, float timeToLive)
{
this.timeToLive = timeToLive;
this.rotationSpeed = rotationSpeed;
this.velocity = velocity;
this.startSize = startSize;
this.endSize = endSize;
this.color = color;
//set position and dimensions
rectangle.X = (int)position.X;
rectangle.Y = (int)position.Y;
active = true;
rotation = 0;
elapsedTimeToLive = timeToLive;
}
public void Update(GameTime gameTime, Rectangle viewport)
{
if (active)
{
float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
elapsedTimeToLive -= elapsed;
//move particle
rectangle.X += (int)(velocity.X * elapsed);
rectangle.Y += (int)(velocity.Y * elapsed);
float size = MathHelper.Lerp(startSize, endSize, 1 - elapsedTimeToLive / timeToLive);
rectangle.Width = (int)size;
rectangle.Height = (int)size;
//increase rotation
rotation += rotationSpeed * elapsed;
//reduce particle opacity as time passes by. It will reach 0 when timeToLive reaches 0.
color.A = (byte)(255 * elapsedTimeToLive / timeToLive);
//if time to live reaches 0 or particle goes out of viewport then particle dies
if (elapsedTimeToLive <= 0 || !rectangle.Intersects(viewport))
{
active = false;
}
}
}
public void Render(SpriteBatch sb)
{
if (active)
{
//if active, draw particle
sb.Draw(texture, rectangle, null, color, rotation, origin, SpriteEffects.None, 0);
}
}
}
H Reset είναι βασικής σημασίας μιας και σε ένα σύστημα σωματιδίων ανακυκλώνουμε σωματίδια και δεν δημιουργούμε συνεχώς νέα, για λόγους απόδοσης. Είναι σημαντικό λοιπόν να μπορούμε να επαναρχικοποιούμε ένα “dead” σωματίδιο. Κατά τα άλλα η Update αλλάζει τις παραμέτρους ενός σωματιδίου (θέση, περιστροφή, διαφάνεια κλπ) όσο αυτό είναι ενεργό (elapsedTimeToLive <= 0). Την διαφάνεια του σωματιδίου την ελαττώνουμε γραμμικά σε σχέση με το χρόνο έτσι ώστε αυτό θα εξαφανίζεται προς το τέλος της ζωής του ομαλά: color.A = (byte)(255 * elapsedTimeToLive / timeToLive);
Η Render απλά απεικονίζει το sprite ενός ενεργού σωματιδίου. Εδώ χρησιμοποιούμε μια διαφορετική έκδοση της Draw η οποία δέχεται ως όρισμα την περιστροφή rotation. Προσοχή πρέπει να δοθεί επίσης στην παράμετρο origin η οποία ορίζει το σημείο περιστροφής (pivot), το οποίο έχουμε ορίζει ως το κέντρο του sprite.
Τώρα, μιας και τα σωματίδια από μόνα τους δεν κάνουν και πολλά, πρέπει να ορίσουμε και ένα σύστημα το οποίο θα τα διαχειρίζεται. Αυτό το κάνει η κλάση ParticleSystem:
abstract class ParticleSystem : GameEntity
{
//particle system parameters
protected List particles;
protected int maxParticles;
protected bool active;
protected bool emitParticles;
protected float timeToNext;
protected Random random;
protected Vector2 position;
protected Vector2 velocity;
protected float duration;
protected float emitParticleDuration;
protected float rate;
protected bool alwaysActive;
//particle parameters
protected float minHorizontalSpeed, maxHorizontalSpeed;
protected float minVerticalSpeed, maxVerticalSpeed;
protected float minRotationSpeed, maxRotationSpeed;
protected float minTimeToLive, maxTimeToLive;
protected int minStartSize, maxStartSize;
protected int minEndSize, maxEndSize;
protected Vector2 gravity;
protected Color minColor, maxColor;
//a particle system is considered active while at least one of the emitted particles is still alive
public bool Active
{
get { return active; }
}
public Vector2 Velocity
{
set { velocity = value; }
}
public Vector2 Position
{
set { position = value; }
}
public bool EmitParticles
{
set { emitParticles = value; }
}
public ParticleSystem()
{ }
public ParticleSystem(Texture2D texture)
{
this.texture = texture;
}
protected virtual void InitialiseSystem()
{
}
public void Initialise()
{
alwaysActive = false;
duration = 0f;
InitialiseSystem();
if (!alwaysActive && duration > 0f)
{
rate = maxParticles / duration;
}
particles = new List(maxParticles);
for (int i = 0; i < maxParticles; i++)
{
particles.Add(new Particle(texture));
}
active = false;
emitParticles = false;
random = new Random();
}
public virtual void Reset(Vector2 position, Vector2 velocity)
{
active = true;
emitParticles = true;
this.position = position;
this.velocity = velocity;
emitParticleDuration = duration;
}
public override void Update(GameTime gameTime, Rectangle viewport)
{
//if particle system is active, update it
if (active)
{
timeToNext -= (float)gameTime.ElapsedGameTime.TotalSeconds;
emitParticleDuration -= (float)gameTime.ElapsedGameTime.TotalSeconds;
//check if we still emit particles
if (emitParticles && timeToNext <= 0)
{
//time to generate a new particle. Find the first unused in the list
foreach (Particle particle in particles)
{
if (!particle.Active)
{
//find particle velocity based on specified horizontal-vertical range
Vector2 vel = new Vector2(MathHelper.Lerp(minHorizontalSpeed, maxHorizontalSpeed, (float)random.NextDouble()),
MathHelper.Lerp(minVerticalSpeed, maxVerticalSpeed, (float)random.NextDouble()));
//add random velocity to particle system velocity to provide a uniform direction to all particles
vel += velocity;
//add gravity to the velocity
vel += gravity;
//find particle start size;
float startSize = MathHelper.Lerp(minStartSize, maxStartSize, (float)random.NextDouble());
//find particle end size
float endSize = MathHelper.Lerp(minEndSize, maxEndSize, (float)random.NextDouble());
//find particle rotation speed
float rotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, (float)random.NextDouble());
//how long will the particle live?
float timeToLive = MathHelper.Lerp(minTimeToLive, maxTimeToLive, (float)random.NextDouble());
//determine particle color
Color color = Color.Lerp(minColor, maxColor, (float)random.NextDouble());
//reset particle
particle.Reset(position, vel, startSize, endSize, rotationSpeed, color, timeToLive);
break;
}
}
timeToNext = 1.0f / rate;
}
if (emitParticleDuration <= 0 && !alwaysActive)
{
//stop emitting particles
emitParticles = false;
}
active = false;
//update particles
foreach (Particle particle in particles)
{
if (particle.Active)
{
particle.Update(gameTime, viewport);
}
//particle system is active as long as at least one particle is active
//this check is not valid if the particle system is always active
active = active || particle.Active || alwaysActive;
}
}
}
public override void Render(SpriteBatch sb)
{
foreach (Particle particle in particles)
{
//render all active particles
if (particle.Active)
{
particle.Render(sb);
}
}
}
}
Ένα particle system είναι στην ουσία μια πηγή η οποία «γεννά» νέα σωματίδια με συγκεκριμένο ρυθμό, ταχύτητα και διεύθυνση. Τη θεωρούμε μια «οντότητα» όπως κάθε άλλο game entity του παιχνιδιού η οποία έχει χρόνο ζωής και μπορεί να μετακινηθεί αν χρειαστεί και η ίδια (φανταστείτε τη φωτιά που βγάζει η τουρμπίνα ενός αεροσκάφους για παράδειγμα). Στην υλοποίηση μας κάνουμε την κλάση ParticleSystem να «κληρονομεί» την GameEntity περισσότερο για ευκολία μιας και έτσι μπορούμε να χρησιμοποιήσουμε το Game Entity Factory του παιχνιδιού και να δημιουργούμε νέα particle systems προσδιορίζοντας το όνομα τους και μόνο.
Η κλάση ParticleSystem κρατά 2 σετ παραμέτρους, αυτές που αφορούν το particle system ίδιο:
//particle system parameters
protected List particles;
protected int maxParticles;
protected bool active;
protected bool emitParticles;
protected float timeToNext;
protected Random random;
protected Vector2 position;
protected Vector2 velocity;
protected float duration;
protected float emitParticleDuration;
protected float rate;
protected bool alwaysActive;
και αυτές που αφορούν τα σωματίδια τα οποία «γεννά»:
//particle parameters
protected float minHorizontalSpeed, maxHorizontalSpeed;
protected float minVerticalSpeed, maxVerticalSpeed;
protected float minRotationSpeed, maxRotationSpeed;
protected float minTimeToLive, maxTimeToLive;
protected int minStartSize, maxStartSize;
protected int minEndSize, maxEndSize;
protected Vector2 gravity;
protected Color minColor, maxColor;
Το κάθε particle system διατηρεί μια λίστα (List particles) με ένα maximum αριθμό σωματιδίων τα οποία δημιουργεί κατά την δημιουργία του και επαναχρησιμοποιεί κάθε φορά που το ενεργοποιούμε. Δεν αποδεσμεύουμε ποτέ τα σωματίδια όσο τρέχει το game loop και δεν δημιουργούμε νέα. Αυτό είναι ιδιαίτερα σημαντικό σε managed περιβάλλοντα όπως το .NET με αυτόματη διαδικασία Garbage Collection (αλλά και εκτός managed περιβαλλόντων δεν θεωρείται καλή πρακτική γενικά να δεσμεύουμε/αποδεσμεύουμε μνήμη στο game loop). Επιπλέον το particle system έχει πληροφορίες όπως θέση και ταχύτητα της πηγής, ρυθμός με τον οποίο γεννά σωματίδια κλπ.
Όσον αφορά τα σωματίδια τα ίδια, κάθε σωματίδιο που γεννά η πηγή πρέπει να αρχικοποιηθεί με κάποιες παραμέτρους όπως η ταχύτητα του, η περιστροφή του, το μέγεθος του, το χρώμα του κλπ. Για να δώσουμε την αίσθηση του τυχαίου και της ποικιλίας στο σωματίδια που δημιουργεί η πηγή δεν ορίζουμε απόλυτες τιμές για τις παραμέτρους αυτές (διαφορετικά θα ήταν όλα ίδια τα σωματίδια και το εφφε όχι τόσο εντυπωσιακό), αλλά ορίζουμε εύρος τιμής για τη κάθε μια παράμετρο. Παράδειγμα για την οριζόντια ταχύτητα ορίζουμε το εύρος [minHorizontalSpeed, maxHorizontalSpeed], για το αρχικό μέγεθος [minStartSize, maxStartSize], για το χρώμα [minColor, maxColor]. Το particle system κάθε φορά που δημιουργεί (δηλαδή επαναρχικοποιεί) ένα σωματίδιο διαλέγει τυχαία μια τιμή για κάθε παράμετρο από αυτό το εύρος τιμών.
Από τις μεθόδους της κλάσης ParticleSystem θα σταθώ σε 2: Η Update αν και φαίνεται περίπλοκη αυτό που κάνει είναι, όσο το σύστημα είναι ενεργό, να εκπέμπει νέα σωματίδια (επιλέγοντας τα ανενεργά από τη λίστα particles) αρχικοποιώντας τα με τυχαίες τιμές από το εύρος τιμών για κάθε παράμετρο, όπως εξηγήσαμε παραπάνω. Πρέπει να παρατηρήσουμε ότι χρησιμοποιούμε 2 boolean μεταβλητές για να ελέγξουμε τη λειτουργία του συστήματος την active και την emitParticles. Όταν η emitParticles είναι false το σύστημα σταματά να γεννά νέα σωματίδια, δεν παύει όμως να είναι ενεργό (active). Το σύστημα γίνεται ανενεργό (οπότε σταματάμε να το κάνουμε Update και Render) μόνο όταν έχουν «πεθάνει» όλα τα σωματίδια που έχει δημιουργήσει. Σε διαφορετική περίπτωση θα δούμε τα σωματίδια να σβήνουν απότομα στην οθόνη όταν απενεργοποιηθεί το σύστημα σωματιδίων. Ενδιαφέρον έχει επίσης και η μέθοδος Initialise() η οποία δημιουργεί τη λίστα με τα σωματίδια (particles). Επιπλέον καλή και μια virtual μέθοδο με το όνομα InitialiseSystem που στην κλάση ParticleSystem δεν φαίνεται να κάνει τίποτα. Και με αφορμή αυτό να εξηγήσω γιατί η κλάση ParticleSystem είναι abstract.
Ο λόγος που η ParticleSystem είναι abstract είναι γιατί δεν θέλω ποτέ να δημιουργήσω ένα αντικείμενο από αυτή. Μιας και δεν έχω δώσει τιμές στις παραμέτρους του particle system αυτό δεν θα κάνει τίποτα. Αυτό που θέλω είναι να ενσωματώσω όλα τα δεδομένα και τις βασικές λειτουργίες ενός συστήματος στην κλάση ParticleSystem και με την χρήση subclassing να δημιουργήσω νέες κλάσης που δίνουν τιμές στις παραμέτρους και εξειδικεύουν την λειτουργία του συστήματος κάνοντας override InitialiseSystem. Αυτό μου δίνει την δυνατότητα να δημιουργήσω μια μεγάλη ποικιλία εφφέ. Για παράδειγμα, η παρακάτω κλάση δημιουργεί μια έκρηξη:
class ParticleExplosion : ParticleSystem
{
public ParticleExplosion(Texture2D texture) : base(texture)
{
}
protected override void InitialiseSystem()
{
//set params specific to the explosion particle system
maxParticles = 100;
duration = 0.5f;
minHorizontalSpeed = -100;
maxHorizontalSpeed = 100;
minVerticalSpeed = -100;
maxVerticalSpeed = 100;
minRotationSpeed = -3;
maxRotationSpeed = 3;
minTimeToLive = 0.5f;
maxTimeToLive = 1.0f;
minStartSize = 50;
maxStartSize = 80;
minEndSize = 90;
maxEndSize = 110 ;
gravity = Vector2.Zero;
minColor = new Color(200, 200, 200, 255);
maxColor = new Color(255, 255, 255, 255);
}
public override GameEntity CreateClone()
{
//create and initialise particle system
ParticleSystem system = new ParticleExplosion(texture);
system.Initialise();
return system;
}
}
Το μόνο που κάνει στην ουσία είναι να override την IntialiseSystem και να δίνει δικές τις τιμές στις μεταβλητές του συστήματος. Η παρακάτω κλάση δημιουργεί σπίθες (sparks):
class ParticleSparks : ParticleSystem
{
public ParticleSparks(Texture2D texture) : base(texture)
{
}
protected override void InitialiseSystem()
{
//set params specific to the explosion particle system
maxParticles = 100;
duration = 0.5f;
minHorizontalSpeed = -500;
maxHorizontalSpeed = 500;
minVerticalSpeed = -500;
maxVerticalSpeed = 500;
minRotationSpeed = 0;
maxRotationSpeed = 0;
minTimeToLive = 0.4f;
maxTimeToLive = 0.6f;
minStartSize = 6;
maxStartSize = 6;
minEndSize = 6;
maxEndSize = 6;
gravity = Vector2.Zero;
minColor = Color.White;
maxColor = Color.White;
}
public override GameEntity CreateClone()
{
//create and initialise particle system
ParticleSystem system = new ParticleSparks(texture);
system.Initialise();
return system;
}
}
Ούτε αυτή κάνει κάτι διαφορετικό αλλά το τελικό εφφέ είναι εντελώς διαφορετικό από την έκρηξη. Μπορούμε να το πάμε ακόμα πιο μακριά δημιουργώντας με ελάχιστες αλλαγές μια φωτιά τουρμπίνας αεροπλάνου (trail):
class ParticleTrail : ParticleSystem
{
public ParticleTrail(Texture2D texture)
: base(texture)
{
}
protected override void InitialiseSystem()
{
//set params specific to the explosion particle system
maxParticles = 100;
alwaysActive = true;
rate = 80;
minHorizontalSpeed = 0;
maxHorizontalSpeed = 0;
minVerticalSpeed = -100;
maxVerticalSpeed = 0;
minRotationSpeed = -3;
maxRotationSpeed = 3;
minTimeToLive = 0.5f;
maxTimeToLive = 1.0f;
minStartSize = 20;
maxStartSize = 30;
minEndSize = 5;
maxEndSize = 5;
gravity = Vector2.Zero;
minColor = new Color(200, 200, 200, 255);
maxColor = new Color(255, 255, 255, 255);
}
public override GameEntity CreateClone()
{
//create and initialise particle system
ParticleSystem system = new ParticleTrail(texture);
system.Initialise();
return system;
}
}
Η βασική διαφορά (εκτός φυσικά από το εύρος των τιμών) είναι ότι θέτουμε την μεταβλητή alwaysActive σε true μιας και δεν θέλουμε να σβήσει η φωτιά της μηχανής.
Σε όλα τα παραπάνω εφέ η πηγή στα συστήματα σωματιδίων ήταν συγκεκριμένη (αν και μπορεί να μετακινούνταν). Με μικρές πάλι αλλαγές μπορούμε με το ίδιο μηχανισμό να φτιάξουμε εφέ όπως βροχή αστεροειδών η οποία δεν έχει συγκεκριμένη πηγή.
class ParticleRain : ParticleSystem
{
public ParticleRain(Texture2D texture)
: base(texture)
{
}
protected override void InitialiseSystem()
{
//set params specific to the explosion particle system
maxParticles = 400;
alwaysActive = true;
rate = 80;
minHorizontalSpeed = 0;
maxHorizontalSpeed = 0;
minVerticalSpeed = 0;
maxVerticalSpeed = 1000;
minRotationSpeed = 0;
maxRotationSpeed = 0;
minTimeToLive = 10f;
maxTimeToLive = 10f;
minStartSize = 6;
maxStartSize = 6;
minEndSize = 6;
maxEndSize = 6;
gravity = Vector2.Zero;
minColor = Color.White;
maxColor = Color.White;
}
public override void Update(GameTime gameTime, Rectangle viewport)
{
position = new Vector2((float)(random.NextDouble() * viewport.Width), 0);
base.Update(gameTime, viewport);
}
public override GameEntity CreateClone()
{
//create and initialise particle system
ParticleSystem system = new ParticleRain(texture);
system.Initialise();
return system;
}
}
Το επιπλέον που έκανα στην περίπτωση αυτή ήταν να override και την Update του ParticleSystem, και να αλλάζω τυχαία την θέση της «πηγής» έτσι ώστε να δημιουργεί νέα σωματίδια σε διαφορετική θέση κάθε φορά. Οι δυνατότητες του τι μπορείτε να πετύχετε είναι απεριόριστες! ![]()
Τελικά κάθε άλλο παρά σύντομο βγήκε και άρθρο και δεν μπήκα και σε λεπτομέρειες υλοποίησης. Για να ολοκληρώσω αξίζει να αναφερθώ στην κλάση ParticleManager την οποίας η λειτουργία είναι να συγκεντρώνει και να ελέγχει όλα τα διαφορετικά συστήματα σωματιδίων που υπάρχουν στο παιχνίδι. Και αυτή λειτουργεί με την λογική την προ-δημιουργίας συστημάτων σωματιδίων και επαναχρησιμοποίησης για την αποφυγή δέσμευσης/αποδέσμευσης μνήμης κατά τη διάρκεια του game loop. Είναι και αυτή Singleton όπως και οι υπόλοιποι managers του παιχνιδιού μιας και χρειάζομαι μόνο ένα.
class ParticleManager
{
protected Dictionary> particleSystems;
static protected ParticleManager particleManager = null;
static public ParticleManager Manager
{
get
{
if (particleManager == null)
{
particleManager = new ParticleManager();
}
return particleManager;
}
}
protected ParticleManager()
{
particleSystems = new Dictionary>(10);
}
public void RegisterParticleSystem(string name)
{
if (!particleSystems.ContainsKey(name))
{
//preallocate particle system list only when no list exists for that name
//I am expecting at most 50 systems of this type on screen at the same time.
List list = new List(50);
for (int i = 0; i < 50; i++)
{
list.Add((ParticleSystem)GameEntityFactory.CreateEntity(name));
}
//add list to dictionary
particleSystems.Add(name, list);
}
}
public ParticleSystem AddParticleSystem(string name, Vector2 position, Vector2 velocity)
{
List list;
if (particleSystems.TryGetValue(name, out list))
{
//grab first dead particle system of this type
foreach (ParticleSystem system in list)
{
if (!system.Active)
{
system.Reset(position, velocity);
return system;
}
}
}
return null;
}
public void Reset()
{
//erase everything
particleSystems.Clear();
}
public void Update(GameTime gameTime, Rectangle viewport)
{
//update all particle systems
foreach (KeyValuePair> pair in particleSystems)
{
foreach (ParticleSystem system in pair.Value)
{
system.Update(gameTime, viewport);
}
}
}
public void Render(SpriteBatch sb)
{
//render all particle systems
foreach (KeyValuePair> pair in particleSystems)
{
foreach (ParticleSystem system in pair.Value)
{
system.Render(sb);
}
}
}
}
Για να χρησιμοποιήσουμε τα συστήματα σωματιδίων πρέπει αρχικά (στην LoadContent) να φτιάξουμε τα template και να τα κάνουμε register στο GameEntityFactory:
//register template for various particle systems
GameEntityFactory.RegisterEntityCategory("Explosion", new ParticleExplosion(Content.Load("Textures/fire")));
GameEntityFactory.RegisterEntityCategory("Trail", new ParticleTrail(Content.Load("Textures/fire")));
GameEntityFactory.RegisterEntityCategory("Sparks", new ParticleSparks(Content.Load("Textures/spark")));
GameEntityFactory.RegisterEntityCategory("Rain", new ParticleRain(Content.Load("Textures/spark")));
Εδώ ορίζουμε και τις υφές που θα χρησιμοποιούν. Στην συνέχεια (στην loadGameLevel) πρέπει να ειδοποιήσουμε το ParticleManager για την ύπαρξη τους:
ParticleManager.Manager.RegisterParticleSystem("Explosion");
ParticleManager.Manager.RegisterParticleSystem("Sparks");
ParticleManager.Manager.RegisterParticleSystem("Rain");
ParticleManager.Manager.RegisterParticleSystem("Trail");
Κάθε φορά που θέλουμε να δημιουργήσουμε ένα εφέ το μόνο που έχουμε να κάνουμε είναι να καλέσουμε τη μέθοδο AddParticleSystem του ParticleManager. Για παράδειγμα αν θέλω να προσθέσω μια έκρηξη με σπίθες κάθε φορά που καταστρέφεται ένας εχθρός θα κάνω τις παρακάτω προσθήκες στην κλάση EnemyShip:
public int ApplyDamage(int damage)
{
shield -= damage;
hitFeedbackElapsed = 0.1;
hitFeedback = true;
if (shield <= 0)
{
status = Status.Dead;
ParticleManager.Manager.AddParticleSystem("Explosion", new Vector2(rectangle.Center.X,rectangle.Center.Y), Vector2.Zero);
ParticleManager.Manager.AddParticleSystem("Sparks", new Vector2(rectangle.Center.X, rectangle.Center.Y), Vector2.Zero);
return maxShield;
}
return 0;
}
Και αυτό ήταν! Φυσικά δεν ξεχνάμε να κάνουμε Update και Render το ParticleManager αντικείμενο στις αντίστοιχες μεθόδους του παιχνιδιού.
Η παρακάτω εικόνα δείχνει τα 4 συστήματα σωματιδίων (έκρηξη, σπίθες, trail, και βροχή) του παιχνιδιού σε δράση:

Ο κώδικας είναι διαθέσιμος στο Code Repository σε zip μορφή.
Μελετήστε το κώδικα του παιχνιδιού ως τώρα, σκεφτείτε αλλαγές και βελτιώσεις, και μείνετε συντονισμένοι γιατί ακολουθεί ανακοίνωση στις αρχές της επόμενης εβδομάδας!

Ανάπτυξη Βιντεοπαιχνιδιών » Blog Archive » [videogames laboratory] Not for Sale! Εκρήξεις και particle systems είπε
[...] Έχει περάσει πολύς καιρός από το προηγούμενο άρθρο στη σειρά tutorial ανάπτυξης του Not for Sale! Και ενώ ο κώδικας για το συγκεκριμένο άρθρο είναι έτοιμος εδώ και καιρό, δεν μπορούσα [Κώστας Αναγνώστου] να βρω χρόνο για να γράψω το κείμενο. Ούτε τώρα βρήκα, αλλά για να μην κάθεται ο κώδικας τον ανεβάζω με μερικά σύντομα σχόλια για την λειτουργία του. [Περισσότερα...] [...]
darklynx είπε
Πολύ όμορφο αποτέλεσμα με σχετικά λίγο κώδικα.Χαίρομαι που δεν πέσατε στον πειρασμό να χρησιμοποιήσετε point sprites για την αναπαράσταση των particles,διότι ενώ στο XNA 3.1 αποτελούν την καλύτερη ίσως λύση,στο XNA 4.0 έχουν εξαφανιστεί.
Αναμένουμε με αγωνία τις ανακοινώσεις!
Κώστας Αναγνώστου είπε
Δεν είναι πάντα τα point sprites η καλύτερη λύση, εξαρτάται πάντα απο φόρτο της gpu και αν είναι vertex η pixel bound το παιχνίδι. Η υλοποίηση του post επιβαρύνει το vertex processing τμήμα περισσότερο, αλλα αυτό για το παιχνίδι μας δεν αποτελεί πρόβλημα μιας και είναι 2d. Γενικά πάντως τα παιχνίδια είναι pixel bound λόγω του μεγέθους των υφων, normal mapping, πολυπλοκων μοντέλων φωτισμού, post-processing κλπ. Αυτό ήταν νομίζω και το σκεπτικό για την κατάργηση των point sprites στο XNA 4.
Γιώργος είπε
aha. Τώρα είδα την συνέχεια του Not For Sale. Ίσως και κάνα tweet, όταν ενημερώνεται να μην ήταν και άσχημη ιδέα. Θα το δοκιμάσω και θα επανέλθω!!
Γιώργος είπε
Τώρα ολοκλήρωσα την ανάγνωση. Μέχρι και τον ορισμό της κλάσης ParticleSystem, μπορώ να πω ότι είναι αρκετά βατό. Μετά που μπλέκει με τον ParticleManager και την GameFactory γίνεται αρκετά advanced.
Το gravity στα particles, ως προς τη ορίζεται; Ως προς το κέντρο του particle emitter;
Κώστας Αναγνώστου είπε
Οχι, το gravity vector δείχνει πάντα προς το κάτω μέρος της οθόνης.