現在のブログ
ゲーム開発ブログ (2025年~) Gamedev Blog (2025~)
レガシーブログ
テクノロジーブログ (2018~2024年) リリースノート (2023~2025年) MeatBSD (2024年)
【組み込み✕ゲーム】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 Uno R3
- ベースシールド、RGB LCD、ボタン、ブザー(SeeedStudio Grove Starter Kit v3に含まれています)
Arduino IDEをインストールし、プロジェクト全体をこちらから取得して、game.inoをArduino IDEで開きます。
ベースシールドをArduinoに接続後、LCDをI2Cに、ボタンをピン3、ブザーをピン7に接続して下さい。
こんな感じになります:
後はコードを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
以上