3.3. Programiranje igara
Hvatanje jabuke – opis igre
Na lijevoj polovici ekrana (8x2 znakova) odvija se igra: u gornjem redu iznad igrača pada jabuka, tj. znakkoji mijenja oblik – od apostrofa, preko srednje točke, do obične točke – čime se simulira kretanje prema dolje.
Igrač kontrolira čovječuljka (korisnički definiran znak) u donjem redu pomoću dvije tipke (lijevo/desno) i pokušava ga dovesti ispod padajuće točke u trenutku kada je prikazana točka; tada osvaja bod, točka nestaje i ponovno se pojavljuje na nasumično odabranoj koloni.
Ako jabuka propadne pored igrača, u donjem se redu na toj koloni kratko prikaže znak (binarni kod 11111000) koji će predstavljati razbijenu jabuku i igrač gubi bod.
Desna polovica LCD‑a služi za rezultat (gore desno) i dodatne informacije (dolje desno) – npr. trenutni level ili poruke poput BRŽE!.
Svakih 10 osvojenih bodova brzina padanja se povećava, pa igra postaje sve zahtjevnija.
Spajanje LCD‑a i tipki na Arduino
Pravilno spoji LCD 1602 s I2C adapterom na Arduino (VCC, GND, SDA, SCL),
Pravilno spoji tipke za pomicanje igrača:
- dvije tipke (Lijevo, Desno) spajamo na dva digitalna pina Arduina (npr. D2 i D3).
- jedna strana tipke ide na GND.
- druga strana tipke ide na digitalni pin s uključenim internim pull‑up otpornikom (
pinMode(pin, INPUT_PULLUP);), tako da je stanje normalno HIGH, a kod pritiska LOW.
Ovako dobivamo pouzdano očitavanje tipki bez dodatnih otpornika, a I2C LCD i dalje koristi samo dva signalna pina.
Što Arduino radi u pozadini ove igre?
Ova mala igra na LCD‑u sastoji se od više elemenata koje Arduino mora stalno pratiti i ažurirati, i to bez trzanja i pauza u odzivu.
Da bi sve radilo glatko, program je podijeljen u nekoliko logičkih dijelova (stanja igre) i koristi mjerenje vremena s millis() umjesto blokirajućih delay() poziva.
-
Intro animacija
Na početku, Arduino u posebnoj intro fazi iscrtava naslov igre, možda pomiče tekst ili kratko prošeta čovječuljka po ekranu.
Ova animacija se odvija u petlji koja se poziva iz glavnogloop()dijela programa, ali je vremenski vođena prekomillis(), tako da se LCD osvježava u malim koracima bez zaustavljanja cijelog programa.
Kada intro završi (npr. nakon nekoliko sekundi ili pritiska tipke), stanje igre se prebacuje na igranje i intro se više ne prikazuje. -
Padanje znaka
Arduino periodički mijenja tri različita simbola (apostrof ⇒ srednja točka ⇒ točka) u gornjem redu, na odabranoj koloni igrališta.
Umjesto da koristidelay(), program u jednoj varijabli pamti kad je zadnji put pomaknut pad (npr.lastFallUpdate) i u svakom prolazu krozloop()provjerava je li prošlo dovoljno vremena (millis() − lastFallUpdate >= interval).
Kad vrijeme istekne, promijeni se faza pada i ekran se osvježi samo na toj poziciji; tako postoji dojam glatke animacije, a Arduino je i dalje slobodan stalno čitati tipke. -
Kontrola igrača (tipke lijevo/desno)
U svakom prolazu krozloop(),neovisno o animaciji, Arduino provjerava stanje tipki.
Ako je lijeva tipka pritisnuta, smanjuje se pozicija čovječuljka (dok ne dođe do lijevog ruba); ako je desna tipka pritisnuta, pozicija se povećava.
Kretanje igrača također može imati svoj mali tajmer (da se ne miče prebrzo), ali i to je izvedeno smillis(), tako da jedan kratki pritisak ne preskoči više polja odjednom. -
Sudar, bodovi i brzina igre
Kada padanje dođe do faze točke (na dnu gornjeg reda), Arduino uspoređuje kolonu padajućeg znaka s pozicijom čovječuljka.
Ako se kolone poklapaju u trenutku prikaza točke, povećava se broj bodova, točka se briše i nasumično odabire nova kolona za sljedeći pad.
Ako se ne poklapaju i promašimo, prikaže se znak propadanja u donjem redu na istoj koloni i smanjuje se broj bodova.
Svakih 10 osvojenih bodova Arduino smanjuje interval padanja (npr. s 400 ms na 300 ms, pa na 250 ms…), pa animacija ide sve brže, ali i dalje pomoću istogmillis()mehanizma. -
Glavna petlja i stroj stanja (state machine)
Cijela igra je organizirana kao state machine: npr.STANJE_INTRO,STANJE_IGRA,STANJE_GAME_OVER.
Uloop()funkciji se ne koristi velikiwhiles blokiranjem, nego se svaki put provjeri trenutno stanje igre i pozove odgovarajuća funkcija:- u
STANJE_INTROse odrađuje animacija i čeka početak, - u
STANJE_IGRAse paralelno brinu o padanju znaka, kretanju igrača i bodovima, - u
STANJE_GAME_OVERse prikaže rezultat i eventualno čeka pritisak tipke za restart.
Sve vremenske akcije (padanje, pomak igrača, intro animacija) oslanjaju se na različite tajmere bazirane namillis(), tako da nema dugih pauza i Arduino može istovremeno: animirati LCD, čitati tipke i reagirati na događaje u igri.
- u
Ne zaboravi u program uključiti Wire.h i LiquidCrystal_I2C.h, definirati objekt zaslona i pozvati lcd.begin().
// C++
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2); // LCD 1602 I2C
// Pinovi za tipke
const int BTN_LEFT = 2;
const int BTN_RIGHT = 3;
// Igralište
const int PLAY_WIDTH = 8; // lijevih 8 stupaca za igru
const int SCORE_COL = 9; // stupac za rezultat (0-based)
const int TOP_ROW = 0;
const int BOTTOM_ROW = 1;
// Stanja igre
enum GameState {
STATE_INTRO,
STATE_PLAY,
STATE_GAME_OVER
};
GameState gameState = STATE_INTRO;
// Vrijeme i intervali (ms)
unsigned long nowMs = 0;
unsigned long lastIntroUpdate = 0;
const unsigned long introInt = 300; // blink naslova
unsigned long lastFallUpdate = 0;
unsigned long fallInterval = 500; // brzina padanja
unsigned long lastPlayerMove = 0;
const unsigned long playerInt = 120; // brzina pomaka igraca
unsigned long failShowStart = 0;
const unsigned long failShowDur = 120; // koliko dugo se prikazuje “propadanje”
bool showFailSymbol = false;
// Igrač i padanje
int playerCol = 3; // 0..PLAY_WIDTH-1
int fallingCol = 5; // kolona padajućeg znaka
int fallPhase = 0; // 0=apostrof, 1=srednja točka, 2=točka
bool fallActive = true;
long score = 0;
// Custom znakovi
byte manChar[8] = {
B00100,
B01110,
B00100,
B01110,
B10101,
B00100,
B01010,
B10001
};
byte dropFailChar[8] = {
B11111,
B11111,
B11111,
B11111,
B00000,
B00000,
B00000,
B00000
};
void setup() {
pinMode(BTN_LEFT, INPUT_PULLUP);
pinMode(BTN_RIGHT, INPUT_PULLUP);
lcd.begin();
lcd.backlight();
lcd.createChar(0, manChar); // čovječuljak
lcd.createChar(1, dropFailChar); // propadanje
startIntro();
}
void loop() {
nowMs = millis();
switch (gameState) {
case STATE_INTRO:
updateIntro();
break;
case STATE_PLAY:
updateGame();
break;
case STATE_GAME_OVER:
updateGameOver();
break;
}
}
// ---------- INTRO ----------
void startIntro() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" Uhvati jabuku ");
lcd.setCursor(0, 1);
lcd.print(" Pritisni tipku ");
lastIntroUpdate = nowMs;
gameState = STATE_INTRO;
}
void updateIntro() {
// Blink naslova bez delay-a
if (nowMs - lastIntroUpdate >= introInt) {
lastIntroUpdate = nowMs;
static bool visible = true;
lcd.setCursor(0, 0);
lcd.print(visible ? " Uhvati jabuku " : " ");
visible = !visible;
}
// start igre na bilo koju tipku
if (buttonPressed(BTN_LEFT) || buttonPressed(BTN_RIGHT)) {
startGame();
}
}
// ---------- IGRA ----------
void startGame() {
lcd.clear();
score = 0;
playerCol = 3;
fallPhase = 0;
fallActive = true;
fallingCol = random(0, PLAY_WIDTH);
fallInterval = 500;
lastFallUpdate = nowMs;
lastPlayerMove = nowMs;
showFailSymbol = false;
drawStaticUI();
drawPlayer();
drawScore();
drawFallingSymbol();
gameState = STATE_PLAY;
}
void updateGame() {
handlePlayerInput();
updateFalling();
updateFailSymbol(); // brine se da “propadanje” kratko traje
// primjer jednostavnog game over uvjeta
if (score < -5) {
startGameOver();
}
}
void handlePlayerInput() {
if (nowMs - lastPlayerMove < playerInt) return;
bool moved = false;
if (buttonPressed(BTN_LEFT) && playerCol > 0) {
playerCol--;
moved = true;
}
if (buttonPressed(BTN_RIGHT) && playerCol < PLAY_WIDTH - 1) {
playerCol++;
moved = true;
}
if (moved) {
lastPlayerMove = nowMs;
drawPlayer();
}
}
void updateFalling() {
if (!fallActive) return;
if (nowMs - lastFallUpdate >= fallInterval) {
lastFallUpdate = nowMs;
fallPhase++;
if (fallPhase <= 2) {
// 0,1,2 → faze pada
drawFallingSymbol();
if (fallPhase == 2) {
// faza “točka” → provjera hvatanja
if (playerCol == fallingCol) {
score++;
drawScore();
// ubrzavanje svaka 10 bodova
if (score > 0 && (score % 10 == 0) && fallInterval > 150) {
fallInterval -= 70;
}
// novi pad
clearFallingColumn();
fallingCol = random(0, PLAY_WIDTH);
fallPhase = 0;
drawFallingSymbol();
}
}
} else {
// pad prošao sve faze - promašaj
if (playerCol != fallingCol) {
score--;
drawScore();
// prikaži “propadanje”
lcd.setCursor(fallingCol, BOTTOM_ROW);
lcd.write(byte(1)); // custom fail
showFailSymbol = true;
failShowStart = nowMs;
}
// priprema za novi pad
fallPhase = 0;
clearFallingColumn(); // ovo će obrisati i dno nakon isteka failShowDur
fallingCol = random(0, PLAY_WIDTH);
drawFallingSymbol();
}
}
}
void updateFailSymbol() {
if (showFailSymbol && (nowMs - failShowStart >= failShowDur)) {
// obriši znak “propadanja” nakon kratkog vremena
lcd.setCursor(fallingCol, BOTTOM_ROW);
lcd.print(" ");
showFailSymbol = false;
}
}
// ---------- GAME OVER ----------
void startGameOver() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Kraj igre!");
lcd.setCursor(0, 1);
lcd.print("Rezultat: ");
lcd.print(score);
gameState = STATE_GAME_OVER;
}
void updateGameOver() {
if (buttonPressed(BTN_LEFT) || buttonPressed(BTN_RIGHT)) {
startGame();
}
}
// ---------- CRTANJE ----------
void drawStaticUI() {
lcd.setCursor(SCORE_COL, TOP_ROW);
lcd.print("SC:0");
}
void drawScore() {
lcd.setCursor(SCORE_COL, TOP_ROW);
lcd.print("REZ: ");
lcd.setCursor(SCORE_COL + 4, TOP_ROW);
lcd.print(score);
}
void drawPlayer() {
// obriši donji red igrališta
for (int c = 0; c < PLAY_WIDTH; c++) {
lcd.setCursor(c, BOTTOM_ROW);
lcd.print(" ");
}
lcd.setCursor(playerCol, BOTTOM_ROW);
lcd.write(byte(0)); // čovječuljak
}
void drawFallingSymbol() {
// obriši gornji red igrališta
for (int c = 0; c < PLAY_WIDTH; c++) {
lcd.setCursor(c, TOP_ROW);
lcd.print(" ");
}
char ch = '\'';
if (fallPhase == 1) ch = 0xA5; // srednja točka (ovisno o fontu LCD-a)
if (fallPhase == 2) ch = '.';
lcd.setCursor(fallingCol, TOP_ROW);
lcd.print(ch);
}
void clearFallingColumn() {
lcd.setCursor(fallingCol, TOP_ROW);
lcd.print(" ");
// dno brišemo preko updateFailSymbol nakon što se fail prikaže dovoljno dugo
}
// ---------- ULAZ ----------
bool buttonPressed(int pin) {
return digitalRead(pin) == LOW;
}
Dodatna pitanja i zadaci:
- Zamijeni simbole kojima prikazujemo padanje jabuke korisnički definiranim (custom) znakovima.
- Više života ili health bar: dodaj brojač života (npr. 3 srca ili male kvadratiće) u donjem desnom kutu, koji se smanjuju kad propustimo znak; igra završava kad životi dođu na nulu.
- Različiti tipovi padajućih znakova: uvedi više custom znakova – npr. normalni znak donosi +1 bod, specijalni +3 boda, a neki loš znak uzima bod ako ga uloviš; igra mora razlikovati tip znaka i reagirati drugačijim efektom.
- Leveli i poruke: dodaj poruke na donji desni dio LCD‑a (npr. LEVEL 2, SUPER!, PAZI!) ovisno o broju bodova ili o tome koliko puta zaredom igrač pogodi.
- Timer ili ograničenje vremena: u gornjem desnom kutu prikaži odbrojavanje (npr. od 60 s) i završi igru kada vrijeme istekne, uz prikaz konačnog rezultata i kratku animaciju.
- Dva igrača naizmjenično: nakon što jedan igrač odigra (npr. 30 sekundi), LCD prikaže njegov rezultat i pozove drugog igrača; igra zadržava najbolji rezultat (HI SCORE) u gornjem desnom kutu.
Što smo naučili?
- Kako koristiti I2C LCD 1602 s Arduinom, uz definiranje i prikaz custom znakova.
- Kako spojiti tipkala s internim pull‑up otpornicima i čitati korisnički unosa u realnom vremenu.
- Kako jednostavnim promjenama znakova na displeju mogžemo simulirati animaciju i kretanje u ograničenoj rezoluciji LCD‑a. - Kako se igra može postupno nadograđivati (brzina, leveli, životi, specijalni znakovi), što ih uvodi u razmišljanje o dizajnu interaktivnih sustava i malih igara.