o.O wtFAQ – We Tinker, Fix and Question
◀ 3.2. Metronom4. Logički sklopovi ▶

3.3. Programiranje igara

hvatanje jabuke

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),

spajanje

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.

  1. 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 glavnog loop() dijela programa, ali je vremenski vođena preko millis(), 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.

  2. 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 koristi delay(), program u jednoj varijabli pamti kad je zadnji put pomaknut pad (npr. lastFallUpdate) i u svakom prolazu kroz loop() 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.

  3. Kontrola igrača (tipke lijevo/desno)
    U svakom prolazu kroz loop(), 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 s millis(), tako da jedan kratki pritisak ne preskoči više polja odjednom.

  4. 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 istog millis() mehanizma.

  5. Glavna petlja i stroj stanja (state machine)
    Cijela igra je organizirana kao state machine: npr. STANJE_INTRO, STANJE_IGRA, STANJE_GAME_OVER.
    ​U loop() funkciji se ne koristi veliki while s blokiranjem, nego se svaki put provjeri trenutno stanje igre i pozove odgovarajuća funkcija:

    • u STANJE_INTRO se odrađuje animacija i čeka početak,
    • u STANJE_IGRA se paralelno brinu o padanju znaka, kretanju igrača i bodovima,
    • u STANJE_GAME_OVER se 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 na millis(), tako da nema dugih pauza i Arduino može istovremeno: animirati LCD, čitati tipke i reagirati na događaje u igri.

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.

ligtbulb Š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.
◀ 3.2. Metronom4. Logički sklopovi ▶