Геймплей от всего сердца, или как я решил превратить Bluetooth-пульсометр в игровой контроллер прямо в браузере
Всем привет! Меня зовут Павел Барсуков, я frontend-разработчик в компании «Программный Продукт».
Предисловие
Помимо программирования, я давно увлекаюсь настольным теннисом. А еще – анализом тренировок по нему. После каждой тренировки я записываю свои мысли: что получилось, какие элементы стоит доработать, как менялась игра по ходу занятия. Вместе с этим, я всегда отслеживаю пульс, чтобы понимать, насколько интенсивной была нагрузка.
Долгое время для этого хватало смарт-часов. Но у оптических датчиков, которые устанавливаются в них, есть известный недостаток: во время активных движений они могут допускать неточности из-за смещения браслета на руке. Не то чтобы мне хотелось идеальной точности, но согласитесь, если знать, что результат может плавать, хочется избавиться от такой особенности. Поэтому в какой-то момент я решил купить нагрудный Bluetooth Low Energy-пульсометр. Он подключается к часам, и получается так, что мониторинг запущен на них, а данные считываются с нагрудника, благодаря чему измерения становятся заметно стабильнее.
После нескольких тренировок мне стало интересно: а можно ли использовать этот датчик не только по его прямому назначению? Ведь по сути это устройство, которое непрерывно передает по BT поток данных в реальном времени.
Первой идеей было сделать небольшой оверлей для игр. Наверняка многие видели подобное у стримеров или летсплееров: на экране отображается текущий пульс, и зрители могут наблюдать, как он меняется в напряженные моменты. Для меня интерес был также скорее аналитический – в мультиплеерных играх типа Fortnite я начинаю нервничать, когда дело доходит до Топ 10, и пульс подскакивает настолько, что аж в голове шумит.

Но эта идея довольно быстро улетела в мусорку, потому что, во-первых, таких проектов уже достаточно в интернете, а во-вторых, появилась потенциально более интересная: а что, если использовать пульсометр как устройство ввода?
Проработка концепта
Так буквально за пару часов родился проект Pulse Maze – небольшая браузерная игра, в которой игроку нужно выбраться из кругового лабиринта, управляя персонажем с помощью клавиатуры… и собственного сердцебиения.
Почему именно лабиринт? А почему нет? Иногда идеи просто появляются из воздуха, и одну из них я поймал за хвост. А круговая форма показалась удачной, потому что позволяет сделать навигацию достаточно интересной, не перегружая игрока сложными правилами.
Вообще изначально идея была не в том, чтобы заставить человека бегать перед монитором. Хотя все таки немного подвигаться действительно придется. Мне было интереснее решить совсем другую задачу – превратить проверку нового пульсометра в небольшой игровой опыт.
Когда я только купил датчик, быстро встал вопрос – а как собственно проверить, что он работает? Я вышел из ситуации следующим образом: подключил его к часам, запустил тренировку, снял часы с руки и попрыгал на месте. Только когда пульс начал меняться на снятых часах, я убедился, что данные берутся именно с нагрудного датчика, а не с оптического, который используют часы. Этот метод муторный, неинтересный и не дает фидбек сразу.
В Pulse Maze обратная связь появляется мгновенно. Пользователь сразу видит, как его действия влияют на происходящее в игре. Получается одновременно проверить устройство, немного размяться и просто получить удовольствие от процесса.

С точки зрения механики все довольно просто. Персонаж перемещается в полярных координатах (спасибо, матфак, они мне действительно пригодились, ура!). Угловой координатой игрок управляет привычным способом: стрелками на клавиатуре или гироскопом смартфона. А вот радиальная координата полностью зависит от сердечного ритма. Чтобы двигаться от центра лабиринта, нужно повысить пульс, а чтобы приблизиться к центру – наоборот, успокоиться.
Сам лабиринт генерируется процедурно. Сейчас он состоит из пяти концентрических колец по двенадцать сегментов в каждом. После нескольких экспериментов именно такая конфигурация показалась оптимальной: лабиринт получается достаточно интересным, но при этом не требует превращать прохождение в полноценную кардиотренировку. И выглядит не перегружено. В дальнейшем хочется сделать этот параметр настраиваемым, чтобы каждый мог подобрать сложность под себя – или, наоборот, проверить, насколько далеко способен зайти.
Соединение датчика с браузером выполняется посредством Web Bluetooth. Очень удачно, что существует такой удобный способ подключить BLE устройство к браузеру. И при этом необязательно устанавливать какие-то дополнительные приложения – достаточно просто разрешить браузеру на базе Chromium (в Firefox работает через костыли, в Safari не работает вовсе) находить устройства поблизости, и в списке будет отображаться датчик пользователя. Никаких драйверов, никаких приложений – только ваше устройство с Bluetooth на борту.
Реализация
Проект написан на Vue 3 + TypeScript и собирается с помощью Vite. Для такой небольшой игры это даже стек с запасом, но здесь мне хотелось работать с теми технологиями, которые я использую повседневно. Поэтому Vue отвечает за экраны и состояние сессии, Canvas 2D – за игровое поле, а вся математика живёт в обычных TypeScript-функциях и вообще ничего не знает о компонентах. Для генерации кода я использовал ChatGPT 5.6 Terra.
Сервера у Pulse Maze нет. Браузер напрямую подключается к пульсометру, обрабатывает измерения и хранит текущее состояние в памяти. Данные о пульсе никуда не отправляются, а после обновления страницы игровая сессия начинается заново. Для прототипа этого оказалось вполне достаточно. Хотелось сначала проверить саму идею, а не строить серверную инфраструктуру.
Первой интересной задачей оказался сам круговой лабиринт. В коде он представлен не набором линий, а полярной сеткой из 60 ячеек: пяти колец по двенадцать секторов. Каждая ячейка хранит состояние своих границ и ссылки на соседей. Ни одной изолированной ячейки быть не должно. Центр при этом вынесен отдельно, потому что считать его ещё одной ячейкой с двенадцатью одинаковыми соседями было бы довольно странно.
Генерация работает через randomized DFS (Depth-First Search, обход графа в глубину). Если совсем просто, алгоритм начинает из центра и идёт вперёд, каждый раз случайным образом выбирая одно из доступных направлений. Когда дальше идти некуда, алгоритм возвращается назад до ближайшей развилки и продолжает исследование. Так постепенно строится связный лабиринт без изолированных областей, причем каждый запуск создает новую уникальную карту. После этого в одной из внешних стен просто открывается выход.
Почти сразу всплыла неожиданная проблема. Формально лабиринт был правильным, но играть в него было скучно. Иногда генератор строил почти прямую дорогу наружу, и вся игра заканчивалась раньше, чем пульс успевал заметно измениться. Поэтому каждый результат проходит небольшой вступительный экзамен: BFS (Breadth-First Search, обход графа в ширину) проверяет связность и путь до выхода, решение должно занимать не меньше 30 ячеек, хотя бы раз вести обратно к центру и иметь несколько боковых ответвлений. Если условия не выполнены, лабиринт генерируется заново. Отдельный тест повторяет эту процедуру тысячу раз, потому что одна удачная случайная генерация, конечно, ничего не доказывает.

