Привет Хабр, сразу скажу, что я не особо программист, но хотелось поделиться хобби и может собрать соплеменников и единомышленников.
Итак начнем....
Почему вообще делать самодельный кубик, который ещё и глючит, но при этом умеет запускать игры вроде пресловутого «динозаврика» из Chrome? Вопрос резонный. Но, как говорится, «почему бы и нет?». Представляю проект MKA – небольшой куб с OLED-экраном, одной сенсорной кнопкой, пищалкой и кастомной прошивкой. Он получился чем-то средним между тамагочи, ретро-миниконсолью и электронным pet rock (если помните такую шутку). В этой статье – немного дневника разработки и технических деталей о том, как и зачем я его собрал.

Идея: тамагочи, динозаврик и кубические грёзы
Идея родилась спонтанно. Хотелось сделать карманное гиковое устройство «ни о чём и обо всём сразу» – чтобы и мигало, и пищало, и что-то показывало на экранчике, и вообще радовало душу. Помните тамагочи? В детстве мне нравилась сама концепция – маленький гаджет, который что-то там «живёт своей жизнью». Я решил: почему бы не создать свою версию цифрового питомца, только вместо пиксельного цыплёнка – электронный камень с эмоциями. А заодно впихнуть в него пару игр для развлечения. Форму выбрал не плоскую кубическую– просто потому что могу (и потому что 3D-принтер простаивал без дела).
Начал я с простого: достал из закромов модуль ESP32-C3, маленький OLED-дисплей 128×64 и подумал – «пора чет делать, сделаю мини-консоль на одной кнопке». Однокнопочные игры – это же вызов, особенно для чела который не особо программист и ардуинщик на коленке! Первым делом вспомнился динозаврик из оффлайн-режима Chrome – идеальная игра под одно нажатие. Так проект оброс идеей встроить сhrome Dino. Дальше – больше: захотелось и Flappybird, и ещё какую-то простую аркаду. В итоге устройство превратилось в такой себе кубик-весельчак: тут и тамагочи с гримасами, и несколько простых игр, и даже парочка шутливых режимов типа рикролла.
«Железо» MKA: что внутри кубика
MKA – это полностью самодельный гаджет в форме куба со сторонами ~5 см. Если глянуть внутрь и посмотреть, из каких компонентов он собран:
-
устройства – ESP32-C3. Этот микроконтроллер я выбрал за компактность и наличие Wi-Fi/BLE (про запас на будущее), потребляет мало энергии и имеет достаточно GPIO для наших целей.
-
Дисплей – OLED 128×64 px. Крохотный монохромный экран (диагональ 1,3), подключённый по I²C. Этого экрана достаточно, чтобы выводить простейшую графику: текст, спрайты игр, пиксельные смайлы и не приглядываться к мелочам. Я использовал распространённый модуль на SSD1306 – с ним удобно работать через библиотеку Adafruit SSD1306.
-
Единственная кнопка – сенсорная. Управление всем кубом осуществляется одним-единственным вводом. В качестве “кнопки” был взят емкостный сенсор TTP223 . Прикосновения он преобразует в цифровой сигнал HIGH/LOW для ESP32-C3. Почему сенсорная? Хотелось обойтись без движущихся частей и спрятать кнопку внутрь корпуса, чтобы снаружи был просто почти гладкий куб. Конечно, пришлось повозиться с настройкой чувствительности и устранением дребезга, но об этом позже.
-
Пищалка (buzzer). Самый простой пассивный буззер служит для озвучки. Он умеет издавать короткие бипы при нажатиях, а также проигрывать простенькие мелодии. Звук, прямо скажем, пипкающий, но для ретро-атмосферы пойдёт. Например, прыжок динозаврика сопровождается коротким «тик», а заодно можно исполнить мотив Never Gonna Give You Up.
-
Питание и прочее. Кубик питается от небольшой Li-Po батарейки 3.7 В, спрятанной внутри (аккумулятор от одноразки на ~300 мА·ч нашёл вторую жизнь). ESP32-C3 подключён через зарядный модуль TP4056, так что куб заряжается по microUSB. Этого аккума хватает примерно на 2-3 часа активных игр или на 5-6 в режиме тамагочи, что вполне сносно. Печатная плата… шутка, никакой платы – всё собрано на проводах. Я взял готовую dev-плату ESP32-C3 и навесным монтажом припаял к ней экран, сенсор и буззер. Затем всё это хозяйство компактно упаковал в 3D-печатный корпус.

