Використовуючи структуру системи на основі компонентів практично


59

Вчора я прочитав презентацію від GDC Canada про систему об'єктів Attribute / Behavior, і думаю, що це досить чудово. Однак я не впевнений, як це використовувати практично, а не лише теоретично. Перш за все, я швидко поясню вам, як працює ця система.


Кожна ігрова сутність (ігровий об’єкт) складається з атрибутів (= даних, до яких можна отримати поведінку, але також і «зовнішнього коду») та поведінки (= логіка, яка містить OnUpdate()і OnMessage()). Так, наприклад, у клоні прориву кожна цегла складається з (приклад!): PositionAttribute , ColorAttribute , HealthAttribute , RenderableBehaviour , HitBehaviour . Останній може виглядати так (це просто неробочий приклад, написаний на C #):

void OnMessage(Message m)
{
    if (m is CollisionMessage) // CollisionMessage is inherited from Message
    {
        Entity otherEntity = m.CollidedWith; // Entity CollisionMessage.CollidedWith
        if (otherEntity.Type = EntityType.Ball) // Collided with ball
        {
            int brickHealth = GetAttribute<int>(Attribute.Health); // owner's attribute
            brickHealth -= otherEntity.GetAttribute<int>(Attribute.DamageImpact);
            SetAttribute<int>(Attribute.Health, brickHealth); // owner's attribute

            // If health is <= 0, "destroy" the brick
            if (brickHealth <= 0)
                SetAttribute<bool>(Attribute.Alive, false);
        }
    }
    else if (m is AttributeChangedMessage) // Some attribute has been changed 'externally'
    {
        if (m.Attribute == Attribute.Health)
        {
            // If health is <= 0, "destroy" the brick
            if (brickHealth <= 0)
                SetAttribute<bool>(Attribute.Alive, false);
        }
    }
}

Якщо вас зацікавила ця система, ви можете прочитати більше тут (.ppt).


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

Отже, що я хочу запитати? Як проектувати поведінку (компоненти). Я читав тут, на GameDev SE, що найпоширеніша помилка - це зробити багато компонентів і просто "зробити все складовою". Я читав, що пропонується не робити візуалізацію в компоненті, а робити це поза нею (тому замість RenderableBehaviour , можливо, це має бути RenderableAttribute , а якщо суб'єкт господарювання має RenderableAttribute встановлений на true, то Renderer(клас не пов'язаний з компоненти, але до самого двигуна) слід намалювати його на екрані?).

Але як щодо поведінки / компонентів? Скажімо, що у мене рівень, і в рівні є Entity button, Entity doorsі Entity player. Коли гравець стикається з кнопкою (це кнопка підлоги, яка перемикається тиском), її натискають. Коли кнопка натискається, вона відкриває двері. Ну, а тепер як це зробити?

Я придумав щось подібне: у гравця є CollisionBehaviour , який перевіряє, чи гравець стикається з чимось. Якщо він стикається з кнопкою, він посилає CollisionMessageдо buttonлиця. Повідомлення міститиме всю необхідну інформацію: хто зіткнувся з кнопкою. Кнопка отримала ToggleableBehaviour , який отримає CollisionMessage. Він перевірить, з ким це зіткнулося, і якщо вага цієї сутності досить велика, щоб перемикати кнопку, кнопка перемикається. Тепер він встановлює значення ToggledAttribute для значення true. Добре, але що тепер?

Чи повинна кнопка надіслати ще одне повідомлення всім іншим об’єктам, щоб сказати їм, що воно перемкнулося? Я думаю, що якби я зробив все так, я мав би тисячі повідомлень, і це стане досить безладним. Тому, можливо, це краще: двері постійно перевіряють, натиснута чи ні кнопка, пов’язана з ними, і відповідно змінює її OpenedAttribute . Але це означає, що OnUpdate()метод дверей буде постійно щось робити (чи це справді проблема?).

І друга проблема: що робити, якщо у мене більше видів кнопок. Одного натискають на тиск, другого перемикають, стріляючи по ньому, третього вмикають, якщо на нього наливають воду тощо. Це означає, що мені доведеться мати різні поведінки, приблизно так:

Behaviour -> ToggleableBehaviour -> ToggleOnPressureBehaviour
                                 -> ToggleOnShotBehaviour
                                 -> ToggleOnWaterBehaviour

