组件化游戏设计中的共享字段

3

我相信我已经完成了使用XNA的C#组件化游戏引擎中的最后一个大的逻辑跳跃。我已经定义了Entity类和抽象组件。我的问题出现在EntityFactory中。

当我想要创建一个新实体时,我将EntityType枚举传递给工厂中的静态方法,并通过switch/case语句找到需要组合的组件。问题是,我正在尝试创建一种方法,使得组件可以共享与同一实体中的其他组件相同的字段,而不必访问所有内容。例如,如果两个组件具有表示位置的Vector2字段,则它们应该都指向同一个Vector2。

我可以通过在实体工厂中初始化所有字段并要求将其传递到组件的构造函数中(并使用原始类型的ref),来实现这一点。但是,这将非常难以维护,因为每次我扩展或更改组件时,我都必须在使用该组件的每个地方重新编写工厂中的代码。我真的想避免这种解决方案,但如果我找不到更好的方法,我将忍受它。

我目前的解决方案是创建一个名为Attribute的包装器类。它包含两个字段:

private AttributeType type;
private Object data;

属性类型是一个枚举,表示属性的目的。因此,在枚举中有位置、旋转、纹理等条目。

EntityFactory创建一个空属性列表,并将其传递给每个组件构造函数。setField方法将由组件的构造函数调用,而不是初始化字段。这是Attribute类和setField方法:

 public class Attribute
{
    private AttributeType type;
    private Object data;

    public AttributeType Type
    {
        get { return this.type; }
    }
    public Object Data
    {
        get { return this.data; }
    }

    public Attribute(AttributeType type, Object data)
    {
        this.type = type;
        this.data = data;
    }

    public static void setField<T>(List<Attribute> attributeList, AttributeType type, out T field, T defaultValue)
    {
        bool attributeFound = false;
        field = defaultValue;

        foreach (Attribute attribute in attributeList)
        {
            if (attribute.Type == type)
            {
                field = (T)attribute.Data;
                attributeFound = true;
                break;
            }
        }

        if (!attributeFound)
        {
            attributeList.Add(new Attribute(type, field));
        }
    }
}

我的问题是当属性包含原始数据类型的数据时。我考虑在Attribute类中编写一个方法。

public void getData<T>(out T field) { field = this.data; }

然而,我似乎无法使用ref将数据传递给Attribute构造函数。我无法使Attribute成为通用类型,因为它不能进入列表中。我只是想知道是否有一种方法来处理值类型和引用类型的数据,或者在整个过程中我是否犯了逻辑错误。

1个回答

10

嘲讽版:恭喜您重新发明了变量。糟糕的是,或者说最好的情况下,它只是接口上的属性。

有用版:

我看到你的设计有几个问题。

第一个问题就是它太复杂了。除非你有一个令人信服和已经存在的理由(即不是“将来可能”需要),否则应该尽量避免复杂化。否则就是YAGNI。在诉诸于创建表达这些概念的系统之前,你应该始终尝试直接在代码中表达概念(就像我所说的重新发明变量;还要考虑this)。

但是假设你确实有一个组件化设计的好理由……

第二个问题是装箱。无论何时你将值类型(例如intfloatVector2、任何struct)直接存储为引用类型(例如objectIEquatable),都会发生装箱。装箱对象是不可变的——因此每次位置改变时,都会创建一个新的装箱对象。变量的装箱是(相对)缓慢的。装箱对象存储在堆上——因此它们会在垃圾回收期间被考虑,并可能引起垃圾回收。因此,你在问题中提出的设计将表现得极差

我假设你对基于组件的设计有一些想法,类似于这篇文章中所解释的。下面是一个有用的图表:

(来源: cowboyprogramming.com) 这也带来了第三个问题: 无论如何,你不应该有多个持有位置的组件!(在你的设计中,你似乎比需要更加细致。)
基本上,基于组件的设计是关于重新发明class,而不是variable。在normal的设计中,你可能会有一个像这样的"Render"函数:
public void Draw()
{
    spriteBatch.Draw(texture, this.Position, Color.White);
}

但在组件化设计中,您将在不同的类中拥有DrawPosition。顺便说一下,我会像这样实现接口:

interface IRenderComponent { void Draw(); }
interface IPositionComponent { Vector2 Position { get; set; } }

所以,Draw 如何访问 Position?嗯,你需要一种表达this的方法(如果你要重新发明类,this 可能是你需要包含的最重要的概念)。
你会如何做到这一点?这是一个粗略的设计想法:
我会让每个组件类继承一个带有属性 SelfComponent 类。我会让 Self 返回某种带有通过接口访问组成复合对象的其他组件的机制的 ComposedObject。因此,你的渲染组件可能看起来像:
class SimpleRenderer : Component, IRenderComponent
{
    public void Draw()
    {
        sb.Draw(texture, Self.Get<IPositionComponent>().Position, Color.White);
    }
}

这类似于GameServiceContainer(即Game.Services属性)的工作方式。这里的想法是,没有一个ComposedObject应该具有每个接口的多个实例。如果您的接口数量很少,ComposedObject甚至不需要使用列表-只需直接存储每个接口。但您可以有实现多个接口的组件。

现在,如果这对您来说太冗长了,也许您可以在ComposedObject上添加一些方便的属性(或使用扩展方法)来表示常见的数据块,例如像Position

public Vector2 Position { get { return Get<IPositionComponent>().Position; } }

那么你的绘图函数可以简单地写成这样:
spriteBatch.Draw(texture, Self.Position, Color.White);

就装箱问题而言,我知道把所有字段都包装在另一个类中会带来相当大的开销。这就是为什么我尝试让字段引用 Attribute 中的数据。一旦 Entity 被创建,将不再引用任何 Attribute 或 List<Attribute>,因此我认为额外的层次将被垃圾回收,只留下下面的数据。我觉得你提出的接口建议会导致组件实现之间的耦合,而我正试图避免这种情况,但也许我正在寻找的分离可能并不存在。 - Godric Seer
@Zak:我不确定我完全理解你关于装箱的想法——但我没有看到任何可以减少所需装箱数量的东西。至于耦合,你必须有一定程度的耦合——例如:要在某个位置绘制某物,你必须有一个位置*。我给出的设计将SimpleRendererIPositionComponent(在代码和组合对象定义中)耦合起来。它避免了糟糕的耦合方式——例如SimpleRenderer可能依赖于特定的位置提供者,如StaticPositionPhysicsPosition - Andrew Russell
我的想法是这些属性将在实体创建期间进行设置,但组件中的所有字段都将是对实际数据的引用,而不是对这些属性之一的引用。这样,当实体完全构建时,就不会有任何属性或属性列表的引用,因此包装类将被垃圾回收,留下它们所包含的数据。至于耦合性,我总是认为组件是动作,但你似乎也把状态和数据包括进来了。我需要再考虑一下。 - Godric Seer
我是否误解了你的设计?因为在我听来,你打算存储值类型(例如:Vector2)的引用。但是这是不可能的。值类型总是被复制的,并且当它们被装箱成引用类型时,它们是不可变的。唯一的方法是拥有一个对可变值类型的“引用”,就是它是一个 class 的成员(就像你的 Attribute,或者类似于我的 IPositionComponent),并且你总是通过那个类访问它。(ref 参数则完全不同。) - Andrew Russell
啊,好的。我现在明白了。是的,那是我的错误,我以为我可以创建对值类型的引用。我试图避免保留包含类的开销,但也许没有简单的方法可以避免它。谢谢你的帮助。 - Godric Seer

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