Xna: 模拟 Texture2D

4

我在编写WinForms / Xna应用程序时,需要一种抽象方式来处理Controller / Model代码中与GraphicsDevice的交互。

我创建了一个名为IGraphicsService的接口。我将使用它来抽象加载纹理等内容。但我无法确定当我需要返回某种纹理信息时该怎么办。我应该创建一个包装Texture2D的类吗?我担心这会带来不必要的开销。最终,我想能够创建某种MockTexture2D。

这一切都是为了使我的应用程序更具可测试性。我不太关心速度,但如果有一种解决方案不会产生太多开销,那就太好了,因为我最终想使用它来使我的游戏更具可测试性。有什么建议吗?

4个回答

3

我个人认为GraphicsDevice类过于复杂,难以进行模拟,任何人都需要更多的工作来指导模拟执行正确的测试(尽管他应该得到勋章:D)。

Visual Studio的“提取接口”重构只能完成部分工作。也许代码生成器可以创建一个完全转发的模拟。如果您担心性能,只要您的模拟接口与真实的图形设备相匹配,您就可以通过一些魔术#if..#endif在发布版本中使用真实的GraphicsDevice,在调试版本中通过接口使用它?

-

无论如何,对于我的单元测试,我实际上是在一个隐藏的表单上创建了一个真正的图形设备用于我的单元测试。这效果出奇地好,运行我的单元测试的构建代理在Windows XP上运行在VMware虚拟机中,在其中Gentoo Linux x64作为主机操作系统。我正在测试具有着色器和渲染目标等实际渲染代码。就性能而言,我也不会抱怨-1300个测试在不到10秒钟内执行。

这是我用于创建伪模拟图形设备服务的代码:MockedGraphicsDeviceService.cs和它的单元测试:MockedGraphicsDeviceService.Test.cs

当然,缺点是您无法检查这种伪模拟图形设备中的任何期望(例如,是否发生了具有234个宽度和456个高度的CreateTexture()调用?)。需要一些聪明的类设计来使逻辑隔离到足以测试我的图形类而不会破坏其抽象。至少我可以以这种方式获得图形代码的测试覆盖率 :)


你的解决方案实际上很有趣,但它比我实际上想要测试的要多一点。我有一个TileMap和一个tile map需要一个TileSheet。而TileSheet又需要一个纹理。我正在寻找一些方法来打破这种依赖关系。不过我现在认为我已经解决了这个问题。我创建了IGraphicsService和ITextureResource接口,用于抽象加载纹理和纹理信息。我认为为纹理创建一个简单的包装类不会对性能造成太大影响,尽管我没有数据来支持这一点,所以我可能是错的。 - smack0007
在单元测试中创建一个真正的、裸露的图形设备是一个很好的想法。 - Adam Kane
很遗憾,这些链接现在不再可用。 - jmattheis

1
你可以尝试使用Scurvy.Test来编写单元测试。这样,你就不需要模拟图形设备,只需使用实际的实例即可 :-)

http://scurvytest.codeplex.com

免责声明:本人为该库的作者


在这个答案上放一个免责声明可能是个好主意,Joel :) - MattDavey
免责声明是关于我编写的测试框架吗?如果你认为有必要,那当然可以加上...但无论是否有人使用它,我都不会从中获得任何利益;-) - Joel Martinez
1
是的,我可能有点过于追求完美了,但是谨慎起见还是好一些...只是想让 Stack Overflow 成为一个更好的地方! :) http://stackoverflow.com/faq#promotion - MattDavey

0

我也曾在单元测试中尝试使用虚拟纹理时遇到了这个问题。以下是我的解决方法:

internal class SneakyTexture2D : Texture2D
{
    private static readonly object Lockobj = new object();
    private static volatile Texture2D instance;

    private SneakyTexture2D()
        : this(() => throw new Exception())
    {
    }

    private SneakyTexture2D(Func<GraphicsDevice> func)
        : this(func())
    {
    }

    // Is never called
    private SneakyTexture2D(GraphicsDevice g)
        : base(g, 0, 0)
    {
    }

    // INTENTIONAL MEMORY LEAK AHOY!!!
    ~SneakyTexture2D()
    {
        instance = this;
    }


    // This is the actual "constructor"
    public static Texture2D CreateNamed(string name)
    {
        lock (Lockobj)
        {
            Texture2D local;
            instance = null;
            while ((local = instance) == null)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();

                GC.WaitForFullGCComplete();
                try
                {
                    DoNotUseMe = new SneakyTexture2D();
                }
                catch
                {
                }
            }

            local.Name = name;
            return local;
        }
    }
}

SneakyTexture2D类继承自Texture2D,但它成功地避免了调用/初始化基对象构造函数,因此不需要Graphicsdevice。

我使用纹理的Name属性有点冒险。通常与未初始化的对象交互有点危险。

非常感谢Jon Skeet在这个主题上的杰出文章:https://codeblog.jonskeet.uk/2014/10/23/violating-the-smart-enum-pattern-in-c/


0

我知道这可能有点超出使用范围,但也许你可以摆脱对纹理的需求。我认为测试纹理本身并不需要它,而是需要测试它所包含的数据。

因此,例如,如果您有这个方法在处理您的纹理:

string output = ConvertTextureToSomeTextualInfo(yourTexture);

...

public static string ConvertTextureToSomeTextualInfo(Texture2D texture) {
    // your magic
}

不要发送纹理,而是发送数据:

Color[] texData = new Color[yourTexture.width * yourTexture.height];
yourTexture.GetData(texData);

string output = ConvertTextureToSomeTextualInfo(texData);

public static string ConvertTextureToSomeTextualInfo(Color[] textureData) {
    // still, your magic
}

在这种情况下,这可能并不容易,但我非常确定可以将代码抽象化更多,尽可能地将逻辑与UI分离,并仅让一个小组件将它们连接在一起,同时舒适地测试逻辑本身。


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