Advertisement

Do I have the idea of Entity-Component System done right? I'm doing it for GUIs and here's an example.

Started by April 30, 2018 01:41 AM
10 comments, last by CircleOfAwesome 6 years, 4 months ago

I'm not sure if you guys can help me, but I figured an entity-component OOP design would be in your realm of experience. I want some feedback on it.

Lets suppose that I have the following (it's mostly pseudo code):

A header type of file:


public:
Button (textLabel, position);

private:
GuiComponent label;
GuiSystem gui;

A implementation type of file:


// Event hookers / hooks the events to the corresponding functions:
label.Clicked(onClickObj); // label is actually a pre-existing object that I     
                          can hook an event to. Now whenever user clicks, 
                          onClickObj. executes.

function Button(text, position){
    label.Text = text;
    label.Color = "Blue";
    label.Position = position;
}

function onClickDo(){
    gui.ChangeColor(label,"Red"); // Passes in our label obj. that we have in             
                                private and set its color to red!
    gui.ChangeText(label, "Button was clicked!");
    pause(2);
    gui.ChangeText(label, "Button"); // Changes text back to "Button."
}

This is basically a button that whenever is clicked, turns red and displays "Button was clicked!" which then turns back to "Button." It's just a simple example. Label is a pre-existing object and I can already manipulate it. I just want to place a whole ECS system on it.

GuiSystem is a container or class blueprint full of methods that can be used on the GuiComponent such as flash color a.k.a makes the label flash random colors, etc.

Then whenever I make something really custom like a server menu and I need buttons, all I need to do is create a custom class blueprint and inherit the Button class above. Then I replace/overwrite some of the functions and bam, I have a server menu. I would add more GuiComponent labels if needed too. Is this generally how gui-design-architecture works?

To show you the example in detail for instance, lets say I want a server menu. I create class blueprint detailing all member functions a server menu would do e.g: AddServer() which adds server buttons, ConnectServer() which calls something like ServerSystem.Connect(server) and connects you to the server, etc. I would then override the click function onClickDo() to do the ConnectServer() type of function. Right?

-------------------------------------------------------------------------------------------------------------------------------------

I know guis usually don't use ECS but I already have a gui class blueprint premade with all the properties and such and abilities to connect events. I'm just adding ECS ontop of it and putting functions in the GuiSystem container and when/if that container becomes full or bloated, I can split it off like GuiSystem.Effects.CreateEffect(label, effect) and etc.

Do I have the idea of ECS on right? Thanks for reading!

The problem with ECS, is that everyone seems to have their own idea on what it needs to be. So you'll get a lot of different answers.

I think you should read up on existing ECS frameworks and see how they're done, and see which ones you like most.

 

I think Unity is a sane example of practical mix of ECS and OOP.

 

 

 

Advertisement
On 4/29/2018 at 8:41 PM, CircleOfAwesome said:

Then I replace/overwrite some of the functions and bam, I have a server menu.

Be careful with that.

The naive implementation is to have a bunch of virtual functions, then call the functions on each object at the appropriate time. The details for this vary by language. Generally this means each object needs to have the function called even if they do nothing or were never implemented. When using the virtual function pattern, you're potentially calling an enormous number of empty virtual functions, all the overhead but with no meaningful functionality.

One easy way handle that that is still having a virtual function (or non-final functions or whatever your language of choice uses) but also having a corresponding flag in the base class. The base class's virtual function sets HasFunctionFoo=false; then a function in the base class handles the redirection with an inline function with: if(HasFunctionFoo) Foo(); adding parameters as needed.  Since the variable is in the base class and the call is non-virtual it can be optimized away, the overhead becomes the cost of a test of a member variable and jump rather than function calls for everything.

More advanced methods involve registering the functions or detecting the function's existence, or doing a bunch with macro magic or templates, with their own pros and cons.

 

On 4/30/2018 at 12:02 PM, Gureen Ryuu said:

The problem with ECS, is that everyone seems to have their own idea on what it needs to be. So you'll get a lot of different answers.

I think you should read up on existing ECS frameworks and see how they're done, and see which ones you like most.

 

I think Unity is a sane example of practical mix of ECS and OOP.

 

 

 

I've actually been reading about ECS for about a week plus now. There's no example that's really specific and it's hard to find. It's all theory, it seems like people just want you to invent your own way of doing things. But given my example above, how would you adapt it to be more ECS-friendly? But can you make this example in a simplistic manner and maybe show me?

I would really like some simple specific-examples that I can grasp. 

21 hours ago, frob said:

Be careful with that.

The naive implementation is to have a bunch of virtual functions, then call the functions on each object at the appropriate time. The details for this vary by language. Generally this means each object needs to have the function called even if they do nothing or were never implemented. When using the virtual function pattern, you're potentially calling an enormous number of empty virtual functions, all the overhead but with no meaningful functionality.

One easy way handle that that is still having a virtual function (or non-final functions or whatever your language of choice uses) but also having a corresponding flag in the base class. The base class's virtual function sets HasFunctionFoo=false; then a function in the base class handles the redirection with an inline function with: if(HasFunctionFoo) Foo(); adding parameters as needed.  Since the variable is in the base class and the call is non-virtual it can be optimized away, the overhead becomes the cost of a test of a member variable and jump rather than function calls for everything.

More advanced methods involve registering the functions or detecting the function's existence, or doing a bunch with macro magic or templates, with their own pros and cons.

 

Virtual functions and templates are kinda' advanced features for the language that I'm using. I don't think they support those, I'm using a simplistic language. It doesn't even offer OOP, but I have a work-around. Hence, this. So given that and my example, would you be able to come up with a simplistic approach to ECS or adapt my example and make it ECS friendly? I just really want some specific examples so that I can better understand the ECS concept. Like why isn't my example ECS-friendly already? What's wrong with it?

This looks like plain old composition to me.

One of the major reasons ECS is used, or just data oriented design in general, is that in languages that support such, you can avoid having this many virtual functions, because you have a single function that operates on a batch of data. That is aside from some other major benefits, of which most relate to making it easier to tune performance. 

In your case with your example, assuming you are using virtual functions and inheritance/have this available, it seems you would have/need something like a virtual Draw method for the button, which then delegates drawing to its 'components', its members. Then you inherit from it, add a component and need to override this Draw method to then be called. If you have a huge amount of buttons, the overhead of these virtual calls adds up, stuff like data locality can become a problem because you are likely jumping around in memory instead of accessing it linearly. Your draw function might be super simple and lightweight, but the overhead can become significant enough to bottleneck.

With a better layout and access pattern, you can improve this. In ECS terms your buttons would look more like this:


struct RenderComponent
{
	Texture Texture;
}

...
  
struct Button
{
 	RenderComponent RenderComponent; 
}

...

Array<RenderComponent> mRenderComponents;

void RenderButtons()
{
	for (RenderComponent render_component : mRenderComponents)
	{
		Render(render_component.Texture);
	}
}

Just to give the basic idea. All rendering components are next to each other in memory, no virtual calls etc. It makes you assume you can treat the data with a single algorithm. Merely simple transformations of data.

UI is somewhat more tricky in this area, as you are likely to do something different for each button being clicked, hovered and so forth. There are some overlaps where you could get some advantages, but it does not benefit as much of DOD as other areas could.

The ECS paradigm is not designed for GUIs - it's designed for 'entities' (hence the name) which, in games, refers to any of the many objects that inhabit the world and interact with each other, but where the objects are likely to vary in ways that don't fit a traditional inheritance-based approach.

There are many existing patterns for GUIs - Model-View-Controller is the standard one regarding how to structure them, although there are alternatives. Additionally, UI widgets and windows are examples of things that fit a standard inheritance system very well.

I would strongly advise looking at existing UI systems and emulating one of those, rather than trying to wedge ECS into somewhere it wasn't designed for and offers no benefits to.

Advertisement
2 hours ago, AthosVG said:

If you have a huge amount of buttons, the overhead of these virtual calls adds up, stuff like data locality can become a problem because you are likely jumping around in memory instead of accessing it linearly. Your draw function might be super simple and lightweight, but the overhead can become significant enough to bottleneck.

But does it, in practice? Agree, virtual functions are usually slower, even quite a bit slower due to jumping around in memory, but is this premature optimization? On a typical tree traversal it might use, in the order of 100 virtual function calls, if that. Even if every one is a cache miss, it's not even going to register on a performance analysis.

In my GUI I have no bottlenecks whatsoever in a simple tree with inheritance. As Kylotan says, ECS and DOD work great in games for things like actors, rendering etc, but I suspect GUI logic (as opposed to filling / fonts etc) hasn't been a performance problem for at least 30 years.

Personally I think the most important criteria is going with a design that is simple for you to understand and reason with, things like debugging and being extensible are far more important issues in practice.

2 minutes ago, lawnjelly said:

Personally I think the most important criteria is going with a design that is simple for you to understand and reason with, things like debugging and being extensible are far more important issues in practice.

No, it rarely does, hence my last part

2 hours ago, AthosVG said:

but it does not benefit as much of DOD as other areas could.

But I agree I should have emphasised this more. Kylotan gives an excellent summary of this; I mostly wanted to show an example how his code is not following tge paradigm. :)

9 hours ago, AthosVG said:

If you have a huge amount of buttons, the overhead of these virtual calls adds up, stuff like data locality can become a problem because you are likely jumping around in memory instead of accessing it linearly.

Partly, but that's not the big cost I was referring to.  The overhead of calling a virtual function is minimal on a modern machine. 

The virtual function indirection itself rarely requires a cache miss thanks to branch prediction and branch target buffers, and thanks to large instruction caches. 

The problem with the virtual aspect is that they cannot be inlined or elided. They must always be called.

It takes time for every function call (virtual or not) to save the registers and restore them when done, to put appropriate markers on the stack for exceptions and stack unwinding, costs for function prologs and epilogs, plus the occasional instruction cache misses. Virtual functions add around 7 ns, but outside of tight loops that overhead is rarely an issue.

The problem is the calls themselves. The naive type of virtual override functions is often completely unnecessary. Many times the functions will have no content; there is all the cost of a function call only to discover the function does absolutely nothing.  For the button example, will have many functions like OnMouseEnter(), OnMouseLeave(), OnMouseMove(), OnMouseDown(), OnClick(), OnDoubleClick(), OnMouseWheel(), OnCreate(), OnDestroy(), OnDragEnter(), OnDragExit(), OnDragDrop(), OnFocus(), OnKeyDown(), OnKeyUp(), OnSelect(), OnDeselect(). Those are in addition to the GUI functions like OnPaint(), OnPrePaint(), OnPostPaint(), and simulation functions like OnUpdate(), OnPreUpdate(), OnPostUpdate(), OnPhysics(), OnPrePhysics(), OnPostPhysics(), and on and on and on. 

