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

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