I tried this sample in the the documentation :
https://docs.unity3d.com/Packages/com.unity.entities.graphics@1.3/manual/runtime-entity-creation.html#example-usage
with 1 000 000 entities, with a cube mesh and a basic material (URP pipeline).
The main thread is stalled for 1.5s, on the command buffer playback function.
Is there a better way to instantiate entities to avoid main thread stalling?
The original code happens to instantiate one-by-one. ECB has an overload that allows for enqueuing an array of entities to create from one prefab, this would probably be a better thing to set up. One could pass the ECB, the prefab Entity value and an Entity array to an IJob so it can be populated off the main thread, then pass the ECB and the array to a component setup job to set properties in parallel.
Setup and playback of an ECB has overhead. In this case, with the large number of entities, it’s probably better to just perform the instantiation through EntityManager (again, with a NativeArray of entities) and set up the component values in a parallel job. The component data could then be set in an IJobFor with a ComponentLookup, with parallel execution using ScheduleParallel (IJobParallelFor is the older counterpart before Schedule/ScheduleParallel was standardized).
The system could be moved close to one of the ECB systems that’s being used in the project if adding another sync point is a concern (following best practice, it should be).
Seems intersting.
I don’t really understand how to write entity localtransform in IJobFor, using the result from the nativearray instantiate overload.
I must use a ComponentLookup to have write access? Will only work in a system?
The concept is something vaguely along these lines:
[BurstCompile]
struct ComponentWriteJob : IJobFor
{
[ReadOnly] public NativeArray<Entity> Spawned;
[NativeDisableParallelForRestriction] public ComponentLookup<MyComponent> MyComponentLookup;
public void Execute(int i)
{
MyComponentLookup[Spawned[i]] = something...;
}
}
// ISystem
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
SpawnController spawnController = ref SystemAPI.GetSingleton<SpawnController>();
if (spawnController.Count <= 0)
{
return;
}
NativeArray<Entity> spawned = state.EntityManager.Instantiate(spawnController.Prefab, spawnController.Count, state.WorldUpdateAllocator);
_myComponentLookup.Update(ref state);
state.Dependency = new ComponentWriteJob { Spawned = spawned, MyComponentLookup = _myComponentLookup }.ScheduleParallel(spawned.Length, 64, state.Dependency);
state.Dependency = spawned.Dispose(state.Dependency);
}
ComponentLookup is indeed something that is restricted to access from a system. You can rework data such that MonoBehaviours can inject things into / set values in the world that trigger systems doing things for you.
Unity has other sample code for spawning from within a system.
Thanks you, it’s better that way.
Even if there is always a stall it’s now around 200ms.
Perhaps I should dispatch less spawning over several frames.
Does ECS have a system for this kind of management?
I’m wondering if ECS is really the easiest way to do stall free large count entity streaming for an open world.
Perhaps it’s not really so much complicated to managed rendering manually with RendererBatchGroup on a custom matrix/mesh/material streamer system.
Here the spawning code :
public partial struct SpawnerSystem : ISystem
{
EntityManager entityManager;
bool done;
ComponentLookup<LocalTransform> componentLookup;
void OnCreate(ref SystemState state)
{
done = false;
state.RequireForUpdate<Components.SpawnerComponent>();
componentLookup = state.GetComponentLookup<LocalTransform>(false);
var world = World.DefaultGameObjectInjectionWorld;
entityManager = world.EntityManager;
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var spawnerComponent = SystemAPI.GetSingleton<Components.SpawnerComponent>();
if (!done && Time.timeAsDouble > spawnerComponent.Time_s)
{
done = true;
NativeArray<Entity> entities = entityManager.Instantiate(spawnerComponent.Prototype, spawnerComponent.EntityCountX * spawnerComponent.EntityCountZ, Allocator.TempJob);
componentLookup.Update(ref state);
var setupJob = new SetupJob
{
Entities = entities,
TransformAccess = componentLookup,
EntityCountX = spawnerComponent.EntityCountX,
EntityCountZ = spawnerComponent.EntityCountZ,
Width = spawnerComponent.Width
};
var jobHandle = setupJob.ScheduleParallel(spawnerComponent.EntityCountX * spawnerComponent.EntityCountZ, 128, state.Dependency);
jobHandle.Complete();
entities.Dispose();
}
}
[BurstCompile]
public struct SetupJob : IJobFor
{
[ReadOnly] public NativeArray<Entity> Entities;
[NativeDisableParallelForRestriction] public ComponentLookup<LocalTransform> TransformAccess;
public int EntityCountX;
public int EntityCountZ;
public float Width;
public void Execute(int index)
{
TransformAccess[Entities[index]] = new LocalTransform { Position = ComputePosition(index), Scale=1 };
}
public float3 ComputePosition(int index)
{
var z = index / EntityCountX;
var x = index - z * EntityCountX;
var offsetX = (EntityCountX * Width) / 2;
var offsetZ = (EntityCountZ * Width) / 2;
return new float3(x * Width - offsetX, 0.5f, z * Width - offsetZ);
}
}