When the calls can be optimized and inlined the empty functions vanish in a puff of logic.  But in languages with dynamic dispatch or virtual functions, they cannot be removed. Even if you're using a language that uses different names for the pattern, like Python or JavaScript or Go or Rust or Python, there is a cost for these functions since the overhead cannot easily be removed. The way programmers usually use them all the calls must be made even if they don't implement the functionality.  I've seen badly written code libraries where the enormous number of empty function calls consumed milliseconds per frame in function call overhead.

 

ECS can be implemented well, with very high performance and minimal unnecessary work.  ECS can be implemented terribly, with enormous wasted effort, terrible performance, and wasteful systems that don't respect the realities of data in the real world (hence the plea for more DOD or consideration of how data actually flows), nor of code execution in the real world (hence the comments about abusing functions).

Building an ECS system is fairly easy.  Building them well is a difficult task.

10 hours ago, frob said:

Partly, but that's not the big cost I was referring to.  The overhead of calling a virtual function is minimal on a modern machine. 

That should have been an 'and', so yes I agree, I was trying to point out a second benefit, my bad! 

Regardless, the point @Kylotan made still stands for UI.

@CircleOfAwesome Also, note that this doesn't mean this approach to creating UI is necessarily bad. You don't have to have a perfect datalayout and minimal to no virtual function overhead. It's nice to have something optimal, but functionality and a decent API are likely to be your main focusses for now.

Have a look into DOD if you are interested in this stuff. You can read a ton of stuff about ECS, but everyone does it differently, even though they may achieve the same, making it possibly unclear what the actual pattern entails. In the end it works towards such data-oriented design, which can be applied upon more than just entities and also more than games.

This topic is closed to new replies.

Advertisement