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 CollidableInstancedModel3D. 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#