第一批重构,主要是物理迁移脚本和部分命名空间的调整

- 结构上迁移文件,将脚本分成 Base/Runtime/Presentation/Procedure/Editor 五大文件夹
- 调整 DataTable 和 Definition 下脚本的命名空间
- 依据 UI-5层架构设计规范 拆分原先的 UI 到 Runtime 和 Presentation 下
This commit is contained in:
SepComet
2026-06-02 19:12:34 +08:00
parent 56f74eee9a
commit 34c0784190
655 changed files with 1272 additions and 516 deletions
@@ -0,0 +1,242 @@
using CustomDebugger;
using Entity;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Collision Broad Phase
private void PrepareCollisionCandidatesForFrame()
{
_collisionCandidateQueryScheduled = false;
if (!_collisionQueryInputs.IsCreated || !_collisionCandidates.IsCreated ||
!_enemyCollisionBuckets.IsCreated)
{
ResetCollisionRuntimeStats();
ClearAreaCollisionTransientBuffers();
return;
}
int projectileCount = _projectileJobOutputs.Length;
int areaQueryCount = _areaCollisionRequests.Count;
if (projectileCount == 0 && areaQueryCount == 0)
{
ResetCollisionRuntimeStats();
return;
}
float queryRadius = Mathf.Max(0.01f, _projectileCollisionQueryRadius);
int maxCandidatesPerQuery = Mathf.Max(1, _projectileMaxCandidatesPerQuery);
float maxQueryRadius = queryRadius;
int queryId = 0;
int projectileQueryCount = 0;
int builtAreaQueryCount = 0;
using (CustomProfilerMarker.Collision_BuildQueries.Auto())
{
for (int i = 0; i < projectileCount; i++)
{
ProjectileJobOutputData projectile = _projectileJobOutputs[i];
if (!projectile.Active || projectile.State != ProjectileStateActive)
{
continue;
}
AddProjectileCollisionQuery(queryId, in projectile, queryRadius, maxCandidatesPerQuery);
queryId++;
projectileQueryCount++;
}
for (int i = 0; i < areaQueryCount; i++)
{
AreaCollisionRequestData request = _areaCollisionRequests[i];
AddAreaCollisionQuery(queryId, request.SourceEntityId, request.SourceOwnerEntityId,
request.SourceWasActiveAtQueryTime, in request.Center, request.Radius, request.MaxTargets,
request.ShapeType, in request.Direction, request.HalfAngleDeg, request.HalfWidth,
request.HalfLength);
queryId++;
builtAreaQueryCount++;
if (request.Radius > maxQueryRadius)
{
maxQueryRadius = request.Radius;
}
}
}
_lastProjectileCollisionQueryCount = projectileQueryCount;
_lastAreaCollisionQueryCount = builtAreaQueryCount;
_lastCollisionQueryCount = projectileQueryCount + builtAreaQueryCount;
_lastResolvedAreaHitCount = 0;
if (_collisionQueryInputs.Length == 0)
{
_lastCollisionCandidateCount = 0;
_lastProjectileCollisionCandidateCount = 0;
_lastAreaCollisionCandidateCount = 0;
_lastCollisionCellSize = 0f;
_lastCollisionHasEnemyTargets = _enemyJobOutputs.Length > 0;
return;
}
float autoCellSize = maxQueryRadius * 2f;
float configuredCellSize = _projectileCollisionCellSize > 0f ? _projectileCollisionCellSize : autoCellSize;
float cellSize = Mathf.Max(0.1f, configuredCellSize);
bool hasEnemyTargets = _enemyJobOutputs.Length > 0;
_lastCollisionCellSize = cellSize;
_lastCollisionHasEnemyTargets = hasEnemyTargets;
using (CustomProfilerMarker.Collision_BuildBuckets.Auto())
{
if (hasEnemyTargets)
{
BuildEnemyCollisionBuckets(cellSize);
}
}
using (CustomProfilerMarker.Collision_QueryCandidates.Auto())
{
_collisionCandidateQueryScheduled = ScheduleCollisionCandidateQueryJob(cellSize, hasEnemyTargets,
out _collisionCandidateQueryHandle);
}
}
private void CompleteCollisionCandidatesForFrame()
{
if (_collisionCandidateQueryScheduled)
{
_collisionCandidateQueryHandle.Complete();
_collisionCandidateQueryScheduled = false;
}
CountCollisionCandidatesBySourceType(out int projectileCandidateCount, out int areaCandidateCount);
_lastProjectileCollisionCandidateCount = projectileCandidateCount;
_lastAreaCollisionCandidateCount = areaCandidateCount;
_lastCollisionCandidateCount = projectileCandidateCount + areaCandidateCount;
}
private void BuildEnemyCollisionBuckets(float cellSize)
{
_enemyCollisionBuckets.Clear();
int enemyCount = _enemyJobOutputs.Length;
if (enemyCount <= 0)
{
return;
}
BuildCollisionBucketsBurstJob job = new BuildCollisionBucketsBurstJob
{
EnemyOutputs = _enemyJobOutputs.AsArray(),
Buckets = _enemyCollisionBuckets.AsParallelWriter(),
CellSize = cellSize
};
using (CustomProfilerMarker.TickEnemies_Complete.Auto())
{
JobHandle handle = job.Schedule(enemyCount, 64);
handle.Complete();
}
}
[BurstCompile]
private struct BuildCollisionBucketsBurstJob : IJobParallelFor
{
[ReadOnly] public NativeArray<EnemyJobOutputData> EnemyOutputs;
public NativeParallelMultiHashMap<long, int>.ParallelWriter Buckets;
public float CellSize;
public void Execute(int index)
{
EnemyJobOutputData enemy = EnemyOutputs[index];
int cellX = (int)math.floor(enemy.Position.x / CellSize);
int cellZ = (int)math.floor(enemy.Position.z / CellSize);
Buckets.Add(SeparationCellKey(cellX, cellZ), index);
}
}
private bool ScheduleCollisionCandidateQueryJob(float cellSize, bool hasEnemyTargets, out JobHandle handle)
{
handle = default;
if (!_collisionQueryInputs.IsCreated || !_collisionCandidates.IsCreated)
{
return false;
}
_collisionCandidates.Clear();
int queryCount = _collisionQueryInputs.Length;
if (queryCount == 0)
{
return false;
}
bool hasPlayerTarget = TryGetPlayerCollisionTarget(out int playerTargetEntityId, out float3 playerPosition);
QueryCollisionCandidatesBurstJob job = new QueryCollisionCandidatesBurstJob
{
Queries = _collisionQueryInputs.AsArray(),
EnemyBuckets = _enemyCollisionBuckets,
EnemyOutputs = _enemyJobOutputs.AsArray(),
Candidates = _collisionCandidates.AsParallelWriter(),
HasEnemyTargets = hasEnemyTargets,
HasPlayerTarget = hasPlayerTarget,
PlayerTargetEntityId = playerTargetEntityId,
PlayerPosition = playerPosition,
CellSize = cellSize
};
handle = job.Schedule(queryCount, 64);
return true;
}
private void CountCollisionCandidatesBySourceType(out int projectileCandidateCount, out int areaCandidateCount)
{
projectileCandidateCount = 0;
areaCandidateCount = 0;
if (!_collisionCandidates.IsCreated)
{
return;
}
for (int i = 0; i < _collisionCandidates.Length; i++)
{
CollisionCandidateData candidate = _collisionCandidates[i];
if (candidate.SourceType == CollisionSourceTypeProjectile)
{
projectileCandidateCount++;
}
else if (candidate.SourceType == CollisionSourceTypeArea)
{
areaCandidateCount++;
}
}
}
private static bool TryGetPlayerCollisionTarget(out int playerEntityId, out float3 playerPosition)
{
playerEntityId = PlayerEntityId;
playerPosition = default;
if (!TryGetAliveTargetableEntity(playerEntityId, out TargetableObject playerTarget))
{
return false;
}
Transform playerTransform = playerTarget.CachedTransform;
if (playerTransform == null)
{
return false;
}
Vector3 position = playerTransform.position;
playerPosition = new float3(position.x, position.y, position.z);
return true;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ac6aca660ff944e69ea8f5e06f0609cf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,10 @@
namespace Simulation
{
public sealed partial class SimulationWorld
{
// Shared collision pipeline configuration and runtime state.
// Request buffering, broad-phase scheduling, resolve, and presentation
// dispatch live in dedicated partial files under Jobs/.
private const int PlayerEntityId = -1;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 87bc5c5ec75920d46a65881e949bd2a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,115 @@
using CustomEvent;
using SepCore.Definition;
using Entity;
using Entity.Weapon;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Collision Presentation
private void DispatchProjectileHitPresentationEvent(int projectileEntityId, int sourceEntityId,
int sourceOwnerEntityId, int targetEntityId, int damage, in Vector3 hitPosition)
{
if (!_dispatchProjectileHitPresentationEvent)
{
return;
}
var eventComponent = GameEntry.Event;
if (eventComponent == null)
{
return;
}
eventComponent.Fire(this, ProjectileHitPresentationEventArgs.Create(
projectileEntityId,
sourceEntityId,
sourceOwnerEntityId,
targetEntityId,
damage,
hitPosition,
_dispatchProjectileHitMarkerEvent,
_dispatchProjectileHitEffectEvent,
_projectileHitPresentationEffectTypeId));
}
private static bool TryResolveImpactSource(EntityBase sourceEntity, EntityBase ownerEntity,
out EntityBase attacker, out ImpactData impactData)
{
if (TryResolveImpactFromEntity(sourceEntity, out impactData))
{
attacker = sourceEntity;
return true;
}
if (TryResolveImpactFromEntity(ownerEntity, out impactData))
{
attacker = ownerEntity;
return true;
}
attacker = null;
impactData = default;
return false;
}
private static bool TryResolveImpactFromEntity(EntityBase entity, out ImpactData impactData)
{
if (entity is WeaponBase weapon)
{
impactData = weapon.GetImpactData();
return true;
}
if (entity is EnemyProjectile enemyProjectile)
{
impactData = enemyProjectile.GetImpactData();
return true;
}
if (entity is TargetableObject targetableObject)
{
impactData = targetableObject.GetImpactData();
return true;
}
impactData = default;
return false;
}
private static bool TryGetAliveTargetableEntity(int entityId, out TargetableObject target)
{
target = null;
var enemyManager = GameEntry.EnemyManager;
if (enemyManager != null && enemyManager.TryGetEnemy(entityId, out EntityBase enemyEntity))
{
if (enemyEntity is TargetableObject enemyTarget && enemyTarget.Available && !enemyTarget.IsDead)
{
target = enemyTarget;
return true;
}
}
EntityBase entity = TryGetEntityById(entityId);
if (entity is TargetableObject targetable && targetable.Available && !targetable.IsDead)
{
target = targetable;
return true;
}
return false;
}
private static EntityBase TryGetEntityById(int entityId)
{
var entityComponent = GameEntry.Entity;
return entityComponent != null ? entityComponent.GetGameEntity(entityId) : null;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 882102e53dea40a2b5b596e1b41bbed6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,121 @@
using Entity;
using Entity.Weapon;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Collision Requests
public bool TryRequestAreaCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
float radius, int maxTargets = 16)
{
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
maxTargets, CollisionShapeCircle, Vector3.forward, 180f, 0f, 0f);
}
public bool TryRequestSectorCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
float radius, in Vector3 direction, float halfAngleDeg, int maxTargets = 16)
{
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
maxTargets, CollisionShapeSector, direction, halfAngleDeg, 0f, 0f);
}
public bool TryRequestRectangleCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
float halfWidth, float halfLength, in Vector3 direction, int maxTargets = 16)
{
float safeHalfWidth = Mathf.Max(0.01f, halfWidth);
float safeHalfLength = Mathf.Max(0.01f, halfLength);
float boundingRadius = Mathf.Sqrt(safeHalfWidth * safeHalfWidth + safeHalfLength * safeHalfLength);
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, boundingRadius,
maxTargets, CollisionShapeRectangle, direction, 0f, safeHalfWidth, safeHalfLength);
}
private bool TryRequestAreaCollisionInternal(int sourceEntityId, int sourceOwnerEntityId,
in Vector3 center, float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg,
float halfWidth, float halfLength)
{
if (sourceEntityId == 0 || radius <= 0f || maxTargets <= 0)
{
return false;
}
int resolvedOwnerEntityId = sourceOwnerEntityId != 0 ? sourceOwnerEntityId : sourceEntityId;
bool sourceWasActiveAtQueryTime = WasCollisionSourceActiveAtQueryTime(sourceEntityId);
Vector3 normalizedDirection = direction;
normalizedDirection.y = 0f;
if (normalizedDirection.sqrMagnitude <= Mathf.Epsilon)
{
normalizedDirection = Vector3.forward;
}
else
{
normalizedDirection.Normalize();
}
_areaCollisionRequests.Add(new AreaCollisionRequestData
{
SourceEntityId = sourceEntityId,
SourceOwnerEntityId = resolvedOwnerEntityId,
SourceWasActiveAtQueryTime = sourceWasActiveAtQueryTime,
Center = center,
Radius = Mathf.Max(0.01f, radius),
MaxTargets = Mathf.Max(1, maxTargets),
ShapeType = shapeType,
Direction = normalizedDirection,
HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f),
HalfWidth = Mathf.Max(0f, halfWidth),
HalfLength = Mathf.Max(0f, halfLength)
});
return true;
}
private int GetPendingAreaCollisionRequestCount()
{
return _areaCollisionRequests.Count;
}
private int EstimatePendingAreaCollisionCandidateCountFromRequests()
{
int expectedCount = 0;
for (int i = 0; i < _areaCollisionRequests.Count; i++)
{
expectedCount += Mathf.Max(1, _areaCollisionRequests[i].MaxTargets);
}
return expectedCount;
}
private void ClearAreaCollisionTransientBuffers()
{
_areaCollisionRequests.Clear();
_areaCollisionHitEvents.Clear();
_areaCollisionHitDedupKeys.Clear();
}
private static bool WasCollisionSourceActiveAtQueryTime(int sourceEntityId)
{
EntityBase sourceEntity = TryGetEntityById(sourceEntityId);
if (sourceEntity == null || !sourceEntity.Available)
{
return false;
}
if (sourceEntity is WeaponBase weapon)
{
return weapon.IsAttacking;
}
if (sourceEntity is EnemyProjectile projectile)
{
return projectile.IsActive;
}
return true;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 56086ce3981e4ed5b95fdc54f7db85a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,363 @@
using Components;
using CustomDebugger;
using CustomUtility;
using SepCore.Definition;
using Entity;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
#region Collision Resolve
private void ResolveCollisionCandidatesOnMainThread()
{
if (!_collisionCandidates.IsCreated)
{
_lastResolvedAreaHitCount = 0;
ClearAreaCollisionTransientBuffers();
return;
}
_projectileResolvedEntityIds.Clear();
_areaCollisionHitEvents.Clear();
_areaCollisionHitDedupKeys.Clear();
if (_collisionCandidates.Length == 0)
{
_lastResolvedAreaHitCount = 0;
ClearAreaCollisionTransientBuffers();
return;
}
using (CustomProfilerMarker.Collision_ResolveProjectile.Auto())
{
for (int i = 0; i < _collisionCandidates.Length; i++)
{
CollisionCandidateData candidate = _collisionCandidates[i];
if (candidate.SourceType == CollisionSourceTypeProjectile)
{
int projectileEntityId = candidate.SourceEntityId;
if (_projectileResolvedEntityIds.Contains(projectileEntityId))
{
continue;
}
if (!TryGetActiveProjectileSimData(projectileEntityId, out _, out ProjectileSimData projectile))
{
_projectileResolvedEntityIds.Add(projectileEntityId);
continue;
}
bool shouldExpireProjectile = true;
bool shouldDispatchPresentation = false;
int damage = 0;
Vector3 hitPosition = projectile.Position;
if (TryGetAliveTargetableEntity(candidate.TargetEntityId, out TargetableObject target))
{
EntityBase sourceEntity = TryGetEntityById(candidate.SourceEntityId);
EntityBase ownerEntity = TryGetEntityById(candidate.SourceOwnerEntityId);
shouldExpireProjectile = ResolveProjectileHitAgainstTarget(target, sourceEntity,
ownerEntity,
in projectile,
out damage, out hitPosition, out shouldDispatchPresentation);
}
if (shouldDispatchPresentation)
{
DispatchProjectileHitPresentationEvent(projectileEntityId, candidate.SourceEntityId,
candidate.SourceOwnerEntityId, candidate.TargetEntityId, damage, in hitPosition);
}
if (shouldExpireProjectile)
{
MarkProjectileAsExpired(projectileEntityId);
_projectileResolvedEntityIds.Add(projectileEntityId);
}
continue;
}
if (candidate.SourceType == CollisionSourceTypeArea)
{
long dedupKey = (((long)candidate.QueryId) << 32) ^ (uint)candidate.TargetEntityId;
if (!_areaCollisionHitDedupKeys.Add(dedupKey))
{
continue;
}
_areaCollisionHitEvents.Add(new AreaCollisionHitEventData
{
QueryId = candidate.QueryId,
SourceEntityId = candidate.SourceEntityId,
SourceOwnerEntityId = candidate.SourceOwnerEntityId,
TargetEntityId = candidate.TargetEntityId,
SqrDistance = candidate.SqrDistance
});
}
}
}
int resolvedAreaHitCount;
using (CustomProfilerMarker.Collision_ResolveArea.Auto())
{
resolvedAreaHitCount = ResolveAreaCollisionHitsOnMainThread();
}
_lastResolvedAreaHitCount = resolvedAreaHitCount;
_projectileResolvedEntityIds.Clear();
ClearAreaCollisionTransientBuffers();
}
private bool ResolveProjectileHitAgainstTarget(TargetableObject target, EntityBase sourceEntity,
EntityBase ownerEntity,
in ProjectileSimData projectile, out int damage, out Vector3 hitPosition,
out bool shouldDispatchPresentation)
{
damage = 0;
hitPosition = projectile.Position;
shouldDispatchPresentation = false;
if (target == null || !target.Available || target.IsDead)
{
return true;
}
if (target.CachedTransform != null)
{
hitPosition = target.CachedTransform.position;
}
if (!TryResolveImpactSource(sourceEntity, ownerEntity, out EntityBase attacker,
out ImpactData sourceImpact))
{
shouldDispatchPresentation = true;
return true;
}
ImpactData targetImpact = target.GetImpactData();
if (AIUtility.GetRelation(targetImpact.Camp, sourceImpact.Camp) == RelationType.Friendly)
{
return false;
}
damage = AIUtility.CalcDamageHP(sourceImpact.AttackBase, sourceImpact.AttackStat,
targetImpact.DefenseStat,
targetImpact.DodgeStat);
shouldDispatchPresentation = true;
if (damage <= 0)
{
return true;
}
target.ApplyDamage(attacker ?? sourceEntity ?? ownerEntity, damage);
return true;
}
private int ResolveAreaCollisionHitsOnMainThread()
{
if (_areaCollisionHitEvents.Count == 0)
{
return 0;
}
int resolvedHitCount = 0;
for (int i = 0; i < _areaCollisionHitEvents.Count; i++)
{
AreaCollisionHitEventData hitEvent = _areaCollisionHitEvents[i];
if (!TryGetCollisionQueryByQueryId(hitEvent.QueryId, out CollisionQueryData query) ||
query.SourceType != CollisionSourceTypeArea)
{
continue;
}
if (!query.SourceWasActiveAtQueryTime)
{
continue;
}
EntityBase sourceEntity = TryGetEntityById(hitEvent.SourceEntityId);
if (sourceEntity == null || !sourceEntity.Available)
{
continue;
}
if (!TryGetAliveTargetableEntity(hitEvent.TargetEntityId, out TargetableObject target))
{
continue;
}
float targetRadius = ResolveAreaTargetRadius(target);
if (!IsAreaTargetInsidePreciseShape(in query, target, targetRadius))
{
continue;
}
AIUtility.PerformCollision(target, sourceEntity, true);
resolvedHitCount++;
}
return resolvedHitCount;
}
private bool TryGetCollisionQueryByQueryId(int queryId, out CollisionQueryData query)
{
query = default;
if (!_collisionQueryInputs.IsCreated || queryId < 0 || queryId >= _collisionQueryInputs.Length)
{
return false;
}
CollisionQueryData direct = _collisionQueryInputs[queryId];
if (direct.QueryId == queryId)
{
query = direct;
return true;
}
for (int i = 0; i < _collisionQueryInputs.Length; i++)
{
CollisionQueryData candidate = _collisionQueryInputs[i];
if (candidate.QueryId != queryId)
{
continue;
}
query = candidate;
return true;
}
return false;
}
private float ResolveAreaTargetRadius(TargetableObject target)
{
if (target == null)
{
return 0f;
}
if (target is EnemyBase && TryGetEnemyData(target.Id, out EnemySimData enemyData))
{
return Mathf.Max(0f, enemyData.EnemyBodyRadius);
}
MovementComponent movementComponent = target.GetComponent<MovementComponent>();
return movementComponent != null ? Mathf.Max(0f, movementComponent.EnemyBodyRadius) : 0f;
}
private static bool IsAreaTargetInsidePreciseShape(in CollisionQueryData query, TargetableObject target,
float targetRadius)
{
if (target == null || target.CachedTransform == null)
{
return false;
}
Vector3 center = new Vector3(query.Position.x, query.Position.y, query.Position.z);
Vector3 toTarget = target.CachedTransform.position - center;
toTarget.y = 0f;
float radius = Mathf.Max(0.01f, query.Radius + Mathf.Max(0f, targetRadius));
float radiusSqr = radius * radius;
float sqrDistance = toTarget.sqrMagnitude;
if (sqrDistance > radiusSqr)
{
return false;
}
if (query.ShapeType == CollisionShapeRectangle)
{
Vector3 forwardRect = new Vector3(query.Direction.x, query.Direction.y, query.Direction.z);
forwardRect.y = 0f;
if (forwardRect.sqrMagnitude <= Mathf.Epsilon)
{
forwardRect = Vector3.forward;
}
else
{
forwardRect.Normalize();
}
Vector3 rightRect = Vector3.Cross(Vector3.up, forwardRect);
float halfWidth = Mathf.Max(0.01f, query.HalfWidth + Mathf.Max(0f, targetRadius));
float halfLength = Mathf.Max(0.01f, query.HalfLength + Mathf.Max(0f, targetRadius));
float forwardDistance = Vector3.Dot(toTarget, forwardRect);
float lateralDistance = Vector3.Dot(toTarget, rightRect);
return Mathf.Abs(forwardDistance) <= halfLength && Mathf.Abs(lateralDistance) <= halfWidth;
}
if (query.ShapeType != CollisionShapeSector)
{
return true;
}
if (sqrDistance <= Mathf.Epsilon)
{
return true;
}
Vector3 forward = new Vector3(query.Direction.x, query.Direction.y, query.Direction.z);
forward.y = 0f;
if (forward.sqrMagnitude <= Mathf.Epsilon)
{
forward = Vector3.forward;
}
else
{
forward.Normalize();
}
float halfAngle = Mathf.Clamp(query.HalfAngleDeg, 0f, 180f);
float angle = Vector3.Angle(forward, toTarget.normalized);
return angle <= halfAngle;
}
private bool TryGetActiveProjectileSimData(int projectileEntityId, out int simulationIndex,
out ProjectileSimData projectile)
{
simulationIndex = -1;
projectile = default;
if (!ProjectileBinding.TryGetSimulationIndex(projectileEntityId, out int foundIndex) || foundIndex < 0 ||
foundIndex >= _projectiles.Count)
{
return false;
}
ProjectileSimData data = _projectiles[foundIndex];
if (!data.Active || data.State != ProjectileStateActive)
{
return false;
}
simulationIndex = foundIndex;
projectile = data;
return true;
}
private bool MarkProjectileAsExpired(int projectileEntityId)
{
if (!ProjectileBinding.TryGetSimulationIndex(projectileEntityId, out int simulationIndex) ||
simulationIndex < 0 || simulationIndex >= _projectiles.Count)
{
return false;
}
ProjectileSimData projectile = _projectiles[simulationIndex];
projectile.Active = false;
projectile.State = ProjectileStateExpired;
projectile.RemainingLifetime = 0f;
if (projectile.LifeTime > 0f && projectile.Age < projectile.LifeTime)
{
projectile.Age = projectile.LifeTime;
}
_projectiles[simulationIndex] = projectile;
return true;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5261fa73dd9c4bfc91fcba0d5ca1bcb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,280 @@
using CustomDebugger;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
// Orchestrates per-tick simulation pipeline and enemy movement/separation jobs.
[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;
int enemySeparationCount = 0;
float enemySeparationMaxRadius = 0.45f;
// 2. BuildInput 阶段:
// 把 _enemies/_projectiles 同步到 Native 输入,准备输出缓冲;
// 统计是否需要敌人分离 Job;
// 预估并准备碰撞查询/候选缓冲。
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
{
SyncSimulationStateToJobInputs();
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 = 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, _enemies.Count * 2 + queryCount);
PrepareCollisionQueryAndCandidateChannels(queryCount, expectedCandidateCount, bucketCapacity);
}
// 3. StateUpdate 阶段(调度两个移动 Job)
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
{
enemyMovementHandle = ExecuteEnemyMovementJob(in context);
projectileMovementHandle = ExecuteProjectileMovementJob(in context);
}
// 4. Schedule 阶段(可选分离 Job)
// 如果有需要分离的敌人,调度:
// BuildEnemySeparationBucketsBurstJob(依赖 EnemyMovement)
// EnemySeparationBurstJob(依赖分桶 Job)
// 然后把“敌人链路 handle”和“投射物移动 handle”合并。
JobHandle simulationHandle;
using (CustomProfilerMarker.TickEnemies_Schedule.Auto())
{
hasEnemySeparationJob = TryScheduleEnemySeparationFromJobOutput(
in context,
enemyMovementHandle,
hasEnemySeparationCandidates,
enemySeparationCount,
enemySeparationMaxRadius,
out enemySeparationHandle);
JobHandle enemyHandle = hasEnemySeparationJob ? enemySeparationHandle : enemyMovementHandle;
simulationHandle = JobHandle.CombineDependencies(enemyHandle, projectileMovementHandle);
}
// 5. Complete 阶段:等待上述 Job 全部完成。
using (CustomProfilerMarker.TickEnemies_Complete.Auto())
{
simulationHandle.Complete();
}
// 6. 主线程后处理阶段:
// - 把分离结果覆盖回敌人输出(如果有分离 Job)
// - 构建碰撞候选(投射物查询 + area 请求查询 + 网格筛选)
using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto())
{
if (hasEnemySeparationJob)
{
CommitEnemySeparationFromJobOutput(enemySeparationCount);
}
}
using (CustomProfilerMarker.Collision.Auto())
{
PrepareCollisionCandidatesForFrame();
CompleteCollisionCandidatesForFrame();
}
// 7. MainThreadCommit 阶段
// - ApplyJobOutputsToSimulationState(写回 _enemies/_projectiles)
// - ResolveCollisionCandidatesOnMainThread(命中结算、事件、范围碰撞)
// - RecycleInactiveAndExpiredProjectiles(隐藏并移除失效投射物)
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
{
using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto())
{
ApplyJobOutputsToSimulationState();
ResolveCollisionCandidatesOnMainThread();
RecycleInactiveAndExpiredProjectiles();
}
}
}
private JobHandle ExecuteEnemyMovementJob(in SimulationTickContext context)
{
int enemyCount = _enemyJobInputs.Length;
if (enemyCount == 0)
{
return default;
}
if (context.DeltaTime <= 0f)
{
CopyEnemyInputsToOutputs();
return default;
}
float3 playerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z);
NativeArray<EnemyJobInputData> inputArray = _enemyJobInputs.AsArray();
NativeArray<EnemyJobOutputData> outputArray = _enemyJobOutputs.AsArray();
EnemyMovementBurstJob burstJob = new EnemyMovementBurstJob
{
Inputs = inputArray,
Outputs = outputArray,
DeltaTime = context.DeltaTime,
PlayerPosition = playerPosition
};
return burstJob.Schedule(enemyCount, 64);
}
private void CopyEnemyInputsToOutputs()
{
for (int i = 0; i < _enemyJobInputs.Length; i++)
{
EnemyJobInputData input = _enemyJobInputs[i];
_enemyJobOutputs[i] = new EnemyJobOutputData
{
EntityId = input.EntityId,
Position = input.Position,
Forward = input.Forward,
Rotation = input.Rotation,
Speed = input.Speed,
AttackRange = input.AttackRange,
AvoidEnemyOverlap = input.AvoidEnemyOverlap,
EnemyBodyRadius = input.EnemyBodyRadius,
SeparationIterations = input.SeparationIterations,
TargetType = input.TargetType,
State = input.State
};
}
}
private bool TryScheduleEnemySeparationFromJobOutput(in SimulationTickContext context, JobHandle dependency,
bool hasSeparationCandidates, int enemyCount, float maxRadius, out JobHandle separationHandle)
{
separationHandle = dependency;
if (enemyCount <= 0 || !hasSeparationCandidates)
{
return false;
}
float autoCellSize = maxRadius * 2f;
float configuredCellSize = _enemySeparationCellSize > 0f ? _enemySeparationCellSize : autoCellSize;
float cellSize = Mathf.Max(0.1f, configuredCellSize);
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();
BuildEnemySeparationBucketsBurstJob buildJob = new BuildEnemySeparationBucketsBurstJob
{
Inputs = inputArray,
Buckets = _enemySeparationBuckets.AsParallelWriter(),
CellSize = cellSize
};
JobHandle buildHandle = buildJob.Schedule(enemyCount, 64, dependency);
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);
return true;
}
private void CommitEnemySeparationFromJobOutput(int enemyCount)
{
if (enemyCount <= 0)
{
return;
}
CommitEnemySeparationTemporalBuffers(enemyCount);
for (int i = 0; i < enemyCount; i++)
{
_enemyJobOutputs[i] = _enemyJobSeparationOutputs[i];
}
}
private static long SeparationCellKey(int x, int z)
{
return ((long)x << 32) ^ (uint)z;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 99b885bb5cfe48e7bd9dba74fe683149
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,111 @@
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
namespace Simulation
{
public sealed partial class SimulationWorld
{
[Header("Projectile Simulation")]
[Tooltip("Recycle projectile when horizontal distance to player exceeds this range. <=0 disables this rule.")]
[SerializeField]
private float _projectileMaxDistanceFromPlayer = 120f;
[Tooltip("Recycle projectile when vertical offset to player exceeds this range. <=0 disables this rule.")]
[SerializeField]
private float _projectileMaxVerticalOffsetFromPlayer = 30f;
#region Projectile Movement Job
private JobHandle ExecuteProjectileMovementJob(in SimulationTickContext context)
{
int projectileCount = _projectileJobInputs.Length;
if (projectileCount == 0)
{
return default;
}
if (context.DeltaTime <= 0f)
{
CopyProjectileInputsToOutputs();
return default;
}
float maxDistance = Mathf.Max(0f, _projectileMaxDistanceFromPlayer);
float maxSqrDistanceFromPlayer = maxDistance > 0f ? maxDistance * maxDistance : -1f;
float maxVerticalOffsetFromPlayer = Mathf.Max(0f, _projectileMaxVerticalOffsetFromPlayer);
float3 playerPosition =
new float3(context.PlayerPosition.x, context.PlayerPosition.y, context.PlayerPosition.z);
NativeArray<ProjectileJobInputData> inputArray = _projectileJobInputs.AsArray();
NativeArray<ProjectileJobOutputData> outputArray = _projectileJobOutputs.AsArray();
ProjectileMovementBurstJob burstJob = new ProjectileMovementBurstJob
{
Inputs = inputArray,
Outputs = outputArray,
DeltaTime = context.DeltaTime,
PlayerPosition = playerPosition,
MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer,
MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer
};
return burstJob.Schedule(projectileCount, 64);
}
#endregion
#region Projectile Cleanup
private void RecycleInactiveAndExpiredProjectiles()
{
_projectileRecycleEntityIds.Clear();
for (int i = 0; i < _projectiles.Count; i++)
{
ProjectileSimData projectile = _projectiles[i];
if (!ShouldRecycleProjectileSimData(projectile))
{
continue;
}
_projectileRecycleEntityIds.Add(projectile.EntityId);
}
if (_projectileRecycleEntityIds.Count == 0)
{
return;
}
var entityComponent = GameEntry.Entity;
for (int i = 0; i < _projectileRecycleEntityIds.Count; i++)
{
int entityId = _projectileRecycleEntityIds[i];
if (entityComponent != null)
{
entityComponent.HideEntity(entityId);
}
RemoveProjectileByEntityId(entityId);
}
_projectileRecycleEntityIds.Clear();
}
private static bool ShouldRecycleProjectileSimData(in ProjectileSimData projectile)
{
if (!projectile.Active)
{
return true;
}
if (projectile.State == ProjectileStateExpired)
{
return true;
}
return projectile.LifeTime > 0f && projectile.Age >= projectile.LifeTime;
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bcb6513f18404795a654500d3d2f8ef9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: