[#] Герион для Брус-16
hugeping(ping,1) — All
2026-02-01 14:00:08


# Фэнтези консоли

Препятствия могут стимулировать творчество. Как частный пример -- ограничения в программировании тоже сами по себе легко превращаются в стимул. Возможно поэтому стали популярны "фэнтези консоли". Возьмём в качестве примера pico-8 -- наверное, самую популярную фэнтези консоль. На самом деле оказывается, что это стилизованная среда для программирования на Lua, а не "настоящая" вымышленная машина. Существует множество проектов-подражателей и все они в основном повторяют концепцию стилизованной среды. В какой-то мере это можно сказать и про мой проект rein, хотя я и не позиционирую его как стилизацию.

Конечно, если говорить о "фентези консоли" ожидаешь увидеть спроектированный процессор со своей системой команд и построенную на его основе систему. Хотя бы гипотетически возможную в виде железа. В качестве примера можно привести uxn. https://wiki.xxiivv.com/site/uxn.html

Но и в таком случае, сложно уйти от "стилизации". Понятно, что пытаясь разработать простую систему мы невольно возвращаемся в эпоху домашних 8-битных систем и черпаем вдохновение там. А можно ли сделать что-то по настоящему самобытное? Оказывается, можно!

# Брус-16

Что если разработать самобытную и простую машину с учётом того, чтобы сделать её в том числе и в "железе"? Такая система была бы не только интересной игрушкой, но и приносила бы пользу в качестве обучающего проекта. Действительно, ведь здесь есть целый пласт связанных задач: от написания компиляторов до разработки аппаратуры и ПО! Пусть в упрощённой форме, но решая нестандартные задачи проектирования такой системы мы получим обобщённые знания и опыт в смежных областях разработки. Увидим связи между этими областями. Увидим, как особенности аппаратуры влияют на ПО и средства разработки и наоборот.

Совсем недавно Пётр Советов (true-grue) создал такой проект. Встречайте:

> Брус-16 - виртуальная машина, позиционируемая как "учебная 16-битная игровая приставка с оригинальной, минималистичной архитектурой", спроектирована с прицелом на аппаратную реализацию на ПЛИС.

https://github.com/true-grue/Brus-16

Что у нас есть?

- изображение: 640x480@16bpp 60fps строится на основе 64-х закрашенных прямоугольников;
- звук: 16 осциляторов на основе синуса с параметрами;
- эмулятор на Си (SDL3);
- реализация на дешёвой ПЛИС (пока без звука);
- 8192 16-битных слов на код;
- 8192 16-битных слов на данные;
- компилятор на основе ast-модуля Python.

Ещё во время разработки Брус-16 true-grue намекнул мне: мол, неплохо было бы написать игру для его "приставки". Но графическая система Бруса показалась мне слишком слабой, чтобы захотеть что-то сделать. Я был готов морально даже к разрешению 64x64, но тут же всего 64 прямоугольника! Разве можно сделать что-то, что будет похоже на игру? Ну, кроме pong и подобного... Некоторое время я "давил" на Петра, надеясь что он увеличит число прямоугольников хотя бы до 128... Но тщетно.

Однако, примерно в это время я завязал играть в Elite Dangerous и освободившееся подсознание, видимо, начало свою работу. Так что скоро я незаметно для себя начал писать игру для Брус-16. Вы можете запустить её собрав Brus-16 или запустив онлайн: https://true-grue.github.io/Brus-16-Apps/brus16.html (выбрать Gerion). Клавиши zx, стрелки. Я подожду.

Ну что же, продолжаем!

# Среда программирования

Я начал с того, что попробовал просто собрать и запустить хоть что-то. В состав Brus-16 входит простенький компилятор на основе ast-модуля Python, который компилирует python-подмножество в байткод. Понятно, что от питона осталось только синтаксическое дерево, да и то урезанное. Например, у нас нет деления. Все числа -- 16-битные целые. Есть адресная арифметика. Никаких сложных типов данных нет. Массивы - просто последовательность 16-битных слов. Циклы - только написанные вручную while с условием и тд. и тп.

Когда я программировал на этом я действительно почувствовал отголоски тех самых "старых добрых времён". И ещё я подумал, что надо было писать на ассемблере. :)

То, что компилятор реализован на питоне даёт интересную возможность -- у нас из коробки появляется "препроцессор". Компилируемая программа это буквально программа на питоне которая вызывает функцию компиляции из текста программы зашитого в виде f-строки. Таким образом, программа для Брус может состоять как бы из двух частей. В преамбуле мы пишем обычный python код, который, скажем, формирует массивы с данными для игры. Этот код выполняется непосредственно перед компиляцией кода для Брус. А потом подготовленные данные пробрасываются с помощью подстановок в f-строку. Но лучше один раз увидеть. Пример макроса:

