- 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:
@@ -5,6 +5,10 @@ namespace CustomDebugger
|
||||
public static class CustomProfilerMarker
|
||||
{
|
||||
public static readonly ProfilerMarker TickEnemies = new ProfilerMarker("TickEnemies");
|
||||
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_WriteBack = new ProfilerMarker("TickEnemies.WriteBack");
|
||||
public static readonly ProfilerMarker Movement_Update = new ProfilerMarker("Movement_Update");
|
||||
public static readonly ProfilerMarker ShopUI_Update = new("UGF.ShopUI.Update");
|
||||
public static readonly ProfilerMarker Inventory_Refresh = new("UGF.Inventory.Refresh");
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -6,70 +5,159 @@ namespace CustomUtility
|
||||
{
|
||||
public static class EnemySeparationSolverProvider
|
||||
{
|
||||
private struct Registration
|
||||
private enum SolverType
|
||||
{
|
||||
GridBucket,
|
||||
Naive
|
||||
}
|
||||
|
||||
private struct LegacyRegistration
|
||||
{
|
||||
public int AgentId;
|
||||
public float BodyRadius;
|
||||
}
|
||||
|
||||
private static IEnemySeparationSolver _current = new GridBucketEnemySeparationSolver();
|
||||
private static readonly Dictionary<Transform, Registration> Registrations = new();
|
||||
private static SolverType _solverType = SolverType.GridBucket;
|
||||
private static float _gridCellSize = 1f;
|
||||
private static IEnemySeparationSolver _legacySolver = CreateSolver();
|
||||
private static IEnemySeparationSolver _simulationSolver = CreateSolver();
|
||||
|
||||
public static IEnemySeparationSolver Current => _current;
|
||||
public static string CurrentSolverName => _current.GetType().Name;
|
||||
private static readonly Dictionary<Transform, LegacyRegistration> LegacyRegistrations = new();
|
||||
private static readonly List<EnemySeparationAgent> LegacyAgents = new();
|
||||
private static readonly List<Transform> LegacyRecycle = new();
|
||||
private static int _legacySnapshotFrame = -1;
|
||||
private static int _nextLegacyAgentId = 1;
|
||||
|
||||
public static IEnemySeparationSolver Current => _simulationSolver;
|
||||
public static string CurrentSolverName => _simulationSolver.GetType().Name;
|
||||
|
||||
public static void SetSolver(IEnemySeparationSolver solver)
|
||||
{
|
||||
if (solver == null) return;
|
||||
_current = solver;
|
||||
ReRegisterAll();
|
||||
|
||||
_legacySolver = solver;
|
||||
_simulationSolver = solver;
|
||||
_legacySnapshotFrame = -1;
|
||||
}
|
||||
|
||||
public static void UseGridBucketSolver(float cellSize = 1f)
|
||||
{
|
||||
SetSolver(new GridBucketEnemySeparationSolver(cellSize));
|
||||
_solverType = SolverType.GridBucket;
|
||||
_gridCellSize = Mathf.Max(0.1f, cellSize);
|
||||
RecreateSolvers();
|
||||
}
|
||||
|
||||
public static void UseNaiveSolver()
|
||||
{
|
||||
SetSolver(new NaiveEnemySeparationSolver());
|
||||
_solverType = SolverType.Naive;
|
||||
RecreateSolvers();
|
||||
}
|
||||
|
||||
public static void Register(Transform transform, float bodyRadius)
|
||||
{
|
||||
if (transform == null) return;
|
||||
|
||||
var registration = new Registration
|
||||
if (LegacyRegistrations.TryGetValue(transform, out LegacyRegistration registration))
|
||||
{
|
||||
BodyRadius = bodyRadius
|
||||
};
|
||||
Registrations[transform] = registration;
|
||||
_current.Register(transform, bodyRadius);
|
||||
registration.BodyRadius = bodyRadius;
|
||||
LegacyRegistrations[transform] = registration;
|
||||
}
|
||||
else
|
||||
{
|
||||
LegacyRegistrations.Add(transform, new LegacyRegistration
|
||||
{
|
||||
AgentId = _nextLegacyAgentId++,
|
||||
BodyRadius = bodyRadius
|
||||
});
|
||||
}
|
||||
|
||||
_legacySnapshotFrame = -1;
|
||||
}
|
||||
|
||||
public static void Unregister(Transform transform)
|
||||
{
|
||||
if (transform == null) return;
|
||||
if (!LegacyRegistrations.Remove(transform)) return;
|
||||
|
||||
_current.Unregister(transform);
|
||||
Registrations.Remove(transform);
|
||||
_legacySnapshotFrame = -1;
|
||||
}
|
||||
|
||||
public static Vector3 Resolve(Transform transform, Vector3 desiredPosition, Vector3 fallbackDirection,
|
||||
int iterations)
|
||||
{
|
||||
return _current.Resolve(transform, desiredPosition, fallbackDirection, iterations);
|
||||
if (transform == null) return desiredPosition;
|
||||
if (!LegacyRegistrations.TryGetValue(transform, out LegacyRegistration registration))
|
||||
{
|
||||
return desiredPosition;
|
||||
}
|
||||
|
||||
EnsureLegacySnapshot();
|
||||
return _legacySolver.Resolve(registration.AgentId, desiredPosition, fallbackDirection, iterations);
|
||||
}
|
||||
|
||||
private static void ReRegisterAll()
|
||||
public static void SetSimulationAgents(IReadOnlyList<EnemySeparationAgent> agents)
|
||||
{
|
||||
foreach (var pair in Registrations)
|
||||
_simulationSolver.SetAgents(agents);
|
||||
}
|
||||
|
||||
public static Vector3 ResolveSimulation(int agentId, Vector3 desiredPosition, Vector3 fallbackDirection,
|
||||
int iterations)
|
||||
{
|
||||
return _simulationSolver.Resolve(agentId, desiredPosition, fallbackDirection, iterations);
|
||||
}
|
||||
|
||||
private static void EnsureLegacySnapshot()
|
||||
{
|
||||
int frame = Time.frameCount;
|
||||
if (_legacySnapshotFrame == frame) return;
|
||||
|
||||
_legacySnapshotFrame = frame;
|
||||
LegacyAgents.Clear();
|
||||
LegacyRecycle.Clear();
|
||||
|
||||
foreach (var pair in LegacyRegistrations)
|
||||
{
|
||||
Transform transform = pair.Key;
|
||||
Registration registration = pair.Value;
|
||||
if (transform == null) continue;
|
||||
if (transform == null)
|
||||
{
|
||||
LegacyRecycle.Add(pair.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
_current.Register(transform, registration.BodyRadius);
|
||||
Vector3 position = transform.position;
|
||||
position.y = 0f;
|
||||
|
||||
LegacyAgents.Add(new EnemySeparationAgent
|
||||
{
|
||||
AgentId = pair.Value.AgentId,
|
||||
Position = position,
|
||||
Radius = Mathf.Max(0.01f, pair.Value.BodyRadius)
|
||||
});
|
||||
}
|
||||
|
||||
for (int i = 0; i < LegacyRecycle.Count; i++)
|
||||
{
|
||||
LegacyRegistrations.Remove(LegacyRecycle[i]);
|
||||
}
|
||||
|
||||
_legacySolver.SetAgents(LegacyAgents);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RecreateSolvers()
|
||||
{
|
||||
_legacySolver = CreateSolver();
|
||||
_simulationSolver = CreateSolver();
|
||||
_legacySnapshotFrame = -1;
|
||||
}
|
||||
|
||||
private static IEnemySeparationSolver CreateSolver()
|
||||
{
|
||||
if (_solverType == SolverType.Naive)
|
||||
{
|
||||
return new NaiveEnemySeparationSolver();
|
||||
}
|
||||
|
||||
return new GridBucketEnemySeparationSolver(_gridCellSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,20 @@ namespace CustomUtility
|
||||
{
|
||||
public sealed class GridBucketEnemySeparationSolver : IEnemySeparationSolver
|
||||
{
|
||||
private sealed class Agent
|
||||
private struct Agent
|
||||
{
|
||||
public Transform Transform;
|
||||
public float Radius;
|
||||
public Vector3 Position;
|
||||
public int CellX;
|
||||
public int CellZ;
|
||||
}
|
||||
|
||||
private readonly System.Collections.Generic.Dictionary<Transform, Agent> _agents = new();
|
||||
|
||||
private readonly System.Collections.Generic.Dictionary<long, System.Collections.Generic.List<Transform>>
|
||||
_buckets = new();
|
||||
|
||||
private readonly System.Collections.Generic.List<Transform> _recycle = new();
|
||||
private readonly System.Collections.Generic.Dictionary<int, Agent> _agents = new();
|
||||
private readonly System.Collections.Generic.Dictionary<long, System.Collections.Generic.List<int>> _buckets = new();
|
||||
private readonly System.Collections.Generic.Stack<System.Collections.Generic.List<int>> _bucketListPool = new();
|
||||
private readonly System.Collections.Generic.List<long> _activeBucketKeys = new();
|
||||
private readonly float _cellSize;
|
||||
|
||||
private int _snapshotFrame = -1;
|
||||
private float _maxRadius = 0.45f;
|
||||
|
||||
public GridBucketEnemySeparationSolver(float cellSize = 1f)
|
||||
@@ -29,44 +25,42 @@ namespace CustomUtility
|
||||
_cellSize = Mathf.Max(0.1f, cellSize);
|
||||
}
|
||||
|
||||
public void Register(Transform transform, float bodyRadius)
|
||||
public void SetAgents(System.Collections.Generic.IReadOnlyList<EnemySeparationAgent> agents)
|
||||
{
|
||||
if (transform == null) return;
|
||||
RecycleBucketsForSnapshot();
|
||||
_agents.Clear();
|
||||
_maxRadius = 0.01f;
|
||||
|
||||
if (!_agents.TryGetValue(transform, out var agent))
|
||||
if (agents == null) return;
|
||||
|
||||
for (int i = 0; i < agents.Count; i++)
|
||||
{
|
||||
agent = new Agent();
|
||||
_agents.Add(transform, agent);
|
||||
}
|
||||
EnemySeparationAgent input = agents[i];
|
||||
Vector3 position = input.Position;
|
||||
position.y = 0f;
|
||||
float radius = Mathf.Max(0.01f, input.Radius);
|
||||
|
||||
agent.Transform = transform;
|
||||
agent.Radius = Mathf.Max(0.01f, bodyRadius);
|
||||
if (agent.Radius > _maxRadius)
|
||||
{
|
||||
_maxRadius = agent.Radius;
|
||||
}
|
||||
Agent agent = new Agent
|
||||
{
|
||||
Radius = radius,
|
||||
Position = position,
|
||||
CellX = ToCell(position.x),
|
||||
CellZ = ToCell(position.z)
|
||||
};
|
||||
|
||||
_snapshotFrame = -1;
|
||||
_agents[input.AgentId] = agent;
|
||||
AddToBucket(input.AgentId, agent.CellX, agent.CellZ);
|
||||
|
||||
if (radius > _maxRadius)
|
||||
{
|
||||
_maxRadius = radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Unregister(Transform transform)
|
||||
public Vector3 Resolve(int agentId, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations)
|
||||
{
|
||||
if (transform == null) return;
|
||||
if (!_agents.TryGetValue(transform, out var agent)) return;
|
||||
|
||||
RemoveFromBucket(transform, agent.CellX, agent.CellZ);
|
||||
_agents.Remove(transform);
|
||||
RecalculateMaxRadius();
|
||||
_snapshotFrame = -1;
|
||||
}
|
||||
|
||||
public Vector3 Resolve(Transform transform, Vector3 desiredPosition, Vector3 fallbackDirection,
|
||||
int iterations)
|
||||
{
|
||||
if (transform == null) return desiredPosition;
|
||||
if (!_agents.TryGetValue(transform, out var self)) return desiredPosition;
|
||||
|
||||
EnsureSnapshot();
|
||||
if (!_agents.TryGetValue(agentId, out var self)) return desiredPosition;
|
||||
|
||||
Vector3 candidate = desiredPosition;
|
||||
candidate.y = 0f;
|
||||
@@ -89,9 +83,9 @@ namespace CustomUtility
|
||||
|
||||
for (int i = 0; i < bucket.Count; i++)
|
||||
{
|
||||
Transform otherTransform = bucket[i];
|
||||
if (otherTransform == transform) continue;
|
||||
if (!_agents.TryGetValue(otherTransform, out var other)) continue;
|
||||
int otherAgentId = bucket[i];
|
||||
if (otherAgentId == agentId) continue;
|
||||
if (!_agents.TryGetValue(otherAgentId, out var other)) continue;
|
||||
|
||||
Vector3 toSelf = candidate - other.Position;
|
||||
float minDistance = self.Radius + other.Radius;
|
||||
@@ -114,98 +108,65 @@ namespace CustomUtility
|
||||
}
|
||||
}
|
||||
|
||||
SyncAgentPosition(transform, self, candidate);
|
||||
SyncAgentPosition(agentId, ref self, candidate);
|
||||
|
||||
candidate.y = desiredPosition.y;
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private void EnsureSnapshot()
|
||||
private void RecycleBucketsForSnapshot()
|
||||
{
|
||||
int frame = Time.frameCount;
|
||||
if (_snapshotFrame == frame) return;
|
||||
|
||||
_snapshotFrame = frame;
|
||||
_buckets.Clear();
|
||||
_recycle.Clear();
|
||||
|
||||
foreach (var pair in _agents)
|
||||
for (int i = 0; i < _activeBucketKeys.Count; i++)
|
||||
{
|
||||
Transform transform = pair.Key;
|
||||
Agent agent = pair.Value;
|
||||
if (transform == null || agent.Transform == null)
|
||||
{
|
||||
_recycle.Add(transform);
|
||||
continue;
|
||||
}
|
||||
long key = _activeBucketKeys[i];
|
||||
if (!_buckets.TryGetValue(key, out var bucket)) continue;
|
||||
|
||||
Vector3 position = agent.Transform.position;
|
||||
position.y = 0f;
|
||||
agent.Position = position;
|
||||
|
||||
agent.CellX = ToCell(position.x);
|
||||
agent.CellZ = ToCell(position.z);
|
||||
AddToBucket(transform, agent.CellX, agent.CellZ);
|
||||
bucket.Clear();
|
||||
_bucketListPool.Push(bucket);
|
||||
_buckets.Remove(key);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _recycle.Count; i++)
|
||||
{
|
||||
_agents.Remove(_recycle[i]);
|
||||
}
|
||||
_activeBucketKeys.Clear();
|
||||
}
|
||||
|
||||
private void SyncAgentPosition(Transform transform, Agent agent, Vector3 position)
|
||||
private void SyncAgentPosition(int agentId, ref Agent agent, Vector3 position)
|
||||
{
|
||||
int newCellX = ToCell(position.x);
|
||||
int newCellZ = ToCell(position.z);
|
||||
|
||||
if (agent.CellX != newCellX || agent.CellZ != newCellZ)
|
||||
{
|
||||
RemoveFromBucket(transform, agent.CellX, agent.CellZ);
|
||||
AddToBucket(transform, newCellX, newCellZ);
|
||||
RemoveFromBucket(agentId, agent.CellX, agent.CellZ);
|
||||
AddToBucket(agentId, newCellX, newCellZ);
|
||||
agent.CellX = newCellX;
|
||||
agent.CellZ = newCellZ;
|
||||
}
|
||||
|
||||
agent.Position = position;
|
||||
_agents[agentId] = agent;
|
||||
}
|
||||
|
||||
private void AddToBucket(Transform transform, int cellX, int cellZ)
|
||||
private void AddToBucket(int agentId, int cellX, int cellZ)
|
||||
{
|
||||
long key = CellKey(cellX, cellZ);
|
||||
if (!_buckets.TryGetValue(key, out var list))
|
||||
{
|
||||
list = new System.Collections.Generic.List<Transform>(8);
|
||||
list = _bucketListPool.Count > 0
|
||||
? _bucketListPool.Pop()
|
||||
: new System.Collections.Generic.List<int>(8);
|
||||
_buckets.Add(key, list);
|
||||
_activeBucketKeys.Add(key);
|
||||
}
|
||||
|
||||
list.Add(transform);
|
||||
list.Add(agentId);
|
||||
}
|
||||
|
||||
private void RemoveFromBucket(Transform transform, int cellX, int cellZ)
|
||||
private void RemoveFromBucket(int agentId, int cellX, int cellZ)
|
||||
{
|
||||
long key = CellKey(cellX, cellZ);
|
||||
if (!_buckets.TryGetValue(key, out var list)) return;
|
||||
|
||||
list.Remove(transform);
|
||||
if (list.Count == 0)
|
||||
{
|
||||
_buckets.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void RecalculateMaxRadius()
|
||||
{
|
||||
float max = 0.01f;
|
||||
foreach (var pair in _agents)
|
||||
{
|
||||
if (pair.Value.Radius > max)
|
||||
{
|
||||
max = pair.Value.Radius;
|
||||
}
|
||||
}
|
||||
|
||||
_maxRadius = max;
|
||||
list.Remove(agentId);
|
||||
}
|
||||
|
||||
private int ToCell(float value)
|
||||
@@ -218,5 +179,4 @@ namespace CustomUtility
|
||||
return ((long)x << 32) ^ (uint)z;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,10 +2,16 @@ using UnityEngine;
|
||||
|
||||
namespace CustomUtility
|
||||
{
|
||||
public struct EnemySeparationAgent
|
||||
{
|
||||
public int AgentId;
|
||||
public Vector3 Position;
|
||||
public float Radius;
|
||||
}
|
||||
|
||||
public interface IEnemySeparationSolver
|
||||
{
|
||||
void Register(Transform transform, float bodyRadius);
|
||||
void Unregister(Transform transform);
|
||||
Vector3 Resolve(Transform transform, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations);
|
||||
void SetAgents(System.Collections.Generic.IReadOnlyList<EnemySeparationAgent> agents);
|
||||
Vector3 Resolve(int agentId, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,39 +4,41 @@ namespace CustomUtility
|
||||
{
|
||||
public sealed class NaiveEnemySeparationSolver : IEnemySeparationSolver
|
||||
{
|
||||
private sealed class Agent
|
||||
private struct Agent
|
||||
{
|
||||
public Transform Transform;
|
||||
public float Radius;
|
||||
public Vector3 Position;
|
||||
}
|
||||
|
||||
private readonly System.Collections.Generic.Dictionary<Transform, Agent> _agents = new();
|
||||
private readonly System.Collections.Generic.List<Transform> _agentKeys = new();
|
||||
private readonly System.Collections.Generic.Dictionary<int, Agent> _agents = new();
|
||||
private readonly System.Collections.Generic.List<int> _agentKeys = new();
|
||||
|
||||
public void Register(Transform transform, float bodyRadius)
|
||||
public void SetAgents(System.Collections.Generic.IReadOnlyList<EnemySeparationAgent> agents)
|
||||
{
|
||||
if (transform == null) return;
|
||||
_agents.Clear();
|
||||
_agentKeys.Clear();
|
||||
if (agents == null) return;
|
||||
|
||||
if (!_agents.TryGetValue(transform, out var agent))
|
||||
for (int i = 0; i < agents.Count; i++)
|
||||
{
|
||||
agent = new Agent();
|
||||
_agents.Add(transform, agent);
|
||||
EnemySeparationAgent input = agents[i];
|
||||
Vector3 position = input.Position;
|
||||
position.y = 0f;
|
||||
|
||||
Agent agent = new Agent
|
||||
{
|
||||
Radius = Mathf.Max(0.01f, input.Radius),
|
||||
Position = position
|
||||
};
|
||||
|
||||
_agents[input.AgentId] = agent;
|
||||
_agentKeys.Add(input.AgentId);
|
||||
}
|
||||
|
||||
agent.Transform = transform;
|
||||
agent.Radius = Mathf.Max(0.01f, bodyRadius);
|
||||
}
|
||||
|
||||
public void Unregister(Transform transform)
|
||||
public Vector3 Resolve(int agentId, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations)
|
||||
{
|
||||
if (transform == null) return;
|
||||
_agents.Remove(transform);
|
||||
}
|
||||
|
||||
public Vector3 Resolve(Transform transform, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations)
|
||||
{
|
||||
if (transform == null) return desiredPosition;
|
||||
if (!_agents.TryGetValue(transform, out var self)) return desiredPosition;
|
||||
if (!_agents.TryGetValue(agentId, out var self)) return desiredPosition;
|
||||
|
||||
Vector3 candidate = desiredPosition;
|
||||
candidate.y = 0f;
|
||||
@@ -44,26 +46,16 @@ namespace CustomUtility
|
||||
Vector3 fallback = fallbackDirection.sqrMagnitude > 0.0001f ? fallbackDirection.normalized : Vector3.right;
|
||||
fallback.y = 0f;
|
||||
|
||||
_agentKeys.Clear();
|
||||
foreach (var pair in _agents)
|
||||
{
|
||||
_agentKeys.Add(pair.Key);
|
||||
}
|
||||
|
||||
int effectiveIterations = Mathf.Max(1, iterations);
|
||||
for (int iter = 0; iter < effectiveIterations; iter++)
|
||||
{
|
||||
for (int i = 0; i < _agentKeys.Count; i++)
|
||||
{
|
||||
Transform otherTransform = _agentKeys[i];
|
||||
if (otherTransform == transform) continue;
|
||||
if (!_agents.TryGetValue(otherTransform, out var other)) continue;
|
||||
if (other.Transform == null) continue;
|
||||
int otherAgentId = _agentKeys[i];
|
||||
if (otherAgentId == agentId) continue;
|
||||
if (!_agents.TryGetValue(otherAgentId, out var other)) continue;
|
||||
|
||||
Vector3 otherPosition = other.Transform.position;
|
||||
otherPosition.y = 0f;
|
||||
|
||||
Vector3 toSelf = candidate - otherPosition;
|
||||
Vector3 toSelf = candidate - other.Position;
|
||||
float minDistance = self.Radius + other.Radius;
|
||||
float minDistanceSq = minDistance * minDistance;
|
||||
float sqrDistance = toSelf.sqrMagnitude;
|
||||
|
||||
Reference in New Issue
Block a user