Система сутності та візуалізація


11

Гаразд, що я знаю поки; Суб'єкт господарювання містить компонент (сховище даних), який містить схожу інформацію; - Текстура / спрайт - Шейдер - тощо

І тоді у мене є система візуалізації, яка все це малює. Але я не розумію, як має бути спроектований рендер. Чи повинен я мати один компонент для кожного "візуального типу". Один компонент без шейдера, один із шейдером тощо?

Просто потрібен деякий вклад про те, що "правильний спосіб" зробити це. Поради та підводні камені, на які слід стежити.


2
Намагайтеся не робити речі занадто загальними. Здавалося б, сутність має компонент Shader, а не компонент Sprite, тому, можливо, Shader повинен бути частиною Sprite. Звичайно, тоді вам знадобиться лише одна система візуалізації.
Джонатан Коннелл

Відповіді:


8

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

Суб'єкт

Я сприймаю жировий клас до ECS, ймовірно, тому, що вважаю екстремальні методи програмування дуже неефективними (з точки зору продуктивності людини). З цією метою сутність для мене - це абстрактний клас, який успадковується більш спеціалізованими класами. Суб'єкт має ряд віртуальних властивостей та простий прапор, який підказує мені, чи має існувати ця сутність. Отже, стосовно вашого запитання щодо системи візуалізації, це Entityвиглядає так:

public abstract class Entity {
    public bool IsAlive = true;
    public virtual SpatialComponent   Spatial   { get; set; }
    public virtual ImageComponent     Image     { get; set; }
    public virtual AnimationComponent Animation { get; set; }
    public virtual InputComponent     Input     { get; set; }
}

Компоненти

Компоненти "дурні" в тому, що нічого не роблять і не знають . Вони не мають посилань на інші компоненти, і вони, як правило, не мають функцій (я працюю в C #, тому я використовую властивості для обробки геттерів / сеттерів - якщо вони мають функції, вони базуються на пошуку даних, які вони містять).

Системи

Системи менш "дурні", але все ще тупі автомати. Вони не мають контексту загальної системи, не мають посилань на інші системи і не містять даних, крім кількох буферів, які можуть знадобитися для їх індивідуальної обробки. Залежно від системи, вона може мати спеціалізований Update, або Drawметод, або в деяких випадках і те, і інше.

Інтерфейси

Інтерфейси - це ключова структура в моїй системі. Вони використовуються для визначення того, що Systemможе обробляти, а на що Entityздатний. Інтерфейсами, які мають значення для візуалізації, є: IRenderableі IAnimatable.

Інтерфейси просто повідомляють системі, які компоненти доступні. Наприклад, система візуалізації повинна знати поле обмеження сутності та зображення для малювання. У моєму випадку це було б SpatialComponentі те ImageComponent. Так це виглядає приблизно так:

public interface IRenderable {
    SpatialComponent Component { get; }
    ImageComponent   Image     { get; }
}

Система візуалізації

Тож як система візуалізації малює сутність? Насправді це дуже просто, тож я просто покажу тобі збитий клас, щоб дати тобі уявлення:

public class RenderSystem {
    private SpriteBatch batch;
    public RenderSystem(SpriteBatch batch) {
        this.batch = batch;
    }
    public void Draw(List<IRenderable> list) {
        foreach(IRenderable obj in list) {
            this.batch.draw(
                obj.Image.Texture,
                obj.Spatial.Position,
                obj.Image.Source,
                Color.White);
        }
    }
}

Дивлячись на клас, система візуалізації навіть не знає, що Entityтаке. Все, про що вона знає, - IRenderableце просто список їх, щоб скласти.

Як це все працює

Це може допомогти зрозуміти, як я створюю нові ігрові об’єкти та як я їх подаю до систем.

Створення утворень

Усі ігрові об’єкти успадковуються від Entity, а також будь-які застосовні інтерфейси, які описують, що може зробити цей ігровий об’єкт. Приблизно все, що анімоване на екрані, виглядає приблизно так:

public class MyAnimatedWidget : Entity, IRenderable, IAnimatable {}

Годування систем

Я зберігаю перелік усіх сутностей, що існують у ігровому світі, в єдиному списку під назвою List<Entity> gameObjects. Кожен кадр я просіюю цей список і копіюю посилання на об'єкти до додаткових списків на основі типу інтерфейсу, наприклад List<IRenderable> renderableObjects, та List<IAnimatable> animatableObjects. Таким чином, якщо різним системам потрібно обробляти одну і ту ж сутність, вони можуть. Тоді я просто передаю ці списки кожній із систем Updateабо Drawметодів і дозволю системам робити свою роботу.

Анімація

Вам може бути цікаво, як працює система анімації. У моєму випадку ви можете побачити інтерфейс IAnimatable:

public interface IAnimatable {
    public AnimationComponent Animation { get; }
    public ImageComponent Image         { get; set; }
}

Головне, що тут слід помітити, - це ImageComponentаспект IAnimatableінтерфейсу не лише для читання; в ньому є сетер .

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

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


Я, мабуть, зауважу, що я не знаю, чи це навіть близько до того, що люди називають сутнісно-складовою системою . У моїй спробі реалізувати композиційну конструкцію я потрапив у цю модель.
Cypher

Цікаво! Я не надто захоплююсь абстрактним класом для вашого Entity, але IRenderable інтерфейс - хороша ідея!
Джонатан Коннелл

5

Дивіться цю відповідь, щоб побачити, про яку систему я говорю.

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


3

Основна причина розділення логіки на компоненти полягає в створенні набору даних, які, поєднуючись в об'єкті, створюють корисну поведінку, що використовується багаторазово. Наприклад, розділення об'єкта на PhysicsComponent і RenderComponent має сенс, оскільки, ймовірно, не всі сутності матимуть фізику, а деякі сутності можуть не мати спрайта.

Для того, щоб відповісти на своє запитання, вам потрібно переглянути свою архітектуру та задати собі два питання:

  1. Чи має сенс мати шейдер без текстури
  2. Чи дозволить відділення шейдера від текстури уникати дублювання коду?

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

Наприклад, і Physics, і Audio можуть використовувати одне і те ж положення, а не обидва компоненти, що зберігають дублюючі позиції, ви перефактуруєте їх в один PositionComponent і вимагаєте, щоб сутності, які використовують PhysicsComponent / AudioComponent, також мали PositionComponent.

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

Якщо припустити, що ви використовуєте щось подібне до T-Machine: Entity Systems, зразок реалізації RenderComponent & RenderSystem в C ++ виглядатиме приблизно так:

struct RenderComponent {
    Texture* textureData;
    Shader* shaderData;
};

class RenderSystem {
    public:
        RenderSystem(EntityManager& manager) :
            m_manager(manager) {
            // Initialize Window, rendering context, etc...
        }

        void update() {
            // Get all the entities with RenderComponent
            std::vector<RenderComponent>& components = m_manager.getComponents<RenderComponent>();

            for(auto component = components.begin(); entity != components.end(); ++components) {
                // Do something with the texture
                doSomethingWithTexture(component->textureData);

                // Do something with the shader if it's not null
                if(component->shaderData != nullptr) {
                    doSomethingWithShader(component->shaderData);
                }
            }
        }
    private:
        EntityManager& m_manager;
}

Це абсолютно неправильно. Вся суть компонентів полягає в тому, щоб відокремити їх від сутностей, а не змушувати системи візуалізації шукати через сутності, щоб їх знайти. Системи візуалізації повинні повністю контролювати власні дані. PS Не ставте std :: vector (особливо з даними екземпляра) в циклі, це жахливо (повільно) C ++.
snake5

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

2
@ snake5 Ви не перераховуєте дані у кожному кадрі, getComponents повертає вектор, що належить m_manager, який уже відомий і змінюється лише при додаванні / видаленні компонентів. Це перевага, коли у вас є система, яка хоче використовувати кілька компонентів одного і того ж об'єкта, наприклад, PhysicsSystem, яка хоче використовувати PositionComponent і PhysicsComponent. Інші системи, ймовірно, захочуть позицію, і, маючи PositionComponent, у вас немає дублікатів даних. В першу чергу це вирішує проблему того, як спілкуються компоненти.
Джейк Вудс

5
@ snake5 Питання не в тому, як повинна бути розроблена система ЄК чи її продуктивності. Питання стосується налаштування системи візуалізації. Існує кілька способів структурувати систему ЕС, не захоплюйтеся питаннями ефективності роботи один над одним тут. ОП, ймовірно, використовує зовсім іншу структуру ЄС, ніж будь-яка з ваших відповідей. Код, наданий у цій відповіді, призначений лише для того, щоб краще показати приклад, а не піддавати критиці за його ефективність. Якби питання стосувалося продуктивності, ніж, можливо, це зробило б відповідь "не корисною", але це не так.
MichaelHouse

2
Я набагато більше віддаю перевагу дизайну, викладеному у цій відповіді, ніж Cyphers. Це дуже схоже на те, що я використовую. Менші компоненти краще imo, навіть якщо вони мають лише одну або дві змінні. Вони повинні визначати аспект сутності, так як мій компонент "Пошкоджуваний" матиме 2, можливо 4 змінних (максимум та поточний для кожного здоров'я та броні). Ці коментарі стають довгими, перейдемо до чату, якщо ви хочете більше обговорити.
Джон Макдональд

2

Pitfall №1: перероблений код. Подумайте, чи справді вам потрібна кожна річ, яку ви реалізуєте, тому що вам доведеться жити з нею досить довго.

Падіння №2: занадто багато об'єктів. Я б не використовував систему з занадто великою кількістю об'єктів (по одному для кожного типу, підтипу та будь-якого іншого), оскільки це просто ускладнює автоматизовану обробку. На мою думку, набагато приємніше кожен об'єкт контролювати певний набір функцій (на відміну від однієї функції). Наприклад, створення компонентів для кожного біту даних, що входить до візуалізації (компонент текстури, шейдерний компонент) занадто розділений - зазвичай вам все-таки потрібно мати всі ці компоненти разом, чи не згодні ви?

Падіння №3: надто суворий зовнішній контроль. Віддайте перевагу зміні імен на шейдери / текстурні об’єкти, оскільки об’єкти можуть змінюватися за допомогою рендерера / типу текстури / формату шейдера / будь-якого іншого. Імена - це прості ідентифікатори - вирішувати, що робити з них, залежить від рендерінга. Одного разу, можливо, ви хочете мати матеріали замість простих шейдерів (наприклад, додайте шейдери, текстури та режими накладання даних із даних). З текстовим інтерфейсом реалізувати це набагато простіше.

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

class Renderer {
    function Draw() { ... }
    function AddSprite( ... ) { ... return sprite; }
    function RemoveSprite( sprite ) { ... }
    ...
};

Це дозволить вам керувати цими об’єктами зі своїх компонентів і тримати їх досить далеко, щоб ви могли їх відтворити будь-яким способом.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.