Just thought I would share my project here for anyone that might be interested to learn or just getting started with Unity or ECS. It’s a fairly simply project based on the original Roll-a-ball tutorial by Unity. I plan on keeping it updated as best as possible with new features and updates that will continue to roll out.
Also, is the ConvertToEntity workflow the optimal approach for instantiating new entities now? Previously I would extract the mesh data from an object in the scene, destroy it, and then instantiate a new entity with that mesh.
Mainly just posting to share. If anyone has any comments or feedback for improvement, I would really appreciate it! Always looking to learn more and improve.
Nice, that will definitly help some people out there
Just some minor comments:
I would recommend showing how to use chunk iteration rather than using ToComponentDataArray(). While ToComponentDataArray seems simpler, it can have important performance impact once the game grows a lot (which I experienced recently). So starting with chunk iteration directly when learning seems better. My new moto is: if you access only one component, then use ToComponentDataArray, otherwise use chunks.
Also when retrieving chunks, or when retrieving components, it seems like a best practice to use the overload that returns a job handle and pass it as additional dependency to the Schedule function:
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
NativeArray<ScoreBox> scoreBox = ScoreBoxGroup.ToComponentDataArray<ScoreBox>(Allocator.TempJob, out var handle);
var collisionJobHandle = new CollisionJob
{
ScoreBoxes = scoreBox,
CommandBuffer = m_EntityCommandBufferSystem.CreateCommandBuffer(),
}.Schedule(this, JobHandle.CombineDependencies(inputDeps, handle));
// Pass final handle to barrier system to ensure dependency completion
// Tell the barrier system which job needs to be completed before the commands can be played back
m_EntityCommandBufferSystem.AddJobHandleForProducer(collisionJobHandle);
return collisionJobHandle;
}
This avoid to keep the main thread waiting for ToComponentDataArray to finish before scheduling the job (and thus, there is some time wasted while you could have scheduled some other jobs from other systems).
You should use the function that doesn’t return a job handle only if you plan to access the array from the system (not the job). Or you could call complete() on the returned handle.
This slightly add some complexity but it seems like good habits to have since the beginning.
Thanks a lot for your feedback! I really appreciate it.
I was able to make the changes for adding the dependencies for the job, but I’m not sure how to go about changing this job system for chunk iteration. I’m checking the positions of two different entities for collision, and then destroying one if they collide and adding to the player entity’s score.
Perhaps I need to separate this logic into two different systems? Or maybe I’m just not thinking about it clearly. I want to do it in a way that leaves room for the future possibility of there being more than one player entity. Right now it’s assumed there’s only one.
My current confusion with chunk iteration for this system is how do I differentiate between the Translation of the ScoreBox and Player entities. They both have the Translation type, but they don’t share ScoreBox or Player types.
That would be something along these lines (didn’t try to compile so there might be some minor mistakes):
using Unity.Entities;
using Unity.Transforms;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Burst;
namespace SimpleECS
{
/*
* Utilizes C# Job System to process collisions between Player and ScoreBox entities.
* Creating and removing entities can only be done inside the main thread.
* This sytem uses an EntityCommandBuffer to handle tasks that can't be completed inside Jobs.
*/
public class CollisionSystem : JobComponentSystem
{
// Define a ComponentGroup for ScoreBox entities
EntityQuery ScoreBoxGroup;
// BeginInitializationEntityCommandBufferSystem is used to create a command buffer that will be played back when the barreir system executes.
BeginInitializationEntityCommandBufferSystem m_EntityCommandBufferSystem;
protected override void OnCreate()
{
m_EntityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
// Query for ScoreBoxes with following components
EntityQueryDesc scoreBoxQuery = new EntityQueryDesc
{
All = new ComponentType[] { typeof(ScoreBox), typeof(Translation) }
};
// Get the ComponentGroup
ScoreBoxGroup = GetEntityQuery(scoreBoxQuery);
}
[BurstCompile]
struct CollisionJob : IJobForEach<Player, Translation>
{
// Access to the EntityCommandBuffer to Destroy entity
[ReadOnly] public EntityCommandBuffer CommandBuffer;
[DeallocateOnJobCompletion] [ReadOnly] public NativeArray<ArchetypeChunk> scorebox_chunks;
[ReadOnly] public ArchetypeChunkComponentType<Translation> translation_type;
[ReadOnly] public ArchetypeChunkComponentType<ScoreBox> score_type;
public void Execute(ref Player player, [ReadOnly] ref Translation position)
{
for (int i = 0; i < scorebox_chunks.Length; i++)
{
var chunk = scorebox_chunks[i];
var translations = chunk.GetNativeArray(translation_type);
var scores = chunk.GetNativeArray(score_type);
for (int j = 0; j < scores.Length; j++)
{
// Calculate the distance between the ScoreBox and Player
float dist = math.distance(position.Value, translations[j].Value);
// If close enough for collision, add to the score and destroy the entity
if (dist < 2.0f)
{
player.Score += 1;
CommandBuffer.DestroyEntity(scores[j].entity);
}
}
}
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var translation_type = GetArchetypeChunkComponentType<Translation>(true);
var score_type = GetArchetypeChunkComponentType<ScoreBox>(true);
var chunks = player_group.CreateArchetypeChunkArray(Allocator.TempJob, out var handle);
var collisionJobHandle = new CollisionJob
{
CommandBuffer = m_EntityCommandBufferSystem.CreateCommandBuffer(),
score_type = score_type,
translation_type = translation_type,
chunks = chunks
}.Schedule(this, JobHandle.CombineDependencies(inputDeps, handle));
// Pass final handle to barrier system to ensure dependency completion
// Tell the barrier system which job needs to be completed before the commands can be played back
m_EntityCommandBufferSystem.AddJobHandleForProducer(collisionJobHandle);
return collisionJobHandle;
}
}
}
Also, I am not sure why you would need the “entity” field in the ScoreBox component.
With the “ToCOmponentArray” approach, you also have “ToEntityArray” which gves the list of entities, and with chunks you can use ArchetypeChunkEntityType just as ArchetypeChunkComponentType
Your ScoreBox just looks like a tag for now (or ad a “score” field to it and use it to increment player’s score). SO I would just put it in the ENtityQuery as a filter, and then only use the Translation and Entity for your job as you don’t require information from this component.
As a side note, if you care for performance, you should remember to use the squared distance instead of the distance (and compare it to your squared threshold, so in your case 2^2 = 4). That saves one call to sqrt() which is not required and can be performance-sensitive
Ahh I almost had it down, but it all seems a lot clearer now. Makes more sense. Thanks again for your help, I implemented all of the changes. I added a ScoreValue field to the ScoreBox so they can have different score increments now. I hope I did everything optimally
using Unity.Entities;
using Unity.Transforms;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Burst;
namespace SimpleECS
{
/*
* Utilizes C# Job System to process collisions between Player and ScoreBox entities.
* Creating and removing entities can only be done inside the main thread.
* This sytem uses an EntityCommandBuffer to handle tasks that can't be completed inside Jobs.
*/
public class CollisionSystem : JobComponentSystem
{
// Define a ComponentGroup for ScoreBox entities
EntityQuery ScoreBoxGroup;
// BeginInitializationEntityCommandBufferSystem is used to create a command buffer that will be played back when the barreir system executes.
BeginInitializationEntityCommandBufferSystem m_EntityCommandBufferSystem;
protected override void OnCreate()
{
m_EntityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
// Query for ScoreBoxes with following components
EntityQueryDesc scoreBoxQuery = new EntityQueryDesc
{
All = new ComponentType[] { typeof(ScoreBox), typeof(Translation) },
};
// Get the ComponentGroup
ScoreBoxGroup = GetEntityQuery(scoreBoxQuery);
}
[BurstCompile]
struct CollisionJob : IJobForEach<Player, Translation>
{
// Access to the EntityCommandBuffer to Destroy entity
[ReadOnly] public EntityCommandBuffer CommandBuffer;
// When dealing with more than one component, better to iterate through chunks
[ReadOnly] public ArchetypeChunkComponentType<Translation> TranslationType;
[ReadOnly] public ArchetypeChunkComponentType<ScoreBox> ScoreBoxType;
[ReadOnly] public ArchetypeChunkEntityType ScoreBoxEntity;
[ReadOnly] [DeallocateOnJobCompletion] public NativeArray<ArchetypeChunk> Chunks;
public void Execute(ref Player player, [ReadOnly] ref Translation position)
{
for (int i = 0; i < Chunks.Length; i++)
{
var chunk = Chunks[i];
var translations = chunk.GetNativeArray(TranslationType);
var scoreBoxes = chunk.GetNativeArray(ScoreBoxType);
var scoreBoxEntities = chunk.GetNativeArray(ScoreBoxEntity);
for (int j = 0; j < scoreBoxes.Length; j++)
{
// Calculate the distance between the ScoreBox and Player
// Use squared distance value to increase performance (saves call to sqrt())
float dist = math.distancesq(position.Value, translations[j].Value);
// If close enough for collision, add to the score and destroy the entity
// Check the squared value of distance threshold (2^2 = 4)
if (dist < 4.0f)
{
player.Score += scoreBoxes[j].ScoreValue;
CommandBuffer.DestroyEntity(scoreBoxEntities[j]);
}
}
}
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var translationType = GetArchetypeChunkComponentType<Translation>(true);
var scoreBoxType = GetArchetypeChunkComponentType<ScoreBox>(true);
var scoreBoxEntity = GetArchetypeChunkEntityType();
var chunks = ScoreBoxGroup.CreateArchetypeChunkArray(Allocator.TempJob, out var handle);
// Create the job and add dependency
var collisionJobHandle = new CollisionJob
{
CommandBuffer = m_EntityCommandBufferSystem.CreateCommandBuffer(),
TranslationType = translationType,
ScoreBoxType = scoreBoxType,
ScoreBoxEntity = scoreBoxEntity,
Chunks = chunks,
}.Schedule(this, JobHandle.CombineDependencies(inputDeps, handle));
// Pass final handle to barrier system to ensure dependency completion
// Tell the barrier system which job needs to be completed before the commands can be played back
m_EntityCommandBufferSystem.AddJobHandleForProducer(collisionJobHandle);
return collisionJobHandle;
}
}
}
Just one more thing, is there a more optimal version for the Execute function in the CollisionJob? Basically, avoiding a nested for loop here. Just wondering out of curiosity.
Well I think you could work on something with hash maps in order to only access entities in areas close to your players and avoid checking collisions with all entities, but that would require some thinking.
If I remember right, they used something similar in one of the first demo of ECS (GitHub - Unity-Technologies/UniteAustinTechnicalPresentation). Be careful thought as I believe this is no up-to-date code.
Did you try your code? I wonder if you shouldn’t be using a Concurrent command buffer (with ToConcurrent()) since you used Schedule() and not ScheduleSingle(). For that you would also need to get the index parameter int he Execute() function of the job.
Alright, I’ll take a look at that and try and come up with something more efficient.
As for the code, it works perfectly fine right now. However, I did update to ScheduleSingle() now. I also wasn’t sure if I needed to use a Concurrent command buffer or not.
Not sure if I should keep it on a single thread or just use a Concurrent command buffer.
I would go with a concurrent buffer and use Schedule() since destroying the entity is quite unlikely.
Try and benchmark it with a lot of entities, I am convinced that will be way faster.
I tried it out and it was a little bit faster. But I’m sure that in a more complex scene, it would be much more beneficial. I’ll just stick with a concurrent buffer and use Schedule() then
Yes, this is the current best practice for the convenience of authoring ECS data in the Editor. You can still instantiate/set up Entities directly in script code, and it may be faster without the extra conversion step, but you’d lost any Editor interactivity.