using SepCore.Debugger; using SepCore.Entity; using Unity.Jobs; using Unity.Mathematics; using UnityEngine; namespace SepCore.Simulation { public sealed partial class SimulationWorld { // Orchestrates the per-tick simulation pipeline over the component systems. private float _playerAbsorbRange = -1f; private float _playerAbsorbSpeed = -1f; private float _playerCollectDistance = -1f; public void SyncPlayerAbsorbSettings(float absorbRange, float absorbSpeed, float collectDistance) { _playerAbsorbRange = absorbRange; _playerAbsorbSpeed = absorbSpeed; _playerCollectDistance = collectDistance; } private float ResolveAbsorbRange() { if (_playerAbsorbRange >= 0f) return _playerAbsorbRange; return GameEntry.Luban != null ? GameEntry.Luban.Global.DefaultAbsorbRange : 2f; } private float ResolveAbsorbSpeed() { if (_playerAbsorbSpeed >= 0f) return _playerAbsorbSpeed; return GameEntry.Luban != null ? GameEntry.Luban.Global.AbsorbSpeed : 8f; } private float ResolveCollectDistance() { if (_playerCollectDistance >= 0f) return _playerCollectDistance; return GameEntry.Luban != null ? GameEntry.Luban.Global.CollectDistance : 0.5f; } [Header("敌人互斥参数")] [Tooltip("敌人互斥分桶使用的网格尺寸。小于等于 0 时,将根据敌人体积半径自动计算。")] [SerializeField] private float _enemySeparationCellSize = 0f; [Tooltip("每次迭代对互斥推力累积值的阻尼系数。数值越大,分离速度越快。")] [SerializeField] private float _enemySeparationPushDamping = 0.75f; [Tooltip("每次迭代允许的最大互斥位移步长(按敌人体积半径倍率计算)。")] [SerializeField] private float _enemySeparationMaxStepScale = 1f; [Tooltip("敌人进入攻击范围后,互斥位移是否保持为相对玩家方向的切向分量(避免被径向推离玩家)。")] [SerializeField] private bool _enemySeparationUseTangentialInAttackRange = true; [Tooltip("互斥推力方向突变时的时间平滑系数。越大越稳定,但响应越慢。")] [SerializeField] private float _enemySeparationPushSmoothing = 0.55f; private void TickSimulationPipeline(in SimulationTickContext context) { // 1. 早退分支:deltaTime <= 0 时只清理碰撞通道和统计,然后返回。 if (context.DeltaTime <= 0f) { PrepareCollisionQueryAndCandidateChannels(0, 0, 0); ResetCollisionRuntimeStats(); ClearAreaCollisionTransientBuffers(); return; } JobHandle enemyMovementHandle = default; JobHandle projectileMovementHandle = default; JobHandle enemySeparationHandle = default; bool hasEnemySeparationJob = false; bool hasEnemySeparationCandidates = false; bool hasMovingEnemy = false; int enemyCount = 0; float enemySeparationMaxRadius = 0.45f; // 2. BuildInput 阶段: // 扫描敌人组件统计是否需要分离系统以及碰撞查询容量。 using (CustomProfilerMarker.TickEnemies_BuildInput.Auto()) { enemyCount = _enemies.Count; for (int i = 0; i < enemyCount; i++) { if (_enemies.MoveInput[i].Speed > 0f) { hasMovingEnemy = true; } EnemyMoveConfigSim config = _enemies.MoveConfig[i]; if (!config.AvoidOverlap) { continue; } hasEnemySeparationCandidates = true; float radius = config.BodyRadius > 0f ? config.BodyRadius : 0.45f; if (radius > enemySeparationMaxRadius) { enemySeparationMaxRadius = radius; } } if (hasEnemySeparationCandidates) { int separationBucketCapacity = Mathf.Max(128, enemyCount * 2); PrepareEnemySeparationJobBuffers(enemyCount, separationBucketCapacity); } int projectileQueryCount = _projectiles.Count; int areaQueryCount = GetPendingAreaCollisionRequestCount(); int queryCount = projectileQueryCount + areaQueryCount; int projectileExpectedCount = projectileQueryCount * Mathf.Max(1, _projectileMaxCandidatesPerQuery); int areaExpectedCount = EstimatePendingAreaCollisionCandidateCountFromRequests(); int expectedCandidateCount = Mathf.Max(16, projectileExpectedCount + areaExpectedCount); int bucketCapacity = Mathf.Max(256, enemyCount * 2 + queryCount); PrepareCollisionQueryAndCandidateChannels(queryCount, expectedCandidateCount, bucketCapacity); int pickupCount = _pickups.Count; if (pickupCount > 0) { EnsureCapacity(ref _collectedPickups, pickupCount); _collectedPickups.Clear(); } } // 3. StateUpdate 阶段:敌人/投射物/掉落物移动系统 JobHandle pickupMovementHandle = default; using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto()) { float3 planarPlayerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z); float3 playerPosition = new float3(context.PlayerPosition.x, context.PlayerPosition.y, context.PlayerPosition.z); enemyMovementHandle = EnemyMovementSystem.Schedule(_enemies, context.DeltaTime, planarPlayerPosition, default); projectileMovementHandle = ProjectileMovementSystem.Schedule(_projectiles, context.DeltaTime, playerPosition, _projectileMaxDistanceFromPlayer, _projectileMaxVerticalOffsetFromPlayer, default); if (_pickups.Count > 0) { float absorbRange = ResolveAbsorbRange(); float absorbSpeed = ResolveAbsorbSpeed(); float collectDistance = ResolveCollectDistance(); pickupMovementHandle = PickupMovementSystem.Schedule(_pickups, _collectedPickups, context.DeltaTime, planarPlayerPosition, absorbRange, absorbSpeed, collectDistance, default); } } // 4. Schedule 阶段:可选敌人分离系统 JobHandle simulationHandle; using (CustomProfilerMarker.TickEnemies_Schedule.Auto()) { hasEnemySeparationJob = TryScheduleEnemySeparationJob( in context, enemyMovementHandle, hasEnemySeparationCandidates, enemyCount, enemySeparationMaxRadius, out enemySeparationHandle); JobHandle enemyHandle = hasEnemySeparationJob ? enemySeparationHandle : enemyMovementHandle; JobHandle movementHandle = JobHandle.CombineDependencies(enemyHandle, projectileMovementHandle); simulationHandle = JobHandle.CombineDependencies(movementHandle, pickupMovementHandle); } // 5. Complete 阶段:等待上述 Job 全部完成。 using (CustomProfilerMarker.TickEnemies_Complete.Auto()) { simulationHandle.Complete(); } // 6. 主线程提交阶段: // - 分离结果写回敌人 Transform 组件 // - 有位移时标记目标选择索引失效 using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto()) { if (hasEnemySeparationJob) { EnemySeparationSystem.Commit(_enemies, _enemySeparationTransforms, enemyCount); } if (hasMovingEnemy || hasEnemySeparationJob) { MarkEnemyTargetSpatialIndexDirty(); } } using (CustomProfilerMarker.Collision.Auto()) { PrepareCollisionCandidatesForFrame(); CompleteCollisionCandidatesForFrame(); } // 7. MainThreadCommit 阶段 using (CustomProfilerMarker.TickEnemies_WriteBack.Auto()) { using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto()) { ResolveCollisionCandidatesOnMainThread(); RecycleInactiveAndExpiredProjectiles(); ResolveCollectedPickupsOnMainThread(); } } } private void ResolveCollectedPickupsOnMainThread() { if (!_collectedPickups.IsCreated || _collectedPickups.Length == 0) { return; } Player player = TryGetPlayer(); for (int i = 0; i < _collectedPickups.Length; i++) { int entityId = _collectedPickups[i]; EntityBase entity = TryGetEntityById(entityId); if (entity == null || !entity.Available) { continue; } if (entity is CoinEntity coin) { if (player != null) { coin.TryCollect(player); } else { var entityComponent = GameEntry.Entity; if (entityComponent != null) { entityComponent.HideEntity(coin); } } } else if (entity is ExpEntity exp) { if (player != null) { exp.TryCollect(player); } else { var entityComponent = GameEntry.Entity; if (entityComponent != null) { entityComponent.HideEntity(exp); } } } else { var entityComponent = GameEntry.Entity; if (entityComponent != null) { entityComponent.HideEntity(entity); } } } _collectedPickups.Clear(); } private Player TryGetPlayer() { if (_playerMovementTransform != null) { Player player = _playerMovementTransform.GetComponent(); if (player != null && player.Available) { return player; } } var entityComponent = GameEntry.Entity; if (entityComponent != null && GameEntry.Luban != null) { var group = entityComponent.GetEntityGroup(GameEntry.Luban.Global.PlayerGroupName); if (group != null && group.EntityCount > 0) { var entities = group.GetAllEntities(); for (int i = 0; i < entities.Length; i++) { if (entities[i] is UnityGameFramework.Runtime.Entity ugfEntity && ugfEntity.Logic is Player player && player.Available) { return player; } } } } return null; } private bool TryScheduleEnemySeparationJob(in SimulationTickContext context, JobHandle dependency, bool hasSeparationCandidates, int enemyCount, float maxRadius, out JobHandle separationHandle) { if (enemyCount <= 0 || !hasSeparationCandidates) { separationHandle = dependency; return false; } float autoCellSize = maxRadius * 2f; float configuredCellSize = _enemySeparationCellSize > 0f ? _enemySeparationCellSize : autoCellSize; EnemySeparationSettings settings = new EnemySeparationSettings( Mathf.Max(0.1f, configuredCellSize), maxRadius, Mathf.Clamp(_enemySeparationPushDamping, 0f, 2f), Mathf.Max(0.1f, _enemySeparationMaxStepScale), _enemySeparationUseTangentialInAttackRange, Mathf.Clamp01(_enemySeparationPushSmoothing)); float3 playerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z); separationHandle = EnemySeparationSystem.Schedule(_enemies, _enemySeparationBuckets, _enemySeparationTransforms, in settings, playerPosition, dependency); return true; } } }