Advertisement

Using ECS for complex engine and game logic

Started by October 28, 2023 01:29 AM
24 comments, last by Tom Sloper 1 year ago

@JoeJ Thank you for the input.

Perhaps I overstate the “lack of information”, so I should put this another way: there is plenty of good information about both the highest and lowest levels of ECS usage (and game development in general), but I think there is a huge gap when it comes to anything in-between. All the “glue code" which gives people case studies and best practices for how they should create an architecture.

Of course, as I mentioned, there's a good chance that this is literally the “depends on the game” situation. And I'm not even looking for a “one size fits all solution” - it is the bane of programming when one implementation tries to be good at everything. Still, in the same way that there can be different approaches to optimal memory management, and toward optimal designer UX, there must be some good recipes for how one can bridge the gap between such systems, and where one should worry about performance. Even when performance isn't the concern, one could then place emphasis on things like “ergonomics” (i.e good APIs that ensure the code doesn't turn into a mess)

I believe ECS communicating with the renderer is a good example, since it's one of the most straightforward and relevant “high level talking to low level” scenarios: you have the game logic in the former, and the engine in the latter, and the two need to talk to each other so that a player can push buttons and see results on the screen.

Yes, as you aptly put, I wouldn't be asking about ECS if I hadn't set my mind on it already. The truth is, I am past the point in my game development history where I want to blindly experiment. I absolutely see the value in using ECS, but don't want to start reinventing the wheel. There are certain scenarios where I already foresee complications (e.g when systems need to handle special cases in the same component combinations), but so far my biggest hurdle was figuring out the bridge between high-level (where ECS can be really helpful) and low-level (which will most likely be specialized architectures). I assume that ECS, given its structure, may allow a closer and more efficient link between the two, but I am hoping to analyze existing implementations to know for sure.

yah-nosh said:
Perhaps I overstate the “lack of information”, so I should put this another way: there is plenty of good information about both the highest and lowest levels of ECS usage (and game development in general), but I think there is a huge gap when it comes to anything in-between. All the “glue code

Hmm, now that you say it this way, i realize my own lack of information here too. And that's really a burden and reason why i've never actually tried ECS in cases where i have considered it.

yah-nosh said:
I believe ECS communicating with the renderer is a good example, since it's one of the most straightforward and relevant “high level talking to low level” scenarios: you have the game logic in the former, and the engine in the latter, and the two need to talk to each other so that a player can push buttons and see results on the screen.

If the two topics are that different, then i just assume there is no design win to expect from some common but enforced way to deal with data or composition. I mean, there surely is, but i'll never find it, no matter how many books i would read.
You know, i can't avoid the mess, the spaghetti code, the hacks. I can't get rid of it. It feels like it just belongs to programming. No complex projects without mess and bad design.
Over the years my practice became to ‘focus’ the mess to few spots at least. So it does not affect everything, but ideally just glue code. And ofc. testing code, examples, debug stuff, etc.

Now, if i was an expert, then likely i would not write blog posts about this ugly mess. I would instead write about my well designed systems on the low level, or about interesting high level concepts.
So maybe that's a reason of why we lack this education on ‘in between, in practice, well it sucks but it works’ code. \:D/
And that's what i believe indeed. Likely, if you can look at professional game engines source code, it's not perfect and uniform either. There's mess, multiple programming paradigms, legacy code, modern code, all side by side.

Some people here work on custom, smaller engines from ground up. So i guess we'll get some interesting feedback…

Advertisement

alice wolfraider said:
“Component iteration” sounds nice: this name is taken from a misunderstanding of programming from the “C++ language segment”, that is, from the C++ developer-programmer sector : this is the wrong approach - in essence, it is the opposite approach to procedural programming: when the meaning of ECS - as a differentiator - is tried to be leveled There’s not much to discuss here, but it turns out that in gamedev everyone needs knoweledge up by ECS: but it turns out that almost no one knows this: but it’s necessary for screenwriters, game designers and programmers, especially web developers

I'm not sure where you're pulling that from, but to me that just sounds weird.

C++ is all about composition, and large C++ programs are absolutely filled with it. Built inheritance trees that are shallow and wide, that's been the declaration for over 30 years. Then you can take all those items, stick them in a generic container, and be done with it.

Any time you iterate over a container you're in a situation where ECS applies. You've got a collection of things, and you operate on those things, and those things all implement the same interface. Entity is the container, components are the things in the container.

Any time people write about “SOLID principles" that type of thing also applies. You've got a bunch of components, each meets them: Single responsibility for each component, Open/Closed principle because they are all extensions of the base, LSP because you don't know or care what the concrete type is, only that they are interchangeable, Interface is segregated as demonstrated by the shared interface they all use in the container, and dependency inversion is in place because the developer is operating on the abstraction of the container.

The C++ standard library containers, the containers used by all the major game engines, all of them favor composition and are very useful for exactly that.

yah-nosh said:
Perhaps I overstate the “lack of information”, so I should put this another way: there is plenty of good information about both the highest and lowest levels of ECS usage (and game development in general), but I think there is a huge gap when it comes to anything in-between. All the “glue code which gives people case studies and best practices for how they should create an architecture.

I think this is mostly people making a much bigger deal about something than is necessary. The concept has been around for many decades before the “ECS” term was coined, and the concept is widely used regardless of the term.

std::vector<IThing*> things;
...
for(IThing* thing : things )
{
   thing->DoStuff();
}

That's it. That's all it takes. You're using components (things) inside an entity of whatever holds the things.

It doesn't matter if you're building in Unity and the “things” are derived from MonoBehaviour rather than IThing. Or if you're in Unreal and you've derived them from UActorComponent, they're still the same concept.

It isn't really a mind-blowing concept for people familiar with programming generally, but it tends to be difficult for beginners to grasp working with interfaces rather than working with concrete types.

The alterative is something beginners tend to do, which is this type of pattern:

if (piece.type == CP_KING) { ... /* do king stuff */ ... }
else if( piece.type == CP_QUEEN) { ... /* do queen stuff */ ... }
else if( peice.type == CP_ROOK) { ... /* do rook stuff */ ... }
else if( peice.type == CP_BISHOP) { ... /* do bishop stuff */ ... }
else if( peice.type == CP_KNIGHT) { ... /* do knight stuff */ ... }
else if( peice.type == CP_PAWN) { ... /* do pawn stuff */ ... }

While that's terrible from an engineering perspective, it is sadly quite common for beginner programmers, and that's the reason why the concept has been so heavily stressed for so many decades.

frob said:
I'm not sure where you're pulling that from, but to me that just sounds weird.

Remembering some former Alice comments, it seems somebody tries to make a chatbot specialized on sounding somewhat informed while outsmarting people.

frob said:
I think this is mostly people making a much bigger deal about something than is necessary. The concept has been around for many decades before the “ECS” term was coined, and the concept is widely used regardless of the term.

std::vector things; ... for(IThing* thing : things ) { thing->DoStuff(); }

That's it. That's all it takes. You're using components (things) inside an entity of whatever holds the things.

That's the conclusion i've got after reading about ECS. It was not clear what's the fuzz about it.
Besides, there is the promise of good memory access patterns by using arrays for performance. But that's much too obvious and applies to anything anyway.
The only thing interesting i've found was various approaches to resolve indirections, so all components of an entity can be found. But that's a general problem as well, and ECS does not introduce any new solution.
I was left with the impression i had used ECS even before OOP, and the only new thing seems the idea of using a general and uniform way to deal with those indirections for the entire code base, while i always came up with multiple and specific ways to deal with indirections.

frob said:
While that's terrible from an engineering perspective, it is sadly quite common for beginner programmers, and that's the reason why the concept has been so heavily stressed for so many decades.

I do this much better. I use switch statements instead if else! \:D/

But i wonder: How was this done in C, before C++ was widely adopted?
Is there some neat work around in case virtual functions can't be used?

frob said:

std::vector<IThing*> things;
...
for(IThing* thing : things )
{
   thing->DoStuff();
}

First of all, I've not implemented an ECS, this is just from what I've read.
From what I am understanding about ECS, usually you would implement it exactly not this way, if you try to archive best performance.

An entity is just a collection of components (in the most extreme case, an entity is just an array index), the component hold the data. Neither the entity nor the components implement functionality, thus they don't have methods like DoStuff(). The systems implement the logic, thus the system will have a method like DoStuffForManyThings() which will then iterate over the entities that are relevant for the system.
The system will, for each entity that has the correct components, gather the relevant data needed to perform the current task from the components it requires and ignore any additional components the entity has.

It's hard to illustrate this with just Thing, so maybe let's assume there are multiple components available.
For example a couple of physics components like:
a PositionComponent, an OrientationComponent, a SizeComponent, a VelocityComponent, a WeightComponent, a BoundingVolumeComponent, a CollisionMeshComponent

and a few rendering components like:
a MeshComponent, a SkeletalAnimationComponent, a TweeningAnimationComponent, a TextureComponent, a ShaderComponent, a SpecialEffectsComponent

And so on....

In reality you would probably be more flexible if you allow multiple Meshes, Textures, Effects e.g. by using a MeshListComponent or something, this is just a simplification, but it's just an example.
You could also argue the BoundingVolumeComponent should be placed in the rendering component category, but in reality the distinction between rendering and physics components is not made or at least not so coarse.

Now entities can have some of those components, but of course don't have to have all of them.

Let's say entity A is a statically placed component that can't move, but in the game world it is visible, is not animated and other objects can collide with it.
Entity A has a PositionComponent, an OrientationComponent, a SizeComponent, a BoundingVolumeComponent, a CollisionMeshComponent, a MeshComponent, a TextureComponent, a ShaderComponent, a SpecialEffectsComponent

Let's say entity B is something like an Indicator on a Map or a Label, it can move, it is visible, it can be animated, it doesn't physically interact with other objects in the game world.
Entity B has a PositionComponent, an OrientationComponent, a SizeComponent, a VelocityComponent, a BoundingVolumeComponent, a MeshComponent, a TweeningAnimationComponent, a TextureComponent, a ShaderComponent, a SpecialEffectsComponent

Let's say entity C is a unit or actor of some kind, maybe player controlled, maybe ai controlled. It can move in the game world, it is visible and animatable, it can interact with the game world.
Entity C has a PositionComponent, an OrientationComponent, a SizeComponent, a VelocityComponent, a WeightComponent, a BoundingVolumeComponent, a CollisionMeshComponent, a MeshComponent, a SkeletalAnimationComponent, a TextureComponent, a ShaderComponent, a SpecialEffectsComponent

Let's say entity D is a region of the map, maybe some kind of control region. It's not visible, it doesn't really interact with the game units, but it might be used to detect if a unit is in a certain area.
Entity D has a PositionComponent, an OrientationComponent, a SizeComponent, a BoundingVolumeComponent

Now a rendering system would only iterate over the entities that have components relevant to it. We might even split the set of entities into multiple sets that are iterated seperatly.
E.g. we might first want to iterate over all visible but static entities (for example the map itself), e.g. all entities with a MeshComponent, a TextureComponent, a ShaderComponent, a PositionComponent, an OrientationComponent, a SizeComponent and a a BoundingVolumeComponent, but none with one of the animation, special effects or veloocity components. So entity A would be in the set of iterated components. The system, let's call it MapRendererSystem will use the data from within the components (that are relevant to the current task), but it won't call any methods of the entity or components, since there aren't any.
Next we might decide the units should be rendered so the rendering system for example a UnitRendererSystem would iterate over all entities that are visible, moveable and can physically interact with the game world. The systen uses the data from the appropriate components to render the units, other components are ignored.

For me the important aspect of ECS is that the entities are a collection of components, the components store the data, neither entities nor components implement logic, systems are the one place where logic is implemented.

Regarding actual implementation, there are many ways to do this.
If I would implement it myself, I would absolutly not use a shallow but wide inheritance tree and put it in a generic container, e.g. a parent class Component from which all components deriver. That's as terrible as the Java Object from which everything derivers.
In the end the system required specific components and the components are widely different from each other, a PositionComponent is different from a TextureComponent in every aspect. Any parent class Component is going to be a completly empty shell. And since the components don't implement logic they don't have any kind of interface.

Regarding interaction with a non-ECS part of the game or engine. The systems are the ones responsible, it would gather all data from the entities/components it iterated over and send them to the non-ECS-parts of your game/engine.

Advertisement

frob said:
"alice wolfraider said" …

___________

you said it as if correctly about containers and so on, but you replaced the meaning of ECS itself: that is, you could not understand what the meaning of ECS is, and then further conversations about ECS go in the same vein

___________

the point of ECS: is that you don’t need the code of a specific engine like Unity5 or UE5: you have your own game engine separately from physical engines - and here is the benefit - you can have gameplay portability to another engine or within the inside of engine

___________

creating a competitive game program requires 5-7 years of development or hundreds of developers - at least dozens of company developers - ECS requires collecting ECS itself from six months to a year, but in the end you get a reduction in development time by several times, also reducing the costs of crowds of parallel developers, and for indies this allows you to create games more personally .. but this is a long tale .. the real point is that any game with which you want to make a profit will take 10 times more effort than it seems and then only ECS:

and this is real “virtual development” as opposed to pseudo-virtual development which many are engaged in

___________

ECS provides many opportunities both in creating a virtual engine for gameplay and object ingredients and maps, etc., and in managing a real selected engine like Unity 5, which is easier in functionality than the heavy UE5, but at the same time ECS greatly increases the capabilities, and Unity 5 itself It also gives you the opportunity to use the Unity Asset Store

Alice Corp Ltd

alice wolfraider said:
the point of ECS: is that you don’t need the code of a specific engine like Unity5 or UE5: you have your own game engine separately from physical engines - and here is the benefit - you can have gameplay portability to another engine or within the inside of engine

The hell are you smoking and where can I get some of it myself? (sorry for OT but this level of gibberish just amuses me).

@Juliean

are you a programmer ? what may not be clear to you here: or you are still reading in syllables, a

non-programmer will not understand here - there is no point in trying.. it’s the same as learning to program in Unity and being an advanced programmer, and then you can understand what the point of ECS is, or understand ECS and learn to program in Unity: a paradox

___________

I hope, as usual, everyone looks from the last page and draws conclusions about of thread

______________________

press the button "go to"last comment"

Alice Corp Ltd

JoeJ said:
How was this done in C, before C++ was widely adopted?

Function pointers.

I learned on books from the 1970's and they were a common pattern. You would build a struct with a bunch of function pointers and replace them as needed.

The first time I encountered it I remember because it felt weird to me, it was a menu system. Knowing what I know today it's a very direct solution, you're providing a pointer to the ExecuteCommand function, but having a menu system that had structures where you needed to fill in 4 function pointers was unexpected to me.

C++ made the common practice into something more formal with a v-table, virtual functions which individual instances automatically replace. It's still a function pointer, but the double indirection is hidden behind your back because the compiler knows about it.

This topic is closed to new replies.

Advertisement