第一批重构,主要是物理迁移脚本和部分命名空间的调整

- 结构上迁移文件,将脚本分成 Base/Runtime/Presentation/Procedure/Editor 五大文件夹
- 调整 DataTable 和 Definition 下脚本的命名空间
- 依据 UI-5层架构设计规范 拆分原先的 UI 到 Runtime 和 Presentation 下
This commit is contained in:
SepComet
2026-06-02 19:12:34 +08:00
parent 56f74eee9a
commit 34c0784190
655 changed files with 1272 additions and 516 deletions
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5f28ff313e954720a04246191b7f9ddd
timeCreated: 1771901064
@@ -0,0 +1,133 @@
using Unity.Mathematics;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Collision Transient
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 ResetCollisionRuntimeStats()
{
_lastCollisionQueryCount = 0;
_lastProjectileCollisionQueryCount = 0;
_lastAreaCollisionQueryCount = 0;
_lastCollisionCandidateCount = 0;
_lastProjectileCollisionCandidateCount = 0;
_lastAreaCollisionCandidateCount = 0;
_lastResolvedAreaHitCount = 0;
_lastCollisionCellSize = 0f;
_lastCollisionHasEnemyTargets = false;
}
private void PrepareCollisionQueryAndCandidateChannels(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,
SourceWasActiveAtQueryTime = true,
Position = projectile.Position,
Radius = radius,
MaxTargets = math.max(1, maxTargets),
ShapeType = CollisionShapeCircle,
Direction = new float3(0f, 0f, 1f),
HalfAngleDeg = 180f,
HalfWidth = 0f,
HalfLength = 0f
});
}
private void AddAreaCollisionQuery(int queryId, int sourceEntityId, int sourceOwnerEntityId,
bool sourceWasActiveAtQueryTime, in Vector3 center, float radius, int maxTargets, int shapeType,
in Vector3 direction, float halfAngleDeg, float halfWidth, float halfLength)
{
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,
SourceWasActiveAtQueryTime = sourceWasActiveAtQueryTime,
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),
HalfWidth = Mathf.Max(0f, halfWidth),
HalfLength = Mathf.Max(0f, halfLength)
});
}
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
});
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b0eaf024db24511988ba3bde25359e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,91 @@
using Unity.Mathematics;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Enemy Separation Temporal
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);
}
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b2c8675358384b21925e6f660ce61a79
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using Unity.Collections;
namespace Simulation
{
public sealed partial class SimulationWorld
{
// Shared channel constants plus compatibility fields still reflected by tests.
private const int CollisionSourceTypeProjectile = 1;
private const int CollisionSourceTypeArea = 2;
private const int CollisionShapeCircle = 0;
private const int CollisionShapeSector = 1;
private const int CollisionShapeRectangle = 2;
// Kept as top-level fields because current regression tests reflect them directly.
private NativeList<CollisionQueryData> _collisionQueryInputs;
private readonly List<AreaCollisionRequestData> _areaCollisionRequests = new(16);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 20926de0e6e14c7b818779593f1f87bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,151 @@
using Unity.Mathematics;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Job Data Conversion
private void SyncSimulationStateToJobInputs()
{
InitializeJobDataChannels();
EnsureCapacity(ref _enemyJobInputs, _enemies.Count);
EnsureCapacity(ref _projectileJobInputs, _projectiles.Count);
_enemyJobInputs.Clear();
_projectileJobInputs.Clear();
foreach (EnemySimData data in _enemies)
{
_enemyJobInputs.Add(ConvertToEnemyJobInput(data));
}
foreach (ProjectileSimData data in _projectiles)
{
_projectileJobInputs.Add(ConvertToProjectileJobInput(data));
}
}
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 CopyProjectileInputsToOutputs()
{
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 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 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 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
};
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 57a3f582d9c04803908205e0cd4b5b75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,271 @@
using System;
using Unity.Collections;
using Unity.Mathematics;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Job Data Lifecycle
private void InitializeJobDataChannels()
{
if (AreJobDataChannelsUsable())
{
return;
}
DisposeJobDataChannels();
_enemyJobInputs = new NativeList<EnemyJobInputData>(64, Allocator.Persistent);
_enemyJobOutputs = new NativeList<EnemyJobOutputData>(64, Allocator.Persistent);
_enemyJobSeparationOutputs = new NativeList<EnemyJobOutputData>(64, Allocator.Persistent);
_enemySeparationPreviousPushes = new NativeList<float2>(64, Allocator.Persistent);
_enemySeparationCurrentPushes = new NativeList<float2>(64, Allocator.Persistent);
_projectileJobInputs = new NativeList<ProjectileJobInputData>(64, Allocator.Persistent);
_projectileJobOutputs = new NativeList<ProjectileJobOutputData>(64, Allocator.Persistent);
_collisionQueryInputs = new NativeList<CollisionQueryData>(64, Allocator.Persistent);
_collisionCandidates = new NativeList<CollisionCandidateData>(128, Allocator.Persistent);
_enemySeparationBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
_enemyCollisionBuckets = new NativeParallelMultiHashMap<long, int>(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 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<T>(ref NativeList<T> nativeList, int targetCount) where T : unmanaged
{
if (!nativeList.IsCreated || targetCount <= 0)
{
return;
}
if (nativeList.Capacity < targetCount)
{
nativeList.Capacity = targetCount;
}
}
private static void EnsureCapacity(ref NativeParallelMultiHashMap<long, int> hashMap, int targetCount)
{
if (!hashMap.IsCreated || targetCount <= 0)
{
return;
}
if (hashMap.Capacity < targetCount)
{
hashMap.Capacity = targetCount;
}
}
private static bool IsNativeListUsable<T>(NativeList<T> nativeList) where T : unmanaged
{
if (!nativeList.IsCreated)
{
return false;
}
try
{
_ = nativeList.Length;
return true;
}
catch (ObjectDisposedException)
{
return false;
}
}
private static bool IsNativeMultiHashMapUsable(NativeParallelMultiHashMap<long, int> hashMap)
{
if (!hashMap.IsCreated)
{
return false;
}
try
{
_ = hashMap.Count();
return true;
}
catch (ObjectDisposedException)
{
return false;
}
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b2e46cc42cdd45f5badd02bd4635ba97
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Job Output Commit
private void ApplyJobOutputsToSimulationState()
{
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]);
}
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 59ae2375305641a6a36d1bea0bf1abc2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,86 @@
using System.Collections.Generic;
namespace Simulation
{
public sealed class EntityBinding
{
private readonly Dictionary<int, int> _entityIdToSimulationIndex = new Dictionary<int, int>();
private readonly Dictionary<int, int> _simulationIndexToEntityId = new Dictionary<int, int>();
public int Count => _entityIdToSimulationIndex.Count;
public void Bind(int entityId, int simulationIndex)
{
if (_entityIdToSimulationIndex.TryGetValue(entityId, out int oldSimulationIndex))
{
_simulationIndexToEntityId.Remove(oldSimulationIndex);
}
if (_simulationIndexToEntityId.TryGetValue(simulationIndex, out int oldEntityId))
{
_entityIdToSimulationIndex.Remove(oldEntityId);
}
_entityIdToSimulationIndex[entityId] = simulationIndex;
_simulationIndexToEntityId[simulationIndex] = entityId;
}
public void RemapIndex(int entityId, int newSimulationIndex)
{
if (!_entityIdToSimulationIndex.TryGetValue(entityId, out int oldSimulationIndex))
{
return;
}
if (_simulationIndexToEntityId.TryGetValue(newSimulationIndex, out int oldEntityId) &&
oldEntityId != entityId)
{
_entityIdToSimulationIndex.Remove(oldEntityId);
}
_simulationIndexToEntityId.Remove(oldSimulationIndex);
_entityIdToSimulationIndex[entityId] = newSimulationIndex;
_simulationIndexToEntityId[newSimulationIndex] = entityId;
}
public bool TryGetSimulationIndex(int entityId, out int simulationIndex)
{
return _entityIdToSimulationIndex.TryGetValue(entityId, out simulationIndex);
}
public bool TryGetEntityId(int simulationIndex, out int entityId)
{
return _simulationIndexToEntityId.TryGetValue(simulationIndex, out entityId);
}
public bool UnbindByEntityId(int entityId)
{
if (!_entityIdToSimulationIndex.TryGetValue(entityId, out int simulationIndex))
{
return false;
}
_entityIdToSimulationIndex.Remove(entityId);
_simulationIndexToEntityId.Remove(simulationIndex);
return true;
}
public bool UnbindBySimulationIndex(int simulationIndex)
{
if (!_simulationIndexToEntityId.TryGetValue(simulationIndex, out int entityId))
{
return false;
}
_simulationIndexToEntityId.Remove(simulationIndex);
_entityIdToSimulationIndex.Remove(entityId);
return true;
}
public void Clear()
{
_entityIdToSimulationIndex.Clear();
_simulationIndexToEntityId.Clear();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c4f6d93561db4b9092ab61182f2983d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3f549ca9decb4f38933329abacbf78ac
timeCreated: 1771901120
@@ -0,0 +1,14 @@
namespace Simulation
{
public sealed partial class SimulationWorld
{
private struct AreaCollisionHitEventData
{
public int QueryId;
public int SourceEntityId;
public int SourceOwnerEntityId;
public int TargetEntityId;
public float SqrDistance;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5140d989cba042e6bcb217893491273d
timeCreated: 1771901785
@@ -0,0 +1,22 @@
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
private struct AreaCollisionRequestData
{
public int SourceEntityId;
public int SourceOwnerEntityId;
public bool SourceWasActiveAtQueryTime;
public Vector3 Center;
public float Radius;
public int MaxTargets;
public int ShapeType;
public Vector3 Direction;
public float HalfAngleDeg;
public float HalfWidth;
public float HalfLength;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e32f4e0724314c70882f5ed043a4dca4
timeCreated: 1771901749
@@ -0,0 +1,48 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace Simulation
{
public sealed partial class SimulationWorld
{
[BurstCompile]
private struct BuildEnemySeparationBucketsBurstJob : IJobParallelFor
{
[ReadOnly] public NativeArray<EnemyJobOutputData> Inputs;
public NativeParallelMultiHashMap<long, int>.ParallelWriter Buckets;
public float CellSize;
public void Execute(int index)
{
BuildEnemySeparationBucket(
index,
Inputs,
Buckets,
CellSize
);
}
}
private static void BuildEnemySeparationBucket(
int index,
NativeArray<EnemyJobOutputData> inputs,
NativeParallelMultiHashMap<long, int>.ParallelWriter buckets,
float cellSize
)
{
EnemyJobOutputData output = inputs[index];
if (!output.AvoidEnemyOverlap)
{
return;
}
float3 position = output.Position;
position.y = 0f;
int cellX = (int)math.floor(position.x / cellSize);
int cellZ = (int)math.floor(position.z / cellSize);
buckets.Add(SeparationCellKey(cellX, cellZ), index);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e833e91677bb49a4a458c9c8b90099e9
timeCreated: 1771901500
@@ -0,0 +1,15 @@
namespace Simulation
{
public sealed partial class SimulationWorld
{
private struct CollisionCandidateData
{
public int QueryId;
public int SourceType;
public int SourceEntityId;
public int SourceOwnerEntityId;
public int TargetEntityId;
public float SqrDistance;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 453c14964a4b4aed89c0abd7b25cd26c
timeCreated: 1771901722
@@ -0,0 +1,24 @@
using Unity.Mathematics;
namespace Simulation
{
public sealed partial class SimulationWorld
{
private struct CollisionQueryData
{
public int QueryId;
public int SourceType;
public int SourceEntityId;
public int SourceOwnerEntityId;
public bool SourceWasActiveAtQueryTime;
public float3 Position;
public float Radius;
public int MaxTargets;
public int ShapeType;
public float3 Direction;
public float HalfAngleDeg;
public float HalfWidth;
public float HalfLength;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 58abe0d4fd7a49acb716fbd26e0fb19f
timeCreated: 1771901691
@@ -0,0 +1,22 @@
using Unity.Mathematics;
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;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 171936b8a6b84a8fbacc7651ce473b98
timeCreated: 1771901146
@@ -0,0 +1,22 @@
using Unity.Mathematics;
namespace Simulation
{
public sealed partial class SimulationWorld
{
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;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 34ef6fbf123f45ca88d8e7ab8311d543
timeCreated: 1771901204
@@ -0,0 +1,93 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace Simulation
{
public sealed partial class SimulationWorld
{
[BurstCompile]
private struct EnemyMovementBurstJob : IJobParallelFor
{
[ReadOnly] public NativeArray<EnemyJobInputData> Inputs;
public NativeArray<EnemyJobOutputData> Outputs;
public float DeltaTime;
public float3 PlayerPosition;
public void Execute(int index)
{
ExecuteEnemyMovement(
index,
Inputs,
Outputs,
DeltaTime,
PlayerPosition
);
}
}
private static void ExecuteEnemyMovement(
int index,
NativeArray<EnemyJobInputData> inputs,
NativeArray<EnemyJobOutputData> outputs,
float deltaTime,
float3 playerPosition
)
{
EnemyJobInputData input = inputs[index];
float attackRange = input.AttackRange > 0f ? input.AttackRange : DefaultAttackRange;
float attackRangeSqr = attackRange * attackRange;
float3 currentPosition = input.Position;
float3 horizontalPosition = new float3(currentPosition.x, 0f, currentPosition.z);
float3 toPlayer = playerPosition - horizontalPosition;
float sqrDistance = math.lengthsq(toPlayer);
bool isInAttackRange = sqrDistance <= attackRangeSqr;
bool canChase = !isInAttackRange && input.Speed > 0f && sqrDistance > float.Epsilon;
float3 forward = input.Forward;
float3 desiredPosition = currentPosition;
quaternion rotation = input.Rotation;
if (canChase)
{
forward = math.normalizesafe(toPlayer, forward);
desiredPosition = currentPosition + forward * input.Speed * deltaTime;
if (math.lengthsq(forward) > float.Epsilon)
{
rotation = quaternion.LookRotationSafe(forward, math.up());
}
}
int nextState;
if (isInAttackRange)
{
nextState = EnemyStateInAttackRange;
}
else if (canChase)
{
nextState = EnemyStateChasing;
}
else
{
nextState = EnemyStateIdle;
}
outputs[index] = new EnemyJobOutputData
{
EntityId = input.EntityId,
Position = desiredPosition,
Forward = forward,
Rotation = rotation,
Speed = input.Speed,
AttackRange = attackRange,
AvoidEnemyOverlap = input.AvoidEnemyOverlap,
EnemyBodyRadius = input.EnemyBodyRadius,
SeparationIterations = input.SeparationIterations,
TargetType = input.TargetType,
State = nextState
};
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: abd5519779ed41d4947280eec326ffb1
timeCreated: 1771901472
@@ -0,0 +1,245 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace Simulation
{
public sealed partial class SimulationWorld
{
[BurstCompile]
private struct EnemySeparationBurstJob : IJobParallelFor
{
[ReadOnly] public NativeArray<EnemyJobOutputData> Inputs;
[ReadOnly] public NativeParallelMultiHashMap<long, int> Buckets;
[ReadOnly] public NativeArray<float2> PreviousPushes;
public NativeArray<EnemyJobOutputData> Outputs;
public NativeArray<float2> CurrentPushes;
public float CellSize;
public float MaxRadius;
public float3 PlayerPosition;
public float PushDamping;
public float MaxStepScale;
public bool UseTangentialInAttackRange;
public float PushSmoothing;
public void Execute(int index)
{
ExecuteEnemySeparation(
index,
Inputs,
Buckets,
Outputs,
CellSize,
MaxRadius,
PlayerPosition,
PushDamping,
MaxStepScale,
UseTangentialInAttackRange,
PreviousPushes,
CurrentPushes,
PushSmoothing
);
}
}
private static void ExecuteEnemySeparation(
int index,
NativeArray<EnemyJobOutputData> inputs,
NativeParallelMultiHashMap<long, int> buckets,
NativeArray<EnemyJobOutputData> outputs,
float cellSize,
float maxRadius,
float3 playerPosition,
float pushDamping,
float maxStepScale,
bool useTangentialInAttackRange,
NativeArray<float2> previousPushes,
NativeArray<float2> currentPushes,
float pushSmoothing
)
{
currentPushes[index] = float2.zero;
EnemyJobOutputData self = inputs[index];
if (!self.AvoidEnemyOverlap)
{
outputs[index] = self;
return;
}
float3 candidate = self.Position;
candidate.y = 0f;
float3 original = candidate;
float3 fallback =
math.normalizesafe(new float3(self.Forward.x, 0f, self.Forward.z), new float3(1f, 0f, 0f));
float selfRadius = self.EnemyBodyRadius > 0f ? self.EnemyBodyRadius : 0.45f;
int iterations = self.SeparationIterations > 0 ? self.SeparationIterations : 1;
int queryRange = math.max(1, (int)math.ceil((selfRadius + maxRadius) / cellSize));
for (int iter = 0; iter < iterations; iter++)
{
int cellX = (int)math.floor(candidate.x / cellSize);
int cellZ = (int)math.floor(candidate.z / cellSize);
float3 pushAccumulation = float3.zero;
for (int dx = -queryRange; dx <= queryRange; dx++)
{
for (int dz = -queryRange; dz <= queryRange; dz++)
{
long key = SeparationCellKey(cellX + dx, cellZ + dz);
if (!buckets.TryGetFirstValue(key, out int otherIndex,
out NativeParallelMultiHashMapIterator<long> iterator))
{
continue;
}
do
{
if (otherIndex == index)
{
continue;
}
EnemyJobOutputData other = inputs[otherIndex];
if (!other.AvoidEnemyOverlap)
{
continue;
}
float otherRadius = other.EnemyBodyRadius > 0f ? other.EnemyBodyRadius : 0.45f;
float minDistance = selfRadius + otherRadius;
float minDistanceSqr = minDistance * minDistance;
float3 otherPosition = other.Position;
otherPosition.y = 0f;
float3 toSelf = candidate - otherPosition;
float sqrDistance = math.lengthsq(toSelf);
if (sqrDistance <= float.Epsilon)
{
float3 zeroDistanceAxis = GetZeroDistanceSeparationAxis(index, otherIndex);
float directionSign = index < otherIndex ? 1f : -1f;
pushAccumulation += zeroDistanceAxis * (selfRadius * 0.25f * directionSign);
continue;
}
if (sqrDistance >= minDistanceSqr)
{
continue;
}
float distance = math.sqrt(sqrDistance);
float penetration = minDistance - distance;
pushAccumulation += (toSelf / distance) * penetration;
} while (buckets.TryGetNextValue(out otherIndex, ref iterator));
}
}
if (math.lengthsq(pushAccumulation) <= float.Epsilon)
{
continue;
}
float3 resolvedPush = pushAccumulation * pushDamping;
float maxStep = selfRadius * maxStepScale;
float pushLength = math.length(resolvedPush);
if (pushLength > maxStep && pushLength > float.Epsilon)
{
resolvedPush = resolvedPush / pushLength * maxStep;
}
candidate += resolvedPush;
}
float3 framePush = candidate - original;
float2 previousPush2 = previousPushes[index];
float3 previousPush = new float3(previousPush2.x, 0f, previousPush2.y);
float3 smoothedPush = SmoothSeparationPush(framePush, previousPush, pushSmoothing);
if (useTangentialInAttackRange && self.State == EnemyStateInAttackRange)
{
smoothedPush = ProjectToTangential(smoothedPush, playerPosition, original);
}
float maxTotalStep = selfRadius * maxStepScale * iterations;
float smoothedLength = math.length(smoothedPush);
if (smoothedLength > maxTotalStep && smoothedLength > float.Epsilon)
{
smoothedPush = smoothedPush / smoothedLength * maxTotalStep;
}
float3 finalPosition = original + smoothedPush;
currentPushes[index] = new float2(smoothedPush.x, smoothedPush.z);
self.Position = new float3(finalPosition.x, self.Position.y, finalPosition.z);
if (math.lengthsq(smoothedPush) > float.Epsilon)
{
self.Forward = new float3(fallback.x, self.Forward.y, fallback.z);
}
outputs[index] = self;
}
private static float3 SmoothSeparationPush(float3 framePush, float3 previousPush, float pushSmoothing)
{
float frameLengthSqr = math.lengthsq(framePush);
float previousLengthSqr = math.lengthsq(previousPush);
if (frameLengthSqr <= float.Epsilon)
{
return float3.zero;
}
if (previousLengthSqr <= float.Epsilon || pushSmoothing <= 0f)
{
return framePush;
}
float frameLength = math.sqrt(frameLengthSqr);
float previousLength = math.sqrt(previousLengthSqr);
float3 frameDirection = framePush / frameLength;
float3 previousDirection = previousPush / previousLength;
float directionAlignment = math.dot(frameDirection, previousDirection);
if (directionAlignment >= 0.35f)
{
return framePush;
}
float directionalFactor = math.saturate((0.35f - directionAlignment) / 1.35f);
float smoothingStrength = pushSmoothing * directionalFactor;
return math.lerp(framePush, previousPush, smoothingStrength);
}
private static float3 ProjectToTangential(float3 push, float3 playerPosition, float3 currentPosition)
{
if (math.lengthsq(push) <= float.Epsilon)
{
return push;
}
float3 toPlayer = playerPosition - currentPosition;
float toPlayerSqr = math.lengthsq(toPlayer);
if (toPlayerSqr <= float.Epsilon)
{
return push;
}
float3 radialDirection = toPlayer / math.sqrt(toPlayerSqr);
float radialOffset = math.dot(push, radialDirection);
return push - radialDirection * radialOffset;
}
private static float3 GetZeroDistanceSeparationAxis(int index, int otherIndex)
{
int lowIndex = math.min(index, otherIndex);
int highIndex = math.max(index, otherIndex);
uint pairHash = (uint)(lowIndex * 73856093) ^ (uint)(highIndex * 19349663);
float axisX = (pairHash & 1023u) / 511.5f - 1f;
float axisZ = ((pairHash >> 10) & 1023u) / 511.5f - 1f;
float3 axis = new float3(axisX, 0f, axisZ);
return math.normalizesafe(axis, new float3(1f, 0f, 0f));
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 94a77477c3a8410cb58b5028dd5d2c63
timeCreated: 1771901543
@@ -0,0 +1,22 @@
using Unity.Mathematics;
namespace Simulation
{
public sealed partial class SimulationWorld
{
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;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 334ca3869d7940058256ba5419573637
timeCreated: 1771901235
@@ -0,0 +1,22 @@
using Unity.Mathematics;
namespace Simulation
{
public sealed partial class SimulationWorld
{
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;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b5c4a144914548379d9991d8ef061da7
timeCreated: 1771901266
@@ -0,0 +1,118 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace Simulation
{
public sealed partial class SimulationWorld
{
[BurstCompile]
private struct ProjectileMovementBurstJob : IJobParallelFor
{
[ReadOnly] public NativeArray<ProjectileJobInputData> Inputs;
public NativeArray<ProjectileJobOutputData> Outputs;
public float DeltaTime;
public float3 PlayerPosition;
public float MaxSqrDistanceFromPlayer;
public float MaxVerticalOffsetFromPlayer;
public void Execute(int index)
{
ExecuteProjectileMovement(
index,
Inputs,
Outputs,
DeltaTime,
PlayerPosition,
MaxSqrDistanceFromPlayer,
MaxVerticalOffsetFromPlayer);
}
}
private static void ExecuteProjectileMovement(
int index,
NativeArray<ProjectileJobInputData> inputs,
NativeArray<ProjectileJobOutputData> outputs,
float deltaTime,
float3 playerPosition,
float maxSqrDistanceFromPlayer,
float maxVerticalOffsetFromPlayer)
{
ProjectileJobInputData input = inputs[index];
ProjectileJobOutputData output = 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
};
if (!input.Active)
{
output.State = ProjectileStateExpired;
outputs[index] = output;
return;
}
float3 position = input.Position;
float3 forward = input.Forward;
float3 velocity = input.Velocity;
if (math.lengthsq(velocity) <= float.Epsilon && input.Speed > 0f)
{
float3 moveDirection = math.normalizesafe(forward, new float3(0f, 0f, 1f));
velocity = moveDirection * input.Speed;
}
float3 nextPosition = position + velocity * deltaTime;
float nextAge = math.max(0f, input.Age + deltaTime);
float nextRemainingLifetime = input.RemainingLifetime;
bool shouldExpire = false;
if (input.LifeTime > 0f)
{
nextRemainingLifetime = math.max(0f, input.LifeTime - nextAge);
shouldExpire = nextAge >= input.LifeTime;
}
else if (input.RemainingLifetime > 0f)
{
nextRemainingLifetime = math.max(0f, input.RemainingLifetime - deltaTime);
shouldExpire = nextRemainingLifetime <= float.Epsilon;
}
if (!shouldExpire && maxSqrDistanceFromPlayer > 0f)
{
float3 horizontalDelta =
new float3(nextPosition.x - playerPosition.x, 0f, nextPosition.z - playerPosition.z);
shouldExpire = math.lengthsq(horizontalDelta) > maxSqrDistanceFromPlayer;
}
if (!shouldExpire && maxVerticalOffsetFromPlayer > 0f)
{
shouldExpire = math.abs(nextPosition.y - playerPosition.y) > maxVerticalOffsetFromPlayer;
}
output.Position = nextPosition;
output.Velocity = velocity;
output.Age = nextAge;
output.RemainingLifetime = nextRemainingLifetime;
output.Active = !shouldExpire;
output.State = shouldExpire ? ProjectileStateExpired : ProjectileStateActive;
if (math.lengthsq(velocity) > float.Epsilon)
{
float3 moveForward = math.normalizesafe(velocity, forward);
output.Forward = moveForward;
}
outputs[index] = output;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 49001e849c1b4afdaa37d851b9b76866
timeCreated: 1771901884
@@ -0,0 +1,132 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace Simulation
{
public sealed partial class SimulationWorld
{
[BurstCompile]
private struct QueryCollisionCandidatesBurstJob : IJobParallelFor
{
[ReadOnly] public NativeArray<CollisionQueryData> Queries;
[ReadOnly] public NativeParallelMultiHashMap<long, int> EnemyBuckets;
[ReadOnly] public NativeArray<EnemyJobOutputData> EnemyOutputs;
public NativeList<CollisionCandidateData>.ParallelWriter Candidates;
public bool HasEnemyTargets;
public bool HasPlayerTarget;
public int PlayerTargetEntityId;
public float3 PlayerPosition;
public float CellSize;
public void Execute(int index)
{
CollisionQueryData query = Queries[index];
int centerCellX = (int)math.floor(query.Position.x / CellSize);
int centerCellZ = (int)math.floor(query.Position.z / CellSize);
int queryRange = math.max(1, (int)math.ceil(query.Radius / CellSize));
int selectedCount = 0;
bool reachedLimit = false;
if (HasPlayerTarget && query.SourceEntityId != PlayerTargetEntityId &&
query.SourceOwnerEntityId != PlayerTargetEntityId)
{
float3 playerPosition = PlayerPosition;
if (query.SourceType == CollisionSourceTypeArea)
{
playerPosition.y = query.Position.y;
}
float3 playerDelta = playerPosition - query.Position;
float playerSqrDistance = math.lengthsq(playerDelta);
float playerRadiusSqr = query.Radius * query.Radius;
if (playerSqrDistance <= playerRadiusSqr)
{
Candidates.AddNoResize(new CollisionCandidateData
{
QueryId = query.QueryId,
SourceType = query.SourceType,
SourceEntityId = query.SourceEntityId,
SourceOwnerEntityId = query.SourceOwnerEntityId,
TargetEntityId = PlayerTargetEntityId,
SqrDistance = playerSqrDistance
});
selectedCount++;
if (selectedCount >= query.MaxTargets)
{
reachedLimit = true;
}
}
}
if (!HasEnemyTargets || reachedLimit)
{
return;
}
for (int dx = -queryRange; dx <= queryRange && !reachedLimit; dx++)
{
for (int dz = -queryRange; dz <= queryRange && !reachedLimit; dz++)
{
long key = SeparationCellKey(centerCellX + dx, centerCellZ + dz);
if (!EnemyBuckets.TryGetFirstValue(key, out int enemyIndex,
out NativeParallelMultiHashMapIterator<long> iterator))
{
continue;
}
do
{
if (enemyIndex < 0 || enemyIndex >= EnemyOutputs.Length)
{
continue;
}
EnemyJobOutputData enemy = EnemyOutputs[enemyIndex];
if (enemy.EntityId == query.SourceOwnerEntityId)
{
continue;
}
float deltaY = query.SourceType == CollisionSourceTypeArea
? 0f
: enemy.Position.y - query.Position.y;
float3 delta = new float3(
enemy.Position.x - query.Position.x,
deltaY,
enemy.Position.z - query.Position.z);
float sqrDistance = math.lengthsq(delta);
float targetRadius = query.SourceType == CollisionSourceTypeArea
? math.max(0f, enemy.EnemyBodyRadius)
: 0f;
float effectiveRadius = query.Radius + targetRadius;
if (sqrDistance > effectiveRadius * effectiveRadius)
{
continue;
}
Candidates.AddNoResize(new CollisionCandidateData
{
QueryId = query.QueryId,
SourceType = query.SourceType,
SourceEntityId = query.SourceEntityId,
SourceOwnerEntityId = query.SourceOwnerEntityId,
TargetEntityId = enemy.EntityId,
SqrDistance = sqrDistance
});
selectedCount++;
if (selectedCount >= query.MaxTargets)
{
reachedLimit = true;
break;
}
} while (EnemyBuckets.TryGetNextValue(out enemyIndex, ref iterator));
}
}
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 81aa43bb35f547c48b867eafa17a3857
timeCreated: 1771914779
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c66a8c10fffb404c9f4819adffea45fd
timeCreated: 1771900893
@@ -0,0 +1,242 @@
using CustomDebugger;
using Entity;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Collision Broad Phase
private void PrepareCollisionCandidatesForFrame()
{
_collisionCandidateQueryScheduled = false;
if (!_collisionQueryInputs.IsCreated || !_collisionCandidates.IsCreated ||
!_enemyCollisionBuckets.IsCreated)
{
ResetCollisionRuntimeStats();
ClearAreaCollisionTransientBuffers();
return;
}
int projectileCount = _projectileJobOutputs.Length;
int areaQueryCount = _areaCollisionRequests.Count;
if (projectileCount == 0 && areaQueryCount == 0)
{
ResetCollisionRuntimeStats();
return;
}
float queryRadius = Mathf.Max(0.01f, _projectileCollisionQueryRadius);
int maxCandidatesPerQuery = Mathf.Max(1, _projectileMaxCandidatesPerQuery);
float maxQueryRadius = queryRadius;
int queryId = 0;
int projectileQueryCount = 0;
int builtAreaQueryCount = 0;
using (CustomProfilerMarker.Collision_BuildQueries.Auto())
{
for (int i = 0; i < projectileCount; i++)
{
ProjectileJobOutputData projectile = _projectileJobOutputs[i];
if (!projectile.Active || projectile.State != ProjectileStateActive)
{
continue;
}
AddProjectileCollisionQuery(queryId, in projectile, queryRadius, maxCandidatesPerQuery);
queryId++;
projectileQueryCount++;
}
for (int i = 0; i < areaQueryCount; i++)
{
AreaCollisionRequestData request = _areaCollisionRequests[i];
AddAreaCollisionQuery(queryId, request.SourceEntityId, request.SourceOwnerEntityId,
request.SourceWasActiveAtQueryTime, in request.Center, request.Radius, request.MaxTargets,
request.ShapeType, in request.Direction, request.HalfAngleDeg, request.HalfWidth,
request.HalfLength);
queryId++;
builtAreaQueryCount++;
if (request.Radius > maxQueryRadius)
{
maxQueryRadius = request.Radius;
}
}
}
_lastProjectileCollisionQueryCount = projectileQueryCount;
_lastAreaCollisionQueryCount = builtAreaQueryCount;
_lastCollisionQueryCount = projectileQueryCount + builtAreaQueryCount;
_lastResolvedAreaHitCount = 0;
if (_collisionQueryInputs.Length == 0)
{
_lastCollisionCandidateCount = 0;
_lastProjectileCollisionCandidateCount = 0;
_lastAreaCollisionCandidateCount = 0;
_lastCollisionCellSize = 0f;
_lastCollisionHasEnemyTargets = _enemyJobOutputs.Length > 0;
return;
}
float autoCellSize = maxQueryRadius * 2f;
float configuredCellSize = _projectileCollisionCellSize > 0f ? _projectileCollisionCellSize : autoCellSize;
float cellSize = Mathf.Max(0.1f, configuredCellSize);
bool hasEnemyTargets = _enemyJobOutputs.Length > 0;
_lastCollisionCellSize = cellSize;
_lastCollisionHasEnemyTargets = hasEnemyTargets;
using (CustomProfilerMarker.Collision_BuildBuckets.Auto())
{
if (hasEnemyTargets)
{
BuildEnemyCollisionBuckets(cellSize);
}
}
using (CustomProfilerMarker.Collision_QueryCandidates.Auto())
{
_collisionCandidateQueryScheduled = ScheduleCollisionCandidateQueryJob(cellSize, hasEnemyTargets,
out _collisionCandidateQueryHandle);
}
}
private void CompleteCollisionCandidatesForFrame()
{
if (_collisionCandidateQueryScheduled)
{
_collisionCandidateQueryHandle.Complete();
_collisionCandidateQueryScheduled = false;
}
CountCollisionCandidatesBySourceType(out int projectileCandidateCount, out int areaCandidateCount);
_lastProjectileCollisionCandidateCount = projectileCandidateCount;
_lastAreaCollisionCandidateCount = areaCandidateCount;
_lastCollisionCandidateCount = projectileCandidateCount + areaCandidateCount;
}
private void BuildEnemyCollisionBuckets(float cellSize)
{
_enemyCollisionBuckets.Clear();
int enemyCount = _enemyJobOutputs.Length;
if (enemyCount <= 0)
{
return;
}
BuildCollisionBucketsBurstJob job = new BuildCollisionBucketsBurstJob
{
EnemyOutputs = _enemyJobOutputs.AsArray(),
Buckets = _enemyCollisionBuckets.AsParallelWriter(),
CellSize = cellSize
};
using (CustomProfilerMarker.TickEnemies_Complete.Auto())
{
JobHandle handle = job.Schedule(enemyCount, 64);
handle.Complete();
}
}
[BurstCompile]
private struct BuildCollisionBucketsBurstJob : IJobParallelFor
{
[ReadOnly] public NativeArray<EnemyJobOutputData> EnemyOutputs;
public NativeParallelMultiHashMap<long, int>.ParallelWriter Buckets;
public float CellSize;
public void Execute(int index)
{
EnemyJobOutputData enemy = EnemyOutputs[index];
int cellX = (int)math.floor(enemy.Position.x / CellSize);
int cellZ = (int)math.floor(enemy.Position.z / CellSize);
Buckets.Add(SeparationCellKey(cellX, cellZ), index);
}
}
private bool ScheduleCollisionCandidateQueryJob(float cellSize, bool hasEnemyTargets, out JobHandle handle)
{
handle = default;
if (!_collisionQueryInputs.IsCreated || !_collisionCandidates.IsCreated)
{
return false;
}
_collisionCandidates.Clear();
int queryCount = _collisionQueryInputs.Length;
if (queryCount == 0)
{
return false;
}
bool hasPlayerTarget = TryGetPlayerCollisionTarget(out int playerTargetEntityId, out float3 playerPosition);
QueryCollisionCandidatesBurstJob job = new QueryCollisionCandidatesBurstJob
{
Queries = _collisionQueryInputs.AsArray(),
EnemyBuckets = _enemyCollisionBuckets,
EnemyOutputs = _enemyJobOutputs.AsArray(),
Candidates = _collisionCandidates.AsParallelWriter(),
HasEnemyTargets = hasEnemyTargets,
HasPlayerTarget = hasPlayerTarget,
PlayerTargetEntityId = playerTargetEntityId,
PlayerPosition = playerPosition,
CellSize = cellSize
};
handle = job.Schedule(queryCount, 64);
return true;
}
private void CountCollisionCandidatesBySourceType(out int projectileCandidateCount, out int areaCandidateCount)
{
projectileCandidateCount = 0;
areaCandidateCount = 0;
if (!_collisionCandidates.IsCreated)
{
return;
}
for (int i = 0; i < _collisionCandidates.Length; i++)
{
CollisionCandidateData candidate = _collisionCandidates[i];
if (candidate.SourceType == CollisionSourceTypeProjectile)
{
projectileCandidateCount++;
}
else if (candidate.SourceType == CollisionSourceTypeArea)
{
areaCandidateCount++;
}
}
}
private static bool TryGetPlayerCollisionTarget(out int playerEntityId, out float3 playerPosition)
{
playerEntityId = PlayerEntityId;
playerPosition = default;
if (!TryGetAliveTargetableEntity(playerEntityId, out TargetableObject playerTarget))
{
return false;
}
Transform playerTransform = playerTarget.CachedTransform;
if (playerTransform == null)
{
return false;
}
Vector3 position = playerTransform.position;
playerPosition = new float3(position.x, position.y, position.z);
return true;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ac6aca660ff944e69ea8f5e06f0609cf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,10 @@
namespace Simulation
{
public sealed partial class SimulationWorld
{
// Shared collision pipeline configuration and runtime state.
// Request buffering, broad-phase scheduling, resolve, and presentation
// dispatch live in dedicated partial files under Jobs/.
private const int PlayerEntityId = -1;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 87bc5c5ec75920d46a65881e949bd2a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,115 @@
using CustomEvent;
using SepCore.Definition;
using Entity;
using Entity.Weapon;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Collision Presentation
private void DispatchProjectileHitPresentationEvent(int projectileEntityId, int sourceEntityId,
int sourceOwnerEntityId, int targetEntityId, int damage, in Vector3 hitPosition)
{
if (!_dispatchProjectileHitPresentationEvent)
{
return;
}
var eventComponent = GameEntry.Event;
if (eventComponent == null)
{
return;
}
eventComponent.Fire(this, ProjectileHitPresentationEventArgs.Create(
projectileEntityId,
sourceEntityId,
sourceOwnerEntityId,
targetEntityId,
damage,
hitPosition,
_dispatchProjectileHitMarkerEvent,
_dispatchProjectileHitEffectEvent,
_projectileHitPresentationEffectTypeId));
}
private static bool TryResolveImpactSource(EntityBase sourceEntity, EntityBase ownerEntity,
out EntityBase attacker, out ImpactData impactData)
{
if (TryResolveImpactFromEntity(sourceEntity, out impactData))
{
attacker = sourceEntity;
return true;
}
if (TryResolveImpactFromEntity(ownerEntity, out impactData))
{
attacker = ownerEntity;
return true;
}
attacker = null;
impactData = default;
return false;
}
private static bool TryResolveImpactFromEntity(EntityBase entity, out ImpactData impactData)
{
if (entity is WeaponBase weapon)
{
impactData = weapon.GetImpactData();
return true;
}
if (entity is EnemyProjectile enemyProjectile)
{
impactData = enemyProjectile.GetImpactData();
return true;
}
if (entity is TargetableObject targetableObject)
{
impactData = targetableObject.GetImpactData();
return true;
}
impactData = default;
return false;
}
private static bool TryGetAliveTargetableEntity(int entityId, out TargetableObject target)
{
target = null;
var enemyManager = GameEntry.EnemyManager;
if (enemyManager != null && enemyManager.TryGetEnemy(entityId, out EntityBase enemyEntity))
{
if (enemyEntity is TargetableObject enemyTarget && enemyTarget.Available && !enemyTarget.IsDead)
{
target = enemyTarget;
return true;
}
}
EntityBase entity = TryGetEntityById(entityId);
if (entity is TargetableObject targetable && targetable.Available && !targetable.IsDead)
{
target = targetable;
return true;
}
return false;
}
private static EntityBase TryGetEntityById(int entityId)
{
var entityComponent = GameEntry.Entity;
return entityComponent != null ? entityComponent.GetGameEntity(entityId) : null;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 882102e53dea40a2b5b596e1b41bbed6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,121 @@
using Entity;
using Entity.Weapon;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Collision Requests
public bool TryRequestAreaCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
float radius, int maxTargets = 16)
{
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
maxTargets, CollisionShapeCircle, Vector3.forward, 180f, 0f, 0f);
}
public bool TryRequestSectorCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
float radius, in Vector3 direction, float halfAngleDeg, int maxTargets = 16)
{
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
maxTargets, CollisionShapeSector, direction, halfAngleDeg, 0f, 0f);
}
public bool TryRequestRectangleCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
float halfWidth, float halfLength, in Vector3 direction, int maxTargets = 16)
{
float safeHalfWidth = Mathf.Max(0.01f, halfWidth);
float safeHalfLength = Mathf.Max(0.01f, halfLength);
float boundingRadius = Mathf.Sqrt(safeHalfWidth * safeHalfWidth + safeHalfLength * safeHalfLength);
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, boundingRadius,
maxTargets, CollisionShapeRectangle, direction, 0f, safeHalfWidth, safeHalfLength);
}
private bool TryRequestAreaCollisionInternal(int sourceEntityId, int sourceOwnerEntityId,
in Vector3 center, float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg,
float halfWidth, float halfLength)
{
if (sourceEntityId == 0 || radius <= 0f || maxTargets <= 0)
{
return false;
}
int resolvedOwnerEntityId = sourceOwnerEntityId != 0 ? sourceOwnerEntityId : sourceEntityId;
bool sourceWasActiveAtQueryTime = WasCollisionSourceActiveAtQueryTime(sourceEntityId);
Vector3 normalizedDirection = direction;
normalizedDirection.y = 0f;
if (normalizedDirection.sqrMagnitude <= Mathf.Epsilon)
{
normalizedDirection = Vector3.forward;
}
else
{
normalizedDirection.Normalize();
}
_areaCollisionRequests.Add(new AreaCollisionRequestData
{
SourceEntityId = sourceEntityId,
SourceOwnerEntityId = resolvedOwnerEntityId,
SourceWasActiveAtQueryTime = sourceWasActiveAtQueryTime,
Center = center,
Radius = Mathf.Max(0.01f, radius),
MaxTargets = Mathf.Max(1, maxTargets),
ShapeType = shapeType,
Direction = normalizedDirection,
HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f),
HalfWidth = Mathf.Max(0f, halfWidth),
HalfLength = Mathf.Max(0f, halfLength)
});
return true;
}
private int GetPendingAreaCollisionRequestCount()
{
return _areaCollisionRequests.Count;
}
private int EstimatePendingAreaCollisionCandidateCountFromRequests()
{
int expectedCount = 0;
for (int i = 0; i < _areaCollisionRequests.Count; i++)
{
expectedCount += Mathf.Max(1, _areaCollisionRequests[i].MaxTargets);
}
return expectedCount;
}
private void ClearAreaCollisionTransientBuffers()
{
_areaCollisionRequests.Clear();
_areaCollisionHitEvents.Clear();
_areaCollisionHitDedupKeys.Clear();
}
private static bool WasCollisionSourceActiveAtQueryTime(int sourceEntityId)
{
EntityBase sourceEntity = TryGetEntityById(sourceEntityId);
if (sourceEntity == null || !sourceEntity.Available)
{
return false;
}
if (sourceEntity is WeaponBase weapon)
{
return weapon.IsAttacking;
}
if (sourceEntity is EnemyProjectile projectile)
{
return projectile.IsActive;
}
return true;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 56086ce3981e4ed5b95fdc54f7db85a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,363 @@
using Components;
using CustomDebugger;
using CustomUtility;
using SepCore.Definition;
using Entity;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Collision Resolve
private void ResolveCollisionCandidatesOnMainThread()
{
if (!_collisionCandidates.IsCreated)
{
_lastResolvedAreaHitCount = 0;
ClearAreaCollisionTransientBuffers();
return;
}
_projectileResolvedEntityIds.Clear();
_areaCollisionHitEvents.Clear();
_areaCollisionHitDedupKeys.Clear();
if (_collisionCandidates.Length == 0)
{
_lastResolvedAreaHitCount = 0;
ClearAreaCollisionTransientBuffers();
return;
}
using (CustomProfilerMarker.Collision_ResolveProjectile.Auto())
{
for (int i = 0; i < _collisionCandidates.Length; i++)
{
CollisionCandidateData candidate = _collisionCandidates[i];
if (candidate.SourceType == CollisionSourceTypeProjectile)
{
int projectileEntityId = candidate.SourceEntityId;
if (_projectileResolvedEntityIds.Contains(projectileEntityId))
{
continue;
}
if (!TryGetActiveProjectileSimData(projectileEntityId, out _, out ProjectileSimData projectile))
{
_projectileResolvedEntityIds.Add(projectileEntityId);
continue;
}
bool shouldExpireProjectile = true;
bool shouldDispatchPresentation = false;
int damage = 0;
Vector3 hitPosition = projectile.Position;
if (TryGetAliveTargetableEntity(candidate.TargetEntityId, out TargetableObject target))
{
EntityBase sourceEntity = TryGetEntityById(candidate.SourceEntityId);
EntityBase ownerEntity = TryGetEntityById(candidate.SourceOwnerEntityId);
shouldExpireProjectile = ResolveProjectileHitAgainstTarget(target, sourceEntity,
ownerEntity,
in projectile,
out damage, out hitPosition, out shouldDispatchPresentation);
}
if (shouldDispatchPresentation)
{
DispatchProjectileHitPresentationEvent(projectileEntityId, candidate.SourceEntityId,
candidate.SourceOwnerEntityId, candidate.TargetEntityId, damage, in hitPosition);
}
if (shouldExpireProjectile)
{
MarkProjectileAsExpired(projectileEntityId);
_projectileResolvedEntityIds.Add(projectileEntityId);
}
continue;
}
if (candidate.SourceType == CollisionSourceTypeArea)
{
long dedupKey = (((long)candidate.QueryId) << 32) ^ (uint)candidate.TargetEntityId;
if (!_areaCollisionHitDedupKeys.Add(dedupKey))
{
continue;
}
_areaCollisionHitEvents.Add(new AreaCollisionHitEventData
{
QueryId = candidate.QueryId,
SourceEntityId = candidate.SourceEntityId,
SourceOwnerEntityId = candidate.SourceOwnerEntityId,
TargetEntityId = candidate.TargetEntityId,
SqrDistance = candidate.SqrDistance
});
}
}
}
int resolvedAreaHitCount;
using (CustomProfilerMarker.Collision_ResolveArea.Auto())
{
resolvedAreaHitCount = ResolveAreaCollisionHitsOnMainThread();
}
_lastResolvedAreaHitCount = resolvedAreaHitCount;
_projectileResolvedEntityIds.Clear();
ClearAreaCollisionTransientBuffers();
}
private bool ResolveProjectileHitAgainstTarget(TargetableObject target, EntityBase sourceEntity,
EntityBase ownerEntity,
in ProjectileSimData projectile, out int damage, out Vector3 hitPosition,
out bool shouldDispatchPresentation)
{
damage = 0;
hitPosition = projectile.Position;
shouldDispatchPresentation = false;
if (target == null || !target.Available || target.IsDead)
{
return true;
}
if (target.CachedTransform != null)
{
hitPosition = target.CachedTransform.position;
}
if (!TryResolveImpactSource(sourceEntity, ownerEntity, out EntityBase attacker,
out ImpactData sourceImpact))
{
shouldDispatchPresentation = true;
return true;
}
ImpactData targetImpact = target.GetImpactData();
if (AIUtility.GetRelation(targetImpact.Camp, sourceImpact.Camp) == RelationType.Friendly)
{
return false;
}
damage = AIUtility.CalcDamageHP(sourceImpact.AttackBase, sourceImpact.AttackStat,
targetImpact.DefenseStat,
targetImpact.DodgeStat);
shouldDispatchPresentation = true;
if (damage <= 0)
{
return true;
}
target.ApplyDamage(attacker ?? sourceEntity ?? ownerEntity, damage);
return true;
}
private int ResolveAreaCollisionHitsOnMainThread()
{
if (_areaCollisionHitEvents.Count == 0)
{
return 0;
}
int resolvedHitCount = 0;
for (int i = 0; i < _areaCollisionHitEvents.Count; i++)
{
AreaCollisionHitEventData hitEvent = _areaCollisionHitEvents[i];
if (!TryGetCollisionQueryByQueryId(hitEvent.QueryId, out CollisionQueryData query) ||
query.SourceType != CollisionSourceTypeArea)
{
continue;
}
if (!query.SourceWasActiveAtQueryTime)
{
continue;
}
EntityBase sourceEntity = TryGetEntityById(hitEvent.SourceEntityId);
if (sourceEntity == null || !sourceEntity.Available)
{
continue;
}
if (!TryGetAliveTargetableEntity(hitEvent.TargetEntityId, out TargetableObject target))
{
continue;
}
float targetRadius = ResolveAreaTargetRadius(target);
if (!IsAreaTargetInsidePreciseShape(in query, target, targetRadius))
{
continue;
}
AIUtility.PerformCollision(target, sourceEntity, true);
resolvedHitCount++;
}
return resolvedHitCount;
}
private bool TryGetCollisionQueryByQueryId(int queryId, out CollisionQueryData query)
{
query = default;
if (!_collisionQueryInputs.IsCreated || queryId < 0 || queryId >= _collisionQueryInputs.Length)
{
return false;
}
CollisionQueryData direct = _collisionQueryInputs[queryId];
if (direct.QueryId == queryId)
{
query = direct;
return true;
}
for (int i = 0; i < _collisionQueryInputs.Length; i++)
{
CollisionQueryData candidate = _collisionQueryInputs[i];
if (candidate.QueryId != queryId)
{
continue;
}
query = candidate;
return true;
}
return false;
}
private float ResolveAreaTargetRadius(TargetableObject target)
{
if (target == null)
{
return 0f;
}
if (target is EnemyBase && TryGetEnemyData(target.Id, out EnemySimData enemyData))
{
return Mathf.Max(0f, enemyData.EnemyBodyRadius);
}
MovementComponent movementComponent = target.GetComponent<MovementComponent>();
return movementComponent != null ? Mathf.Max(0f, movementComponent.EnemyBodyRadius) : 0f;
}
private static bool IsAreaTargetInsidePreciseShape(in CollisionQueryData query, TargetableObject target,
float targetRadius)
{
if (target == null || target.CachedTransform == null)
{
return false;
}
Vector3 center = new Vector3(query.Position.x, query.Position.y, query.Position.z);
Vector3 toTarget = target.CachedTransform.position - center;
toTarget.y = 0f;
float radius = Mathf.Max(0.01f, query.Radius + Mathf.Max(0f, targetRadius));
float radiusSqr = radius * radius;
float sqrDistance = toTarget.sqrMagnitude;
if (sqrDistance > radiusSqr)
{
return false;
}
if (query.ShapeType == CollisionShapeRectangle)
{
Vector3 forwardRect = new Vector3(query.Direction.x, query.Direction.y, query.Direction.z);
forwardRect.y = 0f;
if (forwardRect.sqrMagnitude <= Mathf.Epsilon)
{
forwardRect = Vector3.forward;
}
else
{
forwardRect.Normalize();
}
Vector3 rightRect = Vector3.Cross(Vector3.up, forwardRect);
float halfWidth = Mathf.Max(0.01f, query.HalfWidth + Mathf.Max(0f, targetRadius));
float halfLength = Mathf.Max(0.01f, query.HalfLength + Mathf.Max(0f, targetRadius));
float forwardDistance = Vector3.Dot(toTarget, forwardRect);
float lateralDistance = Vector3.Dot(toTarget, rightRect);
return Mathf.Abs(forwardDistance) <= halfLength && Mathf.Abs(lateralDistance) <= halfWidth;
}
if (query.ShapeType != CollisionShapeSector)
{
return true;
}
if (sqrDistance <= Mathf.Epsilon)
{
return true;
}
Vector3 forward = new Vector3(query.Direction.x, query.Direction.y, query.Direction.z);
forward.y = 0f;
if (forward.sqrMagnitude <= Mathf.Epsilon)
{
forward = Vector3.forward;
}
else
{
forward.Normalize();
}
float halfAngle = Mathf.Clamp(query.HalfAngleDeg, 0f, 180f);
float angle = Vector3.Angle(forward, toTarget.normalized);
return angle <= halfAngle;
}
private bool TryGetActiveProjectileSimData(int projectileEntityId, out int simulationIndex,
out ProjectileSimData projectile)
{
simulationIndex = -1;
projectile = default;
if (!ProjectileBinding.TryGetSimulationIndex(projectileEntityId, out int foundIndex) || foundIndex < 0 ||
foundIndex >= _projectiles.Count)
{
return false;
}
ProjectileSimData data = _projectiles[foundIndex];
if (!data.Active || data.State != ProjectileStateActive)
{
return false;
}
simulationIndex = foundIndex;
projectile = data;
return true;
}
private bool MarkProjectileAsExpired(int projectileEntityId)
{
if (!ProjectileBinding.TryGetSimulationIndex(projectileEntityId, out int simulationIndex) ||
simulationIndex < 0 || simulationIndex >= _projectiles.Count)
{
return false;
}
ProjectileSimData projectile = _projectiles[simulationIndex];
projectile.Active = false;
projectile.State = ProjectileStateExpired;
projectile.RemainingLifetime = 0f;
if (projectile.LifeTime > 0f && projectile.Age < projectile.LifeTime)
{
projectile.Age = projectile.LifeTime;
}
_projectiles[simulationIndex] = projectile;
return true;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5261fa73dd9c4bfc91fcba0d5ca1bcb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,280 @@
using CustomDebugger;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
// Orchestrates per-tick simulation pipeline and enemy movement/separation jobs.
[Header("敌人互斥参数")]
[Tooltip("敌人互斥分桶使用的网格尺寸。小于等于 0 时,将根据敌人体积半径自动计算。")]
[SerializeField]
private float _enemySeparationCellSize = 0f;
[Tooltip("每次迭代对互斥推力累积值的阻尼系数。数值越大,分离速度越快。")]
[SerializeField]
private float _enemySeparationPushDamping = 0.75f;
[Tooltip("每次迭代允许的最大互斥位移步长(按敌人体积半径倍率计算)。")]
[SerializeField]
private float _enemySeparationMaxStepScale = 1f;
[Tooltip("敌人进入攻击范围后,互斥位移是否保持为相对玩家方向的切向分量(避免被径向推离玩家)。")]
[SerializeField]
private bool _enemySeparationUseTangentialInAttackRange = true;
[Tooltip("互斥推力方向突变时的时间平滑系数。越大越稳定,但响应越慢。")]
[SerializeField]
private float _enemySeparationPushSmoothing = 0.55f;
private void TickSimulationPipeline(in SimulationTickContext context)
{
// 1. 早退分支:deltaTime <= 0 时只清理碰撞通道和统计,然后返回。
if (context.DeltaTime <= 0f)
{
PrepareCollisionQueryAndCandidateChannels(0, 0, 0);
ResetCollisionRuntimeStats();
ClearAreaCollisionTransientBuffers();
return;
}
JobHandle enemyMovementHandle = default;
JobHandle projectileMovementHandle = default;
JobHandle enemySeparationHandle = default;
bool hasEnemySeparationJob = false;
bool hasEnemySeparationCandidates = false;
int enemySeparationCount = 0;
float enemySeparationMaxRadius = 0.45f;
// 2. BuildInput 阶段:
// 把 _enemies/_projectiles 同步到 Native 输入,准备输出缓冲;
// 统计是否需要敌人分离 Job;
// 预估并准备碰撞查询/候选缓冲。
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
{
SyncSimulationStateToJobInputs();
int enemyCount = _enemyJobInputs.Length;
int projectileCount = _projectileJobInputs.Length;
PrepareEnemyJobOutputBuffer(enemyCount);
PrepareProjectileJobOutputBuffer(projectileCount);
enemySeparationCount = enemyCount;
for (int i = 0; i < enemyCount; i++)
{
EnemyJobInputData input = _enemyJobInputs[i];
if (!input.AvoidEnemyOverlap)
{
continue;
}
hasEnemySeparationCandidates = true;
float radius = input.EnemyBodyRadius > 0f ? input.EnemyBodyRadius : 0.45f;
if (radius > enemySeparationMaxRadius)
{
enemySeparationMaxRadius = radius;
}
}
if (hasEnemySeparationCandidates)
{
int separationBucketCapacity = Mathf.Max(128, enemyCount * 2);
PrepareEnemySeparationJobBuffers(enemyCount, separationBucketCapacity);
}
int projectileQueryCount = _projectiles.Count;
int areaQueryCount = GetPendingAreaCollisionRequestCount();
int queryCount = projectileQueryCount + areaQueryCount;
int projectileExpectedCount = projectileQueryCount * Mathf.Max(1, _projectileMaxCandidatesPerQuery);
int areaExpectedCount = EstimatePendingAreaCollisionCandidateCountFromRequests();
int expectedCandidateCount = Mathf.Max(16, projectileExpectedCount + areaExpectedCount);
int bucketCapacity = Mathf.Max(256, _enemies.Count * 2 + queryCount);
PrepareCollisionQueryAndCandidateChannels(queryCount, expectedCandidateCount, bucketCapacity);
}
// 3. StateUpdate 阶段(调度两个移动 Job)
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
{
enemyMovementHandle = ExecuteEnemyMovementJob(in context);
projectileMovementHandle = ExecuteProjectileMovementJob(in context);
}
// 4. Schedule 阶段(可选分离 Job)
// 如果有需要分离的敌人,调度:
// BuildEnemySeparationBucketsBurstJob(依赖 EnemyMovement)
// EnemySeparationBurstJob(依赖分桶 Job)
// 然后把“敌人链路 handle”和“投射物移动 handle”合并。
JobHandle simulationHandle;
using (CustomProfilerMarker.TickEnemies_Schedule.Auto())
{
hasEnemySeparationJob = TryScheduleEnemySeparationFromJobOutput(
in context,
enemyMovementHandle,
hasEnemySeparationCandidates,
enemySeparationCount,
enemySeparationMaxRadius,
out enemySeparationHandle);
JobHandle enemyHandle = hasEnemySeparationJob ? enemySeparationHandle : enemyMovementHandle;
simulationHandle = JobHandle.CombineDependencies(enemyHandle, projectileMovementHandle);
}
// 5. Complete 阶段:等待上述 Job 全部完成。
using (CustomProfilerMarker.TickEnemies_Complete.Auto())
{
simulationHandle.Complete();
}
// 6. 主线程后处理阶段:
// - 把分离结果覆盖回敌人输出(如果有分离 Job)
// - 构建碰撞候选(投射物查询 + area 请求查询 + 网格筛选)
using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto())
{
if (hasEnemySeparationJob)
{
CommitEnemySeparationFromJobOutput(enemySeparationCount);
}
}
using (CustomProfilerMarker.Collision.Auto())
{
PrepareCollisionCandidatesForFrame();
CompleteCollisionCandidatesForFrame();
}
// 7. MainThreadCommit 阶段
// - ApplyJobOutputsToSimulationState(写回 _enemies/_projectiles)
// - ResolveCollisionCandidatesOnMainThread(命中结算、事件、范围碰撞)
// - RecycleInactiveAndExpiredProjectiles(隐藏并移除失效投射物)
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
{
using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto())
{
ApplyJobOutputsToSimulationState();
ResolveCollisionCandidatesOnMainThread();
RecycleInactiveAndExpiredProjectiles();
}
}
}
private JobHandle ExecuteEnemyMovementJob(in SimulationTickContext context)
{
int enemyCount = _enemyJobInputs.Length;
if (enemyCount == 0)
{
return default;
}
if (context.DeltaTime <= 0f)
{
CopyEnemyInputsToOutputs();
return default;
}
float3 playerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z);
NativeArray<EnemyJobInputData> inputArray = _enemyJobInputs.AsArray();
NativeArray<EnemyJobOutputData> outputArray = _enemyJobOutputs.AsArray();
EnemyMovementBurstJob burstJob = new EnemyMovementBurstJob
{
Inputs = inputArray,
Outputs = outputArray,
DeltaTime = context.DeltaTime,
PlayerPosition = playerPosition
};
return burstJob.Schedule(enemyCount, 64);
}
private void CopyEnemyInputsToOutputs()
{
for (int i = 0; i < _enemyJobInputs.Length; i++)
{
EnemyJobInputData input = _enemyJobInputs[i];
_enemyJobOutputs[i] = new EnemyJobOutputData
{
EntityId = input.EntityId,
Position = input.Position,
Forward = input.Forward,
Rotation = input.Rotation,
Speed = input.Speed,
AttackRange = input.AttackRange,
AvoidEnemyOverlap = input.AvoidEnemyOverlap,
EnemyBodyRadius = input.EnemyBodyRadius,
SeparationIterations = input.SeparationIterations,
TargetType = input.TargetType,
State = input.State
};
}
}
private bool TryScheduleEnemySeparationFromJobOutput(in SimulationTickContext context, JobHandle dependency,
bool hasSeparationCandidates, int enemyCount, float maxRadius, out JobHandle separationHandle)
{
separationHandle = dependency;
if (enemyCount <= 0 || !hasSeparationCandidates)
{
return false;
}
float autoCellSize = maxRadius * 2f;
float configuredCellSize = _enemySeparationCellSize > 0f ? _enemySeparationCellSize : autoCellSize;
float cellSize = Mathf.Max(0.1f, configuredCellSize);
float3 playerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z);
float pushDamping = Mathf.Clamp(_enemySeparationPushDamping, 0f, 2f);
float maxStepScale = Mathf.Max(0.1f, _enemySeparationMaxStepScale);
bool useTangentialInAttackRange = _enemySeparationUseTangentialInAttackRange;
float pushSmoothing = Mathf.Clamp01(_enemySeparationPushSmoothing);
NativeArray<EnemyJobOutputData> inputArray = _enemyJobOutputs.AsArray();
NativeArray<EnemyJobOutputData> separatedOutputArray = _enemyJobSeparationOutputs.AsArray();
NativeArray<float2> previousPushes = _enemySeparationPreviousPushes.AsArray();
NativeArray<float2> currentPushes = _enemySeparationCurrentPushes.AsArray();
BuildEnemySeparationBucketsBurstJob buildJob = new BuildEnemySeparationBucketsBurstJob
{
Inputs = inputArray,
Buckets = _enemySeparationBuckets.AsParallelWriter(),
CellSize = cellSize
};
JobHandle buildHandle = buildJob.Schedule(enemyCount, 64, dependency);
EnemySeparationBurstJob separationJob = new EnemySeparationBurstJob
{
Inputs = inputArray,
Buckets = _enemySeparationBuckets,
PreviousPushes = previousPushes,
Outputs = separatedOutputArray,
CurrentPushes = currentPushes,
CellSize = cellSize,
MaxRadius = maxRadius,
PlayerPosition = playerPosition,
PushDamping = pushDamping,
MaxStepScale = maxStepScale,
UseTangentialInAttackRange = useTangentialInAttackRange,
PushSmoothing = pushSmoothing
};
separationHandle = separationJob.Schedule(enemyCount, 64, buildHandle);
return true;
}
private void CommitEnemySeparationFromJobOutput(int enemyCount)
{
if (enemyCount <= 0)
{
return;
}
CommitEnemySeparationTemporalBuffers(enemyCount);
for (int i = 0; i < enemyCount; i++)
{
_enemyJobOutputs[i] = _enemyJobSeparationOutputs[i];
}
}
private static long SeparationCellKey(int x, int z)
{
return ((long)x << 32) ^ (uint)z;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 99b885bb5cfe48e7bd9dba74fe683149
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,111 @@
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
[Header("Projectile Simulation")]
[Tooltip("Recycle projectile when horizontal distance to player exceeds this range. <=0 disables this rule.")]
[SerializeField]
private float _projectileMaxDistanceFromPlayer = 120f;
[Tooltip("Recycle projectile when vertical offset to player exceeds this range. <=0 disables this rule.")]
[SerializeField]
private float _projectileMaxVerticalOffsetFromPlayer = 30f;
#region Projectile Movement Job
private JobHandle ExecuteProjectileMovementJob(in SimulationTickContext context)
{
int projectileCount = _projectileJobInputs.Length;
if (projectileCount == 0)
{
return default;
}
if (context.DeltaTime <= 0f)
{
CopyProjectileInputsToOutputs();
return default;
}
float maxDistance = Mathf.Max(0f, _projectileMaxDistanceFromPlayer);
float maxSqrDistanceFromPlayer = maxDistance > 0f ? maxDistance * maxDistance : -1f;
float maxVerticalOffsetFromPlayer = Mathf.Max(0f, _projectileMaxVerticalOffsetFromPlayer);
float3 playerPosition =
new float3(context.PlayerPosition.x, context.PlayerPosition.y, context.PlayerPosition.z);
NativeArray<ProjectileJobInputData> inputArray = _projectileJobInputs.AsArray();
NativeArray<ProjectileJobOutputData> outputArray = _projectileJobOutputs.AsArray();
ProjectileMovementBurstJob burstJob = new ProjectileMovementBurstJob
{
Inputs = inputArray,
Outputs = outputArray,
DeltaTime = context.DeltaTime,
PlayerPosition = playerPosition,
MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer,
MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer
};
return burstJob.Schedule(projectileCount, 64);
}
#endregion
#region Projectile Cleanup
private void RecycleInactiveAndExpiredProjectiles()
{
_projectileRecycleEntityIds.Clear();
for (int i = 0; i < _projectiles.Count; i++)
{
ProjectileSimData projectile = _projectiles[i];
if (!ShouldRecycleProjectileSimData(projectile))
{
continue;
}
_projectileRecycleEntityIds.Add(projectile.EntityId);
}
if (_projectileRecycleEntityIds.Count == 0)
{
return;
}
var entityComponent = GameEntry.Entity;
for (int i = 0; i < _projectileRecycleEntityIds.Count; i++)
{
int entityId = _projectileRecycleEntityIds[i];
if (entityComponent != null)
{
entityComponent.HideEntity(entityId);
}
RemoveProjectileByEntityId(entityId);
}
_projectileRecycleEntityIds.Clear();
}
private static bool ShouldRecycleProjectileSimData(in ProjectileSimData projectile)
{
if (!projectile.Active)
{
return true;
}
if (projectile.State == ProjectileStateExpired)
{
return true;
}
return projectile.LifeTime > 0f && projectile.Age >= projectile.LifeTime;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bcb6513f18404795a654500d3d2f8ef9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4bf448696a8641268abb660b16def6f5
timeCreated: 1771911417
@@ -0,0 +1,135 @@
using CustomEvent;
using Entity;
using Entity.EntityData;
using Entity.Weapon;
using GameFramework.Event;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
[Header("投射物命中表现")]
[Tooltip("是否监听投射物命中表现事件。")]
[SerializeField]
private bool _projectileHitPresentationEnabled = true;
[Tooltip("是否播放投射物命中标记。")]
[SerializeField]
private bool _projectileHitMarkerEnabled = true;
[Tooltip("命中标记尺寸。")][SerializeField] private float _projectileHitMarkerSize = 0.2f;
[Tooltip("命中标记在目标上的高度偏移。")]
[SerializeField]
private float _projectileHitMarkerYOffset = 1.2f;
[Tooltip("命中标记持续时间。")]
[SerializeField]
private float _projectileHitMarkerDuration = 0.15f;
[Tooltip("命中标记颜色。")][SerializeField] private Color _projectileHitMarkerColor = new(1f, 0f, 0f, 0.95f);
[Tooltip("是否播放投射物命中特效实体。")]
[SerializeField]
private bool _projectileHitEffectEnabled;
[Tooltip("投射物命中特效实体类型 Id(<=0 表示不启用)。")]
[SerializeField]
private int _projectileHitEffectTypeId;
private sealed class HitPresentation
{
private readonly SimulationWorld _world;
private HandgunHitMarkerAttackEffect _projectileHitMarkerEffect;
private bool _isProjectileHitEventSubscribed;
public HitPresentation(SimulationWorld world)
{
_world = world;
}
public void OnStart()
{
if (_world == null || !_world._projectileHitPresentationEnabled)
{
return;
}
var eventComponent = GameEntry.Event;
if (eventComponent == null)
{
return;
}
eventComponent.Subscribe(ProjectileHitPresentationEventArgs.EventId, OnProjectileHitPresentationEvent);
_isProjectileHitEventSubscribed = true;
}
public void OnDestroy()
{
if (!_isProjectileHitEventSubscribed)
{
return;
}
var eventComponent = GameEntry.Event;
if (eventComponent != null)
{
eventComponent.Unsubscribe(ProjectileHitPresentationEventArgs.EventId,
OnProjectileHitPresentationEvent);
}
_isProjectileHitEventSubscribed = false;
}
private void OnProjectileHitPresentationEvent(object sender, GameEventArgs e)
{
if (!_isProjectileHitEventSubscribed || _world == null ||
e is not ProjectileHitPresentationEventArgs args)
{
return;
}
if (args.ShowHitMarker && _world._projectileHitMarkerEnabled)
{
PlayHitMarker(args);
}
if (args.ShowHitEffect && _world._projectileHitEffectEnabled)
{
PlayHitEffect(args);
}
}
private void PlayHitMarker(ProjectileHitPresentationEventArgs args)
{
// Projectile hit markers were reusing the handgun marker effect and were
// visually indistinguishable from handgun lock/hit feedback.
}
private void PlayHitEffect(ProjectileHitPresentationEventArgs args)
{
int effectTypeId = args.EffectEntityTypeId > 0
? args.EffectEntityTypeId
: _world._projectileHitEffectTypeId;
if (effectTypeId <= 0)
{
return;
}
var entityComponent = GameEntry.Entity;
if (entityComponent == null)
{
return;
}
EffectData effectData = new EffectData(entityComponent.GenerateSerialId(), effectTypeId)
{
Position = args.HitPosition
};
entityComponent.ShowEffect(effectData);
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 99cec505913140a2b4bdf1498b28c044
timeCreated: 1771911517
@@ -0,0 +1,9 @@
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ed4e018208ef0d042b99372c1f9ae778
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,105 @@
using Entity;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
private sealed class TransformSync
{
private readonly SimulationWorld _world;
public TransformSync(SimulationWorld world)
{
_world = world;
}
public void OnLateUpdate()
{
if (_world == null)
{
return;
}
var enemyManager = GameEntry.EnemyManager;
if (enemyManager != null && enemyManager.Enemies != null)
{
var enemies = enemyManager.Enemies;
foreach (var enemy in enemies)
{
if (enemy is not EnemyBase enemyEntity || !enemyEntity.Available)
{
continue;
}
if (!_world.TryGetEnemyData(enemyEntity.Id, out EnemySimData enemyData))
{
continue;
}
ApplyEnemyPresentation(enemyEntity, enemyData);
}
}
var projectiles = _world._projectiles;
for (int i = 0; i < projectiles.Count; i++)
{
ProjectileSimData projectileData = projectiles[i];
if (!projectileData.Active || projectileData.State != ProjectileStateActive)
{
continue;
}
EntityBase projectileEntity = TryGetEntityById(projectileData.EntityId);
if (projectileEntity == null || !projectileEntity.Available)
{
continue;
}
ApplyProjectilePresentation(projectileEntity, projectileData);
}
}
private static void ApplyEnemyPresentation(EnemyBase enemyEntity, in EnemySimData enemyData)
{
Transform enemyTransform = enemyEntity.CachedTransform;
enemyTransform.position = enemyData.Position;
Quaternion rotation = enemyData.Rotation;
float rotationMagnitude = Mathf.Abs(rotation.x) + Mathf.Abs(rotation.y) + Mathf.Abs(rotation.z) +
Mathf.Abs(rotation.w);
if (rotationMagnitude > float.Epsilon)
{
enemyTransform.rotation = rotation;
return;
}
Vector3 forward = enemyData.Forward;
forward.y = 0f;
if (forward.sqrMagnitude > float.Epsilon)
{
enemyTransform.forward = forward.normalized;
}
}
private static void ApplyProjectilePresentation(EntityBase projectileEntity,
in ProjectileSimData projectileData)
{
Transform projectileTransform = projectileEntity.CachedTransform;
if (projectileTransform == null)
{
return;
}
projectileTransform.position = projectileData.Position;
Vector3 forward = projectileData.Forward;
if (forward.sqrMagnitude <= float.Epsilon)
{
return;
}
projectileTransform.rotation = Quaternion.LookRotation(forward.normalized, Vector3.up);
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 21da1bcb02d247358738e9b562af5512
timeCreated: 1771911431
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e2d0f6135bae4675af78194b2272e280
timeCreated: 1771590468
@@ -0,0 +1,19 @@
using UnityEngine;
namespace Simulation
{
public struct EnemySimData
{
public int EntityId;
public Vector3 Position;
public Vector3 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;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ef2eea85685544189eef2d9f2cece080
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,12 @@
using UnityEngine;
namespace Simulation
{
public struct PickupSimData
{
public int EntityId;
public Vector3 Position;
public float PickupRadius;
public int State;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ff6e8b75e4af4c3ca9e3b732b1383f95
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,19 @@
using UnityEngine;
namespace Simulation
{
public struct ProjectileSimData
{
public int EntityId;
public int OwnerEntityId;
public Vector3 Position;
public Vector3 Forward;
public Vector3 Velocity;
public float Speed;
public float LifeTime;
public float Age;
public bool Active;
public float RemainingLifetime;
public int State;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2eec1f4389004d69bdce8c4dd95d255e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
using UnityEngine;
namespace Simulation
{
public readonly struct SimulationTickContext
{
public SimulationTickContext(float deltaTime, float realDeltaTime, Vector3 playerPosition)
{
DeltaTime = deltaTime;
RealDeltaTime = realDeltaTime;
PlayerPosition = playerPosition;
}
public float DeltaTime { get; }
public float RealDeltaTime { get; }
public Vector3 PlayerPosition { get; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c4c57fcf285f488b821c9141a6d0ad09
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,112 @@
using Entity;
using GameFramework.Event;
using UnityGameFramework.Runtime;
namespace Simulation
{
public sealed partial class SimulationWorld
{
// Bridges entity show/hide events into simulation state registration.
public sealed class EntitySync
{
private const string EnemyGroupName = "Enemy";
private const string DropGroupName = "Drop";
private const string BulletGroupName = "Bullet";
private const string ProjectileGroupName = "Projectile";
private const string EnemyProjectileGroupName = "EnemyProjectile";
private readonly SimulationWorld _world;
private bool _isEventSubscribed;
public EntitySync(SimulationWorld world)
{
_world = world;
}
public void OnStart()
{
var eventComponent = GameEntry.Event;
if (eventComponent == null)
{
return;
}
eventComponent.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
eventComponent.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
_isEventSubscribed = true;
}
public void OnDestroy()
{
if (!_isEventSubscribed)
{
return;
}
var eventComponent = GameEntry.Event;
if (eventComponent != null)
{
eventComponent.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
eventComponent.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
}
_isEventSubscribed = false;
}
private void OnShowEntitySuccess(object sender, GameEventArgs e)
{
if (e is not ShowEntitySuccessEventArgs args) return;
if (args.Entity == null) return;
if (_world == null) return;
string groupName = args.Entity.EntityGroup?.Name;
if (string.IsNullOrEmpty(groupName)) return;
if (groupName == EnemyGroupName && args.Entity.Logic is EnemyBase enemy)
{
_world.RegisterEnemyLifecycle(enemy, args.UserData);
return;
}
if (groupName == DropGroupName && args.Entity.Logic is EntityBase pickupEntity)
{
_world.RegisterPickupLifecycle(pickupEntity);
return;
}
if ((groupName == BulletGroupName || groupName == ProjectileGroupName ||
groupName == EnemyProjectileGroupName) &&
args.Entity.Logic is EntityBase projectileEntity)
{
_world.RegisterProjectileLifecycle(projectileEntity, args.UserData);
}
}
private void OnHideEntityComplete(object sender, GameEventArgs e)
{
if (e is not HideEntityCompleteEventArgs args) return;
if (args.EntityGroup == null) return;
if (_world == null) return;
string groupName = args.EntityGroup.Name;
if (groupName == EnemyGroupName)
{
_world.UnregisterEnemyLifecycle(args.EntityId);
return;
}
if (groupName == DropGroupName)
{
_world.UnregisterPickupLifecycle(args.EntityId);
return;
}
if (groupName == BulletGroupName || groupName == ProjectileGroupName ||
groupName == EnemyProjectileGroupName)
{
_world.UnregisterProjectileLifecycle(args.EntityId);
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 54d72e7241f04623bcfddfd096086dbb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,109 @@
using Components;
using Entity;
using Entity.EntityData;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Entity To Sim Data
private static EnemySimData CreateEnemyInitialSimData(EnemyBase enemy, EnemyData enemyData)
{
Transform enemyTransform = enemy.CachedTransform;
MovementComponent movementComponent = enemy.GetComponent<MovementComponent>();
float speed = 0f;
if (enemyData != null)
{
speed = enemyData.SpeedBase;
}
else if (movementComponent != null)
{
speed = movementComponent.Speed;
}
float attackRange = enemy.AttackRange > 0f
? enemy.AttackRange
: DefaultAttackRange;
return new EnemySimData
{
EntityId = enemy.Id,
Position = enemyTransform.position,
Forward = enemyTransform.forward,
Rotation = enemyTransform.rotation,
Speed = speed,
AttackRange = attackRange,
AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap,
EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f,
SeparationIterations = movementComponent != null ? movementComponent.SeparationIterations : 2,
TargetType = 0,
State = EnemyStateIdle
};
}
private static ProjectileSimData CreateProjectileInitialSimData(EntityBase projectileEntity, object userData)
{
Vector3 forward = projectileEntity.CachedTransform.forward;
int ownerEntityId = 0;
Vector3 velocity = Vector3.zero;
float speed = 0f;
float lifeTime = 0f;
if (userData is EnemyProjectileData enemyProjectileData)
{
ownerEntityId = enemyProjectileData.OwnerEntityId;
Vector3 direction = enemyProjectileData.Direction;
direction.y = 0f;
if (direction.sqrMagnitude > Mathf.Epsilon)
{
direction.Normalize();
forward = direction;
}
else if (forward.sqrMagnitude > Mathf.Epsilon)
{
forward = forward.normalized;
}
else
{
forward = Vector3.forward;
}
speed = Mathf.Max(0f, enemyProjectileData.Speed);
velocity = forward * speed;
lifeTime = Mathf.Max(0f, enemyProjectileData.LifeTime);
}
return new ProjectileSimData
{
EntityId = projectileEntity.Id,
OwnerEntityId = ownerEntityId,
Position = projectileEntity.CachedTransform.position,
Forward = forward,
Velocity = velocity,
Speed = speed,
LifeTime = lifeTime,
Age = 0f,
Active = true,
RemainingLifetime = lifeTime,
State = ProjectileStateActive
};
}
private static PickupSimData CreatePickupInitialSimData(EntityBase pickupEntity)
{
return new PickupSimData
{
EntityId = pickupEntity.Id,
Position = pickupEntity.CachedTransform.position,
PickupRadius = 0.35f,
State = 0
};
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bdedabee0342441e9551c802dd66693a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,82 @@
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
private Transform _playerMovementTransform;
private Vector3 _playerMovementPosition;
private Vector3 _playerMovementDirection = Vector3.zero;
private float _playerMovementSpeed;
private bool _playerMovementActive;
public void SyncPlayerMovementInput(Transform playerTransform, bool isMoving, in Vector3 direction, float speed)
{
if (playerTransform == null)
{
ClearPlayerMovementState();
return;
}
if (_playerMovementTransform != playerTransform)
{
_playerMovementTransform = playerTransform;
_playerMovementPosition = playerTransform.position;
}
Vector3 planarDirection = direction;
planarDirection.y = 0f;
bool hasDirection = planarDirection.sqrMagnitude > Mathf.Epsilon;
if (hasDirection)
{
_playerMovementDirection = planarDirection.normalized;
}
else
{
_playerMovementDirection = Vector3.zero;
}
_playerMovementActive = isMoving && hasDirection;
_playerMovementSpeed = _playerMovementActive ? Mathf.Max(0f, speed) : 0f;
}
public void UnregisterPlayerMovement(Transform playerTransform)
{
if (playerTransform == null || _playerMovementTransform != playerTransform)
{
return;
}
ClearPlayerMovementState();
}
private Vector3 ResolvePlayerPositionForTick(in SimulationTickContext context)
{
if (_playerMovementTransform == null)
{
return context.PlayerPosition;
}
if (!_playerMovementActive || _playerMovementSpeed <= 0f ||
_playerMovementDirection.sqrMagnitude <= Mathf.Epsilon || context.DeltaTime <= 0f)
{
_playerMovementPosition = _playerMovementTransform.position;
return _playerMovementPosition;
}
_playerMovementPosition += _playerMovementDirection * (_playerMovementSpeed * context.DeltaTime);
_playerMovementPosition.y = _playerMovementTransform.position.y;
_playerMovementTransform.position = _playerMovementPosition;
return _playerMovementPosition;
}
private void ClearPlayerMovementState()
{
_playerMovementTransform = null;
_playerMovementPosition = Vector3.zero;
_playerMovementDirection = Vector3.zero;
_playerMovementSpeed = 0f;
_playerMovementActive = false;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 417eac38d86047d096eb9f74647b6cf7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
[SerializeField] private CollisionPipelineSettings _collisionPipelineSettings = new();
[SerializeField] private TargetSelectionSettings _targetSelectionSettings = new();
private readonly SimulationStateStore _simulationState = new();
private readonly JobDataRuntimeState _jobDataRuntime = new();
private readonly CollisionPipelineRuntimeState _collisionPipelineRuntime = new();
private readonly TargetSelectionRuntimeState _targetSelectionRuntime = new();
private List<EnemySimData> _enemies => _simulationState.Enemies;
private List<ProjectileSimData> _projectiles => _simulationState.Projectiles;
private List<PickupSimData> _pickups => _simulationState.Pickups;
private List<int> _projectileRecycleEntityIds => _simulationState.ProjectileRecycleEntityIds;
private HashSet<int> _projectileResolvedEntityIds => _collisionPipelineRuntime.ProjectileResolvedEntityIds;
private EntityBinding EnemyBinding => _simulationState.EnemyBinding;
private EntityBinding ProjectileBinding => _simulationState.ProjectileBinding;
private EntityBinding PickupBinding => _simulationState.PickupBinding;
private ref NativeList<EnemyJobInputData> _enemyJobInputs => ref _jobDataRuntime.EnemyJobInputs;
private ref NativeList<EnemyJobOutputData> _enemyJobOutputs => ref _jobDataRuntime.EnemyJobOutputs;
private ref NativeList<EnemyJobOutputData> _enemyJobSeparationOutputs => ref _jobDataRuntime.EnemyJobSeparationOutputs;
private ref NativeList<float2> _enemySeparationPreviousPushes => ref _jobDataRuntime.EnemySeparationPreviousPushes;
private ref NativeList<float2> _enemySeparationCurrentPushes => ref _jobDataRuntime.EnemySeparationCurrentPushes;
private ref NativeList<ProjectileJobInputData> _projectileJobInputs => ref _jobDataRuntime.ProjectileJobInputs;
private ref NativeList<ProjectileJobOutputData> _projectileJobOutputs => ref _jobDataRuntime.ProjectileJobOutputs;
private ref NativeList<CollisionCandidateData> _collisionCandidates => ref _jobDataRuntime.CollisionCandidates;
private ref NativeParallelMultiHashMap<long, int> _enemySeparationBuckets => ref _jobDataRuntime.EnemySeparationBuckets;
private ref NativeParallelMultiHashMap<long, int> _enemyCollisionBuckets => ref _jobDataRuntime.EnemyCollisionBuckets;
private List<AreaCollisionHitEventData> _areaCollisionHitEvents => _jobDataRuntime.AreaCollisionHitEvents;
private HashSet<long> _areaCollisionHitDedupKeys => _jobDataRuntime.AreaCollisionHitDedupKeys;
private ref int _lastCollisionQueryCount => ref _jobDataRuntime.LastCollisionQueryCount;
private ref int _lastProjectileCollisionQueryCount => ref _jobDataRuntime.LastProjectileCollisionQueryCount;
private ref int _lastAreaCollisionQueryCount => ref _jobDataRuntime.LastAreaCollisionQueryCount;
private ref int _lastCollisionCandidateCount => ref _jobDataRuntime.LastCollisionCandidateCount;
private ref int _lastProjectileCollisionCandidateCount => ref _jobDataRuntime.LastProjectileCollisionCandidateCount;
private ref int _lastAreaCollisionCandidateCount => ref _jobDataRuntime.LastAreaCollisionCandidateCount;
private ref int _lastResolvedAreaHitCount => ref _jobDataRuntime.LastResolvedAreaHitCount;
private ref float _lastCollisionCellSize => ref _jobDataRuntime.LastCollisionCellSize;
private ref bool _lastCollisionHasEnemyTargets => ref _jobDataRuntime.LastCollisionHasEnemyTargets;
private ref JobHandle _collisionCandidateQueryHandle => ref _collisionPipelineRuntime.CollisionCandidateQueryHandle;
private ref bool _collisionCandidateQueryScheduled => ref _collisionPipelineRuntime.CollisionCandidateQueryScheduled;
private ref NativeParallelMultiHashMap<long, int> _enemyTargetBuckets => ref _targetSelectionRuntime.EnemyTargetBuckets;
private ref bool _enemyTargetBucketsDirty => ref _targetSelectionRuntime.EnemyTargetBucketsDirty;
private float _projectileCollisionQueryRadius => _collisionPipelineSettings.ProjectileCollisionQueryRadius;
private int _projectileMaxCandidatesPerQuery => _collisionPipelineSettings.ProjectileMaxCandidatesPerQuery;
private float _projectileCollisionCellSize => _collisionPipelineSettings.ProjectileCollisionCellSize;
private bool _dispatchProjectileHitPresentationEvent => _collisionPipelineSettings.DispatchProjectileHitPresentationEvent;
private bool _dispatchProjectileHitMarkerEvent => _collisionPipelineSettings.DispatchProjectileHitMarkerEvent;
private bool _dispatchProjectileHitEffectEvent => _collisionPipelineSettings.DispatchProjectileHitEffectEvent;
private int _projectileHitPresentationEffectTypeId => _collisionPipelineSettings.ProjectileHitPresentationEffectTypeId;
private float _targetSelectionCellSize => _targetSelectionSettings.CellSize;
[Serializable]
private sealed class CollisionPipelineSettings
{
[Header("Projectile Collision Query")]
[Tooltip("Projectile broad-phase collision query radius.")]
public float ProjectileCollisionQueryRadius = 0.35f;
[Tooltip("Maximum retained candidates per projectile query.")]
public int ProjectileMaxCandidatesPerQuery = 1;
[Tooltip("Broad-phase bucket cell size. <=0 derives from query radius.")]
public float ProjectileCollisionCellSize = 0f;
[Header("Projectile Hit Event Dispatch")]
[Tooltip("Dispatch projectile hit presentation event.")]
public bool DispatchProjectileHitPresentationEvent = true;
[Tooltip("Request hit marker when projectile hits.")]
public bool DispatchProjectileHitMarkerEvent = true;
[Tooltip("Request hit effect when projectile hits.")]
public bool DispatchProjectileHitEffectEvent = true;
[Tooltip("Default hit effect entity type id in presentation event. 0 means not specified.")]
public int ProjectileHitPresentationEffectTypeId;
}
[Serializable]
private sealed class TargetSelectionSettings
{
[Header("Target Selection")]
[Tooltip("Spatial hash cell size for nearest-enemy queries.")]
public float CellSize = 2f;
}
private sealed class SimulationStateStore
{
public readonly List<EnemySimData> Enemies = new();
public readonly List<ProjectileSimData> Projectiles = new();
public readonly List<PickupSimData> Pickups = new();
public readonly List<int> ProjectileRecycleEntityIds = new();
public readonly EntityBinding EnemyBinding = new();
public readonly EntityBinding ProjectileBinding = new();
public readonly EntityBinding PickupBinding = new();
}
private sealed class JobDataRuntimeState
{
public NativeList<EnemyJobInputData> EnemyJobInputs;
public NativeList<EnemyJobOutputData> EnemyJobOutputs;
public NativeList<EnemyJobOutputData> EnemyJobSeparationOutputs;
public NativeList<float2> EnemySeparationPreviousPushes;
public NativeList<float2> EnemySeparationCurrentPushes;
public NativeList<ProjectileJobInputData> ProjectileJobInputs;
public NativeList<ProjectileJobOutputData> ProjectileJobOutputs;
public NativeList<CollisionCandidateData> CollisionCandidates;
public NativeParallelMultiHashMap<long, int> EnemySeparationBuckets;
public NativeParallelMultiHashMap<long, int> EnemyCollisionBuckets;
public readonly List<AreaCollisionHitEventData> AreaCollisionHitEvents = new(32);
public readonly HashSet<long> AreaCollisionHitDedupKeys = new();
public int LastCollisionQueryCount;
public int LastProjectileCollisionQueryCount;
public int LastAreaCollisionQueryCount;
public int LastCollisionCandidateCount;
public int LastProjectileCollisionCandidateCount;
public int LastAreaCollisionCandidateCount;
public int LastResolvedAreaHitCount;
public float LastCollisionCellSize;
public bool LastCollisionHasEnemyTargets;
}
private sealed class CollisionPipelineRuntimeState
{
public JobHandle CollisionCandidateQueryHandle;
public bool CollisionCandidateQueryScheduled;
public readonly HashSet<int> ProjectileResolvedEntityIds = new();
}
private sealed class TargetSelectionRuntimeState
{
public NativeParallelMultiHashMap<long, int> EnemyTargetBuckets;
public bool EnemyTargetBucketsDirty = true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e562a5a506b1424bb1a9eff97c8c469e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,248 @@
using Entity;
using Entity.EntityData;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Simulation State Lifecycle
public void ClearSimulationState()
{
_enemies.Clear();
_projectiles.Clear();
_pickups.Clear();
_projectileRecycleEntityIds.Clear();
_projectileResolvedEntityIds.Clear();
_areaCollisionRequests.Clear();
_areaCollisionHitEvents.Clear();
_areaCollisionHitDedupKeys.Clear();
ClearJobDataChannels();
EnemyBinding.Clear();
ProjectileBinding.Clear();
PickupBinding.Clear();
ClearPlayerMovementState();
}
#endregion
#region Enemy Simulation State
private int AddEnemy(in EnemySimData simData)
{
int simulationIndex = _enemies.Count;
_enemies.Add(simData);
EnemyBinding.Bind(simData.EntityId, simulationIndex);
OnEnemyAddedToSeparationTemporalBuffers();
MarkEnemyTargetSpatialIndexDirty();
return simulationIndex;
}
private int UpsertEnemy(in EnemySimData simData)
{
if (!EnemyBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
{
return AddEnemy(simData);
}
_enemies[simulationIndex] = simData;
MarkEnemyTargetSpatialIndexDirty();
return simulationIndex;
}
private bool RemoveEnemyByEntityId(int entityId)
{
if (!EnemyBinding.TryGetSimulationIndex(entityId, out int simulationIndex))
{
return false;
}
int lastIndex = _enemies.Count - 1;
if (simulationIndex != lastIndex)
{
EnemySimData movedData = _enemies[lastIndex];
_enemies[simulationIndex] = movedData;
EnemyBinding.RemapIndex(movedData.EntityId, simulationIndex);
}
_enemies.RemoveAt(lastIndex);
OnEnemyRemovedFromSeparationTemporalBuffers(simulationIndex);
EnemyBinding.UnbindByEntityId(entityId);
MarkEnemyTargetSpatialIndexDirty();
return true;
}
private void RegisterEnemyLifecycle(EnemyBase enemy, object userData)
{
if (enemy == null || enemy.CachedTransform == null)
{
return;
}
EnemyData enemyData = userData as EnemyData;
UpsertEnemy(CreateEnemyInitialSimData(enemy, enemyData));
}
private void UnregisterEnemyLifecycle(int entityId)
{
RemoveEnemyByEntityId(entityId);
}
private bool TryGetEnemyData(int entityId, out EnemySimData enemyData)
{
if (!EnemyBinding.TryGetSimulationIndex(entityId, out int simulationIndex) || simulationIndex < 0 ||
simulationIndex >= _enemies.Count)
{
enemyData = default;
return false;
}
enemyData = _enemies[simulationIndex];
return true;
}
public void SyncEnemyMovementInput(int entityId, bool isMoving, in UnityEngine.Vector3 direction, float speed,
bool avoidEnemyOverlap, float enemyBodyRadius, int separationIterations)
{
if (!EnemyBinding.TryGetSimulationIndex(entityId, out int simulationIndex) || simulationIndex < 0 ||
simulationIndex >= _enemies.Count)
{
return;
}
EnemySimData enemyData = _enemies[simulationIndex];
enemyData.Speed = isMoving ? UnityEngine.Mathf.Max(0f, speed) : 0f;
enemyData.AvoidEnemyOverlap = avoidEnemyOverlap;
enemyData.EnemyBodyRadius = UnityEngine.Mathf.Max(0.01f, enemyBodyRadius);
enemyData.SeparationIterations = UnityEngine.Mathf.Max(1, separationIterations);
UnityEngine.Vector3 planarDirection = direction;
planarDirection.y = 0f;
if (planarDirection.sqrMagnitude > UnityEngine.Mathf.Epsilon)
{
enemyData.Forward = planarDirection.normalized;
}
_enemies[simulationIndex] = enemyData;
}
#endregion
#region Projectile Simulation State
private int AddProjectile(in ProjectileSimData simData)
{
int simulationIndex = _projectiles.Count;
_projectiles.Add(simData);
ProjectileBinding.Bind(simData.EntityId, simulationIndex);
return simulationIndex;
}
private int UpsertProjectile(in ProjectileSimData simData)
{
if (!ProjectileBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
{
return AddProjectile(simData);
}
_projectiles[simulationIndex] = simData;
return simulationIndex;
}
private bool RemoveProjectileByEntityId(int entityId)
{
if (!ProjectileBinding.TryGetSimulationIndex(entityId, out int simulationIndex))
{
return false;
}
int lastIndex = _projectiles.Count - 1;
if (simulationIndex != lastIndex)
{
ProjectileSimData movedData = _projectiles[lastIndex];
_projectiles[simulationIndex] = movedData;
ProjectileBinding.RemapIndex(movedData.EntityId, simulationIndex);
}
_projectiles.RemoveAt(lastIndex);
ProjectileBinding.UnbindByEntityId(entityId);
return true;
}
private void RegisterProjectileLifecycle(EntityBase projectileEntity, object userData)
{
if (projectileEntity == null || projectileEntity.CachedTransform == null)
{
return;
}
UpsertProjectile(CreateProjectileInitialSimData(projectileEntity, userData));
}
private void UnregisterProjectileLifecycle(int entityId)
{
RemoveProjectileByEntityId(entityId);
}
#endregion
#region Pickup Simulation State
private int AddPickup(in PickupSimData simData)
{
int simulationIndex = _pickups.Count;
_pickups.Add(simData);
PickupBinding.Bind(simData.EntityId, simulationIndex);
return simulationIndex;
}
private int UpsertPickup(in PickupSimData simData)
{
if (!PickupBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
{
return AddPickup(simData);
}
_pickups[simulationIndex] = simData;
return simulationIndex;
}
private bool RemovePickupByEntityId(int entityId)
{
if (!PickupBinding.TryGetSimulationIndex(entityId, out int simulationIndex))
{
return false;
}
int lastIndex = _pickups.Count - 1;
if (simulationIndex != lastIndex)
{
PickupSimData movedData = _pickups[lastIndex];
_pickups[simulationIndex] = movedData;
PickupBinding.RemapIndex(movedData.EntityId, simulationIndex);
}
_pickups.RemoveAt(lastIndex);
PickupBinding.UnbindByEntityId(entityId);
return true;
}
private void RegisterPickupLifecycle(EntityBase pickupEntity)
{
if (pickupEntity == null || pickupEntity.CachedTransform == null)
{
return;
}
UpsertPickup(CreatePickupInitialSimData(pickupEntity));
}
private void UnregisterPickupLifecycle(int entityId)
{
RemovePickupByEntityId(entityId);
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9a3f6ab748524f8f8ab6655e294889f1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,153 @@
using CustomDebugger;
using Unity.Collections;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
public bool TryGetNearestEnemyEntityId(Vector3 origin, float maxSqrRange, out int enemyEntityId)
{
enemyEntityId = 0;
if (maxSqrRange <= 0f || _enemies.Count == 0)
{
return false;
}
BuildEnemyTargetSpatialIndexIfNeeded();
float cellSize = GetTargetSelectionCellSize();
int centerCellX = ToCell(origin.x, cellSize);
int centerCellZ = ToCell(origin.z, cellSize);
float range = Mathf.Sqrt(maxSqrRange);
int queryRange = Mathf.Max(1, Mathf.CeilToInt(range / cellSize));
float minSqrDistance = maxSqrRange;
bool found = false;
using (CustomProfilerMarker.TargetSelection_QueryNeighbors.Auto())
{
for (int dx = -queryRange; dx <= queryRange; dx++)
{
for (int dz = -queryRange; dz <= queryRange; dz++)
{
long key = CellKey(centerCellX + dx, centerCellZ + dz);
if (!_enemyTargetBuckets.TryGetFirstValue(key, out int enemyIndex,
out NativeParallelMultiHashMapIterator<long> iterator))
{
continue;
}
do
{
if (enemyIndex < 0 || enemyIndex >= _enemies.Count)
{
continue;
}
EnemySimData enemy = _enemies[enemyIndex];
Vector3 delta = enemy.Position - origin;
delta.y = 0f;
float sqrDistance = delta.sqrMagnitude;
if (sqrDistance >= minSqrDistance)
{
continue;
}
minSqrDistance = sqrDistance;
enemyEntityId = enemy.EntityId;
found = true;
} while (_enemyTargetBuckets.TryGetNextValue(out enemyIndex, ref iterator));
}
}
}
return found;
}
private void InitializeEnemyTargetSpatialIndex()
{
if (_enemyTargetBuckets.IsCreated)
{
return;
}
_enemyTargetBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
_enemyTargetBucketsDirty = true;
}
private void DisposeEnemyTargetSpatialIndex()
{
if (_enemyTargetBuckets.IsCreated)
{
_enemyTargetBuckets.Dispose();
}
_enemyTargetBuckets = default;
_enemyTargetBucketsDirty = true;
}
private void ClearEnemyTargetSpatialIndex()
{
if (_enemyTargetBuckets.IsCreated)
{
_enemyTargetBuckets.Clear();
}
_enemyTargetBucketsDirty = true;
}
private void MarkEnemyTargetSpatialIndexDirty()
{
_enemyTargetBucketsDirty = true;
}
private void BuildEnemyTargetSpatialIndexIfNeeded()
{
InitializeEnemyTargetSpatialIndex();
if (!_enemyTargetBucketsDirty)
{
return;
}
using (CustomProfilerMarker.TargetSelection_BuildBuckets.Auto())
{
int enemyCount = _enemies.Count;
int desiredCapacity = Mathf.Max(256, enemyCount * 2 + 1);
if (_enemyTargetBuckets.Capacity < desiredCapacity)
{
_enemyTargetBuckets.Capacity = desiredCapacity;
}
_enemyTargetBuckets.Clear();
float cellSize = GetTargetSelectionCellSize();
for (int i = 0; i < enemyCount; i++)
{
EnemySimData enemy = _enemies[i];
int cellX = ToCell(enemy.Position.x, cellSize);
int cellZ = ToCell(enemy.Position.z, cellSize);
_enemyTargetBuckets.Add(CellKey(cellX, cellZ), i);
}
}
_enemyTargetBucketsDirty = false;
}
private float GetTargetSelectionCellSize()
{
return Mathf.Max(0.1f, _targetSelectionCellSize);
}
private static int ToCell(float value, float cellSize)
{
return Mathf.FloorToInt(value / cellSize);
}
private static long CellKey(int x, int z)
{
return ((long)x << 32) ^ (uint)z;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b327174958354540a46e8685f2272cb0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,94 @@
using System.Collections.Generic;
using CustomDebugger;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace Simulation
{
public sealed partial class SimulationWorld : GameFrameworkComponent
{
// Partial layout:
// - SimulationWorld.cs: 鏍稿績鐘舵€併€佸父閲忓拰 Unity 鐢熷懡鍛ㄦ湡鍏ュ彛鐐广€?
// - SimulationWorld.RuntimeModules.cs: 杩愯鏃跺煙瀵硅薄銆侀厤缃拰鐘舵€佷唬鐞嗐€?
// - SimulationWorld.SimEntityState.cs: 妯℃嫙鐘舵€佺殑澧炲垹鏀规煡鍜岀敓鍛藉懆鏈熸敞鍐屻€?
// - SimulationWorld.EntityToSimData.cs: Unity 瀹炰綋鍒?sim data 鐨勫垵濮嬪寲閫傞厤銆?
// - SimulationWorld.EntitySync.cs: GameFramework 瀹炰綋 show/hide 浜嬩欢妗ャ€?
// - SimulationWorld.TargetSelectionSpatialIndex.cs: 鏈€杩戞晫绌洪棿绱㈠紩鏌ヨ銆?
// - Presentation/SimulationWorld.TransformSync.cs: late-update transform 鍚屾妗ャ€?
// - Presentation/SimulationWorld.HitPresentation.cs: 鎶曞皠鐗╁懡涓簨浠惰〃鐜版ˉ銆?
// - DataChannel/SimulationWorld.JobDataChannel.cs: Job 閫氶亾鍏变韩瀛楁銆佸父閲忓拰杩愯鏃剁姸鎬併€?
// - DataChannel/SimulationWorld.JobDataLifecycle.cs: Native 閫氶亾鍒濆鍖栥€佹竻鐞嗗拰 clear銆?
// - DataChannel/SimulationWorld.JobDataConversion.cs: sim/job 鏁版嵁杞崲涓庤緭鍏ヨ緭鍑虹紦鍐插噯澶囥€?
// - DataChannel/SimulationWorld.CollisionTransient.cs: 纰版挒涓存椂閫氶亾鍜岃繍琛屾椂缁熻銆?
// - DataChannel/SimulationWorld.EnemySeparationTemporal.cs: 鏁屼汉鍒嗙鐨勫抚闂翠复鏃剁姸鎬併€?
// - DataChannel/SimulationWorld.JobOutputCommit.cs: Job 杈撳嚭鍥炲啓涓诲鍣ㄣ€?
// - Jobs/SimulationWorld.EnemyJobs.cs: 妯℃嫙閫氶亾 缂栨帓 + 鏁屼汉绉诲姩/鍒嗙 椤哄簭鎵ц
// - Jobs/SimulationWorld.ProjectileJobs.cs: 鎶曞皠鐗╃Щ鍔ㄤ笌鍥炴敹
// - Jobs/SimulationWorld.CollisionPipeline.cs: 纰版挒绠$嚎鍏变韩閰嶇疆鍜岀姸鎬?
// - Jobs/SimulationWorld.CollisionRequests.cs: area/sector 璇锋眰缂撳啿
// - Jobs/SimulationWorld.CollisionBroadPhase.cs: broad-phase 鍊欓€夋瀯寤哄拰 Job 璋冨害
// - Jobs/SimulationWorld.CollisionResolve.cs: 涓荤嚎绋嬪懡涓粨绠椾笌 area settle
// - Jobs/SimulationWorld.CollisionPresentation.cs: 鍛戒腑琛ㄧ幇浜嬩欢鍜屽疄浣?impact 瑙f瀽
// - JobStruct/*.cs: burst job 鍐呮牳鍜岄潰鍚?job 鐨勬暟鎹粨鏋?
private const float DefaultAttackRange = 1f;
private const int EnemyStateIdle = 0;
private const int EnemyStateChasing = 1;
private const int EnemyStateInAttackRange = 2;
private const int ProjectileStateActive = 0;
private const int ProjectileStateExpired = 1;
private EntitySync _entitySync;
private TransformSync _transformSync;
private HitPresentation _hitPresentation;
public IReadOnlyList<EnemySimData> Enemies => _enemies;
public IReadOnlyList<ProjectileSimData> Projectiles => _projectiles;
public IReadOnlyList<PickupSimData> Pickups => _pickups;
#region Lifecycle
protected override void Awake()
{
base.Awake();
_entitySync = new EntitySync(this);
_transformSync = new TransformSync(this);
_hitPresentation = new HitPresentation(this);
InitializeJobDataChannels();
}
private void Start()
{
_entitySync?.OnStart();
_hitPresentation?.OnStart();
}
public void Tick(in SimulationTickContext context)
{
Vector3 playerPosition = ResolvePlayerPositionForTick(in context);
SimulationTickContext resolvedContext =
new SimulationTickContext(context.DeltaTime, context.RealDeltaTime, playerPosition);
using (CustomProfilerMarker.TickEnemies.Auto())
{
TickSimulationPipeline(in resolvedContext);
}
}
private void OnDestroy()
{
_hitPresentation?.OnDestroy();
_entitySync?.OnDestroy();
_entitySync = null;
_transformSync = null;
_hitPresentation = null;
DisposeJobDataChannels();
}
private void LateUpdate()
{
_transformSync?.OnLateUpdate();
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8a558ebbc9cb4d94946ac9f4f27914d8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: