Jump to content
  • Advertisement
Sign in to follow this  
Robrock55

Implementing ECS to a Pong-like - Design questions

This topic is 701 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Howdy all

 

I'm currently trying to create a full ECS game by cloning Pong. As the game is more than simple, I'm trying to implement ECS fully and to the extremes.

?I'm working in Java, it's the language I know the most and I'm not currently looking for performances. I'm not looking for technical informations but for Design informations.

 

  • ?Currently, I defined a Entity class, with only an ID as field.
  • I've got my different components for Position, Velocity, CollisionZone, Render. Each time a component is created, it's registering itself to a HashMap, with the Entity ID as key (I stored the HashMap in a static way to each Component class)
  • 2 systems up to now, a physics one and a renderer.

 

My game loop is set as I want in my main method, getting Physics update in the frequency I want and rendering when possible. The renderer is working as expected and I'm finishing to write the Physics system. However I already have questions at this point that I would like to sort before going further:

 

  • The first one is quite simple. Do I get ECS right ? Does my implementation seems correct?
  • Currently, all my systems are located in my game loop. However I believe that in more complex games, some of them may be called following an observer or an event (currently thinking of death event, end of levels etc.). Would it be a good idea to have them stored in a static and public way on my main class?
  • All my systems seems to be based on a loop of components. I'm looping around all RenderComponent for my RendererSystem, and looping around VelocityComponent for my PhysicsSystem. Do all systems are based on one or multiple ComponentLibrary looping?
  • Finally, and this may go to an other topic, do you suggest to split Physics and Collision? They seems strongly linked but I really want to split them to stay ECS. One of my idea was to have 3 systems: Pre-CollisionPhysics, CollisionDetection, Post-CollisionPhysics. However, it looks odd to me

 

Thanks in advance for your answers

 

Robin

Share this post


Link to post
Share on other sites
Advertisement

Do I get ECS right ? Does my implementation seems correct?

 

There's no such thing as 'right', as there's no legal definition of what 'ECS' means. The way you are working does appear to match the way that some people like to do them.

 

Would it be a good idea to have them stored in a static and public way on my main class?

 

Static members are often an indication of bad code. If your events/observers need to call back into a system, there's no reason you can't just tell the event which system it will need to use at the time that the event is created.

 

Do all systems are based on one or multiple ComponentLibrary looping?

 

When systems map 1-to-1 with component types, then it's common to have an array/vector of the component type, which the system iterates over.

 

When systems map 1-to-many with components, it usually makes no sense to iterate like that; what use does iterating over Position components do, when there's no logic or behaviour inherent to Position alone? Often each system has a list of components that it requires in order to operate, and it will iterate over each entity which has all the required components. How to store this efficiently is a bit trickier because it's not trivial to see which Position components correspond to a Velocity and which ones don't (for example).

Share this post


Link to post
Share on other sites
Do I get ECS right ? Does my implementation seems correct?

 

ECS can be done for 2 basic reasons: 

 

1. the ability to define new entity types from pre-defined component types without having to recompile.   the is the usual reason.

 

2. as a last ditch optimization of update, once you've squeezed all you can out of render, ans still can't hit the publishers required minimum FPS.

 

reasons side, the idea behind ECS as a software architecture method is that games do different things with different sets of data at different times.  so you split things up based on what you do with which sets of data. so its a type of data oriented design. ECS's are almost always data driven as well, loading entity definitions in from a data file, which is where you get the no-recompile capability from, 

 

for render, you need info about what to draw (graphics info) and where to draw (location and orientation).

 

for input or AI moves, you can usually immediately process the input or AI output. even though its technically the move part of update. so input or  AI moves can handle the update of location and oritentation, and attack status and such. usually immediately - with no need for messaging systems. messaging systems are typically required for things like deferred processing, and asynchronous inter-process communication between distributed systems.

 

and update moves everything that input and AI didn't. 

 

moving will require collision checks, which means a collision map (some sort of representation of the collide-able objects in the scene). and things like locations, and radii for bboxes or b-spheres, etc for entities.

 

things like ambient audio will require things like location and radius.

 

so you start listing alt these things you have to do in the game, and what data they need or affect. hopefully you find some commonalities.

 

from the example above, location, or perhaps location and  orientation, or maybe even location, orientation and radius might be good candidates for a "component".

 

the thing to think about is when processing a list of components, the layout of the data in the cache. this will define what components need what vars. 

 

and yes, if you do ECS as data oriented optimzation correctly, you'll end up with a bunch of arrays of structs and for loops. and you wont use hash tables, or templates or anything fancy that cost more clock cycyles, cause you're already having to get cache friendly to run fast enough.

 

