TDD和游戏物理学

8

我正在玩一个小游戏项目,由于我在TDD方面经验不足,因此我希望能得到一些专家对几件事情的意见。

首先,我很早就意识到TDD似乎并不适合游戏开发。关于这个问题,意见似乎相当分歧。我的最初的无知看法是,TDD似乎非常适合所有游戏逻辑。我自认为任何处理视频输出和声音的东西都会被抽象成类,并进行视觉测试。

事情开始顺利。目标是创建一个2D太空飞行游戏(对于那些关心的人来说是类似于《小行星》的游戏)。我为Ship类创建了一系列单元测试。像初始化、旋转等东西可以很容易地进行测试,例如: GetRotation(),TurnRotateRightOn(),Update(1),GetRotation(),Expect_NE(rotation1, rotation2)。然后我遇到了第一个问题。

我对TDD的理解是,你应该按照你认为应该使用类的方式编写测试。我想让飞船移动,所以我编写了一个基本上是这样的类。GetCoordinates(),ThrustOn(),Update(1),GetCoordinates()。这样做没问题,可以确保飞船移动到某个地方。然而,我很快意识到我必须确保飞船朝着正确的方向以正确的速度移动。接下来是一个75行的单元测试,基本上我必须初始化旋转、检查坐标、初始化推力、更新飞船、获取新坐标、检查新旋转。更重要的是,我看不出在游戏中有必要获得飞船的速度(只需要坐标,飞船应该自己更新)。因此,我没有直接获取速度的方法。所以测试基本上必须重新计算速度应该是多少,以便我可以确保它与更新后获取的坐标相匹配。总之,这非常混乱,非常耗时,但起作用了。测试失败,使测试通过,等等。

这很好,直到后来我意识到我想将飞船的更新代码重构为抽象的“Actor”类。我意识到,虽然每个Actor子类都需要能够正确计算新位置,但并不是每个子类都必须以相同的方式更新其速度(一些碰撞,一些不碰撞,一些具有静态速度)。现在,我基本上面临着复制和修改那个庞大的测试代码的前景,我不能不想到应该有更好的方法。

有没有人有处理单元测试这种复杂的黑盒类型工作的经验?看起来我基本上必须在测试中编写完全相同的物理代码,以便我知道结果应该是什么。这似乎是自我毁灭的,我肯定在某个地方错过了所有这些的要点。如果有人能提供任何帮助或建议,我将非常感激。


这个问题不应该属于游戏开发SE吗?在那里他们可能会更好地回答它。 - Spoike
我之前不知道有游戏开发SE!以后我会在那里问类似的问题。谢谢! - Landon
3个回答

7
我建议您首先创建一个组件,根据控制输入序列计算位置和方向。该组件构成了测试的“单元”。该组件的测试用例将涵盖所有可能的情况:零加速度、恒定非零加速度、脉冲加速度命令等。如果应用程序不需要速度,则该组件将不会暴露与速度相关的任何功能。
在生成包含在测试中的预期输出时,重要的是要高度自信这些预期结果是正确的。因此,需要尽量减少生成“预期”结果所需的代码量。特别是,如果您发现自己编写的测试支架与被测试的组件几乎一样复杂,那么测试本身出现错误的可能性就成为一个严重问题。
在这种情况下,我会直接从运动方程中生成测试数据。我使用Mathematica进行此操作,因为我可以直接输入方程式,解决它们,然后生成结果的图表和表格。这些图表让我可视化结果,并因此有信心它们是可信的正确的。Excel / OpenOffice / Google Apps也可以用于相同的目的,以及像Sage这样的Mathematica开源替代品。无论选择哪个工具,关键是能够解决运动方程而不必编写非平凡代码。
一旦我们有了一组良好的测试用例和预期结果,我们就可以编写单元测试。请注意,测试代码非常简单,不执行任何计算。它只是将组件的输出与我们之前获得的硬编码结果进行比较。有了测试用例,我们可以编写组件本身,并添加代码,直到所有测试都通过。当然,在严格的TDD中,这些操作按照完全相同的顺序进行。我承认我个人不遵循瀑布模型,而是在创建测试数据、编写测试和编写组件代码之间反复跳转。
Mathematica或Excel文档本身在初始创建测试之外还有有用的生命。当添加新功能或(上帝禁止)稍后发现错误时,它们可以再次使用。我建议将这些文档视为源代码对待。
此练习结束后,结果是一个“防弹”组件,我们已经确信它将根据任何给定的控制输入计算出正确的位置和方向。从这个基础上,我们可以构建进一步使用该功能的组件,如飞船、小行星、碟形飞行器和射击物。为了避免每个组件的测试用例出现组合爆炸,我会离开严格的黑盒测试方法。因此,例如,如果我们正在测试一个“飞船”组件,我们将编写测试,知道它使用位置/方向组件。使用这种白盒知识,我们可以避免重新测试与运动相关的所有边角情况。飞船单元测试可以执行“冒烟测试”,以验证飞船实际上对控制输入做出响应,但主要重点将放在测试飞船组件本身的任何独特功能。
因此,总结一下:
  • 将位置/方向计算的责任归于单个组件
  • 使用外部工具生成全面测试用例的数据,以高度确信数据的正确性
  • 避免在测试中使用复杂的计算逻辑
  • 通过使用白盒知识,避免组件层次结构中的测试用例的组合爆炸

1
你可以简化你的测试。与其检查正确的向量和加速度,你只需检查被测试对象是否移动即可。在大多数游戏中,你会引入一些微小的随机因素到物理模型中以保持趣味性。

1

我认为采用更“分集”方法可能会有所帮助。与您正在执行的跟踪坐标和旋转不同,您可以简单地指定您的太空飞船在30个游戏步骤后应该在哪里以及旋转多少度。如果证明是正确的,那么您的飞船在中间也做了所有正确的事情。这样你就可以写更少的测试代码。

同样的想法适用于针对碰撞的“分集”。如果台球被设计成从两堵墙上弹回并撞到另一个球,您可以在最终碰撞发生时引发事件,并检查碰撞的“入射角度”。再次,您不需要检查中间的所有步骤。如果入射角度正确,则球在撞击最终球之前很可能已经正确地从两堵墙上弹回。

当然,您必须准备好没有任何碰撞发生的情况。您的测试可以考虑每单位时间的游戏点击次数以达到最终碰撞。您使测试运行所需的游戏点击次数以实现碰撞。如果在规定的点击次数内未发生碰撞,则测试可以正确失败。

所有这些都是在游戏点击中完成的,而不是实时进行的,以便测试可以几乎立即发生,而不必等待预期结果(如果您实际上正在玩游戏,通常会这样做)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接