Мы создали игру «Морской бой» (Battleship) на двух игроков в реальном времени при помощи микроконтроллеров Raspberry Pi Pico W, обменивающихся данными через UDP. К каждому устройству подключён VGA-дисплей 320×240, джойстик для размещения кораблей и ударов, а также тактильные кнопки для взаимодействия с игрой. Для проекта разработан собственный протокол ходов на основе конечных автоматов и интегрирована звуковая обратная связь на основе как DMA, так и прерываний.

Архитектура

Наша идея возникла из желания воссоздать классические настольные игры на основе микроконтроллеров, параллельно получив опыт работы с управлением в реальном времени, сетью и мультимедийной обратной связью. Мы выбрали «Морской бой», потому что это пошаговая игра на квадратном поле в клетку, охватывающая многие базовые элементы из нашего учебного курса «Проектирование цифровых систем на основе микроконтроллеров».

Игра состоит из следующих состояний (или «страниц»):

  1. Начальный экран: отображает название игры и ждёт нажатия на жёлтую кнопку.

  2. Выбор уровня: позволяет игроку выбрать сложность и отображает правила

  3. Размещение кораблей: курсор, позволяющий разместить пять кораблей с обратной связью и проверкой правильности.

  4. Геймплей: конечный автомат, обеспечивающий попеременные ходы при помощи координации по UDP.

  5. Конец игры (победа/проигрыш): запускается, когда потоплены все корабли одного игрока.

Пошаговый конечный автомат FSM (PlayerState) управляет потоком состояниями между YOUR_TURN (YT), RECEIVE_RESPONSE (RR) и RECEIVE_ATTACK (RA).

PlayerStateFSM

Оборудование: мы использовали двухъядерный микроконтроллер RP2040, в котором Core 0 управляет связью, а Core 1 обрабатывает обновления дисплея и игровую логику. Поначалу мы думали о том, могут ли на одном микроконтроллере одновременно работать игры обоих игроков, однако почти сразу признали такое решение непрактичным из-за ограниченности ресурсов и повышения сложности управления двумя экземплярами игры. Создав две полнофункциональные независимые системы, мы позволили игрокам участвовать в игре в двух физически удалённых точках при условии, что оба устройства подключены к одной сети Wi-Fi. Благодаря этому обеспечивается более надёжная схема игры, пусть и с увеличением общей стоимости системы.

Программное обеспечение:

При исследовании способов связи мы рассматривали такие стандартные протоколы, как UART и I2C, но они требовали физически близкого расположения устройств. Так как «Морской бой» — стратегическая игра, в которой необходимы сокрытие информации и расстояние между игроками, нам требовался способ связи, поддерживающий дистанционное взаимодействие, поэтому мы перешли к изучению сетевых протоколов. Из TCP и UDP мы выбрали UDP за его скорость и простоту. В игре используется строгая пошаговая модель, а каждый ход выполняется только после получения корректных данных, поэтому протокол на основе подтверждений наподобие TCP был бы необязательным и слишком сложным. UDP позволил нам избежать оверхеда, обеспечив при этом надёжную синхронизацию благодаря конечному автомату, контролирующему поток выполнения игры.

Наш проект не включал в себя использование или создание каких-либо патентов. Вся базовая функциональность была реализована независимо, без реверс-инжиниринга проприетарных алгоритмов и архитектур. Драйверы VGA-дисплея и звуковые библиотеки были адаптированы из ресурсов, предоставленных нам в рамках учебного курса или взятых из открытых кодовых баз с разрешительными лицензиями. Следовательно, чтобы не нарушать их условия, мы не использовали защищённые правом интеллектуальной собственности и торговыми марками материалы, а сторонние компоненты или имели статус общественного достояния, или были допускали использование в научных или опенсорсных целях.

Демонстрация

Архитектура проекта

Игра «Морской бой» была реализована в виде пошаговой системы для двух игроков на основе микроконтроллеров Raspberry Pi Pico W с использованием VGA-вывода для графики и UDP для синхронизации игроков. Базовая игровая логика выполняется в выделенном потоке анимации (protothread_anim), обрабатывающем переключения состояний через конечный автомат (finite state machine, FSM). Этот FSM перемещает игрока между несколькими этапами: начальным экраном, выбором сложности, размещением кораблей, активным геймплеем и условиями победы/проигрыша. Интерактивность реализуется при помощи джойстика и физических кнопок; нажатия кнопок распознаются через прерывания GPIO с учётом дребезга контактов.

