3.4. Programiranje igara - avantura
Pronađi izlaz
Uvod
Na ovoj radionici izrađujemo malu tekstualnu escape-room igru za OLED 128x64 zaslon i 4x4 tipkovnicu. Glavni lik je mali seljak Marko koji pokušava pobjeći iz podzemnih katakombi tako da u svakoj fazi pronađe tragove, poveže ih i zatim riješi završnu zagonetku koju čuva vitez.
Tekstualne avanture su odličan izbor za Arduino jer dopuštaju da se fokusiramo na priču, logiku i upravljanje stanjima igre, umjesto na skupu grafiku i animacije.
Arduino Uno je vrlo pristupačna platforma, ali ima ograničene resurse: ATmega328P ima 2 KB SRAM-a i 32 KB Flash memorije, što znači da veliki stringovi, bitmap grafika i velike biblioteke brzo postaju problem. Arduino službene smjernice zato preporučuju smanjivanje broja stringova, korištenje manjih tipova podataka, ograničavanje veličine nizova i spremanje konstantnih podataka izvan SRAM-a kad god je moguće.
Za ovu vrstu igre bolji mikrokontroler bio bi ESP32, jer nudi mnogo više memorije: 520 kB (320 kB za podatke i 200 kB za program) i veći prostor za kompleksniji sadržaj, a i dalje ostaje jednostavan za rad s OLED zaslonom, tipkovnicom i Arduino-ekosustavom biblioteka. Arduino Uno je izvrstan za učenje osnova i razumijevanje ograničenja embedded razvoja, ali za veću tekstualnu avanturu s više faza, više soba i bogatijim opisima ESP32 je robusniji izbor.
Tema i kontrole
Tema igre je srednjovjekovni podzemni labirint: u mračnim hodnicima seljak Marko nailazi na duhove, stare natpise, zaključane prolaze i vitezove koji ga puštaju tek kada dokaže da je razumio tragove. Svaka faza ima tri sobe: prve dvije daju clue-ove, a treća je završna soba u kojoj igrač mora donijeti točan zaključak.
Kontrole su jednostavne i prilagođene tipkovnici 4x4:
- 4 i 6 pomiču igrača lijevo i desno po mapi.
- A ulazi u sobu ili pokreće akciju.
- B vraća na mapu.
- C otvara ekran s tragovima.
- D služi za izlaz iz poruke, povratak ili pokušaj otvaranja vrata.
Struktura programa
Program je organiziran kao state machine, što znači da igra u svakom trenutku zna u kojem se zaslonu ili stanju nalazi: naslov, pomoć, mapa, soba, clues ili završetak. Takva struktura je dobra praksa za male embedded igre jer je predvidljiva, laka za debug i troši manje memorije nego razgranat sustav s mnogo međusobno ovisnih objekata.
Više o kôdu u nastavku.
Programski kôd:
// C++
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <Keypad.h>
// ============================================
// OLED - page buffer for lower SRAM usage
// ============================================
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
// ============================================
// KEYPAD 4x4
// ============================================
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
// ============================================
// GAME STATE
// ============================================
enum ScreenState : uint8_t {
SCR_TITLE,
SCR_HELP,
SCR_MAP,
SCR_ROOM,
SCR_CLUES,
SCR_END
};
ScreenState screenState = SCR_TITLE;
uint8_t currentPhase = 0; // 0..2
uint8_t currentRoom = 0; // 0..2
uint8_t solvedMask[3] = {0, 0, 0};
uint8_t clueMask[3] = {0, 0, 0};
bool waitingAnswer = false;
uint8_t messageId = 0;
// ============================================
// HELPERS
// ============================================
bool isSolved(uint8_t phase, uint8_t room) {
return solvedMask[phase] & (1 << room);
}
void markSolved(uint8_t phase, uint8_t room) {
solvedMask[phase] |= (1 << room);
}
bool hasClue(uint8_t phase, uint8_t clueIndex) {
return clueMask[phase] & (1 << clueIndex);
}
void addClue(uint8_t phase, uint8_t clueIndex) {
clueMask[phase] |= (1 << clueIndex);
}
bool cluesReady(uint8_t phase) {
return (clueMask[phase] & 0b00000011) == 0b00000011;
}
bool phaseSolved(uint8_t phase) {
return (clueMask[phase] & 0b00000111) == 0b00000111;
}
void resetGame() {
currentPhase = 0;
currentRoom = 0;
solvedMask[0] = solvedMask[1] = solvedMask[2] = 0;
clueMask[0] = clueMask[1] = clueMask[2] = 0;
waitingAnswer = false;
messageId = 0;
screenState = SCR_TITLE;
}
// ============================================
// MINI SPRITES
// ============================================
void drawFarmer(int x, int y) {
u8g2.drawCircle(x + 4, y + 3, 3);
u8g2.drawLine(x + 4, y + 6, x + 4, y + 12);
u8g2.drawLine(x + 4, y + 8, x + 1, y + 10);
u8g2.drawLine(x + 4, y + 8, x + 7, y + 10);
u8g2.drawLine(x + 4, y + 12, x + 2, y + 15);
u8g2.drawLine(x + 4, y + 12, x + 6, y + 15);
}
void drawKnight(int x, int y) {
u8g2.drawFrame(x + 2, y + 1, 6, 6);
u8g2.drawLine(x + 5, y + 7, x + 5, y + 14);
u8g2.drawLine(x + 5, y + 9, x + 1, y + 11);
u8g2.drawLine(x + 5, y + 9, x + 9, y + 11);
u8g2.drawLine(x + 5, y + 14, x + 3, y + 16);
u8g2.drawLine(x + 5, y + 14, x + 7, y + 16);
}
void drawGhost(int x, int y) {
u8g2.drawCircle(x + 4, y + 4, 4);
u8g2.drawLine(x + 0, y + 8, x + 0, y + 14);
u8g2.drawLine(x + 8, y + 8, x + 8, y + 14);
u8g2.drawLine(x + 0, y + 14, x + 2, y + 12);
u8g2.drawLine(x + 2, y + 12, x + 4, y + 14);
u8g2.drawLine(x + 4, y + 14, x + 6, y + 12);
u8g2.drawLine(x + 6, y + 12, x + 8, y + 14);
}
// ============================================
// HEADER
// ============================================
void drawHeader(const char* title) {
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.setFontPosTop();
u8g2.drawBox(0, 0, 128, 10);
u8g2.setDrawColor(0);
u8g2.drawStr(2, 1, title);
u8g2.setDrawColor(1);
}
// ============================================
// MESSAGES
// ============================================
void drawMessageById(uint8_t id) {
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setFontPosTop();
switch (id) {
case 1:
u8g2.drawStr(0, 16, "Nasao si trag.");
u8g2.drawStr(0, 28, "Tipka C = clues");
u8g2.drawStr(0, 54, "D dalje");
break;
case 2:
u8g2.drawStr(0, 16, "Netocno.");
u8g2.drawStr(0, 28, "Povezi tragove.");
u8g2.drawStr(0, 54, "D dalje");
break;
case 3:
u8g2.drawStr(0, 16, "Vrata su");
u8g2.drawStr(0, 28, "zakljucana.");
u8g2.drawStr(0, 40, "Rijesi fazu.");
u8g2.drawStr(0, 54, "D dalje");
break;
case 4:
u8g2.drawStr(0, 16, "Vitez te pusta");
u8g2.drawStr(0, 28, "dalje.");
u8g2.drawStr(0, 54, "D dalje");
break;
case 5:
u8g2.drawStr(0, 16, "Soba je vec");
u8g2.drawStr(0, 28, "rijesena.");
u8g2.drawStr(0, 54, "D dalje");
break;
case 6:
u8g2.drawStr(0, 16, "Unesi odgovor");
u8g2.drawStr(0, 28, "1 / 2 / 3");
u8g2.drawStr(0, 54, "D nazad");
break;
case 7:
u8g2.drawStr(0, 16, "Prvo nadji");
u8g2.drawStr(0, 28, "oba traga.");
u8g2.drawStr(0, 40, "Tipka C clues");
u8g2.drawStr(0, 54, "D dalje");
break;
}
}
// ============================================
// CLUES
// ============================================
void drawClueLine(uint8_t phase, uint8_t clueIndex, uint8_t y) {
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.setFontPosTop();
if (!hasClue(phase, clueIndex)) {
u8g2.drawStr(0, y, "- ???");
return;
}
if (phase == 0) {
if (clueIndex == 0) u8g2.drawStr(0, y, "- Srednje je pravo.");
if (clueIndex == 1) u8g2.drawStr(0, y, "- Trazeni znak je SUNCE.");
}
if (phase == 1) {
if (clueIndex == 0) u8g2.drawStr(0, y, "- Sveti dar je kruh.");
if (clueIndex == 1) u8g2.drawStr(0, y, "- Vitez bira cvijet.");
}
if (phase == 2) {
if (clueIndex == 0) u8g2.drawStr(0, y, "- Drveni kljuc pase.");
if (clueIndex == 1) u8g2.drawStr(0, y, "- Cuvar trazi list.");
}
}
// ============================================
// RENDER
// ============================================
void renderTitle() {
drawHeader("DUNGEON ESCAPE");
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setFontPosTop();
drawFarmer(8, 18);
drawKnight(102, 18);
u8g2.drawStr(22, 16, "Seljak Marko");
u8g2.drawStr(22, 28, "trazi izlaz.");
u8g2.drawStr(6, 48, "A start");
u8g2.drawStr(72, 48, "D help");
}
void renderHelp() {
drawHeader("KONTROLE");
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setFontPosTop();
u8g2.drawStr(0, 14, "4/6 kretanje");
u8g2.drawStr(0, 26, "A akcija");
u8g2.drawStr(0, 38, "B mapa");
u8g2.drawStr(0, 50, "C clues D nazad");
}
void renderMap() {
drawHeader("MAPA");
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.setFontPosTop();
int rx[3] = {8, 48, 88};
int ry[3] = {24, 24, 24};
for (uint8_t i = 0; i < 3; i++) {
u8g2.drawFrame(rx[i], ry[i], 28, 18);
if (i == currentRoom) {
u8g2.drawBox(rx[i] + 1, ry[i] + 1, 26, 16);
u8g2.setDrawColor(0);
char n[2] = {char('1' + i), 0};
u8g2.drawStr(rx[i] + 11, ry[i] + 5, n);
u8g2.setDrawColor(1);
} else {
char n[2] = {char('1' + i), 0};
u8g2.drawStr(rx[i] + 11, ry[i] + 5, n);
}
if (isSolved(currentPhase, i)) {
u8g2.drawDisc(rx[i] + 24, ry[i] + 4, 2);
}
}
if (currentPhase == 0) u8g2.drawStr(0, 12, "Faza 1");
if (currentPhase == 1) u8g2.drawStr(0, 12, "Faza 2");
if (currentPhase == 2) u8g2.drawStr(0, 12, "Faza 3");
u8g2.drawStr(0, 56, "A soba C clues D vrata");
}
void renderClues() {
drawHeader("CLUES");
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.setFontPosTop();
if (currentPhase == 0) u8g2.drawStr(0, 12, "Faza 1");
if (currentPhase == 1) u8g2.drawStr(0, 12, "Faza 2");
if (currentPhase == 2) u8g2.drawStr(0, 12, "Faza 3");
drawClueLine(currentPhase, 0, 24);
drawClueLine(currentPhase, 1, 36);
if (phaseSolved(currentPhase)) {
u8g2.drawStr(0, 50, "- Zavrsna soba rijesena");
} else {
u8g2.drawStr(0, 54, "D nazad");
}
}
void renderRoom() {
drawHeader("SOBA");
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.setFontPosTop();
if (currentRoom == 0) {
if (currentPhase == 0) {
u8g2.drawStr(0, 14, "Nisa");
u8g2.drawStr(0, 26, "Natpis kaze:");
u8g2.drawStr(0, 36, "pravo nije");
u8g2.drawStr(0, 46, "lijevo ni desno.");
}
if (currentPhase == 1) {
u8g2.drawStr(0, 14, "Oltar");
u8g2.drawStr(0, 26, "Natpis kaze:");
u8g2.drawStr(0, 36, "sveti dar");
u8g2.drawStr(0, 46, "je kruh.");
}
if (currentPhase == 2) {
u8g2.drawStr(0, 14, "Skrinja");
u8g2.drawStr(0, 26, "Natpis kaze:");
u8g2.drawStr(0, 36, "drveni kljuc");
u8g2.drawStr(0, 46, "otvara put.");
}
drawKnight(106, 34);
}
if (currentRoom == 1) {
if (currentPhase == 0) {
u8g2.drawStr(0, 14, "Duh");
u8g2.drawStr(0, 26, "Sapuce:");
u8g2.drawStr(0, 36, "trazeni znak");
u8g2.drawStr(0, 46, "je sunce.");
}
if (currentPhase == 1) {
u8g2.drawStr(0, 14, "Tamnica");
u8g2.drawStr(0, 26, "Duh kaze:");
u8g2.drawStr(0, 36, "vitez bira");
u8g2.drawStr(0, 46, "cvijet.");
}
if (currentPhase == 2) {
u8g2.drawStr(0, 14, "Hodnik");
u8g2.drawStr(0, 26, "Cuvar kaze:");
u8g2.drawStr(0, 36, "na vratima je");
u8g2.drawStr(0, 46, "urezan list.");
}
drawGhost(108, 36);
}
if (currentRoom == 2) {
if (currentPhase == 0) {
u8g2.drawStr(0, 14, "Vrata viteza");
u8g2.drawStr(0, 26, "Koji znak?");
u8g2.drawStr(0, 36, "1=sunce");
u8g2.drawStr(0, 46, "2=oko 3=kruna");
}
if (currentPhase == 1) {
u8g2.drawStr(0, 14, "Zavrsna brava");
u8g2.drawStr(0, 26, "Sto odabrati?");
u8g2.drawStr(0, 36, "1=kruh");
u8g2.drawStr(0, 46, "2=cvijet 3=mac");
}
if (currentPhase == 2) {
u8g2.drawStr(0, 14, "Posljednja brava");
u8g2.drawStr(0, 26, "Koji simbol?");
u8g2.drawStr(0, 36, "1=list");
u8g2.drawStr(0, 46, "2=kljuc 3=mac");
}
drawKnight(106, 34);
}
if (isSolved(currentPhase, currentRoom)) {
u8g2.drawStr(0, 56, "Rijeseno. D nazad");
} else if (!waitingAnswer) {
if (currentRoom < 2) {
u8g2.drawStr(0, 56, "A uzmi trag B mapa");
} else {
u8g2.drawStr(0, 56, "A rijesi C clues");
}
}
}
void renderEnd() {
drawHeader("SLOBODA");
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setFontPosTop();
drawFarmer(10, 20);
u8g2.drawLine(28, 16, 28, 48);
u8g2.drawLine(28, 16, 40, 22);
u8g2.drawLine(28, 48, 40, 42);
u8g2.drawStr(46, 16, "Marko je");
u8g2.drawStr(46, 28, "izasao iz");
u8g2.drawStr(46, 40, "katakombi!");
u8g2.drawStr(0, 54, "A nova igra");
}
void renderCurrentScreen() {
u8g2.firstPage();
do {
switch (screenState) {
case SCR_TITLE: renderTitle(); break;
case SCR_HELP: renderHelp(); break;
case SCR_MAP: renderMap(); break;
case SCR_ROOM: renderRoom(); break;
case SCR_CLUES: renderClues(); break;
case SCR_END: renderEnd(); break;
}
if (messageId != 0 && screenState != SCR_END) {
u8g2.drawBox(0, 12, 128, 52);
u8g2.setDrawColor(0);
drawMessageById(messageId);
u8g2.setDrawColor(1);
}
} while (u8g2.nextPage());
}
// ============================================
// LOGIC
// ============================================
void handleMovement(char key) {
if (key == '4' && currentRoom > 0) currentRoom--;
if (key == '6' && currentRoom < 2) currentRoom++;
}
void solveClueRoom(uint8_t phase, uint8_t room) {
if (isSolved(phase, room)) {
messageId = 5;
return;
}
markSolved(phase, room);
addClue(phase, room);
messageId = 1;
}
void solveFinalRoom(char key) {
if (isSolved(currentPhase, 2)) {
waitingAnswer = false;
messageId = 5;
return;
}
if (!cluesReady(currentPhase)) {
waitingAnswer = false;
messageId = 7;
return;
}
char expected = '1';
if (currentPhase == 0) expected = '1'; // sunce
if (currentPhase == 1) expected = '2'; // cvijet
if (currentPhase == 2) expected = '1'; // list
if (key == expected) {
markSolved(currentPhase, 2);
addClue(currentPhase, 2);
messageId = 1;
} else {
messageId = 2;
}
waitingAnswer = false;
}
void tryDoor() {
if (!phaseSolved(currentPhase)) {
messageId = 3;
return;
}
if (currentPhase < 2) {
currentPhase++;
currentRoom = 0;
waitingAnswer = false;
messageId = 4;
} else {
screenState = SCR_END;
}
}
// ============================================
// SETUP / LOOP
// ============================================
void setup() {
u8g2.begin();
u8g2.setFontMode(1);
u8g2.setBitmapMode(1);
u8g2.setFontDirection(0);
resetGame();
renderCurrentScreen();
}
void loop() {
char key = keypad.getKey();
if (!key) return;
if (messageId != 0) {
if (messageId == 6) {
if (key == 'D') {
waitingAnswer = false;
messageId = 0;
} else if (key == '1' || key == '2' || key == '3') {
solveFinalRoom(key);
}
} else {
if (key == 'D') {
messageId = 0;
}
}
renderCurrentScreen();
return;
}
switch (screenState) {
case SCR_TITLE:
if (key == 'A') screenState = SCR_MAP;
else if (key == 'D') screenState = SCR_HELP;
break;
case SCR_HELP:
if (key == 'D') screenState = SCR_TITLE;
break;
case SCR_MAP:
if (key == '4' || key == '6') {
handleMovement(key);
} else if (key == 'A') {
screenState = SCR_ROOM;
} else if (key == 'C') {
screenState = SCR_CLUES;
} else if (key == 'D') {
tryDoor();
}
break;
case SCR_CLUES:
if (key == 'D' || key == 'B') {
screenState = SCR_MAP;
}
break;
case SCR_ROOM:
if (key == 'B' || key == 'D') {
waitingAnswer = false;
screenState = SCR_MAP;
} else if (key == 'C') {
screenState = SCR_CLUES;
} else if (currentRoom < 2 && key == 'A') {
solveClueRoom(currentPhase, currentRoom);
} else if (currentRoom == 2 && key == 'A') {
waitingAnswer = true;
messageId = 6;
}
break;
case SCR_END:
if (key == 'A') resetGame();
break;
}
renderCurrentScreen();
}
Glavni dijelovi programa su:
- globalne varijable za stanje igre, fazu, sobu i tragove,
- funkcije za crtanje ekrana,
- pomoćne funkcije za provjeru clue-ova i statusa faze,
- logika unosa s tipkovnice,
- glavna petlja loop() koja reagira na pritisnute tipke.
Dokumentacija funkcija
isSolved(uint8_t phase, uint8_t room)
- Provjerava je li određena soba već riješena u zadanoj fazi. Prima broj faze i broj sobe, a vraća
trueilifalse.
markSolved(uint8_t phase, uint8_t room)
- Označava sobu kao riješenu tako da postavlja odgovarajući bit u
solvedMask. Poziva se kada je igrač uspješno uzeo trag ili riješio završnu sobu.
hasClue(uint8_t phase, uint8_t clueIndex)
- Provjerava je li igrač već prikupio određeni clue u zadanoj fazi. Koristi se pri prikazu clue ekrana i pri provjeri može li se rješavati završna soba.
addClue(uint8_t phase, uint8_t clueIndex)
- Dodaje clue u masku tragova za zadanu fazu. Poziva se kad igrač uspješno aktivira clue sobu ili završi finalnu sobu.
cluesReady(uint8_t phase)
- Provjerava jesu li prikupljena oba potrebna clue-a u fazi. Koristi se kako bi se spriječilo prerano ili nasumično rješavanje završne sobe.
phaseSolved(uint8_t phase)
- Provjerava je li cijela faza završena, odnosno jesu li clue-ovi i finalna soba riješeni. Koristi se pri pokušaju otvaranja vrata.
resetGame()
- Vraća cijelu igru u početno stanje: resetira faze, sobe, maske tragova i zaslon. Poziva se na početku programa i pri ponovnom pokretanju igre.
drawFarmer(int x, int y)
- Crta jednostavan sprite glavnog lika na zadanoj poziciji. Parametri su x i y koordinata gornjeg lijevog kuta crteža.
drawKnight(int x, int y)
-‘Crta jednostavnog viteza na zadanoj poziciji. Koristi se na ekranima soba i naslovu igre.
drawGhost(int x, int y)
- Crta duha kao mali tematski grafički element. Parametri određuju položaj na OLED zaslonu.
drawHeader(const char* title)
- Crta zaglavlje na vrhu zaslona i ispisuje naslov ekrana. Prima C-string s nazivom zaslona i koristi se u gotovo svim render funkcijama.
drawMessageById(uint8_t id)
- Prikazuje jednu od unaprijed definiranih poruka, primjerice našao si trag ili vrata su zaključana. Parametar
idodređuje koja će poruka biti nacrtana.
drawClueLine(uint8_t phase, uint8_t clueIndex, uint8_t y)
- Na clue ekranu ispisuje konkretan trag ili ??? ako još nije pronađen. Prima broj faze, indeks traga i vertikalnu poziciju za ispis.
renderTitle()
- Crta početni ekran igre s naslovom i osnovnim opcijama za start i pomoć. Poziva se kada je stanje
SCR_TITLE.
renderHelp()
- Crta ekran s kontrolama tipkovnice. Koristi se kad korisnik izabere pomoć.
renderMap()
- Crta mapu faze i tri sobe kao jednostavne pravokutnike. Također pokazuje koja je soba trenutačno odabrana i koje su već riješene.
renderClues()
- Prikazuje ekran sa skupljenim tragovima za trenutačnu fazu. Koristi
drawClueLine()da pokaže pronađene i neotkrivene clue-ove.
renderRoom()
- Crta sadržaj trenutačne sobe: opis, trag ili završnu zagonetku, ovisno o fazi i broju sobe. Ovdje je font namjerno smanjen na
u8g2_font_5x8_tfkako bi tekst stao pregledno na mali OLED.
renderEnd()
- Crta završni ekran nakon uspješnog izlaska iz katakombi. Prikazuje kraj igre i mogućnost novog početka.
renderCurrentScreen()
- Ovo je centralna funkcija za prikaz: unutar U8g2 page-buffer petlje crta odgovarajući ekran prema trenutačnom stanju igre. Po potrebi dodaje i overlay poruku preko osnovnog ekrana, što je dobar način za OLED prikaz uz manju potrošnju memorije nego full-buffer pristup.
handleMovement(char key)
- Mijenja odabranu sobu na mapi kada igrač pritisne 4 ili 6. Parametar je znak tipke s tipkovnice.
solveClueRoom(uint8_t phase, uint8_t room)
- Rješava clue sobu: označi sobu riješenom, dodaje odgovarajući trag i pokaže poruku o uspjehu. Koristi se za prve dvije sobe svake faze.
solveFinalRoom(char key)
- Provjerava je li završni odgovor točan. Ako clue-ovi još nisu prikupljeni, prikazuje poruku da je prerano; ako jesu, provjerava tipku i po potrebi završava fazu.
tryDoor()
- Pokušava otvoriti vrata prema sljedećoj fazi. Ako faza nije završena, prikazuje poruku o zaključanim vratima; ako jest, otvara sljedeću fazu ili završava igru.
setup()
- Arduino inicijalizacija: pokreće zaslon, postavlja osnovne opcije biblioteke i resetira igru. Izvodi se jednom pri uključivanju ili resetiranju.
loop()
- Glavna petlja programa. Čita tipku s keypad-a, upravlja porukama i prebacuje stanja igre prema pravilima sučelja i logike.
Dodatna pitanja i zadaci:
- Koje upozorenje je gornji kôd prijavio prilikom prijenosa na Arduino?
- Razmisli kako bi mogao povećati radnu memoriju Arduina, makar i prividno?
- Kako optimizirati kôd da se troši manje dinamičke memorije?
Optimizacija memorije
Dio našeg programa čine tekstualne konstante, tablice i nizovi. Oni zauzimaju dosta mjesta u dinamičkoj memoriji. Takve podatke možemo pohraniti u programsku memoriju, koja je puno veća, te ih pozivati i učitavati u dinamičku memoriju po potrebi.
Za tako nešto koristimo ove glavne programske metode:
PROGMEMza tablice, nizove, strukture i tekstove u Flashu.F()makro za string literale koji se ispisuju direktno, bez kopiranja u SRAM kao obični literal.pgm_read_byte,pgm_read_word,pgm_read_ptri slične funkcije za dohvat pojedinačnih vrijednosti iz Flasha.strcpy_P,strlen_P,memcpy_Pi slične funkcije kad želiš nešto iz Flasha kopirati u običan RAM buffer i dalje obrađivati.
Kako to izgleda u praksi?
Za brojeve i tablice koristiš PROGMEM, pa po indeksu učitavaš samo ono što ti treba. To je posebno dobro za lookup tablice, nivoe igre, mape, sprite podatke i stalne konfiguracije.
// C++
#include <avr/pgmspace.h>
const uint8_t roomCosts[] PROGMEM = {3, 7, 12, 5};
void setup() {
Serial.begin(9600);
uint8_t value = pgm_read_byte(&roomCosts[2]);
Serial.println(value); // 12
}
void loop() {}
Ovdje niz ostaje u Flashu, a u SRAM ulazi samo jedna pročitana vrijednost value.
Za kratke tekstualne poruke koje samo ispisuješ najbolje je koristiti F() makro, jer tako tekst ostaje u Flashu i ne troši dragocjeni SRAM. To je jedna od najkorisnijih metoda optimizacije na Unu.
cpp
Serial.println(F(“Pokretanje igre…”));
To je bolje od:
// C++
Serial.println("Pokretanje igre...");
jer obični string literal češće završi kao SRAM opterećenje u AVR obrascima rada, dok F() eksplicitno zadržava tekst u Flashu za ispis.
Ako tekst ne želiš samo ispisati nego ga trebaš:
- uspoređivati,
- uređivati,
- slati u OLED funkciju koja traži RAM buffer,
- spajati s drugim podacima,
onda ga obično držiš u PROGMEM, a po potrebi kopiraš u mali lokalni buffer pomoću strcpy_P() ili memcpy_P().
// C++
#include <avr/pgmspace.h>
const char msg0[] PROGMEM = "Vrata su zakljucana";
const char msg1[] PROGMEM = "Nasao si trag";
const char* const messages[] PROGMEM = {msg0, msg1};
char buffer[24];
void printMessage(uint8_t index) {
const char* ptr = (const char*)pgm_read_ptr(&messages[index]);
strcpy_P(buffer, ptr);
Serial.println(buffer);
}
Ovo je vrlo važan obrazac: tablica pokazivača je u Flashu, i sami stringovi su u Flashu, a u SRAM ulazi samo mali radni buffer.
Možeš spremati i cijele strukture u Flash, pa ih po potrebi čitati u lokalnu varijablu. To je vrlo korisno za opise soba, definicije neprijatelja, iteme i konfiguracije levela.
// C++
#include <avr/pgmspace.h>
struct RoomData {
uint8_t id;
uint8_t correctAnswer;
uint8_t nextRoom;
};
const RoomData rooms[] PROGMEM = {
{0, 2, 1},
{1, 1, 2},
{2, 3, 0}
};
void loadRoom(uint8_t i, RoomData &out) {
memcpy_P(&out, &rooms[i], sizeof(RoomData));
}
Ovo je često robusnije od čitanja svakog polja zasebno, pogotovo ako radiš data-driven igru.
Najveće usko grlo u ovakvom refaktoru nije brzina nego kompleksnost rada s PROGMEM pokazivačima, posebno kad imaš tablice stringova i više faza. Zato je robustnija arhitektura: sve statične poruke, nazive faza i tekstove soba držati u Flashu, a imati jednu pomoćnu funkciju koja iz Flasha učita jedan red u mali RAM buffer i odmah ga nacrta.
Što ova verzija radi?
- sprema sve tekstove igre u Flash,
- koristi mali
lineBuffer[]za crtanje na OLED, - zadržava istu logiku 3 faze,
- i znatno smanjuje SRAM pritisak u odnosu na normalne string konstante.
Optimizirani kôd:
// C++
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <Keypad.h>
#include <avr/pgmspace.h>
// ============================================
// OLED - page buffer
// ============================================
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
// ============================================
// KEYPAD
// ============================================
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
// ============================================
// GAME STATE
// ============================================
enum ScreenState : uint8_t {
SCR_TITLE,
SCR_HELP,
SCR_MAP,
SCR_ROOM,
SCR_CLUES,
SCR_END
};
ScreenState screenState = SCR_TITLE;
uint8_t currentPhase = 0; // 0..2
uint8_t currentRoom = 0; // 0..2
uint8_t solvedMask[3] = {0, 0, 0};
uint8_t clueMask[3] = {0, 0, 0};
bool waitingAnswer = false;
uint8_t messageId = 0;
// Mali radni buffer za ispis linija s OLED-a
char lineBuffer[26];
// ============================================
// PROGMEM STRINGS
// ============================================
// Headers / static UI
const char s_title[] PROGMEM = "DUNGEON ESCAPE";
const char s_help[] PROGMEM = "KONTROLE";
const char s_map[] PROGMEM = "MAPA";
const char s_room[] PROGMEM = "SOBA";
const char s_clues[] PROGMEM = "CLUES";
const char s_end[] PROGMEM = "SLOBODA";
const char s_phase1[] PROGMEM = "Faza 1";
const char s_phase2[] PROGMEM = "Faza 2";
const char s_phase3[] PROGMEM = "Faza 3";
const char s_start[] PROGMEM = "A start";
const char s_help_btn[] PROGMEM = "D help";
const char s_ctrl1[] PROGMEM = "4/6 kretanje";
const char s_ctrl2[] PROGMEM = "A akcija";
const char s_ctrl3[] PROGMEM = "B mapa";
const char s_ctrl4[] PROGMEM = "C clues D nazad";
const char s_map_help[] PROGMEM = "A soba C clues D vrata";
const char s_back[] PROGMEM = "D nazad";
const char s_done[] PROGMEM = "Rijeseno. D nazad";
const char s_room_clue[] PROGMEM = "A uzmi trag B mapa";
const char s_room_solve[] PROGMEM = "A rijesi C clues";
const char s_end1[] PROGMEM = "Marko je";
const char s_end2[] PROGMEM = "izasao iz";
const char s_end3[] PROGMEM = "katakombi!";
const char s_restart[] PROGMEM = "A nova igra";
// Title text
const char s_marko1[] PROGMEM = "Seljak Marko";
const char s_marko2[] PROGMEM = "trazi izlaz.";
// Message texts
const char m1_1[] PROGMEM = "Nasao si trag.";
const char m1_2[] PROGMEM = "Tipka C = clues";
const char m1_3[] PROGMEM = "D dalje";
const char m2_1[] PROGMEM = "Netocno.";
const char m2_2[] PROGMEM = "Povezi tragove.";
const char m2_3[] PROGMEM = "D dalje";
const char m3_1[] PROGMEM = "Vrata su";
const char m3_2[] PROGMEM = "zakljucana.";
const char m3_3[] PROGMEM = "Rijesi fazu.";
const char m3_4[] PROGMEM = "D dalje";
const char m4_1[] PROGMEM = "Vitez te pusta";
const char m4_2[] PROGMEM = "dalje.";
const char m4_3[] PROGMEM = "D dalje";
const char m5_1[] PROGMEM = "Soba je vec";
const char m5_2[] PROGMEM = "rijesena.";
const char m5_3[] PROGMEM = "D dalje";
const char m6_1[] PROGMEM = "Unesi odgovor";
const char m6_2[] PROGMEM = "1 / 2 / 3";
const char m6_3[] PROGMEM = "D nazad";
const char m7_1[] PROGMEM = "Prvo nadji";
const char m7_2[] PROGMEM = "oba traga.";
const char m7_3[] PROGMEM = "Tipka C clues";
const char m7_4[] PROGMEM = "D dalje";
// Clues
const char clue_unknown[] PROGMEM = "- ???";
const char c10[] PROGMEM = "- Srednje je pravo.";
const char c11[] PROGMEM = "- Trazeni znak je SUNCE.";
const char c20[] PROGMEM = "- Sveti dar je kruh.";
const char c21[] PROGMEM = "- Vitez bira cvijet.";
const char c30[] PROGMEM = "- Drveni kljuc pase.";
const char c31[] PROGMEM = "- Cuvar trazi list.";
const char clue_done[] PROGMEM = "- Zavrsna soba rijesena";
// Room lines
const char r100[] PROGMEM = "Nisa";
const char r101[] PROGMEM = "Natpis kaze:";
const char r102[] PROGMEM = "pravo nije";
const char r103[] PROGMEM = "lijevo ni desno.";
const char r110[] PROGMEM = "Duh";
const char r111[] PROGMEM = "Sapuce:";
const char r112[] PROGMEM = "trazeni znak";
const char r113[] PROGMEM = "je sunce.";
const char r120[] PROGMEM = "Vrata viteza";
const char r121[] PROGMEM = "Koji znak?";
const char r122[] PROGMEM = "1=sunce";
const char r123[] PROGMEM = "2=oko 3=kruna";
const char r200[] PROGMEM = "Oltar";
const char r201[] PROGMEM = "Natpis kaze:";
const char r202[] PROGMEM = "sveti dar";
const char r203[] PROGMEM = "je kruh.";
const char r210[] PROGMEM = "Tamnica";
const char r211[] PROGMEM = "Duh kaze:";
const char r212[] PROGMEM = "vitez bira";
const char r213[] PROGMEM = "cvijet.";
const char r220[] PROGMEM = "Zavrsna brava";
const char r221[] PROGMEM = "Sto odabrati?";
const char r222[] PROGMEM = "1=kruh";
const char r223[] PROGMEM = "2=cvijet 3=mac";
const char r300[] PROGMEM = "Skrinja";
const char r301[] PROGMEM = "Natpis kaze:";
const char r302[] PROGMEM = "drveni kljuc";
const char r303[] PROGMEM = "otvara put.";
const char r310[] PROGMEM = "Hodnik";
const char r311[] PROGMEM = "Cuvar kaze:";
const char r312[] PROGMEM = "na vratima je";
const char r313[] PROGMEM = "urezan list.";
const char r320[] PROGMEM = "Posljednja brava";
const char r321[] PROGMEM = "Koji simbol?";
const char r322[] PROGMEM = "1=list";
const char r323[] PROGMEM = "2=kljuc 3=mac";
// ============================================
// HELPERS FOR PROGMEM TEXT
// ============================================
void drawTextP(uint8_t x, uint8_t y, const char* p) {
strcpy_P(lineBuffer, p);
u8g2.drawStr(x, y, lineBuffer);
}
void drawPhaseName(uint8_t phase, uint8_t x, uint8_t y) {
if (phase == 0) drawTextP(x, y, s_phase1);
else if (phase == 1) drawTextP(x, y, s_phase2);
else drawTextP(x, y, s_phase3);
}
// ============================================
// HELPERS
// ============================================
bool isSolved(uint8_t phase, uint8_t room) {
return solvedMask[phase] & (1 << room);
}
void markSolved(uint8_t phase, uint8_t room) {
solvedMask[phase] |= (1 << room);
}
bool hasClue(uint8_t phase, uint8_t clueIndex) {
return clueMask[phase] & (1 << clueIndex);
}
void addClue(uint8_t phase, uint8_t clueIndex) {
clueMask[phase] |= (1 << clueIndex);
}
bool cluesReady(uint8_t phase) {
return (clueMask[phase] & 0b00000011) == 0b00000011;
}
bool phaseSolved(uint8_t phase) {
return (clueMask[phase] & 0b00000111) == 0b00000111;
}
void resetGame() {
currentPhase = 0;
currentRoom = 0;
solvedMask[0] = solvedMask[1] = solvedMask[2] = 0;
clueMask[0] = clueMask[1] = clueMask[2] = 0;
waitingAnswer = false;
messageId = 0;
screenState = SCR_TITLE;
}
// ============================================
// SPRITES
// ============================================
void drawFarmer(int x, int y) {
u8g2.drawCircle(x + 4, y + 3, 3);
u8g2.drawLine(x + 4, y + 6, x + 4, y + 12);
u8g2.drawLine(x + 4, y + 8, x + 1, y + 10);
u8g2.drawLine(x + 4, y + 8, x + 7, y + 10);
u8g2.drawLine(x + 4, y + 12, x + 2, y + 15);
u8g2.drawLine(x + 4, y + 12, x + 6, y + 15);
}
void drawKnight(int x, int y) {
u8g2.drawFrame(x + 2, y + 1, 6, 6);
u8g2.drawLine(x + 5, y + 7, x + 5, y + 14);
u8g2.drawLine(x + 5, y + 9, x + 1, y + 11);
u8g2.drawLine(x + 5, y + 9, x + 9, y + 11);
u8g2.drawLine(x + 5, y + 14, x + 3, y + 16);
u8g2.drawLine(x + 5, y + 14, x + 7, y + 16);
}
void drawGhost(int x, int y) {
u8g2.drawCircle(x + 4, y + 4, 4);
u8g2.drawLine(x + 0, y + 8, x + 0, y + 14);
u8g2.drawLine(x + 8, y + 8, x + 8, y + 14);
u8g2.drawLine(x + 0, y + 14, x + 2, y + 12);
u8g2.drawLine(x + 2, y + 12, x + 4, y + 14);
u8g2.drawLine(x + 4, y + 14, x + 6, y + 12);
u8g2.drawLine(x + 6, y + 12, x + 8, y + 14);
}
// ============================================
// UI
// ============================================
void drawHeader(const char* titleP) {
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.setFontPosTop();
u8g2.drawBox(0, 0, 128, 10);
u8g2.setDrawColor(0);
drawTextP(2, 1, titleP);
u8g2.setDrawColor(1);
}
void drawMessageById(uint8_t id) {
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setFontPosTop();
switch (id) {
case 1:
drawTextP(0, 16, m1_1);
drawTextP(0, 28, m1_2);
drawTextP(0, 54, m1_3);
break;
case 2:
drawTextP(0, 16, m2_1);
drawTextP(0, 28, m2_2);
drawTextP(0, 54, m2_3);
break;
case 3:
drawTextP(0, 16, m3_1);
drawTextP(0, 28, m3_2);
drawTextP(0, 40, m3_3);
drawTextP(0, 54, m3_4);
break;
case 4:
drawTextP(0, 16, m4_1);
drawTextP(0, 28, m4_2);
drawTextP(0, 54, m4_3);
break;
case 5:
drawTextP(0, 16, m5_1);
drawTextP(0, 28, m5_2);
drawTextP(0, 54, m5_3);
break;
case 6:
drawTextP(0, 16, m6_1);
drawTextP(0, 28, m6_2);
drawTextP(0, 54, m6_3);
break;
case 7:
drawTextP(0, 16, m7_1);
drawTextP(0, 28, m7_2);
drawTextP(0, 40, m7_3);
drawTextP(0, 54, m7_4);
break;
}
}
void drawClueLine(uint8_t phase, uint8_t clueIndex, uint8_t y) {
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.setFontPosTop();
if (!hasClue(phase, clueIndex)) {
drawTextP(0, y, clue_unknown);
return;
}
if (phase == 0) {
if (clueIndex == 0) drawTextP(0, y, c10);
else drawTextP(0, y, c11);
} else if (phase == 1) {
if (clueIndex == 0) drawTextP(0, y, c20);
else drawTextP(0, y, c21);
} else {
if (clueIndex == 0) drawTextP(0, y, c30);
else drawTextP(0, y, c31);
}
}
// ============================================
// RENDER
// ============================================
void renderTitle() {
drawHeader(s_title);
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setFontPosTop();
drawFarmer(8, 18);
drawKnight(102, 18);
drawTextP(22, 16, s_marko1);
drawTextP(22, 28, s_marko2);
drawTextP(6, 48, s_start);
drawTextP(72, 48, s_help_btn);
}
void renderHelp() {
drawHeader(s_help);
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setFontPosTop();
drawTextP(0, 14, s_ctrl1);
drawTextP(0, 26, s_ctrl2);
drawTextP(0, 38, s_ctrl3);
drawTextP(0, 50, s_ctrl4);
}
void renderMap() {
drawHeader(s_map);
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.setFontPosTop();
int rx[3] = {8, 48, 88};
int ry[3] = {24, 24, 24};
for (uint8_t i = 0; i < 3; i++) {
u8g2.drawFrame(rx[i], ry[i], 28, 18);
if (i == currentRoom) {
u8g2.drawBox(rx[i] + 1, ry[i] + 1, 26, 16);
u8g2.setDrawColor(0);
char n[2] = {char('1' + i), 0};
u8g2.drawStr(rx[i] + 11, ry[i] + 5, n);
u8g2.setDrawColor(1);
} else {
char n[2] = {char('1' + i), 0};
u8g2.drawStr(rx[i] + 11, ry[i] + 5, n);
}
if (isSolved(currentPhase, i)) {
u8g2.drawDisc(rx[i] + 24, ry[i] + 4, 2);
}
}
drawPhaseName(currentPhase, 0, 12);
drawTextP(0, 56, s_map_help);
}
void renderClues() {
drawHeader(s_clues);
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.setFontPosTop();
drawPhaseName(currentPhase, 0, 12);
drawClueLine(currentPhase, 0, 24);
drawClueLine(currentPhase, 1, 36);
if (phaseSolved(currentPhase)) drawTextP(0, 50, clue_done);
else drawTextP(0, 54, s_back);
}
void renderRoom() {
drawHeader(s_room);
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.setFontPosTop();
if (currentPhase == 0 && currentRoom == 0) {
drawTextP(0, 14, r100); drawTextP(0, 26, r101);
drawTextP(0, 36, r102); drawTextP(0, 46, r103);
drawKnight(106, 34);
} else if (currentPhase == 0 && currentRoom == 1) {
drawTextP(0, 14, r110); drawTextP(0, 26, r111);
drawTextP(0, 36, r112); drawTextP(0, 46, r113);
drawGhost(108, 36);
} else if (currentPhase == 0 && currentRoom == 2) {
drawTextP(0, 14, r120); drawTextP(0, 26, r121);
drawTextP(0, 36, r122); drawTextP(0, 46, r123);
drawKnight(106, 34);
} else if (currentPhase == 1 && currentRoom == 0) {
drawTextP(0, 14, r200); drawTextP(0, 26, r201);
drawTextP(0, 36, r202); drawTextP(0, 46, r203);
drawKnight(106, 34);
} else if (currentPhase == 1 && currentRoom == 1) {
drawTextP(0, 14, r210); drawTextP(0, 26, r211);
drawTextP(0, 36, r212); drawTextP(0, 46, r213);
drawGhost(108, 36);
} else if (currentPhase == 1 && currentRoom == 2) {
drawTextP(0, 14, r220); drawTextP(0, 26, r221);
drawTextP(0, 36, r222); drawTextP(0, 46, r223);
drawKnight(106, 34);
} else if (currentPhase == 2 && currentRoom == 0) {
drawTextP(0, 14, r300); drawTextP(0, 26, r301);
drawTextP(0, 36, r302); drawTextP(0, 46, r303);
drawKnight(106, 34);
} else if (currentPhase == 2 && currentRoom == 1) {
drawTextP(0, 14, r310); drawTextP(0, 26, r311);
drawTextP(0, 36, r312); drawTextP(0, 46, r313);
drawGhost(108, 36);
} else if (currentPhase == 2 && currentRoom == 2) {
drawTextP(0, 14, r320); drawTextP(0, 26, r321);
drawTextP(0, 36, r322); drawTextP(0, 46, r323);
drawKnight(106, 34);
}
if (isSolved(currentPhase, currentRoom)) drawTextP(0, 56, s_done);
else if (!waitingAnswer) {
if (currentRoom < 2) drawTextP(0, 56, s_room_clue);
else drawTextP(0, 56, s_room_solve);
}
}
void renderEnd() {
drawHeader(s_end);
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setFontPosTop();
drawFarmer(10, 20);
u8g2.drawLine(28, 16, 28, 48);
u8g2.drawLine(28, 16, 40, 22);
u8g2.drawLine(28, 48, 40, 42);
drawTextP(46, 16, s_end1);
drawTextP(46, 28, s_end2);
drawTextP(46, 40, s_end3);
drawTextP(0, 54, s_restart);
}
void renderCurrentScreen() {
u8g2.firstPage();
do {
switch (screenState) {
case SCR_TITLE: renderTitle(); break;
case SCR_HELP: renderHelp(); break;
case SCR_MAP: renderMap(); break;
case SCR_ROOM: renderRoom(); break;
case SCR_CLUES: renderClues(); break;
case SCR_END: renderEnd(); break;
}
if (messageId != 0 && screenState != SCR_END) {
u8g2.drawBox(0, 12, 128, 52);
u8g2.setDrawColor(0);
drawMessageById(messageId);
u8g2.setDrawColor(1);
}
} while (u8g2.nextPage());
}
// ============================================
// LOGIC
// ============================================
void handleMovement(char key) {
if (key == '4' && currentRoom > 0) currentRoom--;
if (key == '6' && currentRoom < 2) currentRoom++;
}
void solveClueRoom(uint8_t phase, uint8_t room) {
if (isSolved(phase, room)) {
messageId = 5;
return;
}
markSolved(phase, room);
addClue(phase, room);
messageId = 1;
}
void solveFinalRoom(char key) {
if (isSolved(currentPhase, 2)) {
waitingAnswer = false;
messageId = 5;
return;
}
if (!cluesReady(currentPhase)) {
waitingAnswer = false;
messageId = 7;
return;
}
char expected = '1';
if (currentPhase == 0) expected = '1'; // sunce
if (currentPhase == 1) expected = '2'; // cvijet
if (currentPhase == 2) expected = '1'; // list
if (key == expected) {
markSolved(currentPhase, 2);
addClue(currentPhase, 2);
messageId = 1;
} else {
messageId = 2;
}
waitingAnswer = false;
}
void tryDoor() {
if (!phaseSolved(currentPhase)) {
messageId = 3;
return;
}
if (currentPhase < 2) {
currentPhase++;
currentRoom = 0;
waitingAnswer = false;
messageId = 4;
} else {
screenState = SCR_END;
}
}
// ============================================
// SETUP / LOOP
// ============================================
void setup() {
u8g2.begin();
u8g2.setFontMode(1);
u8g2.setBitmapMode(1);
u8g2.setFontDirection(0);
resetGame();
renderCurrentScreen();
}
void loop() {
char key = keypad.getKey();
if (!key) return;
if (messageId != 0) {
if (messageId == 6) {
if (key == 'D') {
waitingAnswer = false;
messageId = 0;
} else if (key == '1' || key == '2' || key == '3') {
solveFinalRoom(key);
}
} else {
if (key == 'D') {
messageId = 0;
}
}
renderCurrentScreen();
return;
}
switch (screenState) {
case SCR_TITLE:
if (key == 'A') screenState = SCR_MAP;
else if (key == 'D') screenState = SCR_HELP;
break;
case SCR_HELP:
if (key == 'D') screenState = SCR_TITLE;
break;
case SCR_MAP:
if (key == '4' || key == '6') {
handleMovement(key);
} else if (key == 'A') {
screenState = SCR_ROOM;
} else if (key == 'C') {
screenState = SCR_CLUES;
} else if (key == 'D') {
tryDoor();
}
break;
case SCR_CLUES:
if (key == 'D' || key == 'B') {
screenState = SCR_MAP;
}
break;
case SCR_ROOM:
if (key == 'B' || key == 'D') {
waitingAnswer = false;
screenState = SCR_MAP;
} else if (key == 'C') {
screenState = SCR_CLUES;
} else if (currentRoom < 2 && key == 'A') {
solveClueRoom(currentPhase, currentRoom);
} else if (currentRoom == 2 && key == 'A') {
waitingAnswer = true;
messageId = 6;
}
break;
case SCR_END:
if (key == 'A') resetGame();
break;
}
renderCurrentScreen();
}
Dodatna pitanja i zadaci:
- Koliko ova verzija kôda koristi memorije u odnosu na prethodnu verziju?
Što smo naučili?
- I vrlo mali mikrokontroler može pokretati smislenu igru ako dobro ograničimo opseg projekta i pažljivo biramo arhitekturu. Vidjeli smo kako se priča pretvara u state machine, kako se OLED zaslon koristi za više različitih ekrana i zašto su memorijska ograničenja na Arduino Uno stvarna projektna granica, a ne samo tehnički detalj.
- Razliku između ideje koja dobro zvuči i implementacije koja stvarno radi na hardveru: tekstualna avantura mora biti kratka, jasna i logički strukturirana da bi bila stabilna i igriva na Uno platformi. Najvažnija pouka je da su u embedded igrama jednostavnost, čitljivost i dobra organizacija koda često važniji od više sadržaja.
- Kako optimizirati memoriju u samom programu, tako da ne opterećujemo mikrokontroler bez potrebe.