Spawn and Move Prefabs

Full workflows and code to programmatically spawn, update, and destroy prefab entities

What you'll develop on this page

Use game settings to programmatically spawn asteroid prefabs to create an asteroid field (cube).

Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Spawning-Updating-and-Destroying-Asteroids

Spawning asteroid prefabs

First, some background

An entity prefab is nothing more than an entity with a Prefab tag and a LinkedEntityGroup. The former identifies the prefab and makes it invisible to all entity queries but the ones who explicitly include prefabs, and the latter links together a set of entities, since entity prefabs can be complex assemblies (equivalent to GameObject hierarchies).

So the following two components are equivalent, one in classic Unity and the other in DOTS.

// Authoring component
public class PrefabReference : MonoBehaviour
{
    public GameObject Prefab;
}

// Runtime component
public struct PrefabEntityReference : IComponentData
{
    public Entity Prefab;
}

By default, the conversion workflow only processes the actual contents of an authoring scene, so a specific mechanism is required to also include prefabs from the asset folder. This is the purpose of the system group GameObjectDeclareReferencedObjectsGroup, it runs before the primary entities are created in the destination world, and provides a way of registering prefabs for conversion.

From ECS Conversion Workflow documentation "Prefabs" section

Setting up a prefab with ECS

  • Right click in the Hierarchy, choose "3D Object" > "Sphere"

  • Drag the GameObject into the Scripts and Prefabs folder then delete the GameObject from the hierarchy (right click Delete is better then hitting the delete key)

  • Click on the sphere in the Project window, then click "Open Prefab" in the Inspector

  • Rename it to "Asteroid"

  • Adjust the scale to (2,2,2)

  • Remove the "Sphere Collider" component (click three vertical dots menu to find Remove)

    • This is a MonoBehaviour collider, and in the next section, DOTS Physics, we will replace this collider with DOTS collider

  • In Scripts and Prefabs Create "AsteroidTag" runtime component (Create > ECS > Runtime Component Type)

  • AsteroidTag.cs

using Unity.Entities;

public struct AsteroidTag : IComponentData
{
}
  • Try to put the AsteroidTag on the asteroid prefab

  • Trying to put the AsteroidTag on the asteroid prefab causes an error: "Can't add script behavior to AsteroidTag. The script needs to derive from MonoBehaviour."

    • Because the Editor is not ECS, you cannot add IComponentData to a prefab before runtime

    • Even though it "seems" like you should, it is important to remember that the Asteroid is currently a GameObject and so putting a runtime ECS component on it actually doesn't make sense, what we need to do is make AsteroidTag an "authoring component"

For simple runtime components, the GenerateAuthoringComponent attribute can be used to request the automatic creation of an authoring component for a runtime component. You can then add the script containing the runtime component directly to a GameObject within the Editor.

From ECS Conversion Workflow documentation "Generated authoring components" section

  • Add a [GenerateAuthoringComponent] decorator to the AsteroidTag

using Unity.Entities;

[GenerateAuthoringComponent]
public struct AsteroidTag : IComponentData
{
}
  • Now try to put the AsteroidTag on the Asteroid prefab again (select Asteroid in Hierarchy > Click "Add Component" in the Inspector)

  • Now we have an Asteroid entity with an AsteroidTag authoring component. This "tag" enables us to "find" asteroid entities, by checking whether or not entities have an AsteroidTag component

How do we "refer" to the "asteroid" prefab when we want to create asteroids? We need a way to "reference" this prefab when writing our ECS.

  • Create an empty GameObject in ConvertedSubScene named PrefabCollection

  • This GameObject will hold all the references to our prefabs

  • When the SubScene converts this GameObject into an entity, we will refer to the PrefabCollection entity's components to reference our prefabs

  • Create AsteroidAuthoringComponent (right click Scripts and Prefabs > Create > ECS > Authoring Component Type)

using Unity.Entities;

[GenerateAuthoringComponent]
public struct AsteroidAuthoringComponent : IComponentData
{
    public Entity Prefab;
}
  • Go back and select the PrefabCollection GameObject in Hierarchy and add AsteroidAuthoringComponent (click Add Component in Inspector)

  • Drag the asteroid prefab from the Project window into the public Prefab field under the script component in Inspector

  • Save the SubScene then click "Reimport" in Inspector to make sure assets are reimported into SubScene

  • We now have a reference to our asteroid prefab

  • We will be using the following workflow to reference prefabs for the remainder of this gitbook:

    • Create a "PrefabAuthoringComponent" with an Entity field

    • Add it to our PrefabCollection GameObject in our SubScene

    • Drag our prefab from the project window into the Entity field of the authoring component

    • We will be able to reference the authoring component to instantiate our prefabs

Creating a spawning system

  • Navigate to "Scripts and Prefabs", right click and select Create > ECS > System and name the system "AsteroidSpawnSystem"

    • "System" (the S in ECS)

  • The latest update creates a "bad" system (Unity has not updated the auto-generated systems yet) that is missing the "partial" keyword

    • In AsteroidSpawnSystem change public class AsteroidSpawnSystem : SystemBase to public partial class AsteroidSpawSystem : SystemBase

