Commodore 64, или как я перестал бояться и полюбил оперативную память

Не так давно я приобрел Commodore 64. Он довольно долго стоял и пылился, ждал своего часа. И вот наконец-то я подключил к нему старенький ламповый монитор и купил аудиокассеты.

Без этих кассет у меня не было другой возможности сохранить написанный код.

Commodore 64, или как я перестал бояться и полюбил оперативную память

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

Commodore 64, или как я перестал бояться и полюбил оперативную память

Сразу скажу, что эта статья — попытка разобраться в том, что же за зверь такой — Commodore 64, со своими указателями, регистрами памяти, восемью битами и двоичной системой счисления. Поэтому я могу делать ошибки, поступать нелогично и вообще писать откровенный бред. Может быть, у меня вообще ничего не получится, но статья все равно выйдет в свет, дабы я мог поучиться на своих же ошибках.

Содержание:

Чем пользуюсь

Железо. Commodore 64C. Произведен в Гонконге. К сожалению, я не смог найти в сети информацию о серийных номерах, поэтому даже не знаю год выпуска (Возможно, 84-ый).

Магнитофон. Commodore Datassette 1530. Произведен в Сингапуре. Дата выпуска — сентябрь 1988 года.

Эмулятор. Конечно же VICE. Насколько я понимаю, это самый точный эмулятор Commodore 64. В комплекте идут эмуляторы других компьютеров Commodore, включая PET, VIC-20 и С128. Имеется и отладчик, архаичный, но довольно удобный.

Commodore 64, или как я перестал бояться и полюбил оперативную память

Среда разработки. CBM prg Studio. Позволяет писать код на BASIC, ассемблере, редактировать спрайты, символы и текстовые экраны, сохранять проекты в форматах TAP (кассеты), D64 (дискеты). Можно связать IDE и VICE, что позволяет быстро проверять работоспособность программ.

Commodore 64, или как я перестал бояться и полюбил оперативную память

Basic

И вот мы включаем Commodore 64 — самый популярный в США 8-ми битный компьютер из 80-х годов. Встречает нас ламповое сине-голубое текстовое окно, которое с ходу предлагает написать что-нибудь на бейсике.

Commodore 64, или как я перестал бояться и полюбил оперативную память

38911 BASIC BYTES FREE, заманивает нас компьютер. Не будем сопротивляться. На бейсике я уже писал, например, на Apple II и советском калькуляторе Электроника МК-90. Здесь тоже все довольно стандартно. Пишем, по классике, программу, которая выводит 256 раз Hello World! на экран.

В Commodore Basic V2 стандартный для бейсика набор команд. Практически вся логика осуществляется последовательностями FOR-IF-GOTO. Приятный бонус — не обязательно перед объявлением переменных писать ключевое слово LET, что экономит место в памяти. Не обязательно разделять слова пробелами, а несколько строк можно объединить в одну через двоеточие.

Есть свои особенности и баги. Например, функция ASC, которая возвращает цифровое значение символа, не принимает пустую строку (пофиксили в следующих релизах), а функция FRE, которая показывает количество оставшихся байт в памяти бейсика, выводит отрицательные значения (приводится в порядок небольшой формулой).

Самый главный плюс, и этого мне очень не хватало на Apple II — команда IF в случае положительной проверки выполняет все действия после THEN, прописанные через двоеточие. В других версиях старого бейсика, которые я пробовал, код после двоеточия воспринимался как следующая строка, и выполнялся в любом случае.

Commodore 64, или как я перестал бояться и полюбил оперативную память

Но больше всего я удивился, когда не увидел в списке команд бейсика функций для работы с графикой. В компьютерах от Commodore такая возможность появилась только в следующей версии Basic v3.5.

Но, покопавшись, я нашел две прекрасные команды: Poke и Peek, на которые раньше не обращал внимание. И все заверте…

Немного про оперативную память

В Commodore 64, неожиданно, 64 килобайта доступной оперативной памяти, а точнее 65 535 байт или регистров. И все эти регистры объединяются в определенные блоки, которые отвечают за ту или иную работу процессора, видеочипа, звукового чипа и устройств ввода-вывода. Да, в Commodore есть очень продвинутые для своего времени видео- и звуковая карты.

Более подробно прочитать про каждый регистр памяти можно, набрав в гугле «Commodore 64 Memory Map». Вот здесь, или здесь. А если вы являетесь счастливым обладателем книги «Mapping the Commodore 64» (или скачали ее), то вас ждет увлекательное чтиво на 340 страниц.

Сразу скажу, что без книг и гайдов с Commodore 64 делать нечего. Как минимум, нужно найти книги «Commodore 64 Programmer’s Reference Guide», «Mapping the Commodore 64», «Commodore 64 User Manual», а также активно пользоваться сайтом С64-Wiki.

Вернемся к программированию. Функция Poke помещает в один байт памяти определенное значение, а функция Peek считывает значения из памяти. Каждый регистр памяти (байт) занимает 8 бит, то есть от 0 до 255 возможных значений в десятичной системе счисления. Ох уж эти системы счисления, и как сложно понять концепцию человеку, который кроме как на Python и VBA ничего особо не писал. Но, пора пришла.

Двоичная система счисления.

Вот живете вы, и всю жизнь считаете десятками. 0-1-2-3-4…-10-11-12-13…20, и так далее. А что, если бы у человечества было только две цифры: 0 и 1? Тогда числа были бы невероятно длинные, это я вам точно могу сказать.

Идете вы пиво покупать, и надо взять восемь бутылок. Вы начинаете считать: 1-2-3…8. Но в двоичном мире есть только 0 и 1, вот и приходится мучиться. 1-10-11-100-101-110-111. Посчитали семь бутылок. А восьмая? Ну, допустим она будет нулевой.

В Commodore 64 один байт (регистр) памяти равен 8 бит: «0 0 0 0 0 0 0 0». Если считать их также как пиво, то «0 0 0 0 0 0 0 0» будет равно нулю, «1 1 1 0 1 0 1 0» — 234-ем, а «1 1 1 1 1 1 1 1» — 255-ти.

Кстати, позиции битов начинаются с конца числа, и это пригодится в дальнейшем.

7 6 5 4 3 2 1 0: позиция.

0 0 0 0 0 0 0 0: бит.

Собственно, на этом вся система и строится.

Команды Poke и Peek принимают только десятичные значения. Не умеет бейсик работать с двоичными и шестнадцатеричными числами, поэтому стоит вооружиться либо навыками устного счета, либо конвертером (вот это мой вариант).

Приведу пару примеров использования Poke и Peek. Допустим, я хочу поменять цвет рамки, цвет основного экрана и цвет текста. Сделаю банальный, но эффектный стиль матрицы.

Commodore 64, или как я перестал бояться и полюбил оперативную память

Регистры 53281 и 53280 отвечают за цвет рамки и экрана. Мы говорим, что эти регистры теперь будут равны нулю, что соответствует черному цвету. Регистр 646 отвечает за цвет курсора, а значение 5 соответствует зеленому.

А теперь что-нибудь посложнее. Commodore поддерживает всего 16 цветов, поэтому вышеупомянутые регистры могут принимать только значения от 0 до 15. Значения от 16 до 255 будут просто повторять уже пройденные цвета.

Допустим, мы хотим поменять цвет курсора, но не совсем стандартным образом. Мы не просто запишем новое значение в регистр, а используем побитовое И (AND) и побитовое ИЛИ (OR).

Сейчас цвет курсора 5 (зеленый), то есть 0000 0101 в двоичной. Мы хотим получить цвет 9 (серый), то есть 0000 1001 в двоичной. Для этого нужно «выключить» бит 2 и «включить» бит 3.

Сперва применим операцию AND. Берем число 5 (0000 0101) и число 251 (1111 1011). Операция AND сравнивает два бита в каждой позиции и оставляет 1, если оба бита равны единице, и 0 во всех других случаях. На выходе получаем (0000 0001) или 1 в десятичной.

Commodore 64, или как я перестал бояться и полюбил оперативную память

Теперь надо «включить» бит 3. Берем получившееся число 1 и число 8 (0000 1000). Операция OR также сравнивает оба бита в каждой позиции и оставляет 1, если хотя бы один из бит равен единице, и 0 в других случаях. Получаем нужное нам число 9 (0000 1001).

Commodore 64, или как я перестал бояться и полюбил оперативную память

Вот как это выглядит в бейсике:

Commodore 64, или как я перестал бояться и полюбил оперативную память

Вообще, этот пример не слишком полезный, но суть, я надеюсь, передать удалось.

Ну, и раз я уже сделал отсылку на матрицу, то написал небольшую программу, которая выводит всем известные зеленые символы на черном фоне (ну, или пытается это делать).

Придумываю игру

И я решил пойти своим обычным путем: придумать основные механики для простенькой игры и учиться по ходу действия, а потом будь, что будет.

Спустя непродолжительное время (5 минут) я придумал такую банальную по современным меркам концепцию:

По лабиринту перемещается персонаж, при этом уровень находится в тумане войны. Игроку нужно найти от одного до трех ключей, чтобы открыть все запертые двери, а потом разыскать выход. Помимо игрока в лабиринте водятся монстры, которых надо убивать. Вот, собственно, и все. Звучит не очень сложно, и в современных реалиях набросать такую игру можно очень быстро. Но не на Commodore 64.

Свою будущую недонаписанную недоигру я хочу назвать Dungeon Penetration. Ну, а почему бы и нет?

Commodore 64, или как я перестал бояться и полюбил оперативную память

Распределение памяти

Как я уже сказал, регистры памяти в Commodore 64 поделены на функциональные блоки — страницы. Например, регистры с 1024 по 2047 отвечают за Screen Memory, то есть то, что мы видим непосредственно на экране. А точнее, регистры 1024 — 2023. Оставшиеся — это указатели на спрайты, коих в Commodore встроено аж 8 штук на уровне железа. Но об этом позже.

Экран компьютера в стандартном режиме составляет 40 символов в ширину, и 25 в высоту (всего 1000 символов). Если поместить, например, в регистр 1524 (1024+500) число 81, то Commodore выведет прямо посреди экрана шарик.

Ну, почти посередине. Строки съехали после появления Ready.
Ну, почти посередине. Строки съехали после появления Ready.

Но меня сейчас это не очень волнует. Более важный вопрос — сколько памяти у меня вообще есть для написания программы? И ответ не так однозначен, как может показаться на первый взгляд.

Начальный экран пишет, что свободной памяти для программы на BASIC осталось почти 39 килобайт. И действительно, регистры с 2048 по 40959 используются исключительно для хранения бейсик-программ. Для моей небольшой игры этого, наверное, хватит, но хотелось бы научиться пользоваться дополнительными источниками.

Если взглянуть на распределение оперативной памяти, то увидим следующую картину:

Commodore 64, или как я перестал бояться и полюбил оперативную память

Судя по таблице, нам доступно дополнительно как минимум 28 килобайт памяти (регистры 40960-49151 — 8Кб, 49152-53247 — 4Кб и 57344-65535 — 16Кб), но по факту доступно всего 4 килобайта. Первый диапазон — это BASIC ROM, который содержит инструкции для бейсика. Его можно переключить для свободного использования, но не в моем случае. Третий диапазон — KERNAL ROM. Он содержит все инструкции для операционной системы. Насколько я понимаю, его вообще лучше не трогать, если только не пиcать собственную операционку.

