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/
Добавить комментарий