Answers:
这是一个很难回答的问题,因为每个人对于如何构造实体组件系统都有自己的想法。我能做的最好的就是与您分享一些我发现对我最有用的东西。
我对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
能力。与渲染相关的接口是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);
}
}
}
看着类,渲染系统甚至不知道an 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
不是只读的。它有一个塞特犬。
正如您可能已经猜到的那样,动画组件仅保存有关动画的数据。帧列表(它们是图像分量),当前帧,每秒要绘制的帧数,自上一帧递增以来经过的时间以及其他选项。
动画系统利用了渲染系统和图像组件之间的关系。它只是在增加动画帧时更改实体的图像组件。这样,动画将由渲染系统间接渲染。
将逻辑划分为组件的主要原因是创建一组数据,这些数据在实体中组合后会产生有用的可重用的行为。例如,将一个实体分为PhysicsComponent和RenderComponent很有意义,因为可能并非所有实体都具有Physics,而某些实体可能没有Sprite。
为了回答您的问题,您需要查看您的体系结构并问自己两个问题:
拆分组件时,重要的是要问这个问题,如果对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;
}
陷阱1:过度设计的代码。考虑一下您是否真的需要实现所有东西,因为您将不得不忍受相当长的一段时间。
陷阱2:对象太多。我不会使用对象太多的系统(每种类型,子类型以及任何对象一个),因为这只会使自动处理变得更加困难。我认为,让每个对象控制一个特定的功能集(而不是一个功能)会更好。例如,对渲染中包含的每一个数据成分(纹理成分,着色器成分)进行过分分割-无论如何,您通常通常都需要将所有这些成分放在一起,您是否同意?
陷阱三:外部控制过于严格。首选更改名称而不是着色器/纹理对象,因为对象可以随渲染器/纹理类型/着色器格式/其他而改变。名称是简单的标识符-由呈现器决定用这些标识符做什么。有一天,您可能希望使用材质而不是普通着色器(例如,从数据添加着色器,纹理和混合模式)。使用基于文本的界面,可以更轻松地实现该功能。
对于渲染器,它可以是一个简单的界面,用于创建/销毁/维护/渲染由组件创建的对象。它的最原始表示可能是这样的:
class Renderer {
function Draw() { ... }
function AddSprite( ... ) { ... return sprite; }
function RemoveSprite( sprite ) { ... }
...
};
这将允许您从组件中管理这些对象,并使它们保持足够的距离,以使您可以以任何所需的方式呈现它们。