Entity-Component systems

Started by
55 comments, last by CrazyCdn 6 years, 4 months ago
1 hour ago, matt77hias said:

For the scripts, I indeed use pointers to exploit polymorphism. The user is capable of creating an infinite number of different scripts. For the other components, it is possible to store them by value to exploit cache coherence. The user is not capable of creating such components: they are defined by the engine itself

Well if you're going to store them by value, then you are effectively doing what is being done in the Unity video, just slightly less efficient. I have yet to personally find a game that requires such an approach that the benefits outway the cost of increased complexity, but it is a valid approach.

 

1 hour ago, matt77hias said:

.My TransformNode constitutes the scene graph. Though, you are indeed right that if you use some information not contained in the local part of the TransformNode (for example something relative to the world), the correctness depend on the order of executing the scripts.

Yeah, I didn't take the bucketed approach because it has several problems, especially when you decide you want to multithread your engine. Any time spent syncing jobs is time wasted, and a bucketed approach is full of such issues.

 

1 hour ago, matt77hias said:

So based on the above, is it correct that you are only concerned with

From a "cache coherency" perspective , then the answer is yes.

 

1 hour ago, matt77hias said:

But how does this look like from the perspective of the Entity? One common Component base class (OO way of doing things) or a "sort of database" (property/table way of doing things)?




class CEntity
{

	Array<CComponent*>	m_Components;
};
      
// base for all components
class CComponent
{
      virtual void ActivateComponent();
      virtual void DeactivateComponent();
      virtual void TickComponent(float DeltaTime);     
};
      
class CPhysicsComponent : public CComponent
{
      
      
};
      
class CVisualComponent : public CComponent
{
      
      
};

There are many approaches to the EC model, but this is the basics of it.

Advertisement
1 hour ago, AxeGuywithanAxe said:

virtual void TickComponent(float DeltaTime);

Isn't this behavior?

 

All methods are virtual? If I am correct the first two are just specific message handlers. But I do not get the Tick? This involves encapsulating the behavior in the component?

 

1 hour ago, AxeGuywithanAxe said:

then you are effectively doing what is being done in the Unity video

But Unity uses by value for scripts as well. With the "other components", I mean all non-script/MonoBehavior related components such as Models, Lights, Colliders, etc. Based on the video, I have no idea, how Unity is storing these on the C++ engine side.

1 hour ago, AxeGuywithanAxe said:

just slightly less efficient. I have yet to personally find a game that requires such an approach that the benefits outway the cost of increased complexity, but it is a valid approach

Could you elaborate?

🧙

1 hour ago, AxeGuywithanAxe said:

There are many approaches to the EC model, but this is the basics of it.

And assume an instance of CEntity has a pointer to CPhysicsComponent. Is there a clean way to get that component, given the entity?

🧙

37 minutes ago, matt77hias said:

Isn't this behavior?

 

All methods are virtual? If I am correct the first two are just specific message handlers. But I do not get the Tick? This involves encapsulating the behavior in the component?

Yeah, the entity-component model has behavior in the components. It takes the idea of composition , so instead of having a hiearchy like

CEntity -> CPhysicalEntity -> CMovableEntity , you'll have an entity, and you'll add a physicscomponent and a movecomponent to it. Not all functions have to be virtual, I was just giving an example. It's the same approach you would with any OOP hiearchy, you just separate "logic" into component types, i.e physics, visual , movement, script, etc. In my game engine, different component types have speciific logic when they are activated and deactivated, so they are in the base class, and most components have some sort of "tick" logic.

 

39 minutes ago, matt77hias said:

But Unity uses by value for scripts as well. With the "other components", I mean all non-script/MonoBehavior related components such as Models, Lights, Colliders, etc. Based on the video, I have no idea, how Unity is storing these on the C++ engine side.

You'll have to be more descriptive of your method, in most ecs approaches, scripts are handled by using a "CScriptComponent", so you would have to explain what your scripts actually do, and the point of them in more depth.

 

40 minutes ago, matt77hias said:

Could you elaborate?

Well with an ECS method, you will have data in one class and logic in the system class. This means that you're adding two classes in an entity - component -system model for every one class you would have made using an entity component model. I say I've yet to see one that's worth the added complexity because the largest games that exist , the Witcher 3 , Horizon, UE4 based games , Tomb Raider and etc all use an entity-component model. The only two major game engines that I know of that use ECS are the Overwatch engine , and the Insomniac engine (which isn't pure ECS because logic is still maintained in the components as far as I can tell from presentations).

40 minutes ago, matt77hias said:

And assume an instance of CEntity has a pointer to CPhysicsComponent. Is there a clean way to get that component, given the entity?

Yeah , I have a function called "GetComponentOfClass" and "GetComponentsOfClass". I programmed an automatic reflection system that preprocesses my c++ code and generates reflection data for classes, enums, structs, functions, properties, etc, but there are dozens of simple ways to create custom type info.

12 hours ago, matt77hias said:

My rendering system retrieves the data from the components and performs the rendering based on that data; so quite DoD.

 

6 hours ago, matt77hias said:

UniquePtr< TransformNode > m_transform;

This really doesn't look like you're using any DoD. From the look of the code, it seems like you'd have something like this?


for each render component as renderable
  transform = renderable->m_entity->m_transform
  set_draw_data( transform )
  draw( renderable )

 

In DoD, you'd consider two things first: What function consumes transform data (e.g. the renderer), and what function(s) produce transform data (e.g. gameplay, animation, etc). Then consider in isolation, how the consumer function would ideally lay out the input data so that it can process it as quickly as possible. Then consider in isolation how the producer functions would ideally lay out their data so that they can produce it as quickly as possible. Then reconcile those two ideal layouts into something that works in practice. Figure out if you need any conversion transforms in the middle (e.g. gameplay outputs data set X, which is then converted to data set Y, which is consumed by the renderer).

12 hours ago, matt77hias said:

If not, the user can extend the behavior (within the realm of engine support) via inheritance.

Inheritance is not a tool for code re-use / extension. Composition is a tool for code re-use / extension.

12 hours ago, AxeGuywithanAxe said:

In my game engine, different component types have speciific logic when they are activated and deactivated, so they are in the base class, and most components have some sort of "tick" logic.

But that is something, I wanted to avoid. In the beginning a Model was capable of drawing itself by drawing its Mesh and Material. If we avoid inheritance, we can make all these methods non-virtual to gain some performance, but this looks not very DoD, but rather OoD? So currently, I really extract the data from the scene and render the complete batch of Models based on the data of the Model, but not inside the Model. Why would a Model anyway be capable of drawing itself? It merely provides a description about its appearance (declarative vs imperative)?

12 hours ago, AxeGuywithanAxe said:

You'll have to be more descriptive of your method, in most ecs approaches, scripts are handled by using a "CScriptComponent", so you would have to explain what your scripts actually do, and the point of them in more depth.

My scripts do nearly everything at the moment, similar to Unity's MonoBehavior. So there is a duality, I do not like about my codebase:

  • Models contain a description of their appearance, but contain no drawing logic. A render pass will take care of that based on the data.
  • Scripts contain some typical hooks Update etc. in which they are capable of executing some operations (so not very descriptive at all). Currently, this basically involves changing some parameters of whatever at runtime (materials, transforms, etc.).
12 hours ago, AxeGuywithanAxe said:

Well with an ECS method, you will have data in one class and logic in the system class. This means that you're adding two classes in an entity - component -system model for every one class you would have made using an entity component model

Thanks this is a very clear definition. Though given this definition, I would prefer ECS over EC. Why would you add logic to the Component, because this way you will deviate from DoD to OoD? In the most extreme case, you can say that each E and C corresponds to a table in some relational database, which only contains data (so no logic)? ECS seems also somewhat similar to a producer-consumer system.

12 hours ago, AxeGuywithanAxe said:

Yeah , I have a function called "GetComponentOfClass" and "GetComponentsOfClass". I programmed an automatic reflection system that preprocesses my c++ code and generates reflection data for classes, enums, structs, functions, properties, etc, but there are dozens of simple ways to create custom type info.

And I guess, you use that as well for displaying each Component in the editor. Not only the engine defined ones, but user created scripts as well since you can basically display all member variables in an appropriate way. A visitor would be "clean" alternative for the engine defined ones, but could not anticipate the user defined ones without instructing the user to do so. So in that sense reflection is the most flexible solution. Though, you need to give up the data encapsulation provided by the typical getter/setter pair, since detecting an arbitrarily named getter/setter does not look very trivial (at least more difficult than making all member variables public).

12 hours ago, Hodgman said:

This really doesn't look like you're using any DoD. From the look of the code, it seems like you'd have something like this?



for each render component as renderable
  transform = renderable->m_entity->m_transform
  set_draw_data( transform )
  draw( renderable )

The renderer extracts the "renderables" which will be filtered in some collections (e.g. opaque, transparent, brdf, no brdf, etc.). The second line will be consequence of the ECS. At the moment I extend the entities, lacking components, but after refactoring it will indeed look something like that. The last two lines are sort of present as well. Am I misunderstanding something?

The following seems DoD to me from the perspective of the Components:


RenderSystem::set_draw_data(Transform)

RenderSystem::Draw(Renderable)

whereas this seems OoD to me from the perspective of the Components:


Transform::set_draw_data()

Renderable::Draw()

Edit: Though, I now also realize that DoD could mean packing all related data together independent of logical functionality. The drawback could be possible duplication.

12 hours ago, Hodgman said:

