Какие проекты существуют для системы сущностей на основе компонентов, которые являются удобными для пользователя, но все же гибкими?


23

Некоторое время я интересовался системой сущностей, основанной на компонентах, и читал бесчисленные статьи по ней ( игры Insomiac , довольно стандартная Evolve Your Hierarchy , T-Machine , Chronoclast ... и многие другие).

Кажется, что все они имеют внешнюю структуру:

Entity e = Entity.Create();
e.AddComponent(RenderComponent, ...);
//do lots of stuff
e.GetComponent<PositionComponent>(...).SetPos(4, 5, 6);

И если вы привнесете идею общих данных (это лучший дизайн, который я когда-либо видел, с точки зрения не дублирования данных везде)

e.GetProperty<string>("Name").Value = "blah";

Да, это очень эффективно. Тем не менее, это не совсем легко читать или писать; он чувствует себя очень неуклюже и работает против вас.

Я лично хотел бы сделать что-то вроде:

e.SetPosition(4, 5, 6);
e.Name = "Blah";

Хотя, конечно, единственный способ добраться до такого дизайна - вернуться к иерархии Entity-> NPC-> Enemy-> FlyingEnemy-> FlyingEnemyWithAHatOn, которую этот дизайн пытается избежать.

Кто-нибудь видел дизайн такого рода компонентной системы, который по-прежнему гибок, но при этом поддерживает уровень удобства для пользователя? И в этом отношении, удастся ли обойти (вероятно, самая сложная проблема) хранилище данных хорошим способом?

Какие проекты существуют для системы сущностей на основе компонентов, которые являются удобными для пользователя, но все же гибкими?

Ответы:


13

Одна из вещей, которую делает Unity, - это предоставление вспомогательных средств доступа к родительскому игровому объекту для обеспечения более удобного доступа к общим компонентам.

Например, ваша позиция может храниться в компоненте Transform. Используя ваш пример, вы должны написать что-то вроде

e.GetComponent<Transform>().position = new Vector3( whatever );

Но в Unity это упрощается до

e.transform.position = ....;

Где transformбуквально просто простой вспомогательный метод в базовом GameObjectклассе ( Entityкласс в вашем случае), который делает

Transform transform
{ 
    get { return this.GetComponent<Transform>(); }
}

Unity также делает несколько других вещей, например, устанавливает свойство «Имя» для самого игрового объекта, а не в его дочерних компонентах.

Лично мне не нравится идея вашего совместного использования данных по имени. Доступ к свойствам по имени, а не по переменной и наличие пользователя также должны знать, к какому типу это относится, мне кажется, что они действительно подвержены ошибкам. Что делает Unity, так это то, что они используют похожие методы в качестве GameObject transformсвойства в Componentклассах для доступа к одноуровневым компонентам. Таким образом, любой произвольный компонент, который вы пишете, может просто сделать это:

var myPos = this.transform.position;

Чтобы получить доступ к позиции. Где это transformсвойство делает что-то вроде этого

Transform transform
{
    get { return this.gameObject.GetComponent<Transform>(); }
}

Конечно, это немного более многословно, чем просто сказать e.position = whatever, но вы привыкли к этому, и это не выглядит так противно, как общие свойства. И да, вам придется сделать это окольным путем для ваших клиентских компонентов, но идея заключается в том, что все ваши общие компоненты «движка» (средства визуализации, коллайдеры, аудиоисточники, преобразования и т. Д.) Имеют простые средства доступа.


Я понимаю, почему система совместного использования данных по имени может быть проблемой, но как еще я могу избежать проблемы, связанной с необходимостью данных для нескольких компонентов (то есть, в каком из них я что-то храню)? Просто быть очень осторожным на этапе проектирования?
Коммунистическая утка

Кроме того, с точки зрения того, что «пользователь также должен знать, какой это тип», я не могу просто привести его к свойству property.GetType () в методе get ()?
Коммунистическая утка

