Руководства · 16 мин чтения

Игры на Arduino: как сделать Pong, T-Rex, «Змейку» и «Саймона»

Игры на Arduino шаг за шагом: Pong и T-Rex на OLED, «Змейка» на светодиодной матрице, «Саймон», раннер на LCD и тест на реакцию, плюс код для рекорда в EEPROM.

Игры на Arduino — самый весёлый способ изучать электронику и программирование: с OLED-дисплеем, парой кнопок и джойстиком можно своими руками собрать классику вроде Pong, раннера T-Rex, «Змейки» и игры на память «Саймон», а потом предложить друзьям побить твой рекорд. Пока пишется игра, почти незаметно осваиваются важнейшие темы программирования: тайминг, конечные автоматы, системы координат, математика столкновений, случайные числа и постоянная память. Все эти игры работают на одной Arduino Uno; её 2 КБ ОЗУ хватает, если расходовать память с умом.

В этом руководстве мы от начала до конца соберём шесть игр: Pong и раннер T-Rex на OLED-дисплее, «Змейку» на светодиодных матрицах NeoPixel 8x8 и 16x16, «Саймона» с четырьмя светодиодами и четырьмя кнопками, раннер на LCD 16x2 и тест на реакцию с точностью до миллисекунды. К каждой игре есть список деталей, схема подключения и код, который компилируется. Сначала разберём общий для всех скелет — игровой цикл, обнаружение столкновений, подсчёт очков и сохранение рекорда в EEPROM, — а затем будем использовать его в каждой игре. В конце есть и версии для ESP32 и Raspberry Pi Pico.

Игра «Змейка» на Arduino на светодиодной матрице NeoPixel 8x8
Светодиодная матрица WS2812B NeoPixel 8x8: 64 цветных пикселя и один провод данных — идеальный мини-экран для «Змейки».

Какой игре какие детали нужны?

Когда делаешь игру на Arduino, первое решение — дисплей, потому что именно он определяет, какую игру можно сделать. На нескольких светодиодах уже получатся «Саймон» или тест на реакцию, а OLED на 128×64 пикселя открывает Pong и T-Rex с движущейся графикой и физикой. В таблице ниже собраны игры из этого руководства — от самой простой к самой сложной:

ИграДисплейВводДругие деталиСложность
Тест на реакцию1 светодиод + монитор порта1 кнопкаРезистор 220 Ω, зуммер по желаниюЛегко
Игра на память «Саймон»4 цветных светодиода4 кнопки4 резистора по 220 Ω, пассивный зуммерЛегко–средне
Раннер на LCDLCD 16x2 с I2C1 кнопкаПассивный зуммерСредне
PongOLED SSD1306 0,96″Джойстик или потенциометр 10 kΩПассивный зуммерСредне
Раннер T-RexOLED SSD1306 0,96″1 кнопка или кнопка джойстикаПассивный зуммерСредне
«Змейка» (8x8)Матрица NeoPixel 8x8ДжойстикРезистор 330 Ω, конденсатор 1000 µFСредне–сложно
«Змейка» (16x16)Матрица NeoPixel 16x16ДжойстикВнешний источник питания 5 V 3–4 AСложно
Arduino Uno
Arduino Uno
OLED SSD1306 0,96″
OLED SSD1306 0,96″
Джойстик
Джойстик
LCD 16x2 I2C
LCD 16x2 I2C
Зуммер
Зуммер
Тактовая кнопка
Тактовая кнопка

Общая схема: одна плата как игровая приставка

Мы подобрали общие пины так, чтобы все игры, кроме «Саймона», работали на одной схеме. OLED и LCD с I2C используют одни и те же два провода (SDA и SCL); джойстик подключается к аналоговым входам, кнопка действия — к D3, зуммер — к D8. Когда такая «игровая приставка» собрана, игры меняются простой загрузкой другого кода. Полную карту пинов своей платы можно посмотреть в утилите распиновки.

Деталь / пинArduino UnoESP32Raspberry Pi Pico
OLED или LCD с I2C: SDAA4GPIO 21GP4
OLED или LCD с I2C: 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 и может повредить плату. Если к ESP32 подключается 5-вольтовый модуль LCD с I2C, поставь между ними преобразователь уровней. Все кнопки используют встроенную подтяжку: одна ножка идёт на пин, другая на GND, и при нажатии пин читает LOW.

Как работает игровой цикл?

Любая видеоигра построена на цикле из четырёх шагов, который повторяется десятки раз в секунду: прочитать ввод (кнопки, джойстик), обновить мир (положение, скорость, счёт), проверить столкновения и нарисовать экран. Один проход цикла называется кадром. Тридцати кадров в секунду (30 FPS) достаточно, чтобы картинка выглядела плавной, то есть каждый кадр длится около 33 мс. Функция loop() в Arduino уже является циклом, но работает очень быстро и неравномерно, поэтому её нужно разбить на кадры фиксированной длины с помощью millis().

Обходиться без delay() здесь обязательно. При delay(33) Arduino эти 33 мс ничего не делает, может пропустить короткое нажатие кнопки, а скорость игры меняется всякий раз, когда отрисовка занимает больше или меньше времени. С millis() функция loop() читает ввод на каждом проходе, но продвигает игру, только когда пришло время нового кадра. Разница между двумя подходами подробно объяснена в статье Arduino: 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}

Вторая ключевая идея кода — конечный автомат. В любой момент игра находится в одном из трёх состояний: MENU (меню), PLAYING (игра) или GAME_OVER (конец игры). Каждое состояние по-своему реагирует на одну и ту же кнопку: в меню нажатие запускает игру, во время игры заставляет персонажа прыгнуть, а после проигрыша начинает новый раунд. Такое разделение кода убирает вложенные блоки if и позволяет легко добавить новое состояние, например PAUSE. Подробнее эта тема разобрана в статье конечный автомат на Arduino.

Чтение кнопок и джойстика

Подключать кнопки с INPUT_PULLUP — самый чистый способ: внешний резистор не нужен, в покое пин читает HIGH, при нажатии — LOW. Однако механическая кнопка при нажатии и отпускании несколько миллисекунд дребезжит, и Arduino видит это как несколько нажатий подряд. В игре это значит двойной прыжок от одного нажатия или неверный цвет, засчитанный в «Саймоне». Функция buttonPressed() в скелете принимает новое состояние, только если пин 15 мс оставался неизменным, и возвращает true один раз — в момент нажатия. Схему и другие способы смотри в статье как подключить кнопку к Arduino.

Джойстик состоит из двух потенциометров и кнопки. На Uno каждая ось даёт значение от 0 до 1023, и при отпущенной ручке центр около 512, но от модуля к модулю он гуляет примерно от 490 до 530. Поэтому в играх на сетке используются пороги, а не точные значения: ниже 300 — влево или вверх, выше 700 — вправо или вниз. Широкая зона между ними (мёртвая зона) не даёт персонажу уползать самому, когда ручку отпускают. А в Pong аналоговое значение напрямую задаёт положение ракетки: map() переводит диапазон 0–1023 в ход ракетки на 0–48 пикселей. Калибровка и таблица направлений — в руководстве по модулю джойстика.

Как работает обнаружение столкновений?

Обнаружение столкновений отвечает на вопрос, находятся ли два объекта в одном месте в одно и то же время. В играх с экраном чаще всего используется проверка AABB (axis-aligned bounding box, ограничивающий прямоугольник, выровненный по осям): каждый объект считается прямоугольником. Два прямоугольника пересекаются, только если одновременно выполняются все четыре условия: левый край A левее правого края B, правый край A правее левого края B, и те же два условия выполняются по вертикальной оси. В коде это одна строка: ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by. В играх на сетке («Змейка», раннер на LCD) всё ещё проще: если у двух клеток одинаковые координаты, это столкновение.

ИграЧто с чем сталкивается?Способ
PongМяч со стенками и ракеткамиAABB + смена направления скорости
T-RexДинозавр с кактусамиAABB (с чуть уменьшенным прямоугольником)
«Змейка»Голова со стенками, телом и едойСовпадение клеток сетки
Раннер на LCDБегун с кактусомТот же столбец и та же строка
«Саймон»Нажатый цвет с цветом в последовательностиСравнение элементов массива
Тест на реакциюВремя нажатия со временем сигналаСравнение времени (фальстарт)
Маленькие быстрые объекты могут за один кадр пройти сквозь препятствие и пропустить столкновение; это называется туннелированием. В Pong, если мяч за кадр смещается больше чем на 4 пикселя, он пересекает зону проверки 3-пиксельной ракетки, так её и не коснувшись. Решение — держать скорость объекта за кадр меньше ширины зоны проверки, и именно это делает ограничение MAX_SPEED = 2.8 в коде Pong.

Подсчёт очков и сохранение рекорда в EEPROM

Счёт хранится в переменной в ОЗУ и пропадает в тот момент, когда Arduino отключают от питания. Чтобы рекорд сохранялся, используется EEPROM: в ATmega328P на Arduino Uno есть 1024 байта EEPROM, которая хранит данные без питания. Для рекорда хватит двух байт типа uint16_t (0–65535). Единственное слабое место EEPROM — ресурс записи: каждую ячейку можно перезаписать примерно 100 000 раз. Звучит как много, но если записывать счёт каждый кадр, 30 раз в секунду, ячейка износится примерно за 55 минут. Правильно записывать только тогда, когда игра закончилась и рекорд побит; даже при 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() {}

В коде два тонких момента. Во-первых, каждый байт EEPROM, в который ещё ни разу не записывали, равен 0xFF, поэтому при самом первом запуске рекорд показывается как 65535, и проверка на 0xFFFF превращает его в 0. В тесте на реакцию наоборот: 0xFFFF означает «рекорда пока нет», потому что там лучше меньшее время. Во-вторых, EEPROM.put() перед записью читает каждый байт и записывает только изменившиеся, что ещё больше продлевает срок службы. Другие команды EEPROM смотри в статье работа с EEPROM в Arduino. Чтобы игры из этого руководства можно было по очереди загружать в одну и ту же Uno, у каждой игры свой адрес:

ИграАдрес в EEPROMЧто хранится
Раннер T-Rex0–1Лучший счёт
«Змейка»2–3Больше всего съеденной еды
Раннер на LCD4–5Лучший счёт
Тест на реакцию6–7Лучшее время (мс)
«Саймон»8–9Наивысший достигнутый уровень

Pong: OLED-дисплей и джойстик

Pong — игра в настольный теннис 1972 года, с которой началась эпоха видеоигр. В нашей версии левой ракеткой управляет ось Y джойстика (или потенциометр на A1), а правой играет Arduino. Детали: Arduino Uno, OLED SSD1306 0,96 дюйма с I2C, джойстик и пассивный зуммер. Устанавливая библиотеку Adafruit SSD1306 через «Менеджер библиотек» (Library Manager), установи и её зависимость 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.2 пикселя за кадр, поэтому, если отправить мяч под острым углом, она не успевает, и очко твоё. Чтобы усложнить игру, подними это значение до 1.5; для двух игроков удали строки CPU и управляй второй ракеткой потенциометром на A2.

Почему кадр длится 30 мс? Uno отправляет 1024-байтный буфер кадра в OLED по I2C на 400 kHz примерно за 25 мс, поэтому дисплей не может показать больше 35–40 кадров в секунду. Если держать длительность кадра чуть выше этого времени передачи, скорость игры остаётся постоянной. И не стоит доверять отчёту компилятора «Global variables use 28%» (глобальные переменные занимают 28%): буфер SSD1306 выделяется во время работы, так что реальное использование ОЗУ поднимается примерно до 78%. Если памяти не хватит, появится ошибка SSD1306 allocation failed (не удалось выделить память); способы решения мы собрали в статье Arduino: не хватает памяти.

