Hey everyone!
I’ve been silently following these forums collecting all the information I could about Unity’s ECS implementation. But every time I try to sit down and build something in pure ECS I run into roadblocks and have had to resort to hybrid ECS despite not really needing any of Unity’s default components.
I ordered these roadblocks from pettiest to most crippling, so skip to the end if you don’t have much time but want to be helpful! I would really appreciate tips to get around these issues and patch my misunderstandings!
Roadblock 1: bool and enum
I’m really only putting these here for completeness’s sake. I know how to work around them, but I have friends who would try to use these, get compile errors, follow a hybrid ECS example, see that their code works now, and then later wonder why their performance is not great.
Roadblock 2: False Sharing
If I were to only write safe C# and use Unity’s ECS APIs, would I ever still have to worry about this? If not, where is this prevented? In Burst? In IJobParallelFor? Something else? In C++ I usually have to do a bunch of compile-time magic to avoid this, so it’s a question I would rather know the answer to than wait to bite me later in development.
Roadblock 3: ChangedFilter
Most of the time when I want to use ChangedFilter, it is because either the operation should only take place if the data actually changed, or because the calculation is really heavy. But because ChangedFilter only applies to chunks, I have to keep a copy of the data from the previous frame to compare against. That by itself isn’t an issue, because I can keep an ISystemStateComponentData to track that, and I usually care about what the previous frame’s value was anyways. So for ints and floats, this is optimal. But when I need to react to a float4x4 or a struct with several float3 fields, things start to become sub-optimal.
Possible Solution?
It would be nice to have a component type that would carry an internal flag as to whether or not it was changed. It would be similar to how Hi-Z + Early-Z work in graphics land.
Roadblock 4: Sparsing
This is an issue that really only pops up due to me trying to work around other issues. But long story short, between my magic system and rendering, all my entities are scattered over many sparse chunks and now my collision detection and movement systems are running a fair bit slower.
Possible Solution?
I can think of a solution where I have entities that just have components containing sub-entities for each of the different engine systems (magic system, collision system, movement system, rendering system, ect). That way, each engine system has its own set of archetypes it can optimize for without compromising the performance of other engine systems. However, now I have a bunch of different entity IDs that are all actually the same entity. Is this the best solution?
Roadblock 5: Memcpy everywhere
So the traditional way to change the state and behavior of an entity in an ECS is to add or remove components. But every time you do this, the entity has to be copied out of the chunk into a new one. For small, lightweight games that most people have tested so far, this is plenty fast and easy. But for larger games where we could expect a couple hundred components per entity with an average size of float3 per component, now every time a new component gets added or removed, that’s over 1 kB that needs to be copied. Multiply that per system per modified entity per frame, and suddenly there’s a performance concern. I’m not sure if DynamicBuffer makes this worse.
Possible Solution?
Splitting entities the way I described earlier is one way I could solve this. But that requires good game architectural planning. Something simpler would be a hot/cold split with something like an IDynamicComponentData. One way to do this would be to create a separate chunk for these dynamic components. Since most of these components are tags anyways, copying them around would be fairly cheap. It would break vectorization as for each dynamic entity you’d have to check if the cold data entity is the same, but most likely the number of clock cycles wasted on that iteration would be less than the memcpy. I could also imagine restricting the dynamic components to just empty tags and storing them as 1 bit booleans per entity in the chunk. The masking checks could be really fast!
The killer
Roadblock 6: Dependency confusion with jobs
This one is an absolute show-stopper for me! Please help!
The problem I have is that JobComponentSystem executes in two different points in time. It executes for the setup and scheduling of the jobs, and it executes the job itself. Now each of these execution points could have dependencies on Component Systems, the setup portions of JobComponentSystems, and the actual jobs themselves. I want to specify these dependencies properly with the minimal amount of known information and code. Here’s an example case:
I need to write a ScatterMove JobComponentSystem. In the job setup on the main thread, the ScatterMove system checks a PanicTimer component on an entity that was written by a ComponentSystem. If the timer is less than 20 seconds, I schedule the ChaosScatter Job. Otherwise I schedule the SmoothScatter Job. Both of these jobs take Position components, Velocity components, and a separate NativeArray which is a lookup table used to create the scattering patterns. The Position and Velocity components were written to by other jobs earlier in the frame. The NativeArray is picked from a List of ScriptableObjects (you could also think of this as a NativeList of entities with SCDs, works the same way) based on an index calculated by the UpdateMood JobComponentSystem which uses a bunch of game statistics to eventually write a singular index on a singular component.
So the ScatterMove scheduling on the main thread requires the PanicTimer value and the Mood index, but the jobs processing the position and velocity components don’t have to be finished.
The job only needs to run after the Position and Velocity jobs are finished, but doesn’t need to lock the PanicTimer or the Mood components while it is processing.
How would I schedule this? And would I have to know exactly which systems were the last to touch Mood, PanicTimer, Position, and Velocity?
Possible Solution?
If I had to build my own ECS, I probably would make it a rule that all automatic dependency tracking only worked on components. So any data shared between systems would have to be stored in components. I would need more Native types than just DynamicBuffer to be storable in components and accessible in jobs. Then I would have attributes to specify a group the ScatterMove system would run in, some ordering attributes for within the group, and then a list of component group dependencies for the scheduling as well as another list of component group dependencies for the actual job. All I would have to do is look at these dependencies at startup and build two dependency graphs. One to construct the player loop and the other to manage JobHandles.
Do any of my questions make sense? I can sketch up images and post code samples if that helps, but I didn’t want to make this initial post any longer than it already is.