Unity ECS automatically discovers system classes in your project and instantiates them at runtime. It adds each discovered system to one of the default system groups. You can use system attributes to specify the parent group of a system and the order of that system within the group . If you do not specify a parent, Unity adds the system to the Simulation system group of the default world in a deterministic, but unspecified, order. You can also use an attribute to disable automatic creation.

A system's update loop is driven by its parent ComponentSystemGroup. A ComponentSystemGroup is, itself, a specialized kind of system that is responsible for updating its child systems. Groups can be nested. Systems derive their time data from the World they are running in; time is updated by the UpdateWorldTimeSystem.

From ECS Systems documentation

  • Hit the "play" button and check out the Systems window in our DOTS Windows

    • Our AsteroidSpawnSystem is automatically placed into the Simulation System Group

      • We are able to change which SystemGroup our system runs in by adding decorators to our system

    • Click on the AsteroidSpawnSystem and you will see that our system interacts with 2 components, Rotation and Translation

      • These are default components in the boilerplate system that are used by the EntityQuery (the EntityQuery "checks" for these components)

  • AsteroidSpawnSystem.cs 's boilerplate code is below

    • We can see from the boilerplate code that there is a reference to "Translation" and "Rotation" which was picked up in the Debugger

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
public partial class AsteroidSpawnSystem : SystemBase
{
    protected override void OnUpdate()
    {
        // Assign values to local variables captured in your job here, so that it has
        // everything it needs to do its work when it runs later.
        // For example,
        //     float deltaTime = Time.DeltaTime;
        // This declares a new kind of job, which is a unit of work to do.
        // The job is declared as an Entities.ForEach with the target components as parameters,
        // meaning it will process all entities in the world that have both
        // Translation and Rotation components. Change it to process the component
        // types you want.
        Entities.ForEach((ref Translation translation, in Rotation rotation) => {
            // Implement the work to perform for each entity here.
            // You should only access data that is local or that is a
            // field on this job. Note that the 'rotation' parameter is
            // marked as 'in', which means it cannot be modified,
            // but allows this job to run in parallel with other jobs
            // that want to read Rotation component data.
            // For example,
            //     translation.Value += math.mul(rotation.Value, new float3(0, 0, 1)) * deltaTime;
        }).Schedule();
    }
}

Unity ECS provides several types of systems. In general, the systems you write to implement your game behavior and data transformations will extend SystemBase. The other system classes have specialized purposes. You typically use existing instances of the EntityCommandBufferSystem and ComponentSystemGroup classes.

  • SystemBase -- the base class to implement when creating systems.

  • EntityCommandBufferSystem -- provides EntityCommandBuffer instances for other systems. Each of the default system groups maintains an Entity Command Buffer System at the beginning and end of its list of child systems. This allows you to group structural changes so that they incur fewer synchronization points in a frame.

  • ComponentSystemGroup -- provides nested organization and update order for other systems. Unity ECS creates several Component System Groups by default.

  • GameObjectConversionSystem -- converts GameObject-based, in-Editor representations of your game to efficient, entity-based, runtime representations. Game conversion systems run in the Unity Editor.

From ECS Systems documentation

Let's briefly take a look at the system types referenced in the documentation.

SystemBase - this is the type of system we will be using most of the time for our runtime game play. The systems of this type will do things like provide movement to asteroids or Spawn/Destroy.

ComponentSystemGroup - this is groupings of systems.

GameObjectConversionSystem - We interacted with systems of this type when working with our GameSettingsComponent and our SetGameSettingsSystem. These are used for hybrid development.

EntityCommandBufferSystem - So far we have not used this type of system. The systems in EntityCommandBufferSystem are used as "sync" points for structural changes made to entities. You can see these systems in the "Default System Groups" image above. There is a Begin{SystemGroup}EntityCommandBufferSystem and End{SystemGroup}EntityCommandBufferSystem for each of the 3 main default system groups (InitializationSystemGroup, SimulationSystemGroup, and PresentationSystemGroup). If confused about "structural" changes it is good to refer to Unity's overview of ECS concepts.

An Entity is a collection of components. Let's say there is an asteroid with a Translation component which is a float3 and denotes the asteroid's position in space. Updating the Entity's Translation component values is not a structural change. The Entity still has the same amount and types of components, just different values.

A unique combination of component types is called an EntityArchetype. For example, a 3D object might have a component for its world transform, one for its linear movement, one for rotation, and one for its visual representation. Each instance of one of these 3D objects corresponds to a single entity, but because they share the same set of components, ECS classifies them as a single archetype:

In this diagram, entities A and B share archetype M, while entity C has archetype N.

To smoothly change the archetype of an entity, you can add or remove components at runtime. For example, if you remove the Renderer component from entity B, it then moves to archetype N.

From Core ECS Overview

Let's say we add a "DestroyTag" to the asteroid Entity. That will actually "change" the EntityArchetype it is (because it has a different collection of components). Structural changes are computationally more expensive than just updating values.