Таким образом, свободным для использования остается только второй диапазон: регистры 49152-53247. Конечно, есть отдельные свободные регистры, которые разбросаны по всей оперативке, но я не буду их пока трогать.

Вот они, слева направо: $2D02, $C000, $FFFF
Вот они, слева направо: $2D02, $C000, $FFFF

В итоге у меня остается 38Кб памяти для программы на бейсике и 4Кб памяти для других вещей. 42 килобайта, что неплохо. В Электронике МК-90 даже 15-ти не было. Но, как обычно, не все так просто.

Особенности графики

В Commodore 64 стоит продвинутый для своего времени графический чип VIC-2. Да такой продвинутый, что «наблюдает» он непосредственно за оперативной памятью.

Например, в его распоряжении находятся уже упомянутые регистры 1024-2023, которые отвечают за экран. Помимо экрана, VIC-2 также следит за спрайтами и наборами символов.

Проблема заключается в том, что видеочип умеет считывать только 16Кб памяти единовременно, то есть 1/4 от общей памяти компьютера. В эти 16 килобайт должен поместиться экран, набор символов, набор спрайтов, указатели на спрайты, их положение и прочее, и прочее.

Commodore 64, или как я перестал бояться и полюбил оперативную память

В стандартной конфигурации видеочип считывает регистры с 0 до 16383. Давайте разберемся, что входит в эти регистры. С 0 до 1023 находятся регистры, важные для операционной системы, бейсика и прочих вещей. Регистры с 1024 по 2047 — это память экрана. А дальше расположена память, выделенная для программы на BASIC.

Если в своей игре я буду использовать спрайты, то данные каждого спрайта (попиксельно) занимают 63 байта. Допустим, я сделаю для каждого из восьми спрайтов анимацию из трех кадров. Выходит полтора килобайта (63*8*3). И эти 1,5Кб должны обязательно быть в поле зрения видеочипа. В таком случае, нам придется занимать этими спрайтами место, зарезервированное для бейсик-программы, что значительно сокращает память и может попросту сломать программу.

Разработчики предусмотрели и такой вариант. Двумя несложными строчками кода можно заставить видеочип смотреть в другую область памяти. Доступные варианты: с 0 до 16383, с 16384 до 32767, с 32678 до 49151 и с 49152 до 65536. Давайте пробовать.

Commodore 64, или как я перестал бояться и полюбил оперативную память

Я пораздумал и решил переместить видеочип в последний блок. Да, теперь экран частично займет единственную свободную область памяти, но зато видеопамять вообще не будет затрагивать бейсик-программу. К тому же регистры 1024-2047, ранее отведенные для экрана, освободятся.

Но все пошло не по плану. При запуске программы я увидел какой-то набор полосок:

Commodore 64, или как я перестал бояться и полюбил оперативную память

Дело в том, что видеокарте то я сказал собирать вещички, но процессор вообще не в курсе, что экран переехал. Он продолжает упорно моргать курсором и печатать в области 1024-2023.

Пробую заново. Теперь нам надо обновить регистр 648, который отвечает за положение текстового экрана.

Commodore 64, или как я перестал бояться и полюбил оперативную память

Ииииииии… их нет! Придется лицезреть все те же грустные полоски и лезть в мануал.

Сейчас постараюсь объяснить, в чем же тут дело. В память компьютера зашиты наборы символов. Они занимают пространство в регистрах c 53248 до 57343 (так называемый Character generator ROM).

Вот они, слева направо... А блин, было уже.
Вот они, слева направо... А блин, было уже.

Если видеочип смотрит в первый или третий блоки памяти, то все нормально. Необходимые наборы символов бесшумной тенью покрывают часть памяти в нужном блоке (не оказывая на нее влияния), и видеокарта может их считать. Однако, если VIC-2 смотрит во второй или четвертый (наш) блок, то он не может найти символы из ROM, и поэтому обращается к регистру 53272, в котором говорится, на каком смещении от начала видеопамяти находятся наборы символов. А так как на нужном смещении символы вообще не определены, то чип считывает то, что находится в этих регистрах, а именно всякий мусор. Поэтому мы и видим такие полоски.

Так как регистр 53272 отвечает не только за смещение наборов символов, но и за положение памяти экрана внутри видеопамяти, то можно убить сразу двух зайцев: определить, что память экрана вообще не смещена, то есть начинается в регистре 49152, а набор символов смещен на 2048 регистров от начала.

В регистре 53272 биты 1-3 отвечают за положение символов (значение * 2048), а биты 4-7 — за положение экрана (значение * 1024). Нам нужна следующая последовательность: 0 0 0 0 0 0 1 0, или 2 в десятичной. Так и запишем.

Commodore 64, или как я перестал бояться и полюбил оперативную память

Это ничего не изменит визуально, так как еще предстоит создать новые символы, но работает вроде правильно.

Создаю набор символов

В Commodore 64 несколько графических режимов. Первый, и стандартный — это текстовый (40 на 25 символов размером 8*8 пикселей каждый). И текст в Commodore можно использовать как тайлы. Именно этим режимом я и воспользуюсь.

Текстовый режим может быть стандартным (два цвета: цвет фона и цвет символа) и многоцветным (трехцветные символы, при этом два цвета одинаковые для всех символов). Я выбрал многоцветный формат.

Тут надо учесть, что если в обычном режиме символ состоит из 64 пикселей (8*8), то в многоцветном — из 32-ух (4*8). Из-за этого пиксели выглядят широкими. Все оттого, что одна строка символа занимает 1 байт (8 бит). В одноцветном режиме каждый бит отвечает за включение/отключение пикселя. В многоцветном один пиксель занимает два бита: их комбинации меняют цвет символа.

Я посчитал, что для игры мне потребуется 24 тайла. Каждый символ занимает 8 байт, что в сумме дает всего 192 байта. Да, видеочип будет считать, что все байты после 192 тоже входят в состав набора символов, но мы их просто не будем использовать.

Приступаем к творчеству. Встроенный в CBM prg Studio редактор в этом очень хорошо помогает. Нам нужно 8 тайлов для графики, один из которых будет пустым, а также цифры от 0 до 9, буквы: H, P, E, X, Y и двоеточие. И вот что получается:

Commodore 64, или как я перестал бояться и полюбил оперативную память

IDE позволяет экспортировать числовые значения новых символов в формат бейсика. Получается следующее:

1000 DATA 0,0,0,0,0,0,0,0 1010 DATA 247,154,223,166,247,154,223,166 1020 DATA 221,155,118,101,119,230,157,89 1030 DATA 60,215,215,215,219,215,215,215 1040 DATA 60,215,215,60,48,60,48,60 1050 DATA 40,190,190,40,32,40,32,40 1060 DATA 0,20,16,16,40,60,0,0 1070 DATA 0,60,235,215,255,215,235,255 1080 DATA 12,60,60,12,12,12,12,63 1090 DATA 15,51,51,3,12,12,48,63 1100 DATA 60,3,3,60,60,3,3,60 1110 DATA 51,51,51,63,3,3,3,3 1120 DATA 63,48,48,12,3,3,3,60 1130 DATA 12,51,48,60,51,51,51,12 1140 DATA 63,3,3,3,12,12,12,12 1150 DATA 12,51,51,12,51,51,51,12 1160 DATA 12,51,51,51,15,3,51,12 1170 DATA 12,51,51,51,51,51,51,12 1180 DATA 51,51,51,63,63,51,51,51 1190 DATA 63,51,51,51,63,48,48,48 1200 DATA 51,51,51,12,12,51,51,51 1210 DATA 51,51,51,51,63,3,3,63 1220 DATA 0,12,12,0,0,12,12,0 1230 DATA 63,48,48,60,60,48,48,63

Команда DATA позволяет хранить числовые данные, которые потом нужно будет считать c помощью команды READ и поместить в нужные регистры памяти. Но сперва надо включить многоцветный режим и определить цвета символов.

Режимы переключаются в регистре 53270. Первый цвет символа определяется в регистре 53282, а второй — в регистре 53283.

Третий цвет определяется отдельно для каждого символа на экране. Эти цвета находятся в регистрах с 55296 по 56295, но в многоцветном режиме из 16 цветов можно выбрать только цвета 8-15. Пока что я это делать не буду, потому что компьютер находится в режиме ввода текста, и третий цвет символа будет зависеть от цвета курсора. Поэтому вводим временную команду POKE 646,10 для смены цвета курсора.

UPD. Оказалось, что можно не определять цвет для каждого символа, а просто поменять цвет курсора, и они автоматически будут появляться на экране в нужном виде. Так что временная команда превратилась в постоянную.

Теперь надо считать новые символы с помощью команды READ, а потом поместить эти данные начиная с регистра 51200. Помимо этого надо обнулить регистры, отвечающие за 32 символ в наборе — это символ, которым заполняются пустые области экрана.

// считываем данные символов и помещаем в нужные регистры 10 for i=0 to 191: read a: poke 51200+i,a: next i // очищаем символ № 32 20 for i=51456 to 51456+8: poke i,0: next i // цвет экрана и рамки - голубой 30 poke 53281,14: poke 53280,14 // включаем многоцветный режим 40 poke 53270,peek(53270)or16 // меняем два общих цвета символов (синий и зеленый) 50 poke 53282,6: poke 53283,5 // перемещаем область видимости видеочипа в последний блок 60 poke 56578,peek(56578) or 3:poke 56576,(peek(56576)and 252)or0 // перемещаем текстовый экран в нужную область (192*256) 70 poke 648,192 // сообщаем видеокарте, что область экрана начинается с регистра 49152 // а область набора символов - с регистра 51200 80 poke 53272,2 // меняем цвет курсора на красный 90 poke 646,10

И что-то получилось. Теперь можно печатать новыми символами (главное сопоставить их с клавишами) и даже нарисовать какой-нибудь уровень.

Commodore 64, или как я перестал бояться и полюбил оперативную память

Итак, часть работы сделана. Остались свободными следующие регистры:

  • с 1024 по 2047, или 1 Кб.
  • c 50175 по 51200, или 1 Кб.
  • с 51392 по 51456, или 64 байта (и это очень хорошо вписывается в концепцию спрайтов).
  • с 51465 по 53247, или 1,7 Кб.

В эту память (почти 4Кб) мне надо уместить карту уровня (а лучше пары уровней) и спрайты игрока и врагов.

Карта уровня

Я подумал, что надо выделить под уровни не более 1 Кб памяти (да, у меня есть почти 40 Кб бейсик-памяти, и я могу просто засунуть все в массивы, но решил пойти иным путем). Эти уровни я помещу в освободившуюся память с 1023 по 2047 регистр.

Для кодирования данных карты я опять решил обратиться к двоичной системе счисления. Каждый уровень будет размером 16*16 блоков, что в сумме дает 256, а это идеально подходит для 8-ми битной архитектуры.

Информация о каждом блоке будет храниться в одном байте памяти. Биты 0-2 будут отвечать за графику. Всего 8 возможных символов из тех, что я уже нарисовал. Бит 3 будет отвечать за коллизию (0 — сквозь блок можно пройти, 1 — нельзя). Биты 4-7 — это номер триггера. Выходит не более 15 триггеров на уровень, так как 4 бита дают 16 возможных значений, а ноль указывает на отсутствие триггера.

Воспользуемся магией Excel. Сперва накидаю уровень.

W, B — стены, D — двери, K — ключи, S — кнопка и E - выход
W, B — стены, D — двери, K — ключи, S — кнопка и E - выход

Переводим все это в двоичную систему счисления.

Ничего непонятно, но очень интересно
Ничего непонятно, но очень интересно

И теперь все это надо перевести в десятичную систему, чтобы вставить в программу.

Commodore 64, или как я перестал бояться и полюбил оперативную память

И, последний штрих на данный момент — вставить все это безобразие в DATA команды.

1990 rem level 1 data 2000 data 10,10,10,10,10,10,10,10,10,10,10,10,9,9,9,9 2010 data 27,0,0,0,0,0,0,0,0,0,0,10,0,0,0,9 2020 data 10,10,10,10,10,10,10,10,10,10,43,10,9,9,59,9 2030 data 9,0,0,0,0,0,0,0,0,0,0,0,0,9,0,9 2040 data 9,11,9,9,9,9,9,9,9,9,9,9,0,9,0,9 2050 data 9,0,0,0,0,77,9,0,0,0,0,9,0,9,0,9 2060 data 9,9,9,9,9,9,9,94,0,0,0,107,0,9,0,9 2070 data 10,0,0,0,0,0,9,0,0,0,0,9,0,9,0,9 2080 data 10,0,10,10,0,0,9,9,9,9,9,9,0,9,0,9 2090 data 10,0,0,10,0,0,9,0,0,0,0,0,0,9,0,9 2100 data 10,0,10,10,0,0,9,0,9,9,9,9,9,9,0,9 2110 data 10,0,10,0,0,0,9,123,9,0,0,0,0,0,0,9 2120 data 10,0,10,0,0,0,0,0,0,0,0,0,0,9,9,9 2130 data 10,0,10,10,10,10,10,10,10,10,10,10,10,10,140,10 2140 data 10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10 2150 data 10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10

Когда вся программа будет написана, то можно значительно сократить код. Для этого надо убрать пробелы и заполнить строки по максимуму (не более 80-ти символов).

Осталось создать триггеры. Блок с триггерами будет начинаться сразу после конца уровня. Каждый триггер будет занимать два байта. Первый байт — это указатель на блок уровня, который триггер активирует. А второй байт — это тип триггера (убрать блок, получить ключ, закончить уровень, и т. д.). Кроме того, второй байт отвечает и за то, активируется ли триггер только при наличии определенного ключа. Биты 0-2 отвечают за тип триггера, а биты 3-5 — за проверку на наличие одного из трех ключей.

Для первого уровня информация о триггерах будет выглядеть так. Например триггер (46, 1) означает, что блок под номером 46 будет убран с карты, а триггер (16, 5) означает, что 16 блок — это выход с уровня.

2160 data 16,5,42,17,46,1,85,3,65,1,107,1,183,9,222,2

Теперь надо считать данные триггеров и карты и поместить их в нужные регистры.

98 for i=1024 to 2047: poke i,0: next i // очищаем область 100 for i=0 to 271: read a: poke1024+i,a: next i // вносим данные

Я написал небольшую тестовую программу, которая считывает данные карты и выводит ее полностью на экран. Жопой чуял, что она будет работать медленно.

Но мне и не надо быстро. Как я уже сказал, уровень будет скрыт туманом войны, поэтому рисовать нужно будет только до 9 блоков за раз, а не весь уровень в 256 блоков. Можно было бы вообще отрисовать весь уровень в начале, а потом его не трогать, но это не так интересно.

Добавляю игрока. Работа со спрайтами.

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

В Commodore 64 встроено 8 спрайтов на уровне железа. Они нестандартного размера в 24*21 пикселей (но это позволяет уместить изображения для них в 63 байта). Да, спрайтами я сейчас называю не сами изображения, а лишь оболочку. Изображения можно хранить в свободной памяти, а в самих спрайтах просто менять указатели на место хранения этих изображений, например, чтобы сделать анимацию.

Спрайты можно увеличить в два раза, перемещать, включать/выключать. Можно даже считывать коллизии спрайтов с символами и другими спрайтами. По аналогии с символами, спрайты могут быть одноцветными или многоцветными (при этом пиксели также расширяются). Для многоцветных спрайтов действует тоже правило, что и для символов: один цвет уникальный, два цвета общих.

Интересный факт.

Разрешение экрана Commodore 64 составляет 320*200 пикселей, а каждая координата спрайта хранится в одном байте (от 0 до 255 возможных значений).

И если с координатой Y все понятно (200 меньше, чем 255), то работа с координатой X вызывает некоторые трудности. Поэтому, если перемещать спрайт вправо, то в определенный момент он остановится, не достигнув границы экрана (регистр X перевалит за 255), и выскочит ошибка (На ассемблере регистр обнулится и пойдёт считать заново).

Чтобы решить эту проблему, разработчики ввели еще один регистр, с помощью которого можно передвинуть спрайт по горизонтали дальше значения 255. Проблема заключается в том, что это довольно муторное занятие.

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

Но в моем случае эта информация не понадобится.

Итак, самое время нарисовать спрайты, и в этом опять поможет CBM prj Studio. Вот так выглядит экран редактора спрайтов:

Commodore 64, или как я перестал бояться и полюбил оперативную память

Спрайты игрока я решил сделать одноцветными — черными, чтобы выделялись на фоне уровня. Вторая причина — так как тайлы уровня составляют всего 8*8 пикселей, то и спрайты должны быть такого же размера. Если использовать многоцветный режим, то спрайты будут уж очень размытыми.

Враги же наоборот будут многоцветными (да, разные спрайты могут работать в разных режимах).

Спустя какое-то время у меня получились вот такие уродцы:

Commodore 64, или как я перестал бояться и полюбил оперативную память

Думаю, на первое время хватит. У игрока будет простенькая анимация в два кадра.

