"Mini Console"

Video: Demo des Spiels "Ritoruninja" auf der "Mini Console"

In diesem Projekt wird eine sehr rudimentäre Spielekonsole entworfen, gebaut und mit einem Spiel programmiert. Die Herausforderung an diesem Setup ist, dass das LCD nicht allzuviele Möglichkeiten der Darstellung hat, d.h. das gesamte Spielfeld beschränkt sich auf 16×2 Zeichen. Außerdem können ca. 200 Zeichen dargestellt werden, die auf dem internen CGROM des LCD-Moduls fest eingespeichert sind. Darüber hinaus sind noch 8 selbst definierbare Zeichen möglich, hier muss man sich also auf eigene Sprites sehr beschränken.

Verwendete Bauteile

Hardware-Entwurf

Die Konsole benötigt eine Anzeige für das Spiel (hier: ein 16x2 LCD-Panel), einige Bedienelemente (hier: vier Mikrotaster) und einige zusätzliche Indikatoranzeigen, die auf dem LCD keinen Platz mehr haben (hier: fünf verschiedenfarbige LEDs).
Alles zusammen wird von einem Arduino (Uno/Nano) gesteuert, der auch für die Spiel-Logik verantwortlich ist.

Aufbau der Mini Console

Der Prototyp wird auf dem Breadboard bzw. mehreren Breadboards aufgebaut. Für mehr Details über das LCD-Panel kann man mehr auf der Seite "LCD-Anzeige" erfahren und über die genaue Funktionsweise der Taster-Anschlüssen auf "Viele Schalter - 1 Pin". Mehr Details zum Piezo-Element gibt es auf "Sound mit Piezo".

Schaltplan der Mini Console
Abb.: Schaltplan der Mini Console
Aufbau des Prototypen der Mini Console
Abb.: Aufbau des Prototypen der Mini Console

Das Spiel "Ritoruninja"

Anleitung

Der Spieler steuert seine Figur - den kleinen Ninja 忍者. Dieser läuft durch die dunklen Gänge der Burg Osaka in Japan. Sein Ziel ist es, bis zum bösen und grausamen Herrscher Akuyaku 悪役 vorzudringen und einen Attentat auf ihn zu verüben. Nur der Ninja besitzt die Geschicklichkeit, die Wachen und Fallen der Burg zu überwinden.
Der Ninja läuft ununterbrochen durch die Gänge und muss über Hindernisse oder Feinde springen (Taste A). Er selbst kann Pfeile Shuriken werfen (Taste D), um damit Feinde zu erledigen.

Sketch

/**
 * How to play:
 * You little ninja is running through the corridors of Osaka castle to avoid
 * obstacles and kill enemies (dogs and guards). Try to run as far a possible!
 */

#include <LiquidCrystal.h>

#define PIN_LCD_D4   2
#define PIN_LCD_D5   3
#define PIN_LCD_D6   4
#define PIN_LCD_D7   5
#define PIN_PIEZO    6
#define PIN_LED1     7
#define PIN_LED2     8
#define PIN_LED3     9
#define PIN_LED4    10
#define PIN_LCD_EN  11
#define PIN_LCD_RS  12
#define PIN_LED5    13
#define PIN_BUTTONS A0

#define LCD_WIDTH  16
#define LCD_HEIGHT  2

#define GAME_PHASE_INIT    0
#define GAME_PHASE_END     1
#define GAME_PHASE_RUNNING 2

#define BUTTON_A 0
#define BUTTON_B 1
#define BUTTON_C 2
#define BUTTON_D 3

#define LED_PINK   0
#define LED_BLUE   1
#define LED_GREEN  2
#define LED_YELLOW 3
#define LED_RED    4

#define NINJA_1  0
#define NINJA_2  1
#define DOG      2
#define ENEMY    3
#define OBSTACLE 4

LiquidCrystal lcd(PIN_LCD_RS, PIN_LCD_EN, PIN_LCD_D4, PIN_LCD_D5, PIN_LCD_D6, PIN_LCD_D7);
bool buttonPressed[4] = {false, false, false, false}, isSound;
byte ledStatus[5] = {0, 0, 0, 0, 0};
byte ledPins[5] = {PIN_LED1, PIN_LED2, PIN_LED3, PIN_LED4, PIN_LED5};
byte gamePhase, ninjaFrame, ninjaPos, shootPos, levelData[LCD_WIDTH];

byte graphics[][8] = {
  {B01100, B01100, B00000, B01110, B11100, B01100, B11010, B10011}, // Ninja 1
  {B01100, B01100, B00000, B01100, B01100, B01100, B01100, B01110}, // Ninja 2
  {B00000, B00000, B00000, B01000, B11111, B00111, B00101, B00101}, // Dog
  {B01110, B01101, B00000, B10110, B11110, B00111, B01001, B11011}, // Enemy
  {B00000, B00000, B00000, B11111, B10111, B11011, B11101, B11111}, // Obstacle
};
int gameScore, gameSpeed;
unsigned long lastAction, lastJump, lastRelease;

void setup()
{
  lcd.begin(LCD_WIDTH, LCD_HEIGHT);

  pinMode(PIN_LED1, OUTPUT);
  pinMode(PIN_LED2, OUTPUT);
  pinMode(PIN_LED3, OUTPUT);
  pinMode(PIN_LED4, OUTPUT);
  pinMode(PIN_LED5, OUTPUT);
  pinMode(PIN_PIEZO, OUTPUT);
  pinMode(PIN_BUTTONS, INPUT);

  digitalWrite(PIN_LED1, LOW);
  digitalWrite(PIN_LED2, LOW);
  digitalWrite(PIN_LED3, LOW);
  digitalWrite(PIN_LED4, LOW);
  digitalWrite(PIN_LED5, LOW);

  lcd.createChar(NINJA_1, graphics[NINJA_1]);
  lcd.createChar(NINJA_2, graphics[NINJA_2]);
  lcd.createChar(DOG, graphics[DOG]);
  lcd.createChar(ENEMY, graphics[ENEMY]);
  lcd.createChar(OBSTACLE, graphics[OBSTACLE]);

  randomSeed(analogRead(A5));
  gamePhase = GAME_PHASE_INIT;
}

void loop()
{
  checkButtons();
  switch (gamePhase) {
    case GAME_PHASE_INIT:
      lcd.clear();
      lcd.print("Ritoruninja");
      tone(PIN_PIEZO, 440, 220); delay(220);
      tone(PIN_PIEZO, 440, 200); delay(220);
      tone(PIN_PIEZO, 660, 220); delay(220);
      tone(PIN_PIEZO, 440, 220); delay(220);
      tone(PIN_PIEZO, 660, 750); delay(750);
      lcd.setCursor(0, 1);
      lcd.print("- press A -");
      while (!buttonPressed[BUTTON_A]) {
        checkButtons();
      }
      lcd.clear();
      initGame();
      break;
    case GAME_PHASE_END:
      lcd.clear();
      lcd.print("Game over!");
      for (byte i = 0; i < 12; i++) {
        tone(PIN_PIEZO, 200 - i * 10, 100);
        delay(100);
      }
      lcd.setCursor(0, 1);
      lcd.print("Score: ");
      lcd.print(gameScore);
      delay(1500);
      while (!buttonPressed[BUTTON_A]) {
        checkButtons();
      }
      gamePhase = GAME_PHASE_INIT;
      break;
    case GAME_PHASE_RUNNING:
      jumpNinja();
      shootNinja();
      if ((millis() - lastAction) > gameSpeed) {
        updateLevel();
        updateNinja();
        lastAction = millis();
      }
      break;
  }
}

void initGame()
{
  gamePhase = GAME_PHASE_RUNNING;
  gameScore = 0;
  gameSpeed = 450;
  isSound = false;
  ninjaFrame = 0;
  ninjaPos = 1;
  shootPos = 0;
  lastAction = millis();
  lastJump = lastAction;
  for (byte i = 0; i < LCD_WIDTH; i++) {
    levelData[i] = 0;
  }
}

void checkButtons()
{
  int pinValue = analogRead(PIN_BUTTONS);
  for (byte i = 0; i < 4; i++) {
    buttonPressed[i] = false;
  }

  if (pinValue < 520 && pinValue > 500) { // 510
    buttonPressed[BUTTON_A] = true;
  } else if (pinValue < 350 && pinValue > 330) { //340
    buttonPressed[BUTTON_B] = true;
  } else if (pinValue < 265 && pinValue > 245) { // 255
    buttonPressed[BUTTON_C] = true;
  } else if (pinValue < 210 && pinValue > 190) { // 203
    buttonPressed[BUTTON_D] = true;
  }
}

