Dungeon Crawler на Commodore 64. Часть вторая

Посленовогодний апдейт новостей о разработке игры на Commodore 64 на ассемблере.

Dungeon Crawler на Commodore 64. Часть вторая

А добавил я вот что:

HUD

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

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

Dungeon Crawler на Commodore 64. Часть вторая

Сразу скажу, что художник из меня не очень. Так как действие игры происходит в российских реалиях, сперва я не придумал ничего лучше, чем нарисовать HUD в виде знаменитого забора. Но насколько забавно это выглядело в редакторе, насколько несуразно выглядит в самой игре:

Dungeon Crawler на Commodore 64. Часть вторая

Поэтому я нарисовал кое-то попроще:

Dungeon Crawler на Commodore 64. Часть вторая

Карта и перемещение игрока

Карта представляет собой банальный двухмерный массив байт размером 24*24 (576 байт). Один байт (8 бит) кодируется следующим образом:

  • Биты 0-3 – номер текстуры от 0 до 15.
  • Бит 4 – наличие спрайта на блоке.
  • Бит 5 – наличие триггера на блоке.
  • Бит 6 – необходимость отрисовки блока. Можно использовать, например, для невидимых стен.
  • Бит 7 – наличие коллизии. Можно использовать, например, для секретов (неосязаемые стены).
game_map map_row_0 byte $C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1,$C1 map_row_1 byte $C1,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$C1 map_row_2 byte $C1,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$C1 map_row_3 etc...

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

В будущем уровни я планирую рисовать в замечательной программе Tiled. Она сохраняет все данные в JSON или XML, и это дело можно очень удобно сконвертировать в формат ассемблера.

Dungeon Crawler на Commodore 64. Часть вторая

Триггеры

Все триггеры хранятся в памяти в следующем виде:

  • Флаги. Всего два флага: single use и avaliable. Первый переключает второй в ноль. Второй говорит, что триггер можно использовать.
  • Позиция на карте по X.
  • Позиция на карте по Y.
  • Маска ключей, которые нужны для активации триггера (сравнивается с ключами, которые есть у игрока).
  • Адрес функции, которая активирует триггер.
  • 4 аргумента триггера.
trigger_flags byte %00000011 byte %00000001 trigger_x_pos byte 23 byte 20 trigger_y_pos byte 21 byte 19 trigger_function_lb byte <output_text_trigger_activate byte <door_trigger_activate trigger_function_hb byte >output_text_trigger_activate byte >door_trigger_activate trigger_keys byte %00000001 byte %00000000 trigger_arg_1 byte <map_text_1 byte 20 trigger_arg_2 byte >map_text_1 byte 18 trigger_arg_3 byte 140 byte 20 trigger_arg_4 byte 0 byte 20

Как это работает.

Когда игрок нажимает F (да, у нас тут вполне современное управление), проверяется, есть ли вообще на блоке перед игроком триггер. Если есть, то сравниваются координаты блока с координатами триггера. Потом проверяется наличие у игрока нужных ключей.

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

@check_trigger_type lda trigger_function_lb,x sta @activation_label+1 lda trigger_function_hb,x sta @activation_label+2 @activation_label jsr $0000 ;сюда кладем адрес функции

Пока что готовы такие триггеры:

  • Дверь. Позволяет телепортировать игрока за дверь, а потом вернуть обратно. Этот же триггер можно использовать для разделения зон. Например, сделать телепорт, который сперва посылает в одну зону, а потом в другую.
  • Телепорт. Просто телепортирует игрока на конкретный блок карты.
  • Убрать блок. Убирает коллизию и отрисовку блока.
  • Убрать спрайт. Убирает коллизию блока и сам спрайт. Может использоваться, если игрок, например подобрал патроны или ключ.
  • Вывод текста. Выводит текст на экран (140 символов). Об этом позже.
  • Смена текстуры. Просто меняется номер текстуры на блоке.

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

Здесь, например, срабатывают два триггера - текстура кнопки меняется, а соседний блок убирается:

Спрайты

Dungeon Crawler на Commodore 64. Часть вторая

Ох и намучился я с ними. Спрайты хранятся в памяти почти также, как и триггеры:

  • Позиция на карте по X.
  • Позиция на карте по Y.
  • Адрес изображения спрайта в памяти.
  • Цвета (всего 6 штук).
sprites_x byte 20 sprites_y byte 19 sprites_addr_lo byte <sprite_computer sprites_addr_hi byte >sprite_computer sprites_1_color_1 byte $06 sprites_2_color_1 byte $06 sprites_3_color_1 byte $08 sprites_4_color_1 byte $08 sprites_multi_1 byte $0c sprites_multi_2 byte $09

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

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

Один игровой спрайт состоит из 4 аппаратных спрайтов. Каждый аппаратный спрайт может содержать один уникальный цвет и два общих для всех спрайтов. Из-за этого и можно указать 6 цветов: два цвета одинаковых для всех спрайтов, и 4 цвета уникальных для каждого из 4 спрайтов (возможно потом придется обойтись меньшим количеством цветов, если я соберусь использовать оставшиеся 4 аппаратных спрайта).

Спрайты в отладочной программе C64Debugger
Спрайты в отладочной программе C64Debugger

Так почему же я страдал?

Видеочип в Commodore 64 может обращаться всего к 16 Кб оперативной памяти (общей с процессором). И все изображения спрайтов обязательно должны лежать в пределах этих 16 килобайт. Но из-за того, что почти вся память в зоне, которую я использую, занята экранной памятью, кодом игры и таблицами, то для спрайтов места почти не остается. А самая большая проблема, что в том месте, куда спрайты можно положить (на картинке - первая пустая область), зеркалится Character ROM (генератор символов для текстового режима). Процессор воспринимает эту область, как RAM, а вот видеочип уже видит ее как Character ROM. Поэтому в эту область графику класть нельзя.

Можно увидеть, что памяти для игры у меня еще достаточно
Можно увидеть, что памяти для игры у меня еще достаточно

Решил я эту проблему тем, что положил данные спрайтов вообще в другое место в памяти, а в области, на которую смотрит видеочип, выделил 4 блока по 64 байта. В эти блоки изображения банально копируются при необходимости. Думаю, для более динамичной игры такой подход бы не подошел, но в моем случае подгрузка вообще не заметна.

Для каждого объекта одного спрайта можно выбрать любые цвета из палитры

Текстовый режим

В Commodore 64 есть замечательная штука, которая называется Растровые прерывания. Видеочип отрисовывает экран построчно. В PAL версии компьютера (который у меня), видеочип проходит 312 линий для полной отрисовки экрана (это непосредственно экран, рамка и так называемый VBlank - время, которое тратится на возврат луча в начальную позицию).

Dungeon Crawler на Commodore 64. Часть вторая

Видеочип отрисовывает одну линию экрана за 63 такта процессора (в хорошем случае). Но так как процессор и видеочип разделяют одну шину данных, то существуют так называемые badlines, когда видеочип полностью блокирует процессор на 40 тактов. В таком случае за отрисовку одной линии процессор отрабатывает всего 23 такта.

Это я все к чему? На каждой линии отрисовки можно довольно просто вызвать растровое прерывание. В этом случае видеочип говорит процессору: "Забей на все, что ты сейчас делаешь и иди делай то, что я тебе скажу в другом месте, а потом возвращайся обратно".

Почти таким же образом, например, работают прерывания для считывания клавиатуры
Почти таким же образом, например, работают прерывания для считывания клавиатуры

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

Dungeon Crawler на Commodore 64. Часть вторая

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

Но без проблем не обошлось. Тот артефакт, про который я рассказывал в начале статьи, возникал из-за того, что процессор переключал режим еще до того, как видеочип отрисовал всю линию (вся операция занимала меньше 63 тактов). Решилось это банальным добавлением нескольких команд NOP (No OPeration) в прерывание.

irq_set_text_screen nop nop nop nop ; меняем режим lda $d011 and #%11011111 sta $d011 lda $d016 and #%11101111 sta $d016 lda $d018 and #%11110111 sta $d018 ; выставляем указатели на следующее прерывание на 255 линии lda #255 sta $d012 lda #<irq_set_hires_screen sta $0314 lda #>irq_set_hires_screen sta $0315 asl $d019 ; выходим из прерывания jmp $ea31

Но и сейчас проблема до конца не решена. Если посмотреть на изображение, видно, что криво отображается последняя линия графики над текстом. Что с этим делать, я пока не понял.

Dungeon Crawler на Commodore 64. Часть вторая

Подгрузка данных с дискеты

Поскольку по моим подсчетам один уровень, текстуры для него, триггеры, спрайты и т.д. на данный момент занимают примерно 2 388 байт (и это я еще не посчитал графику для спрайтов, которая будет тоже занимать немало места), памяти для нескольких уровней не хватит ни при каком желании.

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

Сама загрузка данных из файлов с дискеты происходит нетривиально. Для этого надо пользоваться командами из KERNAL (местный BIOS).

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

Сюда же надо передать параметр secondary address. Если 0, то данные будут загружаться в то место памяти, которое надо передать позднее в функцию LOAD. Если 1, то данные будут загружаться в место, указанное в заголовке файла.

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

И наконец функцией LOAD загружаем данные из файла.

; ---------- LOAD ROUTINE --------- load_data ; передаем логический номер файла и устройства lda #1 ldx #8 ldy #1 jsr SETLFS ; передаем ссылку на адрес, где лежит имя файла и длину имени lda #2 lo_file_name_label ldx #$00 hi_file_name_label ldy #$00 jsr SETNAM ; загружаем данные из файла в память по адресу, который указан в файле lda #0 jsr LOAD rts

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

На этом пока все. А если было интересно, подписывайтесь на Telegram канал, где я рассказываю о ходе разработки этой игры. Обновления выходят почти каждый день=)

2424
3 комментария

это очень круто! жаль я так не смогу, слишком безвозвратно обновился

1

Почему безвозвратно ?