![image](https://cdn.shopify.com/s/files/1/1494/4102/files/4_e9a3d5a6-85e0-4781-90a0-2b317aad18a2_grande.gif?v=1566480971)
Если вы когда-нибудь занимались программированием, то знакомы с концепцией багов. Если бы они нам не досаждали, процесс разработки стал бы намного быстрее и приятней. Но эти баги просто ждут момента, чтобы испортить наш код, рабочий график и творческий поток. К счастью, существует множество инструментов и стратегий для уничтожения багов даже в коде программистов ретро-игр.
Инструменты отладки
Один из лучших способов отладки кода — использование отладчика. В некоторых версиях эмуляторов FCEUX и Mesen есть встроенный отладчик, позволяющий прерывать в любой момент времени выполнение программы для проверки работоспособности кода.
![](https://cdn.shopify.com/s/files/1/1494/4102/files/1_aa82289d-4ed7-400e-83ff-e7ea02af36f7_grande.png?v=1566480748)
Отладчик эмулятора FCEUX
Стоит заметить, что этот способ больше подходит для продвинутых программистов, работающих с языком ассемблера. Но мы новички, поэтому будем писать на языке C (cc65). Разумеется, компилятор станет играть по своим собственным правилам, и нам будет трудно разбираться с машинным кодом, скомпилированным из кода на C.
Шестнадцатеричный редактор FCEUX
Допустим, нам нужно понаблюдать за какой-то переменной или массивом. Добавим к опциям компоновщика (ld65) такую строку:
-Ln labels.txt
После компиляции проекта в его папке появится файл
labels.txt
. Просто откроем его в любой программе для просмотра текстов и поищем имя переменной, за которой мы хотим понаблюдать.(Примечание: если вы объявили статическую переменную, то она не будет включена в этот список. Поэтому вместо
static unsigned char playerX
используйте unsigned char playerX
)![](https://cdn.shopify.com/s/files/1/1494/4102/files/2_cc0b6d79-a569-41bd-8b70-8ef06ff60a49_grande.png?v=1566480888)
Теперь мы знаем адрес нужной переменной, неплохо. Давайте найдём её в отладчике. Запустим ROM игры в эмуляторе FCEUX. В меню Debug выберем пункт Hex Editor, а в открывшемся окне нажмём Ctrl + G и введём адрес нашей переменной:
![](https://cdn.shopify.com/s/files/1/1494/4102/files/3_b91875ad-0ffc-4b39-a19c-da4068bc4623_large.png?v=1566480932)
Нажмём OK, и курсор переместится к адресу, где расположена переменная. Давайте взглянем на неё:
![](https://cdn.shopify.com/s/files/1/1494/4102/files/4_e9a3d5a6-85e0-4781-90a0-2b317aad18a2_grande.gif?v=1566480971)
Это может быть полезно для проверки правильности заполнения массива или для отслеживания изменений в конкретных переменных. Кроме того, вы сможете почувствовать себя Большим братом, внимательно следящим за своим кодом.
Изучите и другие полезные инструменты меню Debug эмулятора FCEUX, например, PPU Viewer, Name table Viewer и т.п.
Упрощение процесса отладки
А если мы не хотим каждый раз запускать отладчик для наблюдения за переменной? Более продвинутый способ заключается в написании процедуры, выводящей значение на экран. Давайте попробум использовать счёт очков в интерфейсе для отображения позиции игрока по оси Y:
![](https://cdn.shopify.com/s/files/1/1494/4102/files/5_1056a8eb-9477-4e98-ac72-eec8997aaa12_large.gif?v=1566481118)
Работает идеально!
Ретро-кодер и владелец блога nesdoug Дуг Фрейкер создал похожий метод для использования экранных визуализаций в целях отладки. Показанная ниже процедура создаёт на экране серую линию, наглядно демонстрирующую степень нагрузки на ЦП:
// void gray_line(void);
// For debugging. Insert at the end of the game loop, to see how much frame is left.
// Will print a gray line on the screen. Distance to the bottom = how much is left.
// No line, possibly means that you are in v-blank.
_gray_line:
lda <PPU_MASK_VAR
and #$1f ;no color emphasis bits
ora #1 ;yes gray bit
sta PPU_MASK
ldx #20 ;wait
@loop2:
dex
bne @loop2
lda <PPU_MASK_VAR ;normal
sta PPU_MASK
rts
Можно просто скопировать эту процедуру в своей код или включить в проект библиотеку nesdoug.h. Процедуру нужно вызывать после завершения игрового цикла, тогда на экране будет отображаться серая полоса.
![](https://cdn.shopify.com/s/files/1/1494/4102/files/6_a3335394-867f-432d-85df-f0c710642d88_large.gif?v=1566481409)
Сработало, но, похоже, у меня есть ещё один баг! Мы избавимся от него позже. А пока давайте двигаться дальше.
Мощь макросов
Полезным инструментом отладки также могут стать макросы. Они позволят найти место в коде, ставшее источником бага.
Давайте создадим какой-нибудь макрос, который будет подавать нам в нужное время сигналы, например, воспроизводить звук или выделять нулевую палитру необходимым значением. У нас есть несколько макросов, меняющих нулевую палитру на красный, синий и случайные цвета, а также воспроизводящие звук:
![](https://cdn.shopify.com/s/files/1/1494/4102/files/7_1323247c-ddbb-4b3e-98c8-05d4acf30009_large.png?v=1566481479)
Как это работает? Допустим, вам проект успешно скомпилировался, вы запускаете эмулятор со своей игрой, нажимаете на кнопку Start и…
![](https://cdn.shopify.com/s/files/1/1494/4102/files/8_a2f2dec6-ff4e-448b-8d86-a2b33fe02948_large.gif?v=1566481519)
Похоже, кроме белого экрана ничего нет. Кроме того, некоторые эмуляторы могут сообщить в строке состояния «CPU jam!» Что же делать дальше?
Во-первых, нужно локализовать код, в котором происходит ошибка. И здесь в дело вступает мой макрос звука.
Мы точно знаем, что главное меню работает. Давайте посмотрим, что происходит после него:
playMainMenu();
player.lives = 9;
points = 0;
gameFlags = 0;
while(current_level<7 && player.lives>0)
{
set_world(current_world);
debugSound;
playCurrentLevel();
}
У меня есть подозрение, что игра вылетает при выполнении процедуры
set_world
. Давайте проверим эту догадку. Я просто введу имя макроса в следующую строку после проверяемой процедуры.Мы запускаем проект и… Я слышу звук! То есть эта процедура выполнилась успешно, и нам нужно проверить следующую:
playCurrentLevel
. Давайте переместим макрос отладки ниже:while(current_level<7 && player.lives>0)
{
set_world();
playCurrentLevel():
debugSound;
}
Я снова запускаю проект, но звука не слышу. Это означает, что процедура не завершена, и сбой происходит внутри неё.
В таких случаях следует открывать код процедуры и продолжать применять эту методику, пока не удастся сузить круг поисков возможного местонахождения бага.
Изменяющий палитру макрос может также быть полезен для проверки условий. Например наш код выполняет сложную проверку нескольких условий:
if ( (getTile(objX, objY+16) || collide16() ) || (objsOX[i] && objY>objsOX[i]))
{
debugRed;
objsSTATE[i]=THWOMP_SMASH;
objY=objsY[i]-=4;
objsFRM[i]=0;
sfx_play(SFX_THWOMP_SLAM_DOWN,2);
}
Если мы изменим здесь цвет палитры, то увидим, выполняется ли условие:
![](https://cdn.shopify.com/s/files/1/1494/4102/files/10_large.gif?v=1566481808)
Похоже, что с этой курицей всё в порядке. Но если флаг не работает, то одно из условий не выполняется. В этом случае нужно проверить их все по отдельности и тогда, возможно, вы найдёте ещё один баг.
Ядерный вариант
Недавно я обнаружил, что один из призраков в моей игре демонстрирует какое-то подозрительное поведение. Время от времени он отказывался нападать на игрока.
Взгляните на этого поражённого багом призрака — он нападает только когда персонаж находится близко к центру экрана:
![](https://cdn.shopify.com/s/files/1/1494/4102/files/11_large.gif?v=1566481894)
Как бы упорно я ни изучал код этой процедуры, мне не удавалось понять, где скрыт баг, поэтому я решил пойти на крайние меры и протестировать работу этого кода в современной среде разработки.
Я взял всё необходимое: карту экрана, массив с атрибутами метатайлов, код процедуры и просто вставил их в Visual Studio 2017:
![](https://cdn.shopify.com/s/files/1/1494/4102/files/12_861a71ca-31a9-46e2-ad3e-17e781ced5f8_large.png?v=1566481939)
На PC код работал совершенно так же. Как оказалось, баг скрывался в процедуре, заполнявшей кэш для нахождения препятствий между игроком и врагом. Массив заполнялся некорректно. Я уверен, что здесь вместо 0x80 должен быть 0.
![](https://cdn.shopify.com/s/files/1/1494/4102/files/15_a581bce8-1d84-4f3e-b944-1d9969eb49c5_large.png?v=1566481973)
Итак, я попытаюсь пошагово отладить код, чтобы выяснить, почему это происходит.
![](https://cdn.shopify.com/s/files/1/1494/4102/files/16_large.gif?v=1566482016)
Забавно, но похоже, что я выполнял действия в неправильной последовательности. Давайте исправим её и снова проверим массив!
![](https://cdn.shopify.com/s/files/1/1494/4102/files/20_large.png?v=1566482054)
Кажется, теперь массив заполняется правильно. То есть мне достаточно просто исправить код cc65 и снова скомпилировать проект NES.
Итак, современные инструменты разработки способны помочь в отладке алгоритмов и устранении багов.
Избавляйтесь от багов спокойно
Баги раздражают, как может раздражать и отладка кода. Просто оставайтесь спокойными, не теряйте контроля и используйте весь ассортимент доступных инструментов для поиска и уничтожения этих вредителей. Качество вашего кода и спокойствие ума значительно повысятся.
Хотите получать советы непосредственно от профессионалов ретро-разработки? Добро пожаловать в наш Discord!
Наша игра The Meating доступна здесь!