Checkpoint 3 fix

修复将互斥算法由同步转成异步产生的抖动问题
This commit is contained in:
SepComet
2026-02-22 14:11:53 +08:00
parent 34b1424bb6
commit 5fb7ea499f
14 changed files with 983 additions and 44 deletions
@@ -1,5 +1,4 @@
using CustomDebugger;
using CustomUtility;
using CustomDebugger;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
@@ -10,6 +9,21 @@ namespace Simulation
{
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]
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)
{
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
@@ -51,7 +137,7 @@ namespace Simulation
using (CustomProfilerMarker.TickEnemies_MoveSeparation.Auto())
{
ApplyEnemySeparationForJobOutput();
ApplyEnemySeparationForJobOutput(in context);
}
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
@@ -59,6 +145,9 @@ namespace Simulation
SyncProjectilesToJobOutput();
ApplyJobOutputToSimulation();
}
MarkEnemyTargetSpatialIndexDirty();
BuildEnemyTargetSpatialIndexIfNeeded();
}
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;
}
_enemySeparationAgents.Clear();
for (int i = 0; i < _enemyJobOutputs.Length; i++)
bool hasSeparationCandidates = false;
float maxRadius = 0.45f;
for (int i = 0; i < enemyCount; i++)
{
EnemyJobOutputData output = _enemyJobOutputs[i];
if (!output.AvoidEnemyOverlap)
@@ -146,39 +237,295 @@ namespace Simulation
continue;
}
Vector3 position = output.Position;
position.y = 0f;
_enemySeparationAgents.Add(new EnemySeparationAgent
hasSeparationCandidates = true;
float radius = output.EnemyBodyRadius > 0f ? output.EnemyBodyRadius : 0.45f;
if (radius > maxRadius)
{
AgentId = output.EntityId,
Position = position,
Radius = output.EnemyBodyRadius > 0f ? output.EnemyBodyRadius : 0.45f
});
maxRadius = radius;
}
}
if (_enemySeparationAgents.Count == 0)
if (!hasSeparationCandidates)
{
return;
}
EnemySeparationSolverProvider.SetSimulationAgents(_enemySeparationAgents);
for (int i = 0; i < _enemyJobOutputs.Length; i++)
float autoCellSize = maxRadius * 2f;
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];
if (!output.AvoidEnemyOverlap || output.State != EnemyStateChasing)
BuildEnemySeparationBucketsBurstJob buildJob = new BuildEnemySeparationBucketsBurstJob
{
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;
}
Vector3 resolvedPosition = EnemySeparationSolverProvider.ResolveSimulation(
output.EntityId,
output.Position,
output.Forward,
output.SeparationIterations > 0 ? output.SeparationIterations : 1);
float3 resolvedPush = pushAccumulation * pushDamping;
output.Position = resolvedPosition;
_enemyJobOutputs[i] = output;
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 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,
@@ -197,7 +544,8 @@ namespace Simulation
float3 forward = new float3(input.Forward.x, input.Forward.y, input.Forward.z);
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)
{
@@ -239,4 +587,4 @@ namespace Simulation
};
}
}
}
}
@@ -1,5 +1,6 @@
using System;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
namespace Simulation
@@ -60,8 +61,12 @@ namespace Simulation
private NativeList<EnemyJobInputData> _enemyJobInputs;
private NativeList<EnemyJobOutputData> _enemyJobOutputs;
private NativeList<EnemyJobOutputData> _enemyJobSeparationOutputs;
private NativeList<float2> _enemySeparationPreviousPushes;
private NativeList<float2> _enemySeparationCurrentPushes;
private NativeList<ProjectileJobInputData> _projectileJobInputs;
private NativeList<ProjectileJobOutputData> _projectileJobOutputs;
private NativeParallelMultiHashMap<long, int> _enemySeparationBuckets;
private void InitializeJobDataChannels()
{
@@ -73,8 +78,13 @@ namespace Simulation
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);
_enemySeparationBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
InitializeEnemyTargetSpatialIndex();
}
private void DisposeJobDataChannels()
@@ -91,6 +101,24 @@ namespace Simulation
}
_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();
@@ -102,6 +130,14 @@ namespace Simulation
_projectileJobOutputs.Dispose();
}
_projectileJobOutputs = default;
if (_enemySeparationBuckets.IsCreated)
{
_enemySeparationBuckets.Dispose();
}
_enemySeparationBuckets = default;
DisposeEnemyTargetSpatialIndex();
}
private void ClearJobDataChannels()
@@ -130,6 +166,28 @@ namespace Simulation
{
_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()
@@ -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()
{
int enemyCount = Mathf.Min(_enemies.Count, _enemyJobOutputs.Length);
@@ -215,8 +353,12 @@ namespace Simulation
{
return IsNativeListUsable(_enemyJobInputs) &&
IsNativeListUsable(_enemyJobOutputs) &&
IsNativeListUsable(_enemyJobSeparationOutputs) &&
IsNativeListUsable(_enemySeparationPreviousPushes) &&
IsNativeListUsable(_enemySeparationCurrentPushes) &&
IsNativeListUsable(_projectileJobInputs) &&
IsNativeListUsable(_projectileJobOutputs);
IsNativeListUsable(_projectileJobOutputs) &&
IsNativeMultiHashMapUsable(_enemySeparationBuckets);
}
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)
{
return new EnemyJobInputData
@@ -345,5 +505,18 @@ namespace Simulation
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:
@@ -15,7 +15,7 @@ namespace Simulation
private const int EnemyStateIdle = 0;
private const int EnemyStateChasing = 1;
private const int EnemyStateInAttackRange = 2;
private struct EnemyTickWorkItem
{
public int EntityId;
@@ -34,9 +34,14 @@ namespace Simulation
public int NextState;
}
[SerializeField] private bool _useSimulationMovement;
[SerializeField] private bool _useJobSimulation;
[SerializeField] private bool _useBurstJobs = true;
[Header("模拟世界全局设置")] [Tooltip("是否启用世界模拟")] [SerializeField]
private bool _useSimulationMovement;
[Tooltip("是否启用 Job 运算路径")] [SerializeField]
private bool _useJobSimulation;
[Tooltip("是否使用 Burst 来完成计算")] [SerializeField]
private bool _useBurstJobs = true;
private EntitySync _entitySync;
private Presentation _presentation;
@@ -104,6 +109,8 @@ namespace Simulation
int simulationIndex = _enemies.Count;
_enemies.Add(simData);
EnemyBinding.Bind(simData.EntityId, simulationIndex);
OnEnemyAddedToSeparationTemporalBuffers();
MarkEnemyTargetSpatialIndexDirty();
return simulationIndex;
}
@@ -115,6 +122,7 @@ namespace Simulation
}
_enemies[simulationIndex] = simData;
MarkEnemyTargetSpatialIndexDirty();
return simulationIndex;
}
@@ -134,7 +142,9 @@ namespace Simulation
}
_enemies.RemoveAt(lastIndex);
OnEnemyRemovedFromSeparationTemporalBuffers(simulationIndex);
EnemyBinding.UnbindByEntityId(entityId);
MarkEnemyTargetSpatialIndexDirty();
return true;
}
@@ -288,6 +298,7 @@ namespace Simulation
{
TickEnemiesJobified(in context);
}
return;
}
@@ -455,6 +466,8 @@ namespace Simulation
private void WriteBackEnemyTickResults()
{
bool hasPositionChanged = false;
for (int i = 0; i < _enemyTickWorkItems.Count; i++)
{
EnemySimData enemy = _enemies[i];
@@ -468,11 +481,18 @@ namespace Simulation
{
enemy.Rotation = workItem.Rotation;
}
hasPositionChanged = true;
}
enemy.State = workItem.NextState;
_enemies[i] = enemy;
}
if (hasPositionChanged)
{
MarkEnemyTargetSpatialIndexDirty();
}
}
private static EnemySimData CreateEnemyInitialSimData(EnemyBase enemy, EnemyData enemyData)