[NES] Пишем редактор уровней для Prince of Persia. Глава пятая. Отражение

от автора

Глава первая, Глава вторая, Глава третья, Глава четвертая, Глава пятая

Disclaimer

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

В ночь с пятницы на субботу я снова открыл отладчик, RAM Filter и начал искать…

Появление на свет

Началось все с того, что мне прислали модификацию игры, сделанную в редакторе, с вопросом: Почему виснет при входе в одну из комнат?
Комната выглядела в редакторе вполне обычно. Проверка правильности работы редактора показала, что все в порядке: что попросили, то и сохранил. Я попробовал переместить объекты, которые поставил автор модификации в комнате, и обнаружил, что зависание пропало, но в комнате появился двойник, который, к тому же, еще и имел наглость атаковать:

То есть наличие объектов в комнате как-то влияет на наличие/отсутствие двойника. Со стражниками все ясно — их расстановка прописана прямо в заголовке уровня, а вот про двойника нет ни байта информации. Массивы данных для 4, 5 и 6 уровней ничем не отличались от всех остальных по своей сути. Полностью скопированный пятый уровень, скажем, в первый, не «вызывал» двойника из недр кода движка, а значит это как-то вшито в сам движок. Надо было понять, что меняется, если мы входим в комнату с двойником.

Я начал изучать пятый уровень, так как там двойник выполнял больше всего действий: если нажать на кнопку, которая открывает выход, он появлялся, выпивал драгоценное содержимое бутылки, и убегал. RAM Filter выявил аномальную активность при появлении двойника в памяти в районе адресов $0400-$0410: при входе в комнату взводился флаг в ячейке $0401, после нажатия на кнопку дополнительно взводился флаг в ячейке $0402, а затем, после того как тот убегал за пределы комнаты, ячейка $0401 обнулялась и больше не менялась. Значит, будем изучать, что тут происходит.

Наличие или отсутствие дополнительного персонажа (стражник, скелет или двойник) в комнате в NES версии вызывает замедление работы движка, и на эту особенность стоит обратить внимание.
Запускаем игру, ставим в $0401 единицу и действительно начинаем наблюдать замедление работы движка. Более того, нас снова начинает «атаковать» двойник.
Раз ячейка $0401 отвечает за наличие/отсутствие оного, то идем в отладчик, задаем точку останова:

В поле Condition задаем условие: аккумулятор при записи в ячейку не должен содержать 0. Изменение ячейки может происходить и через индексные регистры, но зачастую это производится именно через аккумулятор. Останов приводит нас сюда:

$A1D0:A9 00	LDA #$00 $A1D2:8D 01 04	STA $0401 = #$00 $A1D5:A5 51	LDA $0051 = #$17 $A1D7:8D DE 04	STA $04DE = #$17 $A1DA:AD 02 04	LDA $0402 = #$00 $A1DD:D0 4D	BNE $A22C $A1DF:A9 3D	LDA #$3D $A1E1:85 2F	STA $002F = #$2D $A1E3:A9 A2	LDA #$A2 $A1E5:85 30	STA $0030 = #$A2 $A1E7:A5 70	LDA $0070 = #$04 $A1E9:C9 05	CMP #$05 $A1EB:D0 04	BNE $A1F1 $A1ED:A5 51	LDA $0051 = #$17 $A1EF:F0 2D	BEQ $A21E $A1F1:A9 4D	LDA #$4D $A1F3:85 2F	STA $002F = #$2D $A1F5:A9 A2	LDA #$A2 $A1F7:85 30	STA $0030 = #$A2 $A1F9:AD FD 04	LDA $04FD = #$00 $A1FC:D0 0C	BNE $A20A $A1FE:A5 70	LDA $0070 = #$04 $A200:C9 03	CMP #$03 $A202:D0 06	BNE $A20A $A204:A5 51	LDA $0051 = #$17 $A206:C9 03	CMP #$03 $A208:F0 14	BEQ $A21E $A20A:A9 2D	LDA #$2D $A20C:85 2F	STA $002F = #$2D $A20E:A9 A2	LDA #$A2 $A210:85 30	STA $0030 = #$A2 $A212:A5 70	LDA $0070 = #$04 $A214:C9 04	CMP #$04 $A216:D0 14	BNE $A22C $A218:A5 51	LDA $0051 = #$17 $A21A:C9 17	CMP #$17 $A21C:D0 0E	BNE $A22C $A21E:A9 01	LDA #$01 $A220:8D 01 04	STA $0401 = #$00      ;; <<< останов $A223:8D E0 06	STA $06E0 = #$00 $A226:20 AD F2	JSR $F2AD $A229:20 85 DB	JSR $DB85 $A22C:60	RTS 