Чтобы было нагляднее, я добавил визуализацию под get параметр stepMazeGen на странице игры. Можно открыть ее и посмотреть, как каждый раз строится новый лабиринт.
Положение игрока всё время хранится в полярных координатах: радиус плюс угол. В привычные x и y оно переводится только перед отрисовкой. Благодаря этому движение совпадает со структурой лабиринта: клавиатура или наклон телефона меняют угол, а пульс меняет радиус.
Коллизии тоже пришлось считать вручную. Для круговых стен проверяется пересечение по радиусу, для боковых – переход через границу сектора. При этом игрок считается не математической точкой: у него есть собственный радиус и небольшой зазор до стены. Если между кадрами прошло слишком много времени, перемещение дополнительно разбивается на несколько коротких шагов, чтобы после подвисания вкладки персонаж не телепортировался сквозь препятствие. Никакого физического движка здесь нет – вся математика выполняется внутри игрового цикла на requestAnimationFrame. Каждый кадр сначала обновляет координаты игрока, при необходимости разбивает слишком большой шаг на несколько маленьких, проверяет коллизии и победу, а затем только переходит к отрисовке нового состояния. Благодаря этому обновление состояния и отрисовка идут в одном месте, а даже после просадки FPS персонаж не успевает «перепрыгнуть» через стену.
Сам Canvas подстраивается под доступное место, остаётся квадратным и учитывает devicePixelRatio, поэтому лабиринт не превращается в мыло на экранах с высокой плотностью пикселей.
С пульсометром браузер общается через стандартный BLE Heart Rate Service. После нажатия кнопки Web Bluetooth показывает системный список устройств, подключается к выбранному датчику и подписывается на уведомления характеристики Heart Rate Measurement. Сам пакет устроен довольно просто. Первый байт хранит флаги – по ним понятно, записан пульс в 8 или 16 битах. Следом идёт само значение BPM. Здесь особенно приятно, что не пришлось разбирать приватный протокол конкретного производителя - совместимый датчик должен работать по одному и тому же стандарту.
const device = await navigator.bluetooth.requestDevice({
filters: [{
services: ['heart_rate']
}]
});
// Подключение к устройству из браузера
const server = await device.gatt.connect();Использовать сырые измерения напрямую оказалось плохой идеей. Пульс может прыгнуть на несколько единиц между соседними пакетами, и точка начинала нервно дёргаться вслед за каждой цифрой. Поэтому поток сначала проходит экспоненциальное сглаживание, а перед игрой запускается двадцатисекундная калибровка. Значения вне диапазона 35-220 BPM отбрасываются, а персональный baseline вычисляется как медиана оставшихся измерений и фиксируется на всю игровую сессию.
/**
* Функция сглаживания
*/
export function smoothBpm(
previousBpm: number,
currentBpm: number,
smoothingFactor = HEART_RATE_SMOOTHING_FACTOR,
): number {
return previousBpm + smoothingFactor * (currentBpm - previousBpm)
}Сам BPM не задаёт координату игрока напрямую. Он превращается в коэффициент скорости от -1 до 1 относительно значения, полученного во время калибровки: низкий пульс двигает точку к центру, высокий – наружу, а между порогами скорость меняется плавно. В противном случае одно неудачное измерение могло бы мгновенно перебросить игрока через половину лабиринта, а люди с разным пульсом в покое получили бы совершенно разное управление.
С гироскопом возникла ещё одна небольшая математическая радость. Оси beta и gamma привязаны к телефону, но понятия «по часовой стрелке» и «против часовой» зависят от того, в какой части круга сейчас находится игрок. Поэтому вектор наклона проецируется на касательную к текущему кольцу. Добавил также мёртвую зону и плавный рост скорости, чтобы точка не уезжала от естественного дрожания рук. Спасибо матфаку ещё раз.
Вся сессия собрана как явная машина состояний: от отсутствия поддержки и подключения до калибровки, игры, паузы и финиша. Если датчик отключился, новые данные не приходят больше трёх секунд или вкладка стала неактивной, игра ставится на паузу. Автоматически продолжать её после восстановления соединения я не стал: пользователь сам нажимает кнопку и возвращается в процесс, а таймер за время паузы не растёт.
Наконец, для разработки есть режим ?debugHeartRate=true. В нём настоящий BLE-датчик заменяется эмулятором, а виртуальный пульс можно повышать и понижать стрелками. Рядом выводятся baseline, сырой и сглаженный BPM, полярные координаты и текущая ячейка. Без этого режима каждую проверку пришлось бы проводить в пульсометре и с прыжками на месте, а я всё-таки хотел написать игру, а не случайно устроить себе интервальную тренировку. Убирать в проде не стал, поэтому даже если у вас нет подходящего устройства, вы все равно сможете попроходить лабиринт.
Это вообще игра или утилита?
Чем больше я думал над проектом, тем сильнее понимал, что делаю не совсем игру. С одной стороны, да, перед нами игровой процесс: есть цель, лабиринт, управление и желание пройти уровень как можно быстрее. С другой – это отличный способ протестировать новый пульсометр.
После покупки подобного устройства большинство людей делает так же, как и я: подключает его к телефону или часам, смотрит, меняются ли цифры на экране, и на этом тестирование заканчивается. Мне хотелось сделать процесс проверки устройства интереснее. Чтобы вместо сухой демонстрации чисел человек сразу увидел, что датчик действительно работает, и смог почувствовать, как его собственное тело становится частью управления.
Что хочется сделать дальше?
Сейчас Pulse Maze – это скорее прототип. Мне было важно ответить на главный вопрос: можно ли вообще управлять игрой таким необычным способом? Оказалось, что можно. Дальше хочется развивать идею. Например:
добавить несколько игровых режимов с разными механиками;
реализовать таблицу рекордов по времени прохождения или каким-нибудь абстрактным очкам;
сделать более сложную процедурную генерацию лабиринтов;
сохранять статистику попыток и показывать, как меняется характер прохождения со временем;
добавить профиль устройства, чтобы можно было посмотреть не только пульс, но и более подробную информацию об устройстве.
Заключение
Когда я заказывал нагрудный датчик, я хотел лишь точнее измерять нагрузку во время тренировок по настольному теннису. В итоге это привело к идее браузерной игры, в которой главным контроллером становится не мышь, не геймпад и даже не гироскоп, а собственное сердце. Иногда достаточно просто задаться вопросом «а что ещё можно сделать с этой штукой?», чтобы получить идею нового проекта.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.