Sync points are caused by operations that you cannot safely perform when there are any other jobs that operate on components. Structural changes to the data in ECS are the primary cause of sync points. All of the following are structural changes:

  • Creating entities

  • Deleting entities

  • Adding components to an entity

  • Removing components from an entity

  • Changing the value of shared components

Broadly speaking, any operation that changes the archetype of an entity or causes the order of entities within a chunk to change is a structural change. These structural changes can only be performed on the main thread.

Structural changes not only require a sync point, but they also invalidate all direct references to any component data. This includes instances of DynamicBuffer and the result of methods that provide direct access to the components such as ComponentSystemBase.GetComponentDataFromEntity.

You can use entity command buffers (ECBs) to queue up structural changes instead of immediately performing them. Commands stored in an ECB can be played back at a later point during the frame. This reduces multiple sync points spread across the frame to a single sync point when the ECB is played back.

Each of the standard ComponentSystemGroup instances provides a EntityCommandBufferSystem as the first and last systems updated in the group. By getting an ECB object from one of these standard ECB systems, all structural changes within the group occur at the same point in the frame, resulting in one sync point rather than several. ECBs also allow you to record structural changes within a job. Without an ECB, you can only make structural changes on the main thread. (Even on the main thread, it is typically faster to record commands in an ECB and then play back those commands, than it is to make the structural changes one-by-one using the EntityManager class itself.)

If you cannot use an EntityCommandBufferSystem for a task, try to group any systems that make structural changes together in the system execution order. Two systems that both make structural changes only incur one sync point if they update sequentially.

From ECS Sync points

If sync points and EntityCommandBuffers are making your head hurt, you should really watch Unity's talk on Entity Command Buffers: https://www.youtube.com/watch?v=SecJibpoTYw <-- Worth the watch!

We will be using the BeginSimulationEntityCommandBuffer in our AsteroidSpawnSystem. This command buffer plays back our structural changes (creating asteroid entities).

We will use a Job.WithCode() to generate our asteroids.

The Job.WithCode construction provided by the SystemBase class is an easy way to run a function as a single background job. You can also run Job.WithCode on the main thread and still take advantage of Burst compilation to speed up execution.

You cannot pass parameters to the Job.WithCode lambda function or return a value. Instead, you can capture local variables in your OnUpdate() function.

When you schedule your job to run in the C# Job System using Schedule(), there are additional restrictions:

  • Captured variables must be declared as NativeArray -- or other native container -- or a blittable type.

  • To return data, you must write the return value to a captured native array, even if the data is a single value. (Note that you can write to any captured variable when executing with Run().)

Job.WithCode provides a set of functions to apply read-only and safety attributes to your captured native container variables. For example, you can use WithReadOnly to designate that you don't update the container and WithDisposeOnCompletion to automatically dispose a container after the job finishes. (Entities.ForEach provides the same functions.)

See Job.WithCode for more information about these modifiers and attributes.

From ECS Using Job.WithCode documentation

Think of Job.WithCode as a super-hardcore ECS "for loop" in an Update() function.

  • Make AsteroidSpawnSystem and paste this code snippet into AsteroidSpawnSystem.cs:

using System.Diagnostics;
using Unity.Entities;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
using Unity.Burst;

public partial class AsteroidSpawnSystem : SystemBase
{
    //This will be our query for Asteroids
    private EntityQuery m_AsteroidQuery;

    //We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
    private BeginSimulationEntityCommandBufferSystem m_BeginSimECB;

    //This will be our query to find GameSettingsComponent data to know how many and where to spawn Asteroids
    private EntityQuery m_GameSettingsQuery;

    //This will save our Asteroid prefab to be used to spawn Asteroids
    private Entity m_Prefab;

    protected override void OnCreate()
    {
        //This is an EntityQuery for our Asteroids, they must have an AsteroidTag
        m_AsteroidQuery = GetEntityQuery(ComponentType.ReadWrite<AsteroidTag>());

        //This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
        m_BeginSimECB = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();

        //This is an EntityQuery for the GameSettingsComponent which will drive how many Asteroids we spawn
        m_GameSettingsQuery = GetEntityQuery(ComponentType.ReadWrite<GameSettingsComponent>());

        //This says "do not go to the OnUpdate method until an entity exists that meets this query"
        //We are using GameObjectConversion to create our GameSettingsComponent so we need to make sure 
        //The conversion process is complete before continuing
        RequireForUpdate(m_GameSettingsQuery);
    }
    
