using System; using System.Collections.Generic; using Unity.Collections; using Unity.Mathematics; using UnityEngine; namespace Simulation { public sealed partial class SimulationWorld { private struct EnemyJobInputData { public int EntityId; public float3 Position; public float3 Forward; public quaternion Rotation; public float Speed; public float AttackRange; public bool AvoidEnemyOverlap; public float EnemyBodyRadius; public int SeparationIterations; public int TargetType; public int State; } private struct EnemyJobOutputData { public int EntityId; public float3 Position; public float3 Forward; public quaternion Rotation; public float Speed; public float AttackRange; public bool AvoidEnemyOverlap; public float EnemyBodyRadius; public int SeparationIterations; public int TargetType; public int State; } private struct ProjectileJobInputData { public int EntityId; public int OwnerEntityId; public float3 Position; public float3 Forward; public float3 Velocity; public float Speed; public float LifeTime; public float Age; public bool Active; public float RemainingLifetime; public int State; } private struct ProjectileJobOutputData { public int EntityId; public int OwnerEntityId; public float3 Position; public float3 Forward; public float3 Velocity; public float Speed; public float LifeTime; public float Age; public bool Active; public float RemainingLifetime; public int State; } // Shared broad-phase query payload for CP5 projectile collision and CP6 AOE collision candidates. private struct CollisionQueryData { public int QueryId; public int SourceType; public int SourceEntityId; public int SourceOwnerEntityId; public float3 Position; public float Radius; public int MaxTargets; public int ShapeType; public float3 Direction; public float HalfAngleDeg; } // Shared candidate buffer consumed by the main thread settlement stage. private struct CollisionCandidateData { public int QueryId; public int SourceType; public int SourceEntityId; public int SourceOwnerEntityId; public int TargetEntityId; public float SqrDistance; } private struct AreaCollisionRequestData { public int SourceEntityId; public int SourceOwnerEntityId; public Vector3 Center; public float Radius; public int MaxTargets; public int ShapeType; public Vector3 Direction; public float HalfAngleDeg; } private struct AreaCollisionHitEventData { public int QueryId; public int SourceEntityId; public int SourceOwnerEntityId; public int TargetEntityId; public float SqrDistance; } private const int CollisionSourceTypeProjectile = 1; private const int CollisionSourceTypeArea = 2; private const int CollisionShapeCircle = 0; private const int CollisionShapeSector = 1; private NativeList _enemyJobInputs; private NativeList _enemyJobOutputs; private NativeList _enemyJobSeparationOutputs; private NativeList _enemySeparationPreviousPushes; private NativeList _enemySeparationCurrentPushes; private NativeList _projectileJobInputs; private NativeList _projectileJobOutputs; private NativeList _collisionQueryInputs; private NativeList _collisionCandidates; private NativeParallelMultiHashMap _enemySeparationBuckets; private NativeParallelMultiHashMap _enemyCollisionBuckets; private readonly List _areaCollisionRequests = new(16); private readonly List _areaCollisionHitEvents = new(32); private readonly HashSet _areaCollisionHitDedupKeys = new(); private int _lastCollisionQueryCount; private int _lastProjectileCollisionQueryCount; private int _lastAreaCollisionQueryCount; private int _lastCollisionCandidateCount; private int _lastProjectileCollisionCandidateCount; private int _lastAreaCollisionCandidateCount; private int _lastResolvedAreaHitCount; private float _lastCollisionCellSize; private bool _lastCollisionHasEnemyTargets; public int CollisionCandidateCount => _collisionCandidates.IsCreated ? _collisionCandidates.Length : 0; public int PendingAreaCollisionRequestCount => _areaCollisionRequests.Count; public int LastCollisionQueryCount => _lastCollisionQueryCount; public int LastProjectileCollisionQueryCount => _lastProjectileCollisionQueryCount; public int LastAreaCollisionQueryCount => _lastAreaCollisionQueryCount; public int LastCollisionCandidateCount => _lastCollisionCandidateCount; public int LastProjectileCollisionCandidateCount => _lastProjectileCollisionCandidateCount; public int LastAreaCollisionCandidateCount => _lastAreaCollisionCandidateCount; public int LastResolvedAreaHitCount => _lastResolvedAreaHitCount; public float LastCollisionCellSize => _lastCollisionCellSize; public bool LastCollisionHasEnemyTargets => _lastCollisionHasEnemyTargets; private void InitializeJobDataChannels() { if (AreJobDataChannelsUsable()) { return; } DisposeJobDataChannels(); _enemyJobInputs = new NativeList(64, Allocator.Persistent); _enemyJobOutputs = new NativeList(64, Allocator.Persistent); _enemyJobSeparationOutputs = new NativeList(64, Allocator.Persistent); _enemySeparationPreviousPushes = new NativeList(64, Allocator.Persistent); _enemySeparationCurrentPushes = new NativeList(64, Allocator.Persistent); _projectileJobInputs = new NativeList(64, Allocator.Persistent); _projectileJobOutputs = new NativeList(64, Allocator.Persistent); _collisionQueryInputs = new NativeList(64, Allocator.Persistent); _collisionCandidates = new NativeList(128, Allocator.Persistent); _enemySeparationBuckets = new NativeParallelMultiHashMap(256, Allocator.Persistent); _enemyCollisionBuckets = new NativeParallelMultiHashMap(256, Allocator.Persistent); InitializeEnemyTargetSpatialIndex(); } private void DisposeJobDataChannels() { if (_enemyJobInputs.IsCreated) { _enemyJobInputs.Dispose(); } _enemyJobInputs = default; if (_enemyJobOutputs.IsCreated) { _enemyJobOutputs.Dispose(); } _enemyJobOutputs = default; if (_enemyJobSeparationOutputs.IsCreated) { _enemyJobSeparationOutputs.Dispose(); } _enemyJobSeparationOutputs = default; if (_enemySeparationPreviousPushes.IsCreated) { _enemySeparationPreviousPushes.Dispose(); } _enemySeparationPreviousPushes = default; if (_enemySeparationCurrentPushes.IsCreated) { _enemySeparationCurrentPushes.Dispose(); } _enemySeparationCurrentPushes = default; if (_projectileJobInputs.IsCreated) { _projectileJobInputs.Dispose(); } _projectileJobInputs = default; if (_projectileJobOutputs.IsCreated) { _projectileJobOutputs.Dispose(); } _projectileJobOutputs = default; if (_collisionQueryInputs.IsCreated) { _collisionQueryInputs.Dispose(); } _collisionQueryInputs = default; if (_collisionCandidates.IsCreated) { _collisionCandidates.Dispose(); } _collisionCandidates = default; if (_enemySeparationBuckets.IsCreated) { _enemySeparationBuckets.Dispose(); } _enemySeparationBuckets = default; if (_enemyCollisionBuckets.IsCreated) { _enemyCollisionBuckets.Dispose(); } _enemyCollisionBuckets = default; DisposeEnemyTargetSpatialIndex(); _areaCollisionRequests.Clear(); _areaCollisionHitEvents.Clear(); _areaCollisionHitDedupKeys.Clear(); ResetCollisionRuntimeStats(); } private void ClearJobDataChannels() { if (!AreJobDataChannelsUsable()) { _areaCollisionRequests.Clear(); _areaCollisionHitEvents.Clear(); _areaCollisionHitDedupKeys.Clear(); ResetCollisionRuntimeStats(); return; } if (_enemyJobInputs.IsCreated) { _enemyJobInputs.Clear(); } if (_enemyJobOutputs.IsCreated) { _enemyJobOutputs.Clear(); } if (_projectileJobInputs.IsCreated) { _projectileJobInputs.Clear(); } if (_projectileJobOutputs.IsCreated) { _projectileJobOutputs.Clear(); } if (_collisionQueryInputs.IsCreated) { _collisionQueryInputs.Clear(); } if (_collisionCandidates.IsCreated) { _collisionCandidates.Clear(); } if (_enemyJobSeparationOutputs.IsCreated) { _enemyJobSeparationOutputs.Clear(); } if (_enemySeparationPreviousPushes.IsCreated) { _enemySeparationPreviousPushes.Clear(); } if (_enemySeparationCurrentPushes.IsCreated) { _enemySeparationCurrentPushes.Clear(); } if (_enemySeparationBuckets.IsCreated) { _enemySeparationBuckets.Clear(); } if (_enemyCollisionBuckets.IsCreated) { _enemyCollisionBuckets.Clear(); } ClearEnemyTargetSpatialIndex(); _areaCollisionRequests.Clear(); _areaCollisionHitEvents.Clear(); _areaCollisionHitDedupKeys.Clear(); ResetCollisionRuntimeStats(); } private void ResetCollisionRuntimeStats() { _lastCollisionQueryCount = 0; _lastProjectileCollisionQueryCount = 0; _lastAreaCollisionQueryCount = 0; _lastCollisionCandidateCount = 0; _lastProjectileCollisionCandidateCount = 0; _lastAreaCollisionCandidateCount = 0; _lastResolvedAreaHitCount = 0; _lastCollisionCellSize = 0f; _lastCollisionHasEnemyTargets = false; } private void SyncSimulationToJobInput() { InitializeJobDataChannels(); EnsureCapacity(ref _enemyJobInputs, _enemies.Count); EnsureCapacity(ref _projectileJobInputs, _projectiles.Count); _enemyJobInputs.Clear(); _projectileJobInputs.Clear(); for (int i = 0; i < _enemies.Count; i++) { _enemyJobInputs.Add(ConvertToEnemyJobInput(_enemies[i])); } for (int i = 0; i < _projectiles.Count; i++) { _projectileJobInputs.Add(ConvertToProjectileJobInput(_projectiles[i])); } } private void SyncSimulationToJobOutput() { InitializeJobDataChannels(); EnsureCapacity(ref _enemyJobOutputs, _enemies.Count); EnsureCapacity(ref _projectileJobOutputs, _projectiles.Count); _enemyJobOutputs.Clear(); _projectileJobOutputs.Clear(); for (int i = 0; i < _enemies.Count; i++) { _enemyJobOutputs.Add(ConvertToEnemyJobOutput(_enemies[i])); } for (int i = 0; i < _projectiles.Count; i++) { _projectileJobOutputs.Add(ConvertToProjectileJobOutput(_projectiles[i])); } } private void PrepareEnemyJobOutputBuffer(int enemyCount) { InitializeJobDataChannels(); EnsureCapacity(ref _enemyJobOutputs, enemyCount); _enemyJobOutputs.Clear(); if (enemyCount > 0) { _enemyJobOutputs.ResizeUninitialized(enemyCount); } } private void PrepareProjectileJobOutputBuffer(int projectileCount) { InitializeJobDataChannels(); EnsureCapacity(ref _projectileJobOutputs, projectileCount); _projectileJobOutputs.Clear(); if (projectileCount > 0) { _projectileJobOutputs.ResizeUninitialized(projectileCount); } } private void SyncProjectilesToJobOutput() { InitializeJobDataChannels(); EnsureCapacity(ref _projectileJobOutputs, _projectiles.Count); _projectileJobOutputs.Clear(); for (int i = 0; i < _projectiles.Count; i++) { _projectileJobOutputs.Add(ConvertToProjectileJobOutput(_projectiles[i])); } } private void CopyProjectileInputToOutput() { for (int i = 0; i < _projectileJobInputs.Length; i++) { ProjectileJobInputData input = _projectileJobInputs[i]; _projectileJobOutputs[i] = new ProjectileJobOutputData { EntityId = input.EntityId, OwnerEntityId = input.OwnerEntityId, Position = input.Position, Forward = input.Forward, Velocity = input.Velocity, Speed = input.Speed, LifeTime = input.LifeTime, Age = input.Age, Active = input.Active, RemainingLifetime = input.RemainingLifetime, State = input.State }; } } private void PrepareCollisionCandidateChannels(int queryCount, int expectedCandidateCount, int bucketCapacity) { InitializeJobDataChannels(); EnsureCapacity(ref _collisionQueryInputs, queryCount); EnsureCapacity(ref _collisionCandidates, expectedCandidateCount); EnsureCapacity(ref _enemyCollisionBuckets, bucketCapacity); _collisionQueryInputs.Clear(); _collisionCandidates.Clear(); _enemyCollisionBuckets.Clear(); } private void AddProjectileCollisionQuery(int queryId, in ProjectileJobOutputData projectile, float radius, int maxTargets = 1) { if (!_collisionQueryInputs.IsCreated || radius <= 0f) { return; } _collisionQueryInputs.Add(new CollisionQueryData { QueryId = queryId, SourceType = CollisionSourceTypeProjectile, SourceEntityId = projectile.EntityId, SourceOwnerEntityId = projectile.OwnerEntityId, Position = projectile.Position, Radius = radius, MaxTargets = math.max(1, maxTargets), ShapeType = CollisionShapeCircle, Direction = new float3(0f, 0f, 1f), HalfAngleDeg = 180f }); } private void AddAreaCollisionQuery(int queryId, int sourceEntityId, int sourceOwnerEntityId, in Vector3 center, float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg) { if (!_collisionQueryInputs.IsCreated || radius <= 0f) { return; } Vector3 normalizedDirection = direction; normalizedDirection.y = 0f; if (normalizedDirection.sqrMagnitude <= Mathf.Epsilon) { normalizedDirection = Vector3.forward; } else { normalizedDirection.Normalize(); } _collisionQueryInputs.Add(new CollisionQueryData { QueryId = queryId, SourceType = CollisionSourceTypeArea, SourceEntityId = sourceEntityId, SourceOwnerEntityId = sourceOwnerEntityId, Position = new float3(center.x, center.y, center.z), Radius = radius, MaxTargets = math.max(1, maxTargets), ShapeType = shapeType, Direction = new float3(normalizedDirection.x, normalizedDirection.y, normalizedDirection.z), HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f) }); } private void AddCollisionCandidate(int queryId, int sourceType, int sourceEntityId, int sourceOwnerEntityId, int targetEntityId, float sqrDistance) { if (!_collisionCandidates.IsCreated) { return; } _collisionCandidates.Add(new CollisionCandidateData { QueryId = queryId, SourceType = sourceType, SourceEntityId = sourceEntityId, SourceOwnerEntityId = sourceOwnerEntityId, TargetEntityId = targetEntityId, SqrDistance = sqrDistance }); } private void PrepareEnemySeparationJobBuffers(int enemyCount, int bucketCapacity) { InitializeJobDataChannels(); EnsureCapacity(ref _enemyJobSeparationOutputs, enemyCount); _enemyJobSeparationOutputs.Clear(); if (enemyCount > 0) { _enemyJobSeparationOutputs.ResizeUninitialized(enemyCount); } EnsureCapacity(ref _enemySeparationBuckets, bucketCapacity); _enemySeparationBuckets.Clear(); EnsureCapacity(ref _enemySeparationPreviousPushes, enemyCount); EnsureCapacity(ref _enemySeparationCurrentPushes, enemyCount); if (_enemySeparationPreviousPushes.Length < enemyCount) { int oldLength = _enemySeparationPreviousPushes.Length; _enemySeparationPreviousPushes.ResizeUninitialized(enemyCount); for (int i = oldLength; i < enemyCount; i++) { _enemySeparationPreviousPushes[i] = float2.zero; } } else if (_enemySeparationPreviousPushes.Length > enemyCount) { _enemySeparationPreviousPushes.ResizeUninitialized(enemyCount); } _enemySeparationCurrentPushes.Clear(); if (enemyCount > 0) { _enemySeparationCurrentPushes.ResizeUninitialized(enemyCount); } } private void CommitEnemySeparationTemporalBuffers(int enemyCount) { if (!_enemySeparationPreviousPushes.IsCreated || !_enemySeparationCurrentPushes.IsCreated) { return; } int copyCount = math.min(enemyCount, math.min(_enemySeparationPreviousPushes.Length, _enemySeparationCurrentPushes.Length)); for (int i = 0; i < copyCount; i++) { _enemySeparationPreviousPushes[i] = _enemySeparationCurrentPushes[i]; } } private void OnEnemyAddedToSeparationTemporalBuffers() { if (_enemySeparationPreviousPushes.IsCreated) { _enemySeparationPreviousPushes.Add(float2.zero); } if (_enemySeparationCurrentPushes.IsCreated) { _enemySeparationCurrentPushes.Add(float2.zero); } } private void OnEnemyRemovedFromSeparationTemporalBuffers(int removedIndex) { if (_enemySeparationPreviousPushes.IsCreated && removedIndex >= 0 && removedIndex < _enemySeparationPreviousPushes.Length) { _enemySeparationPreviousPushes.RemoveAtSwapBack(removedIndex); } if (_enemySeparationCurrentPushes.IsCreated && removedIndex >= 0 && removedIndex < _enemySeparationCurrentPushes.Length) { _enemySeparationCurrentPushes.RemoveAtSwapBack(removedIndex); } } private void ApplyJobOutputToSimulation() { int enemyCount = Mathf.Min(_enemies.Count, _enemyJobOutputs.Length); bool hasEnemyPositionChanged = false; for (int i = 0; i < enemyCount; i++) { EnemyJobOutputData output = _enemyJobOutputs[i]; if (!hasEnemyPositionChanged) { Vector3 currentPosition = _enemies[i].Position; hasEnemyPositionChanged = currentPosition.x != output.Position.x || currentPosition.z != output.Position.z; } _enemies[i] = ConvertToEnemySimData(output); } if (hasEnemyPositionChanged) { MarkEnemyTargetSpatialIndexDirty(); } int projectileCount = Mathf.Min(_projectiles.Count, _projectileJobOutputs.Length); for (int i = 0; i < projectileCount; i++) { _projectiles[i] = ConvertToProjectileSimData(_projectileJobOutputs[i]); } } private bool AreJobDataChannelsUsable() { return IsNativeListUsable(_enemyJobInputs) && IsNativeListUsable(_enemyJobOutputs) && IsNativeListUsable(_enemyJobSeparationOutputs) && IsNativeListUsable(_enemySeparationPreviousPushes) && IsNativeListUsable(_enemySeparationCurrentPushes) && IsNativeListUsable(_projectileJobInputs) && IsNativeListUsable(_projectileJobOutputs) && IsNativeListUsable(_collisionQueryInputs) && IsNativeListUsable(_collisionCandidates) && IsNativeMultiHashMapUsable(_enemySeparationBuckets) && IsNativeMultiHashMapUsable(_enemyCollisionBuckets); } private static void EnsureCapacity(ref NativeList nativeList, int targetCount) where T : unmanaged { if (!nativeList.IsCreated || targetCount <= 0) { return; } if (nativeList.Capacity < targetCount) { nativeList.Capacity = targetCount; } } private static bool IsNativeListUsable(NativeList nativeList) where T : unmanaged { if (!nativeList.IsCreated) { return false; } try { _ = nativeList.Length; return true; } catch (ObjectDisposedException) { return false; } } private static bool IsNativeMultiHashMapUsable(NativeParallelMultiHashMap hashMap) { if (!hashMap.IsCreated) { return false; } try { _ = hashMap.Count(); return true; } catch (ObjectDisposedException) { return false; } } private static EnemyJobInputData ConvertToEnemyJobInput(in EnemySimData enemy) { return new EnemyJobInputData { EntityId = enemy.EntityId, Position = new float3(enemy.Position.x, enemy.Position.y, enemy.Position.z), Forward = new float3(enemy.Forward.x, enemy.Forward.y, enemy.Forward.z), Rotation = new quaternion(enemy.Rotation.x, enemy.Rotation.y, enemy.Rotation.z, enemy.Rotation.w), Speed = enemy.Speed, AttackRange = enemy.AttackRange, AvoidEnemyOverlap = enemy.AvoidEnemyOverlap, EnemyBodyRadius = enemy.EnemyBodyRadius, SeparationIterations = enemy.SeparationIterations, TargetType = enemy.TargetType, State = enemy.State }; } private static EnemyJobOutputData ConvertToEnemyJobOutput(in EnemySimData enemy) { return new EnemyJobOutputData { EntityId = enemy.EntityId, Position = new float3(enemy.Position.x, enemy.Position.y, enemy.Position.z), Forward = new float3(enemy.Forward.x, enemy.Forward.y, enemy.Forward.z), Rotation = new quaternion(enemy.Rotation.x, enemy.Rotation.y, enemy.Rotation.z, enemy.Rotation.w), Speed = enemy.Speed, AttackRange = enemy.AttackRange, AvoidEnemyOverlap = enemy.AvoidEnemyOverlap, EnemyBodyRadius = enemy.EnemyBodyRadius, SeparationIterations = enemy.SeparationIterations, TargetType = enemy.TargetType, State = enemy.State }; } private static EnemySimData ConvertToEnemySimData(in EnemyJobOutputData enemy) { return new EnemySimData { EntityId = enemy.EntityId, Position = new Vector3(enemy.Position.x, enemy.Position.y, enemy.Position.z), Forward = new Vector3(enemy.Forward.x, enemy.Forward.y, enemy.Forward.z), Rotation = new Quaternion(enemy.Rotation.value.x, enemy.Rotation.value.y, enemy.Rotation.value.z, enemy.Rotation.value.w), Speed = enemy.Speed, AttackRange = enemy.AttackRange, AvoidEnemyOverlap = enemy.AvoidEnemyOverlap, EnemyBodyRadius = enemy.EnemyBodyRadius, SeparationIterations = enemy.SeparationIterations, TargetType = enemy.TargetType, State = enemy.State }; } private static ProjectileJobInputData ConvertToProjectileJobInput(in ProjectileSimData projectile) { return new ProjectileJobInputData { EntityId = projectile.EntityId, OwnerEntityId = projectile.OwnerEntityId, Position = new float3(projectile.Position.x, projectile.Position.y, projectile.Position.z), Forward = new float3(projectile.Forward.x, projectile.Forward.y, projectile.Forward.z), Velocity = new float3(projectile.Velocity.x, projectile.Velocity.y, projectile.Velocity.z), Speed = projectile.Speed, LifeTime = projectile.LifeTime, Age = projectile.Age, Active = projectile.Active, RemainingLifetime = projectile.RemainingLifetime, State = projectile.State }; } private static ProjectileJobOutputData ConvertToProjectileJobOutput(in ProjectileSimData projectile) { return new ProjectileJobOutputData { EntityId = projectile.EntityId, OwnerEntityId = projectile.OwnerEntityId, Position = new float3(projectile.Position.x, projectile.Position.y, projectile.Position.z), Forward = new float3(projectile.Forward.x, projectile.Forward.y, projectile.Forward.z), Velocity = new float3(projectile.Velocity.x, projectile.Velocity.y, projectile.Velocity.z), Speed = projectile.Speed, LifeTime = projectile.LifeTime, Age = projectile.Age, Active = projectile.Active, RemainingLifetime = projectile.RemainingLifetime, State = projectile.State }; } private static ProjectileSimData ConvertToProjectileSimData(in ProjectileJobOutputData projectile) { return new ProjectileSimData { EntityId = projectile.EntityId, OwnerEntityId = projectile.OwnerEntityId, Position = new Vector3(projectile.Position.x, projectile.Position.y, projectile.Position.z), Forward = new Vector3(projectile.Forward.x, projectile.Forward.y, projectile.Forward.z), Velocity = new Vector3(projectile.Velocity.x, projectile.Velocity.y, projectile.Velocity.z), Speed = projectile.Speed, LifeTime = projectile.LifeTime, Age = projectile.Age, Active = projectile.Active, RemainingLifetime = projectile.RemainingLifetime, State = projectile.State }; } private static void EnsureCapacity(ref NativeParallelMultiHashMap hashMap, int targetCount) { if (!hashMap.IsCreated || targetCount <= 0) { return; } if (hashMap.Capacity < targetCount) { hashMap.Capacity = targetCount; } } } }