Run thousands of basic² ai agents without and with Unity ECS 1.0.11.
FightCube.cs
FightCubeAuthoring.csGameStateStartedAuthoring.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 | |
| } | |
| } | |
| } | |
| } | |
| } |