327 lines
13 KiB
C#
327 lines
13 KiB
C#
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<Player>();
|
|
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;
|
|
}
|
|
}
|
|
}
|