    protected override void OnUpdate()
    {
        //Here we set the prefab we will use
        if (m_Prefab == Entity.Null)
        {
            //We grab the converted PrefabCollection Entity's AsteroidAuthoringComponent
            //and set m_Prefab to its Prefab value
            m_Prefab = GetSingleton<AsteroidAuthoringComponent>().Prefab;

            //we must "return" after setting this prefab because if we were to continue into the Job
            //we would run into errors because the variable was JUST set (ECS funny business)
            //comment out return and see the error
            return;
        }

        //Because of how ECS works we must declare local variables that will be used within the job
        //You cannot "GetSingleton<GameSettingsComponent>()" from within the job, must be declared outside
        var settings = GetSingleton<GameSettingsComponent>();

        //Here we create our commandBuffer where we will "record" our structural changes (creating an Asteroid)
        var commandBuffer = m_BeginSimECB.CreateCommandBuffer();

        //This provides the current amount of Asteroids in the EntityQuery
        var count = m_AsteroidQuery.CalculateEntityCountWithoutFiltering();

        //We must declare our prefab as a local variable (ECS funny business)
        var asteroidPrefab = m_Prefab;

        //We will use this to generate random positions
        var rand = new Unity.Mathematics.Random((uint)Stopwatch.GetTimestamp());

        Job
        .WithCode(() => {
            for (int i = count; i < settings.numAsteroids; ++i)
            {
                // this is how much within perimeter asteroids start
                var padding = 0.1f;

                // we are going to have the asteroids start on the perimeter of the level
                // choose the x, y, z coordinate of perimeter
                // so the x value must be from negative levelWidth/2 to positive levelWidth/2 (within padding)
                var xPosition = rand.NextFloat(-1f*((settings.levelWidth)/2-padding), (settings.levelWidth)/2-padding);
                // so the y value must be from negative levelHeight/2 to positive levelHeight/2 (within padding)
                var yPosition = rand.NextFloat(-1f*((settings.levelHeight)/2-padding), (settings.levelHeight)/2-padding);
                // so the z value must be from negative levelDepth/2 to positive levelDepth/2 (within padding)
                var zPosition = rand.NextFloat(-1f*((settings.levelDepth)/2-padding), (settings.levelDepth)/2-padding);
                
                //We now have xPosition, yPostiion, zPosition in the necessary range
                //With "chooseFace" we will decide which face of the cube the Asteroid will spawn on
                var chooseFace = rand.NextFloat(0,6);
                
                //Based on what face was chosen, we x, y or z to a perimeter value
                //(not important to learn ECS, just a way to make an interesting prespawned shape)
                if (chooseFace < 1) {xPosition = -1*((settings.levelWidth)/2-padding);}
                else if (chooseFace < 2) {xPosition = (settings.levelWidth)/2-padding;}
                else if (chooseFace < 3) {yPosition = -1*((settings.levelHeight)/2-padding);}
                else if (chooseFace < 4) {yPosition = (settings.levelHeight)/2-padding;}
                else if (chooseFace < 5) {zPosition = -1*((settings.levelDepth)/2-padding);}
                else if (chooseFace < 6) {zPosition = (settings.levelDepth)/2-padding;}

                //we then create a new translation component with the randomly generated x, y, and z values                
                var pos = new Translation{Value = new float3(xPosition, yPosition, zPosition)};

                //on our command buffer we record creating an entity from our Asteroid prefab
                var e = commandBuffer.Instantiate(asteroidPrefab);

                //we then set the Translation component of the Asteroid prefab equal to our new translation component
                commandBuffer.SetComponent(e, pos);
            }
        }).Schedule();

        //This will add our dependency to be played back on the BeginSimulationEntityCommandBuffer
        m_BeginSimECB.AddJobHandleForProducer(Dependency);
    }
}
  • Read through the comments to get a line-by-line understanding of what is going on

Important Note: A quirk of ECS is that you must declare local variables in the OnUpdate method if you want to use that data in a Job that runs in OnUpdate().

This means that if you want to use certain variables in your OnUpdate(), and you already declared them in your OnCreate(), you must still also declare variables locally in OnUpdate().

Is there any point to declaring variables in OnCreate()? Yes, there is! If you grab the variable from OnCreate(), you only reach "out" once during runtime. But, if you are calling for the value from OnUpdate(), that means you reach "out" for the local variable on each OnUpdate().

  • You'll notice the general set-up of an ECS system is broken down into two major parts:

    • OnCreate

      • EntityQueries, CommandBuffers, RequireForUpdates

    • OnUpdate

      • Setup your local variables

      • Run your Job.WithCode() or Entities.ForEach()

OnCreate

  • Here we set up EntityQueries on the entities and components we need

    • We will need to know how many Asteroids exist so we can spawn the right amount

    • We need data from our GameSettingsComponent, so we create a query and see if it exists before we continue to our OnUpdate

  • Here we also set up our EntityCommandBuffer which will be used to record structural changes

OnUpdate

  • Here we set our prefab by grabbing the data from our AsteroidAuthoringComponent

  • We declare the local variables we will need for our Job.WithCode()

  • We create a Translation that is on the perimeter of a cube defined by our levelHeight, levelWidth, and levelDepth in our GameSettingsComponent

  • We instantiate a new entity from our prefab and set its Translation component to our translation

Important note: The reason why we are able to easily pass our GameSettingsComponent data into the Job.WithCode() is because the entity with that data is a Singleton. A Singleton is an entity that is the only entity that exists with this particular component.

