speciál
Vývoj her v XNA #9: Jednoduchá 3D hra #4

Vývoj her v XNA #9: Jednoduchá 3D hra #4
Vývoj her v XNA #9: Jednoduchá 3D hra #4
19:45, 04.02.2011

Minule jsme si naši scénu vylepšili graficky a umožnili jsme pohyb hlavního hrdiny. V dnešním díle si naši jednoduchou 3D hru dokončíme. Nejdříve si přidáme do projektu další model, například tohoto penízku ze stránek TurboSquid. Jeho texturu je opět potřeba zmenšit na mocninu dvojky, tedy například na 512x512 pixelů. Takto roztažený obrázek sice bude vypadat zdeformovaně, ale XNA si s ním poradí správně. Nahoru do Game1 si opět přidáme deklaraci, tentokrát dynamického pole penízků:

List<ModelObject> coins = new List<ModelObject>();

Nyní si budeme chtít jejich pozice náhodně nagenerovat na plošinky. Do metody LoadContent() za inicializace všech obdélníků rectangles si přidáme volání metody:

GenerateCoins();

Tuto metodu si umístíme někam do třídy Game1 s tímto (nebo nebo nějakým podobným) kouskem kódu:

/// 

/// Vygenerování náhodných pozic penízků na plošinkách

///

private void GenerateCoins()
{

Random random = new Random();

for (int i = 0; i < 10; i++)

{

int id = random.Next(rectangles.Count);

float x = rectangles[id].Position.X + (float)random.NextDouble() * rectangles[id].Size.X;

float z = rectangles[id].Position.Z + (float)random.NextDouble() * rectangles[id].Size.Z;

ModelObject coin = new ModelObject(new Vector3(x, rectangles[id].Position.Y + 0.2f, z),

new Vector3(MathHelper.PiOver2, (float)random.NextDouble() * MathHelper.TwoPi, 0f),

0.001f);

coin.LoadModel("Coin\\TyveKrone", Content, GraphicsDevice);

coins.Add(coin);
}

}

Metodu jsme si navrhli takto odděleně, abychom si mohli generování penízků volat i kdykoliv později, například při spouštění nové hry. Využíváme zde třídy Random – generátoru pseudonáhodných čísel. Vytváříme 10 penízků, každému vybíráme nějakou plošinku, na kterou by měl přistát, a jeho pozici. Rozložení nebude úplně rovnoměrné, protože některé plošinky můžeme mít různě velké, to nám ale nevadí. Můžete si tento kód podle sebe zkusit změnit. Každému penízku zde potom nastavujeme v ose X pootočení o 90 stupňů (aby byl postavený na hranu) a v ose Y náhodné výchozí pootočení.

Bude pěkné, když se nám budou penízky také efektně otáčet. Do metody Update() si můžeme přidat tento kousek kódu:

// Otáčení penízků

foreach (ModelObject coin in coins)

coin.Rotation += new Vector3(0f, elapsedTime * 0.005f, 0f);

Zbývá nám je už jen vykreslit, to uděláme poměrně očekávatelně v metodě Draw() těmito příkazy:

foreach (ModelObject coin in coins)

coin.RenderModel(camera);

Můžeme si hru zkusit spustit, měly by se nám všechny pěkně točit. Protože jejich pozice generujeme náhodně, pokud si hru spustíme podruhé, objeví se nám na jiných pozicích.

Vykreslování písma

Další, do čeho se pustíme, bude zobrazování aktuálního skóre. Protože ve 3D grafice je potřeba vykreslovat všechno pomocí trojúhelníků a textur, i text se bude muset renderovat podobným způsobem. To je ale naštěstí vyřešeno v XNA poměrně pohodlně. Jen si v určitém souboru zvolíme, z jakého rozsahu ASCII tabulky budeme znaky používat a potřebné bitmapy se nám vygenerují samy. XNA podporuje i Unicode znaky (například české háčky a čárky), na ty je už ale většinou potřeba generovat vlastní tabulku. Nemůžeme si totiž držet v paměti obrázky všech písem na světě, co bychom potom dělali například s čínštinou nebo japonštinou. Často potom ve hře můžeme mít pro každý jazyk jiný soubor. Pro vykreslování speciálních znaků se dá využít například utilitka BMFontGen, my si ale zatím pro jednoduchost háčky a čárky zanedbáme.

Nejdřív si přidáme do složky Content našeho projektu nový Font – soubor popisující písmo. Klikneme pravým tlačítkem na Content, zvolíme Add New Item a vybereme položku SpriteFont. Otevře se nám zdrojový kód, můžeme si zde moci zvolit například velikost nebo typ našeho písma. Musíme si dát pozor, že běžně známé Fonty opět mohou být chráněné autorským zákonem, v našich hrách bychom neměli používat úplně všechny. Můžeme ale bez problémů využít asi 12 typů písma, které si Microsoft licencoval a dal všem vývojářům k dispozici.

Do deklarace třídy Game si opět přidáme další definici:

SpriteFont spriteFont;

A do metody LoadContent() doplníme:

spriteFont = Content.Load<SpriteFont>("SpriteFont1");

Samotné vykreslení provedeme na konci metody Draw() pomocí těchto příkazů:

// Vykreslí text na obrazovku

spriteBatch.Begin();

spriteBatch.DrawString(spriteFont, "Coins: " + coins.Count.ToString(), new Vector2(10, 10), Color.Black);

spriteBatch.DrawString(spriteFont, "Coins: " + coins.Count.ToString(), new Vector2(9, 9), Color.White);

spriteBatch.End();

GraphicsDevice.BlendState = BlendState.Opaque;

GraphicsDevice.DepthStencilState = DepthStencilState.Default;

GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;

Aby byl text dobře vidět i na tmavém pozadí, vykreslujeme si ho nadvakrát – nejdříve černou a potom bílou barvou, s o něco posunutou pozicí. Protože SpriteBatch mění některé nastavení grafického zařízení, musíme na konci ještě vrátit tyto hodnoty do normálu. Je to jedna ze specifických vlastností nového XNA 4.0, na kterou musíme pamatovat.

Sbírání penízků

Sbírání penízků si ošetříme v metodě Update(). Stačí nám přidat tyto tři řádky kódu:

// Sbírání penízků

for (int i = 0; i < coins.Count; i++)

if (hero.TransformedBoundSphere.Intersects(coins[i].TransformedBoundSphere))

coins.RemoveAt(i--);

Protože každý náš model má automaticky vypočítanou nejmenší kouli, do které se vejde, stačí nám vždy ověřit, zda tyto koule spolu nekolidují. Je ale pravda, že některé modely mohou být špatně vytvořené a spočítaná koule nemusí přesně kopírovat jejich tvar. To je například i případ našeho penízku. Do objektu typu ModelObject si můžeme zadat pozici této koule přímo, do metody GenerateCoins(), hned za příkaz coin.LoadModel(…) si vložíme řádek:

coin.BoundSphere = new BoundingSphere(Vector3.Zero, 120f);

Podobně i můj hrdina “bacil” měl svoji kouli trochu posunutou, do metody LoadContent(), hned za hero.LoadModel(…) jsem si přidal tento řádek:

hero.BoundSphere = new BoundingSphere(Vector3.Zero, 300f);

S parametry poloměru koule si musíte trochu zaexperimentovat. V praxi se potom pro pokročilejší kolize mohou používat i další objekty – například BoundingBoxy, nejmenší kvádry obepínající modely. Stejně tak na správné načítání těchto obálek se dají psát vlastní Content Importery a další rozšíření, ty už ale patří rozhodně k pokročilejším technikám. Pro vykreslení našich BoundingSphere nebo BoundingBoxů se už také hodí něco znát (jak se konstruují objekty), pokud si to ale chcete zkusit sami napsat, můžete se inspirovat například vzorovým projektem Primitives3D na stránkách App Hub.

Padání z plošinek a spouštění nové hry

Pokud chceme ověřovat, že hra skončila, nahoru do deklarací si přidáme:

bool endOfGame = false;

Do metody Update() si přidáme kód ověřující, jestli spadnul z plošinek:

// Padání z plošinek

bool onTheBoard = false;

foreach (RectanglePrimitive rect in rectangles)

{

BoundingBox rectBB = new BoundingBox(rect.Position, rect.Position + rect.Size);

if (rectBB.Intersects(hero.TransformedBoundSphere))

onTheBoard = true;
}
if (!onTheBoard)

endOfGame = true;

V tomto cyklu postupně procházíme jednotlivé obdélníky a ověřujeme, zda kolidují s hlavním hrdinou. Pokud žádnou takovou kolizi nenajdeme, nastavíme, že byla hra ukončena. Ještě by bylo pěkné zamezit ovládání hráče v případě konce hry, kousek kódu na začátku metody Update() nastavující pootočení a posun hrdiny nám stačí obalit podmínkou:

if (!endOfGame)

{
// Pootočení do stran...

}

Hra by měla být ukončena i tehdy, když sebere všechny penízky. Někam do metody Update() si přidáme také toto:

// Sebral všechny penízky

if (coins.Count == 0)

endOfGame = true;

To, že hra skončila, by bylo dobré také hráči nějak oznámit. To si můžete udělat podle sebe, já jsem si například jen přidal do metody Draw(), hned za volání spriteBatch.DrawString(…), tento kousek kódu:

if (endOfGame)

spriteBatch.DrawString(spriteFont, "Konec hry! Stiskni mezernik.", new Vector2(GraphicsDevice.Viewport.Width / 2 - 140, 9), Color.White);

if (coins.Count == 0)

spriteBatch.DrawString(spriteFont, "Vyhral jsi!", new Vector2(GraphicsDevice.Viewport.Width / 2 - 140, 39), Color.White);

Ještě nám zbývá, že mezerníkem bychom měli být schopni spustit novou hru. Do metody Update() si jako pokračování podmínky “if (!endOfGame)” přidáme tento kód:

else

{
// Spustit novou hru

if (keyState.IsKeyDown(Keys.Space))

{
endOfGame = false;
coins.Clear();
GenerateCoins();

hero.Position = new Vector3(0f, 0f, 2.5f);

hero.Rotation = Vector3.Zero;

}

}

Tím by naše hra měla být hotová! Když si ji spustíme, můžeme chodit po plošinkách, sbírat body, hra se dá vyhrát i prohrát. Jako další vylepšení si tam můžete zkusit přidat například ubíhající časový limit, nebo další objekty a překážky. Také by se do naší hry určitě hodily nějaké zvuky, ale k tomu se vrátíme až zase třeba někdy jindy.

Klik pro zvětšení (Vývoj her v XNA #9: Jednoduchá 3D hra #4)


CGwillWin