Then reconcile those two ideal layouts into something that works in practice. Figure out if you need any conversion transforms in the middle (e.g. gameplay outputs data set X, which is then converted to data set Y, which is consumed by the renderer).

But during the "conversion" you can have bad cache coherency. So isn't this moving the problem?

12 hours ago, Hodgman said:

Inheritance is not a tool for code re-use / extension. Composition is a tool for code re-use / extension.

UE4 uses this monolytic hierarchy, letting you extend everything. I agree that composition and aggregation are better practices, but inheritance sometimes look like the only way of hooking something to an existing system.

🧙

2 hours ago, matt77hias said:

The following seems DoD to me from the perspective of the Components:
whereas this seems OoD to me from the perspective of the Components:

DoD and OOD are not opposites. They're orthogonal. You can do both. e.g. this presentation could be seen as giving OO a knock-out punch, but it's actually about how to take an OO design and implement it using DoD thought processes instead of "typical" C++ thought processes.

OO has specific code features, such as classes, and special syntax to go with that. DoD is a process in how you think about your code; it doesn't have any specific syntactical features or new coding paradigms like classes.

for each render component as renderable
if the render components are individually allocated with new, this iteration will randomly jump all over memory. You're likely to incur a cache miss on every single iteration.
transform = renderable->m_entity->m_transform
the contents of the renderable won't be in cache, the contents of it's parent entity won't be in cache, and the contents of that entity's transform won't be in cache.

This loop hardly does any work. This loop does an extraordinary amount of nothing -- just waiting on RAM->Cache requests to complete -- and also does little bits of sporadic work. It is the opposite of DoD thinking.

If you're actually interested in DoD, find out:
What is the exact, minimal set of data required to fulfil the Transform::set_draw_data and Renderable::Draw functions.
How can I structure that data set to be as small and compact as possible.
How can I generate this data set ahead of time in an efficient way.
Then that has a knock-on effect to the systems that generate the data -- how can you restructure their data to support your new goals? How can you restructure all the code that interacts with their data?

But, for now, I would probably just forget all about DoD and just focus on making something work, rather than trying to make something fast.

2 hours ago, matt77hias said:

But during the "conversion" you can have bad cache coherency. So isn't this moving the problem?

DoD requires an examination of the specific details of a specific problem. You can't just apply a general pattern to every problem -- that's the opposite of DoD (e.g. The ECS pattern boils down to "just use a table for everything", which is pushing one solution regardless of the problem is -- so blindly following ECS is not DoD).

In some situations, sure, adding extra steps may just be moving problems around with no purpose. In other situations, a multi-pass solution may be the best solution -- e.g. outputting a list of indices into a data set in the first pass, and then collecting the data items associated with those indices in a second pass. It all depends on the specific problems that you're solving.

Note that polymorphism is quite often the opposite of DoD though -- as virtual functions mean "I don't know what code or data is going to execute / be read or written now".... and if you don't know what your program is about to do, that's about the least specific you can possibly be!

2 hours ago, matt77hias said:

UE4 uses this monolytic hierarchy, letting you extend everything. I agree that composition and aggregation are better practices, but inheritance sometimes look like the only way of hooking something to an existing system.

Just because UE4 does it, doesn't mean that it's correct. They can still be trampling all over the rules of OO. Big products made by 100's of programmers are often more likely to do so :P

If your entire code base is designed around the idea of extending classes, then yeah, you get pushed into extending classes in order to add new features to that existing code base.

If your code base is designed around the idea of composing new objects from other objects, then you get pushed into writing new composite classes in order to add new features to that code base.

Inheritance is only "the only way" if the code is forcing that particular "solution" upon you. Good uses of inheritance are pretty rare...

3 hours ago, Hodgman said:

If I am going in this direction, I would never leave the design phase :o But I get the metaphor.

3 hours ago, Hodgman said:

What is the exact, minimal set of data required to fulfil the Transform::set_draw_data and Renderable::Draw functions.

One difficulty with this is: "what do you want to draw". I want to use a minimal number of GPU buffers so everything will be packed together: transform data, material data, etc. Though, some visualizations like the typical debug visualizations (solid, texture, etc.) require less data. But I understand that I should consider the most usual case (PBR) for that.

3 hours ago, Hodgman said:

outputting a list of indices into a data set in the first pass, and then collecting the data items associated with those indices in a second pass

But that will certainly result in jumping around in memory?

For instance, the Transform and the Drawable. Adding them together would be strange, since lots of different component handling systems require a Transform but a differnt Component. The rendering, physics, collision and audio system all need to be aware of a Transform and some specific Component. So, there will always be an indirection.

In this sense, transforms and scripts are the most difficult components imo. Transforms because everyone needs them and scripts because you can have an infinte number of types. I nearly get the impression that handling the latter efficiently is to let the user of a game engine write the scripting/gameplay system himself and hook it to the system.

