Pouze tento týden sleva až 80 % na e-learning týkající se C# .NET. Zároveň využij akce až 50 % zdarma při nákupu e-learningu. Více informací:
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.
Slevovy týden 3/50

Lekce 17 - 3D bludiště v XNA - Instancujeme kolizní objekty

Vítejte po dvacáté sedmé. Posledně, 3D bludiště v XNA - Kolize počtvrté a opravdu ne naposled, jsem sliboval oddychové téma. Ale jelikož se mi zadařilo poměrně hodně postoupit s instancováním, které jsme probírali v dílech 25 a 26, tak ještě jeden díl se na ně podíváme.

Obdržel jsem otázku k čemu je to jako dobré a jestli nemám nějaká čísla. Popravdě jsme nevěděl co odpovědět, ale můj včerejší výzkum mi pár čísel přinesl. Takže sem s nimi:

Máme testovací bludiště 30x30 polí, tedy 900 objektů, které musíme pokaždé vykreslovat. Se základním naivním kódem jsem obdržel nelichotivých 40 FPS. Což v překladu znamená že docházelo k vykreslování 40x za vteřinu. Hra se již viditelně hlavně při otáčení kamery sekala. Stačilo instanciovat podlahy a už jsem byl na 160-ti FPS. Když jsem přidal i zdi s použitím kódu, který vytvoříme nyní (zatím to nelze kvůli kolizím) byl jsem na 240-ti a když jsem zainstancoval i díry (kdo hrál bludiště v ostré verzi, tak zná) tak jsem ze svého 3 roky starého počítače vytáhl 360 FPS. Tedy asi 9x rychleji než původní kód a to myslím stojí za trochu námahy.

Instancované kolidovatelné modely

Jak jsem již naznačil výše, budeme potřebovat instanciovat i zdi, přitom ale chceme zachovat funkční kolize. Budeme na to potřebovat novou třídu, já jsme ji nazval CollidableInstan­cedModel3D. Ale znáte moji představivost. Učiníme ji veřejnou. Dědit budeme od třídy InstancedModel3D a opět s použitím genericity:

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

Co vlastně do třídy potřebujeme propašovat? No, asi nějakou kolizní kůži. Nejlepší řešení (podle mě) je vytvořit z brusu novou kolizní kůži, ve které bude seznam krabic. Přidejme si novou třídu MultipleBoxCollisionSkin, dědíme od třídy CollisionSkin. Uvnitř přidáme List pro krabice:

protected List<BoundingBox> Boxes;

V konstruktoru jej vytvoříme:

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

Potřeba budou také dvě metody. Jedna při přidávání krabice a druhá pro odebírání krabice na patřičném indexu. Proč zrovna takto uvidíte za chviličku:

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

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

Potřeba bude také přepsat metodu Intersects, jednoduše projedeme všechny krabice v listu a zkontrolujeme u nich, zda-li nedochází ke kolizi:

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

Pokud kolidujeme byť s jednou, tak ke kolizi dochází a cyklus ukončujeme. Stejně naložíme s metodou 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émem je metoda Transform. Jak celou tuto soustavu přesunout na jiné místo si zatím nejsem jistý, proto ji sice přepíšeme, ale ponecháme ji prázdnou. Případně si sem můžete vložit zavolání výjimky:

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

Kolizní kůži máme tedy hotovou. Vraťme se do třídy, která je naším hlavním zájmem. Přidáme si konstruktor a v něm si naši kůži vytvoříme:

protected MultipleBoxCollisionSkin Skin;

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

Budeme potřebovat také kolizní krabici pro jeden model, právě tu budeme přidávat do nově vytvořené kůže:

protected BoundingBox Box;

A v metodě Load ji z modelu vytáhneme:

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

Máme vytvořenou kůži, takže už nám stačí ji jen zaregistrovat do kolizního manažeru, uděláme to stejně jako s modelem:

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

A stejně kód pro odebírání:

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

Přepíšeme také metodu pro přidávání instanciovaného objektu. Pokaždé, když objekt přidáme, tak s ním přidáme i kůži. Je tu jen pár problémů, které musíme překonat. Nejprve musíme krabici transformovat na požadované místo. Jak známo tuto informaci obsahuje vkládaný vertex, ale jak to z něj dostat? Nad jednotlivými možnostmi jsem přemýšlel poměrně dlouho. Nakonec nejlepším řešením se ukázalo přidat rozhraní, kde předepíšeme metodu pro vrácení matice World. Nazval jsem ho IInstanceVertexType a vypadá takto:

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

Nezapomeňme upravit požadavek na typ u genericity u obou tříd:

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

a

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

Ve struktuře s naším typem vertexu jen přehodíme rozhraní a metodu naimplementujeme:

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

Ještě upotřebíme metodu pro transformaci krabice s pomocí matice, umístíme ji do třídy Utility. Funguje stejně jako metoda předešlá, vyextrahujeme všechny body krabice a aplikujeme na ně transformaci, potom z nich složíme novou krabici. Jen zde zdůrazním, že nechceme změnit nic v původní 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čně máme vše připraveno a můžeme kůži přidat:

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

Odebírání kůží je o něco málo složitější, musíme najít index, na kterém se objekt nalézá a na stejném indexu musíme odebrat i z kůží. Proto jsme v kůži implementovali odebírání přes index:

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

Tím by se dalo říct, že je hotovo. Ovšem není to pravda. Pokud se budeme snažit přidat kopie a nebudeme mít komponentu načtenou, tak nám program spadne. Nebudeme totiž mít načtený model a tedy ani vytvořenou základní krabici. Můžeme to lehce poupravit. Do přidávání kopií vložíme podmínku:

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

A do metody Load přidáme foreach cyklus:

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

Nyní zbývá už jen a pouze vše zprovoznit. Otevřete si soubor s mapou a zde stejně jako v případě podlah si vytvoříme proměnnou:

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

A do switche do větve, kde jsme přidávali zeď, přidáme stejně jako u podlah novou kopii:

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

Nesmíme zapomenout přidat komponentu do enginu a zdi zobrazit:

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

Ještě zakomentovat přidání obyčejné zdi a máme hotovo. Gratuluji, právě jsme do enginu přidali instanciované kolizní objekty. A ani to moc doufám nebolelo. Pokud nyní hru spustíte tak... se moc daleko nedostanete. Velikost bufferu je menší, než počet přidávaných zdí. Můžeme tomu čelit dvěma způsoby. Buď nedovolit přidat více než je limit a nebo buffer nafouknout. Já jsem zvolil nafouknutí. Předně přidáme do metody Apply podmínku na nulový počet prvků:

if (PrimitivesList.Count == 0) return;

Na tu jsme zapomněli posledně. Do podmínky při prvním vytvoření bufferu nastavíme velikost na maximum. Bude to vypadat nějak takhle:

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 přidáme též podmínku na přetečení bufferu, pokud jej máme již vytvořený:

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);
}

Nyní už by vše mělo fungovat tak jak má. Gratuluji. Máme vše co se dalo nainstanciované. Příště si už dáme něco oddychového. S instanciováním jsme doufám skončili. Opět budu čekat na otázky, nápady, no prostě na komentáře.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 304x (1.78 MB)
Aplikace je včetně zdrojových kódů v jazyce C# XNA

 

Předchozí článek
3D bludiště v XNA - Kolize počtvrté a opravdu ne naposled
Všechny články v sekci
3D bludiště v XNA
Článek pro vás napsal vodacek
Avatar
Uživatelské hodnocení:
1 hlasů
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