Знакомым образом переводим изображения в бейсик.

Commodore 64, или как я перестал бояться и полюбил оперативную память

Одна проблема — в настоящее время данные изображений занимают по 63 байта каждый. Для того, чтобы удобнее было их считывать по 64 байта, надо добавить для каждого изображения значение 0 в самый конец.

Изображения я помещу в самый конец свободной области с 51465 по 53247.

120 for i=0 to 319: read a: poke51465+i,a: next i

И вот изображения в памяти:

Commodore 64, или как я перестал бояться и полюбил оперативную память

Теперь надо поработать со спрайтами. Я решил, что один спрайт будет содержать изображения игрока (они будут меняться при ходьбе), а второй спрайт — изображения врагов, которые будут меняться при столкновениях с разными видами. При этом спрайты врагов будут увеличены в два раза.

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

Указатель на изображение первого (или нулевого) спрайта находится через 1016 регистров после начала экранной памяти. Изображения хранятся в памяти блоками по 64 байта, а первое изображение начинается с 51456-го регистра. Поэтому число в регистре указателя должно быть равно 36-ти. Оно равно количеству блоков по 64 байта от начала видеопамяти (49152+64*36=51456).

Для удобства я ввел три новые переменные. Кроме того, надо указать, что цвет нулевого спрайта — черный. Для этого нам нужен регистр 53287. И да, позиция игрока сейчас: X-24, Y-48. Именно в этих координатах начинается текстовый экран без учета границ.

//начало видеопамяти/начало отрисовки уровня/начало блока символов/начало указателей на изображения 5 ss=49152: sl=ss+41: sc=ss+2048: es=ss+1016 // координаты игрока 130 px=24: py=48 // указываем, где хранится изображение и определяем цвет спрайта (черный) 140 poke es,36: poke 53287,0

Теперь надо включить спрайт игрока. За это отвечает регистр 53269. Каждый бит регистра от 0 до 7 указывает, включен (1) или выключен (0) соответствующий биту спрайт. Нам надо включить нулевой спрайт. И конечно надо указать позицию спрайта. За координаты X и Y нулевого спрайта отвечают регистры 53248 и 53249, соответственно. Помещаем в них значения переменных координат игрока:

// включаем нулевой спрайт 150 poke 53269,1 // перемещаем спрайт в координаты игрока 160 poke 53248,px: poke 53249,py
Commodore 64, или как я перестал бояться и полюбил оперативную память

Теперь попробую сделать перемещение игрока и анимацию. Использовать буду раскладку WASD. Номер последней нажатой клавиши клавиатуры размещен в регистре 197, а анимация будет меняться путем изменения указателя на изображения спрайта (с 36 на 37 и наоборот).

Я немного переработал код, и вот что получилось:

// вводим еще одну переменную, которая указывает на первый блок изображений 5 ss=49152: sl=ss+41: sc=ss+2048: es=ss+1016: sb=36 // вводим переменную, которая указывает на номер изображения в анимации 130 px=24:py=48:ps=0 // указываем на блок начала изображения 165 poke es,sb-ps // считываем клавиши (64 - клавиша не нажата, остальное - WASD) 170 k=peek(197): if k=64 then 170 180 if k<>13 then 200 190 poke 53249, peek(53249)+8: goto 260 200 if k<>9 then 220 210 poke 53249, peek(53249)-8: goto 260 220 if k<>18 then 240 230 poke 53248, peek(53248)+8: goto 260 240 if k<>10 then 170 250 poke 53248, peek(53248)-8:goto 260 // меняем анимацию, если было перемещение 260 ps=not(ps) // возвращаемся в начало 270 goto 165
В конце можно увидеть, что будет, если выйти за пределы 255 по X

Я почистил код, убрал лишние пробелы. На данный момент программа занимает 3423 байта, так что я даже 9% доступного места не использовал. Двигаемся дальше.

Считываю данные карты и активирую триггеры

Стартовая позиция игрока будет в правом верхнем углу карты. Если высчитать точную позицию для спрайта, то получится по X — 136, по Y — 64. Надо внести значения в переменные и проверить.

Все работает
Все работает

Первым делом я переработал код. Вот так выглядят все константы:

// начало экрана, начало уровня на экране, начало памяти символов // начало указателей на спрайты, блок памяти со спрайтом игрока // начало данных уровня в памяти 5 ss=49152: sl=ss+41: sc=ss+2048: es=ss+1016: sb=36: ls=1024: rem start reg

Еще я дополнил код в части передвижения:

// указываем координаты игрока 160 poke 53248,px: poke 53249,py // Перемещаемся в подпрограмму, которая рисует 9 блоков вокруг героя 161 gosub 500 // Меняем указатель на расположение спрайта героя для анимации 165 poke es,sb-ps // Считываем клавиши // Теперь не возвращаемся в начало, а идем дальше проверять триггеры // Еще я ввел новые переменные nx и ny // для считывания следующего блока по направлению движения // без затрагивания текущей позиции 170 k=peek(197): if k=64 then 170 180 ifk<>13 then 200: rem down 190 ny=ay+1: nx=ax: goto 280 200 if k<>9 then 220: rem up 210 ny=ay-1: nx=ax: goto 280 220 if k<>18 then 240: rem right 230 nx=ax+1: ny=ay: goto 280 240 if k<>10 then 170: rem left 250 nx=ax-1: ny=ay: goto 280 // проверяем следующий блок (and 8 - проверка на коллизию) 280 b=peek(ls+ny*16+nx) and 8 // если блок "твердый", то идем проверять триггеры в строке 400 290 if b=8 then 400 // если нет, то меняем значения основных переменных и возвращаемся в начало // кстати, переменные px, py отвечают за положение персонажа в экранной памяти 300 ax=nx: ay=ny: px=24+(ax+1)*8: py=48+(ay+1)*8: ps=not(ps) 320 goto 160

