Entity prefab instantiation workflow
This workflow demonstrates entity prefab instantiation in ECS. The workflow demonstrates the following concepts:
- Authoring GameObject component for controlling the instantiation using the Editor.
- Converting GameObject prefabs into ECS prefabs.
- Creating a Burst-compatible system.
- Optimizing a system to run across multiple threads.
Note
If you've followed the steps in the Authoring and baking workflow example, skip the first step that creates a subscene and start with Create a spawner entity for instantiating prefabs.
Topics in this section are workflow steps that depend on previous steps to work. If you are following along in the Editor, follow the steps in order.
- Create the subscene for the example
- Create a spawner entity for instantiating prefabs
- Create a system that instantiates prefabs
- Make the Spawner system multi-threaded
Prerequisites
This workflow requires a Unity 6 project with the following packages installed:
Create the subscene for the example
The first step in the Entity Component System (ECS) workflow is to create a subscene. ECS uses subscenes instead of scenes to manage the content for your application, because Unity's core scene system is incompatible with ECS.
To create a subscene in Unity:
- In the Editor, open an existing scene.
- In the Hierarchy, right-click and select New Sub Scene > Empty Scene.
- In the prompt that appears, enter the name for the new subscene and save it. Unity adds the subscene to the open scene and you can now use it.
Create a spawner entity for instantiating prefabs
This example creates an authoring GameObject called Spawner to provide a way to control how prefabs are instantiated from the Editor. A baker class passes the data from the Spawner to a corresponding ECS entity.
In the subscene, create a new empty GameObject called Spawner.
Create a C# script called SpawnerAuthoring.cs and replace the contents of the file with the following code:
using UnityEngine; using Unity.Entities; using Unity.Mathematics; class SpawnerAuthoring : MonoBehaviour { public GameObject Prefab; public float SpawnRate; } class SpawnerBaker : Baker<SpawnerAuthoring> { public override void Bake(SpawnerAuthoring authoring) { // This line converts the Spawner GameObject into an Entity. // TransformUsageFlags is None because the Spawner entity is not // rendered and does not need a LocalTransform component. var entity = GetEntity(TransformUsageFlags.None); AddComponent(entity, new Spawner { // This GetEntity call converts a GameObject prefab into an entity // prefab. The prefab is rendered, so it requires the standard Transform // components, that's why TransformUsageFlags is set to Dynamic. Prefab = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic), SpawnPosition = authoring.transform.position, SpawnRate = authoring.SpawnRate, NextSpawnTime = 0f }); } } public struct Spawner : IComponentData { public Entity Prefab; public float3 SpawnPosition; public float SpawnRate; // This field is used only for the multi-threading example. public float NextSpawnTime; }
The spawner entity serves as a configuration object and is not meant to be rendered, so it doesn't need the Transform components. That's why the
GetEntity
call that converts the Spawner GameObject into an entity has theTransformUsageFlags
enum set toNone
.var entity = GetEntity(TransformUsageFlags.None);
The
AddComponent
method adds the theSpawner
component, which includes thePrefab
field.The following
GetEntity
call converts a GameObject prefab into an entity prefab. The prefab represents the rendered cubes, so it requires the standard Transform components, that's why theTransformUsageFlags
enum set toDynamic
.Prefab = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic)
Create a prefab by dragging a cube with the Rotation Speed Authoring component to a folder in the Project window.
Select the Spawner GameObject. In the Spawner Authoring component, in the Prefab field, select the cube prefab.
The ECS framework converts a GameObject prefab into an entity prefab as soon as you select it in the Prefab field. To observe this behavior, do the following:
Open the Entities Hierarchy window using the menu Window > Entities > Hierarchy.
In the regular Hierarchy window, select the Spawner GameObject.
In the Entities Hierarchy window, switch to the Runtime data mode.
If the Spawner GameObject has the Cube prefab selected in the Prefab field, the Entities Hierarchy should display a view similar to this:
The screenshot displays the following:
- The regular Hierarchy window with the Spawner GameObject.
- The Inspector window in the Authoring data mode. You can edit the properties on the Spawner GameObject from the Editor.
- The Entities Hierarchy window in the Runtime data mode. In addition to the Spawner entity, this window displays the Cube entity prefab, which has the blue icon next to it. The
GetEntity
method call in the code example converts the original GameObject prefab into the entity prefab.
The next step describes how to create a system that instantiates the entity prefab.
Create a system that instantiates prefabs
This section describes how to create a system that instantiates entity prefabs and sets component data on them.
Create a new C# script called SpawnerSystem.cs
and replace the contents of the file with the following code:
using Unity.Entities;
using Unity.Transforms;
using Unity.Burst;
using Unity.Mathematics;
public partial struct SpawnerSystem : ISystem
{
private float nextSpawn;
// The Random struct is from the Unity Mathematics package, which provides types
// and functions optimized for Burst.
private Random random;
public void OnCreate(ref SystemState state)
{
// This call prevents the system from updating unless at least one entity with
// the Spawner component exists in the ECS world.
// This also prevents GetSingleton from throwing an exception if it doesn't find
// an object of type Spawner.
state.RequireForUpdate<Spawner>();
random = new Random((uint)System.DateTime.Now.Ticks);
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Use the GetSingleton method when there is only one entity of a
// specific type in the ECS world.
Spawner spawner = SystemAPI.GetSingleton<Spawner>();
if (nextSpawn < SystemAPI.Time.ElapsedTime)
{
// The Prefab field of the spawner variable contains a reference to
// the entity prefab which ECS converts during the baking stage.
Entity newEntity = state.EntityManager.Instantiate(spawner.Prefab);
float3 randomOffset = (random.NextFloat3() - 0.5f) * 10f;
randomOffset.y = 0;
float3 newPosition = spawner.SpawnPosition + randomOffset;
state.EntityManager.SetComponentData(newEntity,
LocalTransform.FromPosition(newPosition));
nextSpawn = (float)SystemAPI.Time.ElapsedTime + spawner.SpawnRate;
}
}
}
ECS systems are not attached to specific entities, which means that a system's OnUpdate
method might run before Unity loads a scene and initializes an entity that the system depends on. Executing the RequireForUpdate method ensures that the system does not run before an entity of type Spawner
is initialized in the world:
state.RequireForUpdate<Spawner>();
In this example, there is only one spawner entity in the subscene, so instead of using a query you can use the GetSingleton method to get the entity:
Spawner spawner = SystemAPI.GetSingleton<Spawner>();
The system instantiates entity prefabs using the EntityManager.Instantiate method. The Prefab
field of the spawner
variable contains a reference to the entity prefab which ECS converts on the baking stage:
Entity newEntity = state.EntityManager.Instantiate(spawner.Prefab);
To avoid instantiating entities in the same location, the example uses the SetComponentData method to set the LocalTransform
values on each new entity to a random position within a small vicinity from the spawner position:
state.EntityManager.SetComponentData(newEntity, LocalTransform.FromPosition(newPosition));
The Random method in the example is from the Unity Mathematics package. The Unity Mathematics package provides types and functions optimized for Burst.
Try the system in action
Enter Play mode. The SpawnerSystem
system starts creating instances of entity prefabs at the rate specified in the Spawn Rate property of the Spawner GameObject.
If you followed the instructions in the Authoring and baking workflow example and your project has the RotationSystem.cs
script, the prefabs should spin in the Game view.
Pause Play mode. Open the Entities Hierarchy window and switch to the Runtime data mode.
The window highlights the source entity prefab with the solid blue icon, and the instantiated entity prefabs with hollow grey icons and blue names.
Select the source entity prefab and view it in the Inspector window in the Runtime data mode. Notice that it has the Prefab tag in the Tags section. This tag excludes the source prefab from system queries that affect the instances of the prefab.
Make the Spawner system multi-threaded
This task shows you how to modify a system so that it runs jobs in parallel on multiple threads.
Note
Before you modify a system to run in parallel on multiple threads, consider whether your system affects data on enough entities to make the benefits of multi-threading exceed the overhead of scheduling the jobs. For more information, refer to Optimize systems.
This task recreates SpawnerSystem
using IJobEntity and schedules the job to run in parallel across multiple threads. Using an IJobEntity
changes how you query and iterate over component data, and changes how you instantiate new entities. For information on component data query and iteration changes due to IJobEntity
, refer to Specify a query.
Unity can only create entities on the main thread which means parallel jobs must use an entity command buffer to record commands to create and configure new entities. After the parallel job runs, Unity plays back the entity command buffer on the main thread to actually create and configure the entities. For more information, refer to Use EntityCommandBuffer in a parallel job and Deterministic playback.
Update the Spawner system
- Open the
SpawnerSystem.cs
script. - Replace the contents of the file with the below code example.
- Duplicate the Spawner GameObject in the subscene so that you have at least 300 Spawner GameObjects. This is to ensure that there are enough entities for ECS to split the work among multiple threads.
- Enter Play mode. You should see that the system behaves as it did previously. However, if you open the Profiler window, you should see that the work runs on multiple threads.
using Unity.Entities;
using Unity.Transforms;
using Unity.Burst;
using Unity.Mathematics;
[BurstCompile]
public partial struct SpawnerSystemMultithreaded : ISystem
{
private Random random;
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<Spawner>();
random = new Random((uint)System.DateTime.Now.Ticks);
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Initialize an EntityCommandBuffer to record entity operations from
// multiple threads. You can only create entities on the main thread.
// Here, we record entity operations in an entity command buffer during
// parallel job execution and play them back on the main thread to create
// and configure the entities.
EntityCommandBuffer.ParallelWriter ecb = GetEntityCommandBuffer(ref state);
// Creates a new instance of the job, assigns the necessary data, and
// schedules the job to run in parallel.
new ProcessSpawnerJob
{
ElapsedTime = SystemAPI.Time.ElapsedTime,
Ecb = ecb,
RandomSeed = random.NextUInt()
}.ScheduleParallel();
// Update the random seed for next frame
random.InitState(random.NextUInt());
}
private EntityCommandBuffer.ParallelWriter
GetEntityCommandBuffer(ref SystemState state)
{
var ecbSingleton = SystemAPI.GetSingleton
<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
return ecb.AsParallelWriter();
}
}
[BurstCompile]
public partial struct ProcessSpawnerJob : IJobEntity
{
public EntityCommandBuffer.ParallelWriter Ecb;
public double ElapsedTime;
public uint RandomSeed;
// IJobEntity generates a component data query based on the parameters of its
// Execute method. This example queries for Spawner components and uses the ref
// keyword to specify that the operation requires read and write access. Unity
// runs the code in the Execute method for each entity that matches the query.
private void Execute([ChunkIndexInQuery] int chunkIndex, ref Spawner spawner)
{
// If the next spawn time has passed.
if (spawner.NextSpawnTime < ElapsedTime)
{
// Create a deterministic random generator for this entity
var randomGenerator = new Random(RandomSeed + (uint)chunkIndex);
// Spawns a new entity and positions it at the spawner.
Entity newEntity = Ecb.Instantiate(chunkIndex, spawner.Prefab);
// Calculate random offset
float3 randomOffset = (randomGenerator.NextFloat3() - 0.5f) * 10f;
randomOffset.y = 0;
float3 newPosition = spawner.SpawnPosition + randomOffset;
Ecb.SetComponent(chunkIndex, newEntity,
LocalTransform.FromPosition(newPosition));
// Resets the next spawn time.
spawner.NextSpawnTime = (float)ElapsedTime + spawner.SpawnRate;
}
}
}
Note that the BeginSimulationEntityCommandBufferSystem
is used to create the entity command buffer. This is a system which runs at the start of the SimulationSystemGroup
, and it ensures that the entity command buffer is played back at the beginning of the Update phase of the player loop. For more information on default system groups see System groups.