Well, there is simple AI logic. In Idle state, each cube finds the closest cube as target and follows them until reaches it than, it searches another closest cube. I tried it with both C# and unity ECS but got similar performance results (both are about 70 fps ). Here are main parts of the codes.
C#:
public class SpawnCubes : MonoBehaviour
{
public GameObject prefab;
public float radius;
// Start is called before the first frame update
public CubeClass[] cubes;
public float speed, stoppingDistance;
void Start()
{
int count = 2000;
cubes = new CubeClass[count];
int id = 0;
for(int i=0; i<count; i++)
{
Vector3 spawnVec = new Vector3(Random.Range(-radius, radius), 0, Random.Range(-radius, radius));
Transform t = Instantiate(prefab, spawnVec, Quaternion.identity).transform;
CubeClass c = new CubeClass();
c.id = id;
c.self = t;
cubes[i] = c;
id++;
}
}
private void Update()
{
for(int i=0; i<cubes.Length; i++)
{
CubeClass cube = cubes[i];
if(cube.state == HumanState.Idle)
{
float distance = 9999999f;
int closest = -1;
for (int k = 0; k < cubes.Length; k++)
{
CubeClass cube2 = cubes[k];
float d = Vector3.SqrMagnitude(cube.self.position- cube2.self.position);
if (d < distance && d > stoppingDistance * 2f)
{
distance = d;
closest = cube2.id;
}
}
if(closest != -1)
{
cube.targetId = closest;
cube.state = HumanState.Chasing;
}
}
else
{
for (int k = 0; k < cubes.Length; k++)
{
CubeClass cube2 = cubes[k];
if(cube2.id == cube.targetId)
{
Vector3 dir = cube2.self.position - cube.self.position;
if(dir.sqrMagnitude < stoppingDistance)
{
cube.state = HumanState.Idle;
break;
}
dir = dir.normalized;
cube.self.position += dir * Time.deltaTime * speed;
break;
}
}
}
}
}
}
public class CubeClass
{
public HumanState state;
public Transform self;
public int id;
public int targetId;
}
ECS:
public partial struct HumanMoveSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<HumanProperties>();
}
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
Entity humanP = SystemAPI.GetSingletonEntity<HumanProperties>();
GameStateAspect gameStateAspect = SystemAPI.GetAspect<GameStateAspect>(humanP);
GameStateType type = gameStateAspect.Value;
if (type != GameStateType.Started)
{
return;
}
float deltaTime = SystemAPI.Time.DeltaTime;
NativeList<float3> aspects = new NativeList<float3>(Allocator.Persistent);
NativeList<int> ids = new NativeList<int>(Allocator.Persistent);
foreach (var human in SystemAPI.Query<HumanActiveAspect>())
{
aspects.Add(human.transform.ValueRO.Position);
ids.Add(human.stats.ValueRO.id);
}
MoveJob job = new MoveJob();
job.aspects = aspects;
job.ids = ids;
job.deltaTime = deltaTime;
job.Schedule();
}
[BurstCompile]
public partial struct MoveJob : IJobEntity
{
public float deltaTime;
public NativeList<float3> aspects;
public NativeList<int> ids;
void Execute(HumanActiveAspect aspect)
{
if (aspect.Value != HumanState.Chasing) return;
for (int i = 0; i < aspects.Length; i++)
{
if (ids[i] == aspect.stats.ValueRO.targetID)
{
if(aspect.Move(deltaTime, aspects[i]))
{
aspect.stats.ValueRW.state = HumanState.Idle;
}
}
}
}
}
[BurstCompile]
public partial struct HumanIdleSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<HumanProperties>();
}
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
Entity humanP = SystemAPI.GetSingletonEntity<HumanProperties>();
GameStateAspect gameStateAspect = SystemAPI.GetAspect<GameStateAspect>(humanP);
GameStateType type = gameStateAspect.Value;
if (type != GameStateType.Started)
{
return;
}
float deltaTime = SystemAPI.Time.DeltaTime;
NativeList<float3> aspects = new NativeList<float3>(Allocator.Persistent);
NativeList<int> ids = new NativeList<int>(Allocator.Persistent);
foreach (var human in SystemAPI.Query<HumanActiveAspect>())
{
aspects.Add(human.transform.ValueRO.Position);
ids.Add(human.stats.ValueRO.id);
}
HumanChooseTargetJob job = new HumanChooseTargetJob();
job.ids = ids;
job.deltaTime = deltaTime;
job.aspects = aspects;
job.Schedule();
}
[BurstCompile]
public partial struct HumanChooseTargetJob : IJobEntity
{
public float deltaTime;
public NativeList<float3> aspects;
public NativeList<int> ids;
void Execute(HumanActiveAspect aspect)
{
if (aspect.Value != HumanState.Idle) return;
int closest = -1;
float distance = 99999999f;
for(int i=0; i<aspects.Length; i++)
{
float3 dir = aspect.transform.ValueRO.Position - aspects[i];
float d = math.lengthsq(dir);
if (d < distance && d > aspect.stats.ValueRO.stopDistance * 2f)
{
distance = d;
closest = ids[i];
}
}
if (closest == -1) return;
aspect.SetTarget(closest);
aspect.stats.ValueRW.state = HumanState.Chasing;
}
}
}
public readonly partial struct HumanActiveAspect : IAspect
{
public readonly Entity entity;
public readonly RefRW<LocalTransform> transform;
public readonly RefRW<HumanStats> stats;
public HumanState Value => stats.ValueRO.state;
public int ID => stats.ValueRO.id;
public float ReturnDistance(float3 pos)
{
return math.distance(pos, transform.ValueRO.Position);
}
public bool Move(float deltaTime, float3 target)
{
float3 direction = target - transform.ValueRW.Position;
float magnitude = math.lengthsq(direction);
if (magnitude < stats.ValueRW.stopDistance) return true;
transform.ValueRW.Position += direction * deltaTime * stats.ValueRW.moveSpeed / magnitude;
return false;
}
/*public (bool away, bool targetDead) Attack(float deltaTime, float3 target)
{
float3 direction = target - transform.ValueRW.Position;
float magnitude = math.length(direction);
if (magnitude < stats.ValueRW.stopDistance) return (true, false);
bool targetDead = IDMaker.Attack(stats.ValueRO.targetID, 10);
return (false, targetDead);
}*/
public void SetTarget(int id)
{
stats.ValueRW.targetID = id;
}
}
public enum HumanState { Idle, Chasing, Attacking, None }
public struct HumanStats : IComponentData
{
public HumanState state;
public int health, maxHealth;
public float moveSpeed;
public float stopDistance;
public int targetID;
public int id;
}
I think you need to understand two things here:
It means that:
If your program has little need for mathematical calculations (algebra, trigonometry, vector and/or matrix transformations etc) then your program will not benefit from Burst.
If your program requires that memory is rarely to be read linearly or most of that read is wasted* then it will not benefit from all this ECS infrastructure.
Programs with little math and random memory access patterns will be as fast as anywhere else.
*wasted = data takes space in a struct but is not read by given code
PS: Also make sure Burst compilation is enabled
Jobs/Burst/Enable Compilation
.MonoBehaviour
implementationHere, as
FightCube.cs
, is an example of faster implementation you can compare with:https://gist.github.com/andrew-raphael-lukasik/9582abd02b237b30d7abbdeb5c5d5b51
What I changed:
GameObject
s (you can't have thousands of these and expect good perf because these were designed in OOP style)struct
arrays to make it's data close to each other (MyClass[]
arrays will be spread all over the RAM)0, 1, 2, 3
and not randomly3, 0, 21, 2
)2000 instance wasn't even sweeting my CPU. But at 20k instances CPU started to get visibly slow. To push performance of this code much further the code that finds nearest neighbor needs to be replaced with a Octree or something like that.
This should answer why your
MonoBehaviour
code is slow.ECS
com.unity.entities 1.0.11
implementationFightCubeAuthoring.cs
under the same link contains implementation that shows some good practices of writing ECS code forcom.unity.entities 1.0.11
https://gist.github.com/andrew-raphael-lukasik/9582abd02b237b30d7abbdeb5c5d5b51
Some of my suggestions:
IAspect
s as it makes code less transparent. It's main lure of convenience is not worth this cost.Persistent
allocators.Allocator.Persistent
is for data that needs to exist for multiple frames or application lifetime. InOnUpdate
method use eitherAllocator.Temp
(fastest allocation) orAllocator.TempJob
(fast alloc + can be used by jobs).IComponentData
, like thisHumanProperties
, into many small that holds singular bits of information.O(n²)
type. You can get away with them while number ofn
elements in is in dozens of maybe in hundreds but it will blow up once you reach thousands for sure. Optimizing or removing this loop is critical for performance stability asn
grows.state.RequireForUpdate<MySingleton>();
to makes system wait (not callOnUpdate
) until specific singleton exists. This singleton can be a system state or a game state (like:game_started
,game_paused
etc).math.lengthsq
is not an optimization. Focus on how you read memory and try to make these reads linear & tiny, get rid of unnecessary ones where possible - this is optimization that gives big results. Burst will optimize the hell out of any math you give it. But Burst can't make AMOUNT of DATA to calculate smaller - this is where you come in.