TOPlist

Vyvíjíme pro WP v XNA: Vykreslení, výběr a pohyb objektů (7. díl)

V minulém díle jsme si vykreslili raketku za pomoci třídy Sprite. Dnes tento pomocný objekt znovu využijeme a pustíme se do vykreslování přilétávajících bonusů.

Ještě, než si připravíte nějakou vlastní originální grafiku k této hře, připojím sem k dispozici ke stažení několik obrázků. Budete si je moci vložit na zkoušku do svého projektu (stačí, když si je nakopírujete do složky Content). Můžete si je stáhnout odsud. Potom si je určitě změníte podle sebe. Tuto hru si také budete moci v brzké době v kompletní podobě odzkoušet i přímo na svém zařízení, bude volně ke stažení na oficiálním Marketplace (spolu i s druhou hrou – hadem z videotutoriálů).

-

Jak jsme si už popsali, bonusy budou objekty přilétávající občas z pravé strany. Když na nějaký klikneme, zmizí, a nám se přičte jednička k počtu aktuálně využitelných akcí. Pokud jich budeme mít nasbíraných víc jak nula, budeme moci využít speciálních schopností raketky, jako laseru a tlakové vlny (ovládaných flick a pinch gestem). Používáním těchto schopností se nám bude počítadlo využitelných akcí zase odečítat, až zase do nuly. Do třídy Game1 si přidáme několik položek:

// Přilétávající bonusy 
List<Sprite> bonuses = new List<Sprite>(); Texture2D bonusTexture; double nextBonusTime = 0d; int availableBonuses = 0;

Jak vidíme, všechny aktuálně letící bonusy po obrazovce si budeme udržovat v dynamickém poli objektů typu Sprite, nazveme si ho bonuses. Protože na začátku toto pole bude ještě prázdné a bonusy si do hry budeme generovat až během hry v metodě Update(), využijeme i položku bonusTexture, do které si v metodě LoadContent() načteme správnou texturu:

bonusTexture = Content.Load<Texture2D>("bonus");

Zbývající dvě položky se nám budou hodit také, nextBonusTime bude určovat kolik ještě zbývá milisekund do vygenerování dalšího bonusu, v položce availableBonuses budeme mít uložen náš počet aktuálně využitelných superschopností raketky. Protože si budeme chtít pozice a časy objevování bonusů generovat náhodně, přidáme si nahoru do deklarací ještě objekt typu Random:

Random random = new Random();

Další funkcionalitu si doplníme do metody Update():

// Vygenerování bonusů
if (nextBonusTime <= 0d) { nextBonusTime = 3000d + random.NextDouble() * 6000d; int y = random.Next(GraphicsDevice.Viewport.Height - 64); bonuses.Add(new Sprite(new Vector2(GraphicsDevice.Viewport.Width, y), bonusTexture)); } else nextBonusTime -= gameTime.ElapsedGameTime.TotalMilliseconds;

Když nám odpočet času nextBonusTime klesne pod nula milisekund, vygenerujeme si nový časový úsek. Ten se bude pohybovat od 3 sekund do devíti. Rovnou si přidáme do pole bonuses nový objekt typu Sprite, bude umístěn na pozici x=šířka obrazovky, pozici y si zvolíme náhodně od nuly až po výšku obrazovky. Hodnota 64, kterou zde odečítáme, je napevno zadaná výška našeho bonusu. Můžete si ji změnit podle svého obrázku, nebo například načítat z nějaké proměnné. V našich objektech sice také máme položku .Size, ze které bychom si výšku mohli zjistit, v tuto chvíli ale naše pole bonuses mohlo být ještě prázdné (takže bychom si ji neměli odkud přečíst).

Dále si můžeme naše bonusy rozpohybovat směrem odprava doleva, to uděláme podobně, jako na konci minulého dílu. Projdeme si kolekci pomocí foreach cyklu, každému objektu o něco odečteme jeho pozici X:

foreach (Sprite s in bonuses)
    s.Position.X -= (float)(0.1f * gameTime.ElapsedGameTime.TotalMilliseconds); 

Zde si můžeme všimnout jedné zajímavosti, kterou jsme minule nedělali. Rychlost posunu si násobíme ještě položkou ElapsedGameTime.TotalMilliseconds. Pokud se nám někdy začne naše hra “sekat” (bude mít malou snímkovací frekvenci = FPS), mezi jednotlivými snímky nám uběhne mnohem více času, než kdyby hra běžela plynule. To by ale mohlo ovlivnit i rychlost animací ve hře. O kolik tedy uběhne ve hře více času, o tolik více posuneme objekt na herním plánu. Je to podobné, jako když usneme ve vlaku, tak také budeme očekávat, že se probudíme o pár kilometrů dál.

Pokud nám letící bonusy utečou nalevo mimo obrazovku, budeme si je chtít nejspíš vymazat z paměti. Nikdo by už na ně stejně nemohl kliknout, jenom by nám ubíraly drahocenný procesorový výkon. To můžeme udělat jednoduše for cyklem, že si jejich seznam projdeme a všechny pohybující se mimo obrazovku smažeme. Protože v naší hře možná budeme potřebovat mazání podle určité podmínky vícekrát, tento kód si oddělíme do vedlejší metody. Abychom měli ještě hezčí zápis v kódu, zkusíme si napsat takzvanou extension metodu.

Do projektu si vložíme novou třídu, nazveme si ji například ExtensionMethods.cs. Zde si napíšeme rozšířující metodu pro seznamy typu List. Ta bude přebírat podmínku, která o každém prvku bude moci rozhodnout, jestli se má smazat, nebo ne. Kompletní kód této třídy nám bude vypadat takto (název namespace si změňte podle svého projektu):

using System;
using System.Collections.Generic;
using System.Linq;

namespace SmartmaniaHra
{
    public static class ExtensionMethods {
        public static void RemoveIf<T>(this List<T> list, Func<T, bool> predicate)
        {
            for (int i = 0; i < list.Count; i++)
                if (predicate(list[i]))
                    list.RemoveAt(i--);
        }
    }
}

Tato rozšiřující metoda nám přidá do projektu možnost, že budeme moci využívat metodu RemoveIf() na každém objektu typu List a budeme si ho moci “filtrovat” podle zadané podmínky. Po zavolání této metody nám v našem listu zůstanou jen prvky, které podmínku nesplňovaly.

Jak je vidět v kódu, v této metodě si jen procházíme List od začátku do konce, pokud narazíme na prvek splňující podmínku, smažeme ho. Takto bychom ale mohli některé prvky přeskočit, snížíme si tedy index na původní hodnotu a zkusíme ověřit podmínku znovu. Tento cyklus by určitě fungoval i tehdy, když bychom si kolekci procházeli odzadu, pak by nám asi odpadl tento problém s indexy odmazávaných prvků.

Přepneme se ale zpět do hlavní třídy Game1, do její metody Update() si tedy budeme moci doplnit jednořádkové volání této extension metody:

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

Předáváme zde takzvanou lambda funkci. Tento zápis znamená, že pro každou položku item z našeho seznamu platí, že pokud splňuje danou podmínku, máme ji smazat. A podmínkou v tomto případě je test, jestli pozice X daného bonusu je menší, než jeho záporná hodnota šířky (tj. jestli je už celý nalevo mimo obrazovku).

Naše přilétávající bonusy si budeme samozřejmě chtít i vykreslit, do metody Draw() si tedy přidáme tento jednoduchý cyklus, který na všech objektech Sprite v tomto poli zavolá jejich metodu Draw():

foreach (Sprite s in bonuses)
    s.Draw(spriteBatch);

Nyní si můžeme odzkoušet, že se naše bonusy na displeji opravdu už postupně budou objevovat a létat zprava doleva. Můžete si pohrát s nastavením parametrů náhodného generátoru, s rychlostí animace a podobně.

-

Ještě si budeme chtít do naší hry doplnit možnost výběru bonusů po kliknutí. Pokud nám stačí jen zjistit, kam se kliknulo na herní plán (neboli, stačí nám informace o jednom dotyku, nepotřebujeme využít multitouch), dá se to udělat úplně stejně, jako na počítači. Zjistíme si stav “myši” a jestli bylo zmáčknuto její levé tlačítko, poté si jednoduše přečteme její pozici. Klikání na bonusy si můžeme zapsat například takto, kód si vložíme do metody Update():

// Kliknutím na bonus - přičteme ho k celkovému počtu, smažeme ho
MouseState mouse = Mouse.GetState(); if (mouse.LeftButton == ButtonState.Pressed) { Vector2 mousePoint = new Vector2(mouse.X, mouse.Y); for (int i = 0; i < bonuses.Count; i++) if (bonuses[i].Intersects(mousePoint)) { availableBonuses++; bonuses.RemoveAt(i--); } }

Provádíme zde vlastně úplně stejný cyklus, jako jsme si implementovali v naší extension metodě, jen s tím, že za každý smazaný bonus si přičteme jedničku k celkovému počtu schopností raketky. Jako podmínku zde využíváme metodu Intersects() na našem objektu typu Sprite. Tu ale ještě nemáme implementovanou, určitě si ji budeme chtít vytvořit. Posoudíme v ní, zda se předaný bod nachází uvnitř obdélníku. Do našeho objektu Sprite si tedy doplníme tuto metodu (nebo klidně obě):

public bool Intersects(Vector2 point)
{
    if (point.X >= Position.X && point.Y >= Position.Y && point.X <= Position.X + Size.X && point.Y <= Position.Y + Size.Y)
        return true;
    else return false;
}

public bool Intersects(Rectangle rect)
{
    return Rect.Intersects(rect);
}

V její první variantě posuzujeme oblast našeho Sprite vůči předanému bodu. Význam kódu je poměrně jasný. Druhou variantu jsme si připravili pro případ, pokud bychom chtěli posuzovat oblast našeho Sprite vůči jinému Rectangle. Vidíme zde, že tuto funkcionalitu už má dokonce XNA v sobě integrovanou, můžeme tedy toto posouzení vrátit jen takto jedním příkazem. Test, zda dva Rectangle spolu kolidují, se nám bude hodit v příštím díle, až začneme řešit kolize.

Autor článku Tomáš Slavíček
Tomáš Slavíček

Kapitoly článku