В преамбуле:
def mswap(a, b):
    return f'''{a} ^= {b}; {b} ^= {a}; {a} ^= {b}'''

В f-строке:
    if (ex < x) | (ey < y):
        {mswap("x", "ex")}
        {mswap("y", "ey")}


Кстати, тут видна ещё одна особенность языка. Нет булевых операций. В условных выражениях остаётся пользоваться битовыми операциями.

# Идея игры

Идея игры формировалась вокруг ограничений. 64 прямоугольника, на первый взгляд, это очень мало! А мне хотелось сделать игру, которая была бы именно игрой и была бы интересна сама по себе. В которой ограничения не воспринимались бы как явный недостаток. И тут тебе дают 64 квадратика!

Сначала я думал о квадратиках как о пикселях. Ну что можно сделать? Воспринимать как кадр 8x8? Ужасно. Мерцать квадратиками через кадр, тем самым удвоить их число? Ещё хуже. Я подумал о лабиринте. Но если лабиринт хоть сколько-нибудь сложен, то даже с оптимизацией расхода прямоугольников их не хватит. Тогда я подумал о зоне видимости! Пусть персонаж игры видит вокруг себя только несколько блоков, а дальше - все скрывает тьма. Но тогда какой смысл в остальной области экрана? Мы просто получаем игру 7x7.

Решение оказалось прямолинейным. Пусть у героя будет радар! Радар будет сканировать окружение и рисовать на месте врагов (а ведь враги - обязательно должны быть!) красные точки! Кстати, код из игры:

# ha-ha :)
def draw_circle(ptr, cx, cy, r, col):
    if (ptr >= {RECT_MEM+RECT_SIZE*RECT_NUM}):
        return ptr
    ptr[0] = 1
    set_rect(ptr, cx - r, cy - r, 2*r, 2*r)
    ptr[5] = col
    return ptr + {RECT_SIZE}

Функция рисует прямоугольник, но принимает параметры так, как будто это закрашенный круг с радиусом. Не знаю почему, но мне кажется это в чём-то любопытным и забавным. Именно эта функция рисует "точки" радара.

Так вокруг ограничений и возникла первая идея игры. Герой-космонавт бродит по лабиринтам и отстреливает "чужих". В игре присутствуют элементы стелса -- герой использует радар так как видит вокруг себя только небольшой освещённый участок.

В качестве оружия предполагалась обычная пушка, стреляющая снарядами. Однако, начав делать игру я понял, что ограничения подталкивают сделать ... лазер! Ведь на лазер тратится лишь один ценный прямоугольник. И анимация лазера - дешёвая операция.

Как только в игре появился лазер, естественным образом возникла идея "батарей", которые опустошаются во время стрельбы и сканирования и восполняются при отдыхе. Также ввод лазера заставил сделать монстров "живучими" и в игре их необходимо "прожаривать" некоторое время. Кроме того, лазер позволил "просвечивать" коридоры. Можно стрелять и как бы сканировать коридор. Если лазер натыкается на стену или монстра -- они подсвечиваются!

Все эти идеи возникли в ответ на ограничения, но оказались настолько гармонично вплетёнными в геймплей, что задним числом хочется сказать - это так и было задумано! Нет, всё шло от ситуации.

# Прямогуольники

Прямоугольники -- дефицитный ресурс Бруса! Даже чтобы нарисовать героя-космонавта ушло аж 8 прямоугольников!

Видимый размер участка карты 7x7 - 49. Это слишком много. Поэтому при отрисовке используется оптимизация. Простая, но достаточно эффективная. Она заключается в объединении однотипных блоков карты в один прямоугольник и отрисовка пола одним прямоугольником. В итоге, затраты прямоугольников на области 7x7 не превышают 25.

Лазер, полоски энергии и прогресса, объекты на карте - тоже съели драгоценный ресурс. А ведь ещё нужно было рисовать "чужих"! До последнего момента я не знал какими они будут. Я начал просто с чёрного прямоугольника и двух красных глаз. Потом понял, что два глаза - слишком жирно. И чужие стали циклопами. Добавил три ноги. Добавил анимацию. Добавил анимацию глаза - когда чужие находятся в поиске и... Они ожили! 5 прямоугольников! Минимализм как он есть. Убери хотя бы один прямоугольник или анимацию - и чужой рассыпется. Но покажи 5 прямоугольников и любой скажет что это -- космическое чудовище! Удивительный опыт. :)

# Данные

Для кодирования уровней я, как обычно, использовал текстовое представление. Выглядит это примерно так:

fg:#CEB7A6
bg:#2F435A
fill:#000000
btn:{}^
btn:zZ^
###############
#*###&   &###*#
# #####z##### #
# ##### ##### #
#}##### #####}#
# ##### ##### #
# ####   #### #
#      @      #
# ####   #### #
#%#####%#####%#
#             #
# #####Z##### #
# #*   {   *# #
# ########### #
#             #

Тут видны объекты и сам уровень. Триггеры-кнопки. А так же задание цвета блока уровней (для разнообразия игрового процесса - палитра уровней может меняться).

Уровни определены в python-преамбуле и во время компиляции транслируются в массивы 16-битных
слов. При этом, данные размещаются довольно компактно (хотя это всё ещё не компрессия!). Когда у меня закончилась память под код, памяти для данных было ещё полно. И true-grue занял её музыкой и звуками.

# Код

Отлаживать код на Brus-16 непросто. Например, вы никогда не узнаете прямо что перезаписали память. И даже нулевой адрес -- это корректный адрес. Я долгое время не мог обнаружить ошибку, которая приводила к перезаписи области прямоугольников. В начале не было возможности даже вывести какие-то отладочные данные и в качестве отладочных средств приходилось использовать while(1) или выводить прямоугольники разных цветов. Романтика! (Сейчас возможность вывести что-то в отладочный порт -- появилась.)

Объём памяти для кода стал заканчиваться до того, как игра была готова. Поэтому приходилось переписывать уже написанные участки и объединять общие вещи в функции. Иногда - менять структуры данных для того, чтобы упростить алгоритмы. Это был интересный опыт и он снова напомнил мне о "былых днях". Я уверен что и сейчас можно освободить часть памяти под код, это бесконечный процесс.

# Эффект присутствия

Когда основная часть игры была написана, я ради эксперимента сделал режим "следования". Центр системы координат переключается на героя. Я поигрался с этим режимом и он мне не понравился. true-grue предложил другую идею - а что если сделать эффект приближения? Это перекликалось с экспериментом следования камеры за игроком. Мы попробовали это сделать и... мне снова не понравилось. Что-то было в этом, какой-то эффект присутствия. Но постоянное дёрганье камеры меня раздражало. Когда игрок шёл - камера приближалась. Когда стоял - отдалялась. Это показывало возможности Брус-16, но с точки зрения игры...

Тем не менее, за несколько итераций удалось найти вариант, который понравился всем. Камера отдалялась только в моменты сканирования и смерти героя. Интересно ещё и то, что теперь игровой экран был полностью заполнен увеличенной видимой областью карты! Ограничения Брус-16 не бросались в глаза. Стало понятно, что идея отлично вписалась в игру.

Вы можете сравнить старый и новый режим просмотра карты нажав в игре c и v - одновременно.

# Герион

Игра была почти готова. Но не было заставки. И не было концовки. Нужно было придумать какой-никакой "сюжет". Названия у игры, кстати, тоже не было. Рабочие варианты какие-то были, но они явно не подходили к игре. Незамысловатый сюжет появился быстро. Место действия -- астероид (не простой!), который приближается к Земле. Название астероида я взял из списка имён из древнегреческой мифологии. В итоге получилось следующее:

> 2126 год. В Солнечной системе обнаружен межзвездный объект -- астероид Герион.
> Его траектория пересекает орбиту Земли.
> К объекту отправлен космический аппарат.

Нарисовал титульный экран. true-grue прослезился и попросил нарисовать знакомого художника. Получилось отлично. Спасибо pixelrat! https://vk.ru/artpixelrat

Сюжет дал концовку. Игра была готова.

# Звук

На момент завершения работы над игрой в Брусе-16 не было звука. Я надеюсь, что "Герион" в том числе подтолкнул true-grue к тому, что звук всё-таки появился. Понятно, что он появился бы в любом случае, но наличие демонстрации в виде озвученной игры -- подогрело интерес. Так что теперь "Герион" звучит в эмуляторе. Но аппаратной реализации звука пока нет. Надеюсь, это временно.

Я уверен, что true-grue мог бы рассказать что-то интересное про звуковую подсистему, но я практически не работал со звуком. Знаю только, что она достаточно простая, но не примитивная. Просто послушайте музыку с заставки.

Звук и музыка заняли оставшееся пространство памяти данных. Хотя память ещё есть! Можно нарисовать много новых уровней!

# Впечатления

За время работы над "Герионом" я получил массу впечатлений. Идея с Брус-16 сработала. Создались нестандартные ограничения и возможности, которые и запустили творческий процесс. Рад, что поучаствовал в этом проекте в качестве автора одной из игр. И даже завидую немного студентам, которые собираются вокруг "Бруса". Ещё много интересного можно сделать.

> P.S. В качестве "награды" два "чита":
> - c-z - пропустить уровень
> - c-x - вернуться на уровень назад

P.S. И если кто-нибудь захочет сделать пару интересных уровней -- я готов вставить их в игру. :)


gerion.png
gerion.png