为NES调试游戏:今天如何发生

图片

如果您曾经参与编程,那么您将熟悉错误的概念。 如果他们不打扰我们,那么开发过程将变得更快,更有趣。 但是这些错误只是在等待破坏我们的代码,工作进度和创意流程的时刻。 幸运的是,即使在复古游戏程序员的代码中,也有许多工具和策略可以消除错误。

调试工具


调试代码的最佳方法之一是使用调试器。 FCEUX和Mesen仿真器的某些版本具有内置的调试器,可让您随时中断程序以检查代码的运行状况。


FCEUX仿真器调试器

值得注意的是,此方法更适合使用汇编语言的高级程序员。 但是我们是新手,所以我们将用C(cc65)编写。 当然,编译器将按照自己的规则运行,并且我们很难处理从C代码编译的机器代码。

十六进制编辑器FCEUX

假设我们需要观察某种变量或数组。 -Ln labels.txt添加到链接器选项(ld65):- -Ln labels.txt

编译项目后, labels.txt文件将出现在其文件夹中。 只需在用于查看文本的任何程序中打开它,然后查找我们要观察的变量的名称即可。

注意:如果声明了静态变量,则该变量将不包含在此列表中。因此, static unsigned char playerX使用unsigned char playerX代替static unsigned char playerX unsigned char playerX


现在我们知道所需变量的地址,还不错。 让我们在调试器中找到它。 在FCEUX仿真器中运行ROM游戏。 在“调试”菜单中,选择“十六进制编辑器”,然后在打开的窗口中按Ctrl + G并输入变量的地址:


按确定,光标将移动到变量所在的地址。 让我们看一下:


这对于检查数组是否正确填充或跟踪特定变量的更改很有用。 此外,仔细观察您的代码,您会感觉像个老大哥。

查看其他有用的FCEUX仿真器Debug菜单工具,例如PPU Viewer,Name table Viewer等。

简化调试过程


如果我们不想每次都运行调试器来观察变量? 一种更高级的方法是编写一个将值显示在屏幕上的过程。 让我们尝试使用界面中的分数来显示玩家在Y轴上的位置:


完美的作品!

Retro-coder和nesdoug博客所有者 Doug Fraker创建了一种类似的方法,用于将屏幕可视化用于调试目的。 下面显示的过程在屏幕上创建了一条灰色线,清楚地表明了CPU负载的程度:

 // 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库包含在项目中。 游戏周期结束后必须调用该过程,然后屏幕上将显示一个灰色条。


它起作用了,但是看起来我还有一个错误! 我们稍后将摆脱他。 同时,让我们继续前进。

宏的力量


宏也可以是有用的调试工具。 它们将使您能够在代码中找到已成为该错误源的位置。

让我们创建一种宏,它可以在正确的时间为我们提供信号,例如,播放声音或选择具有必要值的零调色板。 我们有几个宏可以将零调色板更改为红色,蓝色和随机颜色,并可以再现声音:


如何运作? 假设您已成功编译了一个项目,则使用游戏启动了仿真器,单击“开始”按钮,然后...


似乎只有白色的屏幕。 此外,某些仿真器可能会在状态栏中报告“ CPU卡塞!”。 接下来要做什么?

首先,您需要本地化发生错误的代码。 在这里,我的声音宏开始发挥作用。

我们肯定知道主菜单正在工作。 让我们看看之后会发生什么:

 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); } 

如果在这里更改调色板的颜色,我们将查看条件是否满足:


这只鸡似乎还可以。 但是,如果该标志不起作用,则不满足条件之一。 在这种情况下,您需要分别检查它们,然后,也许会发现另一个错误。

核选项


我最近发现我游戏中的一个鬼魂表现出某种可疑的行为。 他不时拒绝攻击玩家。

看看这个被bug击中的幽灵-它仅在角色靠近屏幕中心时才会攻击:


无论我多么努力地研究此过程的代码,我都无法弄清楚该错误的隐藏位置,所以我决定采取极端措施并在现代开发环境中测试该代码的工作。

我拿走了我需要的所有东西:屏幕图,具有元文件属性的数组,过程代码,然后将它们简单地插入Visual Studio 2017中:


在PC上,代码的工作原理完全相同。 事实证明,该漏洞隐藏在一个程序中,该程序填充了缓存以查找玩家与敌人之间的障碍。 数组填充不正确。 我确定应该是0而不是0x80。


因此,我将尝试逐步调试代码以找出发生这种情况的原因。


这很有趣,但是看起来我按错误的顺序进行了操作。 让我们修复它,然后再次检查阵列!


现在看来该数组正在正确填充。 也就是说,我只需要修复cc65代码并再次编译NES项目即可。


因此,现代开发工具可以帮助调试算法和修复错误。

冷静地消除错误


错误很烦人,代码也很烦人。 保持镇定,不要失去控制,并使用所有可用工具来搜索和消除这些害虫。 您的代码质量和高枕无忧。

是否想直接从复古设计专业人士那里获得技巧? 欢迎来到我们的不和谐!

我们的游戏肉食在这里可用!

Source: https://habr.com/ru/post/zh-CN466653/


All Articles