В целом, основная часть работы завершена. Осталось только описать функции для отрисовки экрана и проверки триггеров.

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

// вводим два цикла 500 for i=-1 to 1: for j=-1 to 1 // проверяем первые три бита блока (тип тайла) 510 b=peek(ls+(ny+i)*16+(nx+j)) and 7 // если блок прозрачный, то идем к следующему блоку 515 if b=0 then 540 // рассчитываем номер регистра в экранной памяти, куда надо поместить блок 520 p=sl+(ny+i)*40+(nx+j) // если в этом месте нет блока, то рисуем его 530 if p<>32 then poke p,b 540 next j: next i // возвращаемся из подпрограммы 550 return

И последний штрих — проверка триггеров. Я сделал проверку только на первый тип триггера (убрать блок с карты) и пятый (завершить игру). Чтобы активировать триггер надо просто врезаться в него с любой стороны.

// считываем номер, указанный в 4 последних битах блока 400 b=(peek((ls+ny*16+nx)) and 240)/16 // если триггер отсутствует, то возвращаемся в цикл перемещения игрока 410 if b=0 then 170 // считываем значение из области памяти нужного триггера 420 p=ls+255+(b*2)-1: pp=peek(p): tr=peek(p+1) and 7 // считываем регистр в экранной памяти, в которой находится блок // на который воздействует триггер // длинный код, потому что в C64 нет операции деления с остатком 425 pz=sl+(40*(int(pp/16))+(pp-(int(pp/16)*16))) // если тип триггера - 1, то удаляем его из памяти уровня и с экранной памяти 430 if tr=1 then poke ls+peek(p),0: poke pz,32 // если тип триггера 5 - то завершаем игру 435 if tr=5 then 600 // возвращаемся в цикл перемещения 440 goto 170

Покажу на примере кнопки и двери, как это работает.

That's All Folks? Ассемблер

Пока что на этом все. Этой статьей я своих целей добился. Я понял, как работает память в Commodore 64 и как ей манипулировать. А также разобрался в различных графических режимах и спрайтах.

В процессе освоения всего этого я решил попробовать себя в ассемблере. Я написал небольшую программу на бейсике и на ассемблере, которая просто заполняет экран символами.

Вот, что получилось (видео):

Результат, как говорится, на лицо. Мало того, что программа на ассемблере меньше весит, так и исполняется в сотни раз быстрее. Поэтому я немного разочаровался в бейсике и последние абзацы этой статьи уже дописывал из под палки. Вместо этого я предпочел погружаться в изучение опкодов и паттернов программирования на ассемблере.

Так что теперь меня ждет прямая и косвенная адресация, регистры, флаги, шестнадцатеричные числа, самомодифицирующийся код и прочие прелести. А бейсик подождет.

Возможно, когда-нибудь я и про это напишу статью, но не сейчас.

P. S. Я веду Телеграм-канал со странным названием. Там я пишу о своих потугах сделать игру (с дискретным управлением), создать что-то адекватное в Blender, разбираюсь в Commodore 64 и просто делюсь мыслями об играх, коде и пиве. Если вам будет не сложно подписаться, то милости прошу=)

А что до игры?

Ну, я сделал больше половины. Большую часть времени заняло изучение регистров памяти. В игру осталось только добавить врагов и прописать другие типы триггеров и проверки на наличие ключей. Это делается не очень сложно.

Вот полное прохождение в ускоренном режиме:

Немного про память. Сама программа на бейсике (с учетом всех сокращений кода и комментариев) заняла 4121 байт. Уровень в памяти занял 271 байт. Набор символов — 192 байта. Спрайты - 320 байт. Итого, почти 4,8Кб памяти. Not great, not terrible.

Прикладываю весь код (без пробелов и с комментариями). До скорых встреч!

