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

时间:2009-04-30 00:27:21

标签: c# unit-testing mocking xna

如果可以模拟一个Game对象来测试我的DrawableGameComponent组件,那我就会徘徊?

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

编辑:以下是关于XNA社区论坛的各个讨论的link。 有什么帮助吗?

6 个答案:

答案 0 :(得分: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);
    }
}

编辑:评论中的其他一些注释:

我的实体类: 我选择将所有游戏对象包装在一个集中的Entity类中,看起来像这样:

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

这使我可以灵活地创建实体的子类(AIEntity,NonInteractiveEntity ...),它可能会覆盖Update()。它还允许我自由地子类化Drawable,而没有像AnimatedSpriteAIEntityParticleEffectNonInteractiveEntityAnimatedSpriteNoninteractiveEntity这样的n ^ 2子类的地狱。相反,我可以这样做:

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类:我有一个抽象类,从中派生出所有绘制的对象。我选择了一个抽象类,因为一些行为将被共享。如果您的代码不正确,那么将其定义为interface是完全可以接受的。

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逻辑。在你的坦克示例中,你可以做一些事情:

  • 为每个项目符号添加新实体
  • 创建一个定义List的TankEntity类,并覆盖Draw()以迭代Bullets(定义自己的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);
        }
    }
}

答案 1 :(得分:3)

MOQRhino Mocks之类的框架并不特别需要接口。他们也可以模拟任何非密封和/或抽象类。游戏是一个抽象类,所以你不应该嘲笑它: - )

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

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

答案 2 :(得分:3)

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

从Game继承并覆盖您打算在测试中使用的方法,以返回您需要的任何方法/属性的固定值或快捷方式计算。然后把假货传给你的测试。

在嘲笑框架之前,人们会推出他们自己的模拟/存根/假货 - 也许它不是那么快捷,但你仍然可以。

答案 3 :(得分: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...
}

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

答案 4 :(得分:1)

如果你不介意的话,我会小心回到你的帖子,因为mine似乎不那么活跃,你已经把你的代表放在了线上;)

当我阅读你的帖子(这里和XNAForum)时,我认为它既可以更平易近人,也可以是我的(我们的)设计并非完美无缺。

该框架可以设计为更容易扩展。我很难相信Shawn perf hit on interfaces的主要论点。支持我his colleague说可以轻松避开性能 请注意,框架已经具有IUpdatable和IDrawable接口。为什么不一直走?

另一方面,我也认为我的(和你的)设计确实没有完美无瑕。在我不依赖于Game对象的地方,我确实依赖于GraphicsDevice对象。我会看看如何绕过这个。它会让代码变得更复杂,但我认为我确实可以打破这些依赖。

答案 5 :(得分:0)

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