parent
34b1424bb6
commit
5fb7ea499f
|
|
@ -154,7 +154,7 @@ MonoBehaviour:
|
||||||
_cachedTransform: {fileID: 7683855655592166216}
|
_cachedTransform: {fileID: 7683855655592166216}
|
||||||
_avoidEnemyOverlap: 0
|
_avoidEnemyOverlap: 0
|
||||||
_enemyBodyRadius: 0.45
|
_enemyBodyRadius: 0.45
|
||||||
_separationIterations: 2
|
_separationIterations: 5
|
||||||
_speedBase: 0
|
_speedBase: 0
|
||||||
--- !u!114 &6353753365317756414
|
--- !u!114 &6353753365317756414
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ using System;
|
||||||
using CustomUtility;
|
using CustomUtility;
|
||||||
using Definition.DataStruct;
|
using Definition.DataStruct;
|
||||||
using Definition.Enum;
|
using Definition.Enum;
|
||||||
using Unity.Profiling;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using CustomDebugger;
|
using CustomDebugger;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ namespace CustomComponent
|
||||||
private EntityComponent _entity;
|
private EntityComponent _entity;
|
||||||
|
|
||||||
private List<EntityBase> _enemies;
|
private List<EntityBase> _enemies;
|
||||||
|
private Dictionary<int, EntityBase> _enemyById;
|
||||||
|
|
||||||
public List<EntityBase> Enemies => _enemies;
|
public List<EntityBase> Enemies => _enemies;
|
||||||
|
|
||||||
|
|
@ -58,6 +59,7 @@ namespace CustomComponent
|
||||||
{
|
{
|
||||||
_entity = GameEntry.Entity;
|
_entity = GameEntry.Entity;
|
||||||
_enemies = new List<EntityBase>();
|
_enemies = new List<EntityBase>();
|
||||||
|
_enemyById = new Dictionary<int, EntityBase>();
|
||||||
|
|
||||||
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||||
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||||
|
|
@ -69,6 +71,7 @@ namespace CustomComponent
|
||||||
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||||
|
|
||||||
_enemies = null;
|
_enemies = null;
|
||||||
|
_enemyById = null;
|
||||||
_entity = null;
|
_entity = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,6 +160,25 @@ namespace CustomComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
_enemies.Clear();
|
_enemies.Clear();
|
||||||
|
_enemyById?.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetEnemy(int entityId, out EntityBase enemy)
|
||||||
|
{
|
||||||
|
enemy = null;
|
||||||
|
if (_enemyById == null || !_enemyById.TryGetValue(entityId, out EntityBase cachedEnemy))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedEnemy == null || !cachedEnemy.Available)
|
||||||
|
{
|
||||||
|
_enemyById.Remove(entityId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
enemy = cachedEnemy;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetSpawnRateScale(float scale)
|
public void SetSpawnRateScale(float scale)
|
||||||
|
|
@ -218,6 +240,7 @@ namespace CustomComponent
|
||||||
enemy.SetTarget(_player);
|
enemy.SetTarget(_player);
|
||||||
RemoveEnemyFromCache(enemy.Id);
|
RemoveEnemyFromCache(enemy.Id);
|
||||||
_enemies.Add(enemy);
|
_enemies.Add(enemy);
|
||||||
|
_enemyById[enemy.Id] = enemy;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ne.EntityLogicType == typeof(Player))
|
if (ne.EntityLogicType == typeof(Player))
|
||||||
|
|
@ -245,11 +268,21 @@ namespace CustomComponent
|
||||||
|
|
||||||
private void RemoveEnemyFromCache(int entityId)
|
private void RemoveEnemyFromCache(int entityId)
|
||||||
{
|
{
|
||||||
|
if (_enemyById != null)
|
||||||
|
{
|
||||||
|
_enemyById.Remove(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = _enemies.Count - 1; i >= 0; i--)
|
for (int i = _enemies.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
EntityBase cachedEnemy = _enemies[i];
|
EntityBase cachedEnemy = _enemies[i];
|
||||||
if (cachedEnemy == null || cachedEnemy.Id == entityId)
|
if (cachedEnemy == null || cachedEnemy.Id == entityId)
|
||||||
{
|
{
|
||||||
|
if (cachedEnemy != null && _enemyById != null)
|
||||||
|
{
|
||||||
|
_enemyById.Remove(cachedEnemy.Id);
|
||||||
|
}
|
||||||
|
|
||||||
_enemies.RemoveAt(i);
|
_enemies.RemoveAt(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ namespace CustomDebugger
|
||||||
public static readonly ProfilerMarker TickEnemies_MoveSeparation = new ProfilerMarker("TickEnemies.MoveSeparation");
|
public static readonly ProfilerMarker TickEnemies_MoveSeparation = new ProfilerMarker("TickEnemies.MoveSeparation");
|
||||||
public static readonly ProfilerMarker TickEnemies_StateUpdate = new ProfilerMarker("TickEnemies.StateUpdate");
|
public static readonly ProfilerMarker TickEnemies_StateUpdate = new ProfilerMarker("TickEnemies.StateUpdate");
|
||||||
public static readonly ProfilerMarker TickEnemies_WriteBack = new ProfilerMarker("TickEnemies.WriteBack");
|
public static readonly ProfilerMarker TickEnemies_WriteBack = new ProfilerMarker("TickEnemies.WriteBack");
|
||||||
|
public static readonly ProfilerMarker TargetSelection_BuildBuckets = new("TargetSelection.BuildBuckets");
|
||||||
|
public static readonly ProfilerMarker TargetSelection_QueryNeighbors = new("TargetSelection.QueryNeighbors");
|
||||||
public static readonly ProfilerMarker Movement_Update = new ProfilerMarker("Movement_Update");
|
public static readonly ProfilerMarker Movement_Update = new ProfilerMarker("Movement_Update");
|
||||||
public static readonly ProfilerMarker ShopUI_Update = new("UGF.ShopUI.Update");
|
public static readonly ProfilerMarker ShopUI_Update = new("UGF.ShopUI.Update");
|
||||||
public static readonly ProfilerMarker Inventory_Refresh = new("UGF.Inventory.Refresh");
|
public static readonly ProfilerMarker Inventory_Refresh = new("UGF.Inventory.Refresh");
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using CustomUtility;
|
using CustomUtility;
|
||||||
|
using CustomComponent;
|
||||||
|
|
||||||
namespace Entity.Weapon
|
namespace Entity.Weapon
|
||||||
{
|
{
|
||||||
|
|
@ -12,6 +13,11 @@ namespace Entity.Weapon
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (TrySelectFromSpatialIndex(weapon, maxSqrRange, out EntityBase indexedTarget))
|
||||||
|
{
|
||||||
|
return indexedTarget;
|
||||||
|
}
|
||||||
|
|
||||||
EntityBase target = null;
|
EntityBase target = null;
|
||||||
float minSqrMagnitude = maxSqrRange > 0f ? maxSqrRange : float.MaxValue;
|
float minSqrMagnitude = maxSqrRange > 0f ? maxSqrRange : float.MaxValue;
|
||||||
|
|
||||||
|
|
@ -28,5 +34,35 @@ namespace Entity.Weapon
|
||||||
|
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TrySelectFromSpatialIndex(WeaponBase weapon, float maxSqrRange, out EntityBase target)
|
||||||
|
{
|
||||||
|
target = null;
|
||||||
|
if (weapon == null || maxSqrRange <= 0f || weapon.CachedTransform == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var simulationWorld = GameEntry.SimulationWorld;
|
||||||
|
if (simulationWorld == null || !simulationWorld.UseSimulationMovement || !simulationWorld.UseJobSimulation)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!simulationWorld.TryGetNearestEnemyEntityId(weapon.CachedTransform.position, maxSqrRange,
|
||||||
|
out int entityId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnemyManagerComponent enemyManager = GameEntry.EnemyManager;
|
||||||
|
if (enemyManager == null || !enemyManager.TryGetEnemy(entityId, out EntityBase enemy))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
target = enemy;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using CustomDebugger;
|
using CustomDebugger;
|
||||||
using CustomUtility;
|
|
||||||
using Unity.Burst;
|
using Unity.Burst;
|
||||||
using Unity.Collections;
|
using Unity.Collections;
|
||||||
using Unity.Jobs;
|
using Unity.Jobs;
|
||||||
|
|
@ -10,6 +9,21 @@ namespace Simulation
|
||||||
{
|
{
|
||||||
public sealed partial class SimulationWorld
|
public sealed partial class SimulationWorld
|
||||||
{
|
{
|
||||||
|
[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;
|
||||||
|
|
||||||
[BurstCompile]
|
[BurstCompile]
|
||||||
private struct EnemyMovementBurstJob : IJobParallelFor
|
private struct EnemyMovementBurstJob : IJobParallelFor
|
||||||
{
|
{
|
||||||
|
|
@ -37,6 +51,78 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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 struct BuildEnemySeparationBucketsJob : 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 struct EnemySeparationJob : 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 void TickEnemiesJobified(in SimulationTickContext context)
|
private void TickEnemiesJobified(in SimulationTickContext context)
|
||||||
{
|
{
|
||||||
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
|
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
|
||||||
|
|
@ -51,7 +137,7 @@ namespace Simulation
|
||||||
|
|
||||||
using (CustomProfilerMarker.TickEnemies_MoveSeparation.Auto())
|
using (CustomProfilerMarker.TickEnemies_MoveSeparation.Auto())
|
||||||
{
|
{
|
||||||
ApplyEnemySeparationForJobOutput();
|
ApplyEnemySeparationForJobOutput(in context);
|
||||||
}
|
}
|
||||||
|
|
||||||
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
|
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
|
||||||
|
|
@ -59,6 +145,9 @@ namespace Simulation
|
||||||
SyncProjectilesToJobOutput();
|
SyncProjectilesToJobOutput();
|
||||||
ApplyJobOutputToSimulation();
|
ApplyJobOutputToSimulation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarkEnemyTargetSpatialIndexDirty();
|
||||||
|
BuildEnemyTargetSpatialIndexIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteEnemyMovementJob(in SimulationTickContext context)
|
private void ExecuteEnemyMovementJob(in SimulationTickContext context)
|
||||||
|
|
@ -130,15 +219,17 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyEnemySeparationForJobOutput()
|
private void ApplyEnemySeparationForJobOutput(in SimulationTickContext context)
|
||||||
{
|
{
|
||||||
if (_enemyJobOutputs.Length == 0)
|
int enemyCount = _enemyJobOutputs.Length;
|
||||||
|
if (enemyCount == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_enemySeparationAgents.Clear();
|
bool hasSeparationCandidates = false;
|
||||||
for (int i = 0; i < _enemyJobOutputs.Length; i++)
|
float maxRadius = 0.45f;
|
||||||
|
for (int i = 0; i < enemyCount; i++)
|
||||||
{
|
{
|
||||||
EnemyJobOutputData output = _enemyJobOutputs[i];
|
EnemyJobOutputData output = _enemyJobOutputs[i];
|
||||||
if (!output.AvoidEnemyOverlap)
|
if (!output.AvoidEnemyOverlap)
|
||||||
|
|
@ -146,39 +237,295 @@ namespace Simulation
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector3 position = output.Position;
|
hasSeparationCandidates = true;
|
||||||
position.y = 0f;
|
float radius = output.EnemyBodyRadius > 0f ? output.EnemyBodyRadius : 0.45f;
|
||||||
_enemySeparationAgents.Add(new EnemySeparationAgent
|
if (radius > maxRadius)
|
||||||
{
|
{
|
||||||
AgentId = output.EntityId,
|
maxRadius = radius;
|
||||||
Position = position,
|
}
|
||||||
Radius = output.EnemyBodyRadius > 0f ? output.EnemyBodyRadius : 0.45f
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_enemySeparationAgents.Count == 0)
|
if (!hasSeparationCandidates)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
EnemySeparationSolverProvider.SetSimulationAgents(_enemySeparationAgents);
|
float autoCellSize = maxRadius * 2f;
|
||||||
for (int i = 0; i < _enemyJobOutputs.Length; i++)
|
float configuredCellSize = _enemySeparationCellSize > 0f ? _enemySeparationCellSize : autoCellSize;
|
||||||
|
float cellSize = Mathf.Max(0.1f, configuredCellSize);
|
||||||
|
int bucketCapacity = Mathf.Max(128, enemyCount * 2);
|
||||||
|
PrepareEnemySeparationJobBuffers(enemyCount, bucketCapacity);
|
||||||
|
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();
|
||||||
|
JobHandle buildHandle;
|
||||||
|
|
||||||
|
if (_useBurstJobs)
|
||||||
{
|
{
|
||||||
EnemyJobOutputData output = _enemyJobOutputs[i];
|
BuildEnemySeparationBucketsBurstJob buildJob = new BuildEnemySeparationBucketsBurstJob
|
||||||
if (!output.AvoidEnemyOverlap || output.State != EnemyStateChasing)
|
{
|
||||||
|
Inputs = inputArray,
|
||||||
|
Buckets = _enemySeparationBuckets.AsParallelWriter(),
|
||||||
|
CellSize = cellSize
|
||||||
|
};
|
||||||
|
buildHandle = buildJob.Schedule(enemyCount, 64);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BuildEnemySeparationBucketsJob buildJob = new BuildEnemySeparationBucketsJob
|
||||||
|
{
|
||||||
|
Inputs = inputArray,
|
||||||
|
Buckets = _enemySeparationBuckets.AsParallelWriter(),
|
||||||
|
CellSize = cellSize
|
||||||
|
};
|
||||||
|
buildHandle = buildJob.Schedule(enemyCount, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
JobHandle separationHandle;
|
||||||
|
if (_useBurstJobs)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EnemySeparationJob separationJob = new EnemySeparationJob
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
separationHandle.Complete();
|
||||||
|
CommitEnemySeparationTemporalBuffers(enemyCount);
|
||||||
|
for (int i = 0; i < enemyCount; i++)
|
||||||
|
{
|
||||||
|
_enemyJobOutputs[i] = _enemyJobSeparationOutputs[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = new float3(output.Position.x, 0f, output.Position.z);
|
||||||
|
int cellX = (int)math.floor(position.x / cellSize);
|
||||||
|
int cellZ = (int)math.floor(position.z / cellSize);
|
||||||
|
buckets.Add(SeparationCellKey(cellX, cellZ), index);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = new float3(self.Position.x, 0f, self.Position.z);
|
||||||
|
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 = new float3(other.Position.x, 0f, other.Position.z);
|
||||||
|
float3 toSelf = candidate - otherPosition;
|
||||||
|
float sqrDistance = math.lengthsq(toSelf);
|
||||||
|
|
||||||
|
if (sqrDistance <= float.Epsilon)
|
||||||
|
{
|
||||||
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector3 resolvedPosition = EnemySeparationSolverProvider.ResolveSimulation(
|
float3 resolvedPush = pushAccumulation * pushDamping;
|
||||||
output.EntityId,
|
|
||||||
output.Position,
|
|
||||||
output.Forward,
|
|
||||||
output.SeparationIterations > 0 ? output.SeparationIterations : 1);
|
|
||||||
|
|
||||||
output.Position = resolvedPosition;
|
float maxStep = selfRadius * maxStepScale;
|
||||||
_enemyJobOutputs[i] = output;
|
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 Vector3(finalPosition.x, self.Position.y, finalPosition.z);
|
||||||
|
if (math.lengthsq(smoothedPush) > float.Epsilon)
|
||||||
|
{
|
||||||
|
self.Forward = new Vector3(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 long SeparationCellKey(int x, int z)
|
||||||
|
{
|
||||||
|
return ((long)x << 32) ^ (uint)z;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ExecuteEnemyMovement(int index, NativeArray<EnemyJobInputData> inputs,
|
private static void ExecuteEnemyMovement(int index, NativeArray<EnemyJobInputData> inputs,
|
||||||
|
|
@ -197,7 +544,8 @@ namespace Simulation
|
||||||
|
|
||||||
float3 forward = new float3(input.Forward.x, input.Forward.y, input.Forward.z);
|
float3 forward = new float3(input.Forward.x, input.Forward.y, input.Forward.z);
|
||||||
float3 desiredPosition = currentPosition;
|
float3 desiredPosition = currentPosition;
|
||||||
quaternion rotation = new quaternion(input.Rotation.x, input.Rotation.y, input.Rotation.z, input.Rotation.w);
|
quaternion rotation =
|
||||||
|
new quaternion(input.Rotation.x, input.Rotation.y, input.Rotation.z, input.Rotation.w);
|
||||||
|
|
||||||
if (canChase)
|
if (canChase)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using Unity.Collections;
|
using Unity.Collections;
|
||||||
|
using Unity.Mathematics;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace Simulation
|
namespace Simulation
|
||||||
|
|
@ -60,8 +61,12 @@ namespace Simulation
|
||||||
|
|
||||||
private NativeList<EnemyJobInputData> _enemyJobInputs;
|
private NativeList<EnemyJobInputData> _enemyJobInputs;
|
||||||
private NativeList<EnemyJobOutputData> _enemyJobOutputs;
|
private NativeList<EnemyJobOutputData> _enemyJobOutputs;
|
||||||
|
private NativeList<EnemyJobOutputData> _enemyJobSeparationOutputs;
|
||||||
|
private NativeList<float2> _enemySeparationPreviousPushes;
|
||||||
|
private NativeList<float2> _enemySeparationCurrentPushes;
|
||||||
private NativeList<ProjectileJobInputData> _projectileJobInputs;
|
private NativeList<ProjectileJobInputData> _projectileJobInputs;
|
||||||
private NativeList<ProjectileJobOutputData> _projectileJobOutputs;
|
private NativeList<ProjectileJobOutputData> _projectileJobOutputs;
|
||||||
|
private NativeParallelMultiHashMap<long, int> _enemySeparationBuckets;
|
||||||
|
|
||||||
private void InitializeJobDataChannels()
|
private void InitializeJobDataChannels()
|
||||||
{
|
{
|
||||||
|
|
@ -73,8 +78,13 @@ namespace Simulation
|
||||||
DisposeJobDataChannels();
|
DisposeJobDataChannels();
|
||||||
_enemyJobInputs = new NativeList<EnemyJobInputData>(64, Allocator.Persistent);
|
_enemyJobInputs = new NativeList<EnemyJobInputData>(64, Allocator.Persistent);
|
||||||
_enemyJobOutputs = new NativeList<EnemyJobOutputData>(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);
|
_projectileJobInputs = new NativeList<ProjectileJobInputData>(64, Allocator.Persistent);
|
||||||
_projectileJobOutputs = new NativeList<ProjectileJobOutputData>(64, Allocator.Persistent);
|
_projectileJobOutputs = new NativeList<ProjectileJobOutputData>(64, Allocator.Persistent);
|
||||||
|
_enemySeparationBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
|
||||||
|
InitializeEnemyTargetSpatialIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisposeJobDataChannels()
|
private void DisposeJobDataChannels()
|
||||||
|
|
@ -91,6 +101,24 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
_enemyJobOutputs = default;
|
_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)
|
if (_projectileJobInputs.IsCreated)
|
||||||
{
|
{
|
||||||
_projectileJobInputs.Dispose();
|
_projectileJobInputs.Dispose();
|
||||||
|
|
@ -102,6 +130,14 @@ namespace Simulation
|
||||||
_projectileJobOutputs.Dispose();
|
_projectileJobOutputs.Dispose();
|
||||||
}
|
}
|
||||||
_projectileJobOutputs = default;
|
_projectileJobOutputs = default;
|
||||||
|
|
||||||
|
if (_enemySeparationBuckets.IsCreated)
|
||||||
|
{
|
||||||
|
_enemySeparationBuckets.Dispose();
|
||||||
|
}
|
||||||
|
_enemySeparationBuckets = default;
|
||||||
|
|
||||||
|
DisposeEnemyTargetSpatialIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearJobDataChannels()
|
private void ClearJobDataChannels()
|
||||||
|
|
@ -130,6 +166,28 @@ namespace Simulation
|
||||||
{
|
{
|
||||||
_projectileJobOutputs.Clear();
|
_projectileJobOutputs.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_enemyJobSeparationOutputs.IsCreated)
|
||||||
|
{
|
||||||
|
_enemyJobSeparationOutputs.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_enemySeparationPreviousPushes.IsCreated)
|
||||||
|
{
|
||||||
|
_enemySeparationPreviousPushes.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_enemySeparationCurrentPushes.IsCreated)
|
||||||
|
{
|
||||||
|
_enemySeparationCurrentPushes.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_enemySeparationBuckets.IsCreated)
|
||||||
|
{
|
||||||
|
_enemySeparationBuckets.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearEnemyTargetSpatialIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SyncSimulationToJobInput()
|
private void SyncSimulationToJobInput()
|
||||||
|
|
@ -196,6 +254,86 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PrepareEnemySeparationJobBuffers(int enemyCount, int bucketCapacity)
|
||||||
|
{
|
||||||
|
InitializeJobDataChannels();
|
||||||
|
EnsureCapacity(ref _enemyJobSeparationOutputs, enemyCount);
|
||||||
|
_enemyJobSeparationOutputs.Clear();
|
||||||
|
if (enemyCount > 0)
|
||||||
|
{
|
||||||
|
_enemyJobSeparationOutputs.ResizeUninitialized(enemyCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureCapacity(ref _enemySeparationBuckets, bucketCapacity);
|
||||||
|
_enemySeparationBuckets.Clear();
|
||||||
|
|
||||||
|
EnsureCapacity(ref _enemySeparationPreviousPushes, enemyCount);
|
||||||
|
EnsureCapacity(ref _enemySeparationCurrentPushes, enemyCount);
|
||||||
|
|
||||||
|
if (_enemySeparationPreviousPushes.Length < enemyCount)
|
||||||
|
{
|
||||||
|
int oldLength = _enemySeparationPreviousPushes.Length;
|
||||||
|
_enemySeparationPreviousPushes.ResizeUninitialized(enemyCount);
|
||||||
|
for (int i = oldLength; i < enemyCount; i++)
|
||||||
|
{
|
||||||
|
_enemySeparationPreviousPushes[i] = float2.zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_enemySeparationPreviousPushes.Length > enemyCount)
|
||||||
|
{
|
||||||
|
_enemySeparationPreviousPushes.ResizeUninitialized(enemyCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
_enemySeparationCurrentPushes.Clear();
|
||||||
|
if (enemyCount > 0)
|
||||||
|
{
|
||||||
|
_enemySeparationCurrentPushes.ResizeUninitialized(enemyCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CommitEnemySeparationTemporalBuffers(int enemyCount)
|
||||||
|
{
|
||||||
|
if (!_enemySeparationPreviousPushes.IsCreated || !_enemySeparationCurrentPushes.IsCreated)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int copyCount = math.min(enemyCount,
|
||||||
|
math.min(_enemySeparationPreviousPushes.Length, _enemySeparationCurrentPushes.Length));
|
||||||
|
for (int i = 0; i < copyCount; i++)
|
||||||
|
{
|
||||||
|
_enemySeparationPreviousPushes[i] = _enemySeparationCurrentPushes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnemyAddedToSeparationTemporalBuffers()
|
||||||
|
{
|
||||||
|
if (_enemySeparationPreviousPushes.IsCreated)
|
||||||
|
{
|
||||||
|
_enemySeparationPreviousPushes.Add(float2.zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_enemySeparationCurrentPushes.IsCreated)
|
||||||
|
{
|
||||||
|
_enemySeparationCurrentPushes.Add(float2.zero);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnemyRemovedFromSeparationTemporalBuffers(int removedIndex)
|
||||||
|
{
|
||||||
|
if (_enemySeparationPreviousPushes.IsCreated && removedIndex >= 0 &&
|
||||||
|
removedIndex < _enemySeparationPreviousPushes.Length)
|
||||||
|
{
|
||||||
|
_enemySeparationPreviousPushes.RemoveAtSwapBack(removedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_enemySeparationCurrentPushes.IsCreated && removedIndex >= 0 &&
|
||||||
|
removedIndex < _enemySeparationCurrentPushes.Length)
|
||||||
|
{
|
||||||
|
_enemySeparationCurrentPushes.RemoveAtSwapBack(removedIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ApplyJobOutputToSimulation()
|
private void ApplyJobOutputToSimulation()
|
||||||
{
|
{
|
||||||
int enemyCount = Mathf.Min(_enemies.Count, _enemyJobOutputs.Length);
|
int enemyCount = Mathf.Min(_enemies.Count, _enemyJobOutputs.Length);
|
||||||
|
|
@ -215,8 +353,12 @@ namespace Simulation
|
||||||
{
|
{
|
||||||
return IsNativeListUsable(_enemyJobInputs) &&
|
return IsNativeListUsable(_enemyJobInputs) &&
|
||||||
IsNativeListUsable(_enemyJobOutputs) &&
|
IsNativeListUsable(_enemyJobOutputs) &&
|
||||||
|
IsNativeListUsable(_enemyJobSeparationOutputs) &&
|
||||||
|
IsNativeListUsable(_enemySeparationPreviousPushes) &&
|
||||||
|
IsNativeListUsable(_enemySeparationCurrentPushes) &&
|
||||||
IsNativeListUsable(_projectileJobInputs) &&
|
IsNativeListUsable(_projectileJobInputs) &&
|
||||||
IsNativeListUsable(_projectileJobOutputs);
|
IsNativeListUsable(_projectileJobOutputs) &&
|
||||||
|
IsNativeMultiHashMapUsable(_enemySeparationBuckets);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void EnsureCapacity<T>(ref NativeList<T> nativeList, int targetCount) where T : unmanaged
|
private static void EnsureCapacity<T>(ref NativeList<T> nativeList, int targetCount) where T : unmanaged
|
||||||
|
|
@ -250,6 +392,24 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsNativeMultiHashMapUsable(NativeParallelMultiHashMap<long, int> hashMap)
|
||||||
|
{
|
||||||
|
if (!hashMap.IsCreated)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = hashMap.Count();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static EnemyJobInputData ConvertToEnemyJobInput(in EnemySimData enemy)
|
private static EnemyJobInputData ConvertToEnemyJobInput(in EnemySimData enemy)
|
||||||
{
|
{
|
||||||
return new EnemyJobInputData
|
return new EnemyJobInputData
|
||||||
|
|
@ -345,5 +505,18 @@ namespace Simulation
|
||||||
State = projectile.State
|
State = projectile.State
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void EnsureCapacity(ref NativeParallelMultiHashMap<long, int> hashMap, int targetCount)
|
||||||
|
{
|
||||||
|
if (!hashMap.IsCreated || targetCount <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashMap.Capacity < targetCount)
|
||||||
|
{
|
||||||
|
hashMap.Capacity = targetCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
using CustomDebugger;
|
||||||
|
using Unity.Collections;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Simulation
|
||||||
|
{
|
||||||
|
public sealed partial class SimulationWorld
|
||||||
|
{
|
||||||
|
private NativeParallelMultiHashMap<long, int> _enemyTargetBuckets;
|
||||||
|
private bool _enemyTargetBucketsDirty = true;
|
||||||
|
private int _enemyTargetBucketsFrame = -1;
|
||||||
|
|
||||||
|
[SerializeField] private float _targetSelectionCellSize = 2f;
|
||||||
|
|
||||||
|
public bool TryGetNearestEnemyEntityId(Vector3 origin, float maxSqrRange, out int enemyEntityId)
|
||||||
|
{
|
||||||
|
enemyEntityId = 0;
|
||||||
|
if (maxSqrRange <= 0f || _enemies.Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_useSimulationMovement || !_useJobSimulation)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
_enemyTargetBucketsFrame = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisposeEnemyTargetSpatialIndex()
|
||||||
|
{
|
||||||
|
if (_enemyTargetBuckets.IsCreated)
|
||||||
|
{
|
||||||
|
_enemyTargetBuckets.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_enemyTargetBuckets = default;
|
||||||
|
_enemyTargetBucketsDirty = true;
|
||||||
|
_enemyTargetBucketsFrame = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearEnemyTargetSpatialIndex()
|
||||||
|
{
|
||||||
|
if (_enemyTargetBuckets.IsCreated)
|
||||||
|
{
|
||||||
|
_enemyTargetBuckets.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
_enemyTargetBucketsDirty = true;
|
||||||
|
_enemyTargetBucketsFrame = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkEnemyTargetSpatialIndexDirty()
|
||||||
|
{
|
||||||
|
_enemyTargetBucketsDirty = true;
|
||||||
|
_enemyTargetBucketsFrame = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildEnemyTargetSpatialIndexIfNeeded()
|
||||||
|
{
|
||||||
|
InitializeEnemyTargetSpatialIndex();
|
||||||
|
|
||||||
|
if (!_enemyTargetBucketsDirty && _enemyTargetBucketsFrame == Time.frameCount)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
_enemyTargetBucketsFrame = Time.frameCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
@ -34,9 +34,14 @@ namespace Simulation
|
||||||
public int NextState;
|
public int NextState;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SerializeField] private bool _useSimulationMovement;
|
[Header("模拟世界全局设置")] [Tooltip("是否启用世界模拟")] [SerializeField]
|
||||||
[SerializeField] private bool _useJobSimulation;
|
private bool _useSimulationMovement;
|
||||||
[SerializeField] private bool _useBurstJobs = true;
|
|
||||||
|
[Tooltip("是否启用 Job 运算路径")] [SerializeField]
|
||||||
|
private bool _useJobSimulation;
|
||||||
|
|
||||||
|
[Tooltip("是否使用 Burst 来完成计算")] [SerializeField]
|
||||||
|
private bool _useBurstJobs = true;
|
||||||
|
|
||||||
private EntitySync _entitySync;
|
private EntitySync _entitySync;
|
||||||
private Presentation _presentation;
|
private Presentation _presentation;
|
||||||
|
|
@ -104,6 +109,8 @@ namespace Simulation
|
||||||
int simulationIndex = _enemies.Count;
|
int simulationIndex = _enemies.Count;
|
||||||
_enemies.Add(simData);
|
_enemies.Add(simData);
|
||||||
EnemyBinding.Bind(simData.EntityId, simulationIndex);
|
EnemyBinding.Bind(simData.EntityId, simulationIndex);
|
||||||
|
OnEnemyAddedToSeparationTemporalBuffers();
|
||||||
|
MarkEnemyTargetSpatialIndexDirty();
|
||||||
return simulationIndex;
|
return simulationIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,6 +122,7 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
|
|
||||||
_enemies[simulationIndex] = simData;
|
_enemies[simulationIndex] = simData;
|
||||||
|
MarkEnemyTargetSpatialIndexDirty();
|
||||||
return simulationIndex;
|
return simulationIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,7 +142,9 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
|
|
||||||
_enemies.RemoveAt(lastIndex);
|
_enemies.RemoveAt(lastIndex);
|
||||||
|
OnEnemyRemovedFromSeparationTemporalBuffers(simulationIndex);
|
||||||
EnemyBinding.UnbindByEntityId(entityId);
|
EnemyBinding.UnbindByEntityId(entityId);
|
||||||
|
MarkEnemyTargetSpatialIndexDirty();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,6 +298,7 @@ namespace Simulation
|
||||||
{
|
{
|
||||||
TickEnemiesJobified(in context);
|
TickEnemiesJobified(in context);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -455,6 +466,8 @@ namespace Simulation
|
||||||
|
|
||||||
private void WriteBackEnemyTickResults()
|
private void WriteBackEnemyTickResults()
|
||||||
{
|
{
|
||||||
|
bool hasPositionChanged = false;
|
||||||
|
|
||||||
for (int i = 0; i < _enemyTickWorkItems.Count; i++)
|
for (int i = 0; i < _enemyTickWorkItems.Count; i++)
|
||||||
{
|
{
|
||||||
EnemySimData enemy = _enemies[i];
|
EnemySimData enemy = _enemies[i];
|
||||||
|
|
@ -468,11 +481,18 @@ namespace Simulation
|
||||||
{
|
{
|
||||||
enemy.Rotation = workItem.Rotation;
|
enemy.Rotation = workItem.Rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasPositionChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
enemy.State = workItem.NextState;
|
enemy.State = workItem.NextState;
|
||||||
_enemies[i] = enemy;
|
_enemies[i] = enemy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasPositionChanged)
|
||||||
|
{
|
||||||
|
MarkEnemyTargetSpatialIndexDirty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static EnemySimData CreateEnemyInitialSimData(EnemyBase enemy, EnemyData enemyData)
|
private static EnemySimData CreateEnemyInitialSimData(EnemyBase enemy, EnemyData enemyData)
|
||||||
|
|
|
||||||
|
|
@ -1427,6 +1427,17 @@ MonoBehaviour:
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
_useSimulationMovement: 1
|
_useSimulationMovement: 1
|
||||||
|
_useJobSimulation: 1
|
||||||
|
_useBurstJobs: 1
|
||||||
|
_enemySeparationCellSize: 0
|
||||||
|
_enemySeparationPushDamping: 0.5
|
||||||
|
_enemySeparationMaxStepScale: 0.5
|
||||||
|
_enemySeparationZeroDistancePushScale: 1
|
||||||
|
_enemySeparationUseTangentialInAttackRange: 1
|
||||||
|
_enemySeparationPushSmoothing: 0.55
|
||||||
|
_enemySeparationPushCarry: 1
|
||||||
|
_enemySeparationMinPushRetain: 1
|
||||||
|
_targetSelectionCellSize: 2
|
||||||
--- !u!1 &1852670052
|
--- !u!1 &1852670052
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ namespace Simulation.Tests.Editor
|
||||||
private static readonly MethodInfo TickMethod =
|
private static readonly MethodInfo TickMethod =
|
||||||
SimulationWorldType?.GetMethod("Tick", PublicInstance);
|
SimulationWorldType?.GetMethod("Tick", PublicInstance);
|
||||||
|
|
||||||
|
private static readonly MethodInfo TryGetNearestEnemyEntityIdMethod =
|
||||||
|
SimulationWorldType?.GetMethod("TryGetNearestEnemyEntityId", PublicInstance);
|
||||||
|
|
||||||
private static readonly MethodInfo SetUseSimulationMovementMethod =
|
private static readonly MethodInfo SetUseSimulationMovementMethod =
|
||||||
SimulationWorldType?.GetMethod("SetUseSimulationMovement", PublicInstance);
|
SimulationWorldType?.GetMethod("SetUseSimulationMovement", PublicInstance);
|
||||||
|
|
||||||
|
|
@ -67,6 +70,7 @@ namespace Simulation.Tests.Editor
|
||||||
Assert.NotNull(RemoveEnemyByEntityIdMethod, "RemoveEnemyByEntityId reflection lookup failed.");
|
Assert.NotNull(RemoveEnemyByEntityIdMethod, "RemoveEnemyByEntityId reflection lookup failed.");
|
||||||
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
||||||
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
||||||
|
Assert.NotNull(TryGetNearestEnemyEntityIdMethod, "TryGetNearestEnemyEntityId reflection lookup failed.");
|
||||||
Assert.NotNull(SetUseSimulationMovementMethod, "SetUseSimulationMovement reflection lookup failed.");
|
Assert.NotNull(SetUseSimulationMovementMethod, "SetUseSimulationMovement reflection lookup failed.");
|
||||||
Assert.NotNull(SetUseJobSimulationMethod, "SetUseJobSimulation reflection lookup failed.");
|
Assert.NotNull(SetUseJobSimulationMethod, "SetUseJobSimulation reflection lookup failed.");
|
||||||
Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
|
Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
|
||||||
|
|
@ -164,7 +168,70 @@ namespace Simulation.Tests.Editor
|
||||||
Assert.That(forward.x, Is.EqualTo(1f).Within(0.0001f));
|
Assert.That(forward.x, Is.EqualTo(1f).Within(0.0001f));
|
||||||
}
|
}
|
||||||
|
|
||||||
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange)
|
[Test]
|
||||||
|
public void TryGetNearestEnemyEntityId_SelectsNearestBucketCandidate_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 1201, position: new Vector3(1f, 0f, 0f), speed: 0f, attackRange: 1f));
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 1202, position: new Vector3(6f, 0f, 0f), speed: 0f, attackRange: 1f));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
object[] parameters = { Vector3.zero, 100f, 0 };
|
||||||
|
bool found = (bool)TryGetNearestEnemyEntityIdMethod.Invoke(_worldComponent, parameters);
|
||||||
|
int nearestEntityId = (int)parameters[2];
|
||||||
|
|
||||||
|
Assert.IsTrue(found);
|
||||||
|
Assert.That(nearestEntityId, Is.EqualTo(1201));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TickEnemies_SeparatesOverlappedEnemies_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 1301, position: new Vector3(0f, 0f, 0f), speed: 1f, attackRange: 0.1f,
|
||||||
|
avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 2));
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 1302, position: new Vector3(0.1f, 0f, 0f), speed: 1f, attackRange: 0.1f,
|
||||||
|
avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 2));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.1f, realDeltaTime: 0.1f, playerPosition: new Vector3(10f, 0f, 0f));
|
||||||
|
|
||||||
|
object enemyA = GetEnemyAt(0);
|
||||||
|
object enemyB = GetEnemyAt(1);
|
||||||
|
Vector3 posA = (Vector3)GetField(enemyA, "Position");
|
||||||
|
Vector3 posB = (Vector3)GetField(enemyB, "Position");
|
||||||
|
posA.y = 0f;
|
||||||
|
posB.y = 0f;
|
||||||
|
float distance = Vector3.Distance(posA, posB);
|
||||||
|
Assert.That(distance, Is.GreaterThanOrEqualTo(0.89f));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TickEnemies_SeparatesOverlappedEnemies_WhenPlayerIsStaticAndInRange()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 1311, position: new Vector3(0f, 0f, 0f), speed: 1f, attackRange: 10f,
|
||||||
|
avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 3));
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 1312, position: new Vector3(0.05f, 0f, 0f), speed: 1f, attackRange: 10f,
|
||||||
|
avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 3));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.1f, realDeltaTime: 0.1f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
object enemyA = GetEnemyAt(0);
|
||||||
|
object enemyB = GetEnemyAt(1);
|
||||||
|
Assert.That((int)GetField(enemyA, "State"), Is.EqualTo(2));
|
||||||
|
Assert.That((int)GetField(enemyB, "State"), Is.EqualTo(2));
|
||||||
|
|
||||||
|
Vector3 posA = (Vector3)GetField(enemyA, "Position");
|
||||||
|
Vector3 posB = (Vector3)GetField(enemyB, "Position");
|
||||||
|
posA.y = 0f;
|
||||||
|
posB.y = 0f;
|
||||||
|
float distance = Vector3.Distance(posA, posB);
|
||||||
|
Assert.That(distance, Is.GreaterThanOrEqualTo(0.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange,
|
||||||
|
bool avoidEnemyOverlap = false, float enemyBodyRadius = 0.45f, int separationIterations = 1)
|
||||||
{
|
{
|
||||||
object enemy = System.Activator.CreateInstance(EnemySimDataType);
|
object enemy = System.Activator.CreateInstance(EnemySimDataType);
|
||||||
SetField(ref enemy, "EntityId", entityId);
|
SetField(ref enemy, "EntityId", entityId);
|
||||||
|
|
@ -173,9 +240,9 @@ namespace Simulation.Tests.Editor
|
||||||
SetField(ref enemy, "Rotation", Quaternion.identity);
|
SetField(ref enemy, "Rotation", Quaternion.identity);
|
||||||
SetField(ref enemy, "Speed", speed);
|
SetField(ref enemy, "Speed", speed);
|
||||||
SetField(ref enemy, "AttackRange", attackRange);
|
SetField(ref enemy, "AttackRange", attackRange);
|
||||||
SetField(ref enemy, "AvoidEnemyOverlap", false);
|
SetField(ref enemy, "AvoidEnemyOverlap", avoidEnemyOverlap);
|
||||||
SetField(ref enemy, "EnemyBodyRadius", 0.45f);
|
SetField(ref enemy, "EnemyBodyRadius", enemyBodyRadius);
|
||||||
SetField(ref enemy, "SeparationIterations", 1);
|
SetField(ref enemy, "SeparationIterations", separationIterations);
|
||||||
SetField(ref enemy, "TargetType", 0);
|
SetField(ref enemy, "TargetType", 0);
|
||||||
SetField(ref enemy, "State", 0);
|
SetField(ref enemy, "State", 0);
|
||||||
return enemy;
|
return enemy;
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ namespace Simulation.Tests.PlayMode
|
||||||
private static readonly MethodInfo TickMethod =
|
private static readonly MethodInfo TickMethod =
|
||||||
SimulationWorldType?.GetMethod("Tick", PublicInstance);
|
SimulationWorldType?.GetMethod("Tick", PublicInstance);
|
||||||
|
|
||||||
|
private static readonly MethodInfo TryGetNearestEnemyEntityIdMethod =
|
||||||
|
SimulationWorldType?.GetMethod("TryGetNearestEnemyEntityId", PublicInstance);
|
||||||
|
|
||||||
private static readonly MethodInfo SetUseSimulationMovementMethod =
|
private static readonly MethodInfo SetUseSimulationMovementMethod =
|
||||||
SimulationWorldType?.GetMethod("SetUseSimulationMovement", PublicInstance);
|
SimulationWorldType?.GetMethod("SetUseSimulationMovement", PublicInstance);
|
||||||
|
|
||||||
|
|
@ -69,6 +72,7 @@ namespace Simulation.Tests.PlayMode
|
||||||
Assert.NotNull(RemoveEnemyByEntityIdMethod, "RemoveEnemyByEntityId reflection lookup failed.");
|
Assert.NotNull(RemoveEnemyByEntityIdMethod, "RemoveEnemyByEntityId reflection lookup failed.");
|
||||||
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
||||||
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
||||||
|
Assert.NotNull(TryGetNearestEnemyEntityIdMethod, "TryGetNearestEnemyEntityId reflection lookup failed.");
|
||||||
Assert.NotNull(SetUseSimulationMovementMethod, "SetUseSimulationMovement reflection lookup failed.");
|
Assert.NotNull(SetUseSimulationMovementMethod, "SetUseSimulationMovement reflection lookup failed.");
|
||||||
Assert.NotNull(SetUseJobSimulationMethod, "SetUseJobSimulation reflection lookup failed.");
|
Assert.NotNull(SetUseJobSimulationMethod, "SetUseJobSimulation reflection lookup failed.");
|
||||||
Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
|
Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
|
||||||
|
|
@ -177,7 +181,73 @@ namespace Simulation.Tests.PlayMode
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange)
|
[UnityTest]
|
||||||
|
public IEnumerator TryGetNearestEnemyEntityId_SelectsNearestBucketCandidate_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 3301, position: new Vector3(1f, 0f, 0f), speed: 0f, attackRange: 1f));
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 3302, position: new Vector3(6f, 0f, 0f), speed: 0f, attackRange: 1f));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
object[] parameters = { Vector3.zero, 100f, 0 };
|
||||||
|
bool found = (bool)TryGetNearestEnemyEntityIdMethod.Invoke(_worldComponent, parameters);
|
||||||
|
int nearestEntityId = (int)parameters[2];
|
||||||
|
|
||||||
|
Assert.IsTrue(found);
|
||||||
|
Assert.That(nearestEntityId, Is.EqualTo(3301));
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator TickEnemies_SeparatesOverlappedEnemies_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 3401, position: new Vector3(0f, 0f, 0f), speed: 1f, attackRange: 0.1f,
|
||||||
|
avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 2));
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 3402, position: new Vector3(0.1f, 0f, 0f), speed: 1f, attackRange: 0.1f,
|
||||||
|
avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 2));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.1f, realDeltaTime: 0.1f, playerPosition: new Vector3(10f, 0f, 0f));
|
||||||
|
|
||||||
|
object enemyA = GetEnemyAt(0);
|
||||||
|
object enemyB = GetEnemyAt(1);
|
||||||
|
Vector3 posA = (Vector3)GetField(enemyA, "Position");
|
||||||
|
Vector3 posB = (Vector3)GetField(enemyB, "Position");
|
||||||
|
posA.y = 0f;
|
||||||
|
posB.y = 0f;
|
||||||
|
float distance = Vector3.Distance(posA, posB);
|
||||||
|
Assert.That(distance, Is.GreaterThanOrEqualTo(0.89f));
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator TickEnemies_SeparatesOverlappedEnemies_WhenPlayerIsStaticAndInRange()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 3411, position: new Vector3(0f, 0f, 0f), speed: 1f, attackRange: 10f,
|
||||||
|
avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 3));
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 3412, position: new Vector3(0.05f, 0f, 0f), speed: 1f, attackRange: 10f,
|
||||||
|
avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 3));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.1f, realDeltaTime: 0.1f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
object enemyA = GetEnemyAt(0);
|
||||||
|
object enemyB = GetEnemyAt(1);
|
||||||
|
Assert.That((int)GetField(enemyA, "State"), Is.EqualTo(2));
|
||||||
|
Assert.That((int)GetField(enemyB, "State"), Is.EqualTo(2));
|
||||||
|
|
||||||
|
Vector3 posA = (Vector3)GetField(enemyA, "Position");
|
||||||
|
Vector3 posB = (Vector3)GetField(enemyB, "Position");
|
||||||
|
posA.y = 0f;
|
||||||
|
posB.y = 0f;
|
||||||
|
float distance = Vector3.Distance(posA, posB);
|
||||||
|
Assert.That(distance, Is.GreaterThanOrEqualTo(0.5f));
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange,
|
||||||
|
bool avoidEnemyOverlap = false, float enemyBodyRadius = 0.45f, int separationIterations = 1)
|
||||||
{
|
{
|
||||||
object enemy = System.Activator.CreateInstance(EnemySimDataType);
|
object enemy = System.Activator.CreateInstance(EnemySimDataType);
|
||||||
SetField(ref enemy, "EntityId", entityId);
|
SetField(ref enemy, "EntityId", entityId);
|
||||||
|
|
@ -186,9 +256,9 @@ namespace Simulation.Tests.PlayMode
|
||||||
SetField(ref enemy, "Rotation", Quaternion.identity);
|
SetField(ref enemy, "Rotation", Quaternion.identity);
|
||||||
SetField(ref enemy, "Speed", speed);
|
SetField(ref enemy, "Speed", speed);
|
||||||
SetField(ref enemy, "AttackRange", attackRange);
|
SetField(ref enemy, "AttackRange", attackRange);
|
||||||
SetField(ref enemy, "AvoidEnemyOverlap", false);
|
SetField(ref enemy, "AvoidEnemyOverlap", avoidEnemyOverlap);
|
||||||
SetField(ref enemy, "EnemyBodyRadius", 0.45f);
|
SetField(ref enemy, "EnemyBodyRadius", enemyBodyRadius);
|
||||||
SetField(ref enemy, "SeparationIterations", 1);
|
SetField(ref enemy, "SeparationIterations", separationIterations);
|
||||||
SetField(ref enemy, "TargetType", 0);
|
SetField(ref enemy, "TargetType", 0);
|
||||||
SetField(ref enemy, "State", 0);
|
SetField(ref enemy, "State", 0);
|
||||||
return enemy;
|
return enemy;
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@
|
||||||
- 保留 A/B 路径:可切换 Job 与旧逻辑对比。
|
- 保留 A/B 路径:可切换 Job 与旧逻辑对比。
|
||||||
- 完成标准:开启 Job 后敌人追踪行为视觉一致;`TickEnemies` 主线程耗时明显下降。
|
- 完成标准:开启 Job 后敌人追踪行为视觉一致;`TickEnemies` 主线程耗时明显下降。
|
||||||
|
|
||||||
- [ ] Checkpoint 4:目标选择加速(空间哈希/网格分桶)
|
- [x] Checkpoint 4:目标选择加速(空间哈希/网格分桶)
|
||||||
- 建立敌人/目标的空间索引容器(建议 `NativeParallelMultiHashMap` 或等价结构)。
|
- 建立敌人/目标的空间索引容器(建议 `NativeParallelMultiHashMap` 或等价结构)。
|
||||||
- 拆分为两个阶段:
|
- 拆分为两个阶段:
|
||||||
- 构建分桶(Build Buckets)
|
- 构建分桶(Build Buckets)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue