ガイド · 読了まで22分

Arduinoでゲームを作る:Pong、T-Rex、スネーク、サイモンの作り方

Arduinoのゲームプロジェクトをステップごとに解説します。OLEDのPongとT-Rex、LEDマトリクスのスネーク、サイモン、LCDのランナーゲーム、反応テスト、さらにEEPROMでハイスコアを保存するコードも紹介します。

Arduinoでのゲーム作りは、電子工作とプログラミングを学ぶいちばん楽しい方法です。OLEDディスプレイ、ボタン数個、ジョイスティックがあれば、Pong、T-Rexラン、スネークゲーム、記憶ゲームのサイモンといった定番ゲームを自分で作り、友だちにハイスコアを破れるか挑戦できます。ゲームを書いているうちに、プログラミングの大切なテーマもほとんど自然に身につきます。タイミング、ステートマシン、座標系、当たり判定の計算、乱数、電源を切っても消えないメモリです。これらのゲームはすべて1台のArduino Unoで動きます。メモリを上手に使えば、2 KBのRAMで十分です。

このガイドでは、6つのゲームを最初から最後まで作ります。OLEDディスプレイのPongとT-Rexラン、8x8と16x16のNeoPixel LEDマトリクスのスネーク、LED4個とボタン4個のサイモン、16x2 LCDのランナーゲーム、ミリ秒単位で測れる反応テストです。どのゲームにも部品リスト、配線、そのままコンパイルできるコードがあります。まず全ゲームに共通する骨組み、つまりゲームループ、当たり判定、スコア、EEPROMへのハイスコア保存を学び、それを各ゲームで使い回します。最後にはESP32版とRaspberry Pi Pico版も紹介します。

8x8 NeoPixel LEDマトリクスで動くArduinoのスネークゲーム
8x8 WS2812B NeoPixel LEDマトリクス:64個のカラーピクセルをデータ線1本で制御でき、スネークゲームにぴったりの小さな画面になります。

どのゲームにどの部品が必要?

Arduinoでゲームを作るとき、最初に決めるのはディスプレイです。ディスプレイによって作れるゲームの種類が決まるからです。LEDが数個あるだけでもサイモンや反応テストは作れますし、128×64ピクセルのOLEDがあれば、動くグラフィックと物理演算を使うPongやT-Rexも作れます。下の表は、このガイドのゲームをやさしい順にまとめたものです。

ゲームディスプレイ入力そのほかの部品難易度
反応テストLED 1個+シリアルモニタボタン1個220 Ωの抵抗、ブザー(任意)簡単
記憶ゲームのサイモン色付きLED 4個ボタン4個220 Ωの抵抗4本、パッシブブザー簡単〜中級
LCDのランナーゲーム16x2 I2C LCDボタン1個パッシブブザー中級
Pong0.96インチSSD1306 OLEDジョイスティックまたは10 kΩのポテンショメーターパッシブブザー中級
T-Rexラン0.96インチSSD1306 OLEDボタン1個またはジョイスティックのボタンパッシブブザー中級
スネーク(8x8)8x8 NeoPixelマトリクスジョイスティック330 Ωの抵抗、1000 µFのコンデンサ中級〜上級
スネーク(16x16)16x16 NeoPixelマトリクスジョイスティック5 V 3〜4 Aの外部電源上級
Arduino Uno
Arduino Uno
0.96インチOLED(SSD1306)
0.96インチOLED(SSD1306)
ジョイスティック
ジョイスティック
16x2 I2C LCDディスプレイ
16x2 I2C LCDディスプレイ
ブザー
ブザー
押しボタン
押しボタン

共通の配線:1つの回路をゲーム機に

サイモン以外のすべてのゲームが同じ回路で動くように、ピンを共通にしました。OLEDとI2C LCDは同じ2本の線(SDAとSCL)を共有し、ジョイスティックはアナログ入力に、アクションボタンはD3に、ブザーはD8につなぎます。この「ゲーム機」を一度組んでしまえば、別のコードを書き込むだけでゲームを切り替えられます。ボードのピン配置の全体は「ピン配置」ツールで確認できます。

部品/ピンArduino UnoESP32Raspberry Pi Pico
OLEDまたはI2C LCD:SDAA4GPIO 21GP4
OLEDまたはI2C LCD:SCLA5GPIO 22GP5
ジョイスティックVRx(X軸)A0GPIO 34GP26
ジョイスティックVRy(Y軸)A1GPIO 35GP27
ジョイスティックSW(ボタン)D2GPIO 25GP15
アクションボタン(もう一方の足はGNDへ)D3GPIO 26GP14
パッシブブザー(+)D8GPIO 27GP16
NeoPixel DIN(330 Ω経由)D6GPIO 13GP22
ESP32とPicoでは、ジョイスティックとOLEDを3.3 Vから給電してください。5 Vで給電したジョイスティックはアナログピンに3.3 Vを超える電圧をかけ、ボードを壊すおそれがあります。5 VのI2C LCDモジュールをESP32につなぐ場合は、間にレベル変換モジュールを入れます。ボタンはすべて内部プルアップ抵抗を使います。片方の足をピンに、もう片方をGNDにつなぎ、押すとピンはLOWを読みます。

ゲームループの仕組み

どのビデオゲームも、1秒間に何十回も繰り返す4つのステップのループでできています。入力を読む(ボタン、ジョイスティック)、世界を更新する(位置、速度、スコア)、当たり判定を行う、画面を描くの4つです。ループの1周をフレームと呼びます。1秒間に30フレーム(30 FPS)あれば滑らかに見え、1フレームは約33 msになります。Arduinoのloop()関数はそれ自体がループですが、とても速く不規則に回るので、millis()で決まった長さのフレームに区切る必要があります。

ここではdelay()を使わないことが欠かせません。delay(33)と書くと、Arduinoはその33 msの間何もせず、短いボタン操作を見逃すことがあり、描画にかかる時間が変わるたびにゲームの速さも変わってしまいます。millis()を使えば、loop()は毎周入力を読みつつ、新しいフレームの時間になったときだけゲームを進めます。2つの違いはmillis()とdelay()の違いで詳しく説明しています。下の骨組みが、このガイドのすべてのゲームの土台です。

C++ (Arduino IDE)
1// Oyun döngüsü iskeleti / Game loop skeleton
2enum State { MENU, PLAYING, GAME_OVER };   // oyunun durumları / game states
3State state = MENU;
4
5const byte BUTTON_PIN = 3;                 // aksiyon butonu / action button
6const unsigned long FRAME_MS = 33;         // ~30 kare/sn / ~30 frames per second
7unsigned long lastFrame = 0;
8unsigned int score = 0;
9
10// Sıçramasız basış: yalnız basıldığı an true / bounce-free: true only at the moment of a press
11bool buttonPressed() {
12  static bool stable = HIGH, lastRead = HIGH;
13  static unsigned long changedAt = 0;
14  bool r = digitalRead(BUTTON_PIN);
15  if (r != lastRead) { lastRead = r; changedAt = millis(); }
16  if (millis() - changedAt > 15 && r != stable) {   // 15 ms sabit kaldı mı? / stable for 15 ms?
17    stable = r;
18    return stable == LOW;
19  }
20  return false;
21}
22
23void setup() {
24  pinMode(BUTTON_PIN, INPUT_PULLUP);       // basınca LOW / LOW when pressed
25  Serial.begin(9600);
26}
27
28void loop() {
29  bool pressed = buttonPressed();          // 1) giriş: her turda oku / input: read on every pass
30  if (pressed && state != PLAYING) {       // menüden ya da oyun sonundan başlat / start from menu or game over
31    score = 0;
32    state = PLAYING;
33  }
34  if (millis() - lastFrame < FRAME_MS) return;   // kare zamanı gelmedi / not time for a frame yet
35  lastFrame = millis();
36
37  if (state == PLAYING) {
38    score++;                               // 2) güncelle: konum, hız, puan / update: position, speed, score
39    if (score >= 300) state = GAME_OVER;   // 3) çarpışma ya da bitiş kontrolü / collision or end check
40    Serial.println(score);                 // 4) çiz: burada ekrana çizilir / draw: the screen is drawn here
41  }
42}

コードの2つ目の大事な考え方はステートマシンです。ゲームはどの瞬間も、MENU、PLAYING、GAME_OVERの3つの状態のどれかにあります。同じボタンでも、状態ごとに反応が違います。メニューでは押すとゲームが始まり、プレイ中はキャラクターがジャンプし、ゲームオーバーの後は新しいラウンドが始まります。このようにコードを分けると入れ子のifがなくなり、PAUSEのような新しい状態も簡単に追加できます。詳しくはArduinoのステートマシンで扱っています。

ボタンとジョイスティックの読み取り

ボタンはINPUT_PULLUPで配線するのがいちばんすっきりします。外付けの抵抗が不要で、ピンは何もしていないときにHIGH、押したときにLOWを読みます。ただし機械式のボタンは、押したときや離したときに数ミリ秒チャタリングし、Arduinoはそれを連続した複数回の押下として受け取ります。ゲームでは、1回押しただけで2回ジャンプしたり、サイモンが間違った色を数えたりする原因になります。骨組みのbuttonPressed()関数は、ピンの状態が15 ms変わらなかったときだけ新しい状態として受け入れ、押した瞬間に一度だけtrueを返します。回路やほかの方法については、Arduinoに押しボタンをつなぐ方法を参照してください。

ジョイスティックは2つのポテンショメーターと1つのボタンでできています。Unoでは各軸が0〜1023の値を返し、スティックを離したときの中央は約512ですが、モジュールによって約490〜530の範囲でばらつきます。そのため、マス目のゲームでは正確な値ではなくしきい値を使います。300未満なら左または上、700より大きければ右または下です。その間の広い範囲(不感帯)があるおかげで、手を離したときにキャラクターが勝手に動き出しません。一方Pongでは、アナログ値をそのままパドルに対応させます。map()で0〜1023の範囲を、パドルが動ける0〜48ピクセルの範囲に変換します。キャリブレーションと方向の表はジョイスティックモジュールのガイドにあります。

当たり判定の仕組み

当たり判定とは、2つの物体が同時に同じ場所にあるかどうかを調べることです。画面のあるゲームでいちばんよく使われるのはAABB(軸に平行な境界ボックス)のテストで、各物体を長方形として扱います。2つの長方形が重なるのは、4つの条件がすべて同時に成り立つときだけです。Aの左端がBの右端より左にあり、Aの右端がBの左端より右にあり、縦方向でも同じ2つの条件が成り立つことです。コードにするとax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > byの1行です。マス目のゲーム(スネーク、LCDのランナーゲーム)はさらに簡単で、2つのマスの座標が同じなら衝突です。

ゲーム何と何がぶつかる?方法
Pongボールと壁・パドルAABB+速度の反転
T-Rexラン恐竜とサボテンAABB(少し小さめのボックス)
スネーク頭と壁・体・エサマスの座標が同じかどうか
LCDのランナーゲームランナーとサボテン同じ列で同じ行
サイモン押した色と順番の中の色配列の要素の比較
反応テスト押した時刻と合図の時刻時刻の比較(フライング)
小さくて速く動く物体は、1フレームの間に障害物をすり抜けて、衝突をとばしてしまうことがあります。これをトンネリングと呼びます。Pongでは、ボールが1フレームに4ピクセルより多く動くと、幅3ピクセルのパドルの判定範囲に一度も触れずに通り過ぎてしまいます。対策は、物体の1フレームあたりの移動量を判定範囲の幅より小さく保つことです。PongのコードのMAX_SPEED = 2.8という上限は、まさにそのためにあります。

スコアとEEPROMへのハイスコア保存

スコアはRAMの変数に入っていて、Arduinoの電源を抜いた瞬間に消えます。ハイスコアを残すにはEEPROMを使います。Arduino UnoのATmega328Pには、電源がなくても内容を保持する1024バイトのEEPROMがあります。ハイスコア(0〜65535)なら、uint16_t型の2バイトで足ります。EEPROMの唯一の弱点は書き込み回数の寿命で、各セルに書き込めるのは約100,000回です。多く聞こえますが、毎フレーム、つまり1秒間に30回スコアを書き込むと、セルは約55分で寿命を迎えます。正しいのは、ゲームが終わって記録を破ったときだけ書き込む方法です。1日に10回新記録が出ても、セルは27年もちます。

C++ (Arduino IDE)
1#include <EEPROM.h>
2
3const int HISCORE_ADDR = 0;          // rekor EEPROM'un 0-1. baytında / high score in EEPROM bytes 0-1
4uint16_t hiScore = 0;
5
6uint16_t loadHiScore() {
7  uint16_t v;
8  EEPROM.get(HISCORE_ADDR, v);       // 2 bayt oku / read 2 bytes
9  if (v == 0xFFFF) v = 0;            // hiç yazılmamış EEPROM 0xFF okunur / blank EEPROM reads 0xFF
10  return v;
11}
12
13void saveIfRecord(uint16_t score) {
14  if (score > hiScore) {             // yalnız rekor kırılınca yaz / write only on a new record
15    hiScore = score;
16    EEPROM.put(HISCORE_ADDR, hiScore);   // put() yalnız değişen baytları yazar / put() writes only changed bytes
17  }
18}
19
20void setup() {
21  Serial.begin(9600);
22  hiScore = loadHiScore();
23  Serial.print(F("Rekor / High score: "));
24  Serial.println(hiScore);
25  saveIfRecord(120);                 // örnek: oyun 120 puanla bitti / example: a game ended with 120 points
26}
27
28void loop() {}

このコードには細かいポイントが2つあります。1つ目は、一度も書き込んだことのないEEPROMのバイトはすべて0xFFなので、最初の起動ではハイスコアが65535と表示されることです。0xFFFFのチェックでこれを0に直しています。反応テストでは逆で、時間が短いほうがよいので、0xFFFFは「まだ記録がない」という意味になります。2つ目は、EEPROM.put()が書き込む前に各バイトを読み、変わったバイトだけを書き込むことです。これで寿命がさらに延びます。EEPROMのほかの命令についてはArduinoのEEPROMの使い方を参照してください。このガイドのゲームを同じUnoに次々と書き込めるように、ゲームごとに別のアドレスを割り当てています。

ゲームEEPROMのアドレス保存する値
T-Rexラン0〜1最高スコア
スネーク2〜3食べたエサの最多数
LCDのランナーゲーム4〜5最高スコア
反応テスト6〜7最短時間(ms)
サイモン8〜9到達した最高レベル

Pong:OLEDディスプレイとジョイスティック

Pongは1972年に発売され、ビデオゲームの時代を切り開いた卓球ゲームです。このバージョンでは、左のパドルをジョイスティックのY軸(またはA1につないだポテンショメーター)で操作し、右のパドルはArduinoが操作します。部品はArduino Uno、0.96インチのSSD1306 I2C OLED、ジョイスティック、パッシブブザーです。ライブラリマネージャでAdafruit SSD1306ライブラリをインストールするときは、依存ライブラリのAdafruit GFXもインストールしてください。ディスプレイの配線と描画の命令はOLEDディスプレイ(SSD1306)で詳しく説明しています。

