Привет Хабр, сразу скажу, что я не особо программист, но хотелось поделиться хобби и может собрать соплеменников и единомышленников.

Итак начнем....

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

Комментарии (0)