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 16 - 3D bludiště v XNA - Kolize počtvrté a opravdu ne naposled

Vítejte po dvacáté šesté. V minulé lekci, 3D bludiště v XNA - Hardwarové instancování podruhé, jsme dokončili instancování modelů.

Už bylo docela na čase se podívat konečně na kolize a provést definitivní řešení. I když uznávám, že na něj bude ještě nutné mnohokrát sáhnout, ale prozatím se s ním spokojíme. Uděláme si taky několik úprav kolizní kůže, aby bylo v budoucnu možné mít více druhů. Tím tak dotáhneme kolizní systém do přijatelnějšího stavu, než je nyní a bludiště se posune o velký krok kupředu. Pojďme tedy na to.

Třída CollisionSkin nám představuje kolizní kůži, prozatím máme pouze jen a jenom jednu. Nemusím snad říkat, že to není moc šikovné. Co když bude potřeba kůže, která bude obsahovat kouli a nebo několik koulí. Proto uděláme z této třídy abstraktní třídu, od které budeme dědit:

public abstract class CollisionSkin

Ve třídě musí zůstat pouze to, co mají všechny kůže společné. Bude tedy potřeba provést stěhování všechno co tam nepatří. Přidáme si tedy novou třídu SimpleBoxCollisionSkin, která nám bude představovat kůže s jednou krabicí. Opět jako vždy třídu učiníme veřejnou, dědit budeme od třídy CollisionSkin. Přesuneme do ní proměnné

public BoundingBox ZakladniKrabice;
public BoundingBox TransformedKrabice;

Upravíme konstruktor aby přijímal krabici:

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

V abstraktní třídě s kůží to naopak odebereme a uděláme konstruktor bezparametrický. Zaměříme se na metodu Draw, uděláme ji virtuální a její obsah překopírujeme do kůže s krabicí:

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

Další metodu, kterou je potřeba vymanit, je metoda TransformBox. Tu přejmenujeme na pouhé Transform, uděláme ji virtuální a její obsah přendáme do nové třídy:

public override void Transform(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;
}

Poslední místečko, kde nám svítí chyba, je v metodě CheckCollision. Jen pro připomenutí, tato metoda nám volá události spojené s kolizí. Ještě než se zbavíme chyby, provedeme několik menších opatření, které by se mohly hodit v budoucnosti. Přidáme si dvě metody:

protected void InvokeCollided(BoundingSphere sp){
  OnCollision(sp);
  if (Collided != null) Collided(sp);
}

protected void InvokeUnCollided(BoundingSphere sp){
  OnUnCollision(sp);
  if (UnCollided != null) UnCollided(sp);
}

Obě metody nedělají nic jiného, než volají příslušné události. V metodě CheckCollision je pak zavoláme, prozatím bude vypadat takto:

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

Už nám zbývá dořešit pouze jednu věc. Jak řešit kolize. Budeme muset přidat metodu, kterou přepíšeme v nové třídě. Do původní třídy tedy přidáme:

public virtual bool Intersects(BoundingSphere sp){
  return false;
}

A do nové:

public override bool Intersects(BoundingSphere sp){
  return TransformedKrabice.Intersects(sp);
}

A pak jen v metodě CheckCollision nahradíme:

koule.Intersects(TransformedKrabice)

za

Intersects(koule)

Hotovo. Poslední věcí, kterou potřebujeme změnit, je zaměnit ve třídě CollidableModel3D starou třídu za novou takže tedy pouze jen:

CollisionSkin = new SimpleBoxCollisionSkin(box);

A pak tu ještě máme dva drobné problémky u startovní a cílové podlahy, ale ty hravě vyřešíme přetypováním:

((SimpleBoxCollisionSkin)(this.CollisionSkin)).ZakladniKrabice.Max.Y = 15;

Výborně! Pokud nyní hru pustíte, tak by mělo vše optimálně fungovat jako předtím. Úprava byla sice jen systémová, ale opět je pro budoucnost. Naplánováno máme ještě další problém a tím je narážení do zdí. Jistě jste si toho všimli. Řešení jsem hledal docela dlouho, ale přitom je to velmi snadné. Jak to už tak bývá. Otevřeme si třídu CollisionManager. Najděte si metodu Collide. Veškerá kouzla budeme dělat v ní. Případy, kdy nedochází ke kolizi fungují jak mají, s těmi tedy nic dělat nemusíme. Ovšem když ke kolizi dojde, může se stát, že pohyb po jedné ose je možný, ale my jej vůbec neprovedeme. Podívejme se na následující obrázek

Kolize - 3D bludiště v XNA

Máme na něm oranžovou kolizní kouli s modrým vektorem pohybu. Červeně je naznačeno místo, kde dochází ke kolizi. V současném systému se koule nikam nepohne. Zůstane prostě stát na místě. Jak to řešit? Odpověď je snadná. Každý vektor lze rozdělit na jednotlivé složky. Ve dvourozměrném prostoru na dvě, na obrázku jsou naznačeny zeleně a fialově. Pokusíme se s koulí pohnout po obou osách samostatně. Pokud i tak dojde ke kolizi, tak daným směrem nepohneme a zkusíme druhou. Pokud se ani s tou nezadaří, tak jsme někde v rohu a hýbat se nebudeme vůbec.

Snadné, pusťme se do toho. Prvně nastavíme střed testovací koule na výchozí polohu a poté nastavíme osu X na novou hodnotu:

sphere.Center = old;
sphere.Center.X = nova.X;

A potom novou hodnotu otestujeme. Nemusíme však testovat úplně se všemi kůžemi, postačí, když testujeme s těmi, se kterými jsme kolidovali při úplném testu a ty máme uloženy v listu. Je to do jisté míry velké zjednodušení a ne vždy bude platné, ale jelikož pohybové vektory jsou poměrně malé vzhledem k velikosti kolizních krabic, tak by to nemělo příliš vadit. Pokud dojde s kolizí byť pouze s jednou kůží, tak cyklus ukončíme a nastavíme starou hodnotu souřadnice:

foreach (CollisionSkin skin in colliding){
  if (skin.CheckCollision(sphere) && skin.Solid){
    // pokad bude kolidovat s jednou tak koncime
    sphere.Center.X = old.X;
    break;
  }
}

Stejně naložíme i s druhou osou. Nutno upozornit, že se jedná o osu Z, Y nám míří nahoru:

sphere.Center.Z = nova.Z;
foreach (CollisionSkin skin in colliding){
  if (skin.CheckCollision(sphere) && skin.Solid){
    // pokad bude kolidovat s jednou tak koncime
    sphere.Center.Z = old.Z;
    break;
  }
}

A na závěr vrátíme místo kde je nyní koule:

return sphere.Center;

A to je kupodivu všechno. Právě jsme odstranili obří slabinu celé hry a vlastně i enginu jako takového. A jelikož zbývá ještě hodně znaků a já jsem provokatér tak tu máme ještě jedno takové vylepšení. Zůstaneme v kolizním manažeru a vlastně taky v metodě Collide. Úplně nahoře projíždíme seznam všech, ale úplně všech kůží. Co když ale budeme chtít, aby se při detekování kolize kůže odebrala. Kupříkladu stejně jako to dělají hvězdičky v hotové hře. Pokud se o to pokusíme nyní, tak nám hra spadne. Proč? Protože během procházení listu změníme jeho obsah. Proto budeme muset pokaždé kůže nakopírovat do pomocného listu a projíždět je pomocí něj. Ale aby se nevytvářel seznam pokaždé nový, což je docela nákladná operace, tak vytvoříme proměnnou ve třídě a před naplněním ji jen vyčistíme:

private List<CollisionSkin> updating;

V konstruktoru ji vytvoříme:

updating = new List<CollisionSkin>();

A pak následně v metodě Collide ji vyčistíme a následně naplníme novými kůžemi:

updating.Clear();
updating.AddRange(Boxes);

A ve foreach cyklu nahradíme kolekci. A máme hotovo. Více toho prozatím s kolizemi dělat nebudeme. Vyléčili jsme je z dětské nemoci, ale ještě nejsou úplně optimální.

Na co se můžete těšit příště? V příští lekci, 3D bludiště v XNA - Instancujeme kolizní objekty, si přidáme instancování i pro objekty, se kterými budeme chtít kolidovat.


 

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

 

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