- 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
@@ -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;
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3d51d9ea4e1d4d84aa3fa2e38d79b2f4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7c2d2681dd4b47f48b0f6a8185e4fa2b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9b8f4bce7ec54a5d8e53d7d4e6f8d12a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
{
"name": "Simulation.EditModeTests",
"references": [],
"optionalUnityReferences": [
"TestAssemblies"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": []
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4f4b677269d9466da1b7f4b3d33895f7
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,220 @@
using System.Reflection;
using NUnit.Framework;
using UnityEngine;
namespace Simulation.Tests.Editor
{
public class SimulationWorldTickTests
{
private const string GameAssemblyName = "Assembly-CSharp";
private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
private const BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance;
private const BindingFlags NonPublicInstance = BindingFlags.NonPublic | BindingFlags.Instance;
private static readonly System.Type SimulationWorldType =
System.Type.GetType($"Simulation.SimulationWorld, {GameAssemblyName}");
private static readonly System.Type SimulationTickContextType =
System.Type.GetType($"Simulation.SimulationTickContext, {GameAssemblyName}");
private static readonly System.Type EnemySimDataType =
System.Type.GetType($"Simulation.EnemySimData, {GameAssemblyName}");
private static readonly System.Type EnemySeparationSolverProviderType =
System.Type.GetType($"CustomUtility.EnemySeparationSolverProvider, {GameAssemblyName}");
private static readonly MethodInfo UpsertEnemyMethod =
SimulationWorldType?.GetMethod("UpsertEnemy", NonPublicInstance);
private static readonly MethodInfo RemoveEnemyByEntityIdMethod =
SimulationWorldType?.GetMethod("RemoveEnemyByEntityId", NonPublicInstance);
private static readonly MethodInfo TryGetEnemyDataMethod =
SimulationWorldType?.GetMethod("TryGetEnemyData", NonPublicInstance);
private static readonly MethodInfo TickMethod =
SimulationWorldType?.GetMethod("Tick", PublicInstance);
private static readonly MethodInfo SetUseSimulationMovementMethod =
SimulationWorldType?.GetMethod("SetUseSimulationMovement", PublicInstance);
private static readonly MethodInfo UseGridBucketSolverMethod =
EnemySeparationSolverProviderType?.GetMethod("UseGridBucketSolver", PublicStatic);
private static readonly FieldInfo EntitySyncField =
SimulationWorldType?.GetField("_entitySync", NonPublicInstance);
private static readonly FieldInfo PresentationField =
SimulationWorldType?.GetField("_presentation", NonPublicInstance);
private static readonly PropertyInfo EnemiesProperty =
SimulationWorldType?.GetProperty("Enemies", PublicInstance);
private GameObject _worldGameObject;
private Component _worldComponent;
[SetUp]
public void SetUp()
{
Assert.NotNull(SimulationWorldType, "SimulationWorld type lookup failed.");
Assert.NotNull(SimulationTickContextType, "SimulationTickContext type lookup failed.");
Assert.NotNull(EnemySimDataType, "EnemySimData type lookup failed.");
Assert.NotNull(EnemySeparationSolverProviderType, "EnemySeparationSolverProvider type lookup failed.");
Assert.NotNull(UpsertEnemyMethod, "UpsertEnemy reflection lookup failed.");
Assert.NotNull(RemoveEnemyByEntityIdMethod, "RemoveEnemyByEntityId reflection lookup failed.");
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
Assert.NotNull(SetUseSimulationMovementMethod, "SetUseSimulationMovement reflection lookup failed.");
Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
Assert.NotNull(EnemiesProperty, "Enemies property reflection lookup failed.");
_worldGameObject = new GameObject("SimulationWorldTickTests");
_worldComponent = _worldGameObject.AddComponent(SimulationWorldType);
SetUseSimulationMovementMethod.Invoke(_worldComponent, new object[] { true });
UseGridBucketSolverMethod.Invoke(null, new object[] { 1f });
}
[TearDown]
public void TearDown()
{
if (_worldComponent != null)
{
EntitySyncField?.SetValue(_worldComponent, null);
PresentationField?.SetValue(_worldComponent, null);
}
if (_worldGameObject != null)
{
Object.DestroyImmediate(_worldGameObject);
}
_worldComponent = null;
_worldGameObject = null;
}
[Test]
public void TickEnemies_ChasesPlayer_WhenOutOfAttackRange()
{
UpsertEnemy(CreateEnemy(entityId: 1001, position: Vector3.zero, speed: 2f, attackRange: 1f));
InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: new Vector3(10f, 0f, 0f));
object enemy = GetEnemyAt(0);
Assert.That((int)GetField(enemy, "State"), Is.EqualTo(1));
Vector3 position = (Vector3)GetField(enemy, "Position");
Vector3 forward = (Vector3)GetField(enemy, "Forward");
Assert.That(position.x, Is.EqualTo(2f).Within(0.0001f));
Assert.That(position.z, Is.EqualTo(0f).Within(0.0001f));
Assert.That(forward.x, Is.EqualTo(1f).Within(0.0001f));
}
[Test]
public void TickEnemies_StopsMovement_WhenInAttackRange()
{
Vector3 startPosition = Vector3.zero;
UpsertEnemy(CreateEnemy(entityId: 1002, position: startPosition, speed: 3f, attackRange: 2f));
InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: new Vector3(1f, 0f, 0f));
object enemy = GetEnemyAt(0);
Assert.That((int)GetField(enemy, "State"), Is.EqualTo(2));
Vector3 position = (Vector3)GetField(enemy, "Position");
Assert.That(position.x, Is.EqualTo(startPosition.x).Within(0.0001f));
Assert.That(position.y, Is.EqualTo(startPosition.y).Within(0.0001f));
Assert.That(position.z, Is.EqualTo(startPosition.z).Within(0.0001f));
}
[Test]
public void RemoveEnemyByEntityId_RemapIndex_ForMovedEnemy()
{
UpsertEnemy(CreateEnemy(entityId: 2001, position: new Vector3(0f, 0f, 0f), speed: 1f, attackRange: 1f));
UpsertEnemy(CreateEnemy(entityId: 2002, position: new Vector3(2f, 0f, 0f), speed: 1f, attackRange: 1f));
UpsertEnemy(CreateEnemy(entityId: 2003, position: new Vector3(4f, 0f, 0f), speed: 1f, attackRange: 1f));
bool removed = RemoveEnemyByEntityId(2002);
bool removedEntityExists = TryGetEnemyData(2002, out _);
bool movedEntityExists = TryGetEnemyData(2003, out object movedEnemy);
Assert.IsTrue(removed);
Assert.That(GetEnemiesCount(), Is.EqualTo(2));
Assert.IsFalse(removedEntityExists);
Assert.IsTrue(movedEntityExists);
Assert.That((int)GetField(movedEnemy, "EntityId"), Is.EqualTo(2003));
Assert.That((int)GetField(GetEnemyAt(1), "EntityId"), Is.EqualTo(2003));
}
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange)
{
object enemy = System.Activator.CreateInstance(EnemySimDataType);
SetField(ref enemy, "EntityId", entityId);
SetField(ref enemy, "Position", position);
SetField(ref enemy, "Forward", Vector3.forward);
SetField(ref enemy, "Rotation", Quaternion.identity);
SetField(ref enemy, "Speed", speed);
SetField(ref enemy, "AttackRange", attackRange);
SetField(ref enemy, "AvoidEnemyOverlap", false);
SetField(ref enemy, "EnemyBodyRadius", 0.45f);
SetField(ref enemy, "SeparationIterations", 1);
SetField(ref enemy, "TargetType", 0);
SetField(ref enemy, "State", 0);
return enemy;
}
private void InvokeTick(float deltaTime, float realDeltaTime, Vector3 playerPosition)
{
object tickContext = System.Activator.CreateInstance(
SimulationTickContextType,
BindingFlags.Public | BindingFlags.Instance,
null,
new object[] { deltaTime, realDeltaTime, playerPosition },
null);
TickMethod.Invoke(_worldComponent, new[] { tickContext });
}
private void UpsertEnemy(object enemy)
{
UpsertEnemyMethod.Invoke(_worldComponent, new[] { enemy });
}
private bool RemoveEnemyByEntityId(int entityId)
{
return (bool)RemoveEnemyByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId });
}
private bool TryGetEnemyData(int entityId, out object enemyData)
{
object boxedDefault = System.Activator.CreateInstance(EnemySimDataType);
object[] parameters = { entityId, boxedDefault };
bool result = (bool)TryGetEnemyDataMethod.Invoke(_worldComponent, parameters);
enemyData = parameters[1];
return result;
}
private object GetEnemyAt(int index)
{
object enemies = EnemiesProperty.GetValue(_worldComponent);
PropertyInfo itemProperty = enemies.GetType().GetProperty("Item", PublicInstance);
return itemProperty.GetValue(enemies, new object[] { index });
}
private int GetEnemiesCount()
{
object enemies = EnemiesProperty.GetValue(_worldComponent);
PropertyInfo countProperty = enemies.GetType().GetProperty("Count", PublicInstance);
return (int)countProperty.GetValue(enemies);
}
private static object GetField(object target, string fieldName)
{
FieldInfo field = target.GetType().GetField(fieldName, PublicInstance);
return field.GetValue(target);
}
private static void SetField(ref object target, string fieldName, object value)
{
FieldInfo field = target.GetType().GetField(fieldName, PublicInstance);
field.SetValue(target, value);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6da65404acbb4cb3acc082171f76a5a7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7f1f9d2cc3fbc2d4ea5cac4fd488b72f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
{
"name": "Simulation.PlayModeTests",
"references": [],
"optionalUnityReferences": [
"TestAssemblies"
],
"includePlatforms": [],
"excludePlatforms": []
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: efb31cfd979449f6b1f6f2f23e6ad5ce
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,232 @@
using System.Collections;
using System.Reflection;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace Simulation.Tests.PlayMode
{
public class SimulationWorldPlayModeTests
{
private const string GameAssemblyName = "Assembly-CSharp";
private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
private const BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance;
private const BindingFlags NonPublicInstance = BindingFlags.NonPublic | BindingFlags.Instance;
private static readonly System.Type SimulationWorldType =
System.Type.GetType($"Simulation.SimulationWorld, {GameAssemblyName}");
private static readonly System.Type SimulationTickContextType =
System.Type.GetType($"Simulation.SimulationTickContext, {GameAssemblyName}");
private static readonly System.Type EnemySimDataType =
System.Type.GetType($"Simulation.EnemySimData, {GameAssemblyName}");
private static readonly System.Type EnemySeparationSolverProviderType =
System.Type.GetType($"CustomUtility.EnemySeparationSolverProvider, {GameAssemblyName}");
private static readonly MethodInfo UpsertEnemyMethod =
SimulationWorldType?.GetMethod("UpsertEnemy", NonPublicInstance);
private static readonly MethodInfo RemoveEnemyByEntityIdMethod =
SimulationWorldType?.GetMethod("RemoveEnemyByEntityId", NonPublicInstance);
private static readonly MethodInfo TryGetEnemyDataMethod =
SimulationWorldType?.GetMethod("TryGetEnemyData", NonPublicInstance);
private static readonly MethodInfo TickMethod =
SimulationWorldType?.GetMethod("Tick", PublicInstance);
private static readonly MethodInfo SetUseSimulationMovementMethod =
SimulationWorldType?.GetMethod("SetUseSimulationMovement", PublicInstance);
private static readonly MethodInfo UseGridBucketSolverMethod =
EnemySeparationSolverProviderType?.GetMethod("UseGridBucketSolver", PublicStatic);
private static readonly FieldInfo EntitySyncField =
SimulationWorldType?.GetField("_entitySync", NonPublicInstance);
private static readonly FieldInfo PresentationField =
SimulationWorldType?.GetField("_presentation", NonPublicInstance);
private static readonly PropertyInfo EnemiesProperty =
SimulationWorldType?.GetProperty("Enemies", PublicInstance);
private GameObject _worldGameObject;
private Component _worldComponent;
[UnitySetUp]
public IEnumerator SetUp()
{
Assert.NotNull(SimulationWorldType, "SimulationWorld type lookup failed.");
Assert.NotNull(SimulationTickContextType, "SimulationTickContext type lookup failed.");
Assert.NotNull(EnemySimDataType, "EnemySimData type lookup failed.");
Assert.NotNull(EnemySeparationSolverProviderType, "EnemySeparationSolverProvider type lookup failed.");
Assert.NotNull(UpsertEnemyMethod, "UpsertEnemy reflection lookup failed.");
Assert.NotNull(RemoveEnemyByEntityIdMethod, "RemoveEnemyByEntityId reflection lookup failed.");
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
Assert.NotNull(SetUseSimulationMovementMethod, "SetUseSimulationMovement reflection lookup failed.");
Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
Assert.NotNull(EnemiesProperty, "Enemies property reflection lookup failed.");
_worldGameObject = new GameObject("SimulationWorldPlayModeTests");
_worldComponent = _worldGameObject.AddComponent(SimulationWorldType);
// Isolate PlayMode regression to simulation behavior only.
EntitySyncField?.SetValue(_worldComponent, null);
PresentationField?.SetValue(_worldComponent, null);
SetUseSimulationMovementMethod.Invoke(_worldComponent, new object[] { true });
UseGridBucketSolverMethod.Invoke(null, new object[] { 1f });
yield return null;
}
[UnityTearDown]
public IEnumerator TearDown()
{
if (_worldComponent != null)
{
EntitySyncField?.SetValue(_worldComponent, null);
PresentationField?.SetValue(_worldComponent, null);
}
if (_worldGameObject != null)
{
Object.Destroy(_worldGameObject);
}
_worldComponent = null;
_worldGameObject = null;
yield return null;
}
[UnityTest]
public IEnumerator TickEnemies_ChasesPlayer_WhenOutOfAttackRange()
{
UpsertEnemy(CreateEnemy(entityId: 3001, position: Vector3.zero, speed: 2f, attackRange: 1f));
InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: new Vector3(10f, 0f, 0f));
object enemy = GetEnemyAt(0);
Assert.That((int)GetField(enemy, "State"), Is.EqualTo(1));
Vector3 position = (Vector3)GetField(enemy, "Position");
Vector3 forward = (Vector3)GetField(enemy, "Forward");
Assert.That(position.x, Is.EqualTo(2f).Within(0.0001f));
Assert.That(position.z, Is.EqualTo(0f).Within(0.0001f));
Assert.That(forward.x, Is.EqualTo(1f).Within(0.0001f));
yield break;
}
[UnityTest]
public IEnumerator TickEnemies_StopsMovement_WhenInAttackRange()
{
Vector3 startPosition = Vector3.zero;
UpsertEnemy(CreateEnemy(entityId: 3002, position: startPosition, speed: 3f, attackRange: 2f));
InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: new Vector3(1f, 0f, 0f));
object enemy = GetEnemyAt(0);
Assert.That((int)GetField(enemy, "State"), Is.EqualTo(2));
Vector3 position = (Vector3)GetField(enemy, "Position");
Assert.That(position.x, Is.EqualTo(startPosition.x).Within(0.0001f));
Assert.That(position.y, Is.EqualTo(startPosition.y).Within(0.0001f));
Assert.That(position.z, Is.EqualTo(startPosition.z).Within(0.0001f));
yield break;
}
[UnityTest]
public IEnumerator RemoveEnemyByEntityId_RemapIndex_ForMovedEnemy()
{
UpsertEnemy(CreateEnemy(entityId: 3101, position: new Vector3(0f, 0f, 0f), speed: 1f, attackRange: 1f));
UpsertEnemy(CreateEnemy(entityId: 3102, position: new Vector3(2f, 0f, 0f), speed: 1f, attackRange: 1f));
UpsertEnemy(CreateEnemy(entityId: 3103, position: new Vector3(4f, 0f, 0f), speed: 1f, attackRange: 1f));
bool removed = RemoveEnemyByEntityId(3102);
bool removedEntityExists = TryGetEnemyData(3102, out _);
bool movedEntityExists = TryGetEnemyData(3103, out object movedEnemy);
Assert.IsTrue(removed);
Assert.That(GetEnemiesCount(), Is.EqualTo(2));
Assert.IsFalse(removedEntityExists);
Assert.IsTrue(movedEntityExists);
Assert.That((int)GetField(movedEnemy, "EntityId"), Is.EqualTo(3103));
Assert.That((int)GetField(GetEnemyAt(1), "EntityId"), Is.EqualTo(3103));
yield break;
}
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange)
{
object enemy = System.Activator.CreateInstance(EnemySimDataType);
SetField(ref enemy, "EntityId", entityId);
SetField(ref enemy, "Position", position);
SetField(ref enemy, "Forward", Vector3.forward);
SetField(ref enemy, "Rotation", Quaternion.identity);
SetField(ref enemy, "Speed", speed);
SetField(ref enemy, "AttackRange", attackRange);
SetField(ref enemy, "AvoidEnemyOverlap", false);
SetField(ref enemy, "EnemyBodyRadius", 0.45f);
SetField(ref enemy, "SeparationIterations", 1);
SetField(ref enemy, "TargetType", 0);
SetField(ref enemy, "State", 0);
return enemy;
}
private void InvokeTick(float deltaTime, float realDeltaTime, Vector3 playerPosition)
{
object tickContext = System.Activator.CreateInstance(
SimulationTickContextType,
BindingFlags.Public | BindingFlags.Instance,
null,
new object[] { deltaTime, realDeltaTime, playerPosition },
null);
TickMethod.Invoke(_worldComponent, new[] { tickContext });
}
private void UpsertEnemy(object enemy)
{
UpsertEnemyMethod.Invoke(_worldComponent, new[] { enemy });
}
private bool RemoveEnemyByEntityId(int entityId)
{
return (bool)RemoveEnemyByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId });
}
private bool TryGetEnemyData(int entityId, out object enemyData)
{
object boxedDefault = System.Activator.CreateInstance(EnemySimDataType);
object[] parameters = { entityId, boxedDefault };
bool result = (bool)TryGetEnemyDataMethod.Invoke(_worldComponent, parameters);
enemyData = parameters[1];
return result;
}
private object GetEnemyAt(int index)
{
object enemies = EnemiesProperty.GetValue(_worldComponent);
PropertyInfo itemProperty = enemies.GetType().GetProperty("Item", PublicInstance);
return itemProperty.GetValue(enemies, new object[] { index });
}
private int GetEnemiesCount()
{
object enemies = EnemiesProperty.GetValue(_worldComponent);
PropertyInfo countProperty = enemies.GetType().GetProperty("Count", PublicInstance);
return (int)countProperty.GetValue(enemies);
}
private static object GetField(object target, string fieldName)
{
FieldInfo field = target.GetType().GetField(fieldName, PublicInstance);
return field.GetValue(target);
}
private static void SetField(ref object target, string fieldName, object value)
{
FieldInfo field = target.GetType().GetField(fieldName, PublicInstance);
field.SetValue(target, value);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9d1958a6d5ed459cb1809b72ad922479
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: