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版も紹介します。

どのゲームにどの部品が必要?
Arduinoでゲームを作るとき、最初に決めるのはディスプレイです。ディスプレイによって作れるゲームの種類が決まるからです。LEDが数個あるだけでもサイモンや反応テストは作れますし、128×64ピクセルのOLEDがあれば、動くグラフィックと物理演算を使うPongやT-Rexも作れます。下の表は、このガイドのゲームをやさしい順にまとめたものです。
| ゲーム | ディスプレイ | 入力 | そのほかの部品 | 難易度 |
|---|---|---|---|---|
| 反応テスト | LED 1個+シリアルモニタ | ボタン1個 | 220 Ωの抵抗、ブザー(任意) | 簡単 |
| 記憶ゲームのサイモン | 色付きLED 4個 | ボタン4個 | 220 Ωの抵抗4本、パッシブブザー | 簡単〜中級 |
| LCDのランナーゲーム | 16x2 I2C LCD | ボタン1個 | パッシブブザー | 中級 |
| Pong | 0.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の外部電源 | 上級 |






共通の配線:1つの回路をゲーム機に
サイモン以外のすべてのゲームが同じ回路で動くように、ピンを共通にしました。OLEDとI2C LCDは同じ2本の線(SDAとSCL)を共有し、ジョイスティックはアナログ入力に、アクションボタンはD3に、ブザーはD8につなぎます。この「ゲーム機」を一度組んでしまえば、別のコードを書き込むだけでゲームを切り替えられます。ボードのピン配置の全体は「ピン配置」ツールで確認できます。
| 部品/ピン | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| OLEDまたはI2C LCD:SDA | A4 | GPIO 21 | GP4 |
| OLEDまたはI2C LCD:SCL | A5 | GPIO 22 | GP5 |
| ジョイスティックVRx(X軸) | A0 | GPIO 34 | GP26 |
| ジョイスティックVRy(Y軸) | A1 | GPIO 35 | GP27 |
| ジョイスティックSW(ボタン) | D2 | GPIO 25 | GP15 |
| アクションボタン(もう一方の足はGNDへ) | D3 | GPIO 26 | GP14 |
| パッシブブザー(+) | D8 | GPIO 27 | GP16 |
| NeoPixel DIN(330 Ω経由) | D6 | GPIO 13 | GP22 |
ゲームループの仕組み
どのビデオゲームも、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()の違いで詳しく説明しています。下の骨組みが、このガイドのすべてのゲームの土台です。
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のランナーゲーム | ランナーとサボテン | 同じ列で同じ行 |
| サイモン | 押した色と順番の中の色 | 配列の要素の比較 |
| 反応テスト | 押した時刻と合図の時刻 | 時刻の比較(フライング) |
スコアとEEPROMへのハイスコア保存
スコアはRAMの変数に入っていて、Arduinoの電源を抜いた瞬間に消えます。ハイスコアを残すにはEEPROMを使います。Arduino UnoのATmega328Pには、電源がなくても内容を保持する1024バイトのEEPROMがあります。ハイスコア(0〜65535)なら、uint16_t型の2バイトで足ります。EEPROMの唯一の弱点は書き込み回数の寿命で、各セルに書き込めるのは約100,000回です。多く聞こえますが、毎フレーム、つまり1秒間に30回スコアを書き込むと、セルは約55分で寿命を迎えます。正しいのは、ゲームが終わって記録を破ったときだけ書き込む方法です。1日に10回新記録が出ても、セルは27年もちます。
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)で詳しく説明しています。
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のボタンかジョイスティックのボタンを使います。
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ピクセル上がり、画面の上端近くまで届く |
| GRAVITY | 0.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のガイドにあります。
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%が埋まります。

記憶ゲームのサイモン:LED4個とボタン4個
サイモンは1978年に発売された伝説的な記憶ゲームです。ラウンドごとに本体が順番に色を1つ追加し、プレイヤーはその順番全体を同じ順に繰り返します。部品は色付きLED4個(緑、赤、黄、青)、それぞれに220 Ωの抵抗、ボタン4個、パッシブブザーです。LEDはD9〜D12に、ボタンはD2〜D5に、ブザーはD8につなぎます。このゲームは共通のゲーム機とは別の回路です。LEDに合う抵抗はLED抵抗計算ツールで選べます。本物のサイモンの4つの音は、イ長調(Aメジャー)の和音の音です。コードでも同じ音(330、440、554、659 Hz)を使っているので、すばやく押しても心地よく聞こえます。
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ディスプレイを参照してください。
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に保存します。
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で保存します。
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とは?を読んでください。
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を試す |
ゲームをさらに発展させるアイデア
- 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)
ボール1個とパドル2本で遊ぶ、ビデオゲームの原点の1つである卓球ゲームを、小さなOLEDディスプレイで楽しめます。2つのボタンで右のパドルを上下に動かします。左のパドルはコンピューターがボールを追いながら操作し、ボールを打つたびにブザーが短く鳴ります。先に9点を取ると勝ちです。その後、新しいゲームが自動で始まります。

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

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

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

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

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

Arduino 音で知らせる反応速度テスト:反射神経をミリ秒で測ろう(LCD+ブザー+LED 3個)
反射神経はどれくらい速いでしょうか。ボタンを押すと、赤LEDが位置について、黄LEDが用意を知らせ、1.5〜3.5秒のランダムな待ち時間が始まります。緑LEDが光ってブザーが鳴った瞬間にもう一度押すと、反応時間がI2C LCDにミリ秒で表示され、下の行にその回の最高記録と平均が出ます。緑より先に押すと画面にTOO EARLY!と表示され、残念な音が鳴ります。ボタンを長押しすると新しいプレイヤーになり、全員が同じ待ち時間の順番で勝負できます。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