C++ (Arduino IDE)
1#include <Wire.h>
2#include <Adafruit_GFX.h>
3#include <Adafruit_SSD1306.h>
4
5Adafruit_SSD1306 display(128, 64, &Wire, -1);
6
7const byte JOY_Y_PIN  = A1;          // joystick VRy ya da potansiyometre / joystick VRy or potentiometer
8const byte BUZZER_PIN = 8;           // pasif buzzer / passive buzzer
9const byte PADDLE_H = 16, PADDLE_W = 3;
10const byte WIN_SCORE = 5;            // 5 sayı alan kazanır / first to 5 wins
11const float MAX_SPEED = 2.8;         // raketten kalın olmasın / keep below paddle thickness + ball size
12const unsigned long FRAME_MS = 30;   // ~33 kare/sn / ~33 frames per second
13
14float ballX, ballY, velX, velY;
15float playerY = 24, cpuY = 24;
16byte playerScore = 0, cpuScore = 0;
17unsigned long lastFrame = 0;
18
19void serve(int dir) {                // topu ortadan at / serve from the center
20  ballX = 63; ballY = 31;
21  velX = 1.8 * dir;
22  velY = random(-10, 11) / 10.0;     // -1,0 ile 1,0 arası / between -1.0 and 1.0
23}
24
25// Topun rakete çarptığı nokta yeni açıyı belirler / where the ball hits the paddle sets the new angle
26void hitPaddle(float paddleY) {
27  float offset = (ballY + 1 - (paddleY + PADDLE_H / 2.0)) / (PADDLE_H / 2.0);  // -1 üst, +1 alt / -1 top, +1 bottom
28  velX = constrain(-velX * 1.05, -MAX_SPEED, MAX_SPEED);   // yön değiştir, %5 hızlan / reverse, 5% faster
29  velY = offset * 1.5;
30  tone(BUZZER_PIN, 880, 15);
31}
32
33void setup() {
34  Serial.begin(9600);
35  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
36    Serial.println(F("SSD1306 allocation failed"));
37    for (;;);                        // ekran yoksa dur / halt without a display
38  }
39  display.setTextColor(SSD1306_WHITE);
40  randomSeed(analogRead(A3));        // boştaki analog pin / a floating analog pin
41  serve(1);
42}
43
44void loop() {
45  if (millis() - lastFrame < FRAME_MS) return;
46  lastFrame = millis();
47
48  // 1) Giriş: oyuncu raketi / Input: player paddle
49  playerY = map(analogRead(JOY_Y_PIN), 0, 1023, 0, 64 - PADDLE_H);
50
51  // 2) Basit yapay zekâ: CPU topu sınırlı hızla izler / Simple AI: the CPU follows the ball at limited speed
52  float target = ballY - PADDLE_H / 2.0;
53  cpuY += constrain(target - cpuY, -1.2, 1.2);
54  cpuY = constrain(cpuY, 0, 64 - PADDLE_H);
55
56  // 3) Topu hareket ettir / Move the ball
57  ballX += velX;
58  ballY += velY;
59
60  // 4) Çarpışmalar / Collisions
61  if (ballY <= 0 || ballY >= 62) {   // üst ve alt duvar / top and bottom walls
62    velY = -velY;
63    ballY = constrain(ballY, 0, 62);
64  }
65  if (velX < 0 && ballX <= 4 && ballX > 0 && ballY + 2 >= playerY && ballY <= playerY + PADDLE_H) {
66    ballX = 4;
67    hitPaddle(playerY);
68  }
69  if (velX > 0 && ballX >= 122 && ballX < 126 && ballY + 2 >= cpuY && ballY <= cpuY + PADDLE_H) {
70    ballX = 122;
71    hitPaddle(cpuY);
72  }
73
74  // 5) Sayı / Point
75  if (ballX <= 0)   { cpuScore++;    tone(BUZZER_PIN, 220, 200); serve(1); }
76  if (ballX >= 126) { playerScore++; tone(BUZZER_PIN, 660, 200); serve(-1); }
77
78  // 6) Çiz / Draw
79  display.clearDisplay();
80  for (byte y = 0; y < 64; y += 6) display.drawFastVLine(63, y, 3, SSD1306_WHITE);  // orta çizgi / center line
81  display.fillRect(1, (int)playerY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
82  display.fillRect(124, (int)cpuY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
83  display.fillRect((int)ballX, (int)ballY, 2, 2, SSD1306_WHITE);
84  display.setTextSize(1);
85  display.setCursor(52, 0); display.print(playerScore);
86  display.setCursor(70, 0); display.print(cpuScore);
87  display.display();
88
89  // 7) Oyun bitti mi? / Game over?
90  if (playerScore >= WIN_SCORE || cpuScore >= WIN_SCORE) {
91    display.setTextSize(2);
92    if (playerScore >= WIN_SCORE) { display.setCursor(22, 24); display.print(F("YOU WIN")); }
93    else                          { display.setCursor(10, 24); display.print(F("GAME OVER")); }
94    display.display();
95    delay(3000);
96    playerScore = cpuScore = 0;
97    serve(1);
98  }
99}

Pongを面白くしているのはhitPaddle()関数です。ボールがパドルの中央に当たるとほぼまっすぐ返り、端に当たると鋭い角度で跳ね返って相手を追い込みます。これは、パドルの中心からのボールの距離を-1〜+1の値にスケーリングし、縦方向の速度に変えることで実現しています。ボールは当たるたびに5%速くなり、MAX_SPEEDで上限が決まります。Arduinoのパドルは1フレームに最大1.2ピクセルしか動けないので、ボールに鋭い角度をつければ追いつけず、得点できます。ゲームを難しくするにはこの値を1.5に上げます。2人で遊ぶ場合は、CPUの行を削除し、2つ目のパドルをA2のポテンショメーターで操作します。

なぜフレーム時間が30 msなのでしょうか。UnoはOLEDに1024バイトのフレームバッファを400 kHzのI2Cで送るのに約25 msかかるため、ディスプレイは1秒間に35〜40フレームより多くは表示できません。フレーム時間をこの転送時間より少しだけ長くしておくと、ゲームの速さが安定します。また、コンパイラの「Global variables use 28%」(グローバル変数が28%を使用)という表示にだまされないでください。SSD1306のバッファは実行時に確保されるので、実際のRAM使用量は約78%まで上がります。メモリが足りなくなるとSSD1306 allocation failed(メモリ確保の失敗)というエラーが出ます。対策はArduinoのメモリ不足にまとめました。

T-Rexラン:ジャンプの物理とハイスコア

オフラインのときにChromeが表示する恐竜ゲームを、OLEDディスプレイで再現します。恐竜はその場で走り続け、サボテンが右から左へ流れてきて、ボタンを押すとジャンプして飛び越えます。ジャンプをリアルに見せるために、簡単な物理を使います。ボタンを押すと縦方向の速度が1フレームあたり-4.2ピクセルになり、毎フレーム重力としてその速度に0.3を足し、位置は速度の分だけ変わります。こうすると恐竜は減速しながら上がり、頂点で一瞬止まり、どんどん速く落ちていきます。上昇するのは約27ピクセルで、空中にいるのは27フレーム、約0.8秒です。部品はPongと同じで、ジャンプにはD3のボタンかジョイスティックのボタンを使います。

C++ (Arduino IDE)
1#include <Wire.h>
2#include <EEPROM.h>
3#include <Adafruit_GFX.h>
4#include <Adafruit_SSD1306.h>
5
6Adafruit_SSD1306 display(128, 64, &Wire, -1);
7
8const byte JUMP_PIN     = 3;         // aksiyon butonu / action button
9const byte JOY_SW_PIN   = 2;         // joystick butonu da zıplatır / the joystick button jumps too
10const byte BUZZER_PIN   = 8;
11const int  HISCORE_ADDR = 0;         // EEPROM 0-1. bayt / EEPROM bytes 0-1
12const byte GROUND_Y = 56;            // zemin çizgisi / ground line
13const byte DINO_X = 10, DINO_W = 14, DINO_H = 14;
14const float JUMP_V  = -4.2;          // zıplama hızı (piksel/kare) / jump speed (px/frame)
15const float GRAVITY = 0.3;           // yer çekimi (piksel/kare²) / gravity (px/frame²)
16const unsigned long FRAME_MS = 30;   // ~33 kare/sn / ~33 frames per second
17
18enum State { MENU, PLAYING, GAME_OVER };
19State state = MENU;
20
21struct Cactus { float x; byte w, h; };
22Cactus cacti[2];
23
24float dinoY, velY, speed;
25uint16_t score = 0, hiScore = 0;
26unsigned long lastFrame = 0;
27
28bool jumpHeld() {
29  return digitalRead(JUMP_PIN) == LOW || digitalRead(JOY_SW_PIN) == LOW;
30}
31
32uint16_t loadHiScore() {
33  uint16_t v;
34  EEPROM.get(HISCORE_ADDR, v);
35  return (v == 0xFFFF) ? 0 : v;      // boş EEPROM 0xFFFF okunur / blank EEPROM reads 0xFFFF
36}
37
38void placeCactus(byte i, float startX) {
39  cacti[i].x = startX;
40  cacti[i].w = random(4, 9);         // 4-8 piksel genişlik / 4-8 px wide
41  cacti[i].h = random(8, 17);        // 8-16 piksel yükseklik / 8-16 px tall
42}
43
44void resetGame() {
45  dinoY = GROUND_Y - DINO_H;
46  velY = 0;
47  speed = 2.0;
48  score = 0;
49  placeCactus(0, 140);
50  placeCactus(1, 140 + random(70, 140));
51}
52
53// İki dikdörtgen üst üste biniyor mu? (AABB) / Do two rectangles overlap? (AABB)
54bool overlap(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh) {
55  return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
56}
57
58void drawDino(int x, int y, bool legA) {
59  display.fillRect(x + 6, y, 8, 5, SSD1306_WHITE);                  // kafa / head
60  display.drawPixel(x + 9, y + 1, SSD1306_BLACK);                   // göz / eye
61  display.fillRect(x, y + 5, 10, 6, SSD1306_WHITE);                 // gövde / body
62  display.fillRect(x + 2, y + 11, 2, legA ? 3 : 1, SSD1306_WHITE);  // sol bacak / left leg
63  display.fillRect(x + 6, y + 11, 2, legA ? 1 : 3, SSD1306_WHITE);  // sağ bacak / right leg
64}
65
66void drawScene() {
67  display.clearDisplay();
68  display.drawFastHLine(0, GROUND_Y, 128, SSD1306_WHITE);
69  bool onGround = dinoY >= GROUND_Y - DINO_H;
70  drawDino(DINO_X, (int)dinoY, onGround ? (score / 4) % 2 : true);  // koşma animasyonu / running animation
71  for (byte i = 0; i < 2; i++) {
72    display.fillRect((int)cacti[i].x, GROUND_Y - cacti[i].h, cacti[i].w, cacti[i].h, SSD1306_WHITE);
73  }
74  display.setTextSize(1);
75  display.setCursor(0, 0);  display.print(score);
76  display.setCursor(80, 0); display.print(F("HI ")); display.print(hiScore);
77}
78
79void showMessage(const __FlashStringHelper *msg) {
80  display.fillRect(14, 18, 100, 28, SSD1306_BLACK);
81  display.drawRect(14, 18, 100, 28, SSD1306_WHITE);
82  display.setCursor(64 - strlen_P((const char *)msg) * 3, 28);   // ortala (karakter 6 piksel) / center (6 px per char)
83  display.print(msg);
84  display.display();
85}
86
87void gameOver() {
88  tone(BUZZER_PIN, 150, 300);
89  if (score > hiScore) {             // yeni rekor: yalnız şimdi yaz / new record: write only now
90    hiScore = score;
91    EEPROM.put(HISCORE_ADDR, hiScore);
92  }
93  drawScene();
94  showMessage(F("GAME OVER"));
95  delay(800);                        // basılı buton hemen yeniden başlatmasın / a held button must not restart at once
96  state = GAME_OVER;
97}
98
99void setup() {
100  pinMode(JUMP_PIN, INPUT_PULLUP);
101  pinMode(JOY_SW_PIN, INPUT_PULLUP);
102  Serial.begin(9600);
103  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
104    Serial.println(F("SSD1306 allocation failed"));
105    for (;;);
106  }
107  display.setTextColor(SSD1306_WHITE);
108  randomSeed(analogRead(A3));
109  hiScore = loadHiScore();
110  if (digitalRead(JUMP_PIN) == LOW) {    // açılışta basılıysa rekoru sıfırla / held at power-up: reset the record
111    hiScore = 0;
112    EEPROM.put(HISCORE_ADDR, hiScore);
113  }
114  resetGame();
115  drawScene();
116  showMessage(F("PRESS START"));
117}
118
119void loop() {
120  if (millis() - lastFrame < FRAME_MS) return;
121  lastFrame = millis();
122
123  if (state != PLAYING) {            // menü ya da oyun sonu: basınca başla / menu or game over: press to start
124    if (jumpHeld()) {
125      while (jumpHeld());            // bırakılmasını bekle / wait for release
126      resetGame();
127      state = PLAYING;
128    }
129    return;
130  }
131
132  // 1) Giriş ve fizik / Input and physics
133  bool onGround = dinoY >= GROUND_Y - DINO_H;
134  if (onGround && jumpHeld()) {
135    velY = JUMP_V;
136    tone(BUZZER_PIN, 1200, 20);
137  }
138  velY += GRAVITY;                   // her karede hız artar / velocity grows every frame
139  dinoY += velY;
140  if (dinoY > GROUND_Y - DINO_H) { dinoY = GROUND_Y - DINO_H; velY = 0; }   // yere indi / landed
141
142  // 2) Engeller ve çarpışma / Obstacles and collision
143  for (byte i = 0; i < 2; i++) {
144    cacti[i].x -= speed;
145    if (cacti[i].x < -(float)cacti[i].w) {            // ekrandan çıktı / left the screen
146      float otherX = cacti[1 - i].x;
147      placeCactus(i, max(128.0, otherX + random(70, 140)));
148    }
149    // biraz küçük çarpışma kutusu daha adil hissettirir / a slightly smaller hitbox feels fairer
150    if (overlap(DINO_X + 2, (int)dinoY + 1, DINO_W - 4, DINO_H - 2,
151                (int)cacti[i].x, GROUND_Y - cacti[i].h, cacti[i].w, cacti[i].h)) {
152      gameOver();
153      return;
154    }
155  }
156
157  // 3) Puan ve zorluk / Score and difficulty
158  score++;
159  if (score % 200 == 0 && speed < 5.0) speed += 0.25;   // yaklaşık 6 sn'de bir hızlan / speed up every ~6 s
160
161  // 4) Çiz / Draw
162  drawScene();
163  display.display();
164}
設定項目値効果
JUMP_V-4.2ジャンプの強さ。-5にすると恐竜は約40ピクセル上がり、画面の上端近くまで届く
GRAVITY0.3値を大きくすると、ジャンプが低く鋭くなる
speed(速度)2.0 → 5.0サボテンの速さ。200フレームごとに0.25ずつ上がる
random(70, 140)(ランダムな間隔)ピクセル2つのサボテンの間の距離。値を小さくするとゲームが難しくなる
当たり判定(ヒットボックス)10×12ピクセル描画した14×14の恐竜より小さいので、ぎりぎりでかわしたときはゲームオーバーにならない

ハイスコアは、ゲームが終わった瞬間に、記録を破ったときだけEEPROMに書き込まれ、右上にHIとして表示されます。D3のボタンを押したままボードの電源を入れると記録がリセットされるので、友だちと新しい大会を始めるときに便利です。サボテンの高さと幅はrandom()で決めます。毎回違う形でゲームが始まるように、randomSeed(analogRead(A3))の行で、何もつないでいないアナログピンのノイズを乱数の種にしています。

LEDマトリクスでスネーク(8x8と16x16)

昔のNokiaの携帯電話で有名になったスネークゲームは、LEDマトリクスで遊ぶのがいちばんです。LED1個がそのままゲームの1マスになるからです。ここではカラーのWS2812B NeoPixelマトリクスを使います。8x8版は64ピクセル、16x16版は256ピクセルで、どちらもデータ線1本で制御します。ヘビの体は2つの配列(snakeXとsnakeY)に保存し、0番目の要素が頭です。1ステップごとに体が1マスずつずれ、頭に新しい位置が書き込まれます。エサを食べたときは尻尾を消さないので、ヘビが1マス伸びます。コードをコンパイルするにはAdafruit NeoPixelライブラリをインストールしてください。配線の詳細はWS2812B NeoPixelのガイドにあります。

C++ (Arduino IDE)
1#include <Adafruit_NeoPixel.h>
2#include <EEPROM.h>
3
4const byte W = 8, H = 8;             // 16x16 matris için 16 yap / set to 16 for a 16x16 matrix
5const bool SERPENTINE = false;       // zikzak dizilimli panelde true / true for zigzag-wired panels
6const byte DATA_PIN = 6;             // DIN, 330 Ω üzerinden / DIN through 330 Ω
7const byte JOY_X = A0, JOY_Y = A1, JOY_SW = 2;
8const byte BUZZER_PIN = 8;
9const int  HISCORE_ADDR = 2;         // EEPROM 2-3. bayt / EEPROM bytes 2-3
10
11Adafruit_NeoPixel matrix(W * H, DATA_PIN, NEO_GRB + NEO_KHZ800);
12
13byte snakeX[W * H], snakeY[W * H];   // [0] baş, sonrası gövde / [0] head, then the body
14int snakeLen;
15int8_t dirX, dirY, nextDX, nextDY;
16byte foodX, foodY;
17unsigned int stepMs;
18unsigned long lastStep = 0;
19uint16_t hiScore;
20bool alive = false;
21
22// (x, y) → LED sırası / (x, y) → LED index
23uint16_t xyToIndex(byte x, byte y) {
24  if (SERPENTINE && (y & 1)) return y * W + (W - 1 - x);   // tek satırlar ters yönde / odd rows run backwards
25  return y * W + x;
26}
27
28bool onSnake(byte x, byte y) {
29  for (int i = 0; i < snakeLen; i++) {
30    if (snakeX[i] == x && snakeY[i] == y) return true;
31  }
32  return false;
33}
34
35void placeFood() {
36  do {                               // yem yılanın üstüne düşmesin / food must not land on the snake
37    foodX = random(W);
38    foodY = random(H);
39  } while (onSnake(foodX, foodY));
40}
41
42void resetGame() {
43  snakeLen = 3;
44  for (int i = 0; i < snakeLen; i++) { snakeX[i] = 3 - i; snakeY[i] = H / 2; }
45  dirX = nextDX = 1;                 // sağa doğru başla / start moving right
46  dirY = nextDY = 0;
47  stepMs = 300;
48  placeFood();
49  alive = true;
50}
51
52void readJoystick() {
53  int x = analogRead(JOY_X), y = analogRead(JOY_Y);
54  // Geri dönüş yasak: kontrol şimdiki yöne göre / no reversing: check against the current direction
55  if      (x < 300 && dirX != 1)  { nextDX = -1; nextDY = 0; }
56  else if (x > 700 && dirX != -1) { nextDX = 1;  nextDY = 0; }
57  else if (y < 300 && dirY != 1)  { nextDX = 0;  nextDY = -1; }
58  else if (y > 700 && dirY != -1) { nextDX = 0;  nextDY = 1; }
59}
60
61void draw() {
62  matrix.clear();
63  matrix.setPixelColor(xyToIndex(foodX, foodY), matrix.Color(255, 0, 0));      // yem kırmızı / food red
64  for (int i = snakeLen - 1; i >= 0; i--) {
65    uint32_t c = (i == 0) ? matrix.Color(255, 200, 0) : matrix.Color(0, 160, 0); // baş sarı / head yellow
66    matrix.setPixelColor(xyToIndex(snakeX[i], snakeY[i]), c);
67  }
68  matrix.show();
69}
70
71void gameOver() {
72  alive = false;
73  uint16_t score = snakeLen - 3;     // yenen yem sayısı / food eaten
74  tone(BUZZER_PIN, 150, 400);
75  if (score > hiScore) { hiScore = score; EEPROM.put(HISCORE_ADDR, hiScore); }
76  for (byte k = 0; k < 3; k++) {     // kırmızı yanıp sön / flash red
77    matrix.fill(matrix.Color(80, 0, 0)); matrix.show(); delay(200);
78    matrix.clear();                      matrix.show(); delay(200);
79  }
80  for (uint16_t i = 0; i < score && i < W * H; i++) matrix.setPixelColor(i, matrix.Color(0, 0, 120));  // puan kadar LED / one LED per point
81  matrix.show();
82  Serial.print(F("Skor / Score: "));  Serial.print(score);
83  Serial.print(F("  Rekor / Best: ")); Serial.println(hiScore);
84}
85
86void step() {
87  dirX = nextDX; dirY = nextDY;
88  int nx = snakeX[0] + dirX, ny = snakeY[0] + dirY;
89  if (nx < 0 || nx >= W || ny < 0 || ny >= H) { gameOver(); return; }   // duvar / wall
90
91  bool eat = (nx == foodX && ny == foodY);
92  // Yem yoksa kuyruk ucu boşalacağı için ona çarpmak sayılmaz / without food the tail tip moves away, so it is not a hit
93  int checkLen = eat ? snakeLen : snakeLen - 1;
94  for (int i = 0; i < checkLen; i++) {
95    if (snakeX[i] == nx && snakeY[i] == ny) { gameOver(); return; }    // kendine çarptı / hit itself
96  }
97
98  if (eat) snakeLen++;
99  for (int i = snakeLen - 1; i > 0; i--) {      // gövdeyi bir hücre kaydır / shift the body one cell
100    snakeX[i] = snakeX[i - 1];
101    snakeY[i] = snakeY[i - 1];
102  }
103  snakeX[0] = nx; snakeY[0] = ny;
104
105  if (eat) {
106    tone(BUZZER_PIN, 1000, 40);
107    if (stepMs > 120) stepMs -= 10;             // her yemde hızlan / faster after every food
108    if (snakeLen == W * H) { gameOver(); return; }   // tahta doldu: kazandın / board full: you win
109    placeFood();
110  }
111}
112
113void setup() {
114  pinMode(JOY_SW, INPUT_PULLUP);
115  Serial.begin(9600);
116  matrix.begin();
117  matrix.setBrightness(30);          // düşük parlaklık = düşük akım / low brightness = low current
118  randomSeed(analogRead(A3));
119  EEPROM.get(HISCORE_ADDR, hiScore);
120  if (hiScore == 0xFFFF) hiScore = 0;
121  resetGame();
122  draw();
123}
124
125void loop() {
126  if (!alive) {                      // oyun bitti: joystick butonuyla yeniden / game over: restart with the joystick button
127    if (digitalRead(JOY_SW) == LOW) { resetGame(); draw(); delay(300); }
128    return;
129  }
130  readJoystick();                    // her turda oku, hiçbir hareketi kaçırma / read on every pass, never miss a move
131  if (millis() - lastStep >= stepMs) {
132    lastStep = millis();
133    step();
134    if (alive) draw();
135  }
136}

コードには大事なルールが3つあります。逆走の禁止:右へ進んでいるヘビが急に左を向くと、自分の首にぶつかります。readJoystick()は新しい方向を、予約中の方向ではなくヘビの現在の方向と比べるので、すばやく2回操作しても反転できません。尻尾の例外:エサを食べないステップでは尻尾の先が動いて空くので、そのマスに頭を進めても衝突にはなりません。エサの配置:placeFood()は、エサがヘビの上に来なくなるまで配置をやり直します。エサを1つ食べるごとにステップ時間が10 msずつ短くなります(300 msから120 msまで)。ゲームが終わると食べたエサの数だけLEDが青く光り、記録はEEPROMに保存されます。

マトリクスのLEDがどう配線されているかも重要です。硬い基板の8x8マトリクスの多くは、LEDが行ごとに同じ方向に並んでいます(SERPENTINE = false)。柔らかい16x16パネルは、たいていジグザグに配線されています。ある行は左から右、次の行は右から左です。ヘビが斜めに跳んだり、1行おきに逆向きに進んだりする場合は、SERPENTINEをtrueにします。方向が逆になる場合は、ジョイスティックを回転させるか、300と700のしきい値を入れ替えてください。16x16にするにはWとHを16にするだけです。このときヘビの配列は512バイト、NeoPixelのバッファは768バイトを使い、UnoのRAMの約75%が埋まります。

16x16 NeoPixel LEDマトリクス
16x16 NeoPixelマトリクス:256ピクセルのフィールドではヘビが長くなり、ゲームも難しくなります。5 Vの外部電源が必要です。
WS2812Bは1個で、白の最大輝度のとき約60 mAを消費します。16x16マトリクスの256個のLEDは、最悪の場合15 Aに達することがあり、USBでもArduinoの5Vピンでもまかなえません。マトリクスは5 V 3〜4 Aの外部電源から給電し、電源のGNDをArduinoのGNDにつなぎ、データ線に330 Ωの抵抗を、電源入力に1000 µFのコンデンサを入れてください。コードのsetBrightness(30)で明るさを約12%に抑えています。スネークでは一度に光るLEDが少ないので、電流は数百mAに収まります。RAMの使用量が少ない単色の選択肢についてはMAX7219 LEDマトリクスを参照してください。

記憶ゲームのサイモン:LED4個とボタン4個

サイモンは1978年に発売された伝説的な記憶ゲームです。ラウンドごとに本体が順番に色を1つ追加し、プレイヤーはその順番全体を同じ順に繰り返します。部品は色付きLED4個(緑、赤、黄、青)、それぞれに220 Ωの抵抗、ボタン4個、パッシブブザーです。LEDはD9〜D12に、ボタンはD2〜D5に、ブザーはD8につなぎます。このゲームは共通のゲーム機とは別の回路です。LEDに合う抵抗はLED抵抗計算ツールで選べます。本物のサイモンの4つの音は、イ長調(Aメジャー)の和音の音です。コードでも同じ音(330、440、554、659 Hz)を使っているので、すばやく押しても心地よく聞こえます。

C++ (Arduino IDE)
1#include <EEPROM.h>
2
3const byte LED_PINS[4]    = {9, 10, 11, 12};       // yeşil, kırmızı, sarı, mavi / green, red, yellow, blue
4const byte BUTTON_PINS[4] = {2, 3, 4, 5};          // LED'lerle aynı sırada / same order as the LEDs
5const int  TONES[4]       = {330, 440, 554, 659};  // E4, A4, C#5, E5: A majör akoru / A major chord
6const byte BUZZER_PIN     = 8;
7const int  HISCORE_ADDR   = 8;                     // EEPROM 8-9. bayt / EEPROM bytes 8-9
8const byte MAX_LEVEL      = 50;
9
10byte sequence[MAX_LEVEL];
11byte level = 0;
12uint16_t hiScore;
13
14void playColor(byte c, int ms) {
15  digitalWrite(LED_PINS[c], HIGH);
16  tone(BUZZER_PIN, TONES[c]);
17  delay(ms);
18  digitalWrite(LED_PINS[c], LOW);
19  noTone(BUZZER_PIN);
20}
21
22// Basılan rengi döndürür; 3 sn içinde basılmazsa -1 / returns the pressed colour; -1 after 3 s
23int waitForButton() {
24  unsigned long start = millis();
25  while (millis() - start < 3000) {
26    for (byte i = 0; i < 4; i++) {
27      if (digitalRead(BUTTON_PINS[i]) == LOW) {
28        playColor(i, 200);                              // basılan rengi göster / show the pressed colour
29        while (digitalRead(BUTTON_PINS[i]) == LOW);     // bırakılmasını bekle / wait for release
30        delay(30);                                      // sıçramayı atla / skip the bounce
31        return i;
32      }
33    }
34  }
35  return -1;
36}
37
38// LED'ler döner; ilk basış oyunu başlatır ve rastgeleliği tohumlar
39// LEDs chase until the first press, which starts the game and seeds the randomness
40void waitForStart() {
41  byte i = 0;
42  while (true) {
43    digitalWrite(LED_PINS[i], HIGH);
44    for (byte t = 0; t < 20; t++) {                     // 200 ms boyunca butonlara bak / poll the buttons for 200 ms
45      for (byte b = 0; b < 4; b++) {
46        if (digitalRead(BUTTON_PINS[b]) == LOW) {
47          digitalWrite(LED_PINS[i], LOW);
48          randomSeed(micros());                         // basış anı her seferinde farklı / the press time always differs
49          while (digitalRead(BUTTON_PINS[b]) == LOW);
50          delay(600);
51          return;
52        }
53      }
54      delay(10);
55    }
56    digitalWrite(LED_PINS[i], LOW);
57    i = (i + 1) % 4;
58  }
59}
60
61void endGame(uint16_t score) {
62  tone(BUZZER_PIN, 110, 700);
63  for (byte k = 0; k < 3; k++) {                        // tüm LED'ler yanıp söner / all LEDs flash
64    for (byte i = 0; i < 4; i++) digitalWrite(LED_PINS[i], HIGH);
65    delay(200);
66    for (byte i = 0; i < 4; i++) digitalWrite(LED_PINS[i], LOW);
67    delay(200);
68  }
69  if (score > hiScore) { hiScore = score; EEPROM.put(HISCORE_ADDR, hiScore); }
70  Serial.print(F("Seviye / Level: "));  Serial.print(score);
71  Serial.print(F("  Rekor / Best: ")); Serial.println(hiScore);
72  level = 0;
73  delay(1000);
74}
75
76void setup() {
77  for (byte i = 0; i < 4; i++) {
78    pinMode(LED_PINS[i], OUTPUT);
79    pinMode(BUTTON_PINS[i], INPUT_PULLUP);              // basınca LOW / LOW when pressed
80  }
81  Serial.begin(9600);
82  EEPROM.get(HISCORE_ADDR, hiScore);
83  if (hiScore == 0xFFFF) hiScore = 0;
84}
85
86void loop() {
87  if (level == 0) waitForStart();
88  if (level == MAX_LEVEL) { endGame(MAX_LEVEL); return; }   // hepsini bildin / you completed every level
89
90  sequence[level++] = random(4);                       // diziye yeni bir renk ekle / append a new colour
91  int showMs = max(180, 520 - level * 20);             // seviye arttıkça hızlanır / faster at higher levels
92  for (byte i = 0; i < level; i++) {                   // diziyi baştan göster / replay the whole sequence
93    playColor(sequence[i], showMs);
94    delay(showMs / 2);
95  }
96  for (byte i = 0; i < level; i++) {                   // oyuncu aynı sırayı tekrarlar / the player repeats it
97    if (waitForButton() != sequence[i]) {              // yanlış ya da geç / wrong or too late
98      endGame(level - 1);
99      return;
100    }
101  }
102  delay(700);                                          // tur arası / pause between rounds
103}

サイモンでは、ランダムさがすべてです。毎回同じ順番なら、プレイヤーは覚えてしまうだけです。何もつないでいないピンを読むrandomSeed(analogRead(A0))の方法は、ピンが似た値を読むことが多いため、実際には弱い方法です。このコードでは、より確実な工夫をしています。ゲームはプレイヤーが最初にボタンを押すのを待ち、その瞬間のmicros()の値を乱数の種にします。押す瞬間をマイクロ秒単位で再現できる人はいないので、毎回違う形でゲームが始まります。表示時間はレベルが上がるごとに20 msずつ短くなり、180 msで落ち着きます。3秒以内に押さなかったり違う色を押したりするとゲームが終わり、到達したレベルが記録ならEEPROMに保存されます。

LCDのランナーゲーム:16x2ディスプレイのカスタム文字

16x2のキャラクターLCDにはピクセル単位で描画できませんが、createChar()を使えば5×8ピクセルのカスタム文字を最大8個まで定義できます。ランナーゲームには3つで足ります。走るポーズ2つとサボテンです。ディスプレイの下の行が地面で、サボテンが右から左へ流れ、ボタンを押すとランナーが上の行に移って数ステップの間空中にとどまります。このゲームはマス目ベースなので、当たり判定は1行で済みます。ランナーが地面にいるときに同じ列にサボテンがあれば、ゲームオーバーです。ライブラリと配線についてはI2C LCDディスプレイを参照してください。

C++ (Arduino IDE)
1#include <Wire.h>
2#include <EEPROM.h>
3#include <LiquidCrystal_I2C.h>
4
5LiquidCrystal_I2C lcd(0x27, 16, 2);  // adres 0x3F de olabilir / the address may also be 0x3F
6
7const byte JUMP_PIN = 3, BUZZER_PIN = 8;
8const int  HISCORE_ADDR = 4;         // EEPROM 4-5. bayt / EEPROM bytes 4-5
9const byte PLAYER_COL = 1;           // koşucunun sütunu / runner's column
10const byte JUMP_STEPS = 4;           // zıplama süresi (adım) / jump length (steps)
11const byte MIN_GAP = 4;              // kaktüsler arası en az boşluk / minimum gap between cacti
12
13// 5x8 özel karakterler / 5x8 custom characters
14byte runA[8]   = {0b01110, 0b01110, 0b00100, 0b01110, 0b10101, 0b00100, 0b01010, 0b10001};
15byte runB[8]   = {0b01110, 0b01110, 0b00100, 0b01110, 0b10101, 0b00100, 0b01010, 0b01010};
16byte cactus[8] = {0b00100, 0b00101, 0b10101, 0b10101, 0b10110, 0b01100, 0b00100, 0b00100};
17
18byte ground[16];                     // 1 = o sütunda kaktüs var / 1 = cactus in that column
19byte gap = 0, jumpLeft = 0;
20uint16_t score = 0, hiScore = 0;
21unsigned int stepMs = 300;
22unsigned long lastStep = 0;
23bool alive = false;
24
25// Sıçramasız basış: yalnız basıldığı an true / bounce-free: true only at the moment of a press
26bool buttonPressed() {
27  static bool stable = HIGH, lastRead = HIGH;
28  static unsigned long changedAt = 0;
29  bool r = digitalRead(JUMP_PIN);
30  if (r != lastRead) { lastRead = r; changedAt = millis(); }
31  if (millis() - changedAt > 15 && r != stable) {
32    stable = r;
33    return stable == LOW;
34  }
35  return false;
36}
37
38void draw() {
39  byte row = jumpLeft > 0 ? 0 : 1;   // havadaysa üst satır / top row while jumping
40  lcd.setCursor(0, 0);
41  for (byte c = 0; c < 11; c++) lcd.write((uint8_t)(row == 0 && c == PLAYER_COL ? 0 : ' '));
42  char buf[6];
43  sprintf(buf, "%5u", score);        // sağa yaslı skor / right-aligned score
44  lcd.print(buf);
45  lcd.setCursor(0, 1);
46  for (byte c = 0; c < 16; c++) {
47    if (row == 1 && c == PLAYER_COL) lcd.write((uint8_t)(score & 1));   // runA / runB sırayla / alternately
48    else lcd.write((uint8_t)(ground[c] ? 2 : ' '));
49  }
50}
51
52void startGame() {
53  memset(ground, 0, sizeof(ground));
54  gap = 0; jumpLeft = 0; score = 0; stepMs = 300;
55  alive = true;
56  lastStep = millis();
57  draw();
58}
59
60void die() {
61  alive = false;
62  tone(BUZZER_PIN, 150, 400);
63  if (score > hiScore) { hiScore = score; EEPROM.put(HISCORE_ADDR, hiScore); }
64  lcd.clear();
65  lcd.print(F("GAME OVER ")); lcd.print(score);
66  lcd.setCursor(0, 1);
67  lcd.print(F("BEST ")); lcd.print(hiScore);
68}
69
70void setup() {
71  pinMode(JUMP_PIN, INPUT_PULLUP);
72  lcd.init();
73  lcd.backlight();
74  lcd.createChar(0, runA);
75  lcd.createChar(1, runB);
76  lcd.createChar(2, cactus);
77  EEPROM.get(HISCORE_ADDR, hiScore);
78  if (hiScore == 0xFFFF) hiScore = 0;
79  randomSeed(analogRead(A3));
80  lcd.setCursor(0, 0); lcd.print(F("LCD RUNNER"));
81  lcd.setCursor(0, 1); lcd.print(F("BEST ")); lcd.print(hiScore);
82}
83
84void loop() {
85  if (buttonPressed()) {
86    if (!alive) startGame();                       // başlat / start
87    else if (jumpLeft == 0) {                      // yerdeyse zıpla / jump only from the ground
88      jumpLeft = JUMP_STEPS;
89      tone(BUZZER_PIN, 1200, 20);
90      draw();                                      // zıplamayı hemen göster / show the jump at once
91    }
92  }
93  if (!alive || millis() - lastStep < stepMs) return;
94  lastStep = millis();
95
96  for (byte c = 0; c < 15; c++) ground[c] = ground[c + 1];   // dünya sola kayar / the world scrolls left
97  if (gap >= MIN_GAP && random(100) < 35) { ground[15] = 1; gap = 0; }
98  else { ground[15] = 0; gap++; }
99
100  if (jumpLeft > 0) jumpLeft--;                              // havada geçen bir adım / one step in the air
101  if (jumpLeft == 0 && ground[PLAYER_COL]) { die(); return; } // yerdeyken kaktüse çarptı / hit a cactus on the ground
102
103  score++;
104  if (score % 25 == 0 && stepMs > 120) stepMs -= 15;         // giderek hızlan / speed up gradually
105  draw();
106}

世界をスクロールさせるには、地面の配列の各要素を1つずつ左にコピーします。いちばん右の列には35パーセントの確率で新しいサボテンが入ります。MIN_GAP = 4のルールで2つのサボテンの間に少なくとも4列の空きができるので、どの障害物も必ず越えられます。25点ごとにステップ時間が15 msずつ短くなり、300 msから120 msまで下がります。lcd.clear()で画面を消す代わりに、毎ステップ2行とも書き直すと、ちらつきを防げます。clear()命令だけで約2 msかかるからです。画面に四角いブロックしか表示されない場合は、モジュール裏のコントラスト調整用の半固定抵抗(トリマー)を回してください。何も表示されない場合は、アドレスを0x27から0x3Fに変えます。

反応テスト:反応時間をミリ秒で測る

いちばんシンプルで、いちばんやみつきになるゲームです。LEDがランダムなタイミングで点灯したら、できるだけ速くボタンを押します。Arduinoは経過時間をミリ秒単位でシリアルモニタに表示します。必要なのは、220 Ωの抵抗を付けてD7につないだLED、D3のボタン、任意のブザーだけです。待ち時間は2〜5秒の間でランダムに決まり、待っている間に押すと「早すぎ」の警告が出るので、勘で押してごまかすことはできません。時間はmicros()で測り、平均を計算し、最高記録をEEPROMに保存します。

C++ (Arduino IDE)
1#include <EEPROM.h>
2
3const byte LED_PIN = 7, BUTTON_PIN = 3, BUZZER_PIN = 8;
4const int  BEST_ADDR = 6;            // EEPROM 6-7. bayt / EEPROM bytes 6-7
5const bool USE_SOUND = false;        // true: LED ile birlikte bip / true: beep together with the LED
6
7uint16_t best;                       // en iyi süre (ms); 0xFFFF = kayıt yok / best time (ms); 0xFFFF = none
8unsigned long total = 0;
9unsigned int rounds = 0;
10
11bool pressed() { return digitalRead(BUTTON_PIN) == LOW; }
12
13void waitRelease() {
14  while (pressed());
15  delay(50);                         // bırakma sıçramasını atla / skip the release bounce
16}
17
18void setup() {
19  pinMode(LED_PIN, OUTPUT);
20  pinMode(BUTTON_PIN, INPUT_PULLUP);
21  Serial.begin(9600);
22  EEPROM.get(BEST_ADDR, best);       // boş EEPROM 0xFFFF = kayıt yok / blank EEPROM 0xFFFF = no record
23  Serial.println(F("Baslamak icin butona bas / Press the button to start"));
24}
25
26void loop() {
27  while (!pressed());                // başlatma basışı / start press
28  randomSeed(micros());
29  waitRelease();
30  Serial.println(F("Hazir ol... / Get ready..."));
31
32  unsigned long waitMs = random(2000, 5001);   // 2-5 sn rastgele bekleme / random 2-5 s wait
33  unsigned long start = millis();
34  while (millis() - start < waitMs) {
35    if (pressed()) {                 // erken basış / false start
36      tone(BUZZER_PIN, 150, 500);
37      Serial.println(F("Erken bastin! / Too early!"));
38      waitRelease();
39      return;
40    }
41  }
42
43  unsigned long t0 = micros();
44  digitalWrite(LED_PIN, HIGH);       // işaret / signal
45  if (USE_SOUND) tone(BUZZER_PIN, 2000, 50);
46  while (!pressed() && micros() - t0 < 2000000UL);   // en fazla 2 sn bekle / wait at most 2 s
47  unsigned long ms = (micros() - t0) / 1000;
48  digitalWrite(LED_PIN, LOW);
49
50  if (ms >= 2000) {
51    Serial.println(F("Cok yavas / Too slow"));
52    return;
53  }
54  total += ms; rounds++;
55  Serial.print(F("Sure / Time: ")); Serial.print(ms);
56  Serial.print(F(" ms   Ortalama / Average: ")); Serial.print(total / rounds);
57  Serial.println(F(" ms"));
58  if (ms < best) {                   // daha kısa süre daha iyi / shorter is better
59    best = ms;
60    EEPROM.put(BEST_ADDR, best);
61    tone(BUZZER_PIN, 1500, 150);
62    Serial.println(F("Yeni rekor! / New record!"));
63  }
64  Serial.print(F("Rekor / Best: ")); Serial.print(best); Serial.println(F(" ms"));
65  waitRelease();
66  delay(1000);
67}

光に対する人の平均的な反応時間は約200〜250 msで、よく集中したプレイヤーならそれより少し速くなります。USE_SOUNDをtrueにすると、LEDと同時にビープ音が鳴ります。音への反応はたいてい光への反応より少し速いので、2つのモードの記録を比べると面白い実験になります。時間をシリアルモニタではなく7セグメントディスプレイやOLEDに表示すれば、パソコンなしで動く電池式の装置になります。

ESP32でゲームを作る

このガイドのコードの多くは、少し変更するだけでESP32でも動きます。Adafruit SSD1306とNeoPixelのライブラリはESP32に対応しており、tone()はESP32 Arduinoコアの2.xと3.xで使えます。変える必要があるのは3つです。ピン(共通の表のESP32の列)、ジョイスティックのしきい値、ハイスコアの保存方法です。ESP32のADCは12ビットで0〜4095の値を返すので、300と700の代わりに約1200と2800を使います。Wi-FiがオンのときはADC2のピンを読めないので、ジョイスティックはADC1のピン、GPIO 32〜39につないでください。

ESP32には本物のEEPROMがありません。EEPROMライブラリはフラッシュの上でEEPROMをエミュレートし、EEPROM.begin()とEEPROM.commit()の呼び出しが必要です。よりすっきりした方法はPreferencesライブラリです。名前空間とキー名を付けて値をフラッシュのNVSパーティションに書き込み、書き込みの消耗も自動で分散してくれます。下の「連打レース」ゲームは、5秒間に何回ボタンを押せるかを数え、記録をPreferencesで保存します。

C++ (Arduino IDE)
1#include <Wire.h>
2#include <Adafruit_GFX.h>
3#include <Adafruit_SSD1306.h>
4#include <Preferences.h>
5
6Adafruit_SSD1306 display(128, 64, &Wire, -1);   // SDA = GPIO 21, SCL = GPIO 22
7Preferences prefs;
8
9const int BUTTON_PIN = 26;           // aksiyon butonu / action button
10const int BUZZER_PIN = 27;           // pasif buzzer / passive buzzer
11const unsigned long ROUND_MS = 5000; // 5 saniyelik tur / 5-second round
12
13uint32_t best = 0;
14
15// Sıçramasız basış: yalnız basıldığı an true / bounce-free: true only at the moment of a press
16bool buttonPressed() {
17  static bool stable = HIGH, lastRead = HIGH;
18  static unsigned long changedAt = 0;
19  bool r = digitalRead(BUTTON_PIN);
20  if (r != lastRead) { lastRead = r; changedAt = millis(); }
21  if (millis() - changedAt > 10 && r != stable) {
22    stable = r;
23    return stable == LOW;
24  }
25  return false;
26}
27
28void screen(const char *title, uint32_t big, uint32_t small) {
29  display.clearDisplay();
30  display.setTextSize(1);
31  display.setCursor(0, 0);   display.print(title);
32  display.setCursor(0, 56);  display.print("BEST "); display.print(best);
33  display.setTextSize(3);
34  display.setCursor(0, 20);  display.print(big);
35  display.setTextSize(1);
36  display.setCursor(90, 56); display.print(small);
37  display.display();
38}
39
40void setup() {
41  pinMode(BUTTON_PIN, INPUT_PULLUP);
42  Serial.begin(115200);
43  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
44    Serial.println("SSD1306 allocation failed");
45    for (;;) delay(1000);
46  }
47  display.setTextColor(SSD1306_WHITE);
48  prefs.begin("oyun", false);        // "oyun" ad alanı, okuma-yazma / "oyun" namespace, read-write
49  best = prefs.getUInt("rekor", 0);  // kayıt yoksa 0 / 0 when nothing is stored
50}
51
52void loop() {
53  screen("TAP RACE: PRESS", 0, 0);
54  while (!buttonPressed()) delay(1); // başlatma / start
55  for (int i = 3; i > 0; i--) {      // 3-2-1 geri sayım / 3-2-1 countdown
56    screen("GET READY", i, 0);
57    tone(BUZZER_PIN, 800, 80);
58    delay(1000);
59  }
60  tone(BUZZER_PIN, 1600, 150);
61
62  uint32_t taps = 0;
63  unsigned long start = millis(), lastDraw = 0;
64  while (millis() - start < ROUND_MS) {
65    if (buttonPressed()) taps++;
66    if (millis() - lastDraw > 100) { // ekranı 10 kez/sn çiz / redraw 10 times per second
67      lastDraw = millis();
68      screen("TAP!", taps, (ROUND_MS - (millis() - start)) / 1000 + 1);
69    }
70  }
71
72  if (taps > best) {                 // yeni rekor flash'a / new record to flash
73    best = taps;
74    prefs.putUInt("rekor", best);
75    tone(BUZZER_PIN, 2000, 300);
76  }
77  screen("RESULT", taps, 0);
78  Serial.printf("Basis / Taps: %lu  Rekor / Best: %lu\n", (unsigned long)taps, (unsigned long)best);
79  delay(3000);
80}

