Привет Хабр, сразу скажу, что я не особо программист, но хотелось поделиться хобби и может собрать соплеменников и единомышленников.
Итак начнем....
Почему вообще делать самодельный кубик, который ещё и глючит, но при этом умеет запускать игры вроде пресловутого «динозаврика» из 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 принёс мне массу удовольствия в процессе, и надеюсь, получился забавным. Завершать я особо не умею, так что. Если вам зашла эта идея – буду рад фидбэку и поддержке. Спасибо за внимание, и помните: лучший баг – это тот, который стал фичей!