【问题标题】:Sharing Fields in Component-Based Game Design基于组件的游戏设计中的共享字段
【发布时间】:2011-07-16 13:37:46
【问题描述】:

在使用 XNA 完成基于 C# 的基于组件的游戏引擎之前,我认为这是最后一次重大的逻辑飞跃。我定义了我的实体类和抽象组件。我的问题出现在我的 EntityFactory 中。

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

我可以通过初始化实体工厂中的所有字段并要求将它们传递到组件的构造函数中来做到这一点(并使用 ref 作为原语),但这将非常难以维护,因为任何时候我扩展或更改组件,我将不得不在工厂中使用该组件的每个地方重写代码。我很想避免这种解决方案,但如果我找不到更好的方法,我会忍受它。

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

private AttributeType type;
private Object data;

属性类型是一个枚举,表示属性的用途。所以Position、Rotation、Texture等的枚举中有条目。

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 通用,因为它不会进入列表。我只是想知道是否有一种方法可以处理值类型和引用类型数据,或者我在整个事情的某个地方犯了一个逻辑错误。

【问题讨论】:

    标签: c# xna


    【解决方案1】:

    Snarky 版本: 恭喜您重新发明了变量。很糟糕。或者,充其量是接口上的一个属性。

    有用的版本:

    我发现您的设计存在一些问题。

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

    但是让我们假设您确实有充分的理由进行基于组件的设计...

    第二个问题是拳击。装箱发生在您将值类型(例如:intfloatVector2、任何struct)直接存储为引用类型(例如:objectIEquatable)的任何地方。盒装对象是不可变的 - 因此每次您的位置发生变化时,都会创建一个新的盒装对象。对变量进行装箱(相对)很慢。装箱的对象存储在堆上 - 因此在垃圾收集期间会考虑它们并且可能导致垃圾收集。因此,您在问题中提出的设计将执行可怕

    我假设您对基于组件的设计的想法类似于the one explained in this article。这是一个有用的图表:


    (来源:cowboyprogramming.com

    这让我想到了第三个问题:无论如何,您不应该拥有一个以上的组件! (在您的设计中,您的方式似乎比您需要的更精细。)

    基本上,基于组件的设计是关于重新发明class,而不是变量。在普通设计中,您可能有这样的“渲染”功能:

    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 可能是您需要包含的最重要的概念)。 p>

    你会怎么做?这是一个粗略的设计思路:

    我会让每个组件类都继承自具有属性SelfComponent 类。我会让Self 返回某种ComposedObject,并使用一种机制来通过接口访问构成组合对象的任何其他组件。所以也许你的渲染组件可能看起来像:

    class SimpleRenderer : Component, IRenderComponent
    {
        public void Draw()
        {
            sb.Draw(texture, Self.Get<IPositionComponent>().Position, Color.White);
        }
    }
    

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

    现在,如果这对您来说过于冗长,也许您可​​以在 ComposedObject 上添加一些便利属性(或使用扩展方法),用于常见数据,例如 Position,如下所示:

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

    那么你的绘图函数可以是这样的:

    spriteBatch.Draw(texture, Self.Position, Color.White);
    

    【讨论】:

    • 至于装箱问题,我知道将所有字段包装在另一个类中会产生相当大的开销。这就是为什么我试图让该字段引用 Attribute 中的数据。创建实体后,不会引用任何属性或 List,所以我认为额外的层将被垃圾收集,只留下下面的数据。我觉得你对接口的建议会导致我试图避免的组件实现之间的耦合,但也许我正在寻找的分离是不可能的。
    • @Zak:我不确定我是否完全理解你关于拳击的想法——但我没有看到任何可以减少所需拳击数量的东西。就耦合而言——你必须有一些级别的耦合——例如:要在你必须有一个位置的位置绘制一些东西。我给SimpleRendererIPositionComponent 的设计(在代码和组合对象定义中)。它避免了糟糕的耦合——SimpleRenderer 可能依赖于特定的位置提供者,例如 StaticPositionPhysicsPosition
    • 我的想法是这些属性将在实体创建期间生成,但组件中的所有字段都将引用实际数据,而不是这些属性之一。这样,当 Entity 完全构造时,不会有对任何属性或属性列表的任何引用,因此包装类将被垃圾收集,而将它们包含的数据留在后面。就耦合而言,我总是将组件视为动作,但您似乎也包括状态和数据。我只需要考虑一下。
    • 我误解了你的设计吗?因为在我看来您打算存储对值类型的引用(例如:Vector2)。但你不能那样做。值类型总是被复制的,当它们被装箱到引用类型中时,它们是不可变的。对可变值类型进行“引用”的唯一方法是,如果它是 class 的成员(比如你的 Attribute,或者有点像我的 IPositionComponent),并且你总是通过该类访问它。 (ref 参数是一个完全不同的故事。)
    • 啊,好吧。我现在知道了。是的,这是我的错误,认为我可以创建对值类型的引用。我试图避免保留包含类的开销,但也许没有一种简单的方法可以避免它。感谢您的帮助。
    猜你喜欢
    • 2010-12-26
    • 2017-07-27
    • 2011-04-26
    • 1970-01-01
    • 2020-05-31
    • 2012-01-14
    • 2011-11-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多