Disclaimer: I’m new to DOTS, but I had this problem so I’ve experimented a bit. However, that was on entites 0.1/0.2, so I’m not sure how it’d look in the new version.
The problem is highly dependent on your exact cast. For example, if you are doing something like Game of Life, which is a grid, the solution is fundamentally different than if you need to find ‘nearest neighbor’.
My original version, pre-DOTS, was a simple sequential solution that worked over arrays (jagged, multi, or single). Inside Unity, this capped out my frame rate very quickly… maybe 150x150 grid (IIRC).
My first ECS/DOTS removed arrays and instead created an entity for every grid/array position. Each entity had a grid position (x,y) component and also had 2-4 components that represented the neighbors. I later changed this to 3-8 for bitmasking tests. I also had current state and future state components. Then I had a system that ran for every component that had the neighbor (this eliminated edge cases - eg, 0,0 would have have no “left/down” neighbor entity at -1,0 0,-1 or -1,-1). I had 4-8 systems, depending on the case, looking at each entity that had a particular “neighbor”. Each would += a value on the entity being calculated’s future state. For example, in the bitmask example, I would have each system (8 of them this time) increment by it’s future state (1,2,4,8,16,32,64,128). I’d ensure these completed, then run a system to change the current state to the future state. For example, overwrite the current bitmask by the newly calculated bitmask. In my case, I’d also reset the future state bitmask. As an aside, I ended up doing it my way because I needed to reduce the work on one frame and was willing to process only “one direction” in a frame, something that most cases wouldn’t allow. Using dynamic buffers might have worked better otherwise. Using entities was certainly faster (by a factor of ~100) regardless.
My next, and current, solution was to use persistent native arrays. My actual case is more involved than the Game of Life “alive/dead”, but it’s similar in nature. I would use two native arrays, with the position in the array corresponding to its 2d grid position. One array holds current state, and the other array contains ‘future state’. I use a IJobParallelFor, passing in the two arrays and the width of the grid (future state = read/write, current state = read only, width of grid = read only). Using the index that is passed in, I check on each neighbour and set the future state. In my case, I would then draw the grid according to futurestate (comparing it to current state first - no change means no redraw - as I’m using unity’s tilemap), then overwrite the currentstate with futurestate.
NativeArrays works better because it can be jobified and burst - ECS actually slows things down. That applies while it is a ‘simple’ grid problem.
I tried quite a few things, including ECB, but all of it was vastly inferior to using a multi-step approach. So, for a DOTS implementation, I would start with:
- Component with current state (byte ?)
- Component with neighbor references (entities)
- Component with future state (byte ?)
Then, forall entites with a current state, pass in (Readonly)current state, (Readonly)Neighbor References, (read/write)Future State. Using IJobForEachWithEntity, you can reference the current state in neighbors via currentState[neighbourEntityReference], and write/increment the future state value (you may have to trick it with writing as futureState[entity] - the current entity index - in many cases).
Once that completes, you can run a job passing in the current and future state, and just overriding currentstate from futurestate.
However, the native array approach was easily 10x faster for me. It’s identical in principle, except you pass in the arrays instead of components, and have no need for neighbor references (but will have out of bounds safety checks in the job logic).