1
Какие глобальные (в рамках сущности) данные вы отправляете в эти общие хранилища данных? Любые данные игровых объектов должны быть легко доступны через ваши классы «движка» (трансформация, коллайдеры, рендереры и т. Д.). Если вы вносите изменения в игровую логику, основанные на этих «общих данных», то гораздо безопаснее иметь один класс и владеть им, а также убедиться, что компонент находится в этом игровом объекте, чем просто слепо спрашивать, существует ли какая-либо именованная переменная. Так что да, вы должны справиться с этим на этапе проектирования. Вы всегда можете сделать компонент, который просто хранит данные, если вам это действительно нужно.
Тетрад

@Tetrad - Не могли бы вы вкратце рассказать, как будет работать предложенный вами метод GetComponent <Transform>? Будет ли он перебирать все компоненты GameObject и возвращать первый компонент типа T? Или там что-то еще происходит?
Майкл

@ Майкл, это бы сработало. Вы можете просто взять на себя ответственность вызывающего абонента за локальное кэширование, что повысит производительность.
Тетрад

3

Я бы посоветовал какой-то класс Interface для ваших объектов Entity. Он может выполнять обработку ошибок с проверкой, чтобы убедиться, что сущность содержит компонент соответствующего значения в одном месте, поэтому вам не придется делать это везде, где вы получаете доступ к значениям. С другой стороны, большинство разработок, которые я когда-либо делал для системы на основе компонентов, я имею дело с компонентами напрямую, запрашивая, например, их позиционный компонент, а затем напрямую / обновляя свойства этого компонента.

Очень базовый на высоком уровне, класс принимает рассматриваемый объект и предоставляет простой в использовании интерфейс с нижележащими компонентами следующим образом:

public class EntityInterface
{
    private Entity entity { get; set };
    public EntityInterface(Entity e)
    {
        entity = e;
    }

    public Vector3 Position
    {
        get
        {
            return entity.GetProperty<Vector3>("Position");
        }
        set
        {
            entity.GetProperty<Vector3>("Position") = value;
        }
    }
}

Если я предполагаю, что мне просто нужно обработать NoPositionComponentException или что-то еще, в чем преимущество этого по сравнению с простым помещением всего этого в класс Entity?
Коммунистическая утка

Не зная, что на самом деле содержит ваш класс сущностей, я не могу сказать, есть ли преимущество или нет. Я лично выступаю за компонентные системы, в которых нет базовой сущности, просто чтобы избежать вопроса «Ну, что в ней принадлежит». Однако я бы посчитал такой интерфейсный класс хорошим аргументом в пользу его существования.
Джеймс

Я вижу, как это проще (мне не нужно запрашивать позицию через GetProperty), но я не вижу преимущества этого способа вместо размещения его в самом классе Entity.
Коммунистическая утка

2

В Python вы можете перехватить часть setPosition e.SetPosition(4,5,6)через объявление __getattr__функции в Entity. Эта функция может выполнять итерацию по компонентам, находить соответствующий метод или свойство и возвращать его, так что вызов функции или ее назначение направляются в нужное место. Возможно, в C # есть похожая система, но это может быть невозможно из-за статической типизации - она, вероятно, не может быть скомпилирована, e.setPositionесли в интерфейсе не установлено setPosition.

Возможно, вы также можете заставить все ваши сущности реализовывать соответствующие интерфейсы для всех компонентов, которые вы когда-либо добавляете к ним, с помощью методов-заглушек, которые вызывают исключение. Затем, когда вы вызываете addComponent, вы просто перенаправляете функции объекта для интерфейса этого компонента на компонент. Хотя немного неуверенно.

Но , пожалуй , проще всего было бы перегрузить оператор] [на классе Entity искать присоединенные компоненты на наличие действительного имущества и возврата , что, так что он может быть назначен следующим образом: e["Name"] = "blah". Каждый компонент должен реализовать свое собственное GetPropertyByName, и Entity вызывает каждого из них по очереди, пока не найдет компонент, отвечающий за рассматриваемое свойство.


Я думал об использовании индексаторов .. это действительно хорошо. Я подожду немного, чтобы увидеть, что еще появится, но я склоняюсь к смеси этого, обычного GetComponent () и некоторых свойств, таких как @James, предлагаемых для общих компонентов.
Коммунистическая утка

Возможно, я упускаю то, что говорится здесь, но это кажется больше заменой для e.GetProperty <type> ("NameOfProperty"). Что бы с e ["NameOfProperty"].
Джеймс

@James Это обертка для него (очень похоже на ваш дизайн) .. за исключением того, что он более динамичен - он делает это автоматически и чище, чем метод .. GetPropertyЕдинственным недостатком является манипулирование строками и сравнение. Но это спорный вопрос.
Коммунистическая утка

Ах, мое недоразумение там. Я предполагал, что GetProperty является частью объекта (и будет делать то, что было описано выше), а не методом C #.
Джеймс

2

Чтобы расширить ответ Kylotan, если вы используете C # 4.0, вы можете статически ввести переменную, которая будет динамической .

Если вы наследуете от System.Dynamic.DynamicObject, вы можете переопределить TryGetMember и TrySetMember (среди множества виртуальных методов), чтобы перехватывать имена компонентов и возвращать запрошенный компонент.

dynamic entity = EntityRegistry.Entities[1000];
entity.MyComponent.MyValue = 100;

Я написал немного о системах сущностей за эти годы, но я предполагаю, что не могу конкурировать с гигантами. Некоторые примечания ES (несколько устаревшие и не обязательно отражающие мое текущее понимание систем сущностей, но это стоит прочитать, imho).


Спасибо за это, поможет :) Разве динамический путь не будет иметь такой же эффект, как просто приведение к свойству. Однако GetType ()?
Коммунистическая утка

В какой-то момент произойдет приведение, поскольку у TryGetMember есть выходной objectпараметр, но все это будет разрешено во время выполнения. Я думаю, что это прославленный способ сохранить свободный интерфейс над то есть, entity.TryGetComponent (compName, out компонент). Однако я не уверен, что понял вопрос :)
Рейн

Вопрос в том, что я мог бы везде использовать динамические типы или (AFAICS) возвращать свойство (property.GetType ()).
Коммунистическая утка

1

Я вижу большой интерес здесь в ES на C #. Проверьте мой порт отличной реализации ES Artemis:

https://github.com/thelinuxlich/artemis_CSharp

Кроме того, пример игры, которая поможет вам начать работу (с использованием XNA 4): https://github.com/thelinuxlich/starwarrior_CSharp

Предложения приветствуются!


Конечно, выглядит красиво. Лично я не являюсь поклонником идеи столь многих EntityProcessingSystems - в коде, как это работает с точки зрения использования?
Коммунистическая утка

Каждая Система - это Аспект, который обрабатывает свои зависимые Компоненты. Посмотрите это, чтобы получить представление: github.com/thelinuxlich/starwarrior_CSharp/tree/master/…
thelinuxlich

0

Я решил использовать отличную систему отражения C #. Я основал это на общей системе компонентов, плюс смесь ответов здесь.

Компоненты добавляются очень легко:

Entity e = new Entity(); //No templates/archetypes yet.
e.AddComponent(new HealthComponent(100));

И может быть запрошен либо по имени, типу или (если общие, такие как Health и Position) по свойствам:

e["Health"] //This, unfortunately, is a bit slower since it requires a dict search
e.GetComponent<HealthComponent>()
e.Health

И удалил:

e.RemoveComponent<HealthComponent>();

К данным, опять же, обычно обращаются через свойства:

e.MaxHP

Который хранится на стороне компонента; меньше накладных расходов, если у него нет HP:

public int? MaxHP
{
get
{

    return this.Health.MaxHP; //Error handling returns null if health isn't here. Excluded for clarity
}
set
{
this.Health.MaxHP = value;
}

Intellisense в VS помогает набирать текст в больших количествах, но по большей части печатать мало - то, что я искал.

За кулисами я храню Dictionary<Type, Component>и Componentsпереопределяю ToStringметод проверки имени. В моем оригинальном дизайне в основном использовались строки для имен и вещей, но с ним стало работать свинья (о, смотри, мне приходится использовать повсюду, а также отсутствие доступных для доступа свойств).

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.