diff --git a/Assets/GameMain/Scripts/Debugger/ProfilerMarker.cs b/Assets/GameMain/Scripts/Debugger/ProfilerMarker.cs index e537ce5..9c24e7b 100644 --- a/Assets/GameMain/Scripts/Debugger/ProfilerMarker.cs +++ b/Assets/GameMain/Scripts/Debugger/ProfilerMarker.cs @@ -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"); diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.EnemyJobs.cs b/Assets/GameMain/Scripts/Simulation/SimulationWorld.EnemyJobs.cs index 69b45bc..3711363 100644 --- a/Assets/GameMain/Scripts/Simulation/SimulationWorld.EnemyJobs.cs +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.EnemyJobs.cs @@ -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()) { - ApplyJobOutputToSimulation(); - ResolveProjectileCollisionCandidatesMainThread(); - RecycleInactiveProjectiles(); + 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 inputArray = _enemyJobInputs.AsArray(); NativeArray outputArray = _enemyJobOutputs.AsArray(); - JobHandle handle; if (_useBurstJobs) { EnemyMovementBurstJob burstJob = new EnemyMovementBurstJob @@ -199,21 +258,17 @@ namespace Simulation DeltaTime = context.DeltaTime, PlayerPosition = playerPosition }; - handle = burstJob.Schedule(enemyCount, 64); - } - else - { - EnemyMovementJob job = new EnemyMovementJob - { - Inputs = inputArray, - Outputs = outputArray, - DeltaTime = context.DeltaTime, - PlayerPosition = playerPosition - }; - handle = job.Schedule(enemyCount, 64); + return burstJob.Schedule(enemyCount, 64); } - handle.Complete(); + EnemyMovementJob job = new EnemyMovementJob + { + Inputs = inputArray, + Outputs = outputArray, + DeltaTime = context.DeltaTime, + PlayerPosition = playerPosition + }; + 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 separatedOutputArray = _enemyJobSeparationOutputs.AsArray(); NativeArray previousPushes = _enemySeparationPreviousPushes.AsArray(); NativeArray 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,28 +340,42 @@ namespace Simulation 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); + return true; + } + + BuildEnemySeparationBucketsJob nonBurstBuildJob = new BuildEnemySeparationBucketsJob + { + Inputs = inputArray, + Buckets = _enemySeparationBuckets.AsParallelWriter(), + CellSize = cellSize + }; + JobHandle nonBurstBuildHandle = nonBurstBuildJob.Schedule(enemyCount, 64, dependency); + EnemySeparationJob nonBurstSeparationJob = 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 = 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++) { diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.ProjectileJobs.cs b/Assets/GameMain/Scripts/Simulation/SimulationWorld.ProjectileJobs.cs index 7d66329..8ec1748 100644 --- a/Assets/GameMain/Scripts/Simulation/SimulationWorld.ProjectileJobs.cs +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.ProjectileJobs.cs @@ -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 inputArray = _projectileJobInputs.AsArray(); NativeArray outputArray = _projectileJobOutputs.AsArray(); - JobHandle handle; if (_useBurstJobs) { ProjectileMovementBurstJob burstJob = new ProjectileMovementBurstJob @@ -184,23 +181,19 @@ namespace Simulation MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer, MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer }; - handle = burstJob.Schedule(projectileCount, 64); - } - else - { - ProjectileMovementJob job = new ProjectileMovementJob - { - Inputs = inputArray, - Outputs = outputArray, - DeltaTime = context.DeltaTime, - PlayerPosition = playerPosition, - MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer, - MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer - }; - handle = job.Schedule(projectileCount, 64); + return burstJob.Schedule(projectileCount, 64); } - handle.Complete(); + ProjectileMovementJob job = new ProjectileMovementJob + { + Inputs = inputArray, + Outputs = outputArray, + DeltaTime = context.DeltaTime, + PlayerPosition = playerPosition, + MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer, + MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer + }; + return job.Schedule(projectileCount, 64); } private void BuildProjectileCollisionCandidates() diff --git a/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs b/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs index a959320..fb74bf3 100644 --- a/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs +++ b/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs @@ -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() { diff --git a/Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs b/Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs index 11c0fe2..ae8e221 100644 --- a/Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs +++ b/Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs @@ -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() { diff --git a/docs/TodoList.md b/docs/TodoList.md index 3c35b95..a6f46e4 100644 --- a/docs/TodoList.md +++ b/docs/TodoList.md @@ -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` 位置,防止隐式同步抖动。 - 清理战斗帧中不必要的主线程循环(尤其逐实体逻辑)。