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

Hra života na Arduinu

0

Tak si tak brouzdám po netu a narazím na něco, co se jmenuje Hra života. Už ani nevím, jestli to bylo na YouTube, na Wikipedii, nebo někde jinde. Ale na Wiki jsem zjistil, že princip výpočtu je vlastně velmi jednoduchý. Napadlo mě si to zkusit nasimulovat, nejdřív jsem chtěl na počítači…ale pak jsem se rozhodl, že opráším displej z Číny, který tu mám více než rok a ještě jsem na něj nesáhl. K tomu malý Arduino a uvidíme, opráším moje rezavý zkušenosti s C++ (Wiring) a uvidíme, co z toho vymáčknu.

Jen krátce o principu hry… jestli to lze tedy nazvat hrou. Hra vlastně není pro žádného hráče. Na začátku je plocha do které se vygenerují „buňky“. Všechny buňky mají naprogramováno identické chování. Buňka může mít pouze dva stavy – živá/mrtvá. Stav buněk se mění podle počtu živých buněk v jejím okolí. Podle druhu algoritmu může hra připomínat vývoj společenství živých organizmů.

Hardware

„Hra“ života mi běží na Arduino Micro, k němu mám připojený barevný grafický displej s rozlišením 320×240 px s driverem ILI9341. Pro ovládání displeje používám knihovny Adafruit_ILI9341.h a Adafruit_GFX.h. K Arduinu mám dále připojené dvě kapacitní „tlačítka“, které reagují přes plastovou krabičku.

Game of life on Arduino

Samotná kapitola pro mě bylo rozchození displeje. Záměrně jsem sáhl po barevném displeji 320×240 px. Ne že by se mi na to nějak přesně hodil, nebo tak… Ale ještě jsem ho nezkoušel a zajímalo mě, jestli ho vůbec dokážu zprovoznit. Chvilku jsem tápal; internet v tomhle směru moc nepomáhá. Krom toho, že existuje asi 20 druhů různých Arduin, několik knihoven na ovládání displejů a asi stejný počet možnosti propojení. Ještě tu mám malé OLED displeje 128*64 px., které mi fungují, mám je vyzkoušené a vypadají dost dobře. Uvidím, k čemu je použiji…

Protože displej má 3,3V a Arduino 5V logiku je nutné použít oddělovač. Já jsem použil 6 odporových děličů. Napájení LED podsvětlení je vyvedeno přímo z výstupu Arduina přes předřadný odpor 120 Ohm. Podsvětlení mám připojeno na výstup, který podporuje PWM. Ne že bych to používal, ale je možnost stmívání. Místo tlačítek používám kapacitní snímače z Aliexpressu.

Napájení ještě nemám vyřešeno. Zatím napájím přes USB kabel, protože upravuju program. Ale už mám vestavěný akumulátor ze staršího telefonu. Takže bude stačit nějaký nabíjecí obvod (na Aliexpressu je toho kupa) a přemýšlel jsem o solárním panelu, abych takovouhle bejkárnu nemusel živit… Stejně přemýšlím, že to rozeberu. Nemám moc rád neužitečný věci.

Schéma zapojení
Vnitřní (ne)uspořádání. Ještě chybí nabíjení akumulátoru a připevnit Arduino. Vlevo nahoře je šestinásobný odporový dělič.

Software

Software kupodivu není zas tak složitý. Největší problém mi dalo vůbec rozchodit displej. Respektive jsem nevěděl jaké piny Arduina použít, aby to fungovalo správně. Nakonec se mi podařilo rozchodit dvě knihovny (jiné jsem ani nezkoušel). Zkoušel jsem knihovny Ucglib a Adafruit_GFX s Adafruit_ILI9341. Poslední dvě jmenované nakonec používám. Myslím že zabírají méně paměti a jsou o něco rychlejší. Ale nějak extra jsem to neporovnával…v podstatě vlastně vůbec. Ale vypadalo to tak.

//Ovladani displeje s ILI9341 s pomocí Arduino MICRO 
//www.mylms.cz knihovna Ucglib
#include <SPI.h>
#include <Ucglib.h>
Ucglib_ILI9341_18×240×320_SWSPI ucg(/sclk=/ 13, /data=/ 11, /cd=/ 6 , /cs=/ 5, /reset=/ 4);
//Ovladani displeje s ILI9341 s pomocí Arduino MICRO 
//www.mylms.cz knihovna Adafruit
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
Adafruit_ILI9341 tft = Adafruit_ILI9341(5, 6, 11, 13, 4, 12);

Pokud funguje ovládání displeje, tak už samotné kreslení na displej je velice jednoduché. Stačí si pročíst průvodce. Knihovna Ucglib se ovládá v podstatě úplně stejně.

//vypsání textu na konkrétní pozici 
tft.setCursor(25, 20); //pozice levého horního rohu 
tft.setTextColor(CYAN); //barva 
tft.setTextSize(4); //velikost textu 
tft.print(„mylms.cz“); //samotné vypsání textu
//vykreslení vyplněného obdélníku
tft.fillRect(x, y, w, h, WHITE);
//a tak dále…