5ss=49152:sl=ss+41:sc=ss+2048:es=ss+1016:sb=36:ls=1024: rem start reg 10fori=0to191:reada:pokesc+i,a:nexti: rem load chars 20fori=sc+256tosc+264:pokei,0:nexti: rem clear 32 char 30poke 53281,14:poke53280,13:rem screen color 40poke 53270,peek(53270)or16:rem char multicolor 50poke 53282,6:poke 53283,5:rem char colors 60poke 56578,peek(56578)or3:poke56576,(peek(56576)and 252)or0:rem move vic 70poke 648,192:rem move screen 80poke 53272,2:rem set screen/charset 90poke 646,10:rem cursor color 95print chr$(147):rem clear screen 98fori=lsto2047:pokei,0:nexti:rem clear former screen 99rem load first level 100fori=0to271:reada:pokels+i,a:nexti 110rem load sprites/player vars 120fori=0to319:reada:poke51465+i,a:nexti 130ax=13:ay=1:nx=13:ny=1:px=24+(ax+1)*8:py=48+(ay+1)*8:ps=0:rem x,y, anim state 140poke53287,0:rem pl color 150poke53269,1:rem enable pl 160poke53248,px:poke53249,py:rem coords 161gosub500:rem draw 9 blocks 162rem animation and movement 165pokees,sb-ps:rem player sprite pointer 170k=peek(197):ifk=64then170:rem keys 180ifk<>13then200:rem down 190ny=ay+1:nx=ax:goto280 200ifk<>9then220: rem up 210ny=ay-1:nx=ax:goto280 220ifk<>18then240: rem right 230nx=ax+1:ny=ay:goto280 240ifk<>10then170: rem left 250nx=ax-1:ny=ay:goto280 280b=peek(ls+ny*16+nx)and8 290ifb=8then400: rem if block is solid goto trigger block 300ax=nx:ay=ny:px=24+(ax+1)*8:py=48+(ay+1)*8:ps=not(ps):rem change position 320goto160 390rem use block 400b=(peek((ls+ny*16+nx))and240)/16 410ifb=0then170 420p=ls+255+(b*2)-1:pp=peek(p):tr=peek(p+1)and7 425pz=sl+(40*(int(pp/16))+(pp-(int(pp/16)*16))) 430iftr=1thenpokels+peek(p),0:pokepz,32: rem remove block 435iftr=5then600: rem end game 440goto170 500fori=-1to1:forj=-1to1:rem drawing cycle 510b=peek(ls+(ny+i)*16+(nx+j))and7 515ifb=0then540 520p=sl+(ny+i)*40+(nx+j) 530ifp<>32thenpokep,b 540nextj:nexti 550return 600 poke56576,(peek(56576)and 252)or3:poke53272,20: rem end game 610 poke53270,peek(53270)and239 620 poke 648,4 630 poke53269,0 640 print chr$(147) 650 poke 646,0 660 print "end of game" 670 goto 660 990rem char data 1000data0,0,0,0,0,0,0,0,247,154,223,166,247,154,223,166,221,155,118,101,119,230 1010data157,89,60,215,215,215,219,215,215,215,60,215,215,60,48,60,48,60,40,190 1020data190,40,32,40,32,40,0,20,16,16,40,60,0,0,0,60,235,215,255,215,235,255,12 1030data60,60,12,12,12,12,63,15,51,51,3,12,12,48,63,60,3,3,60,60,3,3,60,51,51,51 1040data63,3,3,3,3,63,48,48,12,3,3,3,60,12,51,48,60,51,51,51,12,63,3,3,3,12,12 1050data12,12,12,51,51,12,51,51,51,12,12,51,51,51,15,3,51,12,12,51,51,51,51,51 1060data51,12,51,51,51,63,63,51,51,51,63,51,51,51,63,48,48,48,51,51,51,12,12,51 1070data51,51,51,51,51,51,63,3,3,63,0,12,12,0,0,12,12,0,63,48,48,60,60,48,48,63 1990 rem level 1 data 2000data10,10,10,10,10,10,10,10,10,10,10,10,9,9,9,9,27,0,0,0,0,0,0,0,0,0,0,10,0 2010data0,0,9,10,10,10,10,10,10,10,10,10,10,43,10,9,9,59,9,9,0,0,0,0,0,0,0,0,0,0 2020data0,0,9,0,9,9,11,9,9,9,9,9,9,9,9,9,9,0,9,0,9,9,0,0,0,0,77,9,0,0,0,0,9,0,9 2030data0,9,9,9,9,9,9,9,9,94,0,0,0,107,0,9,0,9,10,0,0,0,0,0,9,0,0,0,0,9,0,9,0,9 2040data10,0,10,10,0,0,9,9,9,9,9,9,0,9,0,9,10,0,0,10,0,0,9,0,0,0,0,0,0,9,0,9 2050data10,0,10,10,0,0,9,0,9,9,9,9,9,9,0,9,10,0,10,0,0,0,9,123,9,0,0,0,0,0,0,9 2120data10,0,10,0,0,0,0,0,0,0,0,0,0,9,9,9,10,0,10,10,10,10,10,10,10,10,10,10,10 2130data10,140,10,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,10,10,10,10,10,10,10,10,10 2140data10,10,10,10,10,10,10 2150data16,5,42,17,46,1,85,3,65,1,107,1,183,9,222,2 3000 rem player sprites 3010data120,0,0,117,0,0,125,0,0,57,0,0,127,0,0,125,0,0,56,0,0,40,0,0,0,0,0,0,0,0 3020data0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 3240data0,0,0,120,0,0,117,0,0,125,0,0,57,0,0,127,0,0,125,0,0,40,0,0,0,0,0,0 3250data0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 3460rem enemy sprites 3470data4,20,16,5,125,80,31,235,244,127,105,253,127,150,253,127,150,253,127,170 3480data253,95,235,245,21,255,84,30,190,180,30,126,116,31,255,244,23,255,212 3490data31,255,244,23,255,212,31,235,244,7,170,208,1,190,64,0,125,0,0,20,0 3500data0,0,0,0 3700data4,0,16,4,65,16,20,85,20,20,105,20,101,235,89,102,105,153,101,105,89,101 3710data190,105,105,170,105,105,170,105,105,170,105,105,170,105,105,170,105,105 3720data170,105,105,105,105,105,150,105,29,170,116,6,170,144,1,150,64,0,65,0 3730data0,65,0,0 3930data0,0,0,4,65,16,4,65,16,5,85,80,71,255,209,69,85,81,90,170,165,90,170,165 3940data106,235,169,106,105,169,90,170,165,106,170,169,90,90,165,106,166,169,106 3950data169,169,26,170,164,5,85,80,4,16,64,5,20,80,0,0,0,0,0,0,0
3232
11 комментариев

Комментарий недоступен

2

Круто! а где купил, если не секрет?

1

На Авито
Очень повезло с продавцом - им оказался дедушка, который еще в 80-х привез его из Европы и практически не пользовался. Поэтому компьютер в был идеальном состоянии.
Одна проблема возникла. Когда пытались спать нужный видеокабель и подключить к монитору, то убили начинку, поэтому пришлось покупать донора и менять внутренности. Внутренности оказались более свежей ревизии, так что разницы я не заметил. Обидно конечно вышло.
Кабель в итоге спаяли и все подключили, но осадочек остался)

1

А где-то есть не двоичная система счисления?)

1

Да везде, конечно)
Я к тому, что раньше мне не приходилось с этим сталкиваться настолько тесно, потому что самоучка + не писал ни на чем сложнее питона/гдскрипта/vba. А коммодор меня окунул во все это по полной)