Skip to content

Instantly share code, notes, and snippets.

@leventeren
Created October 10, 2023 08:01
Show Gist options
  • Save leventeren/9923a0f82c56b78b7f44736fd4eb8d37 to your computer and use it in GitHub Desktop.
Save leventeren/9923a0f82c56b78b7f44736fd4eb8d37 to your computer and use it in GitHub Desktop.
How to instantiate a prefab in ECS?
How to instantiate a prefab in ECS?
Reddit is my main social media of choice. I just really like text content more and I can choose to stay in topics or communities that I like and eventually run out of content to consume as compared to endless doom scrolling in other social media sites. Of course, my favorite subs are game development related like r/gamedev, r/Unity3D, and r/programming. One particular thread poked my interest as it is about DOTS. I don’t have the link anymore but I think it was someone showing something cool. Everybody is generally impressed about what DOTS can bring but a certain reply from that thread made me pause and think. He said something like “Yeah, DOTS is great and all but how do you spawn a prefab?” That made me chuckle because I understand what he’s talking about. Spawning an instance of a prefab is trivial in normal Unity C#. It’s still doable in DOTS but takes a few more steps. There are a lot more of these trivial stuff but hard to do in DOTS.
Unity’s DOTS is clearly not for beginners. It is aimed for experienced programmers, especially those who had an experience in native environment but then moved to Unity. They want the speed of C++ but don’t want to touch it again. They also don’t shy away from a little more verbosity and complexity because they’re used to writing that kind of code before. Be that as it may, beginners to average programmers still try DOTS probably because they like what they see but most end up frustrated because it’s just hard to use. Trivial stuff in MonoBehaviour land are not quite so in HPC# land. OOP is out the window and so is every programming pattern that revolves around it. One should forget OOP when using DOTS. So I thought why not do something about this trivial stuff that are not so straightforward to do. I figured that DOTS is still very early and the resources are still sparse. Even the low hanging fruits are not picked yet. Why not contribute to it? Even if the topic would be repeated else where, that can only be good. So allow me to tackle this trivial thing of spawning an instance of a prefab in DOTS.
So how?
I assume that you already know the fundamentals of Unity’s ECS. I should not have to explain it here since it will increase the post and it’s really out of the scope for this article. There are bunch of introductory resources out there.
What we do is we convert a prefab into an entity prefab using a Baker. An entity prefab is just an Entity really, just like how a prefab is a GameObject. Once we have this entity prefab, we can instantiate them using EntityManager.Instantiate() or EntityCommandBuffer.Instantiate(). Let’s have a simple example where we have a component where we can specify the prefab and the amount of instances to spawn. The instances would then be spawned with random position and y rotation.
First, we need the components. We need an IComponentData struct to hold the data in ECS and the authoring MonoBehaviour component so we can specify the prefab and the instance count in the editor. Here’s the code for it:
public struct Instantiator : IComponentData {
public readonly Entity entityPrefab;
public readonly int instanceCount;
public bool instantiated;
public Instantiator(Entity entityPrefab, int instanceCount) {
this.entityPrefab = entityPrefab;
this.instanceCount = instanceCount;
this.instantiated = false;
}
}
public class InstantiatorAuthoring : MonoBehaviour {
[SerializeField]
private GameObject prefab;
[SerializeField]
private int instanceCount;
private class Baker : Baker<InstantiatorAuthoring> {
public override void Bake(InstantiatorAuthoring authoring) {
Entity entityPrefab = GetEntity(authoring.prefab, TransformUsageFlags.Dynamic);
Instantiator instantiator = new(entityPrefab, authoring.instanceCount);
AddComponent(GetEntity(TransformUsageFlags.None), instantiator);
}
}
}
The struct Instantiator is the IComponentData that will hold the entity prefab and the number of instances to spawn. The variable instantiated is used to identify if instances were already instantiated for the component. We just need some way to not instantiate every frame. There other ways to do this like using IEnableableComponent or just destroy the entity holding this component so it will no longer be processed by the system that we will be using. Let’s just keep it simple for now by using a boolean flag for this example.
The class InstantiatorAuthoring is our authoring component. In DOTS lingo, the term authoring just means to have a thing in the editor such that we can setup stuff or change some variables. In most cases, an authoring component just adds a single component whose variables are taken from the editable fields of the script. It’s just a MonoBehaviour after all. In other complex cases, the authoring component may add more ECS components and could also prepare other entities.
For our example here, the authoring component has variables to hold the prefab and the instance count to spawn. The magic happens in the nested Baker class. The Baker prepares the entity when this GameObject would be converted into one. GameObjects are converted into entities when they are inside subscenes. We use the Baker to add the IComponentData components that we need for our logic. In this case, we’re just going to add the Instantiator component.
To get an entity prefab from a GameObject prefab, we use the method GetEntity(). Ignore TransformUsageFlags for now. You can look for its documentation if you want to know more but we can just ignore it for this example. If you’re not sure, just use TransformUsageFlags.Dynamic always.
Now that we have our entity prefab, we’re just going to store it in our Instantiator and add this IComponentData to the primary entity which is the converted entity where InstantiatorAuthoring was added. At this point, we now have an entity with an Instantiator component. Once we have this, we can just make a system that queries these data and do whatever we want like, you know, instantiating copies of the prefab. Let’s do exactly that next. Here’s the code:
public partial struct InstantiateSystem : ISystem {
[BurstCompile]
public void OnCreate(ref SystemState state) {
state.RequireForUpdate<Instantiator>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state) {
EndSimulationEntityCommandBufferSystem.Singleton commandBufferSystem =
SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();
// Schedule job
InstantiateJob instantiateJob = new() {
commandBuffer = commandBufferSystem.CreateCommandBuffer(state.WorldUnmanaged),
minPos = -10.0f,
maxPos = 10.0f
};
state.Dependency = instantiateJob.Schedule(state.Dependency);
}
[BurstCompile]
public void OnDestroy(ref SystemState state) {
}
[BurstCompile]
private partial struct InstantiateJob : IJobEntity {
public EntityCommandBuffer commandBuffer;
public float minPos;
public float maxPos;
public void Execute(ref Instantiator instantiator) {
if (instantiator.instantiated) {
// Already instantiated
return;
}
// Prepare random generator
Random random = new(123456);
for (int i = 0; i < instantiator.instanceCount; i++) {
Entity instance = this.commandBuffer.Instantiate(instantiator.entityPrefab);
// Random position at x and z
float3 position = new() {
x = random.NextFloat(this.minPos, this.maxPos),
z = random.NextFloat(this.minPos, this.maxPos)
};
// Random euler rotation but only at y
float3 euler = new() {
y = random.NextFloat(0.0f, 360.0f)
};
quaternion rotation = quaternion.Euler(euler);
// Set LocalTransform
this.commandBuffer.SetComponent(instance,
LocalTransform.FromPositionRotation(position, rotation));
}
// We set this to true so it will no longer be processed on the next frame
instantiator.instantiated = true;
}
}
}
I’m using a struct here that implements ISystem instead of using a class system that derives from SystemBase. I could have used the class version here but I wanted to show you the latest way of making systems in DOTS 1.o. Ideally, you have to be using this kind of system anyway. I want my readers to get used to it.
In OnCreate(), we just call SystemState.RequireForUpdate<Instantiator>(). This makes the system only execute if there are entities with Instantiator component on them. In OnUpdate(), we prepare the InstantiateJob and schedule it. The meat of the code is really in InstantiateJob.Execute(). It needs an EntityCommandBuffer which we use to instantiate copies of the prefab. It also needs a minimum and maximum positions as bounds for the random positions that we’re going to set on the instantiated copies. I’ve set it to -10 to 10 in this example.
In InstantiateJob.Execute(), we check first if Instatiator.instantiated is already true. This means that instances are already spawned for it and we should skip it. Then we prepare the random generator. There are better ways to get better seeds for the random generator but let’s just keep it simple. Then we start the loop to actually instantiate the prefab copies. We loop until instanceCount. The line
Entity instance = this.commandBuffer.Instantiate(instantiator.entityPrefab);
is equivalent to
GameObject instance = GameObject.Instantiate(prefab);
in normal Unity C#.
The next lines are preparing the random position and a random y rotation. We set these random transform values by also using the EntityCommandBuffer. Now this is important because the entity returned by EntityCommandBuffer.Instantiate() is not yet the actual entity. The actual entity is only created when the sync point system runs which is EndSimulationEntityCommandBufferSystem in our example here. Consider this as a gotcha. Any entity that was created by a EntityCommandBuffer should also be updated by it. We can’t use a ComponentLookup here since the entity instance does not exist yet.
Editor Setup
We’re done with the code. We now need some setup in the editor. We can just use the scene generated when selecting File > New Scene. What we really need to add is a subscene then add an object with the InstantiatorAuthoring. This can easily be done by right clicking on the Hierarchy window then select New Sub Scene > Empty Scene. Just name the new subscene as “Instantiator”. This creates a new scene named Instantiator but referenced as a subscene. Alternatively, you can just create a new GameObject then add the SubScene component then drag and drop a scene in its SceneAsset variable. This is useful if you already have a scene that you want to use as a subscene. Think of it as scene reuse.
Then we create an empty GameObject under the Instantiator subscene. Make sure that the subscene is opened by ticking the checkbox on its rightmost side in Hierarchy or by clicking the Open button in its Inspector. Right click on the Instantiator subscene then select GameObject > Create Empty. Name this new GameObject as “Instantiator” again (it doesn’t matter). Next we add our InstantiatorAuthoring script to this GameObject. Pick a prefab that you want instantiated on the Prefab variable and set Instance Count to 10. I’d like to use this wolf prefab from one of the assets that I’ve bought:
If you followed the instructions well, your setup should look like this:
Now we can press Play and see if instances of the prefabs are really spawned. This is how it looks like in my setup:
We can set Instance Count to 1,000 just to play around:
And there it is, a simple prefab instantiation example using DOTS. Note that ECS graphics only works for projects that were setup with URP or HDRP. What are other trivial stuff that you want to know how to do in DOTS? Let me know.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment