2026-03-03 13:44:53
諏訪子
embedded
gamedev
c++

【組み込み✕ゲーム】ArduinoでDINORUNを作ってみた

ゲーム開発者が組み込みプログラミングをやったらどうなるか?
マイコン上でベアメタルゲームが誕生!

あたしは1ヶ月の組み込みシステム講座に参加したのですが、驚く事に2週間で終わらせてしまいました。
其の後、先生方から更に難しい追加課題を出され、其れも2日で完了。
すると「好きに作っていいよ」と言われたので、ゲームを作る事にしました。
そして、たった1時間で完成させました。

DINORUN

写真
写真

Googleが正式に何と呼んでいるのかは分かりませんが、此のゲームはChromium系ブラウザでネット接続がない時に遊べるあのゲームを元にしています。
但し、いくつか制約があります。
先ず、カスタム文字は最大8個までで、其々5×8ピクセル。
同時に鳴らせる音も1つだけ。
更に、CPUは16MHzで動作する8ビットのAVRプロセッサ、フラッシュ32KiB、SRAM 2KiB、EEPROM 1KiB。
操作に使えるボタンは1つだけです。
勿論増やす事は可能ですが、今回はGrove Starter Kitを1つ渡されたので、付属のボタン1個を使いました。
ちょっとハックすればタッチセンサーで2ボタン化も出来ますが、DINORUNでは必要なかったのでやりませんでした。

最初は岩のランダム性を利用してプレイヤーが無限にジャンプ出来る様にしようと思っていました。
でも先生に遊んでもらったら「簡単すぎる、バグでは?」と言われたので修正しました。
又別の先生からは「delayは使うな、タイマーを使え」とアドバイスをもらったので、其れも実装。
其れ以外は「こんなの作れるんだ」とかなり驚かれ、感心されました。

動かし方

動かすには以下のものが必要です:

Arduino IDEをインストールし、プロジェクト全体をこちらから取得して、game.inoをArduino IDEで開きます。
ベースシールドをArduinoに接続後、LCDをI2Cに、ボタンをピン3、ブザーをピン7に接続して下さい。
こんな感じになります:
Arduino

後はコードをArduinoに書き込めば、直ぐにゲームが遊べます。
コインを取るとスコアが加算され、岩に当たるとゲームオーバーです。

コードの解説

此処ではゲームの「シーン」を管理しています。

typedef enum {
  Start, Play, Dead
} GameState;

volatile GameState state = Start;

此処ではカスタムスプライトを作成しています。
前述の通りフォントは5×8ピクセル限定で、スプライトも実質フォント文字です。

byte dino[8] = {
  0b00100,
  0b01110,
  0b00110,
  0b01100,
  0b01110,
  0b11100,
  0b01100,
  0b10100
};

byte rock[8] = {
  0b00010,
  0b00110,
  0b01110,
  0b00111,
  0b01110,
  0b01110,
  0b11110,
  0b11111
};

byte coin[8] = {
  0b00100,
  0b01110,
  0b01110,
  0b11011,
  0b11011,
  0b01110,
  0b01110,
  0b00100
};

char dinoChar = byte(7);
char rockChar = byte(6);
char coinChar = byte(5);

こちらは初期設定部分です。
各ピンの割り当て、恐竜の状態、ジャンプ、死亡判定、ゲーム速度などを此処で初期化。
最初の数個の岩とコインは固定位置で配置し、残りはゲームループ中にランダム生成しています。

volatile int curCol, curRow;

const int pinButton = 3;
const int pinBuzzer = 7;
...
bool jump = false;
bool jumping = false;
bool died = false;
unsigned long jumpTime = 0;
const unsigned long jumpDuration = 350;

int score = 0;
int dinoY = 1;

const int initRocks[] = { 5, 12, 16, 19, 23, 27, 30, 34, 36, 39, 41, 44, 47, 49, 52 };
const int countRocks = sizeof(initRocks) / sizeof(initRocks[0]);
int rockX[countRocks];
const int initCoins[] = { 8, 17, 25, 29, 32, 40, 43, 48, 55};
const int countCoins = sizeof(initCoins) / sizeof(initCoins[0]);
int coinX[countCoins];

int gameSpeed = 200;
unsigned long lastMove = 0;

unsigned long deathShowStart = 0;
const unsigned long deathScreenMinTime = 2000;

volatile bool btnPressed = false;

void setup() {
  pinMode(pinBuzzer, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(pinButton), buttonHandler, RISING);
  lcd.begin(16, 2);
  lcd.createChar(7, dino);
  lcd.createChar(6, rock);
  lcd.createChar(5, coin);
  resetGame();
}

void buttonHandler() {
  btnPressed = true;
}