note that nothing says you can't compose components from smaller sub-components with redundant data for greater cache friendliness. if you need drawing and location and orientation as the component data for a render, it may be faster to store location, orientation, and drawing info as one component for rendering, and orientation and location as a separate component for movement. at that point is all about cache line size and cache misses. optimization no programmer wants to have to deal with.

 

pong is about right-sized for extremes ECS experiments. 

Edited by Norman Barrows

Share this post


Link to post
Share on other sites

First of all, thanks to both of you for your answers :)

 

There's no such thing as 'right', as there's no legal definition of what 'ECS' means. The way you are working does appear to match the way that some people like to do them.

I didn't precise in my post but this is how I understood it. There's no "official" ECS. However, not being wrong and matching a certain way of ECS is what I intended -> Understand the pattern

 

Static members are often an indication of bad code. If your events/observers need to call back into a system, there's no reason you can't just tell the event which system it will need to use at the time that the event is created.

There's no reason I can't I agree. Still, I would like to understand why static members means bad code ? My school seems to praise static members and I always worked with them (probably laziness I would admit  ^_^ ).

 

it's not trivial to see which Position components correspond to a Velocity and which ones don't

and you wont use hash tables, or templates or anything fancy that cost more clock cycyles, cause you're already having to get cache friendly to run fast enough

That's why I work with HashMaps. With the Entity ID as key, I can easily get the different components of an Entity. Although, I understand that it's not performance-friendly. Is there a better way to proceed? Some implementations tended to store components inside the Entity, but this looks to me odd, as accessing different component will use instanceof instructions or so.

 

 if you need drawing and location and orientation as the component data for a render, it may be faster to store location, orientation, and drawing info as one component for rendering, and orientation and location as a separate component for movement

Storing twice the same information is typically something I would like to avoid. As I know that Rendering will need both of this information, maybe I should keep the both Component and create a "Super-Component" with both of them (simply references)? Then I won't need to use the ID to retrieve one component or an other.

Share this post


Link to post
Share on other sites

Static members are often bad because they imply a sort-of-global value or function. Globals - and sort-of-globals - make it harder to understand your program and to alter it. If system X needs to know about system Y, just pass in that information. Don't make system Y expose global stuff that anyone can use.

 

That's why I work with HashMaps. With the Entity ID as key, I can easily get the different components of an Entity. Although, I understand that it's not performance-friendly. Is there a better way to proceed?

 

Instead of an Entity ID, I like to use a memory address. That address points to a block of memory that contains all the components for that entity, stored together for convenient access. This is known as 'object-oriented programming'. ;)

 

If you want to access individual components via a hashmap like that, it will work. You just have to ask yourself what benefit you're getting. Components are most useful when they can all be updated in complete isolation, each by a single piece of code. You might consider merging some components to make this more practical.

 

Maybe someone else who likes component-based systems can give you more advice.

Share this post


Link to post
Share on other sites
That's why I work with HashMaps. With the Entity ID as key, I can easily get the different components of an Entity. Although, I understand that it's not performance-friendly. Is there a better way to proceed? Some implementations tended to store components inside the Entity, but this looks to me odd, as accessing different component will use instanceof instructions or so.

 

I ended up implementing a caching-system on top of everything else, for the performance-critical parts. When I update my components inside a system, the call will look something like this:

for(auto entity : m_pEntities->EntitiesWithComponents<Animation, Sprite>())
{
    auto& animation = entity.Get<Animation>();
    auto& sprite = entity.Get<Sprite>();
    
    // perform some logic
}

The primitive implementation just iterated over all entities, returned a list of all entities matching the requested components, and then you would use a hash-map access to get the component from the entity. This was generally fast enough, but I found some real performance gains from implementing a (conceptually simple, but implementation-wise complex caching system:

 

For a list of requested component-types, if there is no cache bucket, you generate one. You do this by once again iterating over all entities, and checking if their components match. The cache-bucket is special data-structure, that stores a flat array of pointers to components, grouped by entities:

class CacheBucket
{
public:
    
     // only exposes iterators for the internal array

private:
    array<pair<Animation*, Sprite*>> vCompoments;
};

At least thats what it is compiled to, given a set of componen-types. I'm using heavy template-magic to make this work, IDK how you would do something like this in java.

 

The idea still applies, that you want to generate a linear array of component-pairs/tuples, that you can iterate over without any lookups. So you only pay the cost for resolving the lookups once, then everytime the same set of components is requested afterwards, you get linear, cache-friendly iteration.

 

There are of course a few gotchas - you have to update the cache-buckets when entities change their state. If an entity is deleted you have to remove it, if an entity has its components attached/removed you have to account for it, etc... The primitive approach for this is to simply invalidate all cache-buckets that match the modified entities component list, and recreate them accordingly. This is the simpliest implementation, but potentially slow if entities are frequently modified on a structural level.

A more complex but faster alternative performs action-dependant updating of cache-buckets, ie. after an entity is created and has been initialized, I lookup all cache-buckets that it can fit into, and insert it at the end.

The actual performance-gains when I implemented the caching were actually impressing, I got a significant reduction in cache-misses and a ~4x frametime-reduction when rendering 10k entities with 5 different component-combinations to the screen.

 

 

Now the key point is - all of this is merely an interal optimization. The user code does not care whether I cache the components are not. I could revert to lookup-based access by just supplying a different iterable-datastructure, without modifying the systems code. I also did a second optimzation by storing all components in a linear memory area instead of creating them all on the heap, but again this had no impact on the ECS interface.

(ok, I had to modify it when I first introduced caching, but simply because I had a bad interface to entity-access before).

 

Hope that gives you an idea on how to remove the performance-overhead from lookups/entity-aquisation.

(All this has been done in C++, sorry I cannot give you a more java-esque reply :( )

Edited by Juliean

Share this post


Link to post
Share on other sites

In other words, you work hard to store no pointers to components in the actual Entity class, but then you basically have to replicate that via the caching system anyway.

 

Seems to me you'd do just as well implementing an Entity as an object holding pointers to components. If the Entities are stored contiguously and the Components are stored contiguously, both sorted by entity ID, then I think it would work just as well and be a fair bit simpler.

Share this post


Link to post
Share on other sites
In other words, you work hard to store no pointers to components in the actual Entity class, but then you basically have to replicate that via the caching system anyway. Seems to me you'd do just as well implementing an Entity as an object holding pointers to components. If the Entities are stored contiguously and the Components are stored contiguously, both sorted by entity ID, then I think it would work just as well and be a fair bit simpler.

 

What you describe is what I had before. An entity would store a vector (and later a map) of components. I actually had some reasons for going for the cache-system:

 

1) I need to cache the entities for a set of components anyways, as iterating over all entities and checking their bitsets is utterly wasteful. So storing the components alongside was actually just an addition to that.

2) If I store a pointer to the components inside the entity, I still have an aweful lot of indirections/cache-misses, as I try to access the component. My cache-system gives me a 1:1 mapping from component-type to access-slot (via tuple-type-matching), inside one block of memory.

How would you achieve something similar if the entity stores its components? Any way I can think of, you have to do a search, be it in a vector or a map, which is way slower than having the direct cache-friendly lookup in the cache.

3) Using the cache lets me bypass the entity-object entirely for all systems (its actually merely used for scripting single entity behaviour and editor-stuff now; and there is a method to access components directly, just that it makes a lookup inside the component-allocator instead, meaning there is not a single vector/map per entity).

4) Using the cache lets me (optionally) cache different entity-related objects as well. For example, I ended up with another type of component called "data", which is basically a component that every entity will automatically have (yeah that came from a real evaluation of the workflow using the ECS). Now this data is its own class with an entirely different interface, so I cannot reuse the same code and storage as for regular components; thus it would also require separate storage in the entity. Also theres other implementation-specific things like a script-reference object... the key point is, the cache lets a system request all the data it requires, and pack it tightly in one data-structure, and not have them accessed in different ways from the entity.

 

I hope this gives a bit more sense to the design I ended up with; which I can arguably see would sound a bitl convoluted at first glance.

 

I would say that additional complexity certainly pays off for me, but as I mentioned, this is merely an internal characteristic of my current implementation, the interface to the systems code should be agnostic of it. I achieve this via a specified iterator-interface, and whether I return an iterator to a cache-block, or an entity-block where the component is accessed from the entity itself, doesn't really matter for how you use the system.

And if I want to OP to take away anything from this, then its that: Don't design your ECS around specific performance characteristics or storage details, those should be design-choices you make under the hood after you've evaluated what is best based on your requirements.

Edited by Juliean

Share this post


Link to post
Share on other sites

, I understand that it's not performance-friendly.

 

ECS as an update() optimization method is the only reason to use fastest possible data and code.    for all other applications of  ECS, any code and data that is "fast enough" will do.

Share this post


Link to post
Share on other sites

Storing twice the same information is typically something I would like to avoid.

 

 as do we all.  but remember, ram is cheap, cache memory is not. if keeping two copies in ram means you can pack more instances of all data required into the cache at once, it may be faster.   IE just the data to render, and just the data to move, even though they may both store location and orientation. but these are optimizations. rather premature at the initial design phase.   just start with each var as component,  then start combining once you get that working first.  design is an iterative process. design - implement - test - feedback (test results) - re-design - etc - etc.   as you build your first prototype, patterns will eventually emerge. patterns that you can take advantage of in your next design iteration.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!