Визуальные элементы (сетка поля, курсоры, корабли и сообщения) рендерятся на VGA-дисплей 320x240 при помощи графических процедур, встроенных в библиотеку vga256_graphics. Эти функции обеспечивают контроль на уровне пикселей, они важны для таких динамических элементов, как отрисовка и обновление частей кораблей во время геймплея. Рендеринг дисплея и движение курсора синхронизированы с логикой игры для обеспечения плавного взаимодействия игрока с игрой. Ввод с джойстика преобразуется в плавные движения курсора с переносом через границы сетки. Чтобы избежать мерцания экрана, при движении сохраняется и восстанавливается предыдущее состояние курсора.

Связь между устройствами игроков обрабатывается при помощи UDP через WiFi на основе Pico W. Управление этим слоем связи выполняется в двух protothread: protothread_receive и protothread_send. Эти потоки обрабатывают парсинг и форматирование сообщений, ретранслирующих состояние игры, изменения на поле и координаты атак. UDP-пакеты структурированы на основе семафоров для координации времени получения и отправки сообщений, обеспечивая синхронизацию игрового состояния между устройствами. Параметры сети и WiFi основаны на коде шаблона picow_udp_beacon.h, написанного Брюсом Лэндом и Хантером Адамсом.

Мы решили что игра будет работать только с VGA, а код игровой логики будет иметь полную поддержку CPU, поэтому для управления выводом ЦАП мы использовали каналы DMA. Один канал данных считывает с начала до конца массив сэмплов звукового эффекта и записывает сэмплы в регистр SPI. Ещё один канал сбрасывает канал данных, чтобы начать с первых данных в массиве сэмплов. После завершения работы канала управления он переключает канал данных в режим вывода ЦАП. Поэтому для воспроизведения звукового эффекта мы вызываем канал управления.

Изначально мы разделили нашу нагрузку и реализовали воспроизведение аудио на основе DMA в отдельном проекте PICO. На этом этапе каналы DMA для звуков взрывов и всплесков работали корректно. Однако при интеграции аудиокомпонента в основной проект мы столкнулись с проблемой: DMA звукового эффекта всплеска перестал работать, несмотря на то, что мы явным образом запрашивали неиспользуемый канал DMA и привязывали его к другому таймеру. Так как время на реализацию проекта было ограничено, для воспроизведения всплеска мы решили использовать процедуру сервиса прерываний. Для управления выводом звука и тишиной мы создали глобальный флаг, имеющий значение false. Если основной программе нужно воспроизвести звук всплеска, то она присваивает этому флагу значение true, а прерывание передаёт сэмплы. После завершения всех сэмплов флаг сбрасывается на false, а прерывание снова готово к воспроизведению звука.

Для пользовательского ввода мы добавили джойстик и две кнопки. У джойстика есть пять проводных соединений, одно из которых — GND, а оставшиеся четыре используются для каждого из переключателей направления. Внутри эти переключатели действуют, как движения курсора в четырёх направлениях. Курсор помогает перемещать выделение и управлять им, а также размещать корабли в игре. Мы проверяем каждый отдельный переключатель, и на основании этих данных определяем позиции X и Y на VGA-экране. Обе кнопки сконфигурированы, как подтягивающие вверх вводы. Если они не нажаты, вход GPIO считывает 1, если нажаты, то 0. Процедура прерываний просто считывает входы GPIO и обновляет две глобальные переменные: текущее и предыдущее состояния каждой кнопки. Эти состояния используются в программе игры для определения ввода игрока.

Для начальной страницы и страницы выбора уровня сложности мы используем логику по перепаду сигнала. Действия выполняются, только когда кнопка переходит из ненажатого в нажатое состояние. Этот способ хорошо работает, потому что логика основного цикла на этих страницах относительно короткая. Однако в остальных частях игры мы используем исключительно текущее состояние кнопок. Если кнопка в данный момент нажата, то связанное с ней действие немедленно выполняется. Причина в том, что логика игры становится сложнее и затратнее по времени, что усложняет надёжное определение кратковременных переходов нажатий в одной итерации цикла. Проверяя текущее состояние напрямую, мы обеспечиваем возможность распознавания нажатий кнопок.

На схеме ниже показан поток кода для высокого уровня сложности:

Оборудование

Аппаратная архитектура создана на основе двух плат Raspberry Pi Pico W, служащих в качестве независимых игровых устройств для каждого из игроков. В каждое устройство интегрирована различная периферия для поддержки дисплейного вывода, взаимодействия с пользователем и звуковой обратной связи. Pico W имеет двухъядерные архитектуры, что позволило нам использовать Core 1 для отображения обновлений, игровой логики и обработки ввода игрока, а Core 0 — для обработки UDP-соединения.