prefs.getUInt("rekor", 0)は、何も保存されていなければ2つ目の引数を返すので、EEPROMの0xFFFF問題を気にする必要がありません(rekorはトルコ語で「記録」の意味です)。名前空間とキー名は最大15文字までです。ESP32のプロセッサはUnoより何倍も速いものの、OLEDは同じI2Cバスを使うため、フレームレートはほとんど変わりません。もっと滑らかでカラフルなゲームを作るなら、SPIのILI9341 TFTディスプレイが使えます。また、ESP32のBluetoothは、スマホをゲームコントローラーにする第一歩としてぴったりです。

Raspberry Pi PicoとMicroPythonでゲームを作る

Raspberry Pi PicoをMicroPythonでプログラミングする場合、ゲームのロジックは同じで、変わるのは書き方だけです。millis()の代わりにtime.ticks_ms()とtime.ticks_diff()、analogRead()の代わりに0〜65535を返すread_u16()、tone()の代わりにPWMを使います。PicoにはEEPROMがありませんが、MicroPythonのファイルシステムのおかげで、フラッシュの小さなテキストファイルに記録を書き込むのはとても簡単です。下のコードは反応テストのPico版で、記録をrekor.txtというファイルに保存します。Thonnyでmain.pyとしてボードに保存すれば、電源を入れるたびに自動で始まります。MicroPythonが初めてなら、まずRaspberry Pi Picoとは?を読んでください。

MicroPython
1from machine import Pin, PWM
2import time, random
3
4led = Pin("LED", Pin.OUT)                 # kart üstündeki LED / onboard LED
5button = Pin(14, Pin.IN, Pin.PULL_UP)     # GP14, basınca 0 / 0 when pressed
6buzzer = PWM(Pin(16))                     # GP16, pasif buzzer / passive buzzer
7buzzer.duty_u16(0)                        # başlangıçta sessiz / silent at start
8FILE = "rekor.txt"                        # rekor flash'ta dosyada / record kept in a file on flash
9
10def beep(freq, ms):
11    buzzer.freq(freq)
12    buzzer.duty_u16(32768)                # %50 görev oranı / 50% duty cycle
13    time.sleep_ms(ms)
14    buzzer.duty_u16(0)
15
16def load_best():
17    try:
18        with open(FILE) as f:
19            return int(f.read())
20    except (OSError, ValueError):         # dosya yok ya da bozuk / file missing or corrupt
21        return 99999
22
23def save_best(ms):
24    with open(FILE, "w") as f:
25        f.write(str(ms))
26
27def wait_release():
28    while button.value() == 0:
29        time.sleep_ms(5)
30    time.sleep_ms(50)                     # bırakma sıçraması / release bounce
31
32best = load_best()
33print("Baslamak icin butona bas / Press the button to start")
34
35while True:
36    while button.value() == 1:            # başlatma basışı / start press
37        time.sleep_ms(5)
38    wait_release()
39    print("Hazir ol... / Get ready...")
40
41    wait = random.randint(2000, 5000)     # 2-5 sn rastgele bekleme / random 2-5 s wait
42    start = time.ticks_ms()
43    early = False
44    while time.ticks_diff(time.ticks_ms(), start) < wait:
45        if button.value() == 0:
46            early = True
47            break
48    if early:
49        print("Erken bastin! / Too early!")
50        beep(150, 400)
51        wait_release()
52        continue
53
54    t0 = time.ticks_us()
55    led.on()                              # işaret / signal
56    while button.value() == 1 and time.ticks_diff(time.ticks_us(), t0) < 2000000:
57        pass
58    ms = time.ticks_diff(time.ticks_us(), t0) // 1000
59    led.off()
60
61    if ms >= 2000:
62        print("Cok yavas / Too slow")
63        continue
64    print("Sure / Time:", ms, "ms")
65    if ms < best:                         # daha kısa süre daha iyi / shorter is better
66        best = ms
67        save_best(best)
68        print("Yeni rekor! / New record!")
69        beep(1500, 150)
70    print("Rekor / Best:", best, "ms")
71    wait_release()
72    time.sleep(1)

時間を測るときにticks_diff()を使うことは大切です。カウンターはある値を超えると0に戻るので、2つの値を単純に引き算すると、その瞬間に間違った結果になります。ticks_diff()はこの折り返しを正しく扱います。OLEDのゲームでは、Thonnyのパッケージマネージャからインストールするssd1306ドライバに、Adafruit GFXに似たfill_rect()、text()、show()などの命令があります。NeoPixelマトリクスに必要なneopixelモジュールはMicroPythonに標準で入っています。これで、PongやスネークもPicoに同じロジックで移植できます。

よくある間違いと直し方

症状原因対策
画面に何も表示されないシリアルモニタに「SSD1306 allocation failed」(メモリ確保の失敗)と出ればRAM不足、出なければアドレスか配線の誤り固定の文字列をF()で囲み、大きな配列を小さくする。メッセージが出ないなら、SDA/SCLの配線を確認し、アドレス0x3Dを試す
毎回同じ形でゲームが始まるrandomSeed()がない、または毎回同じ種プレイヤーが最初に押したときのmicros()の値を種にする
1回押すと2回ジャンプするボタンのチャタリング15 ms安定するまで待つbuttonPressed()関数を使う
ハイスコアが65535と表示される一度も書き込んでいないEEPROMは0xFFを読む0xFFFFを0として扱う
ジョイスティックを離してもキャラクターが動く中央が512ではなく、不感帯が狭すぎる300/700のしきい値を保つか、起動時に中央を測ってキャリブレーションする
ヘビが斜めに跳ぶマトリクスがジグザグに配線されているSERPENTINE = trueにする
明るさを上げるとArduinoがリセットされるLEDマトリクスの消費電流が大きすぎるsetBrightness()を下げ、5 Vの外部電源を使う
tone()が鳴ると3番か11番ピンのPWMがおかしくなるUnoではtone()がTimer2を使うPWMが必要な部品を5、6、9、10番ピンに移す
ゲームが進むにつれて遅くなる毎フレームのSerial.printや不要な描画シリアル出力を削除し、フレーム時間をディスプレイの転送時間より長く保つ
LCDに四角いブロックしか表示されないコントラストの設定か、アドレスの誤り裏の半固定抵抗を回し、アドレス0x27か0x3Fを試す
ゲームが思いどおりに動かないときは、まずディスプレイなしでロジックをテストしましょう。毎フレーム、位置、速度、状態をシリアルモニタに表示し、数フレームを手で追ってみます。バグが見つかったら、その行は削除してください。Serial.printは1フレームあたり数ミリ秒かかり、ゲームの速さを狂わせるからです。そもそもボードにコードを書き込めない場合は、Arduinoに書き込めないときのガイドを確認してください。

ゲームをさらに発展させるアイデア

  • 2人用のPong:CPUの行を削除し、2つ目のパドルをA2のポテンショメーターで操作します。長いワイヤーで2つのコントローラーをテーブルの両端まで伸ばしましょう。
  • 傾きで操作:ジョイスティックの代わりにMPU6050を使い、ボードを傾けてヘビやパドルを動かします。
  • 音楽と効果音:DFPlayer Miniでゲーム音楽を流します。短い効果音ならパッシブブザーで十分です。
  • スマホで遊ぶ:HC-05 Bluetoothモジュールで、スマホをワイヤレスのコントローラーにします。
  • パソコン用のゲームパッド:Arduino LeonardoはUSBキーボードとして動作できるので、ジョイスティックとボタンでパソコンゲーム用のコントローラーを作れます。
  • スコアボード:4連(4-in-1)のMAX7219モジュールに、ハイスコアをスクロール表示します。
  • 運だめしのゲーム:WS2812Bのリングでルーレットを作ったり、LEDで電子サイコロを作ったりします。random()を練習するいちばん楽しい方法です。

Arduinoでのゲーム作りは、コードとハードウェアを同時に学べるいちばん楽しい方法です。ゲームループ、当たり判定、EEPROMのハイスコアを理解すれば、自分のゲームを設計するのに必要なのはアイデアだけです。さらにヒントがほしい場合は、Arduinoのプロジェクトアイデアのリストを見てみてください。BlueGraysアプリには、Pong、OLEDのT-Rexラン、8x8と16x16のマトリクスのスネーク、サイモン、LCDのランナーゲーム、反応テストが、回路図とすぐに使えるコード付きのステップごとのプロジェクトとしてそろっています。

