Spawning Prefabs with Unity ECS

How to instantiate copies of a prefab as entities with Unity ECS.

Last updated on July 27, 2020. Created on November 7, 2019.

Video of spawning prefabs with Unity ECS.

Introduction

When thinking in terms of ECS, really there are two modes where spawning can take place:

  1. Authoring
  2. Runtime

Authoring means to, well, author stuff. That's when we're in the editor, moving things around and adjusting parameters. It also includes conversion of GameObjects into entities upon startup, a common pattern. Runtime is what happens after that. Sometimes we're mainly interested in spawning at runtime, but I'll cover spawning in the context of both modes.

Prerequisites

Before we begin, you need to install the Entities and Hybrid Renderer packages in your project. Alternatively, feel free to clone my DOTS-ready GitHub repository like so:

git clone https://github.com/reeseschultz/ReeseUnityDemos.git

Why do we need those packages? Entities includes all the core DOTS and ECS stuff, whereas the Hybrid Renderer maps between GameObjects and entities. Ensuring the mapping between GameObjects and entities is crucial for spawning prefabs, since they start out as GameObjects during authoring. Performance-wise, that's fine because our main concern is minimizing runtime costs.

Prefab Conversion and Spawning One Entity (Authoring)

For the sake of this tutorial, I'll assume each spawn subject is called a "character." To convert a character prefab into an entity, do the following:

  1. Create an empty GameObject in a scene—call it Character.

  2. Create a script called CharacterPrefab.cs.

  3. Add this code to the script, changing the namespace as you like:

using Unity.Entities;

namespace YourNamespace {
    [GenerateAuthoringComponent]
    struct CharacterPrefab : IComponentData
    {
        public Entity Value;
    }
}
  1. Drag-and-drop your prefab (not the GameObject in the scene) onto the attached script's Value field.

  2. Add the built-in Convert To Entity script to the Character GameObject (just search for it after clicking the Add Component button on the GameObject).

  3. Adjust the Convert To Entity script's Conversion Mode to Convert And Inject GameObject if you want to preserve the GameObject and any MonoBehaviours attached to it (effectively spawning one Character at authoring); otherwise, select Convert And Destroy if you only want to use the prefab as a template for spawning entities at runtime.

Spawning Entities From a Prefab (Runtime)

One can spawn multiple entities from prefabs at runtime in one of two contexts:

  1. A single-threaded context, which is accomplished with the EntityManager. What's it do? It manages entities.
  2. A multi-threaded context, where spawning can be accomplished with memory barriers and concurrent command buffers.

Single-Threaded Context Spawning

Getting the EntityManager in a MonoBehaviour. To get an EntityManager in a MonoBehaviour, inject one like so:

EntityManager entityManager => World.DefaultGameObjectInjectionWorld.EntityManager;

Then you can access it in any one of the lifecycle methods, including OnCreate, Update, and LateUpdate.

Getting the EntityManager in a system. To get an EntityManager in a SystemBase-inheriting class, just call EntityManager directly from any one of the lifecycle methods, including OnCreate, OnUpdate, and OnDestroy. How? Because EntityManager is an inherited member.

But there's a caveat. You can call the EntityManager anywhere except the interior of a job. For instance, you cannot call the EntityManager inside the Entities.ForEach lambda. Why? Because that's a multi-threaded context.

Getting the prefab with the EntityManager. To spawn "copies" of the prefab, it's as simple as calling Instantiate(prefab) on an instance of it, where prefab is either an entity converted from a prefab using the scripts discussed earlier, or it can even be a GameObject!

Multi-Threaded Context Spawning

Here I'm really talking about spawning prefabs inside the Entities.ForEach lambda. To do this, you need a memory barrier and a command buffer.

What is a memory barrier? In Unity ECS, it is a reference to the EntityCommandBufferSystem, which can queue up commands performed during parallel-executing jobs. Commands performed with the memory barrier execute deterministically, so they're played back in order to ensure thread safety.

What is a command buffer? This is the actual buffer of commands that the memory barrier enqueues. Think of it like an ordered list of commands to be executed.

Putting what we know about the memory barrier and command buffer together, you can copy-paste the following as a best-practice, repeatable pattern:

namespace YourNamespace
{
    class YourSystem : SystemBase
    {
        EntityCommandBufferSystem barrier => World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();

        protected override void OnUpdate()
        {
            var commandBuffer = barrier.CreateCommandBuffer().AsParallelWriter();

            Entities
                .ForEach((Entity entity, int entityInQueryIndex, ref CharacterPrefab prefab) =>
                {
                    if (someCondition) {
                        commandBuffer.Instantiate(entityInQueryIndex, entity);
                    }
                })
                .WithName("YourJob")
                .ScheduleParallel();

            barrier.AddJobHandleForProducer(Dependency);
        }
    }
}

A few points regarding the above code:

  1. We create a barrier from the EndSimulationEntityCommandBufferSystem. There's also a BeginSimulationEntityCommandBufferSystem. You could create your own buffer system, but generally speaking it's best to just pick one and stay consistent with it.

  2. You'll also note that we create a concurrent command buffer via ToConcurrent.

  3. Entities.ForEach may be a bit semantically misleading in this instance, since we're assuming the query only returns one "prefab" entity from which we can create more. We could write a loop inside the lambda to instantiate some number of entities from a given prefab.

  4. According to the docs, the magic entityInQueryIndex should "be used as the jobIndex for adding commands to a concurrent EntityCommandBuffer," typically used instead of the comparable nativeThreadIndex.

  5. We use barrier.AddJobHandleForProducer(job) to inform the memory barrier that our job produces, as in creates entities—always do this or else you can end up with weird, otherwise inexplicable bugs.

If any of that seems too complicated, it is—that's why I created a package to simplify prefab spawning.

Try My DOTS Spawning Package!

What if you want to spawn during authoring or runtime in an easy, understandable, and consistent way from a system, or even upon button click in a MonoBehaviour? You know, like this:

using Reese.Spawning;
using Unity.Entities;
using UnityEngine;

namespace YourNamespace {
    class CharacterSpawner : MonoBehaviour
    {
        // Get the default world:
        EntityManager entityManager => World.DefaultGameObjectInjectionWorld.EntityManager;

        void Start()
        {
            // Get the prefab entity:
            prefabEntity = entityManager.CreateEntityQuery(typeof(CharacterPrefab)).GetSingleton<CharacterPrefab>().Value;

            // Enqueue spawning (SpawnSystem and Spawn are from Reese.Spawning):
            SpawnSystem.Enqueue(new Spawn()
                .WithPrefab(prefabEntity) //  Optional prefab entity.
                .WithComponentList( // Optional comma-delimited list of IComponentData.
                    new YourComponent
                    {
                        Charisma = 6,
                        Intelligence = 3,
                        Luck = 14,
                        Strength = 16,
                        Wisdom = 9
                    }
                )
                .WithBufferList( // Optional comma-delimited list of IBufferElementData.
                    new YourBufferElement { }
                ),
                50 // Optional number of times to spawn (default is once).
            );
        }
    }
}

That's the power of my spawning package. If you have numerous types of prefabs you want to spawn, and don't want to write a bunch of boilerplate code to support all of them, then this might be what you need. One caveat is that my spawning package is not compatible with IL2CPP due to regrettable use of reflection, but it does make life so much easier if you don't care about that.

Installation

There's a couple ways you can install the spawning package. It's easy.

Install via OpenUPM. If you have Node.js 12 or greater, run the following to use OpenUPM:

npx openupm-cli add com.reese.spawning

Install via Git. Alternatively, just copy one of the below Git URLs:

  • HTTPS: https://github.com/reeseschultz/ReeseUnityDemos.git#spawning
  • SSH: git@github.com:reeseschultz/ReeseUnityDemos.git#spawning

Then go to Window ⇒ Package Manager in the editor. Press the + symbol in the top-left corner, and then click on Add package from git URL. Paste the text you copied and finally click Add.

I hope this helps!

© Reese Schultz

My code is released under the MIT license.