Раннер T-Rex: физика прыжка и рекорд

Переносим на OLED-дисплей игру с динозавром, которую Chrome показывает без интернета. Динозавр бежит на месте, кактусы едут справа налево, а перепрыгивать их нужно нажатием кнопки. Чтобы прыжок выглядел правдоподобно, используется простая физика: нажатие кнопки задаёт вертикальную скорость -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Скорость кактусов; растёт на 0.25 каждые 200 кадров
random(70, 140) (интервал)пикселиРасстояние между двумя кактусами; чем меньше значения, тем сложнее игра
Хитбокс10×12 пикселейМеньше нарисованного динозавра 14×14, чтобы касание вскользь не считалось проигрышем

Рекорд записывается в EEPROM в момент окончания игры и только если он побит; он отображается в правом верхнем углу как HI. Если держать кнопку D3 при включении платы, рекорд сбрасывается — удобно, когда с друзьями начинается новый турнир. Высоту и ширину кактусов задаёт random(). Чтобы каждая игра начиналась по-разному, строка randomSeed(analogRead(A3)) использует в качестве зерна шум неподключённого аналогового пина.

«Змейка» на светодиодной матрице (8x8 и 16x16)

В «Змейку», которую прославили старые телефоны Nokia, лучше всего играть на светодиодной матрице, ведь каждый светодиод — это клетка поля. Мы используем цветную матрицу WS2812B NeoPixel: в версии 8x8 64 пикселя, в версии 16x16 — 256, и все управляются по одному проводу данных. Тело змейки хранится в двух массивах (snakeX и snakeY), элемент 0 — голова. На каждом шаге тело сдвигается на одну клетку, а в голову записывается новое положение; когда змейка ест, хвост не убирается, и она вырастает на одну клетку. Чтобы скомпилировать код, установи библиотеку 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}

В коде три критических правила. Без разворота: если змейка, движущаяся вправо, вдруг повернёт влево, она врежется в собственную шею; readJoystick() сверяет новое направление с текущим направлением змейки, а не с ожидающим, поэтому даже два быстрых движения не развернут её. Исключение для хвоста: на шаге без еды кончик хвоста уходит из клетки, поэтому перемещение головы в эту клетку не считается столкновением. Размещение еды: placeFood() пробует снова и снова, пока еда не окажется вне змейки. Каждая порция еды сокращает время шага на 10 мс (с 300 мс до 120 мс); когда игра заканчивается, за каждую съеденную порцию загорается синий светодиод, а рекорд записывается в EEPROM.

Важно и то, как соединены светодиоды в матрице. На большинстве жёстких плат 8x8 светодиоды идут строка за строкой в одном направлении (SERPENTINE = false). Гибкие панели 16x16 обычно соединены зигзагом: одна строка идёт слева направо, следующая справа налево. Если змейка прыгает по диагонали или через строку бежит назад, установи SERPENTINE в true; если направления получаются перевёрнутыми, поверни джойстик или поменяй местами пороги 300 и 700. Для 16x16 достаточно задать W и H равными 16; тогда массивы змейки занимают 512 байт, буфер NeoPixel — 768 байт, и используется около 75% ОЗУ Uno.

Светодиодная матрица NeoPixel 16x16
Матрица NeoPixel 16x16: игровое поле на 256 пикселей — значит, змейка длиннее, а игра сложнее; нужен внешний источник питания 5 V.
Один WS2812B при полном белом потребляет около 60 mA; 256 светодиодов матрицы 16x16 в худшем случае могут потреблять до 15 A, а этого не выдадут ни USB, ни пин 5V на Arduino. Питай матрицу от внешнего источника 5 V 3–4 A, соедини GND источника с GND Arduino, добавь резистор 330 Ω в линию данных и конденсатор 1000 µF на вход питания. setBrightness(30) в коде снижает яркость примерно до 12%; поскольку в «Змейке» одновременно горят лишь несколько светодиодов, ток остаётся на уровне нескольких сотен mA. Одноцветный вариант, которому нужно меньше ОЗУ, описан в статье светодиодная матрица MAX7219.

