XNA模拟游戏对象或解耦你的游戏

10
我想知道是否有可能模拟一个Game对象来测试我的DrawableGameComponent组件?
我知道模拟框架需要一个接口才能运行,但我需要模拟实际的Game对象。
编辑:这里是XNA社区论坛上相关讨论的链接。有帮助吗?

祝好运。在SlimDX上,我们正在评估全面切换到接口来处理这种用例。 - Promit
@Promit,接口并不是万能的...在某些情况下,抽象类才是正确的选择。虽然接口确实在某些情况下非常有用和正确,但它并不总是正确的工具 :-) - Joel Martinez
@Joel Martinez - 当涉及到模拟/伪造时,接口何时不是正确的工具? - Peter Lillevold
@Peter Lillevold - 关于这个话题最好的指导来自于.NET框架设计准则。你可以在这里在线阅读有趣的部分:http://msdn.microsoft.com/en-us/library/ms229013.aspx - Joel Martinez
6个回答

14

在那个论坛上有一些关于单元测试的好帖子。以下是我在XNA中进行单元测试的个人方法:

  • 忽略Draw()方法
  • 将复杂行为隔离在自己的类方法中
  • 测试棘手的部分,不必为其余部分费神

这是一个测试示例,用于确认我的Update方法在Update()调用之间以正确的距离移动实体。 (我使用NUnit.) 我删除了一些具有不同移动向量的行,但您可以理解:您不需要使用游戏来驱动测试。

[TestFixture]
public class EntityTest {
    [Test]
    public void testMovement() {
        float speed = 1.0f; // units per second
        float updateDuration = 1.0f; // seconds
        Vector2 moveVector = new Vector2(0f, 1f);
        Vector2 originalPosition = new Vector2(8f, 12f);

        Entity entity = new Entity("testGuy");
        entity.NextStep = moveVector;
        entity.Position = originalPosition;
        entity.Speed = speed;

        /*** Look ma, no Game! ***/
        entity.Update(updateDuration);

        Vector2 moveVectorDirection = moveVector;
        moveVectorDirection.Normalize();
        Vector2 expected = originalPosition +
            (speed * updateDuration * moveVectorDirection);

        float epsilon = 0.0001f; // using == on floats: bad idea
        Assert.Less(Math.Abs(expected.X - entity.Position.X), epsilon);
        Assert.Less(Math.Abs(expected.Y - entity.Position.Y), epsilon);
    }
}

编辑:以下是一些来自评论的注释:

我的实体类: 我选择将所有的游戏对象封装在一个集中的实体类中,大致如下:

public class Entity {
    public Vector2 Position { get; set; }
    public Drawable Drawable { get; set; }

    public void Update(double seconds) {
        // Entity Update logic...
        if (Drawable != null) {
            Drawable.Update(seconds);
        }
    }

    public void LoadContent(/* I forget the args */) {
        // Entity LoadContent logic...
        if (Drawable != null) {
            Drawable.LoadContent(seconds);
        }
    }
}
这让我有很大的灵活性,可以创建实体(Entity)的子类(AIEntity、NonInteractiveEntity等),这些子类可能会重写Update()方法。同时,这也让我可以自由地创建Drawable的子类,而不必像 AnimatedSpriteAIEntityParticleEffectNonInteractiveEntityAnimatedSpriteNoninteractiveEntity 那样需要n²个子类。代替这些,我可以这样做:
Entity torch = new NonInteractiveEntity();
torch.Drawable = new AnimatedSpriteDrawable("Animations\litTorch");
SomeGameScreen.AddEntity(torch);

// let's say you can load an enemy AI script like this
Entity enemy = new AIEntity("AIScritps\hostile");
enemy.Drawable = new AnimatedSpriteDrawable("Animations\ogre");
SomeGameScreen.AddEntity(enemy);

我的Drawable类:我有一个抽象类,所有绘制对象都是从该类派生的。我选择了抽象类,因为某些行为将被共享。如果你的代码不是这样的情况,定义为接口也是可以的。

public abstract class Drawable {
    // my game is 2d, so I use a Point to draw...
    public Point Coordinates { get; set; }
    // But I usually store my game state in a Vector2,
    // so I need a convenient way to convert. If this
    // were an interface, I'd have to write this code everywhere
    public void SetPosition(Vector2 value) {
        Coordinates = new Point((int)value.X, (int)value.Y);
    }

    // This is overridden by subclasses like AnimatedSprite and ParticleEffect
    public abstract void Draw(SpriteBatch spriteBatch, Rectangle visibleArea);
}

子类定义自己的绘制逻辑。在你的坦克示例中,你可以尝试以下几种方法:

  • 为每个子弹添加一个新实体
  • 创建一个TankEntity类,它定义了一个List,并覆盖Draw()方法以迭代子弹(它们定义了自己的Draw方法)
  • 创建一个ListDrawable

这是一个忽略了如何管理列表本身的ListDrawable的示例实现。

public class ListDrawable : Drawable {
    private List<Drawable> Children;
    // ...
    public override void Draw(SpriteBatch spriteBatch, Rectangle visibleArea) {
        if (Children == null) {
            return;
        }

        foreach (Drawable child in children) {
            child.Draw(spriteBatch, visibleArea);
        }
    }
}

你可以给我展示一下你的“Entity”接口吗?它是什么?一个组件?一个自定义类? - Andriy Drozdyuk
这是一个自定义类 - 我渲染的每个游戏对象都使用它。除了我的输入处理程序和http://creators.xna.com/en-US/samples/gamestatemanagement ScreenManager之外,我特别避免使用GameComponents。它定义了像Update(double time)这样的方法,该方法将速度*时间移动到NextStep方向。 - ojrac
为了支持多种类型的可绘制对象(精灵、动画精灵、粒子),我的实体包含一个 Drawable——一个处理 Draw() 调用的抽象类。 - ojrac
我设置了我的类结构,以允许有趣的继承——例如,一个扩展实体的AIEntity。如果我想要一个被呈现为粒子效果(例如寻找目标的魔法效果)的AIEntity怎么办?我必须创建一个AIEntity子类ParticleEffectAIEntity。如果我想要制作一个NonInteractiveEntity(一个不可移动的火炬,没有人能够拾起),它也有一个粒子效果(用于火焰)。我必须再创建一个ParticleEffectNonInteractiveEntity。 - ojrac
这次我已经添加了问题。如果想看答案,请跳到“My Drawable Class”部分。 - ojrac
显示剩余4条评论

3

MOQRhino Mocks这样的框架并不需要特定的接口。它们也可以模拟任何非密封和/或抽象类。Game是一个抽象类,所以你不应该有任何问题来模拟它 :-)

至少这两个框架需要注意的是,要对方法或属性设置任何期望值,它们必须是虚拟的或抽象的。原因是生成的模拟实例需要能够进行覆盖。IAmCodeMonkey提到的typemock我相信有一种方法可以解决这个问题,但我认为typemock不是免费的,而我提到的这两个是免费的。

另外,您还可以查看我的一个项目,它可以帮助创建XNA游戏的单元测试,而无需创建模拟:http://scurvytest.codeplex.com/


谢谢回答。不好意思,我真的没时间去调试另一个框架。不过看起来很有趣。 - Andriy Drozdyuk
我很想尝试一下这个。只是担心在过程中增加另一个不确定因素(对XNA和TDD都不熟悉)。有人有相关经验吗? - Boris Callens
@boris callens: 我经常使用Rhino Mocks。它表现非常出色。 - Skurmedel

3

你不必嘲弄它。为什么不创建一个假游戏对象呢?

从Game继承,并覆盖你想在测试中使用的方法,返回你需要的方法或属性的固定值或快捷计算。然后将该假对象传递给你的测试。

在模拟框架出现之前,人们自己编写模拟/存根/假对象 - 也许不那么快速简单,但你仍然可以这样做。


我喜欢。不知何故,这些框架的旋风使我的道路变得模糊了。我会尝试一下,并在这里向您汇报结果! - Andriy Drozdyuk
听起来很不错。祝你好运!我很兴奋想知道它的结果,因为我自己有一个已经搁置的四分之一完成的游戏项目。 - Steven Evers
经过进一步的研究,我很遗憾地无法找到一个简单的方法来解决这个问题。除非我伪造大量的方法,但我不确定它会产生什么副作用。还是谢谢你。 - Andriy Drozdyuk

2

您可以使用一个名为TypeMock的工具,我相信它不需要您拥有一个接口。您另外一个更常用的方法是创建一个新类,该类继承自Game并实现一个与Game对象匹配的接口。然后,您可以针对该接口编写代码,并传入您的“自定义”Game对象。

public class MyGameObject : Game, IGame
{
    //you can leave this empty since you are inheriting from Game.    
}

public IGame
{
    public GameComponentCollection Components { get; set; }
    public ContentManager Content { get; set; }
    //etc...
}

虽然有点繁琐,但它可以让你实现模拟。


有没有办法只继承一个而不是两个东西? 另外,从IGame继承会给我什么?好像不继承它也没什么变化。也许我对此不太清楚,抱歉。 - Andriy Drozdyuk
这样是行不通的,因为DrawableGameComponent的构造函数需要一个Game实例,而不是IGame ;-) - Joel Martinez

1

如果您不介意的话,我想借用一下您的帖子,因为我的似乎不太活跃,而您已经把您的知名度摆在了那里 ;)

当我阅读您的帖子(在这里和XNA论坛上)时,我认为可能是框架更易于接近,也可能是我(我们)的设计存在缺陷。

框架可能设计得更易于扩展。我很难相信 Shawn关于接口性能损失的主要论点。他的同事可以轻松地避免性能损失。
请注意,框架已经具有IUpdatable和IDrawable接口。何不一步到位呢?

另一方面,我也认为我的(和你的)设计确实不完美。在我不依赖于Game对象的情况下,我确实非常依赖GraphicsDevice对象。我将研究如何规避这个问题。这会让代码更加复杂,但我认为我确实可以打破这些依赖关系。

没错。例如,我面临的一个挑战是在构造函数中传递精灵批处理器还是从类内部创建一个。通过构造函数传递它可以使代码更加解耦,但这样我就必须做出关于何时调用sprite_batch.begin()和end()的假设。 - Andriy Drozdyuk
Jeps,我也遇到了同样的问题。现在我将其传递给构造函数,因为我不希望我的类知道它的样子。我认为这使它们可以移植到其他游戏中。 - Boris Callens
此外,在我能想象到的每个类中都使用begin()和end()可能会导致明显的减速。 - Boris Callens
我不确定减速的问题,我在一本书或文章中读到它并没有。 - Andriy Drozdyuk
这确实会改变整个想法。我应该在这方面进行一些搜索。但是,如果没有性能损失,我不明白为什么这些方法首先会存在.. - Boris Callens

0

对于这样的问题,我建议先参考XNA WinForms Sample。通过使用该示例作为模型,似乎在WinForm中可视化组件的一种方法是创建一个控件,其样式与示例中的SpinningTriangleControl相同。这演示了如何在没有Game实例的情况下呈现XNA代码。实际上,Game并不重要,重要的是它为您提供了什么。因此,您需要创建一个Library项目,其中包含组件的Load/Draw逻辑,并在其他项目中创建一个Control类和一个Component类,它们分别是其各自环境中库代码的包装器。这样,您测试的代码不会重复,也不必担心编写始终在两种不同情况下都可行的代码。


抱歉,如果没有阅读那个示例,你的回答就没有太多意义。我晚些时候会在家里查看它。 - Andriy Drozdyuk
好的,这个想法的要点是将渲染逻辑保留在单独的类中,并编写两个基本上是该渲染逻辑外壳的类,分别是GameComponent和WinForms Control。因此,您并不是编写一个伪装游戏实例的组件,而是编写一个从未使用过游戏实例的控件,并在两个演示类之间共享实现。 - Jeremy

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