在基于组件的游戏设计中共享字段

时间:2011-07-16 13:37:46

标签: c# xna

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

1 个答案:

答案 0 :(得分:9)

Snarky版本:恭喜你重新发明了变量。厉害。或者,最好是接口上的属性。

有用版本:

我可以看到您的设计存在一些问题。

第一个问题就是复杂。最好避免复杂化,除非你有一个令人信服的存在的原因(即:不是“可能在将来”需要)。否则YAGNI。你应该总是尝试直接在代码中表达概念,然后再创建系统来在数据中表达这些概念(就像我所说的关于重新发明变量的内容;还要考虑this)。

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

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

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

http://cowboyprogramming.com/images/eyh/Fig-2.gif

这让我想到了第三个问题: 不应该拥有多个持有职位的组件! (在您的设计中,您似乎比您需要的更精细方式。)

基本上,基于组件的设计是重新发明class,而不是变量。在普通设计中,您可能具有“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 的方法(如果你要重新发明类,这个可能是你需要包含的最重要的概念)。

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

我会使每个组件类继承自Component类,其属性为Self。我会让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);