VGA-графику мы генерируем при помощи 8-битных цветов, подключенных к GPIO 8-15. Контакты подключены к резисторам на 330 Ом, 660 Ом и 1 кОм для каждого из сигналов (красного, синего и зелёного). Линии HSync и VSync подключены к контактам GPIO 16 и 17 с соединением GND Pico. Буферизация кадров и отрисовка обрабатываются в специальном цикле отображения, использующем DMA для снижения нагрузки на CPU.

Четыре направления движения джойстика (влево, вправо, вверх и вниз) подключены к контактам GPIO 22, 26-28. Мы подключили эти контакты к резисторам на 330 Ом, а затем соединили их с внутренними переключателями внутри джойстика. Также мы добавили две кнопки на GPIO 18 и 19 в виде подтягивающей вверх цепи. Другой контакт соединён с линией 3,3 В Pico. Также мы добавили между GPIO этой кнопки и GND резистор, чтобы когда она не нажата, вход имел низкое значение.

Также для генерации аудио мы использовали 12-битный ЦАП (MCP49), подключенный через SPI. Соединения SPI соединены с контактами GPIO 5 (выбор чипа), 6 (тактовый генератор), 7 (MOSI). Мы предварительно обработали файлы WAV со звуками взрыва и всплеска, обозначающими попадание и промах. Каналы DMA снова были сконфигурированы для обработки воспроизведения звука передачей данных сэмплов непосредственно в регистр SPI. Мы дополнили старые шаблоны для потокового воспроизведения аудио через ЦАП. Также мы подключили отладчик на RPi для мониторинга производительности системы в реальном времени и устранения проблем в процессе разработки.

Оценка затрат

Деталь

Количество

Оценочная стоимость (USD)

Микроконтроллер RP2040 (Raspberry Pi Pico)

2

$8

Модуль VGA-дисплея

2

$15

Кнопки (для пользовательского ввода)

4

$5

Резисторы (для интерфейсов кнопок)

10

$1

Джойстик

2

Проводники и макетная плата

20

$5

Кабель Micro-USB (для подачи питания)

2

$2

В будущем мы, возможно, будем использовать LED-дисплей

2

$12

Общая оценочная стоимость

$50

Сложности реализации

Одним из сложных аспектов проекта стало обеспечение синхронизации геймплея между двумя устройствами через ненадёжное UDP-соединение. Для этого требовалось тщательное управление состояниями игры и обработка порядка сообщений и контента в потоке получения данных. Ещё одним сложным моментом стала реализация системы перемещения курсора. Для настройки преобразования ввода с джойстика в удобную навигацию по сетке с сохранением отзывчивости нам понадобилось множество итераций.

Трудности вызвало и избавление от дребезга контактов при помощи прерываний GPIO. Система должна была корректно распознавать передний фронт импульса, чтобы избежать ложноположительных срабатываний, а также требовала аккуратного управления переключения состояний кнопок между кадрами. Контакт кнопки был соединён с подтягивающей вверх цепью, поэтому мы добавили резистор от GPIO к заземлению параллельно с соединением кнопки. Это устранило проблему постоянного срабатывания ввода при отсутствии нажатий.

Изначально мы планировали генерировать звуки взрыва и всплеска при помощи Direct Digital Synthesis (DDS), как делали это в одной из лабораторных работ. Но потом мы взглянули на частотный спектр всплеска и взрыва, и они оказались сложными, с быстрыми и нестабильными паттернами волн частот. Поэтому мы решили работать с аудиосэмплами при помощи ЦАП.

Кроме того, нетривиальной задачей оказалась реализация логики размещения кораблей. Чтобы корабли умещались в границы сетки и не накладывались друг на друга, необходимы были условные проверки и интерактивная обратная связь с использованием дисплея. Также продуманного дизайна требовало управление обновлением VGA-экрана без мерцания. Мы не перерисовываем в каждом кадре экран целиком, а только изменившиеся области, а предыдущие пиксельные данные кэшируются при перемещении курсора. Интеграция воспроизведения звука через DMA и SPI добавила ещё один уровень сложности, особенно при координации таймеров и обеспечении правильного форматирования и потоковой передачи аудиосэмплов.

Дополнительные проблемы создало и преобразование из fix15 в int/float. Например, при определении позиции курсора мы использовали в качестве типа данных fix15, несмотря на то, что позиция курсора, по сути, обрабатывалась в коде, как integer. Это несоответствие вызвало мелкие сложности, например, некорректное сравнение координат, ошибки отрисовки и несогласованное поведение при обновлениях. Чрезмерное использование fix15, особенно там где точность чисел с фиксированной запятой не была необходима, усложнила логику и понизила понятность кода.

