Hi ! I am trying to create an AI based on the Utility AI from Dave Mark (here). To simplify my question, I create a test to illustrate my problem.
Simple Test
using NUnit.Framework;
using System;
using Unity.Entities;
using Unity.Jobs;
namespace Tests
{
[Category("ECS Test")]
public class SimpleFilterJobTests
{
World World;
EntityManager Manager;
[SetUp]
public void SetUp()
{
World = new World("Test World");
Manager = World.EntityManager;
}
public void TearDown()
{
if (World != null)
{
World.Dispose();
World = null;
Manager = null;
}
}
[Test]
public void AssertJobPass()
{
Assert.Pass();
}
public struct SharedValue : ISharedComponentData, IEquatable<SharedValue>
{
public int Value;
public bool Equals(SharedValue other)
{
return Value == other.Value;
}
public override int GetHashCode()
{
return Value;
}
}
public struct SimpleValue : IComponentData
{
public float Value;
}
public struct SimpleValueJob : IJobForEach<SimpleValue>
{
public void Execute(ref SimpleValue simple)
{
simple.Value = 10;
}
}
[Test]
public void TestScheduleOnTwoSeperateChunk()
{
for (int i = 0; i < 10; i++)
{
var e = Manager.CreateEntity();
Manager.AddComponent(e, typeof(SimpleValue));
Manager.AddSharedComponentData(e, new SharedValue { Value = 1 });
}
for (int i = 0; i < 10; i++)
{
var e = Manager.CreateEntity();
Manager.AddComponent(e, typeof(SimpleValue));
Manager.AddSharedComponentData(e, new SharedValue { Value = 2 });
}
var query = Manager.CreateEntityQuery(typeof(SharedValue), typeof(SimpleValue));
query.SetFilter(new SharedValue { Value = 1 });
var handle1 = new SimpleValueJob().Schedule(query);
query.SetFilter(new SharedValue { Value = 2 });
var handle2 = new SimpleValueJob().Schedule(query);
JobHandle.CompleteAll(ref handle1, ref handle2);
}
}
}
And the error, I get.
Test Error
TestScheduleOnTwoSeperateChunk (1.281s)
---
System.InvalidOperationException : The previously scheduled job SimpleFilterJobTests:SimpleValueJob writes to the NativeArray SimpleValueJob.Iterator. You are trying to schedule a new job SimpleFilterJobTests:SimpleValueJob, which writes to the same NativeArray (via SimpleValueJob.Iterator). To guarantee safety, you must include SimpleFilterJobTests:SimpleValueJob as a dependency of the newly scheduled job.
---
at Unity.Entities.JobForEachExtensions.Schedule (System.Void* fullData, Unity.Collections.NativeArray`1[T] prefilterData, System.Int32 unfilteredLength, System.Int32 innerloopBatchCount, System.Boolean isParallelFor, System.Boolean isFiltered, Unity.Entities.JobForEachExtensions+JobForEachCache& cache, System.Void* deferredCountData, Unity.Jobs.JobHandle dependsOn, Unity.Jobs.LowLevel.Unsafe.ScheduleMode mode) [0x00066] in D:\Project\VoxelWar\Library\PackageCache\com.unity.entities@0.0.12-preview.33\Unity.Entities\IJobForEach.cs:451
at Unity.Entities.JobForEachExtensions.ScheduleInternal_C[T] (T& jobData, Unity.Entities.ComponentSystemBase system, Unity.Entities.EntityQuery query, System.Int32 innerloopBatchCount, Unity.Jobs.JobHandle dependsOn, Unity.Jobs.LowLevel.Unsafe.ScheduleMode mode) [0x0007c] in D:\Project\VoxelWar\Library\PackageCache\com.unity.entities@0.0.12-preview.33\Unity.Entities\IJobForEach.gen.cs:1510
at Unity.Entities.JobForEachExtensions.Schedule[T] (T jobData, Unity.Entities.EntityQuery query, Unity.Jobs.JobHandle dependsOn) [0x00020] in D:\Project\VoxelWar\Library\PackageCache\com.unity.entities@0.0.12-preview.33\Unity.Entities\IJobForEach.gen.cs:1152
at Tests.SimpleFilterJobTests.TestScheduleOnTwoSeperateChunk () [0x00190] in D:\Project\VoxelWar\Assets\Tests\AI\Behaviour\SimpleFilterJobTests.cs:93
at (wrapper managed-to-native) System.Reflection.MonoMethod.InternalInvoke(System.Reflection.MonoMethod,object,object[ ],System.Exception&)
at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[ ] parameters, System.Globalization.CultureInfo culture) [0x00032] in <1f0c1ef1ad524c38bbc5536809c46b48>:0
- If I understand, I should make my second JobHandle depends from the first job. But why ? If I understand SharedComponentData, the first and second job should read on 2 separates chunks, so they shouldn’t have a problem to write in the NativeArray ?
Now for the context like I say I want to maximize the use of ECS in my Utility System. I know I will have random access but I can still multi-thread my code and use the burst compiler, so I am thinking it’s worth it.
My data is structure like that. I have DecisionMaker which have Decisions which have Considerations. Each considerations and decisions need to give a score. All decisions are unique but, considerations are fixed and there score can be reuse in the same DecisionMaker. So if I have 2 decision that need to consider its health, I can score HealthConsideration one time for 2 decisions. Each decision score is multiply and compensate on one ComponentData and the highest of all decisions in all DecisionMaker is the winner.
One solution, it’s to use one attribute by DecisionMaker
Test One Attribute
using NUnit.Framework;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
namespace Tests
{
[Category("VoxelWar")]
public class AgentBrainTests
{
World World;
EntityManager Manager;
[SetUp]
public void SetUp()
{
World = new World("Test World");
Manager = World.EntityManager;
for (int i = 0; i < 10; i++)
{
var e = Manager.CreateEntity();
Manager.AddComponentData(e, new TestHealth { Value = 10f });
Manager.AddComponent(e, typeof(TestHealthScore));
Manager.AddComponent(e, typeof(TestHighestDecisionScore));
Manager.AddComponent(e, typeof(TestDecisionScore));
Manager.AddComponent(e, typeof(TestDecisionMaker1));
}
for (int i = 0; i < 5; i++)
{
var e = Manager.CreateEntity();
Manager.AddComponentData(e, new TestHealth { Value = 5f });
Manager.AddComponentData(e, new TestDistance { Value = 25f });
Manager.AddComponent(e, typeof(TestHealthScore));
Manager.AddComponent(e, typeof(TestDistanceScore));
Manager.AddComponent(e, typeof(TestHighestDecisionScore));
Manager.AddComponent(e, typeof(TestDecisionScore));
Manager.AddComponent(e, typeof(TestDecisionMaker2));
Manager.AddComponent(e, typeof(TestDecisionMaker1));
}
}
public void TearDown()
{
if (World != null)
{
World.Dispose();
World = null;
Manager = null;
}
}
// A Test behaves as an ordinary method
[Test]
public void AgentBrainTestsSimplePasses()
{
Assert.Pass();
}
public struct TestDecisionMaker1 : IComponentData { }
public struct TestDecisionMaker2 : IComponentData { }
public struct TestHighestDecisionScore : IComponentData { public float Value; }
public struct TestDecisionScore : IComponentData { public float Value; }
public struct TestHealthScore : IComponentData { public float Value; }
public struct TestHealth : IComponentData { public float Value; }
public struct TestDistanceScore : IComponentData { public float Value; }
public struct TestDistance : IComponentData { public float Value; }
public class TestBarrier : EntityCommandBufferSystem { }
public interface ITestConsideration
{
void Init(EntityManager manager, EntityQuery query);
JobHandle Score(EntityQuery query);
JobHandle Merge(EntityQuery query, JobHandle inputDeps);
}
public class TestHealthConsideration : ITestConsideration
{
struct HealthJob : IJobForEach<TestHealthScore, TestHealth>
{
public void Execute(ref TestHealthScore c0, [ReadOnly] ref TestHealth c1)
{
c0.Value = c1.Value / 10;
}
}
struct MergeHealth : IJobForEachWithEntity<TestDecisionScore, TestHealthScore>
{
public void Execute(Entity entity, int index, ref TestDecisionScore c0, [ReadOnly] ref TestHealthScore c1)
{
c0.Value *= c1.Value;
}
}
public void Init(EntityManager manager, EntityQuery query)
{
manager.AddComponent(query, typeof(TestHealthScore));
}
public JobHandle Score(EntityQuery query)
{
return new HealthJob().Schedule(query);
}
public JobHandle Merge(EntityQuery query, JobHandle inputDeps)
{
return new MergeHealth().Schedule(query, inputDeps);
}
}
public class TestDistanceConsideration : ITestConsideration
{
struct DistanceJob : IJobForEach<TestDistanceScore, TestDistance>
{
public void Execute(ref TestDistanceScore c0, [ReadOnly] ref TestDistance c1)
{
c0.Value = c1.Value / 50;
}
}
struct MergeDistance : IJobForEachWithEntity<TestDecisionScore, TestDistanceScore>
{
public void Execute(Entity entity, int index, ref TestDecisionScore c0, [ReadOnly] ref TestDistanceScore c1)
{
c0.Value *= c1.Value;
}
}
public void Init(EntityManager manager, EntityQuery query)
{
manager.AddComponent(query, typeof(TestDistanceScore));
}
public JobHandle Score(EntityQuery query)
{
return new DistanceJob().Schedule(query);
}
public JobHandle Merge(EntityQuery query, JobHandle inputDeps)
{
return new MergeDistance().Schedule(query, inputDeps);
}
}
public struct TestCompareHighestDecisionScore : IJobForEach<TestHighestDecisionScore, TestDecisionScore>
{
public void Execute(ref TestHighestDecisionScore c0, ref TestDecisionScore c1)
{
c0.Value = math.max(c0.Value, c1.Value);
}
}
[Test]
public void AgentBrainDecisionTagComponent()
{
ITestConsideration[] considerations = new ITestConsideration[]
{
new TestHealthConsideration(),
new TestDistanceConsideration()
};
EntityQueryDesc[] decisions = new EntityQueryDesc[]
{
new EntityQueryDesc { All = new ComponentType[] { typeof(TestHealth), typeof(TestDistance) } },
new EntityQueryDesc { All = new ComponentType[] { typeof(TestHealth) } },
new EntityQueryDesc { All = new ComponentType[] { typeof(TestDistance) } }
};
EntityQueryDesc[] decisionScores = new EntityQueryDesc[]
{
new EntityQueryDesc { All = new ComponentType[] { typeof(TestHealthScore), typeof(TestDistanceScore), typeof(TestHealth), typeof(TestDistance) } },
new EntityQueryDesc { All = new ComponentType[] { typeof(TestHealthScore), typeof(TestHealth) } },
new EntityQueryDesc { All = new ComponentType[] { typeof(TestDistanceScore), typeof(TestDistance) } }
};
int[][] decisionConsiderations = new int[][]
{
new int[] { 1, 0 },
new int[] { 0 },
new int[] { 1 }
};
int[] sortedDecision = new int[] { 1, 0, 2 };
int[] sortedDecisionMaker = new int[] { 0, 1, 1 };
EntityQueryDesc[] decisionMakers = new EntityQueryDesc[]
{
new EntityQueryDesc { All = new ComponentType[] { typeof(TestDecisionMaker1), typeof(TestHighestDecisionScore), typeof(TestDecisionScore), typeof(TestHealthScore), typeof(TestHealth) } },
new EntityQueryDesc { All = new ComponentType[] { typeof(TestDecisionMaker2), typeof(TestDecisionMaker1), typeof(TestHighestDecisionScore), typeof(TestDecisionScore), typeof(TestHealthScore), typeof(TestDistanceScore), typeof(TestHealth), typeof(TestDistance) } }
};
EntityQuery[] queries = new EntityQuery[decisionMakers.Length];
queries[0] = Manager.CreateEntityQuery(decisionMakers[0]);
queries[1] = Manager.CreateEntityQuery(decisionMakers[1]);
Assert.AreEqual(15, queries[0].CalculateLength());
Assert.AreEqual(5, queries[1].CalculateLength());
for (int i = 0; i < decisions.Length; i++)
{
EntityQuery query = queries[sortedDecisionMaker[i]];
JobHandle handle = default;
int[] consIndice = decisionConsiderations[sortedDecision[i]];
for (int j = 0; j < consIndice.Length; j++)
{
var consideration = considerations[consIndice[j]];
JobHandle inputDeps = consideration.Score(query);
inputDeps = consideration.Merge(query, JobHandle.CombineDependencies(handle, inputDeps));
handle = JobHandle.CombineDependencies(handle, inputDeps);
}
handle = new TestCompareHighestDecisionScore().Schedule(query, handle);
handle.Complete();
}
using (var scores = queries[0].ToComponentDataArray<TestHealthScore>(Allocator.TempJob))
{
for (int i = 0; i < scores.Length; i++)
{
Assert.IsTrue(scores[i].Value == 1f || scores[i].Value == 0.5f);
}
}
Assert.Pass();
}
}
}
The problem with this solution is that I need to hardcode my DecisionMaker struct. I want to let my designer and player create decision outside the game. So what I can do, it’s create like 100 DecisionMaker struct, which represent one index and could access in a big switch or array and use the unique index of the DecisionMaker to tag my entity. Which I feel is kind a bad practice to be honest. I looked at the generation of ComponentType and there doesn’t seem to be a way to create a fake ComponentType at runtime or something like that.
So my second solution is to use SharedComponentData to seperate create something like above.
Test SharedComponent
using System;
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.TestTools;
namespace Tests
{
[Category("VoxelWar")]
public class AgentBrainSharedTests
{
World World;
EntityManager Manager;
[SetUp]
public void SetUp()
{
World = new World("Test World");
Manager = World.EntityManager;
for (int i = 0; i < 10; i++)
{
var e = Manager.CreateEntity();
Manager.AddComponentData(e, new TestHealth { Value = 10f });
Manager.AddComponent(e, typeof(TestHealthScore));
Manager.AddComponent(e, typeof(TestHighestDecisionScore));
Manager.AddComponent(e, typeof(TestDecisionScore));
Manager.AddSharedComponentData(e, new TestBrainChunk { Value = 1 });
}
for (int i = 0; i < 5; i++)
{
var e = Manager.CreateEntity();
Manager.AddComponentData(e, new TestHealth { Value = 5f });
Manager.AddComponentData(e, new TestDistance { Value = 25f });
Manager.AddComponent(e, typeof(TestHealthScore));
Manager.AddComponent(e, typeof(TestDistanceScore));
Manager.AddComponent(e, typeof(TestHighestDecisionScore));
Manager.AddComponent(e, typeof(TestDecisionScore));
Manager.AddSharedComponentData(e, new TestBrainChunk { Value = 2 });
}
}
public void TearDown()
{
if (World != null)
{
World.Dispose();
World = null;
Manager = null;
}
}
// A Test behaves as an ordinary method
[Test]
public void AgentBrainTestsSimplePasses()
{
Assert.Pass();
}
public struct TestBrainChunk : ISharedComponentData, IEquatable<TestBrainChunk>
{
public int Value;
public bool Equals(TestBrainChunk other)
{
return other.Value == Value;
}
public override int GetHashCode()
{
return Value;
}
}
public struct TestHighestDecisionScore : IComponentData { public float Value; }
public struct TestDecisionScore : IComponentData { public float Value; }
public struct TestHealthScore : IComponentData { public float Value; }
public struct TestHealth : IComponentData { public float Value; }
public struct TestDistanceScore : IComponentData { public float Value; }
public struct TestDistance : IComponentData { public float Value; }
public class TestBarrier : EntityCommandBufferSystem { }
public interface ITestConsideration
{
void Init(EntityManager manager, EntityQuery query);
JobHandle Score(EntityQuery query);
JobHandle Merge(EntityQuery query, JobHandle inputDeps);
}
public class TestHealthConsideration : ITestConsideration
{
struct HealthJob : IJobForEach<TestHealthScore, TestHealth>
{
public void Execute(ref TestHealthScore c0, [ReadOnly] ref TestHealth c1)
{
c0.Value = c1.Value / 10;
}
}
struct MergeHealth : IJobForEachWithEntity<TestDecisionScore, TestHealthScore>
{
public void Execute(Entity entity, int index, ref TestDecisionScore c0, [ReadOnly] ref TestHealthScore c1)
{
c0.Value *= c1.Value;
}
}
public void Init(EntityManager manager, EntityQuery query)
{
manager.AddComponent(query, typeof(TestHealthScore));
}
public JobHandle Score(EntityQuery query)
{
return new HealthJob().Schedule(query);
}
public JobHandle Merge(EntityQuery query, JobHandle inputDeps)
{
return new MergeHealth().Schedule(query, inputDeps);
}
}
public class TestDistanceConsideration : ITestConsideration
{
struct DistanceJob : IJobForEach<TestDistanceScore, TestDistance>
{
public void Execute(ref TestDistanceScore c0, [ReadOnly] ref TestDistance c1)
{
c0.Value = c1.Value / 50;
}
}
struct MergeDistance : IJobForEachWithEntity<TestDecisionScore, TestDistanceScore>
{
public void Execute(Entity entity, int index, ref TestDecisionScore c0, [ReadOnly] ref TestDistanceScore c1)
{
c0.Value *= c1.Value;
}
}
public void Init(EntityManager manager, EntityQuery query)
{
manager.AddComponent(query, typeof(TestDistanceScore));
}
public JobHandle Score(EntityQuery query)
{
return new DistanceJob().Schedule(query);
}
public JobHandle Merge(EntityQuery query, JobHandle inputDeps)
{
return new MergeDistance().Schedule(query, inputDeps);
}
}
public struct TestCompareHighestDecisionScore : IJobForEach<TestHighestDecisionScore, TestDecisionScore>
{
public void Execute(ref TestHighestDecisionScore c0, ref TestDecisionScore c1)
{
c0.Value = math.max(c0.Value, c1.Value);
}
}
[Test]
public void AgentBrainDecisionSharedComponent()
{
ITestConsideration[] considerations = new ITestConsideration[]
{
new TestHealthConsideration(),
new TestDistanceConsideration()
};
EntityQueryDesc[] decisions = new EntityQueryDesc[]
{
new EntityQueryDesc { All = new ComponentType[] { typeof(TestHealth), typeof(TestDistance) } },
new EntityQueryDesc { All = new ComponentType[] { typeof(TestHealth) } },
new EntityQueryDesc { All = new ComponentType[] { typeof(TestDistance) } }
};
EntityQueryDesc[] decisionScores = new EntityQueryDesc[]
{
new EntityQueryDesc { All = new ComponentType[] { typeof(TestHealthScore), typeof(TestDistanceScore), typeof(TestHealth), typeof(TestDistance) } },
new EntityQueryDesc { All = new ComponentType[] { typeof(TestHealthScore), typeof(TestHealth) } },
new EntityQueryDesc { All = new ComponentType[] { typeof(TestDistanceScore), typeof(TestDistance) } }
};
int[][] decisionConsiderations = new int[][]
{
new int[] { 1, 0 },
new int[] { 0 },
new int[] { 1 }
};
int[] sortedDecision = new int[] { 1, 0, 2 };
int[] sortedDecisionMaker = new int[] { 0, 1, 1 };
EntityQueryDesc[] decisionMakers = new EntityQueryDesc[]
{
new EntityQueryDesc { All = new ComponentType[] { typeof(TestBrainChunk), typeof(TestHighestDecisionScore), typeof(TestDecisionScore), typeof(TestHealthScore), typeof(TestHealth) } },
new EntityQueryDesc { All = new ComponentType[] { typeof(TestBrainChunk), typeof(TestHighestDecisionScore), typeof(TestDecisionScore), typeof(TestHealthScore), typeof(TestDistanceScore), typeof(TestHealth), typeof(TestDistance) } }
};
EntityQuery[] queries = new EntityQuery[decisionMakers.Length];
queries[0] = Manager.CreateEntityQuery(decisionMakers[0]);
queries[1] = Manager.CreateEntityQuery(decisionMakers[1]);
Dictionary<int, List<int>> brainChunkDecisionMaker = new Dictionary<int, List<int>>();
brainChunkDecisionMaker.Add(0, new List<int> { 1, 2 });
brainChunkDecisionMaker.Add(1, new List<int> { 2 });
List<int> keys = new List<int>();
NativeList<JobHandle> values = new NativeList<JobHandle>(10, Allocator.Temp);
try
{
for (int i = 0; i < decisions.Length; i++)
{
EntityQuery query = queries[sortedDecisionMaker[i]];
brainChunkDecisionMaker.TryGetValue(sortedDecisionMaker[i], out List<int> makerIndice);
int[] consIndice = decisionConsiderations[sortedDecision[i]];
for (int index = 0; index < makerIndice.Count; index++)
{
int valueIndex = FindOrCreateIndexOf(makerIndice[index], ref keys, ref values);
query.SetFilter(new TestBrainChunk { Value = makerIndice[index] });
for (int j = 0; j < consIndice.Length; j++)
{
JobHandle inputDeps = considerations[consIndice[j]].Score(query);
inputDeps = considerations[consIndice[j]].Merge(query, JobHandle.CombineDependencies(inputDeps, values[valueIndex]));
values[valueIndex] = new TestCompareHighestDecisionScore().Schedule(query, inputDeps);
}
}
JobHandle.CompleteAll(values);
values.Clear();
keys.Clear();
}
}
finally
{
values.Dispose();
}
queries[0].ResetFilter();
queries[1].ResetFilter();
queries[0].SetFilter(new TestBrainChunk { Value = 1 });
using (var scores = queries[0].ToComponentDataArray<TestHealthScore>(Allocator.TempJob))
{
for (int i = 0; i < scores.Length; i++)
{
Assert.AreEqual(1f, scores[i].Value);
}
}
queries[0].SetFilter(new TestBrainChunk { Value = 2 });
using (var scores = queries[0].ToComponentDataArray<TestHealthScore>(Allocator.TempJob))
{
for (int i = 0; i < scores.Length; i++)
{
Assert.AreEqual(0.5f, scores[i].Value);
}
}
queries[1].SetFilter(new TestBrainChunk { Value = 2 });
using (var scores = queries[0].ToComponentDataArray<TestHealthScore>(Allocator.TempJob))
{
for (int i = 0; i < scores.Length; i++)
{
Assert.AreEqual(0.5f, scores[i].Value);
}
}
}
private int FindOrCreateIndexOf(int item, ref List<int> keys, ref NativeList<JobHandle> values)
{
int index = keys.IndexOf(item);
if (index == -1)
{
index = keys.Count;
keys.Add(item);
values.Add(default);
}
return index;
}
}
}
So in my example BrainChunk has either the value 1 or 2. The value 1 represent the DecisionMaker1 and the value 2 represent DecisionMaker1 and DecisionMaker2. I keep in memory in brainChunkDecisionMaker where every DecisionMaker1 is and in what chunk, and sefilter by this value. Maybe you see now my comparison with my first issue. It give me the same error, because even if I am on different chunk I cannot access the same NativeArray for the same job
-
So my question is, is it normal behaviour or a bug ? And if normal, what should I do ? My next idea was to create my own chunk or get my chunks from my EntityQuery. Else I thought maybe to read a dll that my tool create which have a attribute custom for each DecisionMaker and bind to a unique index. It would need to be recompile because type need to be know at compilation. It could be an Ok solution for my designer but not for my player.
-
Another question in the same context. This system need to run in another World to keep my fps stable. I found the EntityManager.MoveEntitiesFrom to move the data of All my agents on another EntityManager. One thing I want to do in my system is to keep a cache of my precedent consideration score. So every update, I want to sync my entity from one world to the other. Is it possible or a good idea ?
-
Last question, I keep my raycast in a DynamicBuffer to reuse for other consideration, so I create my own collector to add the value directly in my DynamicBuffer. Because the DynamicBuffer has a internal max buffer I would like to stop the query when the DynamicBuffer is full. I saw that in an ICollector.Add you need to return a bool. If I return false, does the collector stop its query and if not is there another way to create this behavior ?
Thank you.