Jednoduchá hra Auto v DelphiX (část 2) - Builder.cz - Informacni server o programovani

Odběr fotomagazínu

Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!

 

Zadejte Vaši e-mailovou adresu:

Kamarád fotí rád?

Přihlas ho k odběru fotomagazínu!

 

Zadejte e-mailovou adresu kamaráda:



C/C++

Jednoduchá hra Auto v DelphiX (část 2)

11. října 2001, 00.00 | Naprogramujte si jednoduché autíčko v C++ Builderu. S DelphiX, žádný problém.

Nevím, jestli si ještě vzpomínáte na můj článek, který jsem napsal někdy na jaře. Jednalo se o jednoduchou hru Auto, podobnou 8-biťáckým starým dobrým hrám. Jestli jste nečetli podívejte se Hra Auto (1. část)

Jak jste se dočetli v předchozí části, dělali jsme jednoduchou hru s pohybujícím se autem. Celá hrací plocha se skládala z 11*11 obrázků (políček). Je jasné, že jsme od toho nemohli čekat plynulý pohyb, ani detailní grafiku.
Před nedávnem jsem dostal nápad, udělat dvě nové variace na stejné téma. Jedna verze udělaná v DelphiX s použitím SpriteEnginu a další verze v OpenGL. Ale pěkně po pořádku, dnes si uděláme verzi pro DelphiX. Podívejte se na obrázek, jaké změny nás postihly:

Zvětšil jsem hrací plochu, zjemnil jsem ohyb po ploše a udělal jsem detailnější grafiku. Jelikož nejsem grafik, grafika není malovaná. Vypomohl jsem si auty ze starých časopisů a domy z katalogů (cestovních kanceláří :)). Pokud ale chcete udělat díru do světa, pořiďte si na grafickou úpravu levného profesionálního grafika, který vám udělá grafiku zadarmo.

Ještě než začneme hru vyvíjet, musíme si ještě předem naznačit, co je našim cílem. Budeme tvořit skrólovací hru, s bočním rolováním. Hráč ovládá auto po obrazovce nahoru a dolů a vyhýbá se protijedoucím vozidlům. Jeho volnost je omezena pouze na pohyb nahoru a dolu. Proti protivníkům lze vystřelit, ale jen omezeným počtem střel (aby to nebylo tak jednoduché). Po určité době se pohyb aut a četnost aut zrychluje.

Nyní se můžeme pustit do programování.

Založíme nový projekt a vložíme potřebné komponenty: DXDraw, DXImageList, DXTimer, DXInput, DXWaveList, DXSound a nesmíme zapomenout na DXSpriteEngine.

Připravíme si bitmapy, ty rozdělíme podle jejich funkce do čtyř skupin:
podklad - to co se zobrazuje na pozadí: cesta, tráva
player - auto, se kterým jedeme
enemy - nepřítel, protivník
střely

Veškerou grafiku si uložíme pomocí nějakého editoru do souboru grafika.dxg. Jak uložit grafiku, načíst do DXImageListu se dočtete v tomto článku.
DXImageList1->Items->LoadFromFile(ExtractFilePath(Application->ExeName)+ "grafika.dxg");

Podklad

Podklad můžeme vyřešit dvěma způsoby. Buď vytvoříme část podkladu - cestu+ krajnici+ okolní trávu jako jeden obrázek a budeme jej opakovat a nebo druhá možnost je podklad poskládat z jednotlivých bitmap, aby nevypadal tak staticky. Zvlášť si připravíme bílou krajnici, žlutou středovou čáru, horní trávník, dolní trávník a samozřejmě silnici. Z těchto bitmap poskládáme podklad. Pro veškeré zobrazování použijeme SpriteEngine. Takže si pro vytvoření podkladů odvodíme třídu přímo ušitou na míru od TBackgroundSprite.

////////////BACKGROUND//////////////////////
class TScrollBackground : public TBackgroundSprite {
public:
__fastcall TScrollBackground::TScrollBackground(TSprite *BSprite); double FSpeed; virtual void __fastcall DoMove(int MoveCount); protected: };

Třída neobsahuje žádné speciální finty, pouze jednu proměnou pro rychlost pohybu FSpeed a odvozenou funkci DoMove. Ta se vyvolává při pohybu. Tato událost má pouze jeden řádek
X-=FSpeed;
Z tohoto řádku vyplívá pohyb v ose X zleva doprava o FSpeed pixelů.

