Částicový systém v XNA
V minulé lekci, XNA tvorba ve 3D - vykreslení trojúhelníku, jsme kreslili trojúhelník.
Vítejte po… a vlastně ne, tento díl nebudeme počítat. V tomto díle,
který nikterak nezapadá do sekvence bludiště ani k hlsl shaderům, si
vytvořím pro náš engin snadný částicový systém. Prozatím pouze ve 2D
provedení. Budeme potřebovat projekt s nastaveným enginem. Než začneme
tvořit samotný systém, bylo by dobré popsat si jak to bude fungovat.
Manažer nám bude postupně v přesně zadaném intervalu vypouštět
částice. Částicí budeme rozumět otexturovaný obdélník vykreslený
pomocí SpriteBatche
. Rozhodl jsem se použít právě
SpriteBatch
, protože nám řeší vykreslování a je poměrně
dobře optimalizována. Prostě jen říkáme kde co chceme vykreslit. To pro 2D
systém postačuje, ale pro plnohodnotný 3D systém to je málo. Každá
částice bude mít svou polohu, rychlost pohybu (udanou vektorem), čas od
vypuštění, čas kdy začne mizet, čas kdy zmizí úplně a deaktivuje se
tak, měřítko na začátku a měřítko na konci, barvu na začátku a barvu
na konci, rychlost rotace. Je to poměrně mnoho parametrů, které každá
částice musí znát, ale také je to na výsledku vidět. Na všechny pak
působí ještě gravitace. To jsou veškeré požadavky a není jich zrovínka
málo. Jak to vypadá, když je vše v chodu se můžete podívat na
následujícím videu:
Přidáme si tedy do enginu novou komponentu. Já jsem ji nazval
Particle2DManager
. Třídu opět uděláme veřejnou. Prvně
přidáme parametr pro texturu, kterou budou mít všechny částice:
private string textureName; protected Texture2D texture;
Budeme počítat s tím, že jsou částice všechny stejné. Vložte také
vnitřní třídu Particle
:
protected class Particle{ }
Dovnitř si přidáme proměnnou, která bude mít v sobě aktuální čas života částice, dále proměnnou označující čas kdy začne částice mizet a proměnnou kdy částice zmizí:
public float Time; public float FadeStart; public float FadeEnd;
Každá bude potřebovat také svoji polohu a rychlost pohybu:
public Vector2 Position; public Vector2 Velocity;
Ještě uložíme zda-li je částice aktivní:
public bool Visible;
Pro začátek bude postačovat toto. Do hlavní třídy vložíme kolekci se všemi částicemi a třídu pro generování náhodných čísel:
protected List<Particle> Particles;
Random r;
Potřebujeme ještě určit, jak často budeme vypouštět novou částici budeme-li ji mít.
public TimeSpan NextParticle;
V konstruktoru všechny parametry inicializujeme:
public Particle2DManager(){ Particles = new List<Particle>(); NextParticle = TimeSpan.FromMilliseconds(50); r = new Random(); }
Ještě postrádáme jednu proměnnou a tou bude počítadlo času, který uběhl od posledního vypuštění částice:
TimeSpan next;
Zrealizujeme si tedy metodu skrze, kterou přidáme manažeru částice, se
kterými bude pracovat. Nazval jsem ji AddParticles
a skrze ní
předáme také nastavení částic. Později uvidíte, že těch parametrů
bude dost. Pro začátek si vystačíme se startovní pozicí a směrem,
životností a taky počtem částic a jménem textury:
public void AddParticles(Vector2 startPos, Vector2 minDirection, Vector2 maxDirection, float minFadeStart, float maxFadeStart, float minFadeEnd, float maxFadeEnd, int count, string textureName)
Všechny hodnoty potřebuje v rozmezí min-max, aby je šlo náhodně vybírat. Pozici ovšem ponecháme jeden bod, je pravda že jej půjde snadno nahradit třeba nějakou oblastí, čtvercem, kruhem to už bude na použití. Nejprve pro jistotu promažeme seznam částic a nahrajeme novou texturu:
Particles.Clear(); if (this.textureName != textureName){ this.textureName = textureName; if(texture!=null)texture.Dispose(); if(Loaded)texture=Parent.Content.Load<Texture2D>(textureName); }
Texturu načítáme pouze pokud je načtená komponenta jako taková. To vše aby bylo možné zavolat metodu i pokud není komponenta načtená. Proto si ještě přidáme obvyklou Load metodu:
protected override void Load(){ if(!String.IsNullOrEmpty(textureName)) texture=Parent.Content.Load<Texture2D>(textureName); }
Načítáme texturu pochopitelně jen a pouze pokud nějakou máme zadanou. Načítání textury máme hotové. Můžeme se pustit do částic. Všechny parametry co předáváme metodou si budeme muset uložit. Nedá se svítit, protože budeme chtít generovat nové parametry částice i po jejím znovuoživení. Takže si přidáme patřičné proměnné:
public Vector2 StartLocation; public Vector2 StartMinVelocity; public Vector2 StartMaxVelocity; public float MinFadeStart; public float MaxFadeStart; public float MinFadeEnd; public float MaxFadeEnd;
A přiřadíme jim hodnoty:
StartLocation = startPos; StartMinVelocity = minDirection; StartMaxVelocity = maxDirection; MinFadeStart = minFadeStart; MaxFadeStart = maxFadeStart; MinFadeEnd = minFadeEnd; MaxFadeEnd = maxFadeEnd;
V cyklu pak vytvoříme všechny částice. Nastavíme jim viditelnost na false a přidáme je do pole:
for (int i = 0; i < count; i++){ Particle p = new Particle(); p.Visible = false; Particles.Add(p); }
To bude prozatím vše. Přesuneme se do metody Update
, kde si
napíšeme vypouštění částic. K uběhlému času přičteme čas od
posledního zavolání:
next += Parent.Engine.GameTime.ElapsedGameTime;
A v cyklu postupně projdeme všechny částice a pokud jsou viditelné, tak jim zvedneme čas a vypočteme novou pozici:
foreach(Particle p in Particles){ if (p.Visible){ //pokad je videt tak ji soupnem p.Position += p.Velocity * (float)Parent.Engine.GameTime.ElapsedGameTime.TotalSeconds; p.Time += (float)Parent.Engine.GameTime.ElapsedGameTime.TotalSeconds; } else if(next>NextParticle){ //dame na start a zviditelnime } }
Pokud ovšem není částice viditelná, zkontrolujeme čas a pokud je
větší než interval vypouštění, částici vypustíme. Prostě ji
nastavíme Visible
na true, vygenerujeme jí parametry. Od toho
mám metodu SetParticleParametres
, kde parametry generuji.
Přidejme si ji:
protected void SetParticleParametres(Particle p){ p.Position = StartLocation; p.Velocity = Vector2Mezi(StartMinVelocity, StartMaxVelocity); p.FadeStart = floatMezi(MinFadeStart, MaxFadeStart); p.FadeEnd = floatMezi(MinFadeEnd, MaxFadeEnd); p.Time = 0; }
Napsal jsem si dvě pomocné metůdky pro generování náhodného čísla z rozsahu a taky generování vektoru z rozsahu:
protected float floatMezi(float min, float max){ return (float)(min + (max - min) * r.NextDouble()); } protected Vector2 Vector2Mezi(Vector2 min, Vector2 max){ return new Vector2(floatMezi(min.X,max.X), floatMezi(min.Y,max.Y)); }
Vraťme se do metody Update
a doděláme ji:
p.Visible = true;
SetParticleParametres(p);
next = TimeSpan.Zero;
Částice můžeme nyní přidávat, updatovat ještě nám zbývá
vykreslení. Použije SpriteBatch
a to jen protože vše
obstarává prakticky za nás.
public override void Draw(){ Parent.Engine.SpriteBatch.Begin(); foreach (Particle p in Particles){ if(p.Visible)Parent.Engine.SpriteBatch.Draw(texture, new Rectangle((int)p.Position.X,(int)p.Position.Y, (int) (texture.Width),(int)(texture.Height)), null,Color.White * p.Opacity,0,new Vector2(),SpriteEffects.None,0); } Parent.Engine.SpriteBatch.End(); }
Rozhodl jsem se použít úplnou metodu. Do ní pak už jen dosadíme postupně rotaci, změnu měřítka a další šaškárny. Zastavil bych se ale u tohoto parametru:
Color.White * p.Opacity
Právě zde je vypočítávána průhlednost částice. Ovšem to ještě naše částice nezná. Průhlednost je číslo od nuly do jedné, kdy nula znamená plně průhledné a 1 neprůhledné. Do tohoto rozmezí budeme muset průhlednost vypočítat z času. Přidáme si tedy novou položku do naší částice:
public float Opacity{ get{ if (Time < FadeStart) return 1; if (Time > FadeEnd){ Visible = false; return 0; } return 1-((Time-FadeStart) / (FadeEnd - FadeStart)); } }
Průhlednost budeme měnit pouze pokud je čas v rozmezí ve kterém chceme. Zároveň zde budeme částici pomocí času deaktivovat. Výpočtem zlomku dostaneme číslo v rozmezí 0-1, ale my chceme interval přesně obrácený. Proto odčítáme jedničku. Samozřejmě můžeme opět nastavit i náhodnou počáteční průhlednost. Ale pro jednoduchost to ponecháme takto. Základní částicový systém tedy máme hotov. Ale než ho půjdeme použít, bylo by dobré si říct jak vypočítat kolik částic budeme potřebovat. Pokud chceme aby částice žila maximálně 3 vteřiny a chceme je vypouštět po 100 milisekundách, tak jich potřebujeme maximálně 30. Víc jich v jeden okamžik zobrazeno nebude. Lze to snadno vypočítat:
maxdobaživota[s]*1000/interval[ms]
Obvykle jich bude vidět současně méně, jelikož rozptyl životnosti bývá poměrně široký. Používat komponentu jistě zvládnete sami. Dalo by se doplnit spousta různých dalších doplňujících parametrů, ale na ty není v tomto článku bohužel prostor. A co si budeme povídat, bylo by to celkem dost nudné, navíc se už nyní se blížím k limitu. Jeden ale nemůžu opomenout a tou je vliv gravitace/větru. Gravitace začne částice zpomalovat a vítr je vychyluje z jejich zamýšlené dráhy. Přidejme si tedy proměnnou:
public Vector2 Gravity = new Vector2(0, -50);
Uvnitř máme vlastně zahrnuty oba dva vlivy. Gravitaci působící dolů a
nulový směr větru. Který by působil horizontálně. Tento vliv stačí na
částice aplikovat v Update
metodě stejně jako jsme dávali
rychlost:
p.Velocity -= Gravity*(float)Parent.Engine.GameTime.ElapsedGameTime.TotalSeconds;
A to by bylo pro tento díl všechno. Zbylé parametry naleznete v archivu pod článkem. Budu se těšit na komentáře, prostě jako obvykle a příště nashledanou zas u enginu.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkamiStaženo 702x (563.59 kB)