实体系统和渲染


11

Okey,到目前为止我所知道的;该实体包含一个组件(数据存储),其中包含类似信息;-纹理/精灵-着色器-等等

然后我有一个渲染器系统,可以绘制所有这些图像。但是我不明白渲染器应该如何设计。每个“视觉类型”都应有一个组件吗?一个组件没有着色器,一个组件具有着色器,等等?

只需输入一些“正确方法”即可完成此操作。要提防的提示和陷阱。


2
尽量不要让事情变得太笼统。拥有一个带有Shader组件而不是Sprite组件的实体似乎很奇怪,因此也许Shader应该是Sprite组件的一部分。当然,您只需要一个渲染系统。
乔纳森·康奈尔

Answers:


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#中工作,因此我使用属性来处理getter / setter-如果它们确实具有函数,则它们将基于检索其持有的数据)。

系统篇

系统不那么“愚蠢”,但仍然是愚蠢的自动机。它们没有整个系统的上下文,没有对其他系统的引用,也没有任何数据,除了可能需要做一些单独处理的一些缓冲区外。根据系统的不同,它可能具有专门的Update,或Draw方法,或者在某些情况下两者都有。

介面

接口是我系统中的关键结构。它们用于定义System罐加工以及罐加工的Entity能力。与渲染相关的接口是IRenderableIAnimatable

这些接口只是告诉系统哪些组件可用。例如,渲染系统需要知道实体的边界框和要绘制的图像。就我而言,这就是SpatialComponentImageComponent。所以看起来像这样:

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);
        }
    }
}

看着类,渲染系统甚至不知道an Entity是什么。它所知道的只是,IRenderable并列出了要绘制的列表。

一切如何运作

了解我如何创建新游戏对象以及如何将它们输入系统也可能会有所帮助。

创建实体

所有游戏对象都继承自Entity,以及描述该游戏对象可以做什么的任何适用接口。屏幕上几乎所有的动画如下所示:

public class MyAnimatedWidget : Entity, IRenderable, IAnimatable {}

供料系统

我在一个名为的列表中保留了游戏世界中所有实体的列表List<Entity> gameObjects。每一帧,我然后通过该列表和筛选对象引用复制到基于接口类型多个列表,如List<IRenderable> renderableObjects,和List<IAnimatable> animatableObjects。这样,如果不同的系统需要处理同一实体,则可以。然后,我将这些列表简单地交给每个系统UpdateDraw方法,然后让系统完成其工作。

动画

您可能很好奇动画系统的工作方式。在我的情况下,您可能需要查看IAnimatable接口:

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

这里要注意的关键是接口的ImageComponent方面IAnimatable不是只读的。它有一个塞特犬

正如您可能已经猜到的那样,动画组件仅保存有关动画的数据。帧列表(它们是图像分量),当前帧,每秒要绘制的帧数,自上一帧递增以来经过的时间以及其他选项。

动画系统利用了渲染系统和图像组件之间的关系。它只是在增加动画帧时更改实体的图像组件。这样,动画将由渲染系统间接渲染。


我可能应该注意,我真的不知道这是否与人们所说的实体组件系统非常接近。在尝试实现基于合成的设计时,我发现自己陷入了这种模式。
Cypher 2012年

有趣!我不太喜欢您的Entity的抽象类,但是IRenderable接口是一个好主意!
乔纳森·康奈尔

5

请参阅此答案以了解我所讨论的系统类型。

组件应包含有关绘制内容和绘制方式的详细信息。渲染系统将采用这些细节,并以组件指定的方式绘制实体。只有当您使用截然不同的绘图技术时,才会为单独的样式提供单独的组件。


3

将逻辑划分为组件的主要原因是创建一组数据,这些数据在实体中组合后会产生有用的可重用的行为。例如,将一个实体分为PhysicsComponent和RenderComponent很有意义,因为可能并非所有实体都具有Physics,而某些实体可能没有Sprite。

为了回答您的问题,您需要查看您的体系结构并问自己两个问题:

  1. 有没有纹理的着色器有意义吗
  2. 将Shader与Texture分开会使我避免代码重复吗?

拆分组件时,重要的是要问这个问题,如果对1.的答案是肯定的,那么您很可能会创建两个单独的组件,一个组件带有着色器,一个组件带有纹理。对于2.这样的组件,答案通常是肯定的,例如多个组件可以使用position的Position。

例如,物理和音频可能都使用相同的位置,而不是两个存储重复位置的组件都将它们重构为一个PositionComponent,并要求使用PhysicsComponent / AudioComponent的实体也必须具有PositionComponent。

根据您提供给我们的信息,您的RenderComponent似乎不适合拆分为TextureComponent和ShaderComponent,因为着色器完全依赖于Texture而不是其他任何东西。

假设您使用的是类似于T-Machine:Entity Systems的示例,则C ++中RenderComponent&RenderSystem的示例实现将如下所示:

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 2012年

@ snake5你在两个方面都是正确的。我把代码打在脑海中了,有一些问题,感谢您指出。我已经修复了受影响的代码,使其速度降低了一些,并正确使用了实体系统的习惯用法。
杰克·伍兹

2
@ snake5并不是每帧都重新计算数据,getComponents返回一个由m_manager拥有的向量,该向量是已知的,只有在添加/删除组件时才会更改。如果您有一个系统要使用同一实体的多个组件,例如想要使用PositionComponent和PhysicsComponent的PhysicsSystem,则这是一个优势。其他系统可能会想要该位置,并且通过使用PositionComponent,您不会有重复的数据。首先,它解决了组件之间如何通信的问题。
杰克·伍兹

5
@ snake5问题不是关于EC系统应该如何布置或其性能。问题是关于设置渲染系统。构造EC系统的方法有多种,这里不要一味地讨论性能问题。OP可能会使用与您的答案完全不同的EC结构。此答案中提供的代码仅是为了更好地显示示例,而不是因为其性能而受到批评。如果问题是关于性能的,那么这也许会使答案“无用”,但事实并非如此。
MichaelHouse

2
我比在Cyphers中更喜欢此答案中列出的设计。这与我使用的非常相似。即使组件只有一个或两个变量,较小的组件也是更好的imo。他们应该定义一个实体的一个方面,例如我的“可危”组件将有2个,也许是4个变量(每种健康和盔甲的最大和当前变量)。这些评论越来越长,如果您想讨论更多内容,让我们聊天
约翰·麦当劳2012年

2

陷阱1:过度设计的代码。考虑一下您是否真的需要实现所有东西,因为您将不得不忍受相当长的一段时间。

陷阱2:对象太多。我不会使用对象太多的系统(每种类型,子类型以及任何对象一个),因为这只会使自动处理变得更加困难。我认为,让每个对象控制一个特定的功能集(而不是一个功能)会更好。例如,对渲染中包含的每一个数据成分(纹理成分,着色器成分)进行过分分割-无论如何,您通常通常都需要将所有这些成分放在一起,您是否同意?

陷阱三:外部控制过于严格。首选更改名称而不是着色器/纹理对象,因为对象可以随渲染器/纹理类型/着色器格式/其他而改变。名称是简单的标识符-由呈现器决定用这些标识符做什么。有一天,您可能希望使用材质而不是普通着色器(例如,从数据添加着色器,纹理和混合模式)。使用基于文本的界面,可以更轻松地实现该功能。

对于渲染器,它可以是一个简单的界面,用于创建/销毁/维护/渲染由组件创建的对象。它的最原始表示可能是这样的:

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

这将允许您从组件中管理这些对象,并使它们保持足够的距离,以使您可以以任何所需的方式呈现它们。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.