Jobs aren't always the fastest for all operations.
EntityManager operation mostly have to cause structural change on the main thread. Let's see how we could speed this up.
Bad : Schedule a job just to queue
It is true that worker threads are great but the mistake is that
EntityCommandBuffer is for remembering what to do, then work on them later. When "played back", it would be like
DestroyEntity one by one in the main thread. It is not like they are being destroyed in a job. (Pay attention to the word "command buffer". The commands are not being executed just yet.)
EntityCommandBuffer.Concurrent won't help you much either, you are just remembering what to do in parallel. (And
Concurrent blocks the other thread on write if they clashed.)
The purpose of
EntityCommandBuffer is to defer
EntityManager commands, not to speed up
EntityManager commands. And therefore this job is a complete waste of time if you schedule a job just to add/remove/delete entities via
EntityCommandBuffer. You better off just do it without the job.
So it does not matter if you scheduled
IJobChunk, or anything highly parallelized. Command buffer make them happen later. And now is better than later. (Performance-wise, sometimes you better do it in a job where something else happened)
Good : Just use
EntityManager right there
Even iterate destroying with
EntityManager directly, naively, will be faster because you didn’t schedule a job!
Be careful of invalidating the thing you are iterating on. Use
PostUpdateCommand and iterate destroy should be faster on low number of entities. Or use
ToEntityArray to make a copy of things you want to work on to avoid invalidation.
Better : Use
The strength of this method is that you can put whatever
NativeArray<Entity> that you may allocate by yourself and handpicked
Entity in them, from
EntityQuery, or from
NativeSlice subset of
public void AddComponent(NativeArray<Entity> entities, ComponentType componentType); public void CreateEntity(EntityArchetype archetype, NativeArray<Entity> entities); public void DestroyEntity(NativeArray<Entity> entities); public void Instantiate(Entity srcEntity, NativeArray<Entity> outputEntities); public void RemoveComponent(NativeArray<Entity> entities, ComponentType type);
Out of 5 methods :
Add/Remove : This is just an iteration over each one and add/remove. (not much better)
Instantiate/Create/Destroy : Looks like it can really batch create and destroy to save time!
Maybe even better : Use
But if you wanna destroy (or other EM operations) in the job for real, then use
ExclusiveEntityTransaction is like an inverse of what normally occurs. Normally to do things to
EntityManager we have to “come back” to the main thread for a moment (at
ExclusiveEntityTransaction, we can “lock the
EntityManager” for one thread to work on and prevent the main thread from using it. At the same time the main thread can go on and do other things which do not touch
So the use of
ExclusiveEntityTransaction is heavily geared towards having multiple worlds. It renders 1
World near unusable (
EntityManager became busy in-job) but your other worlds may still work on with their own
EntityManager and the remaining worker threads. Now you see something that only multiple worlds can achieve! Worker threads do work stealing automatically and is a shared resource for all
But to make multiple worlds useful to each other it would requires more careful planning how to communicate. (Which is probably by
Setting up for
ExclusiveEntityTransaction with other world doing
EntityManager.MoveEntitiesFrom from your main world
This method has
EntityQuery overload which from roughly looking, runs a job that cuts off queried chunk pointers and hand it to the 2nd
EntityManager . No copy or anything. This way you could "get rid" of entities quickly, or just setting up for
EntityQuery must be made from the world owning things you want to move!
EntityQuery is not interchangable between worlds.
Also this overload asks for entity remapper
NativeArray working space from the caller, which you can get from
EntityManager.CreateEntityRemapArray . You will get just enough size for the method to work. (Please make it from
EntityManager of the world you are moving FROM) As a bonus you get the remap result back, but it is safe to just dispose it immediately or just use
using on the
Finally when your 1st world run out of things to do unless it has the processed components, move them back. This step should be a bit difficult to determine “when”, as you don’t have
JobComponentSystem’s dep chain to manage this dependency for you. Possibly, position a
ComponentSystem conditioned with
UpdateAfter all systems which does not require the thing processing in the 2nd world. This system then
MoveEntitiesFrom , and all other systems which requires the processed entities should be conditioned with
UpdateAfter this world sync system.
The most awesome : Use the
EntityQuery overload of
ExclusiveEntityTransaction doesn’t even have this ability, so all hail main thread.
Any time you use the
EntityQuery instead of
Entity, you are doing it to everything in a whole chunk at the same time instead of to each entities one by one. Since
EntityQuery represent a chunk! Multiple chunks even, if you have so many
Entity that it spans several chunks on one
Here’s all of them you can use.
public void RemoveComponent(EntityQuery entityQuery, ComponentType componentType); public void AddComponent(EntityQuery entityQuery, ComponentType componentType); public void AddSharedComponentData<T>(EntityQuery entityQuery, T componentData) where T : struct, ISharedComponentData; public void DestroyEntity(EntityQuery entityQueryFilter);
NativeArray<Entity> overload? These are much better, since things like add and remove is really happening on the whole chunk and no one is moving, and
NativeArray way couldn't add/remove.
Add/RemoveComponent: If this add is a normal component, the chunk data layout need to be reformed to be the shape of new archetype (but at the same time, still faster than one by one). If this is a tag component, the chunk data don't need to be reformed. Almost free, just setting a new archetype.
Notice the lack of
Dataon the name
AddComponent. This is unlike the
Entityoverload where we could specify what data. This one will default the value for everything in the chunk.
AddComponentto the everything in a chunk is not the same as "chunk component", a different feature that allows you to add component to the chunk itself. (With
AddSharedComponentData: I think this is almost as fast as adding a tag component, since for shared component data no actual chunk space is needed.
DestroyEntity: You are just throwing away chunks. Should be very fast.
it even works when the
EntityQuery has been
.SetFilter -ed too. So you can even do selective mass operation based on your
SharedComponentData or your
.SetFilterChanged criteria. Just don’t forget to
.ResetFilter when you want to make the query go back to normal.
As an example, I have 10000 of things to show but only a subset (1000) of them is visible+processed at any given time (governed by
Process tag component) and this subset move forward from 0,1000, 2000, 3000, ... until the end.
So for each 1000 entities I add
ISharedComponentData with integer 1~10, when it is time to remove all
Process of the previous 1000 entities and add to the next 1000 at once, I could achieve that with 1
RemoveComponent and 1
AddComponent with filtered
EntityQuery (add filter, do remove, add new filter, do add, reset the filter if needed) instead of 2000 iterations.
Main article about SCD and its filter ability : https://gametorrahod.com/everything-about-isharedcomponentdata#filtering