Це як справжні ігри, чи я просто дурний? Можливо, я міг би мати лише один ToggleableBehaviour, і він буде вести себе відповідно до ButtonTypeAttribute . Отже, якщо це a ButtonType.Pressure, він робить це, якщо це a ButtonType.Shot, він робить щось інше ...

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

Відповіді:


46

Компоненти чудові, але може знадобитися певний час, щоб знайти рішення, яке вам добре почуватиметься. Не хвилюйтесь, ви туди потрапите. :)

Організація компонентів

Ви майже на правильному шляху, я б сказав. Я спробую описати рішення в зворотному порядку, починаючи з дверей і закінчуючи вимикачами. Моя реалізація широко використовує події; нижче я описую, як ви можете використовувати події ефективніше, щоб вони не стали проблемою.

Якщо у вас є механізм з'єднання об'єктів між ними, я б змусив перемикач безпосередньо повідомити двері, що вона була натиснута, і тоді двері можуть вирішити, що робити.

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

Якщо у вас є більш ніж один тип комутатора, я б PressureToggle, WaterToggleі через ShotToggleповедінку теж, але я не впевнений , що база ToggleableBehaviourнічого хорошого, так що я б покінчити з цим (якщо, звичайно, у вас є хороший причина його збереження).

Behaviour -> ToggleOnPressureBehaviour
          -> ToggleOnShotBehaviour
          -> ToggleOnWaterBehaviour

Ефективна робота з подіями

Що стосується занепокоєння, що навколо нас літає занадто багато подій, ви можете зробити одне. Замість того, щоб кожен компонент був сповіщений про кожну подію, яка відбулася, тоді перевірте компонент, чи це правильний тип події, ось інший механізм ...

Ви можете мати EventDispatcherз subscribeметодом , який виглядає приблизно так (псевдокод):

EventDispatcher.subscribe(event_type, function)

Потім, коли ви публікуєте подію, диспетчер перевіряє її тип і повідомляє лише ті функції, які підписалися на конкретний тип події. Ви можете реалізувати це як карту, яка пов'язує типи подій зі списками функцій.

Таким чином, система є значно ефективнішою: є набагато менше викликів функцій на подію, і компоненти можуть бути впевнені, що вони отримали правильний тип події, і не потрібно подвоювати перевірку.

Я просто опублікував просту реалізацію цього часу на StackOverflow. Це написано в Python, але, можливо, це може допомогти вам:
https://stackoverflow.com/a/7294148/627005

Ця реалізація є досить загальною: вона працює з будь-яким типом функцій, а не лише з компонентів. Якщо вам це не потрібно, замість цього у functionвас може бути behaviorпараметр у вашому subscribeметоді - екземпляр поведінки, про який потрібно повідомити.

Атрибути і поведінка

Я прийшов використовувати атрибути і поведінку сам , а не прості старі компоненти. Однак, з вашого опису того, як ви використовували систему в грі Breakout, я думаю, ви перестараєтеся.

Я використовую атрибути лише тоді, коли дві поведінки потребують доступу до одних і тих же даних. Атрибут допомагає тримати поведінку окремо, а залежності між компонентами (будь то атрибути чи поведінки) не заплутуються, оскільки вони дотримуються дуже простих і чітких правил:

  • Атрибути не використовують жодних інших компонентів (ні інших атрибутів, ні поведінки), вони самодостатні.

  • Поведінки не використовують і не знають про іншу поведінку. Вони знають лише про деякі атрибути (ті, які їм суворо потрібні).

Коли деякі дані потрібні лише одній і лише одній з форм поведінки, я не бачу причини вносити це в атрибут, я дозволяю поведінці зберігати його.


коментар @ heishe

Чи не виникне ця проблема і з нормальними компонентами?

У всякому разі, я не повинен перевірити типи подій , тому що кожна функція обов'язково отримати правильний тип події, завжди .

Також залежності поведінки (тобто атрибути, які їм потрібні) вирішуються під час побудови, тому вам не доведеться шукати атрибути щоразу при кожному оновленні.

І нарешті, я використовую Python для свого логічного коду гри (двигун є в С ++), тому немає необхідності в кастингу. Python робить свою штучку, і все працює добре. Але навіть якби я не використовував мову з набором качок, я б це зробив (спрощений приклад):

class SomeBehavior
{
  public:
    SomeBehavior(std::map<std::string, Attribute*> attribs, EventDispatcher* events)
        // For the purposes of this example, I'll assume that the attributes I
        // receive are the right ones. 
        : health_(static_cast<HealthAttribute*>(attribs["health"])),
          armor_(static_cast<ArmorAttribute*>(attribs["armor"]))
    {
        // Boost's polymorphic_downcast would probably be more secure than
        // a static_cast here, but nonetheless...
        // Also, I'd probably use some smart pointers instead of plain
        // old C pointers for the attributes.

        // This is how I'd subscribe a function to a certain type of event.
        // The dispatcher returns a `Subscription` object; the subscription 
        // is alive for as long this object is alive.
        subscription_ = events->subscribe(event::type<DamageEvent>(),
            std::bind(&SomeBehavior::onDamageEvent, this, _1));
    }

    void onDamageEvent(std::shared_ptr<Event> e)
    {
        DamageEvent* damage = boost::polymorphic_downcast<DamageEvent*>(e.get());
        // Simplistic and incorrect formula: health = health - damage + armor
        health_->value(health_->value() - damage->amount() + armor_->protection());
    }

    void update(boost::chrono::duration timePassed)
    {
        // Behaviors also have an `update` function, just like
        // traditional components.
    }

  private:
    HealthAttribute* health_;
    ArmorAttribute* armor_;
    EventDispatcher::Subscription subscription_;
};

На відміну від поведінки, атрибути не виконують жодної updateфункції - їм не потрібно, їх мета - зберігати дані, а не виконувати складну ігрову логіку.

Ви все ще можете змусити ваші атрибути виконувати просту логіку. У цьому прикладі HealthAttributeможе переконатися, що 0 <= value <= max_healthце завжди правда. Він також може надсилати HealthCriticalEventіншим компонентам того ж об'єкта, коли він опускається нижче, скажімо, 25 відсотків, але він не може виконувати логіку більш складну, ніж це.


Приклад класу атрибутів:

class HealthAttribute : public EntityAttribute
{
  public:
    HealthAttribute(Entity* entity, double max, double critical)
        : max_(max), critical_(critical), current_(max)
    { }

    double value() const {
        return current_;
    }    

    void value(double val)
    {
        // Ensure that 0 <= current <= max 
        if (0 <= val && val <= max_)
            current_ = val;

        // Notify other components belonging to this entity that
        // health is too low.
        if (current_ <= critical_) {
            auto ev = std::shared_ptr<Event>(new HealthCriticalEvent())
            entity_->events().post(ev)
        }
    }

  private:
    double current_, max_, critical_;
};

Дякую! Це саме той відповідь, якого я хотів. Мені також подобається, що ваше уявлення про EventDispatcher краще, ніж просте повідомлення, яке передається всім організаціям. Тепер, до останнього, що ви мені сказали: ви в основному говорите, що Health and DamageImpact не повинні бути атрибутами в цьому прикладі. Отже, замість атрибутів вони будуть просто приватними змінними поведінки? Це означає, що "DamageImpact" пройде через подію? Наприклад, EventArgs.DamageImpact? Це звучить добре ... Але якби я хотів, щоб цегла змінила колір відповідно до свого здоров’я, то здоров’я повинно бути атрибутом, правда? Дякую!
TomsonTom

2
@TomsonTom Так, це все. Проведення подій, що містять будь-які дані, слухачам потрібно знати, дуже хороше рішення.
Пол Манта

3
Це чудова відповідь! (як ваш pdf) - Коли у вас є шанс, ви могли б трохи детальніше розглянути, як ви обробляєте візуалізацію з цією системою? Ця модель атрибутів / поведінки для мене абсолютно нова, але дуже інтригуюча.
Майкл

1
@TomsonTom Про візуалізацію дивіться відповідь, яку я дав Майклу. Щодо зіткнень, я особисто взяв ярлик. Я використав бібліотеку під назвою Box2D, яка є досить простою у використанні і справляється зіткненнями набагато краще, ніж я міг. Але я не використовую бібліотеку безпосередньо в логічному коді гри. У кожного Entityє ан EntityBody, який абстрагує всі потворні шматочки. Потім поведінка може читати положення з боку EntityBody, застосовувати до нього сили, використовувати суглоби та двигуни, якими володіє тіло і т. Д. Настільки сильне фізичне моделювання фізики, як Box2D, безумовно, приносить нові виклики, але вони досить веселі, imo.
Пол Манта

1
@thelinuxlich Отже, ви розробник Artemis! : D Я бачив Component/ Systemсхему, на яку посилався кілька разів на дошках. Наші реалізації дійсно мають дуже багато подібності.
Пол Манта
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.