Logika hry je velice jednoduchá. Jde o to naplnit pole buňkami a potom je podle vzorce přepočítat. Přepočet taky není složitý. Nejhorší je paměťová náročnost pole buněk. Pole buněk je třeba mít dvakrát. Jedno staré, ze kterého se vypočítá pole nové. To se vykreslí, uloží se jako pole staré a jde se počítat znova. Celý program vlastně funguje takto:

  1. Start
  2. Inicializace displeje
  3. Vykreslení úvodní obrazovky
  4. Vytvoření náhodného vzoru a uložení do pole oldArray
  5. Výpočet nového pole (newArray) ze starého pole (oldArray) *
    1. zjištění kolik sousedů má aktuálně kontrolovaná buňka
    2. změna životnosti buňky podle počtu sousedů a aktuálního (oldArray) stavu
  6. Vykreslení na displej (pouze pokud displej svítí)
    1. Zjištění jestli se změnila buňka (porovnání oldArray a newArray *** ) (ANO – > skok na 6.2/ NE → skok na 6.1)
    2. Vykreslit barvu podle životnosti buňky
    3. Pokud není překresleno celé pole tak skok na 6.1
  7. Zkopírování newArray → oldArray
  8. Test stisku tlačítka – pokud bylo stisknuto, tak rozsvítit displej **
  9. Pokud displej svítí více než 60 sekund, tak zhasnout ***
  10. Skok na 5
 * tato funkce se provádí každých 1000 ms; ** tato funkce se provádí každých 50 ms; *** optimalizace kvůli rychlosti/spotřebě

Kvůli velikosti paměti Arduina používám dvě bool pole oldArray a newArray. Obě o stejné velikosti plochy (nyní 30 * 30 buněk). Tedy 900 bitů v jednom poli. Dohromady to je 1800 bitů, které se musejí stále dokola přepočítávat a vykreslovat.

Po spuštění a po inicializaci displeje (intro atd.) se naplní staré pole náhodnými daty. Jednoduše, pomocí smyčky FOR projedu dvourozměrné pole a vygeneruju náhodně TRUE/FALSE

void naplneni() { 
 //naplnění pole náhodnými hodnotami 
 for (byte x = 0; x < WORLDSIZE; x++) { 
  for (byte y = 0; y < WORLDSIZE; y++) { 
   rndNumber = random(255); 
   if(rndNumber >= 220) { 
    oldArray[x][y] = true; 
   } 
   else { 
    oldArray[x][y] = false; 
   } 
  } 
 } 
}

Tentokrát jsem kvůli úspoře místa a rychlosti nepoužil Tasker jako v sodobaru, ale jednoduše počítám čas od posledního spuštění funkce. Žádné delay(), které by brzdilo procesor. Elegantnější by samozřejmě bylo používat přerušení. Více se o této části kódu dočtete v článku Kusy kódu k Arduinu.

void loop(void){ 
 //stisk tlacitka 
 if (millis() - time_setDisplay >= 50) { 
  time_setDisplay = millis(); 
  setDisplay(); 
 }
//vykreslení grafiky
if (millis() - time_grafika >= 500) {
time_grafika = millis();
grafika();
}
}

V proceduře setDisplay() jednoduše hlídám, jestli nejsou stisknuta tlačítka. Pokud ano, tak rozsvítím podsvícení LCD. Pokud nejsou stisknuta tlačítka, tak počítám počet překreslení od uvolnění. Po nastavené době displej zhasne. V proceduře grafika() se provádí veškerá práce s grafikou, tedy výpočet a pokud svítí displej, tak i vykreslení.

Protože počítám i počet živých buněk z kterých vykresluji graf, tak si nejdřív vynuluji proměnnou population. Ve smyčce poté procházím celé pole a pozici aktuálně kontrolované buňky předávám funkce pocetSousedu(x, y);. Ta vrátí počet sousedících živých buněk. Potom podle životnosti buňky a počtu okolních buněk rozhodnu, jestli buňka bude žít. Pokud ano, tak nastavím TRUE a přičtu živou buňku do population.
Aby mi buňky nevychcípaly, mám v na konci vykreslování proceduru injectCells(). Pokud má populace dvakrát po sobě stejný počet živých buněk, tak na náhodné místo vložím několik buněk (vykresluji Křídlo/Glider). Sice to neřeší pokud by se cyklicky měnil počet třeba ze čtyřech na pět a zpět. Ale taková pravděpodobnost je malá… A lze se tomu vyhnout zavoláním funkce např. jednou za 1000 generací…

void vypocet() { 
 population = 0; //reset populace před sečtením 
 generation++; //další generace buněk 
 for (byte x = 0; x < WORLDSIZE; x++) { 
  for (byte y = 0; y < WORLDSIZE; y++) { 
   neighbors = pocetSousedu(x,y); //zobrazení počtu okolních buněk
if (oldArray[x][y]) {
//ziva bunka
if ((neighbors == 2 )||(neighbors == 3)) {
newArray[x][y] = true; //bunka zustane
population++;
}
else {
newArray[x][y] = false; //bunka umre
}
}
else {
//mrtva bunka
if (neighbors == 3) {
newArray[x][y] = true;
//bunka ozije
population++;
}
else {
newArray[x][y] = false; //bunka neozije
}
}
}
}
//přidání buněk při stejné populaci jako minule
if (lastPopulation == population) {
injectCells();
}
lastPopulation = population;
}

Samotná funkce pocetSousedu(x, y); vrací počet sousedících buněk. Její výstup může být 0 až 9. Protože používám malou plochu, buňky sousedící s krajem mají za sousedy buňky z druhého okraje. Takže buňka na souřadnici 0, 0 (levý horní okraj) má za sousedy i buňky z pravého horního okraje, levého dolního okraje, … Viz obrázek. Černá buňka je kontrolovaná a ze šedých buněk se bere výsledek. Takže buňky v podstatě nejsou na 2D ploše ale na toroidu, nebo tak na něčem podobným.

Kvůli tomu, že kontroluji buňky za hranami (hlídám zápornou hodnotu, nebo přetečení) „musím“ používat proměnou typu integer. Rychlejší by možná bylo použít typ byte a k pozici připočítávat offset. Pak by se počítalo pouze s kladnými čísly. Výpočet pole buněk však není největší brzda…

int pocetSousedu(byte x, byte y) { 
 byte result = 0;
//příprava pro přetečení z plochy
ax = x + 1;
ay = y + 1;
bx = x - 1;
by = y - 1;
if (ax == WORLDSIZE) { ax = 0; }
if (ay == WORLDSIZE) { ay = 0; }
if (bx == -1) { bx = WORLDSIZE-1; }
if (by == -1) { by = WORLDSIZE-1; }
if (oldArray[bx][by]) { result++; }
if (oldArray[x][by]) { result++; }
if (oldArray[ax][by]) { result++; }
if (oldArray[bx][y]) { result++; }
if (oldArray[ax][y]) { result++; }
if (oldArray[bx][ay]) { result++; }
if (oldArray[x][ay]) { result++; }
if (oldArray[ax][ay]) { result++; }
//return oldArray[bx][by] + oldArray[x][by] + oldArray[ax][by] + oldArray[bx][y] + oldArray[ax][y] + oldArray[bx][ay] + oldArray[x][ay] + oldArray[ax][ay];
return result;
}
Výpočet okolních buněk

Nejpomalejší z celého programu je vykreslování na displej. Zatím se mi nepodařilo zjistit, jestli je možné do displeje odeslat data a potom je naráz vykreslit (omezilo by se alespoň blikání), nebo lze vykreslování zrychlit. Možná to bude tím, že jsem po tom zatím nepátral… Opět ve smyčce projíždím pole, ale ještě před vykreslením zjišťuji jestli se buňka oproti dřívějšímu stavu změnila. Pokud ne, je zbytečné ji překreslovat. Pokud ano, tak ji překreslím. Vykreslování je díky tomu minimálně o 50 % rychlejší. Čím míň změněných buněk, tím je vykreslování rychlejší.
Teď mě napadlo, že by možná bylo možný vše vykreslovat v jedné smyčce. Ale nové pole by se na staré stejně muselo  zkopírovat až nakonec, protože je nepřípustné aby se tyto dvě pole míchala dokupy. Takže by ke konci stejně musela být smyčka… Každopádně problém s rychlostí vykreslování to neřeší…

void vykresleni() { 
 byte cellSize = 240 / WORLDSIZE;
for (byte x = 0; x < WORLDSIZE; x++) {
for (byte y = 0; y < WORLDSIZE; y++) {
if (oldArray[x][y] != newArray[x][y]) {
if (newArray[x][y]) {
//živá buňka
tft.fillRect(x * cellSize, y * cellSize, cellSize - 2, cellSize - 2, WHITE);
}
else {
//mrtvá buňka
tft.fillRect(x * cellSize, y * cellSize, cellSize - 2, cellSize - 2, BLACK);
}
}
oldArray[x][y] = newArray[x][y];
}
}
}

Takhle to je asi vše… Celý program si můžete stáhnout tady. Je to přesně ten program, který mi funguje. Ale funkci zaručit nemůžu, protože když vidím, jaký jsou peripetie s těmi displeji z Číny

Celkově z toho mám rozporuplný pocity. Tohle je přesně taková ta věc, která k ničemu není. Na zapřemýšlení nad algoritmem života buněk sice dobrý, ale na nějaké reálnější situace pomalé. Lepší by byl program na PC, který by zpracoval např. tisíce buněk několikrát rychleji. Ostatně, na netu si můžete najít online hru života, se kterou se dá i víc pracovat. Je to takový Tamagotchi (pro mladší generace Pou) o které se nemusíte starat. Ale jako zajímavost dobrý…

Napsat komentář

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