Игра на память «Саймон»: 4 светодиода и 4 кнопки

«Саймон» — легендарная игра на память 1978 года: каждый раунд устройство добавляет к последовательности один цвет, а игроку нужно повторить всю последовательность в том же порядке. Детали: четыре цветных светодиода (зелёный, красный, жёлтый, синий), по резистору 220 Ω на каждый, четыре кнопки и пассивный зуммер. Подключи светодиоды к D9–D12, кнопки к D2–D5, зуммер к D8; эта игра собирается отдельно от общей приставки. Подходящий резистор для своих светодиодов можно подобрать в калькуляторе резистора для светодиода. Четыре звука оригинального «Саймона» — ноты ля-мажорного аккорда; в коде те же ноты (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 мс и останавливается на 180 мс; если не нажать кнопку за 3 секунды или нажать не тот цвет, игра заканчивается, а достигнутый уровень, если это рекорд, сохраняется в EEPROM.

Раннер на LCD: свои символы на дисплее 16x2

На символьном LCD 16x2 нельзя рисовать пиксели, зато с помощью createChar() можно определить до 8 собственных символов по 5×8 пикселей. Для раннера хватит трёх: двух поз бега и кактуса. Нижняя строка дисплея — земля; кактусы едут справа налево, а при нажатии кнопки бегун переходит на верхнюю строку и несколько шагов остаётся в воздухе. Игра построена на сетке, поэтому проверка столкновения занимает одну строку: если в столбце бегуна есть кактус, пока бегун на земле, игра окончена. Библиотеку и подключение смотри в статье LCD-дисплей с I2C.

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}

Прокрутка мира — это копирование каждого элемента массива земли на одну позицию влево; в крайний правый столбец с вероятностью 35 процентов ставится новый кактус. Правило MIN_GAP = 4 оставляет между двумя кактусами не меньше четырёх пустых столбцов, так что любое препятствие можно перепрыгнуть. Каждые 25 очков время шага уменьшается на 15 мс, с 300 мс до 120 мс. Если на каждом шаге переписывать обе строки, а не стирать дисплей командой lcd.clear(), мерцания не будет; одна только команда clear() занимает около 2 мс. Если на экране видны только прямоугольники, покрути подстроечный резистор контрастности на обратной стороне модуля; если не видно ничего, поменяй адрес с 0x27 на 0x3F.

Тест на реакцию: измерь время реакции в миллисекундах

Это самая простая игра и самая затягивающая: светодиод загорается в случайный момент, ты как можно быстрее нажимаешь кнопку, а Arduino выводит прошедшее время в миллисекундах в монитор порта. Нужен только светодиод на D7 с резистором 220 Ω, кнопка на 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 мс; хорошо сосредоточенный игрок может опуститься чуть ниже. Если установить USE_SOUND в true, вместе со светодиодом прозвучит сигнал. На звук обычно реагируют немного быстрее, чем на свет, поэтому сравнение рекордов двух режимов — интересный эксперимент. Выведи время на 7-сегментный индикатор или OLED вместо монитора порта — и получится устройство на батарейках, которому не нужен компьютер.

Игры на ESP32

Большая часть кода из этого руководства работает на ESP32 с небольшими изменениями: библиотеки Adafruit SSD1306 и NeoPixel поддерживают ESP32, а tone() есть в версиях 2.x и 3.x ядра ESP32 для Arduino. Изменить нужно три вещи: пины (столбец ESP32 в общей таблице), пороги джойстика и способ сохранения рекорда. АЦП ESP32 12-битный и возвращает значения от 0 до 4095, поэтому вместо 300 и 700 используй примерно 1200 и 2800. Подключай джойстик к пинам ADC1, GPIO 32–39, потому что пины ADC2 нельзя читать при включённом Wi-Fi.

Настоящей EEPROM у ESP32 нет; библиотека 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) возвращает второй параметр, если ничего не сохранено, так что с проблемой 0xFFFF из EEPROM сталкиваться не придётся. Имена пространства имён и ключей — не длиннее 15 символов. Хотя процессор ESP32 во много раз быстрее, чем у Uno, OLED использует ту же шину I2C, поэтому частота кадров почти не меняется; для гораздо более плавных и цветных игр можно взять TFT-дисплей ILI9341 с SPI. А Bluetooth в ESP32 — хорошая отправная точка, чтобы превратить телефон в игровой контроллер.

Игры на Raspberry Pi Pico и MicroPython

Если программировать Raspberry Pi Pico на MicroPython, игровая логика та же, меняется только синтаксис: time.ticks_ms() и time.ticks_diff() вместо millis(), read_u16(), который возвращает 0–65535, вместо analogRead(), и PWM вместо tone(). EEPROM в Pico нет, но благодаря файловой системе MicroPython рекорд очень легко записать в небольшой текстовый файл во флеш-памяти. Код ниже — версия теста на реакцию для Pico, она хранит рекорд в файле rekor.txt; сохрани его на плату под именем main.py в Thonny, и он будет запускаться сам при каждом включении. Если 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(): счётчики после определённого значения переполняются и начинают с начала, и простое вычитание двух значений в этот момент даёт неверный результат; ticks_diff() корректно учитывает переполнение. Для игр на OLED драйвер ssd1306, который ставится через менеджер пакетов Thonny, предлагает команды fill_rect(), text() и show(), похожие на Adafruit GFX. Модуль neopixel, нужный для матрицы NeoPixel, уже входит в MicroPython. Так что Pong и «Змейку» можно перенести на Pico с той же логикой.

Частые ошибки и как их исправить

СимптомПричинаРешение
Пустой экранЕсли в мониторе порта видно «SSD1306 allocation failed», не хватает ОЗУ; если нет — неверный адрес или подключениеПомести постоянный текст в F() и уменьши большие массивы; если сообщения нет, проверь провода SDA/SCL и попробуй адрес 0x3D
Игра каждый раз начинается одинаковоНет randomSeed() или зерно всегда одно и то жеВозьми в качестве зерна значение micros() при первом нажатии игрока
От одного нажатия персонаж прыгает дваждыДребезг контактов кнопкиИспользуй функцию buttonPressed(), которая ждёт 15 мс стабильного состояния
Рекорд показывает 65535Ни разу не записанная EEPROM читается как 0xFFСчитай 0xFFFF нулём
Персонаж уползает, когда джойстик отпущенЦентр не равен 512, а мёртвая зона слишком узкаяОставь пороги 300/700 или измерь центр при запуске и откалибруй
Змейка прыгает по диагоналиМатрица соединена зигзагомУстанови SERPENTINE = true
Arduino перезагружается, когда растёт яркостьСветодиодная матрица потребляет слишком большой токУменьши setBrightness() и используй внешний источник 5 V
ШИМ на пине 3 или 11 ломается, когда играет tone()На Uno tone() использует Timer2Перенеси деталь, которой нужен ШИМ, на пин 5, 6, 9 или 10
Игра со временем замедляетсяSerial.print или лишняя отрисовка в каждом кадреУбери вывод в порт и держи длительность кадра выше времени передачи на дисплей
LCD показывает только прямоугольникиНастройка контрастности или неверный адресПокрути подстроечный резистор сзади и попробуй адрес 0x27 или 0x3F
Если игра ведёт себя не так, как ожидается, сначала проверь логику без дисплея: выводи положение, скорость и состояние в монитор порта каждый кадр и проследи несколько кадров вручную. Когда ошибка найдена, удали эти строки, потому что Serial.print отнимает несколько миллисекунд на кадр и сбивает скорость игры. Если код вообще не загружается в плату, загляни в руководство код не загружается в Arduino.

Идеи, как развить свои игры

  • Pong на двоих: удали строки CPU, управляй второй ракеткой потенциометром на A2 и выведи оба контроллера на длинных проводах к противоположным концам стола.
  • Управление наклоном: используй MPU6050 вместо джойстика и управляй змейкой или ракеткой, наклоняя плату.
  • Музыка и звуковые эффекты: включай игровую музыку с DFPlayer Mini; для коротких эффектов хватит пассивного зуммера.
  • Игра с телефона: преврати телефон в беспроводной контроллер с помощью Bluetooth-модуля HC-05.
  • Геймпад для компьютера: Arduino Leonardo умеет работать как USB-клавиатура, поэтому из джойстика и кнопок можно собрать контроллер для игр на ПК.
  • Табло рекордов: выводи рекорды бегущей строкой на модуль MAX7219 «4 в 1».
  • Азартные игры: сделай рулетку на кольце WS2812B или электронный кубик на светодиодах — самый весёлый способ потренироваться с random().

Игры на Arduino — самый приятный способ одновременно изучать код и железо. Когда игровой цикл, обнаружение столкновений и рекорд в EEPROM понятны, придумать собственную игру — дело одной идеи; за вдохновением загляни в список идей проектов на Arduino. В приложении BlueGrays есть Pong, T-Rex на OLED, «Змейка» на матрицах 8x8 и 16x16, «Саймон», раннер на LCD и тест на реакцию — в виде пошаговых проектов со схемами и готовым кодом.

Частые вопросы

Какие игры можно сделать на Arduino?

На Arduino Uno можно сделать Pong, раннер T-Rex, игры в духе Flappy Bird, «Змейку», простые игры с блоками, игру на память «Саймон», тест на реакцию и азартные игры вроде кубика или рулетки. Для игр с графикой хватит OLED 128×64, для цветных игр на сетке — светодиодной матрицы NeoPixel. В «Саймона» и тест на реакцию играют только со светодиодами и кнопками, без всякого дисплея.

Какой дисплей лучше всего подходит для игры на Arduino?

Для начинающих лучший выбор — OLED SSD1306 0,96 дюйма: он дешёвый, подключается всего двумя проводами данных и рисует движущуюся графику в разрешении 128×64 пикселя. Если нужна более крупная цветная картинка, подойдёт TFT-дисплей ILI9341 или ST7789, а для игр в стиле пиксель-арт — светодиодная матрица 8x8 или 16x16. Для простых раннеров хватит и LCD 16x2 со своими символами.

Как сохранить рекорд на Arduino?

Если записать рекорд в EEPROM, он не пропадёт при отключении питания. Записывается он командой EEPROM.put(адрес, счёт), а читается командой EEPROM.get(адрес, счёт); на Arduino Uno для этого есть 1024 байта. Каждую ячейку можно перезаписать примерно 100 000 раз, поэтому записывай только тогда, когда игра закончилась и рекорд побит.

Можно ли сделать геймпад на Arduino?

Да. Платы с процессором ATmega32U4, такие как Arduino Leonardo, Micro или Pro Micro, могут определяться компьютером как USB-клавиатура, мышь или игровой контроллер: они читают джойстик и кнопки и отправляют нажатия клавиш. Arduino Uno так напрямую не умеет, но может передавать значения джойстика по последовательному порту программе на компьютере.

Почему моя игра на Arduino тормозит или работает медленно?

Чаще всего причина в циклах на delay(), вызовах Serial.print в каждом кадре и времени передачи на дисплей. На Uno отправка всего экрана OLED занимает около 25 мс, поэтому кадр в 30 мс — реалистичная длительность. Используй фиксированную длительность кадра с millis() вместо delay(), убери вывод в порт и рисуй только то, что нужно.

Какая игра на Arduino самая простая для начинающих?

Проще всего тест на реакцию: нужны один светодиод, одна кнопка и короткий скетч, а учит он важным вещам — таймингу, случайным числам и EEPROM. После него хороший путь обучения — «Саймон» с четырьмя светодиодами и четырьмя кнопками, а затем Pong и T-Rex на OLED-дисплее.

Все проекты
Игра в пинг-понг — SSD1306
#86Средний
Premium

Игра в пинг-понг — SSD1306

Мяч, две ракетки и ты: на маленьком OLED-экране ты играешь в настольный теннис — одну из самых первых видеоигр. Две кнопки двигают твою ракетку справа вверх и вниз, а левой ракеткой управляет компьюте

4.5(47)
65
OLED T-Rex Run: игра с динозавром на Arduino
#87Продвинутый
Premium

OLED T-Rex Run: игра с динозавром на Arduino

Игра с динозавром, которая появляется, когда пропадает интернет, теперь у тебя на столе: динозавр бежит по маленькому OLED-экрану, навстречу едут кактусы, а прыгаешь ты одной кнопкой. Счёт растёт в ре

4.7(33)
53
Пиксельная змейка на матрице 8x8
#88Продвинутый
Premium

Пиксельная змейка на матрице 8x8

Собери классическую игру «Змейка» своими руками и сыграй в неё. На цветной светодиодной матрице 8x8 ты джойстиком управляешь зелёной змейкой и ешь красные яблоки; с каждым яблоком змейка растёт, немно

4.7(39)
57
Змейка на матрице 16x16
#89Продвинутый
Premium

Змейка на матрице 16x16

Классическая игра «Змейка», на этот раз на огромном светодиодном дисплее из 256 огоньков. Ты джойстиком управляешь зелёной змейкой и собираешь красные яблоки; с каждым яблоком змейка растёт, счёт увел

4.8(52)
67
Игра на память на Arduino (4 светодиода + 4 кнопки + зуммер)
#93Продвинутый
Premium

Игра на память на Arduino (4 светодиода + 4 кнопки + зуммер)

Доверяешь своей памяти? Четыре цветных светодиода загораются в определённой последовательности, каждый со своей нотой, а ты повторяешь тот же порядок кнопками. Каждый верный раунд добавляет в последов

4.8(46)
69
Игра-бегалка на LCD с Arduino
#98Средний
Premium

Игра-бегалка на LCD с Arduino

Бесконечная игра-бегалка с горизонтальной прокруткой на символьном LCD-экране 16x2. Герой бежит сам, а игрок перепрыгивает препятствия нажатием кнопки. Случайные препятствия появляются в верхней и нижней строке. Пассивный зуммер играет вступительную мелодию, звук прыжка, праздничный звук на 50 очков и мелодию конца игры. При запуске показывается заставка «BlueGrays - Arduino Projects». Счёт обновляется сразу в правом верхнем углу экрана.

4.6(32)
55
Тест времени реакции на Arduino со звуковым сигналом: измерь рефлексы в миллисекундах (LCD + зуммер + 3 светодиода)
#97Средний
Premium

Тест времени реакции на Arduino со звуковым сигналом: измерь рефлексы в миллисекундах (LCD + зуммер + 3 светодиода)

Насколько быстрые у тебя рефлексы? Нажми кнопку: красный светодиод говорит приготовиться, жёлтый — внимание, и начинается случайное ожидание от 1,5 до 3,5 секунды. Как только загорится зелёный светодиод и пискнет зуммер, нажми снова; время реакции в миллисекундах появится на I2C LCD, а под ним — лучший и средний результат сессии. Нажмёшь до зелёного — на экране TOO EARLY! и звук проигрыша. Удержание кнопки — новый игрок, и у всех одинаковый порядок ожиданий.

4.6(33)
50