IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

17. diel - 3D bludisko v XNA - Instancujeme kolízne objekty

Vitajte po dvadsiatej siedmej. Naposledy som sľuboval oddychové tému. Ale keďže sa mi zadarilo pomerne veľa postúpiť s instancováním, ktoré sme preberali v dielach 25 a 26, tak ešte jeden diel sa na ne pozrieme. Dostal som otázku na čo je to ako dobré a či nemám nejaké čísla. Popravde sme nevedel čo odpovedať, ale môj včerajší výskum mi pár čísel priniesol. Takže sem s nimi:

Máme testovacie bludisko 30x30 polí, teda 900 objektov, ktoré musíme zakaždým vykresľovať. So základným naivným kódom som dostal nelichotivých 40 FPS. Čo v preklade znamená že dochádzalo k vykresľovanie 40x za sekundu. Hra sa už viditeľne hlavne pri otáčaní kamery kosila. Stačilo instanciovat podlahy a už som bol na 160-tich FPS. Keď som pridal aj steny s použitím kódu, ktorý vytvoríme teraz (zatiaľ to nedá kvôli kolíziám) bol som na 240-tich a keď som zainstancoval aj diery (kto hral bludisko v ostrej verzii, tak pozná) tak som zo svojho 3 roky starého počítača vytiahol 360 FPS. Teda asi 9x rýchlejšie než pôvodný kód a to myslím stojí za trochu námahy.

Instancované kolidovatelné modely

Ako som už naznačil vyššie, budeme potrebovať instanciovat aj múry, pritom ale chceme zachovať funkčné kolízie. Budeme na to potrebovať novú triedu, ja sme ju nazval CollidableInstan­cedModel3D. Ale poznáte moji predstavivosť. Urobíme ju verejnú. Dediť budeme od triedy InstancedModel3D a opäť s použitím genericity:

public class CollidableInstancedModel3D<T>: InstancedModel3D<T> where T: struct, IvertexType

Čo vlastne do triedy potrebujeme prepašovať? No, asi nejakú kolízne kožu. Najlepšie riešenie (podľa mňa) je vytvoriť z brusu novú kolíznu kožu, v ktorej bude zoznam krabíc. Pridajme si novú triedu MultipleBoxCollisionSkin, dedíme od triedy CollisionSkin. Vnútri pridáme List pre krabice:

protected List<BoundingBox> Boxes;

V konstruktoru ho vytvoríme:

public MultipleBoxCollisionSkin(){
  Boxes = new List<BoundingBox>();
}

Potreba budú tiež dve metódy. Jedna pri pridávaní krabice a druhá pre odoberanie krabice na patričnom indexu. Prečo zrovna takto uvidíte za chvíľočku:

public void AddBox(BoundingBox b){
  Boxes.Add(b);
}

public void RemoveBox(int index){
  Boxes.RemoveAt(index);
}

Potreba bude tiež prepísať metódu Intersects, jednoducho prejdeme všetky krabice v liste a skontrolujeme u nich, či nedochádza ku kolízii:

public override bool Intersects(BoundingSphere sp){
  foreach (BoundingBox b in Boxes){
    if (b.Intersects(sp)) return true;
  }
  return false;
}

Ak kolidujú hoci s jednou, tak ku kolízii dochádza a cyklus ukončujeme. Rovnako naložíme s metódou Draw:

public override void Draw(){
  foreach (BoundingBox b in Boxes){
    BoundingRenderer.Render(b, Manager.Parent.Kamera.View, Manager.Parent.Kamera.Projection, LastCollision ? Color.Red : Color.Black);
  }
}

Jediným problémom je metóda Transform. Ako celú túto sústavu presunúť na iné miesto si zatiaľ nie som istý, preto ju síce prepíšeme, ale ponecháme ju prázdnu. Prípadne si sem môžete vložiť zavolanie výnimky:

public override void Transform(Vector3 meritko, Vector3 pozice, Matrix rotace){
}