Teď již nezbývá než inicializovat jednotlivé podklady samostatně. Aby podklad jako celek nevypadal tak staticky, budou středová čára a cesta skrólovat jinou rychlostí než tráva na okraji. Úplně do pozadí namapujeme beton (přes celou plochu).

Podklad->SetMapSize(1, 1);
Podklad->Tile = true;

Namapujeme pouze jednu bitmapu cesta pomocí SetMapSize(1, 1). Vlastností Tile = true, obrázkem pokryjeme celou plochu na pozadí (dlaždice). Na to posadíme trávu, krajnice atd... Tak například

trava = new TScrollBackground(DXSpriteEngine1->Engine);
trava->Image = Form1->DXImageList1->Items->Find("trava2");
trava->FSpeed = 0.7;
trava->Z = -12; // posunout do pozadí
trava->Y = 0;
trava->SetMapSize(10000, 1); // namapovat 10000x vedle sebe

Definovali jsme travinu na okraji. Za zmínku stojí řádek trava->SetMapSize(10000, 1). Jelikož nemáme nastaveno Tile = true, posadíme jednu bitmapu na danou souřadnici X, Y, Z. A SetMapSize určí, že se bude 10000x opakovat vedle sebe. Kdyby bylo v závorkách (1, 10000) skládali by se bitmapy pod sebe.

Player

Jak jsem se již zmínil, hráč pohybuje autem nahoru a dolu. Potřebujeme minimálně tři bitmapy, které budou představovat naše auto - pro pohyb nahoru, dolu a rovně. A opět si odvodíme třídu, která bude popisovat auto.

// třída hráče
class TPlayerSprite : public TImageSprite {
protected:
public:
__fastcall TPlayerSprite::TPlayerSprite(TSprite *BSprite):TImageSprite(BSprite)
{
 . . . inic hodnot . . .
}
 bool animace; // jestli hráče animovat
 bool vybuch; // jestli došlo ke kolizi, výbuch 
 int Body; // počet nahraných bodů 
 double posunY;
 int wait; // čítač pro čekání

protected:
 virtual __fastcall void DoCollision(TSprite* Sprite,  bool &Done);
 virtual void __fastcall DoMove(int MoveCount);

};

Třídu jsem odvodil od TImageSprite a obdařil jsem ji velkou spoustou proměnných. K čemu slouží? Proměnná animace říká, jestli se má sprit animovat nebo ne. Implicitně máme nastaveno false, ale pokud dojde ke kolizi, přepneme na true. Proměnná výbuch indikuje, jestli jsme náhodou nevybuchli. Proměnná Body ukazuje počet nahraných bodů ve hře. PosunY je hodnota, o kterou se při pohybu posuneme nahoru nebo dolu. A nakonec wait je čítač pro čekání.

Podívejme se na události. Nejprve nahlédněme do DoMove. Při každém volání testujeme kolizi procedurou Collision. Mimoto zajišťujeme pohyb nahoru a dolu pomocí kurzorových kláves.

Form1->DXInput1->Update();  // zjistí aktuální změny na DXInput
if (Form1->DXInput1->States.Contains(isUp) == true && Y > 35 
  && vybuch == false) {
 Y -= posunY; // posuneme se nahoru o posunY
 Image = Form1->DXImageList1->Items->Find("porschel");
 wait = 15; // nastavíme čekání
}
if (Form1->DXInput1->States.Contains(isDown) == true && Y < 435-Image->Height 
 && vybuch == false) {
 Image = Form1->DXImageList1->Items->Find("porscher");
 Y += posunY;
 wait = 15;
}
if (wait > 0) 
 wait--; // čekání
else {
 if (vybuch == false)
 Image = Form1->DXImageList1->Items->Find("porsche");
}

Kód v tabulce je reakce na klávesy šipka nahoru a šipka dolu. Sprit se posune o posunY pixelů v ose Y.
Kód je myslím naprosto výstižný, snad jen za zmínku stojí proměnná wait. Pokud stisknete šipku nahoru (dolu), změní se bitmapa spritu na porschel (porscher). Pokud bychom šipku pustíme, okamžitě by se objevil původní obrázek auta (obrázek porsche). To se nestane, protože to opticky nevypadá moc dobře, je dobré chvíli počkat. No a k tomu poslouží proměnná wait, kterou nastavíme například na 15 a při každém volání DoMove ji snížíme o 1. Když je wait 0, Tak zobrazíme původní bitmapu (porsche).
Událost DoMove obsluhuje střelbu přes klávesu ENTER. Počet vystřelených střel se počítá v globální proměnné palba, a po každém výstřelu se opět čeká (čítačem prodleva), aby nelítala jedna střela za druhou. Střelba je též omezena počtem střel (pocetstr).

Tím se dostáváme ke kolizi auta, o tu se stará událost DoCollision. Při kolizi s kterýmkoli spritem dojde k výbuchu
Image = Form1->DXImageList1->Items->Find("explose");
Proměnné vybuch, animace a AnimLooped přepneme na true, tím se sprit začne animovat. AnimSpeed nastavíme na 4/1000 a nezapomeneme přetypovat na double (jinak se explose ani nepohne). Spolu s autem vybuchne i protivník
((TEnemySprite *) Sprite)->Image = Form1->DXImageList1->Items->Find("explose");
Parametr sprite v události DoCollision přetypujeme na TEnemySprite, to je speciální třída pro nepřátele.

Enemy

Pro nepřátele jsem vytvořil speciální třídu TEnemySprite, opět odvozenou od TImageSprite. Třída je podobná té předchozí. Navíc potřebujeme proměnné movingX a movingY. To jsou indexy, které jsem zavedl, abych identifikoval různé pohyby. Tak například pokud movingX = 1, bude se nepřítel pohybovat doleva, když movingX = 2, bude se nepřítel pohybovat doprava. Pro movingY to platí analogicky. Možná vás napadne, jestli to není zbytečné, přece stačilo by mít pouze proměnné pripocetX a pripocetY a ty odčítat nebo přičítat podle toho jaký pohyb bychom chtěli udělat. To nás ale značně omezuje, protože takhle bychom mohli pohybovat nepřítelem pouze do osmi směru. Použitím movingX a movingY zvládneme libovolný pohyb.

// trajektorie
if (Zivot > 0) {
if (movingY == 1)
Y-=pripocetY; // pohyb nahoru
if (movingY == 2)
Y+=pripocetY; // pohyb dolu
 if (movingX == 1)
  X-=pripocetX;   // pohyb doleva
 if (movingX == 2)
  X+=pripocetX;  // pohyb doprava
 if (movingX == 3) 
  X+=pripocetY*cos(3.14/180*abspoc)-1; // kosínus
}

Takže movingX a movingY jsou velmi výhodné, podívejte se na movingX = 3, nepřítel se bude pohybovat v x v kosínusovém intervalu a ještě o jedno doleva, vznikne tak zajímavý pohyb v kombinaci s movingY = 1. Ve hře se jednou za čas objeví vosa, která vyletí z pravého dolního rohu a poletí po kosínusovce nahoru.

Střelba

Naše auto jsem vybavil novou vlastností, střelbou. Do třetice si odvodíme třídu od TImageSprite. Nazval jsem ji TStrela. Má pouze proměnné Síla, což je ničivá síla. Hodnota, která se odečte od hodnoty Zivot u nepřítele. Takže v našem případě je velikost života nepřítele 10 a síla střely také 10. To znamená, že sestřelíme nepřítele jedinou střelou. Pokud budeme chtít, aby byl nepřítel sestřelen na dvakrát, nastavíme sílu střely na 5. Opět tu také máme movingX a movingY. Mimo události DoMove tu musíme vyřešit kolize DoCollision:

((TEnemySprite *)Sprite)->Zivot -= Sila;
Přetypujeme parametr Sprite v události DoCollision na TEnemySprite a odečteme od života sílu střely.


Na závěr musíme vyřešit to, co dělá naši hru hrou. To je pohyb, musíme zajistit, aby vše bylo v pohybu. K tomu nám poslouží obsluha události OnTimer u časovače DXTimer. Zde zajistíme překreslení obrazovky, umístění nepřátel atd.

Hra funguje asi takto: probíhá cyklus vyvolaný událostí OnTimer, při každé obrátce zajistíme vymazání obrazu a překreslení SpriteEnginu. SpriteEngine přesune všechny sprity na nové umístění a zjistí, jestli některé sprity nekolidují. Pokud došlo ke kolizi hráče Auto->vybuch = true, hra končí.

Auto ve hře ve skutečnosti stojí na jednom místě, pohybuje se jen podklad a nepřátelé. O umístění nepřátel se též staráme mi a generátor náhodných čísel.

V každém průchodu události OnTimer vygenerujeme náhodné číslo. Toto číslo je z intervalu 0 - 1000. Pokud je číslo například 0 - 5 dosadíme nepřítele na náhodně zvolenou (ale pozor vhodně) souřadnici těsně za obrazovku. Definujeme mu pohyb do leva, takže ihned jakoby vyjede na scénu :). To samé provádíme i s baráčky nahoře. Podívejte se jak sázíme do scény nepřátele:

int rnd = rand() % 1000;
if (rnd < cetnost) {
teroristi[teroristove]->Image = Form1->DXImageList1->Items->Find("enemy");
teroristi[teroristove]->AnimPos = rnd;
teroristi[teroristove]->AnimCount = teroristi[teroristove]->Image->PatternCount;
teroristi[teroristove]->X = 10000+DXDraw1->Width;
teroristi[teroristove]->Width = teroristi[teroristove]->Image->PatternWidth;
teroristi[teroristove]->Height = teroristi[teroristove]->Image->PatternHeight;
teroristi[teroristove]->animace = false;
teroristi[teroristove]->Collisioned = true;
teroristi[teroristove]->Zivot = 10;
teroristi[teroristove]->pripocetX = -level;
teroristi[teroristove]->movingY = 0;
teroristi[teroristove]->Y = 60+rand() % (DXDraw1->Height-130-teroristi[teroristove]->Height);
teroristove++;
if (teroristove >= MAXENEMY)
teroristove = 0; }

Na obrázku vidíte malý výřez z obrázku enemy, zde jsou poskládány auta nepřátel do řádky vedle sebe (12x). Jak již jsem říkal naše náhodné číslo je mále například 0-5. Vybereme tedy z obrázku enemy auto, které je v pořadí rnd (AnimPos), tedy stejném jako náhodné číslo. Pro správnou kolizi je potřeba nastavit hodnoty Width a Height. Nepřítel je umístěn těsně za obrazovku na 10000+DXDraw1->Width. Y souřadnici bereme tak, aby se auto vešlo do vozovky. Opět si pomůžeme generátorem náhodných čísel.

Na závěr bych se krátce zmínil o kolizích, které jsou brány obdélníkově, což považuji za chybu. To jakou kolizi DelphiX použije nastavujeme přepínačem PixelCheck. V poloze false, jsou kolize brány obdélníkově a je to bráno podle hodnot Width a Height spritu (šířky a délky). Pokud šířku a výšku nastavíte menší než je skutečná (PatternWidth a PatternHeight) dosáhnete lepšího výsledku. Nicméně zase se vám změní umístění spritu na obrazovce. Dejme tomu, že máme sprit umístěn na souřadnici 100*100. Pokud tedy vyzkoušíme toto umístění spritu dvakrát, pokaždé s jiným nastavením Width a Height, budou obrázky umístěny různě. A to není vůbec příjemné.
Pokud nastavíme PixelCheck na true, kolize je sledována skutečným tvarem obrázku. Na první pohled je vše v pořádku, ale trable nastanou při animovaných spritech nebo spritech páskových, které jsme použili mi. Protože u takových spritů DelphiX bere kolize pouze na první snímek. Takže pokud bychom v našem případě vzali AnimPos = 2, tak bychom normálně mohli přejet přes sprit a nedošlo by ke kolizi. Jak je to možné? To skutečně nevím. Pokud tento problém vyřešíte, prosím napište mi.

Doufám, že se vám dnešní článek líbil a bude inspirací pro vaši budoucí tvorbu.

Výsledek (655KB)

Relevantní články

Seriál - Tvorba her v DelphiX
Seriál - Tvorba hry Had v DelphiX
Zobrazení části obrázku v DelphiX
Komponenta DXImageList
Sprity v DelphiX (C++ Builder)

Tématické zařazení:

 » Rubriky  » C/C++  

 

 

 

Nejčtenější články
Nejlépe hodnocené články

 

Přihlášení k mému účtu

Uživatelské jméno:

Heslo: