IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
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í.

Lekce 10 - 3D bludiště v XNA - Kolize potřetí a snad naposledy

Vítejte po dvacáté. V předchozí lekci, 3D bludiště v XNA - Kolize podruhé, jsme dokončili náš systém pro 3D kolize.

Původně jsem doufal, že si v tomto díle projdeme bludiště již konečně jako hru s kolizemi o zdi, počítáním času, který byl potřeba pro cestu bludištěm. Zkrátka že bude vše hotovo a jen si v dalším díle přidáme menu a k tomu změnu levelů a hra bude prakticky hotová. Bohužel tomu tak není, máme sice hotové kolize se zdmi, ale chybí nám jeden malý detail. Projděme si ještě jednou jaké máme nároky na kolize. Chceme, aby nešlo procházet zdí a zároveň chceme, aby se hra ukončila když se dostaneme do cíle. První požadavek splňujeme, ale ten druhý ne. Máme opět několik možností. Můžeme srabsky vytvořit krabici na dané místo a pokaždé kontrolovat kolizi mimo napsaný systém. A nebo můžeme upravit stávající systém, a tady rozumíme úpravou přepsat skoro vše, a tím dostat opět univerzální řešení tohoto problému. Dostaneme pak automatický systém kde se kolize budou samy na sebe upozorňovat voláním událostí. To je myslím dobrý cíl, pokusím se to nacpat do jednoho dílu, abychom se pohli vpřed, takže jdeme na to.

Podíváme-li se do kolizního manažeru, zjistíme, že máme v poli uložené krabice přímo. Aby bylo možné detekovat zda-li kolidujeme nyní poprvé a nebo jestli už i minule jsme kolidovali, potřebujeme stejně jako u klávesnice a myši uchovat předchozí stav. První změnu, kterou musíme provést, je přidat indikaci tohoto stavu. Nejlepší bude obalit krabici do nového objektu. Přidáme si tedy novou třídu CollisionSkin. Nebo třeba si ji pojmenujte jinak. Učiníme si ji veřejnou, upravíme jmenný prostor jako obyčejně. Přidáme opět dvě proměnné pro krabice, stejně jako jsme měli u Modelu. Prakticky vše, co budeme dělat, bude přesouvat věci sem :-)

public BoundingBox ZakladniKrabice;
public BoundingBox TransformedKrabice;

Dále přidáme odkaz na kolizní manažer, stejně jako jsme dělali jinde, je to náš běžný zvyk:

private CollisionManager fManager;

public CollisionManager Manager{
  get{
    return fManager;
  }
  set{
    fManager = value;
  }
}

Přidáme také konstruktor, jako parametr předáme krabici a tu si uložíme:

public CollisionSkin(BoundingBox box){
  ZakladniKrabice = box;
  TransformedKrabice = box;
}

Dále přeneseme do této třídy metodu TransformBox ze třídy s kolizním modelem, bude nyní potřeba zde. Změnu měřítka, pozice a také rotace předáme v parametru, takže telegraficky:

public void TransformBox(Vector3 meritko, Vector3 pozice, Matrix rotace){
  Matrix transform = Matrix.CreateScale(meritko) * Matrix.CreateTranslation(pozice);
  BoundingBox transformed = ZakladniKrabice;
  transformed.Min = Vector3.Transform(transformed.Min, transform);
  transformed.Max = Vector3.Transform(transformed.Max, transform);

  Vector3[] body = new Vector3[8];
  transformed.GetCorners(body);
  for (int i = 0; i < body.Length; i++){
    body[i] = Vector3.Transform(body[i], rotace);
  }
  transformed = BoundingBox.CreateFromPoints(body);

  TransformedKrabice.Min = transformed.Min;
  TransformedKrabice.Max = transformed.Max;
}

Ve třídě pro model také umažeme všechna volání této metody. Dodáme také metodu Draw, která nám jak jinak než vykreslí krabici:

public void Draw(){
  BoundingRenderer.Render(TransformedKrabice, Manager.Parent.Kamera.View, Manager.Parent.Kamera.Projection, Color.Black);
}

Přidáme též metodu pro kontrolu kolizí, zde si ji pojmenujeme CheckCollision a bude vracet true a nebo false. Prakticky jen vracíme výsledek metody Intersets:

public bool CheckCollision(BoundingSphere koule){
  if (koule.Intersects(TransformedKrabice)){
    return true;
  }
  return false;
}

Až doteď jsme si jen přesunuli do této třídy vše co jsme měli minule decentralizované. Novinky příjdou až nyní. Musíme přidat indikaci zda-li jsme již nekolidovali minule, takže:

public bool LastCollision{
  get;
  protected set;
}

Je pravdou, že i zde by se hodil nějaký objekt, který by ukládal jaký útvar přesně kolidoval, ale v našem případě máme pouze jednu kouli, která může kolidovat a proto si vystačíme s tímto zjednodušením. Kolize budeme vyřizovat ve virtuálních a chráněných metodách:

protected virtual void OnCollision(BoundingSphere sphere){

}

protected virtual void OnUnCollision(BoundingSphere sphere){

}

a také skrze volání událostí. Předpokládám že události budou využívány častěji, ale i tak je dobré ty metody mít, umožňuje to snadnou specializaci potomků. Kdo se s událostmi ještě nesetkal tak si může přečíst tento článek. Nebudeme ale používat vestavěný delegát ale delegát náš vlastní:

public delegate void CollisionBegin(BoundingSphere koule);
public delegate void CollisionEnd(BoundingSphere koule);

Do třídy přidáme příslušné události:

public event CollisionBegin Collided;
public event CollisionEnd UnCollided;

První se zavolá tehdy, když je detekována poprvé kolize. Druhá, když se těleso pěkně řečeno odkoliduje. Začneme prvním případem. Ten nastává pokud je hodnota proměnné LastCollision false a nyní ke kolizi dochází. Přidáme tedy do metody CheckCollision tento stav:

public bool CheckCollision(BoundingSphere koule){
  if (koule.Intersects(TransformedKrabice)){
    if (!LastCollision){
      LastCollision = true;
      OnCollision(koule);
      if (Collided != null) Collided(koule);
    }
    return true;
  }
  LastCollision = false;
  return false;
}

Pokud jsme minule nekolidovali, nastavíme proměnnou na true zavoláme místní metodu a pokud je nějaká metoda v události zaregistrována, tak ji také zavoláme. Stejně vyřešíme druhou možnost, která je přesně opačný případ, takže celá metoda vypadá následovně:

public bool CheckCollision(BoundingSphere koule){
  if (koule.Intersects(TransformedKrabice)){
    if (!LastCollision){
      LastCollision = true;
      OnCollision(koule);
      if (Collided != null) Collided(koule);
    }
    return true;
  }
  if (LastCollision){
    OnUnCollision(koule);
    if (UnCollided != null) UnCollided(koule);
  }
  LastCollision = false;
  return false;
}

Hotovo, nyní už jen třídu naimplementujeme do hotového systému. První kupa změn bude potřeba udělat v kolizním manažeru. Prakticky všude kde pracujeme s krabiceme budeme pracovat s kůžemi. Takže změníme kolekci s krabicemi na kolekci s kůžemi:

protected List<CollisionSkin> Boxes;

Jméno asi ponecháme. Upravíme též v konstruktoru a metody pro přidávání a odebírání taktéž:

public void AddBox(CollisionSkin skin){
  if (!Boxes.Contains(skin)){
    Boxes.Add(skin);
    skin.Manager = this;
  }
}

public void RemoveBox(CollisionSkin skin){
  Boxes.Remove(skin);
  skin.Manager = null;
}

Vykreslení přenecháme na kůži:

public void Draw(Matrix View, Matrix Projection){
  if (DebugDraw){
    foreach (CollisionSkin skin in Boxes){
      skin.Draw();
    }
  }
}

Metoda pro kolizi doznává největší změny. Přenecháme rozhodování o kolizi právě naší kůži.

List<CollisionSkin> colliding = new List<CollisionSkin>();
foreach (CollisionSkin skin in Boxes){
  if(skin.CheckCollision(sphere))colliding.Add(skin);
}

Podíváme-li se nyní na chyby, které nám VisualStudio vypisuje, zjistíme, že jsou již pouze v modelu. Otevřeme si jej. První věcí, co je potřeba umazat, jsou dvě proměnné pro krabice. Pryč s nimi, jsou již obsaženy v kůži. Moc jsme si nepomohli, počet chyb nám narostl :-) Přidáme privátní proměnnou pro kůži:

private CollisionSkin fCollisionSkin;

Getter a setter, který sloužil pro transformovanou krabici, nahradíme getterem a setterem pro kůži:

public CollisionSkin CollisionSkin{
  get{
    return fCollisionSkin;
  }
  set{
    if (fCollisionSkin != value){
      if (Parent is CollidableGameScreen){
        CollidableGameScreen okno = Parent as CollidableGameScreen;
        if(fCollisionSkin!=null)okno.CollisionManager.RemoveBox(fCollisionSkin);
        okno.CollisionManager.AddBox(value);
        fCollisionSkin = value;
      }
    }
  }
}

V metodě Load kůži vytvoříme:

BoundingBox box=Utility.VypoctiBoundingBox(Model, transformace);
CollisionSkin = new CollisionSkin(box);

Do metody Update kůži transformujeme:

if (CollisionSkin != null) CollisionSkin.TransformBox(Meritko, Pozice, Rotace);

A konečně v metodách OnAdded a OnRemoved namísto krabice přidáme kůži. Pokud nyní hru spustíme, tak dostaneme stejný výsledek jako minule. Aby byl vidět alespoň nějaký rozdíl, tak při vykreslení kůže změníme barvu, tehdy pokud je s kůží kolidováno. Poslední parametr nahradíme ternárním operátorem:

LastCollision ? Color.Red:Color.Black

A to by bylo pro dnešek všechno, problém se nám povedlo zdárně překonat.

V příštím díle, 3D bludiště v XNA - Časujeme, si přijdeme na další problém :-) Ten je ale řešitelný velmi, velmi snadno. A to vše jen díky tomu, co jsme si v tomto díle vytrpěli. Budiž to takové memento toho, že je i přidání pár nových a velmi obyčejných požadavků může stát poměrně vysoké úsilí. Opět bych byl rád za komentáře a tak podobně, ale asi se nedočkám.


 

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 179x (1.79 MB)
Aplikace je včetně zdrojových kódů v jazyce C# XNA

 

Předchozí článek
3D bludiště v XNA - Kolize podruhé
Všechny články v sekci
3D bludiště v XNA
Přeskočit článek
(nedoporučujeme)
3D bludiště v XNA - Časujeme
Č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