メインのゲームループをタイトル画面・ゲームプレイ・死亡画面に分けています。
恐竜以外はすべて左に流れ、最初の数個以降は岩とコインがランダムに出現。
ジャンプ・コイン取得・死亡時にブザー音を鳴らし、死亡時にはLCDを一時的に赤くしています

void resetGame() {
  score = 0;
  jump = false;
  jumping = false;
  jumpTime = 0;
  dinoY = 1;
  lastMove = millis();
  deathShowStart = 0;
  lcd.setRGB(0, 0, 255);

  for (int i = 0; i < countRocks; ++i) {
    rockX[i] = initRocks[i];
  }

  for (int i = 0; i < countCoins; ++i) {
    coinX[i] = initCoins[i];
  }
}

void titlescreen() {
  char buf[16];
  lcd.clear();
  lcd.setCursor(4, 0);
  lcd.print("DINO RUN");
  lcd.setCursor(0, 1);
  sprintf(buf, "%c    %c  %c   %c", dinoChar, rockChar, coinChar, rockChar);
  lcd.print(buf);

  if (btnPressed) {
    btnPressed = false;
    state = Play;
    resetGame();
  }
}

void gameplay() {
  unsigned long now = millis();

  if (btnPressed) {
    btnPressed = false;
    if (!jumping) {
      tone(pinBuzzer, 523, 100);
      jump = true;
      jumpTime = now;
    }
  }

  if (jump) {
    jumping = true;
    dinoY = 0;
    if (now - jumpTime >= jumpDuration) {
      jump = false;
      jumping = false;
      dinoY = 1;
    }
  }

  if (now - lastMove >= (unsigned long)gameSpeed) {
    lastMove = now;

    for (int i = 0; i < countRocks; ++i) {
      rockX[i]--;
      if (rockX[i] < -1) rockX[i] = 25 + random(12, 22);
    }

    for (int i = 0; i < countCoins; ++i) {
      coinX[i]--;
      if (coinX[i] < -1) coinX[i] = 32 + random(15, 30);
    }
  }

  lcd.clear();

  lcd.setCursor(3, 0);
  char scoreBuf[16];
  sprintf(scoreBuf, "SCORE: %3d", score);
  lcd.print(scoreBuf);

  char player[2];
  char obstacle[2];
  char collect[2];

  sprintf(player, "%c", dinoChar);
  sprintf(obstacle, "%c", rockChar);
  sprintf(collect, "%c", coinChar);

  lcd.setCursor(1, dinoY);
  lcd.print(player);

  bool hit = false;
  for (int i = 0; i < countRocks; ++i) {
    int x = rockX[i];
    if (x >= 0 && x < 16) {
      lcd.setCursor(x, 1);
      lcd.print(obstacle);
      if (dinoY == 1 && x == 1) {
        hit = true;
      }
    }
  }

  for (int i = 0; i < countCoins; ++i) {
    int x = coinX[i];
    if (x >= 0 && x < 16) {
      lcd.setCursor(x, 1);
      if (dinoY == 1 && x == 1) {
        tone(pinBuzzer, 880, 80);
        score++;
      }
      else lcd.print(collect);
    }
  }

  if (hit) {
    unsigned long n = millis();
    lcd.setRGB(255, 0, 0);
    tone(pinBuzzer, 698, 1000);

    if (n >= 2000) {
      state = Dead;
      deathShowStart = now;
    }
  }
}

void dead() {
  unsigned long now = millis();
  lcd.setRGB(0, 0, 255);

  lcd.clear();
  char buf[16];
  lcd.setCursor(3, 0);
  sprintf(buf, "SCORE: %3d", score);
  lcd.print(buf);
  lcd.setCursor(4, 1);
  lcd.print("GAMEOVER");
  if (now - deathShowStart < deathScreenMinTime) return;

  if (btnPressed) {
    btnPressed = false;
    state = Start;
    resetGame();
  }
}

void loop() {
  delay(200);
  switch (state) {
    case Start:
      titlescreen();
      break;
    case Play:
      gameplay();
      break;
    case Dead:
      dead();
      break;
  }
  delay(200);
}

まとめ

御覧の通り、Chromium系ブラウザで4GiB以上のRAMを食うあのゲームを、たった450バイト程度のArduinoで再現しました。
グラフィックは明らかに劣りますが、それ以外の面(音、演出など)ではむしろ上回っているとも言えます。

現代の基準では「原始的」とされるプラットフォームで、こんなシンプルなゲームを作るのはとても楽しかったです。
改めて「制約が創造性を生む」ということを実感しました。
今後はゲーム開発記事だけでなく、組み込み系の記事も此のブログに書いていこうと思います。
次回はすでに構想がある物で、STM32ボードを使ったかなり本格的なものになる予定です。

ソースコード

ダウンロード(ソースコード)
Microsoft Github
Codeberg

以上