В процессе реализации проекта мы пробовали множество решений, которые в конечном итоге оказались неподходящими. Изначально мы ради повышения производительности пытались запускать оба звуковых эффекта при помощи каналов DMA, но такое решение было ограничено доступными ресурсами DMA. Также на ранних стадиях разработки был реализован режим отката к последовательному вводу для симуляции тестовых атак, но в финальной версии мы от него отказались. Кроме того, наша первая система логики многопользовательской игры использовала связь по UDP на основе опросов, из-за чего возникали несовпадения таймингов и рассинхронизация между игроками; позже мы решили эту проблему благодаря связи на основе прерываний. Также мы экспериментировали с использованием изображений в качестве сеток игрового поля, однако из-за ограничений DMA реализовать эту задумку не удалось. В будущем мы планируем снова вернуться к ней.

Авторство кода

В проекте активно использовался фундаментальный код, написанный Брюсом Лэндом и Хантером Адамсом. Их библиотека vga256_graphics послужила основой для всех процедур дисплейного рендеринга, в том числе для отрисовки фигур, текста и синхронизации VGA. Кроме того, схема связи по UDP и фреймворк обработки сообщений основан на их примерах интеграции picow_udp_beacon и lwIP. Эти источники оказались крайне важными для обеспечения возможности взаимодействия игроков по WiFi в реальном времени и создали стабильный графический фреймворк, поддерживающий динамические элементы игры.

Результаты

Система демонстрирует приличную производительность скорости исполнения, интерактивности и синхронизации. VGA-дисплей поддерживает стабильную частоту кадров с минимальным мерцанием, а обновления курсора остаются отзывчивыми, с задержками менее 100 мс, что позволяет обеспечивать плавный игровой процесс. Связь по UDP между устройствами в реальном времени гарантирует точную и своевременную синхронизацию ходов, необходимую для пошагового игрового процесса «Морского боя». Однако время от времени мы наблюдали торможение при рендеринге текста, и в особенности при перезаписи строк переменной длины. Более существенная проблема возникла из-за того, что VGA VSync и связь по UDP использовали один канал DMA, и это ограничение нам так и не удалось устранить из-за ограниченной гибкости DMA в RP2040. Это привело к неполному рендерингу каждой второй строки VGA, создающему полосатые визуальные артефакты. Несмотря на эти сложности, в самых важных аспектах система обеспечивала точное поведение: координаты корректно преобразовывались из экранных пиксельных значений и обратно, звуковые частоты эффектов соответствовали ожиданиям (всплеск на 11 кГц и взрыв на 16 кГц), а тайминг сигнала VGA оставался синхронизированным с правильным выводом цветов. Эти результаты привели к созданию качественно функционирующей встроенной системы реального времени с возможностью дальнейшей оптимизации визуального рендеринга и распределением каналов DMA.

Для обеспечения надёжной и безошибочной работы мы реализовали в архитектуре множество защитных механизмов. Ввод с кнопок на начальной странице и странице выбора уровня сложности обрабатывается распознаванием переднего фронта сигнала, по сути, устраняя проблемы, вызываемые дребезгом контактов и ложными срабатываниями. Благодаря этому во время игрового процесса регистрируются только намеренные нажатия. Кроме того, в логике интерфейса были реализованы проверки границ, позволяющие избежать перемещения курсора от выхода из видимой области дисплея, неверного размещения кораблей и координат атак. Эти меры защиты позволили создать более стабильный и предсказуемый игровой процесс.

При проектировании системы мы учитывали юзабилити, стараясь имитировать знакомую структуру и процесс игры настольной игры «Морской бой». На каждом этапе игры отображаются понятные текстовые подсказки, помогающие пользователям в подготовке к игре, геймплее и смене ходов. Благодаря этому с системой могут без проблем взаимодействовать и её разработчики, и новые пользователи, ничего не знающие об игре.

Галерея

Заключение

Получившийся продукт полностью оправдал наши ожидания. Реализация конечного автомата для управления ходами, графической обратной связи на VGA-дисплее в реальном времени и два звуковых эффекта попадания и промаха существенно повысили интерактивность и удобство игрового процесса. В будущих итерациях возможно дополнительное совершенствование различных аспектов, в том числе добавление более плавного управления вращением и размещением кораблей, переход с UDP на TCP для гарантированной доставки сообщений и реализация более надёжной беспроводной связи и механизма повторных попыток для учёта сетевых помех. Архитектура соответствует спецификациям таймингов VGA и использует стек UDP/IP через WiFi, поддерживаемый PicoW SDK.

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