7 ms·
Where ECS gets muddy for me is when you have systems working on entities with multiple types of components at once. * physics - position component + physics co
by de_keyboard 5y ago
Where ECS gets muddy for me is when you have systems working on entities with multiple types of components at once.
* physics - position component + physics component
* rendering - position component + animation component
* etc..
How do we now store these components? How do we create / access aggregates efficiently?
If we have two arrays then there is lots of hopping around:
PositionComponent[]
PhysicsComponent[]
Maybe we need some kind of grouping class?
struct EntityData {
PositionComponent position; // Null if not a position entity
PhysicsComponent physics; // Null if not a physics entity
}
Interfaces have their issues too, but at least it's fairly clear what to do:
class BouncyBall : IHasPosition, IHasPhysics {
IPosition getPosition() {
// ...
}
IPhysics getPhysics() {
// ...
}
}
Anyone solved this before?
- royjacobs 5y agoWhat's wrong with iterating across multiple components? You seem to imply that this is "hopping around" and therefore bad, but it's perfectly acceptable to do so. Of course, if your system needs to iterate across 20 components to do its job then maybe you need to check if you've factored your components correctly.
- munificent 5y ago> What's wrong with iterating across multiple components? It's bad for spatial locality. You end up with many more CPU cache misses, which significantly slows down execution. Using the CPU cache effectively is one of the primary reasons to use ECS.
- Twisol 5y agoRecognizing your username, I'd probably best not argue, but wouldn't iterating across (say) two component arrays only cost you two pages in the cache at any given time, since you're doing a sequential scan? You should have the same number of cache misses overall, unless you're doing something very complicated for each entity to cause the cache to vacate one of the pages. Of course, if you access N components you need N pages in the cache concurrently, which is going to fall over for a not-too-large N. But N=2 or N=3 seems unlikely to kill spatial locality. I can imagine it gets a little more complicated with prefetch, but you're still using the prefetched pages -- you just need to prefetch pages for two separate arrays (potentially at different rates based on component size) rather than one. Do these details end up snowballing in a way I'm not seeing, or are there details I'm just missing outright?
- munificent 5y ago> two component arrays only cost you two pages in the cache at any given time It sounds like you're thinking of virtual memory (i.e. pages of memory either being in RAM or on disk). But CPU caching is about the much smaller L1, L2, and L3 caches directly on the chip itself. Let's say you have two kinds of components, A and B. You have those stored in two contiguous arrays: AAAAAAAAAAAAAAAAAAA... BBBBBBBBBBBBBBBBBBB... Each entity has an A and B and your system needs to access both of those to do its work. The code will look like: for each entity: access some data component A access some data component B do some computation On the first access of A, the A component for that entity and a bunch of subsequent As for other entities get loaded into the cache line. On the next access of B, the B for that entity along with a bunch of subsequent Bs gets loaded into a cache line. If you are lucky the A and B arrays will be at addresses such that the chip is able to put them in different cache lines. At that point, I think you'll mostly be OK. But if you're unlucky and they are fighting for the same cache line, then each access can end up evicting the previous one and forcing a main memory look up for every single component.
- quotemstr 5y agoYou should be able to use some kind of coloring approach to avoid that kind of false sharing, right?
- gugagore 5y agoI don't think this is false sharing since the issue can occur without any writes.
- munificent 5y agoI'm not an expert at the details of hardware architecture, but my understanding is that you're basically stuck with whatever associativity and cache policy the chip supports. That, and the addresses that your components happen to be at, will determine whether they end up fighting over cache lines or not. https://en.wikipedia.org/wiki/Cache_placement_policies https://en.wikipedia.org/wiki/Cache_placement_policies
- nikki93 5y agoIt depends on the storage pattern: archetype storages may actually keep those components together / interspersed anyway, or hybrid things like the "groups" in entt. It does, generally speaking, just seem to give you an opportunity to see this issue re: cache misses arise in practice and rearrange your storage accordingly, by decoupling your processing logic (body of a query) from the actual layout. Esp. if the ECS provides a switch like "store A and B interspersed in a single array" that you can enable or disable at any point and profile both ways (part of the data-oriented design idea: orient your data for how it's used in practice).
- royjacobs 5y agoYes, that was the point I was trying to make (also in my other comment). It's certainly bad, but if you iterate across, say, two components it shouldn't be too bad? It's also an option to have your component data interleaved, if you know the iteration usage upfront, I suppose.
- jayd16 5y agoIt's bad compared to what? If you have one system that needs to iterate through A and B and another that need to iterate through B and C, what is a more ideal system?
- deleted 5y ago[deleted]
- throwaway13337 5y agoPosition is usually an entity level property. But, assuming it isn't, you would make that portion of things it's own component/trait of the entity. Components that rely on it could be declared to require it (in Unity, there is a RequireComponent annotation). So you can be sure if that component exists, that its required component also exists on the entity. I think this is a reasonably satisfying solution.
- jcelerier 5y ago> Position is usually an entity level property. that's a very very video game centric point of view. If a pattern only works in a couple fields of application, it's not a very good pattern..
- adamrezich 5y ago...for anything other than the field where the pattern makes sense, of course
- void_mint 5y agoThis is a dogmatic take. From wikipedia > Entity–component–system (ECS) is a software architectural pattern that is mostly used in video game development.
- munificent 5y ago> that's a very very video game centric point of view. ECS was invented for and is primarily used by videogames. > If a pattern only works in a couple fields of application, it's not a very good pattern. I completely and totally disagree. How would you even define a "field of application" without there being patterns and practices that are unique to it? If every domain uses the same techniques, what is the difference? Off the top of my head, here are some patterns that I rarely see outside of their primary domain: Programming languages and compilers: * Recursive descent * Top-down operator parsing * The visitor pattern * Mark-sweep garbage collection and the tri-color abstraction Game and simulation programming: * ECS * Per-frame arena allocators * Game loops * Double buffering * Spatial partitioning
- meheleventyone 5y agoThis is why the ECS pattern isn’t actually as performant as people make out. At least by default. I wrote this explanation of the archetype approach to making an ECS fast a while ago: This is partly why a lot of ECS demos have a lot of homogeneous elements (they share all components in common). For example particle systems have long been written in a data oriented manner when running on the CPU. So if you implement it in the ECS style you can just run through the arrays in order and its all good. Or Unity's city sim example. But games tend to have much more heterogeneous entities (they share less or few components in common). The most obvious example I can think of to dispel the myth of ECS's inherent DoDness is an ECS wherein each component storage is a linked list with each element individually allocated. Even iterating through the homogeneous entity example is likely to be extremely slow in comparison to flat arrays. So there is nothing about the pattern that demands it be implemented in a data-oriented manner. But back to a more heterogeneous example. I'm going to try to explain it generally because I think a worked version would be enormous and maybe cloud things more? Typically component storage is indexed by the entity ID. You want to look up the component in the storage associated with a particular ID. If all your storages are flat arrays where the entity ID is just an index into the array the more heterogeneous your entities the more gaps you will have to iterate over and correspondingly more memory your game will take up. This isn't great for cache locality or memory usage and we have to iterate over every entity for all systems to find the valid ones. So the next step uses a dense array and a secondary backing array that is indexed by the entity id. So we can keep our components packed nicely but still look them up easily. Instead of iterating over all the entities for every system we can find the shortest component storage for the set of components the system uses and iterate directly over that and lookup the other components in their storages by the current entity ID. Now we iterate over potentially many fewer entities but essentially do a random lookup into the other component storages for each one. So we're introducing cache misses for the benefit of less things to iterate over. So what we want is the benefits of blazing through arrays without the downsides of them being pretty sparse and ideally minimizing cache misses. Which is why the concept of an Archetype was invented. If we keep our components in flat arrays but crucially change our storage so we're not keeping flat arrays of every component but keeping separate component storages for each archetype of entity we have right now. Going from: AAAAAAAAAA BBBBBBBBBB CCCCCCCCCC To: (ABC) A B C (AB) AAA BBB (AC) AAAAA CCCCC (C) CCCCC If we have a system that just iterates C's it can find all the archetype storages and iterate straight through the C array for them one by one. So ideally we only pay a cache miss when we change archetype, have good cache locality and are iterating the minimum set. Similarly a system that uses components A and C will only iterate the archetype storage of ABC and AC and blaze straight through the A and C arrays of each. Same deal. This comes at a cost of making adding and removing components from an entity more expensive. We're also ignoring interacting with other components or the world and how that might work. For example we might want to do damage to another entity entirely. Or we might want to look up the properties of the piece of ground we're stood on. So there is a whole other layer of places we can ruin all this good work by wanting to access stuff pretty randomly. Relationships in games tend to be spatial and stuff tends to move around so it's hard to see a general case solution to the problem. Then there is other axis to think on like ease of creating the game, how flexible it is to change the game, iteration speed, designer friendliness and so on. Rarely IME has the gameplay code itself been the bottleneck outside of stupid mistakes. In games this level of optimization is really great when you do have a big mostly homogenous set of things. Then it's well worth the time to structure your data for efficient memory access. City Sims, games like Factorio and so on are classic examples.”
- sparkie 5y agoThere's a fairly recent work called SHAPES[1] which attempts to address this kind of customized memory layout without having to give up the OOP abstraction. You can try out different memory layouts without having to modify the types themselves. [1]:https://www.doc.ic.ac.uk/%7Escd/ShapesOnwards.pdf; https://www.doc.ic.ac.uk/%7Escd/ShapesOnwards.pdf; A more recent revision of the work here: https://www.researchgate.net/publication/341693673_Reshape_your_layouts_not_your_programs_A_safe_language_extension_for_better_cache_locality https://www.researchgate.net/publication/341693673_Reshape_y...
- quotemstr 5y agoFlexibility with object layout is one of the big potential unexploited advantages of managed code systems. Automatically "column-izing" large collections of objects ought to be in the wheelhouse of sufficiently clever JVM and CLR implementations, but this is a very under-explored line of research.
- jayd16 5y agoUnity seems to store things by unique combination of components. In your case they would have an arrays for entities with a position component and a physics component, and then an array of entities with a position component and an animation component, and possibly an array for entities with components that have all three. Unity then schedules work for your system by passing all the relevant arrays. Described in detail here: https://docs.unity3d.com/Packages/com.unity.entities@0.17/manual/ecs_core.html https://docs.unity3d.com/Packages/com.unity.entities@0.17/ma...
- michannne 5y agoTypically, component is just some POCO class and systems iterate over combinations of these components. > if we have two arrays then there is lots of hopping around Why? You can just create a custom allocation scheme assigning one giant chunk of memory to all components and give each system a custom iterator that iterates accordingly for the alignment of components it cares about
- BulgarianIdiot 5y agoThis is not specific to ECS, but comes down to the "single controller" (or single writer, single owner etc. many names) problem. Ideally you want to have one modifier/controller, but you can have as many readers as you want. When you can't have a single controller, you have several options: 1. Pass ownership. Animated components control position only by animation. Physics components control position only by physics. You can pass this control in time from physics to animation and back. 2. Express one through the other. In this case, express animation as acting on physics constraints, and let the physics engine compute the final position. This way animation becomes just another "physical force" in your game. It can be hard to do sophisticated animation this way though. 3. Have physics-specific position and animation-specific position and have the final position be computed as a formula of both. Maybe you sum them. So either one that moves from a base offset, impacts the position. This depends on what the position is of.
- viktorcode 5y agoI didn't find a perfect solution, but it goes like this: it doesn't matter how many components an entity has, as long as all components are stored in corresponding arrays. So, you have positions array, velocity array, etc. Those arrays contain the data ideal for consumption by corresponding systems. The problem here lies in linking those separate components (i.e. indices in arrays) to entities.
- throw149102 5y agoWhat you're looking for is "Arrays of Structs of Arrays". See: https://en.wikipedia.org/wiki/AoS_and_SoA https://en.wikipedia.org/wiki/AoS_and_SoA Jonathan Blow has a good talk about it here: https://www.youtube.com/watch?v=YGTZr6bmNmk https://www.youtube.com/watch?v=YGTZr6bmNmk
- resonantjacket5 5y agoUsually you'd have the "physics" system hold onto the array while the EntityData would hold a pointer to the PhysicsComponent for that Entity. That way each entity can access it's data quickly, and if there is some heavy physics computation it can easily iterate it in a list (aka checking for collisions).
- danbolt 5y agoEnTT's registry might be an interesting read if you haven't read it before. [1] The specs crate also provides a variety of storage implementations for varying types of components. [2] I didn't work on the game, but I spoke with some of the developers of Homeworld: Deserts of Kharak. Since there was a straightforward quantity of entities and components (a bunch of vehicles in a closed desert space), the space for all data was preallocated at initialization time. I can't speak further on the specifics though. [1] https://github.com/skypjack/entt/blob/master/docs/md/entity.md#the-registry-the-entity-and-the-component https://github.com/skypjack/entt/blob/master/docs/md/entity.... [2] https://docs.rs/specs/0.17.0/specs/struct.VecStorage.html https://docs.rs/specs/0.17.0/specs/struct.VecStorage.html
- beiller 5y agoI solved it by duplicating the data. Because a physics object when created needs a starting position. And sometimes you need to 'reset' the position and just having one position variable won't allow that. The rendering logic checks if it has a physics state and if not use the other position etc.
- dkersten 5y ago> Anyone solved this before? Sure. Take a look at EnTT[1], a popular C++ ECS library. It comes with two main tools to deal with this: Sorting[2] and groups[3]. EnTT gives you a large spectrum of tools with different trade-offs so that you can tune your code based on usage patterns. Obviously different bits of code will have conflicting access patterns, so there's no one-size-fits-all solution, but EnTT lets you optimise the patterns that are most important to you (based on profiling, hopefully). [1] https://github.com/skypjack/entt https://github.com/skypjack/entt [2] Sort one component to be in the same order as another component, so that they can be efficiently accessed together: https://github.com/skypjack/entt/wiki/Crash-Course:-entity-component-system#sorting-is-it-possible https://github.com/skypjack/entt/wiki/Crash-Course:-entity-c... [3] https://github.com/skypjack/entt/wiki/Crash-Course:-entity-component-system#groups https://github.com/skypjack/entt/wiki/Crash-Course:-entity-c...