Checkpoint 8
1. 统一调度收口为单点 Complete 2. 移除 Job 内部 Complete(),改为返回 JobHandle 3. 修复调度后 NativeList 安全冲突(关键) - 将 PrepareEnemyJobOutputBuffer/PrepareProjectileJobOutputBuffer/PrepareEnemySeparationJobBuffers 前置到 BuildInput - 互斥候选统计改为读取 _enemyJobInputs,不再在 Complete 前读取 _enemyJobOutputs 4. 新增 CP8 profiler markers 5. 新增回归用例(验证使用最新敌人移动结果构建碰撞候选)
This commit is contained in:
parent
e6d722ee1d
commit
1a45b513f2
|
|
@ -8,6 +8,9 @@ namespace CustomDebugger
|
|||
public static readonly ProfilerMarker TickEnemies_BuildInput = new ProfilerMarker("TickEnemies.BuildInput");
|
||||
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_Schedule = new ProfilerMarker("TickEnemies.Schedule");
|
||||
public static readonly ProfilerMarker TickEnemies_Complete = new ProfilerMarker("TickEnemies.Complete");
|
||||
public static readonly ProfilerMarker TickEnemies_MainThreadCommit = new ProfilerMarker("TickEnemies.MainThreadCommit");
|
||||
public static readonly ProfilerMarker TickEnemies_WriteBack = new ProfilerMarker("TickEnemies.WriteBack");
|
||||
public static readonly ProfilerMarker Collision_BuildQueries = new("Collision.BuildQueries");
|
||||
public static readonly ProfilerMarker Collision_BuildBuckets = new("Collision.BuildBuckets");
|
||||
|
|
|
|||
|
|
@ -133,9 +133,45 @@ namespace Simulation
|
|||
return;
|
||||
}
|
||||
|
||||
JobHandle enemyMovementHandle = default;
|
||||
JobHandle projectileMovementHandle = default;
|
||||
JobHandle enemySeparationHandle = default;
|
||||
bool hasEnemySeparationJob = false;
|
||||
bool hasEnemySeparationCandidates = false;
|
||||
int enemySeparationCount = 0;
|
||||
float enemySeparationMaxRadius = 0.45f;
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
|
||||
{
|
||||
SyncSimulationToJobInput();
|
||||
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 = GetPendingAreaCollisionQueryCount();
|
||||
int queryCount = projectileQueryCount + areaQueryCount;
|
||||
|
|
@ -148,48 +184,71 @@ namespace Simulation
|
|||
|
||||
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
|
||||
{
|
||||
ExecuteEnemyMovementJob(in context);
|
||||
ExecuteProjectileMovementJob(in context);
|
||||
enemyMovementHandle = ExecuteEnemyMovementJob(in context);
|
||||
projectileMovementHandle = ExecuteProjectileMovementJob(in context);
|
||||
}
|
||||
|
||||
JobHandle simulationHandle;
|
||||
using (CustomProfilerMarker.TickEnemies_Schedule.Auto())
|
||||
{
|
||||
hasEnemySeparationJob = TryScheduleEnemySeparationForJobOutput(
|
||||
in context,
|
||||
enemyMovementHandle,
|
||||
hasEnemySeparationCandidates,
|
||||
enemySeparationCount,
|
||||
enemySeparationMaxRadius,
|
||||
out enemySeparationHandle);
|
||||
JobHandle enemyHandle = hasEnemySeparationJob ? enemySeparationHandle : enemyMovementHandle;
|
||||
simulationHandle = JobHandle.CombineDependencies(enemyHandle, projectileMovementHandle);
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_Complete.Auto())
|
||||
{
|
||||
simulationHandle.Complete();
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_MoveSeparation.Auto())
|
||||
{
|
||||
ApplyEnemySeparationForJobOutput(in context);
|
||||
if (hasEnemySeparationJob)
|
||||
{
|
||||
CommitEnemySeparationForJobOutput(enemySeparationCount);
|
||||
}
|
||||
|
||||
BuildProjectileCollisionCandidates();
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
|
||||
{
|
||||
using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto())
|
||||
{
|
||||
ApplyJobOutputToSimulation();
|
||||
ResolveProjectileCollisionCandidatesMainThread();
|
||||
RecycleInactiveProjectiles();
|
||||
}
|
||||
}
|
||||
|
||||
MarkEnemyTargetSpatialIndexDirty();
|
||||
BuildEnemyTargetSpatialIndexIfNeeded();
|
||||
}
|
||||
|
||||
private void ExecuteEnemyMovementJob(in SimulationTickContext context)
|
||||
private JobHandle ExecuteEnemyMovementJob(in SimulationTickContext context)
|
||||
{
|
||||
int enemyCount = _enemyJobInputs.Length;
|
||||
PrepareEnemyJobOutputBuffer(enemyCount);
|
||||
|
||||
if (enemyCount == 0)
|
||||
{
|
||||
return;
|
||||
return default;
|
||||
}
|
||||
|
||||
if (context.DeltaTime <= 0f)
|
||||
{
|
||||
CopyEnemyInputToOutput();
|
||||
return;
|
||||
return default;
|
||||
}
|
||||
|
||||
float3 playerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z);
|
||||
NativeArray<EnemyJobInputData> inputArray = _enemyJobInputs.AsArray();
|
||||
NativeArray<EnemyJobOutputData> outputArray = _enemyJobOutputs.AsArray();
|
||||
|
||||
JobHandle handle;
|
||||
if (_useBurstJobs)
|
||||
{
|
||||
EnemyMovementBurstJob burstJob = new EnemyMovementBurstJob
|
||||
|
|
@ -199,10 +258,9 @@ namespace Simulation
|
|||
DeltaTime = context.DeltaTime,
|
||||
PlayerPosition = playerPosition
|
||||
};
|
||||
handle = burstJob.Schedule(enemyCount, 64);
|
||||
return burstJob.Schedule(enemyCount, 64);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
EnemyMovementJob job = new EnemyMovementJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
|
|
@ -210,10 +268,7 @@ namespace Simulation
|
|||
DeltaTime = context.DeltaTime,
|
||||
PlayerPosition = playerPosition
|
||||
};
|
||||
handle = job.Schedule(enemyCount, 64);
|
||||
}
|
||||
|
||||
handle.Complete();
|
||||
return job.Schedule(enemyCount, 64);
|
||||
}
|
||||
|
||||
private void CopyEnemyInputToOutput()
|
||||
|
|
@ -238,42 +293,18 @@ namespace Simulation
|
|||
}
|
||||
}
|
||||
|
||||
private void ApplyEnemySeparationForJobOutput(in SimulationTickContext context)
|
||||
private bool TryScheduleEnemySeparationForJobOutput(in SimulationTickContext context, JobHandle dependency,
|
||||
bool hasSeparationCandidates, int enemyCount, float maxRadius, out JobHandle separationHandle)
|
||||
{
|
||||
int enemyCount = _enemyJobOutputs.Length;
|
||||
if (enemyCount == 0)
|
||||
separationHandle = dependency;
|
||||
if (enemyCount <= 0 || !hasSeparationCandidates)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasSeparationCandidates = false;
|
||||
float maxRadius = 0.45f;
|
||||
for (int i = 0; i < enemyCount; i++)
|
||||
{
|
||||
EnemyJobOutputData output = _enemyJobOutputs[i];
|
||||
if (!output.AvoidEnemyOverlap)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasSeparationCandidates = true;
|
||||
float radius = output.EnemyBodyRadius > 0f ? output.EnemyBodyRadius : 0.45f;
|
||||
if (radius > maxRadius)
|
||||
{
|
||||
maxRadius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSeparationCandidates)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
@ -284,8 +315,6 @@ namespace Simulation
|
|||
NativeArray<EnemyJobOutputData> separatedOutputArray = _enemyJobSeparationOutputs.AsArray();
|
||||
NativeArray<float2> previousPushes = _enemySeparationPreviousPushes.AsArray();
|
||||
NativeArray<float2> currentPushes = _enemySeparationCurrentPushes.AsArray();
|
||||
JobHandle buildHandle;
|
||||
|
||||
if (_useBurstJobs)
|
||||
{
|
||||
BuildEnemySeparationBucketsBurstJob buildJob = new BuildEnemySeparationBucketsBurstJob
|
||||
|
|
@ -294,22 +323,7 @@ namespace Simulation
|
|||
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)
|
||||
{
|
||||
JobHandle buildHandle = buildJob.Schedule(enemyCount, 64, dependency);
|
||||
EnemySeparationBurstJob separationJob = new EnemySeparationBurstJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
|
|
@ -326,10 +340,17 @@ namespace Simulation
|
|||
PushSmoothing = pushSmoothing
|
||||
};
|
||||
separationHandle = separationJob.Schedule(enemyCount, 64, buildHandle);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
|
||||
BuildEnemySeparationBucketsJob nonBurstBuildJob = new BuildEnemySeparationBucketsJob
|
||||
{
|
||||
EnemySeparationJob separationJob = new EnemySeparationJob
|
||||
Inputs = inputArray,
|
||||
Buckets = _enemySeparationBuckets.AsParallelWriter(),
|
||||
CellSize = cellSize
|
||||
};
|
||||
JobHandle nonBurstBuildHandle = nonBurstBuildJob.Schedule(enemyCount, 64, dependency);
|
||||
EnemySeparationJob nonBurstSeparationJob = new EnemySeparationJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
Buckets = _enemySeparationBuckets,
|
||||
|
|
@ -344,10 +365,17 @@ namespace Simulation
|
|||
UseTangentialInAttackRange = useTangentialInAttackRange,
|
||||
PushSmoothing = pushSmoothing
|
||||
};
|
||||
separationHandle = separationJob.Schedule(enemyCount, 64, buildHandle);
|
||||
separationHandle = nonBurstSeparationJob.Schedule(enemyCount, 64, nonBurstBuildHandle);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CommitEnemySeparationForJobOutput(int enemyCount)
|
||||
{
|
||||
if (enemyCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
separationHandle.Complete();
|
||||
CommitEnemySeparationTemporalBuffers(enemyCount);
|
||||
for (int i = 0; i < enemyCount; i++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -148,20 +148,18 @@ namespace Simulation
|
|||
return expectedCount;
|
||||
}
|
||||
|
||||
private void ExecuteProjectileMovementJob(in SimulationTickContext context)
|
||||
private JobHandle ExecuteProjectileMovementJob(in SimulationTickContext context)
|
||||
{
|
||||
int projectileCount = _projectileJobInputs.Length;
|
||||
PrepareProjectileJobOutputBuffer(projectileCount);
|
||||
|
||||
if (projectileCount == 0)
|
||||
{
|
||||
return;
|
||||
return default;
|
||||
}
|
||||
|
||||
if (context.DeltaTime <= 0f)
|
||||
{
|
||||
CopyProjectileInputToOutput();
|
||||
return;
|
||||
return default;
|
||||
}
|
||||
|
||||
float maxDistance = Mathf.Max(0f, _projectileMaxDistanceFromPlayer);
|
||||
|
|
@ -172,7 +170,6 @@ namespace Simulation
|
|||
NativeArray<ProjectileJobInputData> inputArray = _projectileJobInputs.AsArray();
|
||||
NativeArray<ProjectileJobOutputData> outputArray = _projectileJobOutputs.AsArray();
|
||||
|
||||
JobHandle handle;
|
||||
if (_useBurstJobs)
|
||||
{
|
||||
ProjectileMovementBurstJob burstJob = new ProjectileMovementBurstJob
|
||||
|
|
@ -184,10 +181,9 @@ namespace Simulation
|
|||
MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer,
|
||||
MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer
|
||||
};
|
||||
handle = burstJob.Schedule(projectileCount, 64);
|
||||
return burstJob.Schedule(projectileCount, 64);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
ProjectileMovementJob job = new ProjectileMovementJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
|
|
@ -197,10 +193,7 @@ namespace Simulation
|
|||
MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer,
|
||||
MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer
|
||||
};
|
||||
handle = job.Schedule(projectileCount, 64);
|
||||
}
|
||||
|
||||
handle.Complete();
|
||||
return job.Schedule(projectileCount, 64);
|
||||
}
|
||||
|
||||
private void BuildProjectileCollisionCandidates()
|
||||
|
|
|
|||
|
|
@ -535,6 +535,20 @@ namespace Simulation.Tests.Editor
|
|||
Assert.That(GetCollisionCandidateCount(), Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TickProjectiles_BuildsCollisionCandidates_WithLatestEnemyMovement_WhenJobSimulationEnabled()
|
||||
{
|
||||
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||
UpsertEnemy(CreateEnemy(entityId: 5211, position: new Vector3(2f, 0f, 0f), speed: 1f, attackRange: 0.1f));
|
||||
UpsertProjectile(CreateProjectile(entityId: 5212, position: new Vector3(1f, 0f, 0f),
|
||||
forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 2f, age: 0f, active: true,
|
||||
remainingLifetime: 2f, state: 0));
|
||||
|
||||
InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: Vector3.zero);
|
||||
|
||||
Assert.That(GetCollisionCandidateCount(), Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TickProjectiles_ExpiresAfterCollisionCandidateConsumed_WhenJobSimulationEnabled()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -565,6 +565,21 @@ namespace Simulation.Tests.PlayMode
|
|||
yield break;
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator TickProjectiles_BuildsCollisionCandidates_WithLatestEnemyMovement_WhenJobSimulationEnabled()
|
||||
{
|
||||
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||
UpsertEnemy(CreateEnemy(entityId: 5511, position: new Vector3(2f, 0f, 0f), speed: 1f, attackRange: 0.1f));
|
||||
UpsertProjectile(CreateProjectile(entityId: 5512, position: new Vector3(1f, 0f, 0f),
|
||||
forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 2f, age: 0f, active: true,
|
||||
remainingLifetime: 2f, state: 0));
|
||||
|
||||
InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: Vector3.zero);
|
||||
|
||||
Assert.That(GetCollisionCandidateCount(), Is.GreaterThan(0));
|
||||
yield break;
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator TickProjectiles_ExpiresAfterCollisionCandidateConsumed_WhenJobSimulationEnabled()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -156,13 +156,13 @@
|
|||
- 建立命中事件缓冲区,统一在主线程提交表现层事件。
|
||||
- 完成标准:命中结果与现有逻辑一致,候选数量与耗时显著下降。
|
||||
|
||||
- [ ] Checkpoint 7:Burst 策略落地与热路径约束
|
||||
- [x] Checkpoint 7:Burst 策略落地与热路径约束
|
||||
- 热路径 Job 全部添加 `[BurstCompile]`,并在 Burst Inspector 确认已生效。
|
||||
- 清理 Job 内不兼容写法:托管分配、虚调用、LINQ、异常路径热调用。
|
||||
- 数学计算统一迁移到 `Unity.Mathematics`。
|
||||
- 完成标准:核心 Job 均由 Burst 编译,且无安全检查错误/降级回 Mono 的关键路径。
|
||||
|
||||
- [ ] Checkpoint 8:主线程职责收口与调度稳定
|
||||
- [x] Checkpoint 8:主线程职责收口与调度稳定
|
||||
- 明确主线程只做:输入采样、状态切换、UI 同步、实体显隐、最终写回。
|
||||
- 统一 `Schedule -> Dependency Combine -> Complete` 位置,防止隐式同步抖动。
|
||||
- 清理战斗帧中不必要的主线程循环(尤其逐实体逻辑)。
|
||||
|
|
|
|||
Loading…
Reference in New Issue