Kolízne kožu máme teda hotovú. Vráťme sa do triedy, ktorá je naším hlavným záujmom. Pridáme si konštruktor a v ňom si našu kožu vytvoríme:

protected MultipleBoxCollisionSkin Skin;

public CollidableInstancedModel3D(string model, int max, string effect) : base(model, max, effect){
  Skin = new MultipleBoxCollisionSkin();
}

Budeme potrebovať tiež kolízne krabicu pre jeden model, práve tu budeme pridávať do novovytvorenej kože:

protected BoundingBox Box;

A v metóde Load ju z modelu vytiahneme:

protected override void Load(){
  base.Load();
  Matrix[] transformace = new Matrix[Model.Bones.Count];
  Model.CopyAbsoluteBoneTransformsTo(transformace);
  Box = Utility.VypoctiBoundingBox(Model, transformace);
}

Máme vytvorenú kožu, takže už nám stačí ju len zaregistrovať do kolízneho manažéra, urobíme to rovnako ako s modelom:

public override void OnAdded(){
  base.OnAdded();
  if (Parent is CollidableGameScreen){
    CollidableGameScreen gs = Parent as CollidableGameScreen;
    gs.CollisionManager.AddBox(Skin);
  }
}

A rovnako kód pre odoberanie:

public override void OnRemoved(GameScreen okno){
  base.OnRemoved(okno);
  if (okno is CollidableGameScreen){
    CollidableGameScreen gs = okno as CollidableGameScreen;
    gs.CollisionManager.RemoveBox(Skin);
  }
}

Prepíšeme tiež metódu pre pridávanie instanciovaného objektu. Zakaždým, keď objekt pridáme, tak s ním pridáme i kožu. Je tu len zopár problémov, ktoré musíme prekonať. Najprv musíme krabicu transformovať na požadované miesto. Ako je známe túto informáciu obsahuje vkladaný vertex, ale ako to z neho dostať? Nad jednotlivými možnosťami som premýšľal pomerne dlho. Nakoniec najlepším riešením sa ukázalo pridať rozhranie, kde predpíšeme metódu pre vrátenie matice World. Nazval som ho IInstanceVertexType a vyzerá takto:

public interface IInstanceVertexType : IVertexType{
  Matrix GetWorld();
}

Nezabudnime upraviť požiadavku na typ u genericity u oboch tried:

public class CollidableInstancedModel3D<T>: InstancedModel3D<T> where T: struct, IinstanceVertexType

a

public class InstancedModel3D<T> : Component where T : struct, IinstanceVertexType

V štruktúre s našim typom vertexu len prehodíme rozhrania a metódu naimplementujeme:

public Matrix IInstanceVertexType.GetWorld(){
  return World;
}

Ešte použijeme metódu pre transformáciu škatule s pomocou matice, umiestnime ju do triedy Utility. Funguje rovnako ako metóda predošlá, vyextrahujeme všetky body krabice a aplikujeme na ne transformáciu, potom z nich zložíme novú krabicu. Len tu zdôrazním, že nechceme zmeniť nič v pôvodnej krabici:

public static BoundingBox Transform(BoundingBox box, Matrix transform){
  BoundingBox bb;

  Vector3[] body = new Vector3[8];
  box.GetCorners(body);
  for (int i = 0; i < body.Length; i++){
    body[i] = Vector3.Transform(body[i], transform);
  }

  bb = BoundingBox.CreateFromPoints(body);
  return bb;
}

Konečne máme všetko pripravené a môžeme kožu pridať:

public override void AddPrimitive(T obj){
  base.AddPrimitive(obj);
  Skin.AddBox(Utility.Transform(Box,obj.GetWorld()));
}

Odoberanie kožou je o niečo málo zložitejšie, musíme nájsť index, na ktorom sa objekt nachádza a na rovnakom indexe musíme odobrať iz kožou. Preto sme v koži implementovali odoberanie cez index:

public override void RemovePrimitive(T obj){
  int id = PrimitivesList.IndexOf(obj);
  base.RemovePrimitive(obj);
  Skin.RemoveBox(id);
}

Tým by sa dalo povedať, že je hotovo. Avšak nie je to pravda. Ak sa budeme snažiť pridať kópie a nebudeme mať komponent načítanú, tak nám program spadne. Nebudeme totiž mať načítaný model a teda ani vytvorenú základnej krabici. Môžeme to ľahko poupraviť. Do pridávanie kópií vložíme podmienku:

if(!Loaded)Skin.AddBox(Utility.Transform(Box,obj.GetWorld()));

A do metódy Load pridáme foreach cyklus:

foreach (T obj in PrimitivesList){
  Skin.AddBox(Utility.Transform(Box, obj.GetWorld()));
}

Teraz zostáva už len a len všetko sprevádzkovať. Otvorte si súbor s mapou a tu rovnako ako v prípade podláh si vytvoríme premennú:

CollidableInstancedModel3D<InstanceDataVertex> zdi = new CollidableInstancedModel3D<InstanceDataVertex>("zed", 50, "instancedeffect");

A do switche do vetvy, kde sme pridávali múr, pridáme rovnako ako u podláh novú kópiu:

zdi.AddPrimitive(new InstanceDataVertex(Utility.CreateWorld( new Vector3(i * 20 + 10, 0, j * 20 + 10), Matrix.Identity, new Vector3(1.34f)), Color.Green));

Nesmieme zabudnúť pridať komponent do enginu a steny zobraziť:

Parent.AddComponent(zdi);
zdi.Apply();

Ešte zakomentovat pridanie obyčajné múru a máme hotovo. Gratulujem, práve sme do enginu pridali instanciované kolízne objekty. A ani to moc dúfam nebolelo. Ak teraz hru spustíte tak ... sa moc ďaleko nedostanete. Veľkosť bufferu je menší, než počet pridávaných múrov. Môžeme tomu čeliť dvoma spôsobmi. Buď nedovoliť pridať viac ako je limit a alebo buffer nafúknuť. Ja som zvolil nafúknutie. Predovšetkým pridáme do metódy Apply podmienku na nulový počet prvkov:

if (PrimitivesList.Count == 0) return;

Na tú sme zabudli posledne. Do podmienky pri prvom vytvorení bufferu nastavíme veľkosť na maximum. Bude to vyzerať nejako takto:

if (Primitives == null){
  MaxCount = Math.Max(MaxCount, PrimitivesList.Count);
  Primitives = new DynamicVertexBuffer(Parent.Engine.GraphicsDevice, PrimitivesList[0].VertexDeclaration, MaxCount, BufferUsage.None);
  binding[1] = new VertexBufferBinding(Primitives, 0, 1);
}

A pridáme tiež podmienku na pretečenie bufferu, ak ho máme už vytvorený:

if (MaxCount < PrimitivesList.Count){
  MaxCount = PrimitivesList.Count;
  Primitives = new DynamicVertexBuffer(Parent.Engine.GraphicsDevice, PrimitivesList[0].VertexDeclaration, MaxCount, BufferUsage.None);
  binding[1] = new VertexBufferBinding(Primitives, 0, 1);
}

Teraz už by všetko malo fungovať tak ako má. Gratulujem. Máme všetko čo sa dalo nainstanciované. Nabudúce si už dáme niečo oddychového. S instanciováním sme dúfam skončili. Opäť budem čakať na otázky, nápady, no proste na komentáre.


 

Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 307x (1.78 MB)
Aplikácia je vrátane zdrojových kódov v jazyku C#

 

Predchádzajúci článok
3D bludisko v XNA - Kolízia štvrtýkrát a naozaj nie naposledy
Všetky články v sekcii
3D bludisko v XNA
Článok pre vás napísal vodacek
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
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.
Aktivity