Částicový systém v XNA

C# .NET XNA game studio 3D grafika Částicový systém v XNA

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ženo 255x (563.59 kB)

 

  Aktivity (1)

Článek pro vás napsal vodacek
Avatar
Vodáček dělá že umí C#, naplno se již pět let angažuje v projektu ŽvB. Nyní studuje na FEI Upa informatiku, ikdyž si připadá spíš na ekonomice. Není mu také cizí PHP a SQL. Naopak cizí mu je Java a Python.

Jak se ti líbí článek?
Celkem (3 hlasů) :
55555


 



 

 

Komentáře

Avatar
Milan Lhoták:

Ahoj, nemá náhodou u nahrávání být Parent.Engine­.Content.Load­.. ?

 
Odpovědět 8. března 21:40
Avatar
vodacek
Redaktor
Avatar
Odpovídá na Milan Lhoták
vodacek:

záleží jakou verzi enginu používáš, starý má content pouze ve třídě Engine, nová opravená verze má vlastní content manažer pro každé herní okno

 
Odpovědět 8. března 23:39
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 2 zpráv z 2.