Skip to content

Instantly share code, notes, and snippets.

@andrew-raphael-lukasik
Last active September 12, 2023 11:31
Show Gist options
  • Save andrew-raphael-lukasik/9582abd02b237b30d7abbdeb5c5d5b51 to your computer and use it in GitHub Desktop.
Save andrew-raphael-lukasik/9582abd02b237b30d7abbdeb5c5d5b51 to your computer and use it in GitHub Desktop.
Run thousands of basic² ai agents without and with Unity ECS `1.0.11`

Run thousands of basic² ai agents without and with Unity ECS 1.0.11.

No Unity ECS

  • FightCube.cs

Unity ECS

  • 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
}
}
}
}
}
// src* https://gist.github.com/andrew-raphael-lukasik/9582abd02b237b30d7abbdeb5c5d5b51
using UnityEngine;
using Unity.Entities;
using Unity.Transforms;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Burst;
using Random = Unity.Mathematics.Random;
namespace FightCubesECS
{
// notes:
// - place this component on a mesh and bake it in a SubScene to create a visible entity
// - bake multiple of such entities as a sole one won't have a target to chase
// - remember to use a material with Instancing enabled
// - simulation won't start until GameStateStarted entity exists (see GameStateStartedAuthoring file for details)
[DisallowMultipleComponent]
public class FightCubeAuthoring : MonoBehaviour
{
//[SerializeField][Min(1)] int _health = 1;
//[SerializeField][Min(1)] int _maxHealth = 1;
[SerializeField][Min(0)] float _moveSpeed = 10;
[SerializeField][Min(0)] float _stopDistance = 1;
public class Baker : Baker<FightCubeAuthoring>
{
public override void Bake ( FightCubeAuthoring authoring )
{
Entity entity = GetEntity( authoring , TransformUsageFlags.Dynamic );
AddComponent<IsHuman>( entity );
AddComponent( entity , new HumanState{ Value=HumanState.EValue.Idle } );
//AddComponent( entity , new Health{ Value=authoring._health } );
//AddComponent( entity , new MaxHealth{ Value=authoring._maxHealth } );
AddComponent( entity , new HumanMoveTarget{ Value=Entity.Null } );
AddComponent( entity , new HumanMoveSpeed{ Value=authoring._moveSpeed } );
AddComponent( entity , new HumanMoveStopDistance{ Value=authoring._stopDistance } );
}
}
}
[BurstCompile]
[UpdateInGroup( typeof(SimulationSystemGroup) )]
public partial struct HumanIdleSystem : ISystem
{
[BurstCompile]
public void OnCreate ( ref SystemState state )
{
state.RequireForUpdate<GameStateStarted>();
var singletonArchetype = state.EntityManager.CreateArchetype( new NativeList<ComponentType>(1,Allocator.Temp){ ComponentType.ReadWrite<SystemData>() }.AsArray() );
Entity singleton = state.EntityManager.CreateEntity( singletonArchetype );
SystemAPI.SetSingleton( new SystemData{
Query = state.GetEntityQuery( ComponentType.ReadOnly<IsHuman>() )
} );
}
[BurstCompile]
public void OnDestroy ( ref SystemState state )
{
if( SystemAPI.TryGetSingletonEntity<SystemData>(out Entity singleton) )
state.EntityManager.DestroyEntity( singleton );
}
[BurstCompile]
public void OnUpdate ( ref SystemState state )
{
var systemData = SystemAPI.GetSingleton<SystemData>();
int numEntities = systemData.Query.CalculateEntityCount();
var humanPositions = new NativeArray<float3>( numEntities , Allocator.TempJob );
var humanEntities = new NativeArray<Entity>( numEntities , Allocator.TempJob );
state.Dependency = new FillDataJob
{
HumanPositions = humanPositions ,
HumanEntities = humanEntities ,
}.Schedule( state.Dependency );
state.Dependency = new HumanChooseTargetJob
{
AllHumans = humanEntities ,
AllHumansPosition = humanPositions ,
}.Schedule( state.Dependency );
// release
humanPositions.Dispose( state.Dependency );
humanEntities.Dispose( state.Dependency );
var commandBufferSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>();
var commandBuffer = commandBufferSingleton.CreateCommandBuffer( state.WorldUnmanaged );
var commandBufferParallelWriter = commandBuffer.AsParallelWriter();
state.Dependency = new AttackTargetJob
{
GameTime = SystemAPI.Time.ElapsedTime ,
CommandBuffer = commandBufferParallelWriter ,
}.Schedule( state.Dependency );
}
struct SystemData : IComponentData
{
public EntityQuery Query;
}
}
[BurstCompile]
[UpdateInGroup( typeof(SimulationSystemGroup) )]
[UpdateAfter( typeof(HumanIdleSystem) )]
public partial struct HumanChaseSystem : ISystem
{
[BurstCompile]
public void OnCreate ( ref SystemState state )
{
state.RequireForUpdate<GameStateStarted>();
}
[BurstCompile]
public void OnDestroy ( ref SystemState state ) { }
[BurstCompile]
public void OnUpdate ( ref SystemState state )
{
float deltaTime = SystemAPI.Time.DeltaTime;
var transformsRW = SystemAPI.GetComponentLookup<LocalTransform>( isReadOnly:false );
new ChaseTargetJob
{
DeltaTime = deltaTime ,
TransformsRW = transformsRW,
}.Schedule();
}
}
public struct GameStateStarted : IComponentData {}// singleton to signal the game start
public struct IsHuman : IComponentData {}// tag
public struct HumanState : IComponentData
{
public EValue Value;
public enum EValue : byte { Idle=0, Chasing, Attacking }
}
//public struct Health : IComponentData { public int Value; }
//public struct MaxHealth : IComponentData { public int Value; }
public struct HumanMoveTarget : IComponentData { public Entity Value; }
public struct HumanMoveSpeed : IComponentData { public float Value; }
public struct HumanMoveStopDistance : IComponentData { public float Value; }
[BurstCompile]
[WithAll( typeof(IsHuman) )]
public partial struct FillDataJob : IJobEntity
{
[WriteOnly] public NativeArray<float3> HumanPositions;
[WriteOnly] public NativeArray<Entity> HumanEntities;
void Execute
(
[EntityIndexInQuery] int entityIndexInQuery ,
in Entity entity ,
in LocalTransform transform
)
{
HumanEntities[entityIndexInQuery] = entity;
HumanPositions[entityIndexInQuery] = transform.Position;
}
}
[BurstCompile]
[WithAll( typeof(IsHuman) )]
public partial struct HumanChooseTargetJob : IJobEntity
{
[ReadOnly] public NativeArray<Entity> AllHumans;
[ReadOnly] public NativeArray<float3> AllHumansPosition;
void Execute
(
ref HumanState humanState ,
ref HumanMoveTarget target ,
in Entity entity ,
in LocalTransform transform
)
{
if( humanState.Value!=HumanState.EValue.Idle ) return;
Entity nearestGuy = Entity.Null;
float nearestGuyDistance = float.MaxValue;
float3 pos = transform.Position;
for( int i=0 ; i<AllHumans.Length ; i++ )
{
float dist = math.length( AllHumansPosition[i] - pos );
if(
dist<nearestGuyDistance// is closer than the other guy
&& AllHumans[i]!=entity// and is not me :V
)
{
nearestGuyDistance = dist;
nearestGuy = AllHumans[i];
}
}
if( nearestGuy!=Entity.Null )
{
target.Value = nearestGuy;
humanState.Value = HumanState.EValue.Chasing;
}
}
}
[BurstCompile]
[WithAll( typeof(IsHuman) )]
public partial struct ChaseTargetJob : IJobEntity
{
public float DeltaTime;
public ComponentLookup<LocalTransform> TransformsRW;
void Execute
(
ref HumanState humanState ,
in Entity entity ,
in HumanMoveTarget target ,
in HumanMoveStopDistance stopDistance ,
in HumanMoveSpeed moveSpeed
)
{
if( humanState.Value!=HumanState.EValue.Chasing ) return;
LocalTransform transform = TransformsRW[ entity ];
if( TransformsRW.TryGetComponent( target.Value , out LocalTransform targetTransform ) )
{
float3 toTarget = targetTransform.Position - transform.Position;
float distanceToTarget = math.length( toTarget );
float maxMoveDistance = distanceToTarget - stopDistance.Value;
if( maxMoveDistance>0.0001 )// maxMoveDistance>0 made some entities stuck due to float precision
{
// target not reached yet
float step = math.min( DeltaTime * moveSpeed.Value , maxMoveDistance );
transform.Position += math.normalize(toTarget) * step;
TransformsRW[ entity ] = transform;
}
else
{
// target reached
humanState.Value = HumanState.EValue.Attacking;
}
}
else
{
// target no longer exists
humanState.Value = HumanState.EValue.Idle;
}
}
}
[BurstCompile]
[WithAll( typeof(IsHuman) )]
public partial struct AttackTargetJob : IJobEntity
{
public double GameTime;// source of randomness
public EntityCommandBuffer.ParallelWriter CommandBuffer;
void Execute
(
ref HumanState humanState ,
ref HumanMoveTarget target ,
[EntityIndexInQuery] int entityIndexInQuery ,
in Entity entity
)
{
if( humanState.Value!=HumanState.EValue.Attacking ) return;
Random rnd;
unchecked
{
uint seed = 17;
seed = seed * 23 + (uint)math.abs( entity.Index.GetHashCode() );
seed = seed * 23 + (uint)math.abs( GameTime.GetHashCode() );
rnd = new Random( seed );
};
if( rnd.NextBool() )
{
// attack succeeded
CommandBuffer.DestroyEntity( sortKey:entityIndexInQuery , target.Value );// schedule destroy
// go idle
humanState.Value = HumanState.EValue.Idle;
}
else
{
// attack failed
}
}
}
}
// src* https://gist.github.com/andrew-raphael-lukasik/9582abd02b237b30d7abbdeb5c5d5b51
using UnityEngine;
using Unity.Entities;
namespace FightCubesECS
{
// notes:
// - place this component on a gameObject and bake it in a SubScene to create a entity that will trigger start of the simulation
[DisallowMultipleComponent]
public class GameStateStartedAuthoring : MonoBehaviour
{
public class Baker : Baker<GameStateStartedAuthoring>
{
public override void Bake ( GameStateStartedAuthoring authoring )
{
Entity entity = GetEntity( authoring , TransformUsageFlags.ManualOverride );
AddComponent<GameStateStarted>( entity );
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment