Run thousands of basic² ai agents without and with Unity ECS 1.0.11
.
FightCube.cs
FightCubeAuthoring.cs
GameStateStartedAuthoring.cs
note: quadtree was beyond the scope of this exercise; there is still room to get more perf here.
// src* https://gist.github.com/andrew-raphael-lukasik/9582abd02b237b30d7abbdeb5c5d5b51 | |
using UnityEngine; | |
using UnityEngine.Jobs; | |
using Unity.Entities; | |
using Unity.Transforms; | |
using Unity.Collections; | |
using Unity.Collections.LowLevel.Unsafe; | |
using Unity.Mathematics; | |
using Unity.Jobs; | |
using Random = Unity.Mathematics.Random; | |
namespace FightCubes | |
{ | |
public class FightCube : MonoBehaviour | |
{ | |
[SerializeField] uint _randomSeed = 1234; | |
[SerializeField] Mesh _mesh; | |
[SerializeField] Material _material; | |
[SerializeField] float3 _spawnVolume = new float3(100 , 100 , 100); | |
[SerializeField] float _moveSpeed = 10; | |
[SerializeField] float _stoppingDistance = 1; | |
[SerializeField][Min(2)] int _numEntitiesToCreate = 2000; | |
int _numEntities; | |
NativeArray<float4x4> _transforms; | |
NativeArray<EHumanState> _states; | |
NativeArray<int> _targets; | |
NativeQueue<int> _attacks; | |
public JobHandle Dependency; | |
void OnEnable () | |
{ | |
_transforms = new NativeArray<float4x4>(_numEntitiesToCreate , Allocator.Persistent); | |
_states = new NativeArray<EHumanState>(_numEntitiesToCreate , Allocator.Persistent); | |
_targets = new NativeArray<int>(_numEntitiesToCreate , Allocator.Persistent); | |
_attacks = new NativeQueue<int>(Allocator.Persistent); | |
var rnd = new Random(_randomSeed!=0 ? _randomSeed : (uint)System.DateTime.Now.GetHashCode()); | |
float3 origin = transform.position; | |
for( int entity = 0 ; entity<_numEntitiesToCreate ; entity++ ) | |
{ | |
float3 randomPoint = rnd.NextFloat3(-_spawnVolume/2 , _spawnVolume/2); | |
_transforms[entity] = float4x4.Translate(origin + randomPoint); | |
_states[entity] = EHumanState.Idle; | |
_targets[entity] = -1; | |
} | |
_numEntities = _numEntitiesToCreate; | |
} | |
void OnDisable () | |
{ | |
Dependency.Complete(); | |
if( _transforms.IsCreated ) _transforms.Dispose(); | |
if( _states.IsCreated ) _states.Dispose(); | |
if( _targets.IsCreated ) _targets.Dispose(); | |
if( _attacks.IsCreated ) _attacks.Dispose(); | |
_numEntities = 0; | |
} | |
void Update () | |
{ | |
Graphics.RenderMeshInstanced( | |
rparams: new RenderParams(_material) , | |
mesh: _mesh , | |
submeshIndex: 0 , | |
instanceData: _transforms.Reinterpret<Matrix4x4>() , | |
instanceCount: _numEntities | |
); | |
} | |
void FixedUpdate () | |
{ | |
Dependency.Complete(); | |
int batchCount = _numEntities / (SystemInfo.processorCount * 2); | |
var fixedUpdateJob = new FixedUpdateJob | |
{ | |
NumEntities = _numEntities , | |
MoveSpeed = _moveSpeed , | |
StoppingDistance = _stoppingDistance , | |
DeltaTime = Time.deltaTime , | |
Transforms = _transforms , | |
States = _states , | |
Targets = _targets , | |
Attacks = _attacks.AsParallelWriter() , | |
}; | |
Dependency = fixedUpdateJob.Schedule(_numEntities , batchCount , Dependency); | |
var resolveAttacks = new ResolveAttacksJob | |
{ | |
SourceOfRandomness = (uint)System.DateTime.Now.GetHashCode() , | |
Transforms = _transforms , | |
States = _states , | |
Attacks = _attacks , | |
}; | |
Dependency = resolveAttacks.Schedule(Dependency); | |
} | |
#if UNITY_EDITOR | |
void OnDrawGizmos () | |
{ | |
if( _transforms.IsCreated ) | |
{ | |
Gizmos.color = Color.yellow; | |
for( int entity = 0 ; entity<_numEntities ; entity++ ) | |
Gizmos.DrawWireSphere(_transforms[entity].Translation() , _stoppingDistance); | |
} | |
else | |
{ | |
float3 origin = transform.position; | |
var rnd = new Random(_randomSeed!=0 ? _randomSeed : (uint)System.DateTime.Now.GetHashCode()); | |
Gizmos.color = Color.yellow; | |
Gizmos.DrawWireCube(origin , _spawnVolume); | |
for( int i = 0 ; i<_numEntitiesToCreate ; i++ ) | |
{ | |
float3 randomPoint = rnd.NextFloat3(-_spawnVolume/2 , _spawnVolume/2); | |
Gizmos.DrawWireSphere(origin + randomPoint , _stoppingDistance); | |
} | |
} | |
} | |
#endif | |
} | |
public enum EHumanState : byte | |
{ | |
Idle, | |
Chasing, | |
Attacking, | |
Dead | |
} | |
[Unity.Burst.BurstCompile] | |
public struct FixedUpdateJob : IJobParallelFor | |
{ | |
public int NumEntities; | |
public float MoveSpeed; | |
public float StoppingDistance; | |
public float DeltaTime; | |
[NativeDisableContainerSafetyRestriction] public NativeArray<float4x4> Transforms;// NativeDisableContainerSafetyRestriction is here ONLY to render these transforms at the same time | |
[NativeDisableParallelForRestriction] public NativeArray<EHumanState> States; | |
public NativeArray<int> Targets; | |
[WriteOnly] public NativeQueue<int>.ParallelWriter Attacks; | |
void IJobParallelFor.Execute ( int entity ) | |
{ | |
EHumanState state = States[entity]; | |
if( state==EHumanState.Dead ) | |
{ | |
// dead x_x | |
} | |
else if( state==EHumanState.Idle ) | |
{ | |
float closestDist = float.MaxValue; | |
int closest = -1; | |
float3 position = Transforms[entity].Translation(); | |
for( int other = 0 ; other<NumEntities ; other++ ) | |
if( other!=entity )// don't compare to self | |
if( States[other]!=EHumanState.Dead )// ignore dead ones | |
{ | |
float3 otherPos = Transforms[other].Translation(); | |
float dist = math.length(position - otherPos); | |
if( dist<closestDist ) | |
{ | |
closestDist = dist; | |
closest = other; | |
} | |
} | |
if( closest!=-1 ) | |
{ | |
Targets[entity] = closest; | |
States[entity] = EHumanState.Chasing; | |
} | |
} | |
else if( state==EHumanState.Chasing ) | |
{ | |
int target = Targets[entity]; | |
if( target==-1 || target>=NumEntities || target==entity || States[target]==EHumanState.Dead ) | |
{ | |
// invalid target | |
States[entity] = EHumanState.Idle; | |
return; | |
} | |
float4x4 transform = Transforms[entity]; | |
float3 position = transform.Translation(); | |
float3 toTarget = Transforms[target].Translation() - position; | |
if( math.length(toTarget)>StoppingDistance ) | |
{ | |
// moving toward target | |
float3 dirToTarget = math.normalizesafe(toTarget); | |
float3 deltaPos = dirToTarget * (MoveSpeed * DeltaTime); | |
transform.c3 += new float4(deltaPos , 0);// c3 holds position as float4(x,y,z,1) where `1` at the end must be left unchanged | |
Transforms[entity] = transform; | |
} | |
else | |
{ | |
// stopping distance reached | |
States[entity] = EHumanState.Attacking; | |
} | |
} | |
else if( state==EHumanState.Attacking ) | |
{ | |
int target = Targets[entity]; | |
if( target==-1 || target>=NumEntities || target==entity || States[target]==EHumanState.Dead ) | |
{ | |
// invalid target | |
States[entity] = EHumanState.Idle; | |
} | |
else | |
{ | |
// takes a swing at target! | |
Attacks.Enqueue(target); | |
} | |
} | |
else throw new System.NotImplementedException($"entity:{entity} has state:{state} which is not implemented over here"); | |
} | |
} | |
[Unity.Burst.BurstCompile] | |
public struct ResolveAttacksJob : IJob | |
{ | |
public uint SourceOfRandomness; | |
[WriteOnly][NativeDisableContainerSafetyRestriction] public NativeArray<float4x4> Transforms;// NativeDisableContainerSafetyRestriction is here ONLY to render these transforms at the same time | |
[WriteOnly] public NativeArray<EHumanState> States; | |
public NativeQueue<int> Attacks; | |
void IJob.Execute () | |
{ | |
var rnd = new Random(SourceOfRandomness); | |
while( Attacks.Count!=0 ) | |
{ | |
int attacked = Attacks.Dequeue(); | |
if( rnd.NextBool() ) | |
{ | |
// attack succeeded | |
States[attacked] = EHumanState.Dead; | |
// flatten the cube | |
float4x4 transform = Transforms[attacked]; | |
transform.c0 = new float4(2f , 0 , 0 , 0);// c0 holds X axis direction (rotation) & scale as float4(x,y,z,0) | |
transform.c1 = new float4(0 , 0.1f , 0 , 0);// c1 holds Y axis direction (rotation) & scale as float4(x,y,z,0) | |
transform.c2 = new float4(0 , 0 , 2f , 0);// c2 holds Z axis direction (rotation) & scale as float4(x,y,z,0) | |
Transforms[attacked] = transform; | |
} | |
else | |
{ | |
// attack failed | |
} | |
} | |
} | |
} | |
} |