Sledujte nás na YouTube

Vyvíjíme pro WP v XNA: Implementace kolizí (8. díl)

V předchozích dílech seriálu jsme si vykreslili pozadí, raketku a rozpohybovali jsme přilétávající bonusy. Nyní se pustíme do tvorby asteroidů, střel a implementace kolizí.

Naše asteroidy si naprogramujeme velice podobně, jako jsme si dělali přilétávající bonusy. Mohli bychom k tomu opět využít objektů typu Sprite. My si to ale ještě trochu rozšíříme. Budeme chtít, aby mohl mít každý asteroid nejen různou velikost, ale i rychlost a odolnost proti střelám. Tyto planetky nepůjdou rozbít hned na první zásah, ale raketka se do nich bude muset strefit několikrát. Čím méně bude asteroidu zbývat života, tím více se bude také zabarvovat do červena.

- 

Zkusíme si rozšířit náš objekt Sprite tak, že si ve složce GameObjects vytvoříme ještě jednu třídu, nazveme si ji AsteroidSprite. Tato třída bude dědit naši původní třídu Sprite (neboli, bude mít všechny její původní vlastnosti), přidáme do ní ale ještě další funkcionalitu. Opět si tuto třídu označíme jako public a doplníme tam na začátek potřebné usingy. Vložíme si do ní datové položky, abychom si mohli pamatovat celkový a zbývající počet životů, stejně tak jako její rychlost. Aby tato třída měla k dispozici i všechny položky ze třídy Sprite zajistíme tak, že si za její název doplníme dvojtečku a slovo Sprite. Bude tedy vypadat nějak takto:

public class AsteroidSprite : Sprite {
    public int MaxLife = 5;
    public int Life = 5;
    public float Speed = 0.1f;
}

Doplníme si do ní také konstruktor, budeme zde chtít nastavovat pozici, texturu a rychlost pohybu. Rychlost si nastavíme přímo v něm, zpracování ostatních položek přenecháme konstruktoru třídy Sprite. Bude vypadat nějak takto:

public AsteroidSprite(Vector2 position, Texture2D texture, float speed)
    : base(position, texture)
{
    Speed = speed;
}

Zkusíme si také naprogramovat to zabarvování podle zbývajícího počtu životů. K tomu využijeme metodu Draw(). Protože už třída Sprite jednu takovou metodu má a my ji budeme chtít nahradit, provedeme jednu změnu. Ve třídě Sprite změníme předpis metody Draw() takto:

public virtual void Draw(SpriteBatch spriteBatch)

Bude to znamenat, že všechny poděděné třídy od Sprite si budou moci tuto metodu přepsat po svém. Do naší třídy AsteroidSprite si potom doplníme tuto implementaci:

public override void Draw(SpriteBatch spriteBatch)
{
    int c = (int)((float)Life / (float)MaxLife * 255f);
    Color light = new Color(255, c, c);

    spriteBatch.Draw(Texture, Rect, light);
}

Opět zde provádíme volání spriteBatch.Draw(), jen s rozdílem, že posledním parametrem už není bílá barva. Tato položka je barva světla, můžeme si takto jednoduše obarvit jakýkoliv 2D objekt. Na prvních dvou řádcích si tedy vypočítáváme naši barvu, červený kanál zůstane vždy naplno, zelená a modrá barva bude určena podle zbývajícího počtu životů (výsledná barva se tedy bude pohybovat od bílé až po červenou). Ke třídě AsteroidSprite to bude zatím vše, nyní si k asteroidům doplníme jejich chování. Bude to prakticky stejné, jako jsme to dělali s bonusy. Do deklarací ve třídě Game1 si přidáme tyto položky:

// Asteroidy 
List<AsteroidSprite> asteroids = new List<AsteroidSprite>(); Texture2D asteroidTexture; double nextAsteroidTime = 0d;

Do metody LoadContent() načtení dané textury:

asteroidTexture = Content.Load<Texture2D>("asteroid");

A do metody Update() kód starající se o jejich generování:

// Generování asteroidů
if (nextAsteroidTime <= 0d) { nextAsteroidTime = 1000d + random.NextDouble() * 3000d; float speed = 0.05f + (float)random.NextDouble() * 0.1f; float width = speed * 1000f; int y = random.Next(GraphicsDevice.Viewport.Height - (int)width); AsteroidSprite ast = new AsteroidSprite(new Vector2(GraphicsDevice.Viewport.Width, y), asteroidTexture, 0.15f - (speed - 0.05f)); ast.Size = new Vector2(width, width); ast.Life = ast.MaxLife = (int)(speed * 70f); asteroids.Add(ast); } else nextAsteroidTime -= gameTime.ElapsedGameTime.TotalMilliseconds;

Asteroidy nám tedy budou přilétat každou jednu až čtyři sekundy, jejich rychlost bude určena náhodně od 0,05 do 0,15 pixelů za 1 milisekundu herního času. Velikost bude záviset také na rychlosti, velké asteroidy poletí pomaleji, bude ale potřeba se do nich víckrát strefit (od cca 4 do 10 střel). Také si doplníme ještě dva kousky kódu starající se o jejich pohyb a smazání při vyjetí mimo obrazovku (úplně stejně, jako jsme to řešili u bonusů):

foreach (AsteroidSprite s in asteroids)
    s.Position.X -= (float)(s.Speed * gameTime.ElapsedGameTime.TotalMilliseconds);

asteroids.RemoveIf(item => item.Position.X < -item.Size.X);

Generování střel si uděláme obdobně, do deklarací si přidáme položky:

// Střely
List<Sprite> bullets = new List<Sprite>(); Texture2D bulletTexture; double nextBulletTime = 0d;

Do metody LoadContent() opět načtení textury:

bulletTexture = Content.Load<Texture2D>("strela");

A do metody Update() obsluhující kód:

// Generování střel a jejich pohyb
if (nextBulletTime <= 0d) { nextBulletTime += 300f; bullets.Add(new Sprite(new Vector2(jet.Size.X, jet.Position.Y + jet.Size.Y / 2f - 5f), bulletTexture)); } else nextBulletTime -= gameTime.ElapsedGameTime.TotalMilliseconds; foreach (Sprite s in bullets) s.Position.X += (float)(0.15f * gameTime.ElapsedGameTime.TotalMilliseconds); bullets.RemoveIf(item => item.Position.X > GraphicsDevice.Viewport.Width);

Hlavní rozdíly jsou zde v tom, že si střely generujeme každých 300 ms (ne po náhodném čase), jejich pozici si volíme podle aktuálního umístění raketky. Pozici v ose X jim tentokrát přičítáme a mažeme je tehdy, pokud celé vyjedou z pravé části obrazovky. Asteroidy i střely si budeme chtít určitě i vykreslit, do metody Draw() si tedy přidáme tyto cykly:

foreach (Sprite s in asteroids)
    s.Draw(spriteBatch);
foreach (Sprite s in bullets)
    s.Draw(spriteBatch);

Dále si budeme chtít určitě dokončit i kolize střel s asteroidy. Využijeme naší předpřipravené metody Intersects(). Do metody Update() třídy Game1 si přidáme takovýto kousek kódu:

// Kolize střel a asteroidů
foreach (AsteroidSprite s in asteroids)
    for (int i = 0; i < bullets.Count; i++)
        if (bullets[i].Intersects(s.Rect))
        {
            s.Life--;
            bullets.RemoveAt(i--);
        }

asteroids.RemoveIf(item => item.Life <= 0);

Procházíme kolekci asteroidů a zjišťujeme, zda s každým z nich nekoliduje nějaká střela. Pokud ano, tak střelu smažeme a snížíme život asteroidu. Nakonec si pomocí naší extension metody odstraníme z pole asteroidů všechny mrtvé kusy. Mimochodem, tak, jak jsme si navrhli naši metodu, budou kolize posuzovány jako obdélník vůči obdélníku. Můžete si zkusit metodu Intersects() ve třídě AsteroidSprite přetížit podobně jako metodu Draw() a přepsat ji tak, aby reagovala správně na to, že asteroid je kulatý.

-

Do hry si ještě doplníme ošetření posledního typu kolizí, mezi raketkou a asteroidy. Pokud se naše raketka srazí s přilétávající planetkou, nastane konec hry. Vykreslování se zastaví a vypíše se nějaký text. Kliknutím kamkoliv na displej si budeme moci hru znovu spustit. Do deklarací ve třídě Game1 si přidáme položku, která nám bude představovat aktuální stav hry:

bool endOfGame = false;

Někam na konec metody Update() si doplníme krátký kód ošetřující kolize:

// Kolize raketky a asteroidů
foreach (AsteroidSprite s in asteroids) if (jet.Intersects(s.Rect)) endOfGame = true;

Teď si budeme chtít zařídit, aby se nám na konci hry zastavily všechny objekty (celá scéna jakoby zamrzla). To si zařídíme snadno, budeme chtít, aby v tomto případě nebyl volán žádný kód z metody Update(). Na začátek této metody (nejspíš za ty první 2-3 řádky, které nám ošetřují možnost ukončení aplikace tlačítkem zpět) si přidáme tento kousek kódu:

// Ošetření konce hry, začátek nové
if (endOfGame) { if (Mouse.GetState().LeftButton == ButtonState.Pressed) { // Spustit novou hru…
} else return; }

Pokud nám nastane konec hry, provede se příkaz return, ten nám ukončí provádění všech dalších příkazů v metodě Update(). Jedinou šancí, jak toto bude možné změnit, bude kliknutím na displej. Do této podmínky si doplníme kód zajišťující spuštění nové hry:

bonuses.Clear();
asteroids.Clear();
bullets.Clear();

availableBonuses = 0;
nextBonusTime = 0d;
nextAsteroidTime = 0d;
nextBulletTime = 0d;

endOfGame = false;

Takto si zinicializujeme všechny položky na jejich původní hodnoty, jako bychom si spustili hru úplně od začátku. Všimneme si, že naše hra má ještě jednu vlastnost, že se při každém jejím spuštění rovnou vygeneruje jeden asteroid a jeden bonus, není zde žádná časová mezera. To si také budete moci zkusit upravit. Využijte připravený objekt random a zde si nastavte položky nextBonusTime a nextAsteroidTime, například na náhodný interval od 0 do 3000 milisekund. Podobně to udělejte i při úplně prvním spouštění hry, využijte k tomu metodu Initialize().

V příštím díle se podíváme podrobně na ovládání raketky pohybovým senzorem a následně se naučíme vykreslovat text (zobrazíme si na displeji počet sesbíraných bonusů, herní čas a počet bodů získaných za sestřelené asteroidy).

Tomáš Slavíček

2 komentáře

  1. we (neregistrovaný)

    Otázečka – mám (funkční) menu-tlačítko. Neporadili byste mi, jak jej při každém kliknutí pomalu otočil o 180°?

    • Tomáš Slavíček (neregistrovaný)

      Jednoduché animace budou popsané v 10. článku tohoto seriálu, případně jsem se jim věnoval v 5. a 6. díle videonávodů (viz animace záře) [odkaz]

      Hlavní myšlenkou je určit si dvě položky: Čas, jak dlouho má animace trvat, a kolik zbývá do jejího konce (v milisekundách). Na začátku budou obě hodnoty nastaveny stejně. V metodě Update() si budeme čas do konce animace každý snímek snižovat o gameTime.ElapsedGameTime.TotalMilliseconds. Když poklesne pod nulu, animace končí. Podle poměru celková/uběhnutá doba si potom budeme budeme moci určit přesnou pozici např. toho tlačítka (tj. pokud tento poměr budeme mít od nuly do jedné, výslednou pozici si určíme jako počátační pozici + poměr * celkové posunutí/pootočení).

      Toto je v XNA udělané trochu nepříjemně, že je potřeba pro vytvoření každé jednoduché animace (zprůhlednění, pootočení apod.) psát tolik kódu. Můžete si tuto funkcionalitu ale zabalit do nějakého objektu, např. poděděného od našeho Sprite, kterému jen zadáte parametry a příkaz „teď se začni animovat“, on si potom bude tyto hodnoty sám přepočítávat v nějaké své metodě Update().

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *