Navigate / search

ECS ‘Hello World’

This article is an attempt at illustrating some of the basic syntax of Unity ECS by demonstrating some of the various ways Entities should be accessed and manipulated. I hope to achieve this by showing how you can rotate an object three different ways. I may look at dedicating a page to each method at a later date, but for now I am just going to briefly discuss them and provide documented source code.

Basic Chunk Iteration

The ‘basic’ way to accessing and manipulating the ComponentData attached to an Entity is by getting a Chunk array and iterating each entity within the chunk. I say basic, but compared to a method we will see later, it is actually quite verbose.

/// <summary>
/// System demonstrating chunk iteration within a <see cref="ComponentSystem"/>.
/// </summary>
public class ChunkIterationSystem : ComponentSystem
{
	private ComponentGroup query;

	/// <inheritdoc />
	protected override void OnCreateManager()
	{
		// Create our query
		this.query = this.GetComponentGroup(new EntityArchetypeQuery
		{
			All = new[] { ComponentType.Create<Rotation>(), },
		});
	}

	/// <inheritdoc />
	protected override void OnUpdate()
	{
		// Just some rotation value we will use for this demo.
		var offset = quaternion.Euler(math.radians(45) * UnityEngine.Time.deltaTime, 0, 0);

		// Gather the types of the components we want to manipulate.
		ArchetypeChunkComponentType<Rotation> rotationTypeRW = this.GetArchetypeChunkComponentType<Rotation>();

		// Get all our chunks that matches our query. This must be allocated as TempJob at the moment.
		NativeArray<ArchetypeChunk> chunks = this.query.CreateArchetypeChunkArray(Allocator.TempJob);

		// Iterate all the chunks
		for (var chunkIndex = 0; chunkIndex < chunks.Length; chunkIndex++)
		{
			ArchetypeChunk chunk = chunks[chunkIndex];

			// Get an array of the rotation components from the entities within this chunk
			NativeArray<Rotation> rotations = chunk.GetNativeArray(rotationTypeRW);

			// Iterate the entities within the chunk
			for (var index = 0; index < chunk.Count; index++)
			{
				// Get the rotation value
				Rotation rotation = rotations[index];

				// Manipulate the rotation value
				rotation.Value = math.mul(rotation.Value, offset);

				// Must apply rotation back as IComponentData is a struct
				rotations[index] = rotation;
			}
		}

		// Must dispose our native array
		chunks.Dispose();
	}
}

A main thing to note about this is that we are doing all the work within the OnUpdate method of ComponentSystem. This means it is done entirely the main thread without any parallelization.

The general use case for this is when you need to interact with systems that exist outside the Unity ECS world. For example, things such as input, physics, animations or audio that have not been converted to ECS by the Unity development team.

IJobChunk

To add parallel processing support you need to use a Job. It might seem obvious at first to use the IJobParallelFor job, but for chunk iteration this has been basically been superseded by IJobChunk. This is not to say
IJobParallelFor does not have it’s place, it’s just more for iterating arrays not chunks.

Compared to our previous example, the majority of the code is quite similar. Instead of doing our iterations in OnUpdate, we now move the code to the Execute method of our job. The actual chunk iteration is handled automatically by the system and instead we are only responsible for iterating the entities within the chunk.

/// <summary>
/// System demonstrating chunk iteration using a <see cref="IJobChunk"/>.
/// </summary>
public class JobChunkSystem : JobComponentSystem
{
	private ComponentGroup query;

	/// <inheritdoc />
	protected override void OnCreateManager()
	{
		// Create our query
		this.query = this.GetComponentGroup(new EntityArchetypeQuery
		{
			All = new[] { ComponentType.Create<Rotation>(), },
		});
	}

	/// <inheritdoc />
	protected override JobHandle OnUpdate(JobHandle handle)
	{
		// Just some rotation value we will use for this demo.
		var offset = quaternion.Euler(math.radians(45) * UnityEngine.Time.deltaTime, 0, 0);

		// Gather the types of the components we want to manipulate.
		ArchetypeChunkComponentType<Rotation> rotationTypeRW = this.GetArchetypeChunkComponentType<Rotation>();

		// Setup our job.
		var job = new RotateJob
		{
			RotationType = rotationTypeRW,
			Offset = offset,
		};

		// Instead of creating and managing the chunk array, we pass the ComponentGroup
		return job.Schedule(this.query, handle);
	}

	[BurstCompile]
	private struct RotateJob : IJobChunk
	{
		public ArchetypeChunkComponentType<Rotation> RotationType;

		public quaternion Offset;

		/// <inheritdoc />
		public void Execute(ArchetypeChunk chunk, int chunkIndex)
		{
			// Get an array of the rotation components from the entities within this chunk
			NativeArray<Rotation> rotations = chunk.GetNativeArray(this.RotationType);

			// Iterate the entities within the chunk
			for (var index = 0; index < chunk.Count; index++)
			{
				// Get the rotation value
				Rotation rotation = rotations[index];

				// Manipulate the rotation value
				rotation.Value = math.mul(rotation.Value, this.Offset);

				// Must apply rotation back as IComponentData is a struct
				rotations[index] = rotation;
			}
		}
	}
}

For IJobChunk, each chunk is processed on its own thread giving you great multi-thread performance as your entity count increases. However, you probably won’t end up using IJobChunk a lot as you will end up doing most of your parallel work with IJobProcessComponentData, which we will look at next. However there are limitations of IJobProcessComponentData, such as too many components or needing to iterating multiple chunks which will require you to fall back on IJobChunk.

IJobProcessComponentData

IJobProcessComponentData is really the primary workhorse of the Unity ECS system. You should find yourself using it more than any other method and for good reason. I’ll let the code do the talking.

/// <summary>
/// System demonstrating chunk iteration using a <see cref="IJobProcessComponentData{T}"/>.
/// </summary>
public class JobProcessComponentDataSystem : JobComponentSystem
{
	/// <inheritdoc />
	protected override JobHandle OnUpdate(JobHandle handle)
	{
		// Just some rotation value we will use for this demo.
		var offset = quaternion.Euler(math.radians(45) * UnityEngine.Time.deltaTime, 0, 0);

		// Setup our job.
		var job = new RotateJob
		{
			Offset = offset,
		};

		// Instead of creating the ComponentGroup, we pass system
		return job.Schedule(this, handle);
	}

	[BurstCompile]
	private struct RotateJob : IJobProcessComponentData<Rotation>
	{
		public quaternion Offset;

		/// <inheritdoc />
		public void Execute(ref Rotation rotation)
		{
			// Manipulate the rotation value
			rotation.Value = math.mul(rotation.Value, this.Offset);
		}
	}
}

The first thing you should notice is how much cleaner the whole system is. You no longer need to build your query, create a chunk array or get chunk component types. This is all handled for you. Under the hood, it works very similar to IJobChunk except it only presents you with the data for each Entity you want to manipulate. Each chunk (not entity) is still processed on its own thread giving you the same great performance as you scale up.