What if we wanted to pass in PlayerComponent data to the Job.WithCode() and there could be multiple players active during the OnUpdate()?

In this case you would use:

m_PlayersQuery = GetEntityQuery(ComponentType.ReadWrite<PlayerComponent>()); JobHandle playersDep; var players = m_PlayersQuery.ToComponentDataArrayAsync(Allocator.TempJob, out playersDep);

  • This creates a NativeArray of Player component data

  • This array can now be referenced within a Job

  • Now there is an additional dependency to create that NativeArray which also must be disposed, this is where it gets tricky

  • You will need to combine dependencies to ensure there are no race conditions

Using NativeArrays will be discussed in future sections. So why are we confusing you by bringing up this additional information that isn't needed right now? That's because introducing future workflows to you now might help you build a better mental model of how to navigate ECS.

You might have noticed our AsteroidSpawnSystem ends with a .Schedule() which means we are running a a single job. A great benefit of Unity DOTS is the ability to schedule parallel jobs to make the most of the targeting compute platform. If we wanted to make AsteroidSpawnSystem run parallel jobs we would need to use an IJobFor.

Note: To run a parallel job, implement IJobFor, which you can schedule using ScheduleParallel() in the system OnUpdate() function.

We do not use an IJobFor here because it is a bit more involved than a Job.WithCode(), and we are just starting to warm up to ECS! If you are feeling adventurous, you can implement AsteroidJobSystem with an IJobFor and checkout the performance difference between single jobs and parallel jobs for spawning asteroids. If you have interesting results, please reach out to the Moetsi team either on our Discord or emailing us at olenka@moetsi.com and we will include it in this gitbook!

  • Hit the "play" button and see the spawned asteroids

    • Not working? Reimport the ConvertedSubScene if there are issues grabbing the AsteroidAuthoringComponent Singleton

  • Checkout the DOTS Hierarchy to see all the newly created entities

But where are all our asteroids?

  • We can see from the DOTS Hierarchy that the Entities are there but the Translation values of the asteroids are verrrrrry far apart, so we cannot see them and so we need to update our GameSettings

  • Let's go to our ConvertedSubScene in the Hierarchy, select Game Settings, and change our levelWidth, levelHeight, and levelDepth to 20 and change the number of asteroids to 20,000 in the Inspector

But we still can't see the asteroids even now when they are closer together, WHAT GIVES?!

The issue is that we are still using the "built in" render pipeline, rather than the Universal Render Pipeline (URP). Unity does not automatically change over. Following Unity's docs let's create a Universal Render Pipeline Asset and then Add the Asset to your Graphics Settings

  • Creating the Universal Render Pipeline Asset

    The Universal Render Pipeline Asset controls the global rendering and quality settings of your Project, and creates the rendering pipeline instance. The rendering pipeline instance contains intermediate resources and the render pipeline implementation.

    To create a Universal Render Pipeline Asset:

    1. In the Editor, go to the Project window.

    2. Right-click in the Project window in Assets/, and select Create > Rendering > Universal Render Pipeline > Pipeline Asset. Alternatively, navigate to the menu bar at the top, and select Assets > Create > Rendering > Universal Render Pipeline > Pipeline Asset (Forward Renderer).

    You can either leave the default name for the new Universal Render Pipeline Asset, or type a new one.

  • Adding the Asset to your Graphics settings

    To use URP, you need to add the newly created Universal Render Pipeline Asset to your Graphics settings in Unity. If you don't, Unity still tries to use the Built-in render pipeline.

    To add the Universal Render Pipeline Asset to your Graphics settings:

    1. Navigate to Edit > Project Settings... > Graphics.

    2. In the Scriptable Render Pipeline Settings field, add the Universal Render Pipeline Asset you created earlier. When you add the Universal Render Pipeline Asset, the available Graphics settings immediately change. Your Project is now using URP.

    From Install URP Into A Project

  • Now let's hit play and see those Asteroids!

Why can't we see them?! It is because we are still using a Material from the built in render pipeline. Let's make a material that is compatible with Universal Render Pipeline.

  • Right click in Assets/ and Prefabs and choose Material and call it GreyMaterial

  • Choose the base map to be a Grey Color

  • Now let's add it to our AsteroidPrefab

  • Now let's hit play

  • Woah. That is way too many 😬

  • Let's change the number of Asteroids to 200

  • This looks more like an asteroid field ☑️

We now have spawned an Asteroid prefab (entity prefab) using ECS

  • we created an Asteroid prefab

  • we created a AsteroidTag authoring component

  • We placed the AsteroidTag on the prefab

  • We created AsteroidSpawnSystem which spawns Asteroids in a cube of a given height, width and depth

Many people come to ECS looking to increase the performance of their games and tools. While what you've built so far is a toy example, take a second to play around with the asteroid counts.

Push the asteroid counts as high as your machine can handle. Get comfortable with exploring which Systems carry the most load.

You've taken your first steps in high concurrency ECS systems! Congratulations!

Adding movement to prefabs

  • Create a Velocity component (right click Scripts and Prefabs > Create > ECS > Authoring Component Type, you get the hang of this now, right?)

  • We will add this to our asteroid prefab and update the AsteroidSpawnSystem to set the Velocity component on our asteroid entity when instantiating

    • Just a heads up: we're going to shake things up a bit in the next section, where we use Unity Physics for velocity instead

  • paste the code snippet below into VelocityComponent.cs:

using Unity.Entities;
using Unity.Mathematics;

[GenerateAuthoringComponent]
public struct VelocityComponent : IComponentData
{
    public float3 Value;
}
  • Then add the VelocityComponent to the asteroid prefab

    • We can add the VelocityComponent to the prefab because it has the [GenerateAuthoringComponent] decorator

  • We must now update our AsteroidSpawnSystem to initialize the asteroids VelocityComponent

  • Add the below code snippet to the end of the Job.WithCode() code block (just before the closing curly brace) in AsteroidSpawnSystem.cs:


                //We will now set the VelocityComponent of our asteroids
                //here we generate a random Vector3 with x, y and z between -1 and 1
                var randomVel = new Vector3(rand.NextFloat(-1f, 1f), rand.NextFloat(-1f, 1f), rand.NextFloat(-1f, 1f));
                //next we normalize it so it has a magnitude of 1
                randomVel.Normalize();
                //now we set the magnitude equal to the game settings
                randomVel = randomVel * settings.asteroidVelocity;
                //here we create a new VelocityComponent with the velocity data
                var vel = new VelocityComponent{Value = new float3(randomVel.x, randomVel.y, randomVel.z)};
                //now we set the velocity component in our asteroid prefab
                commandBuffer.SetComponent(e, vel);
  • Reimport ConvertedSubScene, hit "play", then go checkout the Entity list of asteroids in the DOTS Hierarchy, now with their new VelocityComponents

  • Now we need to make a system to update Translation values based on VelocityComponent data

  • Create MovementSystem in Scripts and Prefabs (Create > ECS > System)

  • Create MovementSystem and paste this code snippet into MovementSystem.cs:

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
using Unity.Burst;

public partial class MovementSystem : SystemBase
{

    protected override void OnUpdate()
    {
        var deltaTime = Time.DeltaTime;
        Entities
        .ForEach((ref Translation position, in VelocityComponent velocity) =>
        {
            position.Value.xyz += velocity.Value * deltaTime;
        }).ScheduleParallel();
    }
}
  • Notice that this system is much simpler than AsteroidSpawningSystem

  • This is because there are no structural changes to be made and we do not need to use any data beyond the data that is already on the entities we want to update

    • All we want to do in MovementSystem is read data from one component (Velocity), and use it to adjust the data in another component (Translation)

Use the Entities.ForEach construction provided by the SystemBase class as a concise way to define and execute your algorithms over entities and their components. Entities.ForEach executes a lambda function you define over all the entities selected by an entity query.

To execute a job lambda function, you either schedule the job using Schedule() and ScheduleParallel(), or execute it immediately (on the main thread) with Run(). You can use additional methods defined on Entities.ForEach to set the entity query as well as various job options.

...

When you define the lambda function to use with Entities.ForEach, you can declare parameters that the SystemBase class uses to pass in information about the current entity when it executes the function.

A typical lambda function looks like:

Entities.ForEach(
    (Entity entity,
        int entityInQueryIndex,
        ref Translation translation,
        in Movement move) => { /* .. */})

By default, you can pass up to eight parameters to an Entities.ForEach lambda function. (If you need to pass more parameters, you can define a custom delegate.) When using the standard delegates, you must group the parameters in the following order:

  1. Parameters passed-by-value first (no parameter modifiers)

  2. Writable parameters second (ref parameter modifier)

  3. Read-only parameters last (in parameter modifier)

All components should use either the ref or the in parameter modifier keywords. Otherwise, the component struct passed to your function is a copy instead of a reference. This means an extra memory copy for read-only parameters and means that any changes to components you intended to update are silently thrown when the copied struct goes out of scope after the function returns.

From ECS Using Entities.ForEach documentation

  • Our MovementSystem queries all entities that have both a Translation and a Velocity component

    • We put "ref Translation position" because we will be writing to the Translation component

    • We put "in VelocityComponent velocity" because we are only reading from the Velocity component

  • Wait, where is this "Burst" we have been hearing so much about?

    • Job.WithCode() and Entities.ForEach() are automatically Burst compiled (woo!)

    • Sometimes you must specify .WithoutBurst() for certain workflows (we will see this later)

    • Burst documentation if you want to read up.

You can execute the lambda function on the main thread using Run(), as a single job using Schedule(), or as a parallel job using ScheduleParallel(). These different execution methods have different constraints on how you access data. In addition, Burst uses a restricted subset of the C# language, so you need to specify WithoutBurst() when using C# features outside this subset (including accessing managed types).

From ECS Using Entities.ForEach documentation

  • If possible use .ScheduleParallel() to make the most of Unity ECS

    • That is the whole point of doing things in this new data-oriented way, performance improvements! 🚀

    • If you're curious, try and create the same AsteroidSpawnSystem using MonoBehaviours to see the performance difference

    • Sometimes you will not be able to use Burst, Schedule(), or ScheduleParallel() based on your workflow (limitations of the technology)

The following table shows which features are currently supported in Entities.ForEach for the different methods of scheduling available in SystemBase:

Supported FeatureRunScheduleScheduleParallel

Capture local value type

x

x

x

Capture local reference type

x (only WithoutBurst and not in ISystem)

Writing to captured variables

x

Use field on the system class

x (only WithoutBurst)

Methods on reference types

x (only WithoutBurst and not in ISystem)

Shared Components

x (only WithoutBurst and not in ISystem)

Managed Components

x (only WithoutBurst and not in ISystem)

Structural changes

x (only WithStructuralChanges and not in ISystem)

SystemBase.GetComponent

x

x

x

SystemBase.SetComponent

x

x

GetComponentDataFromEntity

x

x

x (only as ReadOnly)

HasComponent

x

x

x

WithDisposeOnCompletion

x

x

x

WithScheduleGranularity

x

Note: WithStructuralChanges() will disable Burst. Do not use this option if you want to achieve high levels of performance in your Entities.ForEach (instead use an EntityCommandBuffer).

An Entities.ForEach construction uses Roslyn source generators to translate the code you write for the construction into correct ECS code. This translation allows you to express the intent of your algorithm without having to include complex, boilerplate code. However, it can mean that some common ways of writing code are not allowed.

The following features are not currently supported:

Unsupported Feature

Dynamic code in .With invocations

SharedComponent parameters by ref

Nested Entities.ForEach lambda expressions

Calling with delegate stored in variable, field or by method

SetComponent with lambda parameter type

GetComponent with writable lambda parameter

Generic parameters in lambdas

In systems with generic parameters

From ECS Using Entities.ForEach documentation

  • Hit "play" and see the asteroids move in their random velocities

  • Go to the GameSettings in ConvertedSubScene and adjust the Asteroid Velocity to 2 to slow down the asteroids and make them more Space-like

  • Reimport the Sub Scene, hit "play", and checkout the Entity Debugger to see how the asteroids' Translation values are changed in real-time

We now have asteroids prefabs moving in random directions

  • We created a VelocityComponent and added it to the asteroid prefab

  • We added to the AsteroidSpawnSystem to set random velocities on the asteroids

  • We created MovementSystem to take any entity with a Translation and VelocityComponent and adjust the Translation value using the VelocityComponent

Destroying Prefabs

  • We are going to destroy any asteroid prefabs that leave the perimeter of our cube

  • We will create an AsteroidsOutOfBoundsSystem that adds a DestroyTag to any asteroid that leaves the cube perimeter

  • Create DestroyTag and paste this code snippet into DestroyTag.cs:

using Unity.Entities;

public struct DestroyTag : IComponentData
{
}
  • Notice we did not add [GenerateAuthoringComponent] to this tag

    • This is because this is data we add at runtime not at authoring time

  • Now let's create the AsteroidsOutOfBoundsSystem that will add the DestroyTag to any asteroid that leaves the cube's perimeter

  • Create AsteroidsOutOfBoundsSystem and paste this code snippet into AsteroidsOutOfBoundsSystem.cs:

using Unity.Burst;
using Unity.Entities;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Jobs;
using Unity.Transforms;
using UnityEngine;
//We are adding this system within the FixedStepSimulationGroup
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateBefore(typeof(EndFixedStepSimulationEntityCommandBufferSystem))] 
public partial class AsteroidsOutOfBoundsSystem : SystemBase
{
    //We are going to use the EndFixedStepSimECB
    //This is because when we use Unity Physics our physics will run in the FixedStepSimulationSystem
    //We are dipping our toes into placing our systems in specific system groups
    //The FixedStepSimGroup has its own EntityCommandBufferSystem we will use to make the structural change
    //of adding the DestroyTag
    private EndFixedStepSimulationEntityCommandBufferSystem m_EndFixedStepSimECB;
    protected override void OnCreate()
    {
        //We grab the EndFixedStepSimECB for our OnUpdate
        m_EndFixedStepSimECB = World.GetOrCreateSystem<EndFixedStepSimulationEntityCommandBufferSystem>();
        //We want to make sure we don't update until we have our GameSettingsComponent
        //because we need the data from this component to know where the perimeter of our cube is
        RequireSingletonForUpdate<GameSettingsComponent>();
    }

    protected override void OnUpdate()
    {
        //We want to run this as parallel jobs so we need to add "AsParallelWriter" when creating
        //our command buffer
        var commandBuffer = m_EndFixedStepSimECB.CreateCommandBuffer().AsParallelWriter();
        //We must declare our local variables that we will use in our job
        var settings = GetSingleton<GameSettingsComponent>();
        //This time we query entities with components by using "WithAll" tag
        //This makes sure that we only grab entities with an AsteroidTag component so we don't affect other entities
        //that might have passed the perimeter of the cube  
        Entities
        .WithAll<AsteroidTag>()
        .ForEach((Entity entity, int entityInQueryIndex, in Translation position) =>
        {
            //We check if the current Translation value is out of bounds
            if (Mathf.Abs(position.Value.x) > settings.levelWidth/2 ||
                Mathf.Abs(position.Value.y) > settings.levelHeight/2 ||
                Mathf.Abs(position.Value.z) > settings.levelDepth/2)
            {
                //If it is out of bounds wee add the DestroyTag component to the entity and return
                commandBuffer.AddComponent(entityInQueryIndex, entity, new DestroyTag());
                return;
            }
        }).ScheduleParallel();
        //We add the dependencies to the CommandBuffer that will be playing back these structural changes (adding a DestroyTag)
        m_EndFixedStepSimECB.AddJobHandleForProducer(Dependency);
    }
}
  • Notice we are placing this system in a specific SystemGroup, the FixedStepSimulationGroup

    • We want to make sure it updates before the EndFixedStepSimulationEntityCommandBufferSystem because the latter is where recorded structural changes will playback

    • We want to start getting comfortable with placing systems in different groups and utilizing different EntityCommandBufferSystems because not only is it important in ECS in general, but also because we will be using Unity Physics in the next section (which is run in the FixedStepSimulationGroup)

  • We use .WithAll<AsteroidTag>() to query all entities with an AsteroidTag

  • We also needed to include "int entityInQueryIndex" in our .ForEach() because when running parallel jobs, we must include the entityInQueryIndexwhen making our changes. It is needed for parallel jobs to work!

    • commandBuffer.AddComponent(entityInQueryIndex, entity, new DestroyTag());

  • int entityInQueryIndex — the index of the entity in the list of all entities selected by the query. Use the entity index value when you have a native array that you need to fill with a unique value for each entity. You can use the entityInQueryIndex as the index in that array. The entityInQueryIndex should also be used as the sortKey for adding commands to a concurrent EntityCommandBuffer.

From Special, named parameters

  • Now we need to create a system that destroys any asteroids with a DestroyTag

  • Create AsteroidsDestructionSystem and paste this code snippet into AsteroidsDestructionSystem.cs:

using Unity.Burst;
using Unity.Entities;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Jobs;
using Unity.Transforms;
using UnityEngine;

//We are going to update LATE once all other systems are complete
//because we don't want to destroy the Entity before other systems have
//had a chance to interact with it if they need to
[UpdateInGroup(typeof(LateSimulationSystemGroup))]
public partial class AsteroidsDestructionSystem : SystemBase
{
    private EndSimulationEntityCommandBufferSystem m_EndSimEcb;    

    protected override void OnCreate()
    {
        //We grab the EndSimulationEntityCommandBufferSystem to record our structural changes
        m_EndSimEcb = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    protected override void OnUpdate()
    {
        //We add "AsParallelWriter" when we create our command buffer because we want
        //to run our jobs in parallel
        var commandBuffer = m_EndSimEcb.CreateCommandBuffer().AsParallelWriter();

        //We now any entities with a DestroyTag and an AsteroidTag
        //We could just query for a DestroyTag, but we might want to run different processes
        //if different entities are destroyed, so we made this one specifically for Asteroids
        Entities
        .WithAll<DestroyTag, AsteroidTag>()
        .ForEach((Entity entity, int entityInQueryIndex) =>
        {
            commandBuffer.DestroyEntity(entityInQueryIndex, entity);

        }).ScheduleParallel();

        //We then add the dependencies of these jobs to the EndSimulationEntityCOmmandBufferSystem
        //that will be playing back the structural changes recorded in this sytem
        m_EndSimEcb.AddJobHandleForProducer(Dependency);
    
    }
}
  • We run this system in the LateSimulationSystemGroup

    • This way all systems are able to interact with the entity before we delete it

  • We will use the LateSimulationSystemGroup's CommandBufferSystem to playback our recorded structural changes (destroying the asteroid entity)

  • Hit "play" and see how the asteroids are destroyed when they pass the perimeter and how the AsteroidSpawnSystem creates new asteroids

  • Check out the DOTS Hierarchy and see the AsteroidPrefabs get created and destroyed

Celebrate your win; Congrats!

You just made a giant cloud of asteroids! Congratulations, you've taken your first steps in high concurrency ECS systems!

Many come to ECS looking to increase the performance of their games and tools. While what you've built so far is a toy example, take a second to play around with the asteroid counts. Push the asteroid counts as high as your machine can handle. Get comfortable with exploring which Systems carry the most load.

We now have an asteroid field

  • We created a DestroyTag which is added by the AsteroidsOutOfBoundsSystem when an asteroid leaves the cube perimeter

  • We created AsteroidsDestructionSystem that destroys asteroids with a destroy tag

Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Spawning-Updating-and-Destroying-Asteroids

git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Spawning-Updating-and-Destroying-Asteroids'

Last updated