- Checkpoint 1:清理 TickEnemies 侧 GC

- Checkpoint 2:解耦 Simulation 核心与 `Transform` 运行时依赖
- Checkpoint 3:收口 `EntitySync` 职责边界
- Checkpoint 4:拆分 Simulation Tick 阶段
- Checkpoint 5:补最小回归测试
- Checkpoint 6:补充 P1.5 结项文档
This commit is contained in:
SepComet
2026-02-21 13:39:14 +08:00
parent 200277a703
commit d55ead69a0
28 changed files with 1390 additions and 257 deletions
@@ -1,6 +1,4 @@
using Components;
using Entity;
using Entity.EntityData;
using GameFramework.Event;
using UnityGameFramework.Runtime;
@@ -45,21 +43,20 @@ namespace Simulation
if (groupName == EnemyGroupName && args.Entity.Logic is EnemyBase enemy)
{
_world.RegisterEnemyTransform(enemy.Id, enemy.CachedTransform);
_world.UpsertEnemy(CreateEnemySimData(enemy, args.UserData as EnemyData));
_world.RegisterEnemyLifecycle(enemy, args.UserData);
return;
}
if (groupName == DropGroupName && args.Entity.Logic is EntityBase pickupEntity)
{
_world.UpsertPickup(CreatePickupSimData(pickupEntity));
_world.RegisterPickupLifecycle(pickupEntity);
return;
}
if ((groupName == BulletGroupName || groupName == ProjectileGroupName) &&
args.Entity.Logic is EntityBase projectileEntity)
{
_world.UpsertProjectile(CreateProjectileSimData(projectileEntity));
_world.RegisterProjectileLifecycle(projectileEntity);
}
}
@@ -72,67 +69,21 @@ namespace Simulation
string groupName = args.EntityGroup.Name;
if (groupName == EnemyGroupName)
{
_world.UnregisterEnemyTransform(args.EntityId);
_world.RemoveEnemyByEntityId(args.EntityId);
_world.UnregisterEnemyLifecycle(args.EntityId);
return;
}
if (groupName == DropGroupName)
{
_world.RemovePickupByEntityId(args.EntityId);
_world.UnregisterPickupLifecycle(args.EntityId);
return;
}
if (groupName == BulletGroupName || groupName == ProjectileGroupName)
{
_world.RemoveProjectileByEntityId(args.EntityId);
_world.UnregisterProjectileLifecycle(args.EntityId);
}
}
private static EnemySimData CreateEnemySimData(EnemyBase enemy, EnemyData enemyData)
{
MovementComponent movementComponent = enemy != null ? enemy.GetComponent<MovementComponent>() : null;
return new EnemySimData
{
EntityId = enemy.Id,
Position = enemy.CachedTransform.position,
Forward = enemy.CachedTransform.forward,
Rotation = enemy.CachedTransform.rotation,
Speed = enemyData != null ? enemyData.SpeedBase : 0f,
AttackRange = 1f,
AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap,
EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f,
SeparationIterations = movementComponent != null ? movementComponent.SeparationIterations : 2,
TargetType = 0,
State = 0
};
}
private static PickupSimData CreatePickupSimData(EntityBase pickupEntity)
{
return new PickupSimData
{
EntityId = pickupEntity.Id,
Position = pickupEntity.CachedTransform.position,
PickupRadius = 0.35f,
State = 0
};
}
private static ProjectileSimData CreateProjectileSimData(EntityBase projectileEntity)
{
return new ProjectileSimData
{
EntityId = projectileEntity.Id,
OwnerEntityId = 0,
Position = projectileEntity.CachedTransform.position,
Forward = projectileEntity.CachedTransform.forward,
Speed = 0f,
RemainingLifetime = 0f,
State = 0
};
}
}
}
}
}
@@ -1,7 +1,9 @@
using System.Collections.Generic;
using Components;
using CustomDebugger;
using CustomUtility;
using Unity.Profiling;
using Entity;
using Entity.EntityData;
using UnityEngine;
using UnityGameFramework.Runtime;
@@ -13,6 +15,24 @@ namespace Simulation
private const int EnemyStateIdle = 0;
private const int EnemyStateChasing = 1;
private const int EnemyStateInAttackRange = 2;
private struct EnemyTickWorkItem
{
public int EntityId;
public Vector3 CurrentPosition;
public Vector3 DesiredPosition;
public Vector3 ToPlayer;
public Vector3 Forward;
public Quaternion Rotation;
public float SqrDistanceToPlayer;
public float AttackRangeSqr;
public float Speed;
public int SeparationIterations;
public bool AvoidEnemyOverlap;
public bool CanChase;
public bool HasRotationUpdate;
public int NextState;
}
[SerializeField] private bool _useSimulationMovement;
@@ -22,7 +42,8 @@ namespace Simulation
private readonly List<EnemySimData> _enemies = new List<EnemySimData>();
private readonly List<ProjectileSimData> _projectiles = new List<ProjectileSimData>();
private readonly List<PickupSimData> _pickups = new List<PickupSimData>();
private readonly Dictionary<int, Transform> _enemyTransforms = new Dictionary<int, Transform>();
private readonly List<EnemySeparationAgent> _enemySeparationAgents = new List<EnemySeparationAgent>();
private readonly List<EnemyTickWorkItem> _enemyTickWorkItems = new List<EnemyTickWorkItem>();
private EntityBinding EnemyBinding { get; } = new EntityBinding();
private EntityBinding ProjectileBinding { get; } = new EntityBinding();
@@ -98,24 +119,23 @@ namespace Simulation
_enemies.RemoveAt(lastIndex);
EnemyBinding.UnbindByEntityId(entityId);
_enemyTransforms.Remove(entityId);
return true;
}
private void RegisterEnemyTransform(int entityId, Transform transform)
private void RegisterEnemyLifecycle(EnemyBase enemy, object userData)
{
if (transform == null)
if (enemy == null || enemy.CachedTransform == null)
{
_enemyTransforms.Remove(entityId);
return;
}
_enemyTransforms[entityId] = transform;
EnemyData enemyData = userData as EnemyData;
UpsertEnemy(CreateEnemyInitialSimData(enemy, enemyData));
}
private void UnregisterEnemyTransform(int entityId)
private void UnregisterEnemyLifecycle(int entityId)
{
_enemyTransforms.Remove(entityId);
RemoveEnemyByEntityId(entityId);
}
private bool TryGetEnemyData(int entityId, out EnemySimData enemyData)
@@ -170,6 +190,21 @@ namespace Simulation
return true;
}
private void RegisterProjectileLifecycle(EntityBase projectileEntity)
{
if (projectileEntity == null || projectileEntity.CachedTransform == null)
{
return;
}
UpsertProjectile(CreateProjectileInitialSimData(projectileEntity));
}
private void UnregisterProjectileLifecycle(int entityId)
{
RemoveProjectileByEntityId(entityId);
}
private int AddPickup(in PickupSimData simData)
{
int simulationIndex = _pickups.Count;
@@ -209,6 +244,21 @@ namespace Simulation
return true;
}
private void RegisterPickupLifecycle(EntityBase pickupEntity)
{
if (pickupEntity == null || pickupEntity.CachedTransform == null)
{
return;
}
UpsertPickup(CreatePickupInitialSimData(pickupEntity));
}
private void UnregisterPickupLifecycle(int entityId)
{
RemovePickupByEntityId(entityId);
}
public void Tick(in SimulationTickContext context)
{
if (!_useSimulationMovement)
@@ -227,7 +277,8 @@ namespace Simulation
_enemies.Clear();
_projectiles.Clear();
_pickups.Clear();
_enemyTransforms.Clear();
_enemySeparationAgents.Clear();
_enemyTickWorkItems.Clear();
EnemyBinding.Clear();
ProjectileBinding.Clear();
@@ -244,52 +295,214 @@ namespace Simulation
Vector3 playerPosition = context.PlayerPosition;
playerPosition.y = 0f;
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
{
BuildEnemyTickInput(in playerPosition);
}
using (CustomProfilerMarker.TickEnemies_MoveSeparation.Auto())
{
MoveAndSeparateEnemies(context.DeltaTime);
}
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
{
UpdateEnemyStates();
}
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
{
WriteBackEnemyTickResults();
}
}
private void BuildEnemyTickInput(in Vector3 playerPosition)
{
_enemyTickWorkItems.Clear();
_enemySeparationAgents.Clear();
for (int i = 0; i < _enemies.Count; i++)
{
EnemySimData enemy = _enemies[i];
_enemyTransforms.TryGetValue(enemy.EntityId, out Transform enemyTransform);
Vector3 currentPosition = enemy.Position;
currentPosition.y = 0f;
Vector3 toPlayer = playerPosition - currentPosition;
Vector3 horizontalPosition = currentPosition;
horizontalPosition.y = 0f;
Vector3 toPlayer = playerPosition - horizontalPosition;
float sqrDistance = toPlayer.sqrMagnitude;
float attackRange = enemy.AttackRange > 0f ? enemy.AttackRange : DefaultAttackRange;
float attackRangeSqr = attackRange * attackRange;
bool isInAttackRange = sqrDistance <= attackRangeSqr;
bool canChase = !isInAttackRange && enemy.Speed > 0f && sqrDistance > float.Epsilon;
if (sqrDistance <= attackRangeSqr)
EnemyTickWorkItem workItem = new EnemyTickWorkItem
{
enemy.State = EnemyStateInAttackRange;
EntityId = enemy.EntityId,
CurrentPosition = currentPosition,
DesiredPosition = currentPosition,
ToPlayer = toPlayer,
Forward = enemy.Forward,
Rotation = enemy.Rotation,
SqrDistanceToPlayer = sqrDistance,
AttackRangeSqr = attackRangeSqr,
Speed = enemy.Speed,
SeparationIterations = enemy.SeparationIterations > 0 ? enemy.SeparationIterations : 1,
AvoidEnemyOverlap = enemy.AvoidEnemyOverlap,
CanChase = canChase,
HasRotationUpdate = false,
NextState = EnemyStateIdle
};
_enemyTickWorkItems.Add(workItem);
if (!enemy.AvoidEnemyOverlap) continue;
_enemySeparationAgents.Add(new EnemySeparationAgent
{
AgentId = enemy.EntityId,
Position = horizontalPosition,
Radius = enemy.EnemyBodyRadius > 0f ? enemy.EnemyBodyRadius : 0.45f
});
}
}
private void MoveAndSeparateEnemies(float deltaTime)
{
EnemySeparationSolverProvider.SetSimulationAgents(_enemySeparationAgents);
for (int i = 0; i < _enemyTickWorkItems.Count; i++)
{
EnemyTickWorkItem workItem = _enemyTickWorkItems[i];
if (!workItem.CanChase)
{
_enemyTickWorkItems[i] = workItem;
continue;
}
else if (enemy.Speed <= 0f || sqrDistance <= float.Epsilon)
Vector3 forward = workItem.ToPlayer.normalized;
Vector3 desiredPosition = workItem.CurrentPosition + forward * workItem.Speed * deltaTime;
if (workItem.AvoidEnemyOverlap)
{
enemy.State = EnemyStateIdle;
desiredPosition = EnemySeparationSolverProvider.ResolveSimulation(
workItem.EntityId,
desiredPosition,
forward,
workItem.SeparationIterations);
}
workItem.Forward = forward;
workItem.DesiredPosition = desiredPosition;
if (forward.sqrMagnitude > float.Epsilon)
{
workItem.Rotation = Quaternion.LookRotation(forward, Vector3.up);
workItem.HasRotationUpdate = true;
}
_enemyTickWorkItems[i] = workItem;
}
}
private void UpdateEnemyStates()
{
for (int i = 0; i < _enemyTickWorkItems.Count; i++)
{
EnemyTickWorkItem workItem = _enemyTickWorkItems[i];
if (workItem.SqrDistanceToPlayer <= workItem.AttackRangeSqr)
{
workItem.NextState = EnemyStateInAttackRange;
}
else if (workItem.CanChase)
{
workItem.NextState = EnemyStateChasing;
}
else
{
Vector3 forward = toPlayer.normalized;
enemy.Forward = forward;
Vector3 desiredPosition = enemy.Position + forward * enemy.Speed * context.DeltaTime;
if (enemy.AvoidEnemyOverlap && enemyTransform != null)
{
int separationIterations = enemy.SeparationIterations > 0 ? enemy.SeparationIterations : 1;
desiredPosition = EnemySeparationSolverProvider.Resolve(
enemyTransform,
desiredPosition,
forward,
separationIterations);
}
workItem.NextState = EnemyStateIdle;
}
enemy.Position = desiredPosition;
enemy.State = EnemyStateChasing;
if (forward.sqrMagnitude > float.Epsilon)
_enemyTickWorkItems[i] = workItem;
}
}
private void WriteBackEnemyTickResults()
{
for (int i = 0; i < _enemyTickWorkItems.Count; i++)
{
EnemySimData enemy = _enemies[i];
EnemyTickWorkItem workItem = _enemyTickWorkItems[i];
if (workItem.CanChase)
{
enemy.Forward = workItem.Forward;
enemy.Position = workItem.DesiredPosition;
if (workItem.HasRotationUpdate)
{
enemy.Rotation = Quaternion.LookRotation(forward, Vector3.up);
enemy.Rotation = workItem.Rotation;
}
}
enemy.State = workItem.NextState;
_enemies[i] = enemy;
}
}
private static EnemySimData CreateEnemyInitialSimData(EnemyBase enemy, EnemyData enemyData)
{
Transform enemyTransform = enemy.CachedTransform;
MovementComponent movementComponent = enemy.GetComponent<MovementComponent>();
float speed = 0f;
if (enemyData != null)
{
speed = enemyData.SpeedBase;
}
else if (movementComponent != null)
{
speed = movementComponent.Speed;
}
return new EnemySimData
{
EntityId = enemy.Id,
Position = enemyTransform.position,
Forward = enemyTransform.forward,
Rotation = enemyTransform.rotation,
Speed = speed,
AttackRange = 1f,
AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap,
EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f,
SeparationIterations = movementComponent != null ? movementComponent.SeparationIterations : 2,
TargetType = 0,
State = EnemyStateIdle
};
}
private static PickupSimData CreatePickupInitialSimData(EntityBase pickupEntity)
{
return new PickupSimData
{
EntityId = pickupEntity.Id,
Position = pickupEntity.CachedTransform.position,
PickupRadius = 0.35f,
State = 0
};
}
private static ProjectileSimData CreateProjectileInitialSimData(EntityBase projectileEntity)
{
return new ProjectileSimData
{
EntityId = projectileEntity.Id,
OwnerEntityId = 0,
Position = projectileEntity.CachedTransform.position,
Forward = projectileEntity.CachedTransform.forward,
Speed = 0f,
RemainingLifetime = 0f,
State = 0
};
}
}
}
}