Совместное использование полей в компонентном игровом дизайне - PullRequest
3 голосов
/ 16 июля 2011

Я нахожусь на том, что я считаю последним большим логическим прыжком перед тем, как завершить свой компонентный игровой движок в C # с использованием XNA. У меня есть класс 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; }

однако я не могу передать данные конструктору Attribute, используя ref. Я не могу сделать общий атрибут, так как он не будет включен в список. Мне просто интересно, есть ли способ обрабатывать тип значения, а также данные ссылочного типа, или я делаю логическую ошибку где-то во всей этой вещи.

1 Ответ

10 голосов
/ 16 июля 2011

Snarky версия: Поздравляем, вы заново изобрели переменную.Плохо.Или, в лучшем случае, свойство интерфейса.

Полезная версия:

Я вижу несколько проблем с вашим дизайном.

Первыйпроблема в том, что это сложный .Осложнения лучше избегать, если у вас нет убедительной и существующей причины для этого (то есть: нет необходимости «возможно в будущем»).В противном случае ЯГНИ .Вы должны всегда пытаться выразить понятия непосредственно в коде, прежде чем прибегать к созданию систем для выражения этих понятий в данных (например, то, что я сказал о переизобретении переменной; также рассмотрим this ).

Но давайтепредположим, что у вас есть веская причина для разработки на основе компонентов ...

Второй вопрос - boxing .Упаковка происходит везде, где вы сохраняете тип значения (например: int, float, Vector2, любой struct) непосредственно в качестве ссылочного типа (например: object, IEquatable).Объекты в штучной упаковке являются неизменными - поэтому каждый раз, когда ваша позиция меняется, создается новый объект в штучной упаковке.Бокс переменной является (относительно) медленным.Объекты в штучной упаковке хранятся в куче - поэтому они учитываются во время и могут вызывать сборку мусора.Таким образом, дизайн, который вы предлагаете в своем вопросе, будет выполнять ужасно .

Я предполагаю, что ваша идея для дизайна на основе компонентов похожа на идею, описанную в этой статье ,Вот полезная диаграмма:

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

И это подводит меня к третьей проблеме: у вас не должно быть более одного компонента, который содержитпозиция в любом случае!(Похоже, в вашем дизайне вы собираетесь на более более гранулированный, чем вам нужно.)

По сути, компонентный дизайн - это переизобретение class, а не переменная нормальном дизайне вы можете использовать функцию «Визуализация», например:

public void Draw()
{
    spriteBatch.Draw(texture, this.Position, Color.White);
}

Но в компонентном дизайне вы будете иметь Draw и Position в разные классы .Который, между прочим, у меня был бы реализовывать интерфейсы как:

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);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...