Прошивка: состояния, одна кнопка и FSM
Чтобы заставить этот зоопарк работать, я написал прошивку на Arduino C++ с довольно простым языком в котором пытался разобраться при помощи гугла и чатажпт. Подход следующий: устройство может находиться в одном из нескольких режимов/состояний (enum AppState), и в каждом режиме свой код отрисовки и логики. Смена состояний происходит либо по событию (долгое/короткое нажатие на кнопку), либо по внутренним условиям (например, окончание игры).
Вот основные состояния, которые я заложил:
enum AppState {
STATE_FACES, // режим "лица" (тамагочи)
STATE_MENU, // меню выбора игр/режимов
STATE_DINO_GAME, // игра Dino
STATE_FLAPPY, // игра Flappy
STATE_TOWER, // игра Tower
STATE_PADDLE, // игра Paddle
STATE_LANE, // игра Lane
STATE_REACTION // игра Reaction (тест реакции)
};
В функции loop() прошивки – простой диспетчер по состояниям:
switch (currentState) {
case STATE_FACES: renderFaces(); break;
case STATE_MENU: renderMenu(); break;
case STATE_DINO_GAME: updateDinoGame(); break;
case STATE_FLAPPY: updateFlappy(); break;
case STATE_TOWER: updateTower(); break;
// ... и так далее для остальных игр
}
Каждый режим имеет свою функцию render/update, которая занимается логикой этого состояния и рисует картинку на экран. Например, renderFaces() показывает анимированное «лицо» нашего кубика-тамагочи, updateDinoGame() – выполняет один тик игрового цикла динозаврика (обрабатывает прыжки, движение кактусов и пр.). Такой подход позволил изолировать код игр и режимов друг от друга и удобно переключаться между ними.
Управление одной кнопкой. Пожалуй, самый интересный момент – это обработка единственного ввода. Используется два типа нажатия:
-
Короткое прикосновение (tap) – обычно выполняет какое-то действие (прыжок в игре, переход к следующему пункту меню и т.д.).
-
Долгое нажатие (hold) – выполняет другую функцию, например, выход или выбор. Например, долго удержать в тамагочи – откроется меню, долго удержать в меню – выберется пункт.
При реализации я столкнулся с проблемой дребезга и “фантомных” срабатываний сенсора, поэтому написал небольшой код для фильтрации и определения длительности нажатия. Алгоритм такой: опрашиваем GPIO с некоторой частотой, игнорируем быстрые осцилляции, замеряем время между нажатием и отжатием. Ниже фрагмент функции, определяющей короткое или длинное нажатие:
const uint16_t DEBOUNCE_MS = 30;
const uint16_t LONG_PRESS_MS = 500;
const uint16_t MIN_SHORT_MS = 25;
bool stablePressed = false;
unsigned long pressStartMs = 0;
void pollTouch() {
bool pressed = digitalRead(TOUCH_PIN); // чтение сенсора (активный HIGH)
if (!stablePressed && pressed) {
// кнопка только что нажата
stablePressed = true;
pressStartMs = millis();
}
if (stablePressed && !pressed) {
// кнопка отпущена
stablePressed = false;
unsigned long held = millis() - pressStartMs;
if (held >= LONG_PRESS_MS) {
doLongTapImpl(); // обработка долгого нажатия
} else if (held >= MIN_SHORT_MS) {
doShortTapImpl(); // обработка короткого нажатия
}
}
}
Функции doShortTapImpl() и doLongTapImpl() в свою очередь разруливают, что именно делать при нажатии в зависимости от текущего состояния. Например, короткий тап в тамагочи вызывает смену эмоции (случайное лицо) и проигрывает короткую анимацию, короткий тап в меню переключает на следующий пункт, а в играх – заставляет персонажа прыгнуть или совершить действие. Длинное нажатие, наоборот, выступает как «Enter/Exit»: из тамагочи – вход в меню, в меню – запуск выбранной игры, в спецрежимах – может выходить обратно. Такой вот минималистичный UI на одном сенсоре. Кстати, короткий звуковой сигнал beep сопровождает любое нажатие (чисто для тактильного ощущения).

Меню и структура программы
Раз уж у нас несколько режимов и игр, нужно удобное меню для их запуска.
Меню в MKA – многоуровневое, текстовое, управляется той же одной кнопкой:
-
Первый уровень – корневой. Здесь несколько пунктов, например: Games, Modes, About, Exit. (с момента фоток добавил смену языка и уже большая часть на русском)
-
Второй уровень – подменю. Например, выбрав Games, попадаем в список доступных игр. Выбрав Modes (условно «режимы» или «fun»), увидим специальные режимы и пасхалки.
Навигация: короткий тап перелистывает пункты, длинный – входит в выбранный. Сделал ещё псевдо-пункт < Back> для удобства возврата. Меню реализовано простым массивом строк для каждого уровня и парой переменных индексов. Когда пользователь нажимает долго на пункт, происходит либо переход в подменю, либо запуск игры/режима. Фрагмент кода для выбора пункта:
void selectMenuItem() {
if (menuLevel == 0) {
// на корневом уровне
enterRootItem(currentIdx); // перейти в выбранный раздел
} else if (menuLevel == 1) {
if (currentIdx == 0) {
// пункт "< Back>" в списке игр
menuLevel = 0;
currentIdx = 0;
} else {
startGameByIndex(currentIdx); // запуск выбранной игры
}
} else {
// для возможных будущих уровней
menuLevel = 0;
currentIdx = 0;
}
}
Как видно, логика несложная: например, на уровне игр currentIdx == 0 зарезервирован под «Back», остальные – соответствуют играм (Dino, Flappy и т.д.). startGameByIndex(idx) просто выставляет currentState = STATE_DINO_GAME или нужную игру и вызывает функцию инициализации этой игры.
Отрисовка меню (renderMenu()) выводит 4 пункта за раз (экран маленький, больше не влазит) и стрелочки вверх/вниз, если список длиннее. Так что пришлось делать прокрутку списка – чтобы было понятно, куда движемся. В целом, меню оказалось чуточку громоздким для одной кнопки, но работает вполне терпимо.

Игры: Dino, Flappy, Tower и другие
Главная забава в MKA – это, конечно, игры, пусть и очень простые. Пока реализовано несколько мини-игр, все – с управлением одним действием (tap):
-
Dino Game. Тот самый динозавр, который скачет через кактусы в оффлайн-режиме Chrome. Реализован в пиксельной графике: динозаврик – some 10×16 пикселей спрайт, кактусы генерируются случайно. С каждым прыжком скорость слегка растёт. Игрок управляет прыжками динозавра (короткое нажатие = прыгнуть). Цель – пробежать как можно дальше, не врезавшись. После столкновения выводится "GAME OVER" и счёт. Кстати, звук прыжка – короткий бип, а при проигрыше кубик ог��рчённо молчит.
-
Flappy Bird. Клон культовой Flappy Bird: мой герой – пиксельная птичка, которая падает вниз, а короткие нажатия подбрасывают её вверх. Пролетаем сквозь отверстия между столбами. Физика примитивная (гравитация + импульс), но играть можно. Скажу честно, на маленьком экране вполне сносно, пару очков набрать реально. После гибели (столкновения) снова видим счёт.
-
Tower (Башенка). Игра по мотивам Stack: на экране движется платформа, и надо вовремя нажать, чтобы «уронить» её на предыдущий этаж башни. Если платформа не совпала идеально, свисающая часть отсекается. Постепенно скорость увеличивается. Цель – отстроить как можно более высокую башню, пока не промахнёшься слишком сильно. Одно нажатие сбрасывает текущий блок.
-
Paddle. Импровизация на тему Pong/Breakout, адаптированная под одну кнопку. Здесь на экране платформа двигается туда-обратно автоматически, а мяч отскакивает. Нажатием игрок меняет направление движения платформы. Нужно отбивать мяч и не дать ему упасть. В одиночку довольно хаотично, но как техническая демка – почему бы нет.
-
Lane Runner. Нечто вроде олдскульной «второклассной» игры: наш герой бежит по шоссе с двумя полосами, объезжая препятствия. Нажатием переключаемся между левым и правым рядом (lane). Препятствия (скажем, машинки) появляются случайно. Задача – уворачиваться, сколько сможешь, набирая очки за каждое преодоленное препятствие. Очень простая, но зато динамичная игра.
-
Reaction Test. Не совсем игра, а скорее режим на реакцию. Кубик гасит экран и потом в случайный момент включает что-то (например, мигает или пищит), и нужно как можно быстрее нажать кнопку. Время реакции выводится на экране. Долгим нажатием можно перезапустить/выйти. Полезная штука, чтобы понять, стоит ли вам сегодня садиться за руль.
Все игры пишутся вручную с костылями, без игровых движков, естественно. Где-то используется простая физика (как во Flappy), где-то чисто тайминги и рандом. Ограничения экрана диктуют минимализм: в основном графика – это прямоугольники и пиксели. Тем не менее, мне удалось впихнуть даже небольшие анимации – например, в Dino при приседании динозавра (хотя приседание управляется автоматически без участия игрока).
Отдельного упоминания заслуживает управление скоростью/сложностью. Практически во всех играх я сделал нарастание сложности со временем: ускоряется прокрутка, сокращаются интервалы появления препятствий и т.п. Это сделано для того, чтобы игры не были бесконечными и скучными.

Режимы и пасхалки: тамагочи, глюки, рикролл
Помимо игр, в MKA есть несколько специальных режимов – для развлечения и демонстрации возможностей (а по факту – некоторые родились из багов и экспериментов):
-
Тамагочи (Faces). Это основной экран по умолчанию, когда ничего не запущено. Кубик показывает «лицо» – забавный смайлик, который может моргать, косить глазами, улыбаться или хмуриться. Каждые несколько секунд выражение случайно меняется, имитируя настроение. Коротким тапом можно заставить его сменить эмоцию принудительно – своего рода «погладить» питомца. Особых геймплейных элементов тут нет, просто цифровой камень живёт на столе, строит вам рожи. Зато мне было весело рисовать пиксельные глазки и рты – они хранятся в массивах и отрисовываются примитивами (окружности, дуги).
-
Счётчик (Counter). Простейший режим: на экране отображается число, которое увеличивается при каждом коротком нажатии. Зачем это нужно? В принципе ни зачем – считать чаек на кухне или подсчитывать, сколько раз за день нажал на кнопку. На самом деле, этот режим появился для отладки сенсора (я отслеживал пропуски нажатий), но остался как пасхалка.
-
Glitch-режим. Специально добавил глючный режим, чтобы… оправдать случайные баги отображения. Шучу, но доля правды есть: однажды из-за ошибки в буфере экран начал выводить абракадабру – шум, битые пиксели. Это выглядело забавно, и я оформил это как отдельный режим "Glitch". В нём на экране нарочно генерируется хаотичный узор, дергаются пиксели, иногда проскакивают перевёрнутые смайлики. Такой артхаус в духе глитч-арта.
-
Rickroll. Куда же без него! В MKA есть скрытый режим, где кубик прокручивает мелодию Never Gonna Give You Up. На экране в это время отображаются строчки типа “Never gonna give you up, never gonna let you down...”. Звук, конечно, крайне условный – пищалка старается, но выходит что-то 8-битное.
-
Leaderboard. Это скорее утилита: таблица рекордов. Кубик хранит в памяти лучшие результаты в играх (рекорд по Dino, Flappy и т.д.), и в отдельном разделе меню можно их посмотреть. Реализовано хранение в EEPROM (точнее, эмуляция EEPROM во флеше ESP32). Так что даже после выключения рекорды не сбрасываются – можно соревноваться с самим собой или передавать кубик другу. Интерфейс примитивный: список “Game – Score”. Сброс рекордов, делается длинным нажатием на кнопке, находясь на экране лидеров.
Конечно, помимо перечисленного, я припрятал пару пасхалок для самых дотошных. Например, если ввести особую последовательность коротких и длинных нажатий, куб покажет секретное послание.

Сборка: пайка, печать и капля безумия
Разработка MKA шла в свободное время, и каждая стадия преподносила свои уроки. Аппаратная сборка оказалась испытанием на терпение: из-за отсутствия печатной платы все соединения выполнены тонкими проводками. ESP32-C3 модуль приклеен внутри корпуса, OLED-дисплей закреплён на передней стенке, провода к нему – длиной пару сантиметров. Паяльником пришлось орудовать ювелирно, чтобы ничего не оторвать. Несколько раз во время отладки провода отрывались – получал “глюки” на экране или пропадание кнопки. В конце концов всё залил термоклеем для надёжности – теперь внутренности кубика выглядят как гнездо кибернетической осы, но зато ничего не коротит.
3D-печать корпуса – отдельная история. Я смоделировал куб из двух половинок, с вырезом под экран и отверстиями под зарядку и кнопку (на случай, если бы решил вывести внешнюю кнопку). Первый вариант напечатал слишком маленьким – не влезла вся электроника. Пришлось переделывать модель, увеличивать на пару миллиметров. В итоге кубик получился не идеальным: местами стенки тонковаты, да и вообще дизайн крайне утилитарный. Зато удалось достичь главного – он похож на игрушку, а не на горсть радиодеталей. При печати использовал pla, на экран поклеил пленку. Теперь устройство можно бросить в рюкзак – экран не поцарапается.
Отладка и баги. Как всегда в самоделках, не всё сразу заработало как хотелось. Перечислю основные затыки и как я их поборол:
-
Дребезг сенсора: поначалу кубик регистрировал одно касание как десяток. Решение – программный дебаунс (см. код выше) и небольшой конденсатор на входе сенсора для сглаживания.
-
Ложные срабатывания: сенсорная кнопка иногда реагировала сама по себе (наводки, статическое электричество). Особенно если близко поднести руку, то могла засчитать нажатие ещё до касания. Я уменьшил чувствительность аппаратно (резистором на TTP223) и программно игнорировал слишком длинные «нажатия» как шум.
-
Переполнение памяти: добавляя игры, внезапно столкнулся с нехваткой памяти (исключение в Arduino при выделении буфера). Пришлось оптимизировать: некоторые константные данные (спрайты, шрифты) хранить в PROGMEM, убрать дубль библиотек (Adafruit GFX уже тянул Adafruit SSD1306, пришлось не подключать лишнего). Также выкинул неиспользуемые части (например, у меня была идея сделать музыку получше, загружал ноты массивом – отложил, когда увидел, сколько это весит).
-
Разряд батареи: в первой версии не было индикации низкого заряда, и однажды куб просто выключился в разгар игры – обидно. Добавил простейший мониторинг: измеряю напряжение на батарее через аналоговый вход, и при просадке ниже порога – на экране тамагочи появляется «грустный» разряженный смайл и отключаются игры (не запускаются, пока не зарядить). Это уберегло меня пару раз.
-
Глюки дисплея: одна из игр (Tower) периодически вызывала кашу на экране. Отладка показала, что я забывал очищать экран или выходить правильно из функций отрисовки, из-за чего разные буферы накладывались. Методично прошёлся по всем
display.clearDisplay()и убедился, что каждый кадр рисуется с нуля. Это устранило артефакты. Но один намеренный глюк я, как говорил, оставил – в пасхалке.
Конечно, при всём при том, стабильность MKA далека от промышленной. Это же хобби-проект: иногда он может перезагрузиться (ESP32, бывает, ловит wdt reset, если зациклиться где-то), иногда музыка идёт не в такт из-за прерываний Wi-Fi (пока что вообще Wi-Fi выключил, но кто знает). Я решил относиться философски: падение – тоже результат. Если куб завис или умер – ну, значит, была такая игровая механика. Благо, перезагружается он быстро.
Что дальше?
Проект MKA ещё будет развиваться. В планах у меня:
-
Добавить mini-SDK для игр. То есть продумать удобный интерфейс, через который можно писать новые мини-игры для куба, и возможно, выпустить код открыто. Вдруг найдутся энтузиасты, которые захотят добавить свою игрушку или анимацию? Сейчас игры довольно жёстко зашиты, но я хочу сделать что-то вроде базового класса Game с методами
start(),update(),onButtonPress()и так далее, чтобы подключать новые игры без копипасты. -
Новые игры. Идей куча! Например, сделать змейку (правда, со сменой направления одной кнопкой – задачка, но придумаю, может, по таймеру поворачивать), или какую-нибудь простенькую текстовую игру (на экране ведь можно и текст квеста крутить). Ещё хочу портировать игру «2048» – она пошаговая, одну кнопку можно трактовать как сдвиг по очереди в разные стороны.
-
BLE-функциональность. Раз уж ESP32-C3 поддерживает Bluetooth Low Energy, грех не попробовать. Хочу прикрутить BLE, чтобы, например, можно было со смартфона управлять кубом. Или отправлять на куб какие-то сообщения/уведомления – тогда он сможет мигать мордочкой при новом письме, условно. Возможностей много, нужно только время это реализовать.
-
Корпус 2.0. Не исключено, что я перепечатаю корпус в более приличном виде, добавлю цветной экран или даже закажу плату. Было бы здорово собрать такой кубик друзьям в подарок, а с платой это упростит процесс (и уменьшит количество проводов=глюков).
-
И конечно, багфикс и полировка. Буду рад избавиться от оставшихся хаотичных багов (кроме тех, что стали фичами).
В целом, проект MKA принёс мне массу удовольствия в процессе, и надеюсь, получился забавным. Завершать я особо не умею, так что. Если вам зашла эта идея – буду рад фидбэку и поддержке. Спасибо за внимание, и помните: лучший баг – это тот, который стал фичей!
Автор: 3loydemidrol
