Stiskněte "Enter" pro přeskočení obsahu

Arduino hra – Kanóny v kaňonu

0

Tato hra vznikla naprosto spontánně a bez jakýchkoli příprav, dlouhého přemýšlení apod… Už ani nevím co jsem řešil, ale napadlo mě zkusit si vygenerovat nějakou grafiku pomocí Arduina*. Původně třeba nějakou 3D kostku na OLED displeji, později třeba 3D arénu. Ale protože jsem zrovna OLED nějak neměl při ruce, použil jsem můj pokusný teploměr s barevným LCD 320 × 240 px. Ten je navíc poháněn rychlejším procesorem, takže by výsledek nemusel tolik problikávat a mohl bych si dovolit více věcí.

Nažhavil jsem Visual Studio, ale nakonec jsem si na 3D netroufl… no, úplně nakonec vlastně ano. Ale vzpomněl jsem si, jak jsme tehdá na základce hráli Scorched Earth, kterému nikdo neřekl jinak než „šorch“. Takže jsem se rozhodl zkusit vygenerovat 1D výškový terén. Když už byl terén hotový, napadlo mě, že bych mohl dokreslit tanky (děla)… jen tak. A když už byla děla hotová, řekl jsem si, že bych mohl zkusit vygenerovat balistickou křivku a děla by mohla rázem střílet a bojovat mezi sebou…

*V článku píši Arduino, ale jedná se o desku „BluePill/BlackPill“ s procesorem STM32. Nejedná se tedy o Arduino jako takové, ale programování je s knihovnou Wiring podobné.

 

Hardware

Co se hardware týče, nejedná se o nic speciálního. Řízení obstarává procesor STM32F103C8T6 na desce „Blue Pill/Black Pill“. Dříve jsem o ní psal článek, Vývojová deska „Arduino“ s procesorem STM32. Oproti podobným deskám (Arduino Nano, Micro apod.) má procesor větší rychlost, paměť, I/O apod. Rozdílné je také napájení, které není 5 V, ale 3,3 V. Nižší napájecí napětí přijde vhod kvůli použitému displeji, který má 3,3V logiku, takže není nutné používat např. odporové děliče jako když jsem vyráběl Hru života.

Zapojení displeje k desce Arduino. U BluePill není nutné používat děliče.

Protože je vše napájené z 9V baterie, je jako první použit DC-DC měnič, který vytvoří potřebné napětí 3,3 V. Kromě samotné desky a displeje je zde ještě osm tlačítek. Tlačítka jsou spínána proti GND – vstupní piny mají nastaven INTERNAL_PULLUP. Není potom potřeba použít externí rezistory. Logika vstupů se otáčí hned při načítání vstupů do proměnných**.

**Pokud jsou v programu použité vstupy, může být dobré si je hned na začátku smyčky loop() načítat do pomocných proměnných. Kromě toho, že je možné je hned invertovat v případě použití pull-up rezistoru, je zajištěno, že se v průběhu smyčky programu jejich hodnota nezmění. Dále je pak možné třeba detekovat náběžnou/sestupnou hranu těchto signálů bez nutnosti použít přerušení.

Na výstupní pin (je nutné použít výstup, který podporuje PWM) je připojen malý piezo reproduktor. Jeho odběr je malý, takže není nutné použít žádné tranzistory kvůli zesílení apod. Na jiný pin podporující PWM je připojeno podsvětlení displeje.

 

Generování terénu

První, do čeho jsem se pustil bylo generování terénu. Samotný terén se skládá z černého pozadí, na němž je vykreslena část měsíce a náhodně generované hvězdy. V pozadí jsou tmavé hory a v popředí je samotný zelený terén. Terén je samozřejmě vygenerován z výškové mapy. Ale jak tuto výškovou mapu vygenerovat? Každý pixel nelze generovat zcela náhodně, protože by terén byl moc „chlupatý“ a skutečnou krajinu by rozhodně nepřipomínal. Je tedy generováno 11 bodů rovnoměrně rozmístěných po mapě v náhodné výšce. Pokud by se tyto body spojily úsečkami vypadal by terén lépe, ale měl by ostré hrany – je tedy nutné ho vyhladit.

K vyhlazení jsou použity Bézierovy křivky. To jsou takové ty čáry v malování, jak je skoro nikdo nenakreslí na první pokus tak, jak by chtěl – zkuste tuto hru. Nejjednodušší je použít kvadratickou bézierovu křivku mezi třemi body. Mezi bodem P0 a P1 a mezi bodem P1 a P2 se postupně vytvoří pomocné body. Tyto pomocné body se spojí úsečkou a postupně se z úsečky odečítá bod, který už udává výšku terénu. Z těch výpočtů na wiki taky nejsem moc moudrej, ale animace to popisuje myslím dost jasně.

 

V cyklu se postupně vygeneruje z původních jedenácti bodů pět křivek, které díky společným bodům na sebe navazují. V některých místech to tvoří hladké přechody, jinde zase špičaté kopce. Určitě by šel algoritmus ještě vyladit, ale takhle to funguje docela pěkně. Celé se to samozřejmě opakuje dvakrát – pro pozadí a přední terén. Výsledek výpočtu jsou dvě pole bodů, které lze vykreslit a s kterým lze pracovat. Pozadí je posunuto o určitý offset tak, aby bylo výš než popředí.

//make bezier curve
for (byte i = 0; i < 9; i += 2) {
for (float j = 0; j < 1; j += 0.01) {
int x1 = i * 32;
int x2 = (i + 1) * 32;
int x3 = (i + 2) * 32;int y1 = terrainPoints[i];
int y2 = terrainPoints[i + 1];
int y3 = terrainPoints[i + 2];int xa = GetPoint(x1, x2, j);
int ya = GetPoint(y1, y2, j);
int xb = GetPoint(x2, x3, j);
int yb = GetPoint(y2, y3, j);int x = GetPoint(xa, xb, j);
int y = GetPoint(ya, yb, j);

if (trn2Enbl) {
terrain2[x] = y;
}
else {
terrain1[x] = y;
}
}
}

int GetPoint(int inX, int inY, float inPer) {
//get bezier point for terrain generate
int diff = inY - inX;
return inX + (diff * inPer);
}

Kvůli zjednodušení překreslení scény za tankem, se při generování terénu hlídá, jestli je zadní terén výše, než přední terén. Pokud ne (přece jen, je generován z náhodných čísel), tak je jeho výška zvýšena o nějakých 20 pixelů oproti přednímu terénu.

 

 

Generování pozic děl

Samotné dělo se skládá pouze z několika čtverečků a hlavně. Vykreslit takový tvar není nijak složité. Problém je s umístěním jednotlivých děl. Náhodná čísla nejsou nikdy zcela náhodná, což u jednoduchých procesorů platí dvojnásob. Pseudonáhodná čísla jsou generována pomocí generátoru, který pracuje se šumem z analogového pinu, časem od spuštění procesoru atd. Ale pokud jsou chvíli po sobě vygenerována náhodná čísla, může se stát, že jsou podobná. U terénu to vytvoří rovinu, u děl to vygeneruje dvě děla na stejné, nebo hodně blízké pozici. Největší problém nastává, když se děla vykreslí tak, že se prolínají – to jednoduše nelze akceptovat.

Při každém vykreslení děl se porovnává pozice všech děl s pozicí ostatních děl. Pokud je nějaké dělo jinému blíž, než 30 pixelů je výsledek zahozen a pozice všech děl se generuje znovu. Sice je to časově náročné v případě, že se vícekrát po sobě vygeneruje nevhodné umístění, ale výsledný kód je velice jednoduchý. Reálně nelze časový rozdíl postřehnout.

while (!generateDone) {
aliveTanks = tanksCount; //all tanks are alive for next level//generate possition of tanks
for (int i = 0; i < tanksCount; i++) {
randomSeed(analogRead(PA3) + millis() + i);
tankX[i] = random(10, 310);tankAngle[i] = 90;
tankPower[i] = 50;if (tankPoints[i] > 0) {
 tankAlive[i] = true;
 }
else {
 //kill tank without any points
 tankAlive[i] = false; //kill the tank
 aliveTanks--; //some tank was deleted
 }
}

int actualGap = 0;
int smallestGap = 320;

for (byte i = 0; i < tanksCount; i++) {
for (byte j = 0; j < tanksCount; j++) {
//check all combination
if (i != j) {
//test only different tanks
actualGap = abs(tankX[i] - tankX[j]); //count distance
if ((actualGap < smallestGap)) {
 smallestGap = actualGap;
 }
 }
 }
}

if (smallestGap > 30) {
 //smalest gap between two tanks is bigger than 30 pix.
 generateDone = true;
 }
}

Po vygenerování správných pozic jsou děla postupně vykreslena.

 

Překreslení děla v případě pohybu hlavně

Tohle je taková ta věc, které se někdo, kdo v životě neprogramoval asi diví. Ale při vykreslení nějaké změny grafiky je nutné předchozí stav nějakým způsobem uklidit. V případě pohybu hlavně by za ní zůstávala šmouha a za chvíli by nebylo vidět, kam dělo vlastně míří.

Překreslení celé plochy je časově velice náročné, takže by hrozilo blikání celé obrazovky. Optimální je překreslovat vždy co nejméně. V případě pohybu hlavně je vykreslen jenom malý čtvereček terénu okolo děla a poté vykresleno samotné dělo. Generovat terén není problém, protože pozice (přechod přední-zadní terén) je uložena v paměti a pozice děla je také známá. Takže se vykreslí od děla pár pixelů na všechny strany plus text s aktuálním nastavením aktivního děla. Blikání se tím minimalizuje.

Já jsem zvolil vykreslování vysokého „obdélníku“, který začíná několik pixelů pod dělem a končí až nad zadním terénem. Pokud by došlo při zničení tanku a propadnutí terénu o více pixelů, mohla by po tanku zůstat šmouha.

 

Generování trajektorie střely, zasažení terénu, zničení děla

Aby to byla správná hra, musejí děla samozřejmě umět střílet. A aby bylo vidět kam se vlastně střílí nechávají střely za sebou svojí trajektorii… což je mimochodem daleko snadnější na vykreslování – stačí vykreslit, nemusí se mazat.

Problém je zaplnění obrazovky šmouhami po střelách. Navíc by při překreslení scény za tankem (kvůli možnosti propadnutí tanku) docházelo k mazání trajektorií. Je tedy v každém cyklu zaznamenána pozice střely a v dalším cyklu je před vykreslením nové pozice stará překreslena. Přitom je nutné podle pozice střely měnit barvu mezi barvou terénu v pozadí, nebo oblohy.

Základní parametry pro směr střely je úhel děla a síla. Jako doplněk slouží náhodně generovaná intenzita větru v 10 úrovních na každou stranu. Při výpočtu směru střely jsou (jak jinak) použity goniometrické funkce sinus a cosinus. Trochu jsem si u toho zavzpomínal na program Twister, který jsem programoval před spoustou let a kde jsem si vlastně pořádně uvědomil k čemu sinus a kosinus můžou být.

Po stisknutí tlačítka pro výstřel je ve smyčce while generována trajektorie. Horizontální pozice je korigována silou větru. Vertikální pozice je korigována gravitací. Výpočet asi není úplně dokonalý, ale střílet se s tím dá.

V každé iteraci se kontroluje, jestli střela nevletěla za hranu disleje, nebo jestli nenarazila do terénu – to lze zjistit jednoduše porovnáním aktuální výšky střely a výšky terénu na stejné pozici. Pokud je terén zasažen, je z hodnot okolo místa zásahu odečtena nějaká hodnota (největší přímo v místě zásahu) – terén se o zadanou hodnotu propadne. Místo zásahu musí být samozřejmě překresleno, aby se změna projevila i na displeji. Zde lze překreslit pouze opravdu to, co se změnilo – je známá jak výška nového terénu, tak hloubka propadu terénu. Pokud je na propadlém terénu dělo, je překreslena i jeho pozice. Proto je nutné před každým vykreslením děla vymazat staré s dostatečnou výškovou rezervou – není totiž jisté, o kolik se dělo propadne.

Pokud je místo zásahu v blízkosti děla (ano, je nutné nehlídat přímo referenční pozici děla, která je široká pouze jeden pixel, ale nějaký rozsah), je dělo zničeno (nastaven příznak života na false), vymazáno a bliknuto s obrazovkou jako záblesk – efekt. Pokud ve scéně zbývá pouze jedno dělo, je vygenerováno další kolo. Pokud je ve scéně děl více, pokračuje se na další dělo (pokud je toto dělo již zničeno, opakuje se skok na další dělo, dokud není nalezeno jedno „naživu“).

Jakmile je na živu pouze jedno dělo a ostatní už mají příliš „málo života“, hra končí a jsou vyhlášeny výsledky. Ty nejsou pro zjednodušení kódu nijak seřazeny. Pouze se vykreslí jakýsi 3D objekt, který byl vlastně na počátku programování.

Takto to dopadne, když mám volné odpoledne. Základ tohoto článku pochází někde z roku 2019, teď „díky“ stavbě domu volný čas opravdu nemám. Program je možné stáhnout na mém GitHubu. Kód není nijak upravován a revidován. Prostě tak, jak jsem to naklapal do počítače…

Napsat komentář

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