Які конструкції існують для компонентної системи сутності, зручної для користувачів, але все ще гнучкої?


23

Я деякий час цікавився системою сутності на базі компонентів і читав про неї незліченну кількість статей ( Ігри Insomiac , досить стандартний Evolve Your Iierarchy , 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 також робить декілька інших речей, як-от встановлення властивості "Name" на самому ігровому об'єкті замість його дочірніх компонентів.

Особисто мені не подобається ідея вашого спільного використання даних по імені. Доступ до властивостей по імені замість змінної та користувачеві також повинен знати, який тип він мені здається дійсно схильним до помилок. Що Unity робить, це те, що вони використовують аналогічні методи, як GameObject transformвластивість у Componentкласах, для доступу до компонентів братів. Отже, будь-який довільний компонент, який ви пишете, може просто зробити це:

var myPos = this.transform.position;

Для доступу до позиції. Де ця transformвласність робить щось подібне

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

Звичайно, це трохи більш багатослівно, ніж просто говорити e.position = whatever, але ти звикаєш до цього, і це виглядає не так неприємно, як родові властивості. І так, вам доведеться зробити це в обхідному напрямку для клієнтських компонентів, але ідея полягає в тому, що всі ваші загальні компоненти "двигуна" (рендери, колайдери, джерела звуку, перетворення тощо) мають прості аксесуари.


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

Крім того, з точки зору того, що "користувач також повинен знати, що це за тип", чи не міг я просто передати його властивості.GetType () методом get ()?
Качка комуніста

1
Які глобальні дані (в області сутності) ви пересуваєте до цих сховищ даних? Дані будь-яких даних про ігрові об’єкти повинні бути легко доступними через ваші "двигунові" класи (трансформація, колайдери, рендери тощо). Якщо ви вносите зміни логіки гри на основі цього "спільних даних", тоді набагато безпечніше мати один клас власника і насправді переконайтесь, що компонент знаходиться на цьому ігровому об'єкті, ніж просто сліпо просити перевірити, чи існує якась названа змінна. Так що так, вам слід впоратися з цим на стадії проектування. Ви завжди можете зробити компонент, який просто зберігає дані, якщо вони вам справді потрібні.
Тетрад

@Tetrad - Чи можете ви коротко пояснити, як буде працювати запропонований метод GetComponent <Transform>? Чи перегляне він усі компоненти GameObject і поверне перший компонент типу T? Або там щось інше відбувається?
Майкл

@Michael, який би спрацював. Ви можете просто взяти на себе відповідальність за абонент за кеш-пам'ять, що локально, як покращення продуктивності.
Тетрад

3

Я б запропонував якийсь клас інтерфейсів для ваших об'єктів 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якщо e десь не встановивPosition в інтерфейсі.

Можливо, ви також можете змусити всі ваші суб'єкти реалізовувати відповідні інтерфейси для всіх компонентів, які ви могли б коли-небудь до них додати, за допомогою методів stub, які створюють виняток. Потім, коли ви викликаєте addComponent, ви просто перенаправляєте функції сутності для інтерфейсу цього компонента на компонент. Трохи химерно, хоча.

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


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

Можливо, мені не вистачає сказаного тут, але це здається більшою мірою заміною e.GetProperty <type> ("NameOfProperty"). Що б не було з "[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 (дещо застарілі і не обов'язково відображають моє сучасне розуміння сутнісних систем, але це варто прочитати, імхо).


Дякую за це, допоможе :) Невже динамічний спосіб не матиме такого ж ефекту, як просто передавання до властивості.GetType (), однак?
Качка комуніста

У якийсь момент відбудеться трансляція, оскільки у TryGetMember є objectпараметр out - але все це буде вирішено під час виконання. Я думаю, що це прославлений спосіб зберегти вільний інтерфейс над, тобто сутністю.TryGetComponent (compName, out компонент). Я не впевнений, що зрозумів питання, однак :)
Raine

Питання в тому, що я можу використовувати динамічні типи скрізь або (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));

І його можна запитувати за назвою, типом або (якщо такі поширені, як Здоров'я та Положення) за властивостями:

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.