よくある質問

Arduinoではどんなゲームが作れますか?

Arduino Unoなら、Pong、T-Rexラン、Flappy Bird風のゲーム、スネーク、簡単なブロックゲーム、記憶ゲームのサイモン、反応テスト、サイコロやルーレットのような運だめしのゲームが作れます。グラフィックのあるゲームには128×64のOLEDで十分で、カラフルなマス目のゲームにはNeoPixel LEDマトリクスが合います。サイモンや反応テストのようなゲームは、ディスプレイなしでLEDとボタンだけで遊べます。

Arduinoのゲームにいちばん合うディスプレイは?

初心者にいちばんおすすめなのは0.96インチのSSD1306 OLEDです。安く、データ線2本だけで接続でき、128×64ピクセルで動くグラフィックを描けます。もっと大きなカラー画面がほしいならILI9341やST7789のTFTディスプレイ、ドット絵のゲームなら8x8や16x16のLEDマトリクスが合います。カスタム文字を使えば、16x2のLCDでも簡単なランナーゲームには十分です。

Arduinoでハイスコアを保存するには?

ハイスコアをEEPROMに書き込めば、電源が切れても消えません。EEPROM.put(address, score)で書き込み、EEPROM.get(address, score)で読み出します。Arduino Unoには、そのための1024バイトがあります。各セルに書き込めるのは約100,000回なので、ゲームが終わって記録を破ったときだけ書き込みましょう。

Arduinoでゲームパッドは作れますか?

はい。Arduino Leonardo、Micro、Pro MicroのようにATmega32U4を搭載したボードは、パソコンからUSBキーボード、マウス、ゲームコントローラーとして認識させることができます。ジョイスティックとボタンを読み、キー入力を送ります。Arduino Unoは直接はできませんが、ジョイスティックの値をシリアルポート経由でパソコン上のプログラムに送ることはできます。

Arduinoのゲームが遅い、またはカクつくのはなぜですか?

よくある原因は、delay()で作ったループ、毎フレームのSerial.print、ディスプレイの転送時間です。UnoではOLEDの画面全体を送るのに約25 msかかるので、30 msのフレーム時間が現実的です。delay()の代わりにmillis()で決まったフレーム時間を使い、シリアル出力を削除し、必要なものだけを描画してください。

初心者にいちばん簡単なArduinoのゲームは?

いちばん簡単なのは反応テストです。LED1個、ボタン1個、短いスケッチで作れるのに、タイミング、乱数、EEPROMといった基本の考え方を学べます。その次はLED4個とボタン4個のサイモン、さらにOLEDディスプレイのPongとT-Rexへと進むのが、よい学習の順番です。

全プロジェクトを見る
パドルボールゲーム(SSD1306)
#86中級
プレミアム

パドルボールゲーム(SSD1306)

ボール1個とパドル2本で遊ぶ、ビデオゲームの原点の1つである卓球ゲームを、小さなOLEDディスプレイで楽しめます。2つのボタンで右のパドルを上下に動かします。左のパドルはコンピューターがボールを追いながら操作し、ボールを打つたびにブザーが短く鳴ります。先に9点を取ると勝ちです。その後、新しいゲームが自動で始まります。

4.5(47)
65
OLED T-Rexラン:Arduino恐竜ゲーム
#87上級
プレミアム

OLED T-Rexラン:Arduino恐竜ゲーム

インターネットが切れると現れる恐竜ゲームを、机の上で楽しめます。小さなOLEDディスプレイを恐竜が走り、迫るサボテンを1つのボタンでジャンプして避けます。横の4桁ディスプレイには得点がリアルタイムで表示され、100点ごとにブザーがお祝いの音を鳴らします。サボテンにぶつかるとゲーム終了です。ボタンを押すと、もう一度始められます。

4.7(33)
53
ピクセルスネーク:8x8マトリクス
#88上級
プレミアム

ピクセルスネーク:8x8マトリクス

定番のスネークゲームを自分の手で作って遊びましょう。カラフルな8x8のLEDマトリクスで、ジョイスティックを使って緑のヘビを動かし、赤いリンゴを食べます。リンゴを食べるたびにヘビが長くなり、少しずつ速くなって、小さな数字ディスプレイのスコアも増えます。壁や自分のしっぽにぶつかると画面が赤く3回点滅し、ボタンを押すと再び始められます。

4.7(39)
57
マトリクススネーク16x16
#89上級
プレミアム

マトリクススネーク16x16

定番のスネークゲームを、今度は256個のLEDでできた大きなディスプレイで遊びます。ジョイスティックで緑のヘビを動かし、赤いリンゴを集めます。リンゴを食べるたびにヘビが長くなり、小さな数字ディスプレイのスコアが増え、ゲームも少しずつ速くなります。壁や自分の体にぶつかると画面が赤く点滅し、ボタンを押すと新しいゲームが始まります。

4.8(52)
67
Arduino記憶ゲーム(LED4個+ボタン4個+ブザー)
#93上級
プレミアム

Arduino記憶ゲーム(LED4個+ボタン4個+ブザー)

記憶力に自信はありますか?4色のLEDが順番に点灯し、それぞれの音を鳴らします。同じ順番でボタンを押して再現してください。正解するたびに新しい色が1つ追加され、短い勝利のメロディーが流れて、LCDディスプレイにスコアが表示されます。1回でも押し間違えると、悲しい音とともにゲームが終了し、最初からやり直しになります。

4.8(46)
69
Arduino LCDランナーゲーム
#98中級
プレミアム

Arduino LCDランナーゲーム

16x2文字のLCDディスプレイで動作する横スクロール型のエンドレスランナーゲームです。キャラクターは自動で走り、ボタンを押すとジャンプして障害物を飛び越えます。障害物は上段と下段にランダムに現れます。パッシブブザーで開始時のメロディー、ジャンプ音、50点獲得を祝う音、ゲーム終了時のメロディーを鳴らします。起動時には「BlueGrays — Arduino Projects」の画面を表示します。スコアは画面右上でリアルタイムに更新されます。

4.6(32)
55
Arduino 音で知らせる反応速度テスト:反射神経をミリ秒で測ろう(LCD+ブザー+LED 3個)
#97中級
プレミアム

Arduino 音で知らせる反応速度テスト:反射神経をミリ秒で測ろう(LCD+ブザー+LED 3個)

反射神経はどれくらい速いでしょうか。ボタンを押すと、赤LEDが位置について、黄LEDが用意を知らせ、1.5〜3.5秒のランダムな待ち時間が始まります。緑LEDが光ってブザーが鳴った瞬間にもう一度押すと、反応時間がI2C LCDにミリ秒で表示され、下の行にその回の最高記録と平均が出ます。緑より先に押すと画面にTOO EARLY!と表示され、残念な音が鳴ります。ボタンを長押しすると新しいプレイヤーになり、全員が同じ待ち時間の順番で勝負できます。

4.6(33)
50