Эта простыня довольно сложна для понимания, поэтому переведу ее в псевдокод, обозвав ячейки следующим образом:
— $70 — LEVEL;
— $51 — ROOM;
— $401 — MIRROR_FLAG.
С остальными позже разберемся:

char sub_A1D0() { 	MIRROR_FLAG = 0; 	$04DE = ROOM; 	if ( $0402 ) goto label_A22C; 	$2F = #3D; $30 = #A2;  	if ( LEVEL != #05 ) goto label_A1F1; 	if ( !ROOM ) goto label_A21E;  label_A1F1: 	$2F = #4D; $30 = #A2;  	if ( $04FD ) goto label_A20A; 	if ( LEVEL != #03 ) goto label_A20A; 	if ( ROOM == #03 ) goto label_A21E;  label_A20A: 	$2F = #2D; $30 = #A2; 	 	if ( LEVEL != #04 ) goto label_A22C; 	if ( ROOM != #17 ) goto label_A22C;  label_A21E: 	MIRROR_FLAG = $06E0 = #01; 	sub_F2AD(); 	sub_DB85();  label_A22C: 	return; } 

Теперь это проще изучить. Как видим, тут перебираются аккурат те уровни и комнаты в них, где появляется двойник, устанавливается флаг его наличия и вызываются еще две процедуры. Приводить я их не буду, лишь опишу то, что они делают.
— $F2AD — выполняет поиск удвоенного маркера #FF, начиная с адреса $060E, затем возвращает длину последовательности байт между $060E и маркером #FFFF (не включая последний) в регистре Y;
— $DB85 — выполняет сдвиг последовательности байт, начиная с найденного предыдущей процедурой маркера #FFFF, вперед, на длину последовательности, адрес которой хранится в ячейках $2F:$30, и которая также заканчивается маркером #FFFF.
В ячейки $2F и $30, как мы видим, вносятся жестко заданные адреса где-то в ROM: $A22D, $A23D, $A24D.

Простого внесения единицы в ячейку $0401 недостаточно, надо еще сделать некие магические действия.

Двое из ларца, одинаковых с лица

Так как двойник появляется только тогда, когда взводится флаг в ячейке $0402, то поищем сперва, где производится запись в нее, а затем, где читается значение из нее. Взводится флаг, как и следовало ожидать, тогда, когда мы нажимаем на кнопку, открывающую выход (там ничего интересного). А вот чтение из ячейки производится тут:

$A319:A9 89	LDA #$89 $A31B:85 72	STA $0072 = #$89 $A31D:A9 A3	LDA #$A3 $A31F:85 73	STA $0073 = #$A3 $A321:AD 02 04	LDA $0402 = #$01        ;; << останов $A324:F0 53	BEQ $A379 $A326:AD 01 04	LDA $0401 = #$01 $A329:F0 4E	BEQ $A379 $A32B:EE 04 04	INC $0404 = #$00 $A32E:AC 03 04	LDY $0403 = #$00 $A331:B1 72	LDA ($72),Y @ $A389 = #$15 $A333:C9 FF	CMP #$FF $A335:F0 43     BEQ $A37A $A337:CD 04 04  CMP $0404 = #$01 $A33A:D0 0B     BNE $A347 $A33C:A9 00     LDA #$00 $A33E:8D 04 04  STA $0404 = #$01 $A341:EE 03 04  INC $0403 = #$00 $A344:EE 03 04  INC $0403 = #$00 $A347:C8        INY $A348:B1 72     LDA ($72),Y @ $A389 = #$15 ;; ... 

Примечательная процедура, не правда ли? Суть ее действий, если изучить ее целиком, очень напоминает процедуру, которая играет за нас в demo play. В ячейках $72:$73 у нас адрес $A389:
15 01 06 0D 02 02 03 32 0C 4E 02 05 17 01 FF
Тот же маркер #FF, те же структуры, состоящие из двух байт, где первый из них — время, а второй… нет, второй — уже не имитация геймпада, а что-то иное. Когда отсчитываемое в ячейке $0404 значение сравнивается с первым байтом структуры, счетчик в ячейке $0403 увеличивается на 2 и процесс повторяется. Если посмотреть, что происходит во время этого процесса со вторым байтом, то мы придем к некоему массиву указателей:
0x15602: 00 00 AE 96 DB 96 64 97 9D 97 ...
Индексом в этом массиве как раз будет служить наш второй байт. Каждый указатель в этом массиве указывает на структуру, с которой движок работает довольно хитро. Если в этой структуре попадается значение, которое больше некоторого числа, то оно декодируется в индекс, который используется в массиве указателей на определенные процедуры в коде. Если же число меньше некоторого числа, то оно используется как аргумент для вышеобозначенных процедур. Процедуры эти, выполняя определенные действия, вносят следующий указатель в структуру, отвечающую за персонажа. Таким образом, после того, как в структуре персонажа будет задан некий стартовый указатель, начинает выполняться саморегулирующийся цикл, который приводит спрайт в движение. Например, если мы запишем в структуру указатель действия «бег», то каждый шаг бега будет инициироваться предыдущим, задавая новый указатель в этой же структуре в поле ActionPtr во время каждой итерации. Кроме того, в этих процедурах будет производиться перемещение спрайта на экране и озвучивание его действий.

Всего указателей в том массиве 93 штуки. То есть игра поддерживает 93 действия для персонажа. Но поскольку указатели иногда повторяются, то различных действий несколько меньше. Эту структуру (персонажа) я приводил ранее, поэтому не буду подробно останавливаться на ее разборе. Если же изучить действия двойника, то можно заметить, что его структура по смыслу повторяет структуру самого принца. Иными словами, когда в комнате появляется двойник, то после структуры, описывающей принца, вставляется такая же структура, которая описывает уже двойника. Взводится флаг в ячейке $0401, а дальше движок, в зависимости от номера уровня, номера комнаты и наших действий, вносит в эту структуру изменения, приводя, таким образом, двойника в движение.

Полный код (в псевдокоде) 'привода' двойника

char sub_A25D() { 	if ( !MIRROR_FLAG ) goto label_A277; 	$06E0 = MIRROR_FLAG; 	if ( level == #03 ) goto label_A28D;  	if ( level != #04 ) goto label_A272; 	goto label_A319;  label_A272: 	// here CMP #05 	goto label_A398;  label_A277: 	return;  label_A278: 	$0072 = #86; 	$0073 = #A3; 	$04FE = #00; 	MIRROR.Y.LOW = #40; 	return sub_A326();  label_A28D: 	$04FB = $0402 = #00; 	MIRROR.DIRECTION = #FF; 	if ( room != #03 ) goto label_A2CF;  	if ( $04FD ) goto label_A278;  	if ( PRINCE.Y.LOW >= #48 ) goto label_A2CF;	 	$04FB = $04FC;	// if prince around mirror (by Y pos) 	if ( !$04FB ) goto label_A2C3;  	if ( PRINCE.X.HI ) goto label_A2C3; 	if ( PRINCE.X.LOW <= #AC ) goto label_A2D0;  label_A2C3: 	A = #02; 	Y = #0E; 	sub_CAFD(); 	MIRROR.X.HI = #02;  label_A2CF: 	return;	  label_A2D0: 	X = #98; 	if ( !PRINCE.DIRECTION ) goto label_A2D9; 	X = #94;  label_A2D9: 	MIRROR.X.LOW = X; 	MIRROR.X.HI = #00; 	MIRROR.Y.LOW = PRINCE.Y.LOW; 	if ( PRINCE.ACTION_INDEX == #06 ) goto label_A2C3; 	if ( PRINCE.POSE_INDEX <= #06 ) goto label_A2F9; 	if ( PRINCE.POSE_INDEX <= #0E ) goto label_A301;  label_A2F9: 	if ( PRINCE.POSE_INDEX <= #20 ) goto label_A302; 	if ( PRINCE.POSE_INDEX >= #28 ) goto label_A302;  label_A301: 	return;  label_A302: 	X = #00; 	Y = #05;  label_A306: 	MIRROR.ACTION_PTR = PRINCE.ACTION_PTR; 	X++; 	Y--; 	if ( Y ) goto label_A306;  	MIRROR.DIRECTION = PRINCE.DIRECTION xor #FF; 	return;  label_A319: 	$0072 = #89; 	$0073 = #A3; 	if ( !$0402 ) goto label_A379; 	 label_A326: 	if ( !$0401 ) goto label_A379; 	$404++; 	if ( $72[Y] == #FF ) goto label_A37A; 	if ( $0404 ) goto label_A347;  	$0404 = #00; 	$0403 += 2;  label_A347: 	Y++; 	 	A = $72[Y]; 	Y = #0E; 	sub_CAFD(); 	if ( MIRROR.POSE_INDEX != #6D ) goto label_A379; 	$0054 = $06F0 = #00; 	$04B1 = #03; 	sub_DB23(); 	A = #27; 	sub_F203(); 	#0610[X] = #02; 	A = #20; 	sub_F203(); 	#0610[X] = #02;  label_A379: 	return;  label_A37A: 	$060E = $061C = $0401 = #00; 	return;  A386: 	.data[XX:YY],1C:01 FF // XX:time, YY:action, FF:EOF  A389: 	.data[XX:YY],15:01 06:0D 02:02 03:32 0C:4E 02:05 17:01 FF  label_A398: 	if ( level == #05 && !$610 ) goto label_A3A7; 	if ( PRINCE.X.LOW <= #10 ) goto label_A3A7; 	goto label_A3D1;  label_A3A7: 	$0054 = #00; 	$04B1 = #0C; 	if ( !sub_DB18() ) goto label_A3D1; 	 	$0072 = #D2; 	$0073 = #A3; 	sub_A326(); 	if ( MIRROR.POSE_INDEX != #7C ) goto label_A3D1; 	$06FC = #00; 	$06FB = #0B;  label_A3D1: 	return;	  A3D2: 	.data[XX:YY]: 04:02 19:2A F0:02 F0:02 F0:02 FF  }  char sub_F203() { 	$0017 = A; 	switch_bank(#02); 	sub_B298(); 	$04BF = Y; 	switch_bank($06D1); 	Y = $04BF; 	return; }  char sub_B298() { 	X=#00;  label_B29A:	// aka sub_B29A 	Y=#00; 	if ( #060E[X] != #FF ) goto label_B2A4; 	Y++;  label_B2A4: 	if ( #060E[X] & #7F == $0017 ) goto label_B2BC; 	if ( #060F[X] != #FF ) goto label_B2B2; 	Y++;  label_B2B2: 	if ( Y == #02 ) goto label_B2BF; 	sub_F215(); 	goto label_B29A;  label_B2BC: 	Y = #01; 	return;  label_B2BF: 	Y = #00; 	return; }  char sub_8730() { 	if ( A == #0616[Y] ) goto label_874B; // $0616+Y - address of MIRROR.ACTION_INDEX 	$0616[Y] = A; 	X = A << 1; 	#0613[Y] = #95F2[X];            // set MIRROR.ACTION_PTR to new value ($0613 + Y - address of ACTION_PTR in MIRROR struct) 	#0614[Y] = #95F3[X]; 	#0618[Y] = #FF;                    // set EOF marker  label_874B: 	return; } 

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

Делаем патч

Такой код увязать с редактором довольно сложно. Можно, конечно, редактировать те короткие массивы данных, которые использует вышеприведенный код, но пришлось бы проделать определенную работу, а эффект был бы незначительный. Редактировать же этот код редактором слишком сложно. Хотелось чего-то большего. И решение было найдено.

Для управления двойником нам достаточно будет скопировать его структуру после структуры самого принца и выставить флаг $0401. В дальнейшем мы будем задавать действия, которые он будет выполнять, записывая указатель в его структуру. Но как это сделать? Нужно написать код. Но куда его вставить? В игре практически нет свободных мест, а те небольшие огрызки по несколько байт, которые остались между кодом и данными, использовать невозможно. Значит, надо изыскивать дополнительное место иными способами.

Шире круг

Как мы помним, Mapper #02 содержит в себе два различных вида маппинга. Один из них — UNROM, — содержит в себе 8 банков PRG-ROM, а второй — UOROM, — 16. Если вставить в ROM-файл еще 8×16 кБ, то маппер изменится на UOROM без вреда для игры. Вставить, однако, надо так, чтобы последний банк так и остался последним, а первые 7 должны остаться в начале.

Лезем в шестнадцатеричный редактор, меняем в заголовке число банков (пятый по счету байт, смещение 0x04) с #08 на #10, затем вставляем в файл 8×16 кБ нулей, начиная со смещения 0x1C010. Размер ROM-файла изменится и станет равным 262 160 байт. Запустим полученный ROM в эмуляторе… Работает!
Если мы будем выполнять эту процедуру в «железе», то нам потребуется поменять контроллер маппера, а ROM-память поставить увеличенную — 32×8 кБ, и мы также получим работающую игру.

ROM увеличили, место есть, но как им воспользоваться? Для того, чтобы вызвать код или прочитать оттуда данные, нам надо включить этот банк и передать туда управление. Сделать это можно безопасно только из последнего банка, но в нем нет места. Куда же писать код?
Зададимся требованиями:

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

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

;; ... $CC3B:20 00 CB	JSR $CB00 $CC3E:20 12 9F	JSR $9F12 $CC41:20 DD A3	JSR $A3DD $CC44:4C 1A CC	JMP $CC1A 

Процедуры $9F12, $A3DD трогать нельзя, так как они в другом банке, на месте которого должен быть наш. Перенести их туда тоже нельзя, так как они потянут за собой все остальное содержимое банка. Можно, однако, поменять адрес $CB00 на адрес новой процедуры, которая будет включать наш банк #07, вызывать наш код, затем вызывать оригинальную процедуру $CB00 и возвращать управление обратно в цикл. Код примерно такой:

LDA #07 JSR $F2D3			;; включаем банк #07 JSR $8000			;; вызываем наш код, который будет в начале банка JMP $CB00			;; вызываем оригинальную процедуру $CB00,  					;; которая сама инструкцией RTS вернет управление в основной цикл 

12 байт (по 3 байта на каждую инструкцию). Немного, но их надо куда-то вставить, а места и так нет. Немного места можно выиграть путем модификации существующего кода в последнем банке. Достаточно взять какую-нибудь длинную процедуру, которая не использует данные из банков с #00 по #06, которая так же не вызывает процедур, использующих эти банки, и которая вызывается из процедур, которые не используют эти банки. После того, как мы ее найдем, мы сможем перенести ее в новый банк, на ее месте разместить наш код, а перед нашим кодом поместим вызов страдалицы из нового банка.

Долго ли, коротко ли, такая процедура была найдена:

$C111:AD C6 06	LDA $06C6 = #$00 $C114:C9 06	CMP #$06 $C116:B0 20	BCS $C138 $C118:AD D7 06	LDA $06D7 = #$04 $C11B:D0 1B	BNE $C138 $C11D:8D 30 07	STA $0730 = #$00 $C120:85 54	STA $0054 = #$01 $C122:8D F0 06	STA $06F0 = #$00 $C125:AD 17 06	LDA $0617 = #$0C $C128:C9 11	CMP #$11 $C12A:90 0D	BCC $C139 $C12C:C9 2B	CMP #$2B $C12E:B0 09	BCS $C139 $C130:C9 1A	CMP #$1A $C132:90 04	BCC $C138 $C134:C9 26	CMP #$26 $C136:90 01	BCC $C139 $C138:60	RTS 

Что она делает — вопрос открытый, но впрочем это и не важно. Главное, что она соответствует всем необходимым требованиям: не вызывается из «динамических» банков, сама не вызывает код из них и не обращается к ним. А переход по адресу $C139 мы немного переделаем.
Немного модифицируем наш код включения банка и поместим его по адресу $C111:

$C111:20 17 C1	JSR $C117		;; вызываем код включения банка $C114:4C 90 BF	JMP $BF90		;; и вызываем оригинальную процедуру (она теперь живет по адресу $BF90) 										;; потом она инструкцией RTS вернет управление вызвавшему коду сама  ;;	======== процедура включения нашего банка ========== $C117:A9 07	LDA #$07 $C119:4C D3 F2	JMP $F2D3 ;;	======== конец процедуры включения нашего банка ==========  										;; а теперь код, вызывающий наш патч $C11C:20 17 C1	JSR $C117		;; включаем банк $C11F:20 10 B0	JSR $B010		;; вызываем наш патч. Начало нашего кода - $B010. $C122:4C 00 CB	JMP $CB00		;; вызываем оригинальную $CB00, которая вернет нас обратно в цикл $C125:00	BRK ;; ... $C138:00	BRK 

Мало того, что мы успешно впихнули вызов нашего кода, так у нас еще и осталось уйма места с $C125 по $C138, которое мы можем использовать как-нибудь еще: ведь у нас есть еще целых 7(!) свободных банков, а с ними тоже надо будет работать из последнего банка (если мы их будем в будущем использовать). Адрес нового кода я разместил по адресу $B010 (примерно середина банка), так как нам придется размещать копию уровней и комнат в нашем банке, плюс еще кое-какие данные. Но об этом чуть ниже.

Модифицируем и саму процедуру, так как в ней есть переходы на адрес $C139, который за ее пределами:

$BF90:AD C6 06		LDA $06C6 = #$00  ;; ============ CUT ============  $BFAA:90 0D		BCC $BFB9 $BFAC:C9 2B		CMP #$2B $BFAE:B0 09		BCS $BFB9 $BFB0:C9 1A		CMP #$1A $BFB2:90 04		BCC $BFB8 $BFB4:C9 26		CMP #$26 $BFB6:90 01		BCC $BFB9 $BFB8:60		RTS $BFB9:4C 39 C1		JMP $C139 

Все! То, что когда-то переходило на адрес $C139 теперь переходит на адрес $BFB9, по которому инструкция JMP заставляет прыгнуть на $C139, словно мы никуда и не перемещали код.
Тело основного цикла теперь мы можем поменять на следующее:

;; ... $CC3B:20 00 CB	JSR $C11С $CC3E:20 12 9F	JSR $9F12 $CC41:20 DD A3	JSR $A3DD $CC44:4C 1A CC	JMP $CC1A 

Можем разместить по адресу $B010 какую-нибудь пустышку вроде «RTS» и запустить игру в эмуляторе. Все как было — так и осталось.

Пишем свой «привод» для «отражения»

Осталось только разработать свой алгоритм появления отражения в комнате.
Я разработал следующую структуру данных:

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

  • Структура, описывающая начальное состояние персонажа (struct CHARACTER);
  • Пары «время»:«действие», которые описывают, что будет делать персонаж и в какой интервал времени;
  • Маркер окончания #FF.

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

Реагируем на события

Безусловное появление отражения неинтересно. Вроде оно и есть, но толку никакого. Хотелось бы, чтобы он появлялся в соответствии с какими-либо игровыми событиями и что-то умел делать.

Начиная с адреса $0500 в памяти у нас лежат данные, которые определяют те или иные изменения в комнатах. Например, если персонаж выпьет зелье из бутылки, то бутылка там больше не появится. Либо, если упадет плита, то в этом массиве будет хранится как то, что теперь на ее месте дырка, так и то, что на том месте, куда она упала — ее осколки. То же самое с открытыми и закрытыми решетками. Каждое такое событие кодируется двумя битами. В комнате у нас 30 блоков, на каждую строку из 10 блоков приходится по 3 байта (4 блока умещается в 1 байте, причем в третьем байте оставшиеся 4 бита остаются неиспользованными), итого по 9 байт на комнату. На уровень, таким образом, выходит 9*24 = 216 байт.

Как только произошло какое-нибудь действие, соответствующая пара бит в этом массиве устанавливается в определенное значение. Всего возможных комбинаций — 3 (00 — означает, что ничего не происходило), а событий много: решетка открылась, решетка закрылась, выпили из бутылки, плита отсутствует (упала), осколки упавшей плиты; соответственно события перекрываются. Например, если мы поставим бутылку, а над ней повесим падающую плиту, то после ее падения мы либо недосчитаемся бутылки, либо не увидим осколков упавшей плиты.

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

Заставляем нажать кнопку

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

Пока наш принц что-то делает в игре, его координаты постоянно сравниваются с блоком, в котором он находится, и если этот блок «активный», то предпринимаются определенные действия: например, если мы попали в блок «Кнопка», то эта кнопка будет нажиматься. Двойник же бегает сам по себе и никто за его передвижениями не следит. Значит, мы в своем патче обязаны это сделать за основной движок. Для этого достаточно передать в основной движок (в процедуру проверки) координату двойника вместо координаты принца, а дальше все произойдет само. Но проблема в том, что движок сравнивает блок с массивом данных комнаты, который хранится в другом банке. Тем не менее, в нашем банке еще достаточно места, а во время проверки будет включен именно он, поэтому мы… просто скопируем игровые данные из оригинального банка в наш на то же место, и движок будет считывать эти данные как ни в чем не бывало (помните, ранее мы разместили код в середине банка?). Во время редактирования, правда, теперь надо будет учесть, что изменения следует вносить в основную копию и в резервную.

Результат налицо:

FIN

В конечном итоге удалось заставить двойника быть не просто мебелью, а выполнять простейшее действие — нажать кнопку и открыть нам какую-нибудь дверь. Уже с такой простой вещью в игру удалось привнести элемент головоломки: пока не нажмешь кнопку, не разобьешь плиту или не выпьешь из бутылки в одном конце уровня — в другом не появится отражение, которое не нажмет за тебя кнопку, открывающую, к примеру, выход.

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

PS

Но это еще не конец. После окончания последует эпилог: с редактором я закончил, но остался еще один невыясненный вопрос, который хотелось бы разобрать. Так что впереди «Эпилог. Темница».

ссылка на оригинал статьи http://habrahabr.ru/post/192832/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *