10

如果可以模拟 Game 对象以测试我的 DrawableGameComponent 组件,我在徘徊吗?

我知道模拟框架需要一个接口才能运行,但我需要模拟实际的Game对象。

编辑:这里是XNA 社区论坛上各自讨论的链接。有什么帮助吗?

4

6 回答 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);
        }
    }
}

这给了我很大的灵活性来制作可能覆盖 Update() 的实体(AIEntity、NonInteractiveEntity...)的子类。它还让我可以自由地对 Drawable 进行子类化,而无需像AnimatedSpriteAIEntity,ParticleEffectNonInteractiveEntityAnimatedSpriteNoninteractiveEntity. 相反,我可以这样做:

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

子类定义了它们自己的 Draw 逻辑。在您的坦克示例中,您可以执行以下操作:

  • 为每个项目符号添加一个新实体
  • 创建一个 TankEntity 类,它定义了一个列表,并覆盖 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);
        }
    }
}
于 2009-05-06T06:02:10.433 回答
3

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

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

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

于 2009-05-05T16:06:16.823 回答
3

你不必嘲笑它。为什么不制作一个假游戏对象?

从 Game 继承并覆盖您打算在测试中使用的方法,以便为您需要的任何方法/属性返回预设值或快捷计算。然后将假货传递给您的测试。

在模拟框架之前,人们推出了他们自己的模拟/存根/假货——也许它没有那么快速和容易,但你仍然可以。

于 2009-05-05T17:31:58.100 回答
2

你可以使用一个名为 TypeMock 的工具,我相信它不需要你有一个界面。您的另一个更常用的方法是创建一个继承自 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...
}

它有点乏味,但它可以让你实现可模拟性。

于 2009-05-04T12:46:36.907 回答
1

如果你不介意的话,我会背上你的帖子,因为的帖子似乎不太活跃,而且你已经把你的代表放在了线上;)

当我阅读您的帖子(包括此处和 XNAForum)时,我认为这既是框架可能更平易近人,也是我(我们的)设计并非完美无缺。

该框架可以设计得更容易扩展。我很难相信肖恩关于接口性能命中的主要论点。为了支持我,他的同事说性能命中可以很容易地规避。
请注意,该框架已经具有 IUpdatable 和 IDrawable 接口。为什么不一路走?

另一方面,我也认为我(和你)的设计确实不是完美无缺的。在我不依赖 Game 对象的地方,我确实非常依赖 GraphicsDevice 对象。我要看看我怎么能绕过这个。它会使代码变得更复杂,但我认为我确实可以打破这些依赖关系。

于 2009-05-06T07:48:17.247 回答
0

对于这样的事情的起点,我会点击XNA WinForms Sample。使用此示例作为模型,在 WinForm 中可视化组件的一种方法似乎是为它创建一个控件,其样式与示例中的 SpinningTriangleControl 相同。这演示了如何在没有 Game 实例的情况下呈现 XNA 代码。游戏真的不重要,重要的是它为你做了什么。因此,您要做的是创建一个库项目,该项目在一个类和您的其他项目中具有组件的加载/绘制逻辑,创建一个控件类和一个组件类,它们是各自环境中库代码的包装器。这样,您的测试代码就不会重复,您不必担心编写在两种不同场景中始终可行的代码。

于 2009-05-06T06:34:58.937 回答