Советский геймдев для друзей партии
Не так давно в мое распоряжение попал микрокомпьютер Электроника МК-90 1990 года выпуска. Я давно мечтал об этой машинке, но отталкивала средняя цена на Авито в 50 тыс. рублей.
Случилось чудо, и полностью рабочий калькулятор достался мне в подарок абсолютно бесплатно. К слову, МК-90 и при Советском союзе стоил недешево. Мой экземпляр по состоянию на дату выпуска (июнь 1990 года) можно было купить за баснословные 1 500 рублей (6 среднемесячных зарплат), а ранние модели стоили от 3 до 3.5 тысяч. За такие деньги можно было половину машины купить.
Неудивительно, что популярностью этот микрокалькулятор не пользовался, уступая место более дешевому МК-85 и еще более дешевому МК-61 (про него я кстати писал в предыдущей статье).
Сперва я назвал МК-90 микрокомпьютером, потом микрокалькулятором, и это неудивительно, ведь и у разработчиков из Минска были проблемы с его позиционированием. На лицевой стороне МК-90 написано «Микрокомпьютер», при этом на задней панели, да и в инструкции он значится как «Микрокалькулятор». Сейчас МК-90 скорее назвали бы планшетом или КПК.
Хвалим и ругаем
МК-90 превосходил по своим возможностям многие другие модели портативных электронных устройств. У меня сложилось впечатление, что разработчикам дали карт-бланш, и поэтому они сделали настолько дорогое, мощное, но не очень полезное для общественности устройство. Кажется, что доработать МК-90 напильником, пофиксить проблемы (коих при всех достоинствах хватало), и вышел бы флагман советской электронной промышленности, которым и на мировой арене не стыдно похвастаться. Но СССР закончился, а вместе с ним и мечты о светлом электронном будущем.
Чем же выделялся МК-90 на фоне других советских калькуляторов?
Во-первых, размерами. Сравнение с МК-61:
Во-вторых, в него встроен интерпретатор BASIC (в моем экземпляре BASIC 1.0, но в дальнейшем вышла версия 2.0). Да, и в МК-85 есть BASIC, но согласитесь, программировать на экране размером 120*64 пикселя гораздо удобнее, чем на однострочном, 12-ти символьном. И это третий плюс. МК-90 поддерживает и графический режим, который позволяет попиксельно выводить двухцветные спрайты, рисовать геометрические фигуры и даже графики.
В-четвертых, в комплекте шли два модуля оперативной памяти (МПО), которые позволяют сохранять записанные программы. Да, память энергозависимая, и батарейки в МПО нужно менять как минимум раз в полтора года. Да, при замене батареек память обнуляется. Но, согласитесь, это довольное удобное решение.
Но перейдем к недостаткам, первым из которых станет клавиатура. Мало того, что раскладка фонетическая и не соответствует общепринятой qwerty, так еще и клавиши нажимаются через раз (возможно, это от старости). Неудобно расположены стрелки и некоторые важные символы. Например, бесполезная точка с запятой выводится одной клавишей, а часто используемые знаки равно, сложения и умножения — двумя.
Еще я не очень понимаю, зачем в МК-90 понадобилась русская раскладка. Конечно, калькулятор советский, и русский язык должен быть, но он никак не используется при работе. Более того, использование русской раскладки может привести к ошибкам в работе программ.
Однако, это незначительные минусы, и при должной усидчивости к ним быстро привыкаешь.
Пишем игру
В дальнейшем я буду пользоваться точным эмулятором МК-90, который написал польский товарищ Piotr Piatek. На его сайте было много полезной информации, которая пригодилась мне для написания этой статьи. К сожалению, Piotr удалил все упоминания о советских калькуляторах в середине 2022 года (какое досадное совпадение). Хорошо, что интернет помнит все. Я скачал образы обоих версий BASICа: 1.0 и 2.0. Для чего мне это понадобилось, объясню ниже.
При включении эмулятора на экране отобразится название и три графы: бейсик, СМП0 и СМП1. При этом, калькулятор называется «Электроника — ПК100», и это рабочее название, что подтверждает проблемы с позиционированием. Во второй версии прошивки это поправили.
СМП0 и СМП1 отвечают за запуск программ с модулей памяти. При этом программы, написанные на BASIC, с помощью этих граф запустить нельзя. Только написанные на ассемблере.
Заходим в режим программирования BASIC и видим командную строку.
Я не буду углубляться в тонкости программирования, тем более, что в интернете можно найти подробную инструкцию, пусть и не всегда понятно написанную. Поэтому сразу перейду к разработке игры.
Пользователю доступны 11 824 байта памяти первой версии и 12 248 байт во второй. При этом память занимают не только данные, но и сам текст кода. Поэтому придется экономить не только на переменных, но и на количестве символов в коде, например обходиться без пробелов (а BASIC это позволяет) и по максимуму использовать каждую строку.
Сперва я хотел сделать игру с открытым, случайно-генерируемым миром, размером 64*64 тайла. Когда я понял, что МК-90 не позволяет создавать массивы таких размеров, а производительность просто ужасная, то снизил количество объектов на карте до 20-ти. Но и это не помогло. Карта генерировалась не меньше трех минут, а рендеринг одного кадра занимал от минуты и дольше. Для разработки и отладки мне пришлось даже ускорить работу эмулятора в 5-10 раз. От этой идеи я решил отказаться.
Поэтому я решил пойти другим путем, и создать мир размером в один экран калькулятора, где каждый тайл занимает 8*8 пикселей. То есть общее количество тайлов на карте (15*8) = 120 штук. Кажется, что 120 объектов больше, чем 20, но на самом деле рендеринг всех тайлов занимает от силы секунду.
МК-90 позволяет рисовать графику: точки, отрезки, линии, круги и прямоугольники, но с их помощью графику сделать сложно, да и память жалко. Хорошо, что помимо графических примитивов, МК-90 умеет рисовать спрайты. Для этого используется функция DRAW M, которая принимает как аргумент последовательность байт.
Вот эти 16 строк рисуют игровой мир и персонажа (он представляет собой простой черный квадрат с усеченными углами) .
Допустим, у нас есть спрайт персонажа. Мысленно делим его по вертикали пополам и на 8 блоков по горизонтали.
Черный пиксель равен единице, прозрачный — нулю. Создаем битовую маску:
И переводим эти биты в шестнадцатеричную систему счисления.
Вот эту последовательность байт (181C183C5A18282C) и принимает функция DRAW M.
Эта функция и стала причиной, из-за которой я перешел с аутентичного моему устройству BASIC 1.0 на BASIC 2.0. Оказалось, что в первой версии функция DRAW M почему-то пожирает и так ограниченную память. Во второй версии такого нет, но есть свои приколы, о которых я расскажу позже.
А сперва мне нужно было разобраться, как перенести в память калькулятора последовательность из 1 920 байт (15 столбцов по 64 строки пикселей, каждая из которых представляет собой два символа), чтобы заполнить весь экран нужным мне изображением.
Для начала это изображение нужно было нарисовать, и опять в этом мне помог старый добрый Aseprite. Получился такой вот открытый мини-мир:
Стартовая локация — у домика в правом верхнем углу. Передвигаться можно только по штрихованным дорогам и пустому ущелью посреди карты. В замке сидит финальный босс с повышенными характеристиками, и для его победы нужно посетить 4-е доступные пещеры, чтобы поднять уровень.
Сразу скажу, что часть со сражениями (а предполагается система уровней для игрока и монстров, отдельный экран для баталий и возможность лечиться в процессе) еще не готова. В этой статье я опишу только рендеринг и перемещение по игровому миру.
Сразу возникла идея создать битовую маску на основе получившегося изображения, а потом написать программу, которая преобразит ее в нужную мне последовательность байт. Для первой цели я использовал графический редактор GIMP. Он позволяет сохранить индексированные изображения в формате заголовочного файла для языков C/C++. Вот, что вышло (я не буду приводить весь текст):
Дело осталось за малым: привести все в удобоваримый вид и написать на Python программу, которая преобразует биты в последовательность байт. То, что получилось, было представлено чуть выше.
Осталось решить вопрос, как перенести все это в память МК-90. Сперва казалось, что это не так сложно. В папке эмулятора лежат два файла: smp0.bin и smp1.bin, которые отвечают за нулевой и первый модули памяти, соответственно. Первая версия BASIC сохраняет код на модуль в чистом текстовом виде, а вот вторая версия все ключевые слова кодирует определенными байтами (хорошо, что не весь текст). Например код FOR I = 0 to 10 будет выглядеть так:
Вторая проблема заключалась в том, что текстовый редактор кодирует переход на следующую строку как 0x0D, а калькулятор принимает исключительно 0x0A, иначе возникают ошибки. Пришлось лезть в HEX-редактор и менять эти байты вручную. Тут же выяснилось, что программа (а точнее конкретный файл с программой) хранится на модуле памяти блоками по 512 байт, и если вручную не отредактировать количество таких блоков, то калькулятор выдаст очередную ошибку. Зато я разобрался, как модуль памяти хранит наименования файлов и прочие их параметры.
Итак, мир игры готов:
Теперь нужно завести переменные для определения позиции игрока, временные переменные для проверки свободных зон для движения:
Долго мучился над вопросом, как определять, в какие зоны персонаж может попасть, ведь создавать массив размером 8*15 элементов, а потом заполнять его нулями и единицами слишком затратно в части памяти. По моим подсчетам, такой способ занимает как минимум 1.3 Кб. Я опробовал несколько вариантов, в том числе с использованием операторов DATA и READ, созданием отдельных массивов с индексами и т.д. В одном случае обработка одного кадра занимала 15 секунд, в другом — 6 секунд. В итоге я вернулся к первоначальному варианту и пожертвовал памятью ради производительности. Обработка одного кадра теперь занимает не более секунды.
Нужно создать двухмерный массив и заполнить его нужными значениями: сперва нулями, а потом позиционно проставить единицы, которые будут определять 42 свободные зоны. Да, вышло не очень красиво, и без помощи блокнота я бы не справился, зато быстродействие оказалось на высоте.
Данные для определения свободных тайлов внесены. Пришло время перейти к считыванию клавиш и проверке возможности движения.
Клавиши считываются с помощью оператора INC, и тут же временные переменные меняются в сторону увеличения или уменьшения для последующих проверок. Если клавиши не были нажаты, то с помощью всеми ненавистного оператора GOTO программа возвращается в начало.
Строки со 150-ой по 153-ю проверяют, находится ли новое положение в пределах индексов массива. Да, пришлось использовать целых четыре строки, потому что BASIC ругался, если все проверки находились в одной.
Если все проверки пройдены, то определяем, свободен ли тайл в следующей позиции. Если нет, то возвращаемся в начало, иначе стираем персонажа в старой позиции и рисуем в следующей (DRAW M инвертирует пиксели, поэтому можно не заморачиваться с позиционной отрисовкой).
Для калькулятора 30-ти летней давности и программы на BASICе вышло довольно неплохо:
Первый этап завершен. Дальше я приступлю к описанию геймплея, а пока на этом все. Ждите продолжение.