bool toggleLed(byte ledNumber)
{
  ledStatus[ledNumber]++;
  ledStatus[ledNumber] %= 2;
  digitalWrite(ledPins[ledNumber], ledStatus[ledNumber]);
  if (ledStatus[ledNumber] == 1) {
    return true;
  }
  return false;
}

void jumpNinja()
{
  if ((millis() - lastJump) > 800) {
    if ((millis() - lastRelease > 500) && ninjaPos == 1 && buttonPressed[BUTTON_A] == true) {
      ninjaPos = 0;
      lastJump = millis();
    } else if (ninjaPos == 0) {
      ninjaPos = 1;
      lastRelease = millis();
    }
  }
}

void shootNinja()
{
  if (shootPos == 0 && ninjaPos == 1 && buttonPressed[BUTTON_B] == true) {
    shootPos = 3;
  }
}

void updateNinja()
{
  if (ninjaPos == 0) {
    if (levelData[2] == 0) {
      lcd.setCursor(2, 1);
      lcd.print(" ");
    }
    lcd.setCursor(2, ninjaPos);
    lcd.write((byte)0);
  } else {
    lcd.setCursor(2, 0);
    lcd.print(" ");
    lcd.setCursor(2, ninjaPos);
    lcd.write(ninjaFrame++);
    ninjaFrame %= 2;
  }

}

void updateLevel()
{
  if (shootPos > 0) {
    shootPos++;
    if (shootPos >= LCD_WIDTH) {
      Serial.println("shoot ends");
      shootPos = 0;
    }
    if (levelData[shootPos] == OBSTACLE || levelData[shootPos - 1] == OBSTACLE) {
      Serial.println("hit obstacle");
      shootPos = 0;
    }
    if (levelData[shootPos] == DOG) {
      Serial.println("killed dog");
      levelData[shootPos] = 0;
      shootPos = 0;
      gameScore += 2;
    }
    if (levelData[shootPos - 1] == DOG) {
      Serial.println("killed dog");
      levelData[shootPos - 1] = 0;
      shootPos = 0;
      gameScore += 2;
    }
    if (levelData[shootPos] == ENEMY) {
      Serial.println("killed enemy");
      levelData[shootPos] = 0;
      shootPos = 0;
      gameScore += 4;
    }
    if (levelData[shootPos - 1] == ENEMY) {
      Serial.println("killed ENEMY");
      levelData[shootPos - 1] = 0;
      shootPos = 0;
      gameScore += 4;
    }
  }

  for (byte i = 1; i < LCD_WIDTH; i++) {
    levelData[i - 1] = levelData[i];
  }

  // check ninja hit
  if (levelData[2] != 0) {
    if (ninjaPos == 1) {
      gamePhase = GAME_PHASE_END;
    } else {
      gameScore++;
    }
  }

  // generate new obstacle/enemy
  byte rnd = random(100);
  if (rnd < 5 && levelData[LCD_WIDTH - 2] == 0) {
    levelData[LCD_WIDTH - 1] = OBSTACLE;
  } else if (rnd < 7 && levelData[LCD_WIDTH - 2] == 0) {
    levelData[LCD_WIDTH - 1] = DOG;
  } else if (rnd < 9 && levelData[LCD_WIDTH - 2] == 0) {
    levelData[LCD_WIDTH - 1] = ENEMY;
  } else {
    levelData[LCD_WIDTH - 1] = 0;
  }

  lcd.setCursor(0, 1);
  for (byte i = 0; i < LCD_WIDTH; i++) {
    if (levelData[i] != 0) {
      lcd.write(levelData[i]);
    } else {
      lcd.print(" ");
    }
  }
  if (shootPos > 0) {
    lcd.setCursor(shootPos, 1);
    lcd.print((char)126);
  }

  if (gameSpeed > 80 && rnd < 50) {
    gameSpeed -= 3;
  }
}

Bedienelemente und Front-Ansicht

Bedienelemente der Mini Console
Bedienelemente der Mini Console [17kB]

Zusätzliche Bauteile für fertige Konsole

zurück