Entity System и рендеринг

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
Интересно! Я не слишком заинтересован в абстрактном классе для вашей сущности, но интерфейс IRenderable - хорошая идея!
Джонатан Коннелл
5

Посмотрите этот ответ, чтобы увидеть тип системы, о которой я говорю.

Компонент должен содержать сведения о том, что рисовать и как его рисовать. Система рендеринга берет эти детали и рисует объект так, как указано компонентом. Только если вы будете использовать существенно разные технологии рисования, у вас будут отдельные компоненты для разных стилей.

MichaelHouse
источник
3

Основная причина разделения логики на компоненты заключается в создании набора данных, которые при объединении в сущности дают полезное, многократно используемое поведение. Например, имеет смысл разделить сущность на PhysicsComponent и RenderComponent, поскольку вполне вероятно, что не все сущности будут иметь физику, а некоторые сущности могут не иметь Sprite.

Чтобы ответить на ваш вопрос, вам нужно взглянуть на свою архитектуру и задать себе два вопроса:

  1. Имеет ли смысл иметь шейдер без текстуры
  2. Позволит ли отделение шейдера от текстуры избежать дублирования кода?

При разбиении компонента важно задать этот вопрос: если ответ на вопрос 1. да, то у вас, вероятно, есть хороший кандидат для создания двух отдельных компонентов, один с шейдером, а другой с текстурой. Ответ на вопрос 2. обычно да для таких компонентов, как Position, где несколько компонентов могут использовать 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 Вопрос не в том, как выстроить систему ЕС или как она работает. Вопрос в настройке системы рендера. Есть несколько способов структурировать EC-систему, не занимайтесь проблемами производительности одного над другим здесь. ОП, скорее всего, использует совершенно другую структуру ЕС, чем любой из ваших ответов. Код, приведенный в этом ответе, предназначен просто для того, чтобы лучше показать пример, а не подвергаться критике за его производительность. Если бы вопрос касался производительности, то, возможно, это сделало бы ответ «бесполезным», но это не так.
MichaelHouse
2
Я предпочитаю дизайн, изложенный в этом ответе, чем в Cyphers. Это очень похоже на тот, который я использую. Меньшие компоненты лучше imo, даже если они имеют только одну или две переменные. Они должны определять аспект сущности, как если бы у моего компонента «Damagable» было 2, может быть, 4 переменные (максимальная и текущая для каждого здоровья и брони). Эти комментарии становятся длиннее, давайте перейдем к чату, если вы хотите обсудить больше.
Джон Макдональд
2

Подводный камень # 1: переработанный код. Подумайте, действительно ли вам нужно все, что вы реализуете, потому что вам придется жить с этим довольно долго.

Подводный камень # 2: слишком много объектов. Я бы не использовал систему со слишком большим количеством объектов (по одному для каждого типа, подтипа и т. Д.), Потому что это только усложняет автоматизированную обработку. На мой взгляд, гораздо приятнее, чтобы каждый объект управлял определенным набором функций (в отличие от одной функции). Например, создание компонентов для каждого бита данных, включенного в рендеринг (компонент текстуры, компонент шейдера), слишком разделено - в любом случае, как правило, все эти компоненты должны быть вместе, не так ли?

Подводный камень # 3: слишком строгий внешний контроль. Предпочитаю менять имена на шейдерные / текстурные объекты, потому что объекты могут меняться с помощью рендерера / типа текстуры / формата шейдера / чего угодно. Имена - это простые идентификаторы - решать, что из них делать, решать визуализатору. Однажды вы можете захотеть использовать материалы вместо простых шейдеров (например, добавить шейдеры, текстуры и режимы наложения из данных). С помощью текстового интерфейса это гораздо проще реализовать.

Что касается средства визуализации, это может быть простой интерфейс, который создает / уничтожает / поддерживает / отображает объекты, созданные компонентами. Самым примитивным представлением этого может быть что-то вроде этого:

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

Это позволило бы вам управлять этими объектами из ваших компонентов и располагать их достаточно далеко, чтобы вы могли отображать их любым удобным для вас способом.

snake5
источник