3 hours ago, Hodgman said:

for each render component as renderable

But like I said for the renderables, i can store them by value in contiguous memory:

23 hours ago, matt77hias said:

vector< Spotlight > m_spotlights; ~~~> RenderingSystem

but it could be more efficient to collect the transforms as well and use one "conversion" structure while moving from the scene (producer) to the rendering system (consumer). But this requires going to the entity or in a pure component model going to the associated transform component (different locations in memory).

 

Currently I did the following:


void XM_CALLCONV VariableShadingPass::ProcessModels(
        const vector< const ModelNode * > &models,
        FXMMATRIX world_to_projection, 
        CXMMATRIX world_to_view, 
        CXMMATRIX view_to_world,
        bool transparency) {

        for (const auto node : models) {
            
            // Obtain node components (1/2).
            const TransformNode * const transform = node->GetTransform();
            const Model         * const model     = node->GetModel();
            const XMMATRIX object_to_world        = transform->GetObjectToWorldMatrix();
            const XMMATRIX object_to_projection   = object_to_world * world_to_projection;

            // Cull the model against the view frustum.
            if (ViewFrustum::Cull(object_to_projection, model->GetAABB())) {
                continue;
            }

            // Obtain node components (2/2).
            const XMMATRIX object_to_view         = object_to_world * world_to_view;
            const XMMATRIX world_to_object        = transform->GetWorldToObjectMatrix();
            const XMMATRIX view_to_object         = view_to_world * world_to_object;
            const XMMATRIX texture_transform      = node->GetTextureTransform()->GetTransformMatrix();
            const Material * const material       = model->GetMaterial();

            // Bind the model data.
            BindModelData(object_to_view, view_to_object, texture_transform, material);
            // Bind the pixel shader.
            BindPS(material, transparency);
            // Bind the model mesh.
            model->BindMesh(m_device_context);
            // Draw the model.
            model->Draw(m_device_context);
        }
    }

My taxonomy is:

  • Node
    • has a TransformNode
      • has a Transform
      • has a parent and childs
  • is derived by ModelNode
    • has a TextureTransform
    • has a Model
      • has a Mesh
      • has an offset and number of indices in the Mesh
      • has an immutable local AABB and BS
      • has a Material
  • is derived by OmniLightNode/SpotLightNode
    • has an OmniLight/SpotLight
  • is derived by OrthographicCameraNode/PerspectiveCameraNode
    • has a OrthographicCamera/PerspectiveCamera ~~> virtual function to get the view-to-projection and projection-to-view matrices
    • has viewport transform
  • ...

But I am going to remove the Node hierarchy, making all the remaining Components. Furthermore, I can store some data by value to avoid some pointer jumping.

 

 

🧙

3 hours ago, Hodgman said:

A little OT, but this saved my day today, thanks for that awesome link!

Thats the thing every programmer should know and need to be aware of - most will call this "micro-optimizations" or "premature optimizations", but its just the correct way to think and code.

Now to get to the topic, that code:


void XM_CALLCONV VariableShadingPass::ProcessModels(
        const vector< const ModelNode * > &models,
        FXMMATRIX world_to_projection, 
        CXMMATRIX world_to_view, 
        CXMMATRIX view_to_world,
        bool transparency) {

        for (const auto node : models) {
            
            // Obtain node components (1/2).
            const TransformNode * const transform = node->GetTransform();
            const Model         * const model     = node->GetModel();
            const XMMATRIX object_to_world        = transform->GetObjectToWorldMatrix();
            const XMMATRIX object_to_projection   = object_to_world * world_to_projection;

            // Cull the model against the view frustum.
            if (ViewFrustum::Cull(object_to_projection, model->GetAABB())) {
                continue;
            }

            // Obtain node components (2/2).
            const XMMATRIX object_to_view         = object_to_world * world_to_view;
            const XMMATRIX world_to_object        = transform->GetWorldToObjectMatrix();
            const XMMATRIX view_to_object         = view_to_world * world_to_object;
            const XMMATRIX texture_transform      = node->GetTextureTransform()->GetTransformMatrix();
            const Material * const material       = model->GetMaterial();

            // Bind the model data.
            BindModelData(object_to_view, view_to_object, texture_transform, material);
            // Bind the pixel shader.
            BindPS(material, transparency);
            // Bind the model mesh.
            model->BindMesh(m_device_context);
            // Draw the model.
            model->Draw(m_device_context);
        }
    }

Does too many things:

- Compute matrices for culling

- Detect visbility for every model

- Compute matrices for rendering

- Draw the actual model

 

So to make this more cache friendly, make 3 functions:

- One which just computes all the matrices

- One which sort your models based on visibility

- One which draws the sorted models

This topic is closed to new replies.

Advertisement