diff --git a/Assets/GameMain/Scripts/Components/MovementComponent.cs b/Assets/GameMain/Scripts/Components/MovementComponent.cs index 3052135..8b92390 100644 --- a/Assets/GameMain/Scripts/Components/MovementComponent.cs +++ b/Assets/GameMain/Scripts/Components/MovementComponent.cs @@ -2,6 +2,8 @@ using System; using CustomUtility; using Definition.DataStruct; using Definition.Enum; +using Entity; +using Simulation; using UnityEngine; using CustomDebugger; @@ -20,6 +22,9 @@ namespace Components public bool AvoidEnemyOverlap => _avoidEnemyOverlap; public float EnemyBodyRadius => _enemyBodyRadius; public int SeparationIterations => _separationIterations; + public bool IsMoving => _isMoving; + public Vector3 Direction => _direction; + public Transform CachedTransform => _cachedTransform; [SerializeField] private float _speedBase; private StatComponent _statComponent; @@ -42,7 +47,10 @@ namespace Components { _movementStat = _statComponent.GetStat(StatType.MovementSpeed); _movementStatCallback = (modifier, isApply) => + { _statComponent.UpdateStat(_movementStat, modifier, isApply); + SyncToSimulationWorld(); + }; _statComponent.Subscribe(StatType.MovementSpeed, _movementStatCallback); } else @@ -50,15 +58,12 @@ namespace Components _movementStat = new StatProperty(); } - RefreshEnemyRegistration(); + SyncToSimulationWorld(); } public void OnUpdate(float elapseSeconds, float realElapseSeconds) { - if (_isMoving && _cachedTransform != null) - { - Move(elapseSeconds); - } + SyncToSimulationWorld(); } public void OnReset() @@ -81,43 +86,59 @@ namespace Components _statComponent = null; - UnregisterEnemyMover(transformToUnregister); - } - - private void Move(float deltaTime = 0) - { - using (CustomProfilerMarker.Movement_Update.Auto()) + if (transformToUnregister != null) { - if (_cachedTransform == null) return; - - Vector3 displacement = Speed * deltaTime * _direction; - Vector3 nextPosition = _cachedTransform.position + displacement; - if (_avoidEnemyOverlap) - { - nextPosition = EnemySeparationSolverProvider.Resolve( - _cachedTransform, - nextPosition, - _direction, - _separationIterations); - } - - _cachedTransform.position = nextPosition; + var simulationWorld = GameEntry.SimulationWorld; + simulationWorld?.UnregisterPlayerMovement(transformToUnregister); } } - public void SetMove(bool isMoving) => _isMoving = isMoving; - public void SetDirection(Vector3 direction) => _direction = direction; - - private void RefreshEnemyRegistration() + public void SetMove(bool isMoving) { - UnregisterEnemyMover(); - if (!_avoidEnemyOverlap) return; - EnemySeparationSolverProvider.Register(_cachedTransform, _enemyBodyRadius); + _isMoving = isMoving; + SyncToSimulationWorld(); } - private void UnregisterEnemyMover(Transform transform = null) + public void SetDirection(Vector3 direction) { - EnemySeparationSolverProvider.Unregister(transform ?? _cachedTransform); + _direction = direction; + SyncToSimulationWorld(); + } + + private void SyncToSimulationWorld() + { + using (CustomProfilerMarker.Movement_Update.Auto()) + { + if (_cachedTransform == null) + { + return; + } + + SimulationWorld simulationWorld = GameEntry.SimulationWorld; + if (simulationWorld == null) + { + return; + } + + Vector3 direction = _direction; + direction.y = 0f; + if (direction.sqrMagnitude > Mathf.Epsilon) + { + direction.Normalize(); + } + + if (TryGetComponent(out Player _)) + { + simulationWorld.SyncPlayerMovementInput(_cachedTransform, _isMoving, direction, Speed); + return; + } + + if (TryGetComponent(out EnemyBase enemy)) + { + simulationWorld.SyncEnemyMovementInput(enemy.Id, _isMoving, direction, Speed, + _avoidEnemyOverlap, _enemyBodyRadius, _separationIterations); + } + } } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/DebugPanel/RuntimeDebugPanelComponent.cs b/Assets/GameMain/Scripts/CustomComponent/DebugPanel/RuntimeDebugPanelComponent.cs index 5f18aed..05bff40 100644 --- a/Assets/GameMain/Scripts/CustomComponent/DebugPanel/RuntimeDebugPanelComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/DebugPanel/RuntimeDebugPanelComponent.cs @@ -29,7 +29,6 @@ namespace CustomComponent [SerializeField] private bool _showCollisionStats = true; [SerializeField] private bool _showSpawnControls = true; [SerializeField] private bool _showBattleDurationControls = true; - [SerializeField] private bool _showSeparationSolverControls = true; [SerializeField] private bool _showPlayerWeaponControls = true; [SerializeField] private bool _showPlayerHealthControls = true; [SerializeField] private bool _showTips = true; @@ -283,24 +282,6 @@ namespace CustomComponent GUILayout.EndHorizontal(); } - if (_showSeparationSolverControls) - { - GUILayout.Space(4f); - GUILayout.Label($"Enemy Separation Solver: {EnemySeparationSolverProvider.CurrentSolverName}"); - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Use Naive O(N^2)", GUILayout.Height(24f))) - { - EnemySeparationSolverProvider.UseNaiveSolver(); - } - - if (GUILayout.Button("Use Grid Bucket", GUILayout.Height(24f))) - { - EnemySeparationSolverProvider.UseGridBucketSolver(); - } - - GUILayout.EndHorizontal(); - } - if (_showPlayerWeaponControls) { GUILayout.Label( @@ -353,7 +334,6 @@ namespace CustomComponent _showCollisionStats || _showSpawnControls || _showBattleDurationControls || - _showSeparationSolverControls || _showPlayerWeaponControls || _showPlayerHealthControls; } diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyBase.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyBase.cs index d10f4d4..03ae712 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyBase.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyBase.cs @@ -13,7 +13,6 @@ public abstract class EnemyBase : TargetableObject protected bool IsSimulationMovementEnabled() { - var simulationWorld = GameEntry.SimulationWorld; - return simulationWorld != null && simulationWorld.UseSimulationMovement; + return true; } } diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyProjectile.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyProjectile.cs index 5d19702..f3711e1 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyProjectile.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyProjectile.cs @@ -10,9 +10,7 @@ namespace Entity { private EnemyProjectileData _projectileData; private Vector3 _direction = Vector3.forward; - private float _elapsedTime; private bool _isActive; - private bool _isSimulationDriven; private ImpactData _impactData; private Collider[] _cachedColliders; @@ -33,7 +31,6 @@ namespace Entity } _isActive = true; - _elapsedTime = 0f; _impactData = new ImpactData(_projectileData.OwnerCamp, _projectileData.AttackDamage); _direction = _projectileData.Direction; @@ -64,8 +61,7 @@ namespace Entity gameObject.layer = LayerMask.NameToLayer("EnemyWeapon"); } - _isSimulationDriven = IsDrivenBySimulationWorld(); - SetColliderEnabled(!_isSimulationDriven); + SetColliderEnabled(false); } protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) @@ -73,34 +69,12 @@ namespace Entity base.OnUpdate(elapseSeconds, realElapseSeconds); if (!_isActive || _projectileData == null) return; - - bool isSimulationDriven = IsDrivenBySimulationWorld(); - if (isSimulationDriven != _isSimulationDriven) - { - _isSimulationDriven = isSimulationDriven; - SetColliderEnabled(!_isSimulationDriven); - } - - if (_isSimulationDriven) return; - - if (_projectileData.Speed > 0f) - { - CachedTransform.position += _direction * (_projectileData.Speed * elapseSeconds); - } - - _elapsedTime += elapseSeconds; - if (_projectileData.LifeTime > 0f && _elapsedTime >= _projectileData.LifeTime) - { - Expire(); - } } protected override void OnHide(bool isShutdown, object userData) { _isActive = false; _projectileData = null; - _elapsedTime = 0f; - _isSimulationDriven = false; _impactData = default; _direction = Vector3.forward; @@ -114,12 +88,6 @@ namespace Entity GameEntry.Entity.HideEntity(this); } - private static bool IsDrivenBySimulationWorld() - { - var simulationWorld = GameEntry.SimulationWorld; - return simulationWorld != null && simulationWorld.UseSimulationMovement; - } - private void SetColliderEnabled(bool enabled) { _cachedColliders ??= GetComponentsInChildren(true); @@ -137,4 +105,4 @@ namespace Entity } } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/MeleeEnemy.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/MeleeEnemy.cs index 33f1902..9812a2a 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/MeleeEnemy.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/MeleeEnemy.cs @@ -82,13 +82,6 @@ namespace Entity base.OnUpdate(elapseSeconds, realElapseSeconds); UpdateAttackState(elapseSeconds); - - if (IsSimulationMovementEnabled()) - { - return; - } - - _movementComponent.OnUpdate(elapseSeconds, realElapseSeconds); } protected override void OnDead(EntityBase attacker) diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/RemoteEnemy.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/RemoteEnemy.cs index 16051ac..479c223 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/RemoteEnemy.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/RemoteEnemy.cs @@ -95,11 +95,6 @@ namespace Entity if (_target == null) { _movementComponent.SetMove(false); - if (!IsSimulationMovementEnabled()) - { - _movementComponent.OnUpdate(elapseSeconds, realElapseSeconds); - } - return; } @@ -116,11 +111,6 @@ namespace Entity _movementComponent.SetMove(true); _movementComponent.SetDirection(GetTargetDirection()); } - - if (!IsSimulationMovementEnabled()) - { - _movementComponent.OnUpdate(elapseSeconds, realElapseSeconds); - } } protected override void OnHide(bool isShutdown, object userData) diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Player.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Player.cs index 7cdef64..331cbd5 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Player.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Player.cs @@ -212,7 +212,6 @@ namespace Entity _inputComponent.OnUpdate(elapseSeconds, realElapseSeconds); _movementComponent.SetDirection(_inputComponent.Direction); - _movementComponent.OnUpdate(elapseSeconds, realElapseSeconds); _absorbComponent.OnUpdate(elapseSeconds, realElapseSeconds); } diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/TargetSelector/NearestTargetSelector.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/TargetSelector/NearestTargetSelector.cs index 1d72447..b4c7874 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/TargetSelector/NearestTargetSelector.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/TargetSelector/NearestTargetSelector.cs @@ -13,26 +13,7 @@ namespace Entity.Weapon return null; } - if (TrySelectFromSpatialIndex(weapon, maxSqrRange, out EntityBase indexedTarget)) - { - return indexedTarget; - } - - EntityBase target = null; - float minSqrMagnitude = maxSqrRange > 0f ? maxSqrRange : float.MaxValue; - - foreach (var candidate in candidates) - { - if (candidate == null || !candidate.Available) continue; - - float sqrMagnitude = AIUtility.GetSqrMagnitudeXZ(weapon, candidate); - if (sqrMagnitude >= minSqrMagnitude) continue; - - minSqrMagnitude = sqrMagnitude; - target = candidate; - } - - return target; + return TrySelectFromSpatialIndex(weapon, maxSqrRange, out EntityBase indexedTarget) ? indexedTarget : null; } private static bool TrySelectFromSpatialIndex(WeaponBase weapon, float maxSqrRange, out EntityBase target) @@ -44,7 +25,7 @@ namespace Entity.Weapon } var simulationWorld = GameEntry.SimulationWorld; - if (simulationWorld == null || !simulationWorld.UseSimulationMovement) + if (simulationWorld == null) { return false; } @@ -65,4 +46,4 @@ namespace Entity.Weapon return true; } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionRequests.cs b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionRequests.cs index 4b790c6..0e02e2e 100644 --- a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionRequests.cs +++ b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionRequests.cs @@ -36,11 +36,6 @@ namespace Simulation in Vector3 center, float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg, float halfWidth, float halfLength) { - if (!_useSimulationMovement) - { - return false; - } - if (sourceEntityId == 0 || radius <= 0f || maxTargets <= 0) { return false; diff --git a/Assets/GameMain/Scripts/Simulation/Presentation/SimulationWorld.TransformSync.cs b/Assets/GameMain/Scripts/Simulation/Presentation/SimulationWorld.TransformSync.cs index 5217514..11b80ed 100644 --- a/Assets/GameMain/Scripts/Simulation/Presentation/SimulationWorld.TransformSync.cs +++ b/Assets/GameMain/Scripts/Simulation/Presentation/SimulationWorld.TransformSync.cs @@ -16,7 +16,7 @@ namespace Simulation public void OnLateUpdate() { - if (_world == null || !_world.UseSimulationMovement) + if (_world == null) { return; } @@ -104,4 +104,4 @@ namespace Simulation } } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.PlayerMovement.cs b/Assets/GameMain/Scripts/Simulation/SimulationWorld.PlayerMovement.cs new file mode 100644 index 0000000..467ca7b --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.PlayerMovement.cs @@ -0,0 +1,82 @@ +using UnityEngine; + +namespace Simulation +{ + public sealed partial class SimulationWorld + { + private Transform _playerMovementTransform; + private Vector3 _playerMovementPosition; + private Vector3 _playerMovementDirection = Vector3.zero; + private float _playerMovementSpeed; + private bool _playerMovementActive; + + public void SyncPlayerMovementInput(Transform playerTransform, bool isMoving, in Vector3 direction, float speed) + { + if (playerTransform == null) + { + ClearPlayerMovementState(); + return; + } + + if (_playerMovementTransform != playerTransform) + { + _playerMovementTransform = playerTransform; + _playerMovementPosition = playerTransform.position; + } + + Vector3 planarDirection = direction; + planarDirection.y = 0f; + bool hasDirection = planarDirection.sqrMagnitude > Mathf.Epsilon; + if (hasDirection) + { + _playerMovementDirection = planarDirection.normalized; + } + else + { + _playerMovementDirection = Vector3.zero; + } + + _playerMovementActive = isMoving && hasDirection; + _playerMovementSpeed = _playerMovementActive ? Mathf.Max(0f, speed) : 0f; + } + + public void UnregisterPlayerMovement(Transform playerTransform) + { + if (playerTransform == null || _playerMovementTransform != playerTransform) + { + return; + } + + ClearPlayerMovementState(); + } + + private Vector3 ResolvePlayerPositionForTick(in SimulationTickContext context) + { + if (_playerMovementTransform == null) + { + return context.PlayerPosition; + } + + if (!_playerMovementActive || _playerMovementSpeed <= 0f || + _playerMovementDirection.sqrMagnitude <= Mathf.Epsilon || context.DeltaTime <= 0f) + { + _playerMovementPosition = _playerMovementTransform.position; + return _playerMovementPosition; + } + + _playerMovementPosition += _playerMovementDirection * (_playerMovementSpeed * context.DeltaTime); + _playerMovementPosition.y = _playerMovementTransform.position.y; + _playerMovementTransform.position = _playerMovementPosition; + return _playerMovementPosition; + } + + private void ClearPlayerMovementState() + { + _playerMovementTransform = null; + _playerMovementPosition = Vector3.zero; + _playerMovementDirection = Vector3.zero; + _playerMovementSpeed = 0f; + _playerMovementActive = false; + } + } +} diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.PlayerMovement.cs.meta b/Assets/GameMain/Scripts/Simulation/SimulationWorld.PlayerMovement.cs.meta new file mode 100644 index 0000000..a390712 --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.PlayerMovement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 417eac38d86047d096eb9f74647b6cf7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.SimEntityState.cs b/Assets/GameMain/Scripts/Simulation/SimulationWorld.SimEntityState.cs index dd4f0a1..489097a 100644 --- a/Assets/GameMain/Scripts/Simulation/SimulationWorld.SimEntityState.cs +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.SimEntityState.cs @@ -22,6 +22,7 @@ namespace Simulation EnemyBinding.Clear(); ProjectileBinding.Clear(); PickupBinding.Clear(); + ClearPlayerMovementState(); } #endregion @@ -101,6 +102,31 @@ namespace Simulation return true; } + public void SyncEnemyMovementInput(int entityId, bool isMoving, in UnityEngine.Vector3 direction, float speed, + bool avoidEnemyOverlap, float enemyBodyRadius, int separationIterations) + { + if (!EnemyBinding.TryGetSimulationIndex(entityId, out int simulationIndex) || simulationIndex < 0 || + simulationIndex >= _enemies.Count) + { + return; + } + + EnemySimData enemyData = _enemies[simulationIndex]; + enemyData.Speed = isMoving ? UnityEngine.Mathf.Max(0f, speed) : 0f; + enemyData.AvoidEnemyOverlap = avoidEnemyOverlap; + enemyData.EnemyBodyRadius = UnityEngine.Mathf.Max(0.01f, enemyBodyRadius); + enemyData.SeparationIterations = UnityEngine.Mathf.Max(1, separationIterations); + + UnityEngine.Vector3 planarDirection = direction; + planarDirection.y = 0f; + if (planarDirection.sqrMagnitude > UnityEngine.Mathf.Epsilon) + { + enemyData.Forward = planarDirection.normalized; + } + + _enemies[simulationIndex] = enemyData; + } + #endregion #region Projectile Simulation State @@ -219,4 +245,4 @@ namespace Simulation #endregion } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.TargetSelectionSpatialIndex.cs b/Assets/GameMain/Scripts/Simulation/SimulationWorld.TargetSelectionSpatialIndex.cs index 60af9b3..23f3cf7 100644 --- a/Assets/GameMain/Scripts/Simulation/SimulationWorld.TargetSelectionSpatialIndex.cs +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.TargetSelectionSpatialIndex.cs @@ -14,11 +14,6 @@ namespace Simulation return false; } - if (!_useSimulationMovement) - { - return false; - } - BuildEnemyTargetSpatialIndexIfNeeded(); float cellSize = GetTargetSelectionCellSize(); diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs b/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs index a200ee1..3cfa136 100644 --- a/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs @@ -37,9 +37,6 @@ namespace Simulation private const int ProjectileStateActive = 0; private const int ProjectileStateExpired = 1; - [Header("模拟世界全局设置")] [Tooltip("是否启用世界模拟")] [SerializeField] - private bool _useSimulationMovement = true; - private EntitySync _entitySync; private TransformSync _transformSync; private HitPresentation _hitPresentation; @@ -47,7 +44,7 @@ namespace Simulation public IReadOnlyList Enemies => _enemies; public IReadOnlyList Projectiles => _projectiles; public IReadOnlyList Pickups => _pickups; - public bool UseSimulationMovement => _useSimulationMovement; + public bool UseSimulationMovement => true; #region Lifecycle @@ -68,14 +65,12 @@ namespace Simulation public void Tick(in SimulationTickContext context) { - if (!_useSimulationMovement) - { - return; - } - + Vector3 playerPosition = ResolvePlayerPositionForTick(in context); + SimulationTickContext resolvedContext = + new SimulationTickContext(context.DeltaTime, context.RealDeltaTime, playerPosition); using (CustomProfilerMarker.TickEnemies.Auto()) { - TickSimulationPipeline(in context); + TickSimulationPipeline(in resolvedContext); } } diff --git a/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs b/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs index 547a405..d8ca668 100644 --- a/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs +++ b/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs @@ -1,1063 +1,168 @@ -using System; -using System.Collections.Generic; using System.Reflection; +using Components; using NUnit.Framework; using UnityEngine; -using Procedure; -using GameFramework.Fsm; -using GameFramework.Procedure; -using Object = UnityEngine.Object; namespace Simulation.Tests.Editor { public class SimulationWorldTickTests { - private const string GameAssemblyName = "VampireLike"; - private const string RuntimeAssemblyName = "UnityGameFramework.Runtime"; - 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 const BindingFlags NonPublicStatic = BindingFlags.NonPublic | BindingFlags.Static; - - 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 ProjectileSimDataType = - System.Type.GetType($"Simulation.ProjectileSimData, {GameAssemblyName}"); - - private static readonly System.Type PickupSimDataType = - System.Type.GetType($"Simulation.PickupSimData, {GameAssemblyName}"); - - private static readonly System.Type EnemyProjectileType = - System.Type.GetType($"Entity.EnemyProjectile, {GameAssemblyName}"); - - private static readonly System.Type EnemyProjectileDataType = - System.Type.GetType($"Entity.EntityData.EnemyProjectileData, {GameAssemblyName}"); - - private static readonly System.Type CampTypeType = - System.Type.GetType($"Definition.Enum.CampType, {GameAssemblyName}"); - - private static readonly System.Type GameEntryType = - System.Type.GetType($"GameEntry, {GameAssemblyName}"); - - private static readonly System.Type EnemySeparationSolverProviderType = - System.Type.GetType($"CustomUtility.EnemySeparationSolverProvider, {GameAssemblyName}"); - - private static readonly System.Type EnemyManagerComponentType = - System.Type.GetType($"CustomComponent.EnemyManagerComponent, {GameAssemblyName}"); - - private static readonly System.Type PlayerType = - System.Type.GetType($"Entity.Player, {GameAssemblyName}"); - - private static readonly System.Type EntityBaseType = - System.Type.GetType($"Entity.EntityBase, {GameAssemblyName}"); - - private static readonly System.Type HealthComponentType = - System.Type.GetType($"Components.HealthComponent, {GameAssemblyName}"); - - private static readonly System.Type EntityLogicType = - System.Type.GetType($"UnityGameFramework.Runtime.EntityLogic, {RuntimeAssemblyName}"); - - private static readonly System.Type ProcedureComponentType = - System.Type.GetType($"UnityGameFramework.Runtime.ProcedureComponent, {RuntimeAssemblyName}"); - - private static readonly System.Type ProcedureGameType = - System.Type.GetType($"Procedure.ProcedureGame, {GameAssemblyName}"); - - private static readonly System.Type GameStateTypeType = - System.Type.GetType($"Procedure.GameStateType, {GameAssemblyName}"); - - private static readonly System.Type ProcedureManagerType = - System.Type.GetType("GameFramework.Procedure.ProcedureManager, GameFramework"); - - private static readonly System.Type ProcedureManagerInterfaceType = - System.Type.GetType("GameFramework.Procedure.IProcedureManager, GameFramework"); - - private static readonly System.Type FsmOpenGenericType = - System.Type.GetType("GameFramework.Fsm.Fsm`1, GameFramework"); + private static readonly BindingFlags NonPublicInstance = + BindingFlags.Instance | BindingFlags.NonPublic; private static readonly MethodInfo UpsertEnemyMethod = - SimulationWorldType?.GetMethod("UpsertEnemy", NonPublicInstance); - - private static readonly MethodInfo RemoveEnemyByEntityIdMethod = - SimulationWorldType?.GetMethod("RemoveEnemyByEntityId", NonPublicInstance); + typeof(SimulationWorld).GetMethod("UpsertEnemy", NonPublicInstance); private static readonly MethodInfo UpsertProjectileMethod = - SimulationWorldType?.GetMethod("UpsertProjectile", NonPublicInstance); + typeof(SimulationWorld).GetMethod("UpsertProjectile", NonPublicInstance); - private static readonly MethodInfo RemoveProjectileByEntityIdMethod = - SimulationWorldType?.GetMethod("RemoveProjectileByEntityId", NonPublicInstance); - - private static readonly MethodInfo UpsertPickupMethod = - SimulationWorldType?.GetMethod("UpsertPickup", NonPublicInstance); - - private static readonly MethodInfo RemovePickupByEntityIdMethod = - SimulationWorldType?.GetMethod("RemovePickupByEntityId", NonPublicInstance); - - private static readonly MethodInfo TryGetEnemyDataMethod = - SimulationWorldType?.GetMethod("TryGetEnemyData", NonPublicInstance); - - private static readonly MethodInfo TickMethod = - SimulationWorldType?.GetMethod("Tick", PublicInstance); - - private static readonly MethodInfo TryGetNearestEnemyEntityIdMethod = - SimulationWorldType?.GetMethod("TryGetNearestEnemyEntityId", PublicInstance); - - private static readonly MethodInfo TryRequestAreaCollisionMethod = - SimulationWorldType?.GetMethod("TryRequestAreaCollision", PublicInstance); - - private static readonly MethodInfo ClearSimulationStateMethod = - SimulationWorldType?.GetMethod("ClearSimulationState", PublicInstance); - - private static readonly MethodInfo UseGridBucketSolverMethod = - EnemySeparationSolverProviderType?.GetMethod("UseGridBucketSolver", PublicStatic); - - private static readonly FieldInfo EntitySyncField = - SimulationWorldType?.GetField("_entitySync", NonPublicInstance); - - private static readonly FieldInfo TransformSyncField = - SimulationWorldType?.GetField("_transformSync", NonPublicInstance); - - private static readonly FieldInfo HitPresentationField = - SimulationWorldType?.GetField("_hitPresentation", NonPublicInstance); - - private static readonly FieldInfo UseSimulationMovementField = - SimulationWorldType?.GetField("_useSimulationMovement", NonPublicInstance); - - private static readonly PropertyInfo EnemiesProperty = - SimulationWorldType?.GetProperty("Enemies", PublicInstance); - - private static readonly PropertyInfo ProjectilesProperty = - SimulationWorldType?.GetProperty("Projectiles", PublicInstance); - - private static readonly PropertyInfo PickupsProperty = - SimulationWorldType?.GetProperty("Pickups", PublicInstance); - - private static readonly PropertyInfo CollisionCandidateCountProperty = - SimulationWorldType?.GetProperty("CollisionCandidateCount", PublicInstance); - - private static readonly PropertyInfo UseSimulationMovementProperty = - SimulationWorldType?.GetProperty("UseSimulationMovement", PublicInstance); - - private static readonly PropertyInfo LastResolvedAreaHitCountProperty = - SimulationWorldType?.GetProperty("LastResolvedAreaHitCount", PublicInstance); - - private static readonly FieldInfo CollisionQueryInputsField = - SimulationWorldType?.GetField("_collisionQueryInputs", NonPublicInstance); - - private static readonly FieldInfo AreaCollisionRequestsField = - SimulationWorldType?.GetField("_areaCollisionRequests", NonPublicInstance); - - private static readonly MethodInfo EnemyProjectileOnUpdateMethod = - EnemyProjectileType?.GetMethod("OnUpdate", NonPublicInstance); - - private static readonly FieldInfo EnemyProjectileDataField = - EnemyProjectileType?.GetField("_projectileData", NonPublicInstance); - - private static readonly FieldInfo EnemyProjectileIsActiveField = - EnemyProjectileType?.GetField("_isActive", NonPublicInstance); - - private static readonly FieldInfo EnemyProjectileIsSimulationDrivenField = - EnemyProjectileType?.GetField("_isSimulationDriven", NonPublicInstance); - - private static readonly PropertyInfo GameEntrySimulationWorldProperty = - GameEntryType?.GetProperty("SimulationWorld", PublicStatic); - - private static readonly MethodInfo GameEntryGetSimulationWorldMethod = - GameEntrySimulationWorldProperty?.GetGetMethod(true); - - private static readonly MethodInfo GameEntrySetSimulationWorldMethod = - GameEntrySimulationWorldProperty?.GetSetMethod(true); - - private static readonly PropertyInfo GameEntryEnemyManagerProperty = - GameEntryType?.GetProperty("EnemyManager", PublicStatic); - - private static readonly MethodInfo GameEntryGetEnemyManagerMethod = - GameEntryEnemyManagerProperty?.GetGetMethod(true); - - private static readonly MethodInfo GameEntrySetEnemyManagerMethod = - GameEntryEnemyManagerProperty?.GetSetMethod(true); - - private static readonly PropertyInfo GameEntryProcedureProperty = - GameEntryType?.GetProperty("Procedure", PublicStatic); - - private static readonly MethodInfo GameEntryGetProcedureMethod = - GameEntryProcedureProperty?.GetGetMethod(true); - - private static readonly MethodInfo GameEntrySetProcedureMethod = - GameEntryProcedureProperty?.GetSetMethod(true); - - private static readonly MethodInfo HealthComponentOnInitMethod = - HealthComponentType?.GetMethod("OnInit", PublicInstance); - - private static readonly FieldInfo ProjectileMaxDistanceFromPlayerField = - SimulationWorldType?.GetField("_projectileMaxDistanceFromPlayer", NonPublicInstance); - - private static readonly FieldInfo ProjectileMaxVerticalOffsetFromPlayerField = - SimulationWorldType?.GetField("_projectileMaxVerticalOffsetFromPlayer", NonPublicInstance); - - private GameObject _worldGameObject; - private Component _worldComponent; + private GameObject _worldObject; + private SimulationWorld _world; [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(ProjectileSimDataType, "ProjectileSimData type lookup failed."); - Assert.NotNull(PickupSimDataType, "PickupSimData type lookup failed."); - Assert.NotNull(EnemyProjectileType, "EnemyProjectile type lookup failed."); - Assert.NotNull(EnemyProjectileDataType, "EnemyProjectileData type lookup failed."); - Assert.NotNull(CampTypeType, "CampType type lookup failed."); - Assert.NotNull(GameEntryType, "GameEntry type lookup failed."); - Assert.NotNull(EnemySeparationSolverProviderType, "EnemySeparationSolverProvider type lookup failed."); - Assert.NotNull(EnemyManagerComponentType, "EnemyManagerComponent type lookup failed."); - Assert.NotNull(PlayerType, "Player type lookup failed."); - Assert.NotNull(EntityBaseType, "EntityBase type lookup failed."); - Assert.NotNull(HealthComponentType, "HealthComponent type lookup failed."); - Assert.NotNull(EntityLogicType, "EntityLogic type lookup failed."); - Assert.NotNull(ProcedureComponentType, "ProcedureComponent type lookup failed."); - Assert.NotNull(ProcedureGameType, "ProcedureGame type lookup failed."); - Assert.NotNull(GameStateTypeType, "GameStateType type lookup failed."); - Assert.NotNull(ProcedureManagerType, "ProcedureManager type lookup failed."); - Assert.NotNull(ProcedureManagerInterfaceType, "IProcedureManager type lookup failed."); - Assert.NotNull(FsmOpenGenericType, "Fsm`1 type lookup failed."); - Assert.NotNull(UpsertEnemyMethod, "UpsertEnemy reflection lookup failed."); - Assert.NotNull(RemoveEnemyByEntityIdMethod, "RemoveEnemyByEntityId reflection lookup failed."); - Assert.NotNull(UpsertProjectileMethod, "UpsertProjectile reflection lookup failed."); - Assert.NotNull(RemoveProjectileByEntityIdMethod, "RemoveProjectileByEntityId reflection lookup failed."); - Assert.NotNull(UpsertPickupMethod, "UpsertPickup reflection lookup failed."); - Assert.NotNull(RemovePickupByEntityIdMethod, "RemovePickupByEntityId reflection lookup failed."); - Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed."); - Assert.NotNull(TickMethod, "Tick reflection lookup failed."); - Assert.NotNull(TryGetNearestEnemyEntityIdMethod, "TryGetNearestEnemyEntityId reflection lookup failed."); - Assert.NotNull(TryRequestAreaCollisionMethod, "TryRequestAreaCollision reflection lookup failed."); - Assert.NotNull(ClearSimulationStateMethod, "ClearSimulationState reflection lookup failed."); - Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed."); - Assert.NotNull(EnemiesProperty, "Enemies property reflection lookup failed."); - Assert.NotNull(ProjectilesProperty, "Projectiles property reflection lookup failed."); - Assert.NotNull(PickupsProperty, "Pickups property reflection lookup failed."); - Assert.NotNull(CollisionCandidateCountProperty, "CollisionCandidateCount property reflection lookup failed."); - Assert.NotNull(UseSimulationMovementProperty, "UseSimulationMovement property reflection lookup failed."); - Assert.NotNull(UseSimulationMovementField, "_useSimulationMovement field reflection lookup failed."); - Assert.NotNull(LastResolvedAreaHitCountProperty, "LastResolvedAreaHitCount property reflection lookup failed."); - Assert.NotNull(CollisionQueryInputsField, "Collision query inputs field reflection lookup failed."); - Assert.NotNull(AreaCollisionRequestsField, "Area collision requests field reflection lookup failed."); - Assert.NotNull(ProjectileMaxDistanceFromPlayerField, - "Projectile max distance field reflection lookup failed."); - Assert.NotNull(ProjectileMaxVerticalOffsetFromPlayerField, - "Projectile max vertical offset field reflection lookup failed."); - Assert.NotNull(EnemyProjectileOnUpdateMethod, "EnemyProjectile.OnUpdate reflection lookup failed."); - Assert.NotNull(EnemyProjectileDataField, "EnemyProjectile _projectileData reflection lookup failed."); - Assert.NotNull(EnemyProjectileIsActiveField, "EnemyProjectile _isActive reflection lookup failed."); - Assert.NotNull(EnemyProjectileIsSimulationDrivenField, - "EnemyProjectile _isSimulationDriven reflection lookup failed."); - Assert.NotNull(GameEntrySimulationWorldProperty, "GameEntry.SimulationWorld property lookup failed."); - Assert.NotNull(GameEntryGetSimulationWorldMethod, - "GameEntry.SimulationWorld getter reflection lookup failed."); - Assert.NotNull(GameEntrySetSimulationWorldMethod, - "GameEntry.SimulationWorld setter reflection lookup failed."); - Assert.NotNull(GameEntryEnemyManagerProperty, "GameEntry.EnemyManager property lookup failed."); - Assert.NotNull(GameEntryGetEnemyManagerMethod, "GameEntry.EnemyManager getter reflection lookup failed."); - Assert.NotNull(GameEntrySetEnemyManagerMethod, "GameEntry.EnemyManager setter reflection lookup failed."); - Assert.NotNull(GameEntryProcedureProperty, "GameEntry.Procedure property lookup failed."); - Assert.NotNull(GameEntryGetProcedureMethod, "GameEntry.Procedure getter reflection lookup failed."); - Assert.NotNull(GameEntrySetProcedureMethod, "GameEntry.Procedure setter reflection lookup failed."); - Assert.NotNull(HealthComponentOnInitMethod, "HealthComponent.OnInit reflection lookup failed."); - - _worldGameObject = new GameObject("SimulationWorldTickTests"); - _worldComponent = _worldGameObject.AddComponent(SimulationWorldType); - SetUseSimulationMovement(true); - UseGridBucketSolverMethod.Invoke(null, new object[] { 1f }); + _worldObject = new GameObject("SimulationWorldTests"); + _world = _worldObject.AddComponent(); } [TearDown] public void TearDown() { - if (_worldComponent != null) + if (_worldObject != null) { - EntitySyncField?.SetValue(_worldComponent, null); - TransformSyncField?.SetValue(_worldComponent, null); - HitPresentationField?.SetValue(_worldComponent, null); + Object.DestroyImmediate(_worldObject); } - - if (_worldGameObject != null) - { - Object.DestroyImmediate(_worldGameObject); - } - - _worldComponent = null; - _worldGameObject = null; } [Test] - public void TickEnemies_ChasesPlayer_WhenOutOfAttackRange() + public void MovementComponent_OnUpdate_DoesNotMoveTransformDirectly() { - 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)); - } - - [Test] - public void TickEnemies_ChasesPlayer_WhenJobSimulationChannelEnabled() - { - UpsertEnemy(CreateEnemy(entityId: 1101, 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_MatchesOutput_AfterClearSimulationState() - { - UpsertEnemy(CreateEnemy(entityId: 1151, position: Vector3.zero, speed: 2f, attackRange: 1f)); - InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: new Vector3(10f, 0f, 0f)); - - object nonBurstEnemy = GetEnemyAt(0); - int nonBurstState = (int)GetField(nonBurstEnemy, "State"); - Vector3 nonBurstPosition = (Vector3)GetField(nonBurstEnemy, "Position"); - Vector3 nonBurstForward = (Vector3)GetField(nonBurstEnemy, "Forward"); - - ClearSimulationStateMethod.Invoke(_worldComponent, null); - - SetUseSimulationMovement(true); - UpsertEnemy(CreateEnemy(entityId: 1151, position: Vector3.zero, speed: 2f, attackRange: 1f)); - InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: new Vector3(10f, 0f, 0f)); - - object burstEnemy = GetEnemyAt(0); - int burstState = (int)GetField(burstEnemy, "State"); - Vector3 burstPosition = (Vector3)GetField(burstEnemy, "Position"); - Vector3 burstForward = (Vector3)GetField(burstEnemy, "Forward"); - - Assert.That(burstState, Is.EqualTo(nonBurstState)); - Assert.That((burstPosition - nonBurstPosition).sqrMagnitude, Is.LessThanOrEqualTo(1e-8f)); - Assert.That((burstForward - nonBurstForward).sqrMagnitude, Is.LessThanOrEqualTo(1e-8f)); - } - - [Test] - public void TryGetNearestEnemyEntityId_SelectsNearestBucketCandidate_WhenJobSimulationEnabled() - { - UpsertEnemy(CreateEnemy(entityId: 1201, position: new Vector3(1f, 0f, 0f), speed: 0f, attackRange: 1f)); - UpsertEnemy(CreateEnemy(entityId: 1202, position: new Vector3(6f, 0f, 0f), speed: 0f, attackRange: 1f)); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - - object[] parameters = { Vector3.zero, 100f, 0 }; - bool found = (bool)TryGetNearestEnemyEntityIdMethod.Invoke(_worldComponent, parameters); - int nearestEntityId = (int)parameters[2]; - - Assert.IsTrue(found); - Assert.That(nearestEntityId, Is.EqualTo(1201)); - } - - [Test] - public void TickEnemies_SeparatesOverlappedEnemies_WhenJobSimulationEnabled() - { - UpsertEnemy(CreateEnemy(entityId: 1301, position: new Vector3(0f, 0f, 0f), speed: 1f, attackRange: 0.1f, - avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 2)); - UpsertEnemy(CreateEnemy(entityId: 1302, position: new Vector3(0.1f, 0f, 0f), speed: 1f, attackRange: 0.1f, - avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 2)); - - InvokeTick(deltaTime: 0.1f, realDeltaTime: 0.1f, playerPosition: new Vector3(10f, 0f, 0f)); - - object enemyA = GetEnemyAt(0); - object enemyB = GetEnemyAt(1); - Vector3 posA = (Vector3)GetField(enemyA, "Position"); - Vector3 posB = (Vector3)GetField(enemyB, "Position"); - posA.y = 0f; - posB.y = 0f; - float distance = Vector3.Distance(posA, posB); - Assert.That(distance, Is.GreaterThanOrEqualTo(0.89f)); - } - - [Test] - public void TickEnemies_SeparatesOverlappedEnemies_WhenPlayerIsStaticAndInRange() - { - UpsertEnemy(CreateEnemy(entityId: 1311, position: new Vector3(0f, 0f, 0f), speed: 1f, attackRange: 10f, - avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 3)); - UpsertEnemy(CreateEnemy(entityId: 1312, position: new Vector3(0.05f, 0f, 0f), speed: 1f, attackRange: 10f, - avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 3)); - - InvokeTick(deltaTime: 0.1f, realDeltaTime: 0.1f, playerPosition: Vector3.zero); - - object enemyA = GetEnemyAt(0); - object enemyB = GetEnemyAt(1); - Assert.That((int)GetField(enemyA, "State"), Is.EqualTo(2)); - Assert.That((int)GetField(enemyB, "State"), Is.EqualTo(2)); - - Vector3 posA = (Vector3)GetField(enemyA, "Position"); - Vector3 posB = (Vector3)GetField(enemyB, "Position"); - posA.y = 0f; - posB.y = 0f; - float distance = Vector3.Distance(posA, posB); - Assert.That(distance, Is.GreaterThanOrEqualTo(0.5f)); - } - - [Test] - public void TickProjectiles_MovesAndUpdatesLifetime_WhenJobSimulationEnabled() - { - UpsertProjectile(CreateProjectile(entityId: 5101, position: Vector3.zero, forward: Vector3.right, - velocity: new Vector3(2f, 0f, 0f), speed: 0f, lifeTime: 2f, age: 0f, active: true, - remainingLifetime: 2f, state: 0)); - - InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero); - - Assert.That(GetProjectilesCount(), Is.EqualTo(1)); - object projectile = GetProjectileAt(0); - Vector3 position = (Vector3)GetField(projectile, "Position"); - float age = (float)GetField(projectile, "Age"); - float remainingLifetime = (float)GetField(projectile, "RemainingLifetime"); - bool active = (bool)GetField(projectile, "Active"); - - Assert.That(position.x, Is.EqualTo(1f).Within(0.0001f)); - Assert.That(age, Is.EqualTo(0.5f).Within(0.0001f)); - Assert.That(remainingLifetime, Is.EqualTo(1.5f).Within(0.0001f)); - Assert.IsTrue(active); - } - - [Test] - public void TickProjectiles_ContinuesFromLatestState_AcrossConsecutiveTicks() - { - UpsertProjectile(CreateProjectile(entityId: 5110, position: Vector3.zero, forward: Vector3.right, - velocity: new Vector3(2f, 0f, 0f), speed: 0f, lifeTime: 5f, age: 0f, active: true, - remainingLifetime: 5f, state: 0)); - - InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero); - object afterJobEnabled = GetProjectileAt(0); - Vector3 positionAfterJobEnabled = (Vector3)GetField(afterJobEnabled, "Position"); - float ageAfterJobEnabled = (float)GetField(afterJobEnabled, "Age"); - Assert.That(positionAfterJobEnabled.x, Is.EqualTo(1f).Within(0.0001f)); - Assert.That(ageAfterJobEnabled, Is.EqualTo(0.5f).Within(0.0001f)); - - InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero); - object afterSecondTick = GetProjectileAt(0); - Vector3 positionAfterSecondTick = (Vector3)GetField(afterSecondTick, "Position"); - float ageAfterSecondTick = (float)GetField(afterSecondTick, "Age"); - float remainingLifetimeAfterSecondTick = (float)GetField(afterSecondTick, "RemainingLifetime"); - bool activeAfterSecondTick = (bool)GetField(afterSecondTick, "Active"); - - Assert.That(positionAfterSecondTick.x, Is.EqualTo(2f).Within(0.0001f)); - Assert.That(ageAfterSecondTick, Is.EqualTo(1f).Within(0.0001f)); - Assert.That(remainingLifetimeAfterSecondTick, Is.EqualTo(4f).Within(0.0001f)); - Assert.IsTrue(activeAfterSecondTick); - } - - [Test] - public void EnemyProjectile_TogglesCollider_WhenSimulationMovementSwitchesAtRuntime() - { - SetUseSimulationMovement(false); - - GameObject projectileObject = new GameObject("EnemyProjectileColliderToggleEditMode"); + GameObject moverObject = new GameObject("Mover"); try { - Component projectileComponent = projectileObject.AddComponent(EnemyProjectileType); - Collider projectileCollider = projectileObject.AddComponent(); - projectileCollider.enabled = true; + MovementComponent movement = moverObject.AddComponent(); + moverObject.transform.position = new Vector3(1f, 0f, 2f); - object previousSimulationWorld = GetGameEntrySimulationWorld(); - SetGameEntrySimulationWorld(_worldComponent); - try - { - object neutralCamp = System.Enum.Parse(CampTypeType, "Neutral"); - object projectileData = System.Activator.CreateInstance( - EnemyProjectileDataType, - BindingFlags.Public | BindingFlags.Instance, - null, - new object[] { 7001, 1, neutralCamp, 1, 0f, 10f, Vector3.forward }, - null); + movement.OnInit(5f, moverObject.transform); + movement.SetMove(true); + movement.SetDirection(Vector3.right); - EnemyProjectileDataField.SetValue(projectileComponent, projectileData); - EnemyProjectileIsActiveField.SetValue(projectileComponent, true); - EnemyProjectileIsSimulationDrivenField.SetValue(projectileComponent, false); + movement.OnUpdate(1f, 1f); - InvokeEnemyProjectileUpdate(projectileComponent, 0.016f, 0.016f); - Assert.IsTrue(projectileCollider.enabled); - - SetUseSimulationMovement(true); - InvokeEnemyProjectileUpdate(projectileComponent, 0.016f, 0.016f); - Assert.IsFalse(projectileCollider.enabled); - - SetUseSimulationMovement(false); - InvokeEnemyProjectileUpdate(projectileComponent, 0.016f, 0.016f); - Assert.IsTrue(projectileCollider.enabled); - } - finally - { - SetGameEntrySimulationWorld(previousSimulationWorld); - } + Assert.That(moverObject.transform.position.x, Is.EqualTo(1f).Within(0.0001f)); + Assert.That(moverObject.transform.position.y, Is.EqualTo(0f).Within(0.0001f)); + Assert.That(moverObject.transform.position.z, Is.EqualTo(2f).Within(0.0001f)); } finally { - Object.DestroyImmediate(projectileObject); + Object.DestroyImmediate(moverObject); } } [Test] - public void RemoveProjectileByEntityId_RemapIndex_ForMovedProjectile() + public void Tick_AdvancesEnemyAndUpdatesNearestTargetQuery() { - UpsertProjectile(CreateProjectile(entityId: 5105, position: new Vector3(0f, 0f, 0f), - forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true, - remainingLifetime: 3f, state: 0)); - UpsertProjectile(CreateProjectile(entityId: 5106, position: new Vector3(1f, 0f, 0f), - forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true, - remainingLifetime: 3f, state: 0)); - UpsertProjectile(CreateProjectile(entityId: 5107, position: new Vector3(2f, 0f, 0f), - forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true, - remainingLifetime: 3f, state: 0)); - - bool removed = RemoveProjectileByEntityId(5106); - bool removedMoved = RemoveProjectileByEntityId(5107); - - Assert.IsTrue(removed); - Assert.That(GetProjectilesCount(), Is.EqualTo(1)); - Assert.IsTrue(removedMoved); - Assert.That((int)GetField(GetProjectileAt(0), "EntityId"), Is.EqualTo(5105)); - } - - [Test] - public void TickProjectiles_RecyclesWhenExceedingPlayerDistance_WhenJobSimulationEnabled() - { - ProjectileMaxDistanceFromPlayerField.SetValue(_worldComponent, 5f); - ProjectileMaxVerticalOffsetFromPlayerField.SetValue(_worldComponent, 1000f); - UpsertProjectile(CreateProjectile(entityId: 5108, position: new Vector3(6f, 0f, 0f), - forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true, - remainingLifetime: 3f, state: 0)); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - - Assert.That(GetProjectilesCount(), Is.EqualTo(0)); - } - - [Test] - public void TickProjectiles_RecyclesWhenExceedingVerticalOffset_WhenJobSimulationEnabled() - { - ProjectileMaxDistanceFromPlayerField.SetValue(_worldComponent, 0f); - ProjectileMaxVerticalOffsetFromPlayerField.SetValue(_worldComponent, 1f); - UpsertProjectile(CreateProjectile(entityId: 5109, position: new Vector3(0f, 2f, 0f), - forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true, - remainingLifetime: 3f, state: 0)); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - - Assert.That(GetProjectilesCount(), Is.EqualTo(0)); - } - - [Test] - public void TickProjectiles_RecyclesExpiredProjectile_WhenJobSimulationEnabled() - { - UpsertProjectile(CreateProjectile(entityId: 5102, position: Vector3.zero, forward: Vector3.forward, - velocity: Vector3.zero, speed: 0f, lifeTime: 1f, age: 0.95f, active: true, remainingLifetime: 0.05f, - state: 0)); - - InvokeTick(deltaTime: 0.1f, realDeltaTime: 0.1f, playerPosition: Vector3.zero); - - Assert.That(GetProjectilesCount(), Is.EqualTo(0)); - } - - [Test] - public void TickProjectiles_BuildsCollisionCandidatesAgainstEnemies_WhenJobSimulationEnabled() - { - UpsertEnemy(CreateEnemy(entityId: 5201, position: Vector3.zero, speed: 0f, attackRange: 1f)); - UpsertProjectile(CreateProjectile(entityId: 5202, position: Vector3.zero, forward: Vector3.forward, - velocity: Vector3.zero, speed: 0f, lifeTime: 2f, age: 0f, active: true, remainingLifetime: 2f, - state: 0)); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - - Assert.That(GetCollisionCandidateCount(), Is.GreaterThan(0)); - } - - [Test] - public void TickProjectiles_BuildsCollisionCandidates_WithLatestEnemyMovement_WhenJobSimulationEnabled() - { - UpsertEnemy(CreateEnemy(entityId: 5211, position: new Vector3(2f, 0f, 0f), speed: 1f, attackRange: 0.1f)); - UpsertProjectile(CreateProjectile(entityId: 5212, position: new Vector3(1f, 0f, 0f), - forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 2f, age: 0f, active: true, - remainingLifetime: 2f, state: 0)); - - InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: Vector3.zero); - - Assert.That(GetCollisionCandidateCount(), Is.GreaterThan(0)); - } - - [Test] - public void TickProjectiles_ExpiresAfterCollisionCandidateConsumed_WhenJobSimulationEnabled() - { - UpsertEnemy(CreateEnemy(entityId: 5203, position: Vector3.zero, speed: 0f, attackRange: 1f)); - UpsertProjectile(CreateProjectile(entityId: 5204, position: Vector3.zero, forward: Vector3.forward, - velocity: Vector3.zero, speed: 0f, lifeTime: 10f, age: 0f, active: true, remainingLifetime: 10f, - state: 0)); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - - Assert.That(GetProjectilesCount(), Is.EqualTo(0)); - } - - [Test] - public void TickProjectiles_LimitsCandidatesToMaxTargets_IncludingPlayerCandidate() - { - object previousEnemyManager = GetGameEntryEnemyManager(); - GameObject enemyManagerObject = new GameObject("EnemyManagerMaxTargetsEditMode"); - GameObject playerObject = new GameObject("PlayerTargetMaxTargetsEditMode"); - try + UpsertEnemy(new EnemySimData { - Component enemyManager = enemyManagerObject.AddComponent(EnemyManagerComponentType); - Component player = playerObject.AddComponent(PlayerType); - Component healthComponent = playerObject.AddComponent(HealthComponentType); - HealthComponentOnInitMethod.Invoke(healthComponent, new object[] { 100, null }); - SetPrivateField(player, "_healthComponent", healthComponent); - SetPrivateField(player, "m_CachedTransform", playerObject.transform); - SetPrivateField(player, "m_Available", true); + EntityId = 1001, + Position = new Vector3(0f, 0f, 0f), + Forward = Vector3.forward, + Rotation = Quaternion.identity, + Speed = 3f, + AttackRange = 0.5f, + AvoidEnemyOverlap = true, + EnemyBodyRadius = 0.45f, + SeparationIterations = 2 + }); - object enemyById = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(typeof(int), EntityBaseType)); - enemyById.GetType().GetMethod("Add")?.Invoke(enemyById, new object[] { -1, player }); - object enemies = Activator.CreateInstance(typeof(List<>).MakeGenericType(EntityBaseType)); - enemies.GetType().GetMethod("Add")?.Invoke(enemies, new object[] { player }); - SetPrivateField(enemyManager, "_enemyById", enemyById); - SetPrivateField(enemyManager, "_enemies", enemies); - SetGameEntryEnemyManager(enemyManager); + _world.Tick(new SimulationTickContext(1f, 1f, new Vector3(10f, 0f, 0f))); - UpsertEnemy(CreateEnemy(entityId: 5221, position: Vector3.zero, speed: 0f, attackRange: 1f)); - UpsertProjectile(CreateProjectile(entityId: 5222, position: Vector3.zero, forward: Vector3.forward, - velocity: Vector3.zero, speed: 0f, lifeTime: 1f, age: 0f, active: true, remainingLifetime: 1f, - state: 0)); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - - Assert.That(GetCollisionCandidateCount(), Is.EqualTo(1)); - } - finally - { - SetGameEntryEnemyManager(previousEnemyManager); - Object.DestroyImmediate(enemyManagerObject); - Object.DestroyImmediate(playerObject); - } + Assert.That(_world.Enemies.Count, Is.EqualTo(1)); + EnemySimData enemy = _world.Enemies[0]; + Assert.That(enemy.State, Is.EqualTo(1)); + Assert.That(enemy.Position.x, Is.EqualTo(3f).Within(0.001f)); + Assert.That(_world.TryGetNearestEnemyEntityId(new Vector3(3.2f, 0f, 0f), 1f, out int enemyEntityId), + Is.True); + Assert.That(enemyEntityId, Is.EqualTo(1001)); } [Test] - public void TryRequestAreaCollision_ReturnsFalse_WhenSimulationMovementDisabled() + public void SyncEnemyMovementInput_DisablesEnemyMovementOnSimulationPath() { - SetUseSimulationMovement(false); - object[] requestArgs = { 5230, 5230, Vector3.zero, 1f, 1 }; - bool requestResult = (bool)TryRequestAreaCollisionMethod.Invoke(_worldComponent, requestArgs); + UpsertEnemy(new EnemySimData + { + EntityId = 1002, + Position = Vector3.zero, + Forward = Vector3.forward, + Rotation = Quaternion.identity, + Speed = 4f, + AttackRange = 0.5f, + AvoidEnemyOverlap = true, + EnemyBodyRadius = 0.45f, + SeparationIterations = 2 + }); - Assert.IsFalse(requestResult); - Assert.IsFalse((bool)UseSimulationMovementProperty.GetValue(_worldComponent)); + _world.SyncEnemyMovementInput(1002, false, Vector3.left, 4f, true, 0.45f, 2); + _world.Tick(new SimulationTickContext(1f, 1f, new Vector3(10f, 0f, 0f))); + + EnemySimData enemy = _world.Enemies[0]; + Assert.That(enemy.State, Is.EqualTo(0)); + Assert.That(enemy.Position.x, Is.EqualTo(0f).Within(0.0001f)); + Assert.That(enemy.Position.y, Is.EqualTo(0f).Within(0.0001f)); + Assert.That(enemy.Position.z, Is.EqualTo(0f).Within(0.0001f)); } [Test] - public void EnqueueAreaQuery_CapturesInactiveSourceSnapshot_WhenSourceEntityUnavailable() + public void Tick_ExpiresProjectileThroughSimulationLifetime() { - UpsertEnemy(CreateEnemy(entityId: 5231, position: Vector3.zero, speed: 0f, attackRange: 1f)); + UpsertProjectile(new ProjectileSimData + { + EntityId = 2001, + OwnerEntityId = 1001, + Position = Vector3.zero, + Forward = Vector3.right, + Velocity = Vector3.right * 4f, + Speed = 4f, + LifeTime = 0.25f, + Age = 0f, + Active = true, + RemainingLifetime = 0.25f, + State = 0 + }); - object[] enqueueArgs = { 99999, 99999, Vector3.zero, 1f, 1 }; - bool enqueueResult = (bool)TryRequestAreaCollisionMethod.Invoke(_worldComponent, enqueueArgs); - Assert.IsTrue(enqueueResult); + _world.Tick(new SimulationTickContext(0.5f, 0.5f, Vector3.zero)); - object areaCollisionRequests = AreaCollisionRequestsField.GetValue(_worldComponent); - Assert.NotNull(areaCollisionRequests); - PropertyInfo requestCountProperty = areaCollisionRequests.GetType().GetProperty("Count", PublicInstance); - int requestCount = (int)requestCountProperty.GetValue(areaCollisionRequests); - Assert.That(requestCount, Is.GreaterThan(0)); - - PropertyInfo requestItemProperty = areaCollisionRequests.GetType().GetProperty("Item", PublicInstance); - object firstRequest = requestItemProperty.GetValue(areaCollisionRequests, new object[] { 0 }); - FieldInfo requestSnapshotField = - firstRequest.GetType().GetField("SourceWasActiveAtQueryTime", PublicInstance); - Assert.NotNull(requestSnapshotField); - bool requestSnapshot = (bool)requestSnapshotField.GetValue(firstRequest); - Assert.IsFalse(requestSnapshot); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - Assert.That(GetLastResolvedAreaHitCount(), Is.EqualTo(0)); + Assert.That(_world.Projectiles.Count, Is.EqualTo(1)); + ProjectileSimData projectile = _world.Projectiles[0]; + Assert.That(projectile.Active, Is.False); + Assert.That(projectile.State, Is.EqualTo(1)); + Assert.That(projectile.Age, Is.GreaterThanOrEqualTo(projectile.LifeTime)); } [Test] - public void ProcedureGame_TransitionsBattleToLevelUpShopAndBackToBattle() + public void ClearSimulationState_ClearsPublicStateContainers() { - var procedureGame = (ProcedureGame)Activator.CreateInstance(ProcedureGameType); - GameObject playerObject = new GameObject("ProcedureGameTransitionPlayer"); - try - { - var player = playerObject.AddComponent(PlayerType); - Assert.NotNull(player); - PlayerType.GetProperty("PendingLevelPoints", PublicInstance)?.SetValue(player, 1); - ProcedureGameType.GetField("Player", PublicInstance)?.SetValue(procedureGame, player); + UpsertEnemy(new EnemySimData { EntityId = 1, Position = Vector3.zero, Forward = Vector3.forward }); + UpsertProjectile(new ProjectileSimData { EntityId = 2, Active = true, State = 0 }); - var battleState = new TrackingGameState(GameStateType.Battle); - var levelUpState = new TrackingGameState(GameStateType.LevelUp); - var shopState = new TrackingGameState(GameStateType.Shop); - var gameStates = new Dictionary - { - { GameStateType.Battle, battleState }, - { GameStateType.LevelUp, levelUpState }, - { GameStateType.Shop, shopState }, - }; + _world.ClearSimulationState(); - SetPrivateField(procedureGame, "_gameStates", gameStates); - SetPrivateField(procedureGame, "_currentGameState", GameStateType.Battle); - SetPrivateField(procedureGame, "_procedureOwner", null); - - procedureGame.BattleToShopOrLevelUp(); - Assert.That(procedureGame.CurrentLevel, Is.EqualTo(2)); - Assert.That(procedureGame.CurrentGameStateType, Is.EqualTo(GameStateType.LevelUp)); - Assert.That(battleState.LeaveCount, Is.EqualTo(1)); - Assert.That(levelUpState.EnterCount, Is.EqualTo(1)); - - PlayerType.GetProperty("PendingLevelPoints", PublicInstance)?.SetValue(player, 0); - procedureGame.LevelUpToShop(); - Assert.That(procedureGame.CurrentGameStateType, Is.EqualTo(GameStateType.Shop)); - Assert.That(levelUpState.LeaveCount, Is.EqualTo(1)); - Assert.That(shopState.EnterCount, Is.EqualTo(1)); - - procedureGame.ShopToBattle(); - Assert.That(procedureGame.CurrentGameStateType, Is.EqualTo(GameStateType.Battle)); - Assert.That(shopState.LeaveCount, Is.EqualTo(1)); - Assert.That(battleState.EnterCount, Is.EqualTo(1)); - } - finally - { - Object.DestroyImmediate(playerObject); - } + Assert.That(_world.Enemies, Is.Empty); + Assert.That(_world.Projectiles, Is.Empty); + Assert.That(_world.Pickups, Is.Empty); } - [Test] - public void PickupLifecycle_UpsertAndRemove_KeepsBindingsConsistent() + private void UpsertEnemy(EnemySimData simData) { - UpsertPickup(CreatePickup(entityId: 6101, position: new Vector3(1f, 0f, 0f), pickupRadius: 0.35f, state: 0)); - UpsertPickup(CreatePickup(entityId: 6102, position: new Vector3(2f, 0f, 0f), pickupRadius: 0.35f, state: 0)); - UpsertPickup(CreatePickup(entityId: 6103, position: new Vector3(3f, 0f, 0f), pickupRadius: 0.35f, state: 0)); - - Assert.That(GetPickupsCount(), Is.EqualTo(3)); - - UpsertPickup(CreatePickup(entityId: 6101, position: new Vector3(10f, 0f, 0f), pickupRadius: 0.5f, state: 1)); - Assert.That(GetPickupsCount(), Is.EqualTo(3)); - object updatedPickup = GetPickupAt(0); - Assert.That((int)GetField(updatedPickup, "EntityId"), Is.EqualTo(6101)); - Assert.That(((Vector3)GetField(updatedPickup, "Position")).x, Is.EqualTo(10f).Within(0.0001f)); - Assert.That((float)GetField(updatedPickup, "PickupRadius"), Is.EqualTo(0.5f).Within(0.0001f)); - - bool removedMiddle = RemovePickupByEntityId(6102); - bool removedMoved = RemovePickupByEntityId(6103); - - Assert.IsTrue(removedMiddle); - Assert.That(GetPickupsCount(), Is.EqualTo(1)); - Assert.IsTrue(removedMoved); - Assert.That((int)GetField(GetPickupAt(0), "EntityId"), Is.EqualTo(6101)); + Assert.NotNull(UpsertEnemyMethod); + UpsertEnemyMethod.Invoke(_world, new object[] { simData }); } - private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange, - bool avoidEnemyOverlap = false, float enemyBodyRadius = 0.45f, int separationIterations = 1) + private void UpsertProjectile(ProjectileSimData simData) { - 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", avoidEnemyOverlap); - SetField(ref enemy, "EnemyBodyRadius", enemyBodyRadius); - SetField(ref enemy, "SeparationIterations", separationIterations); - SetField(ref enemy, "TargetType", 0); - SetField(ref enemy, "State", 0); - return enemy; - } - - private object CreateProjectile(int entityId, Vector3 position, Vector3 forward, Vector3 velocity, float speed, - float lifeTime, float age, bool active, float remainingLifetime, int state) - { - object projectile = System.Activator.CreateInstance(ProjectileSimDataType); - SetField(ref projectile, "EntityId", entityId); - SetField(ref projectile, "OwnerEntityId", 0); - SetField(ref projectile, "Position", position); - SetField(ref projectile, "Forward", forward); - SetField(ref projectile, "Velocity", velocity); - SetField(ref projectile, "Speed", speed); - SetField(ref projectile, "LifeTime", lifeTime); - SetField(ref projectile, "Age", age); - SetField(ref projectile, "Active", active); - SetField(ref projectile, "RemainingLifetime", remainingLifetime); - SetField(ref projectile, "State", state); - return projectile; - } - - private object CreatePickup(int entityId, Vector3 position, float pickupRadius, int state) - { - object pickup = Activator.CreateInstance(PickupSimDataType); - SetField(ref pickup, "EntityId", entityId); - SetField(ref pickup, "Position", position); - SetField(ref pickup, "PickupRadius", pickupRadius); - SetField(ref pickup, "State", state); - return pickup; - } - - 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 SetUseSimulationMovement(bool enabled) - { - UseSimulationMovementField.SetValue(_worldComponent, enabled); - } - - private void UpsertEnemy(object enemy) - { - UpsertEnemyMethod.Invoke(_worldComponent, new[] { enemy }); - } - - private void UpsertProjectile(object projectile) - { - UpsertProjectileMethod.Invoke(_worldComponent, new[] { projectile }); - } - - private void UpsertPickup(object pickup) - { - UpsertPickupMethod.Invoke(_worldComponent, new[] { pickup }); - } - - private bool RemoveEnemyByEntityId(int entityId) - { - return (bool)RemoveEnemyByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId }); - } - - private bool RemoveProjectileByEntityId(int entityId) - { - return (bool)RemoveProjectileByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId }); - } - - private bool RemovePickupByEntityId(int entityId) - { - return (bool)RemovePickupByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId }); - } - - private static object GetGameEntrySimulationWorld() - { - return GameEntryGetSimulationWorldMethod.Invoke(null, null); - } - - private static void SetGameEntrySimulationWorld(object simulationWorld) - { - GameEntrySetSimulationWorldMethod.Invoke(null, new[] { simulationWorld }); - } - - private static object GetGameEntryEnemyManager() - { - return GameEntryGetEnemyManagerMethod.Invoke(null, null); - } - - private static void SetGameEntryEnemyManager(object enemyManager) - { - GameEntrySetEnemyManagerMethod.Invoke(null, new[] { enemyManager }); - } - - private static object GetGameEntryProcedure() - { - return GameEntryGetProcedureMethod.Invoke(null, null); - } - - private static void SetGameEntryProcedure(object procedureComponent) - { - GameEntrySetProcedureMethod.Invoke(null, new[] { procedureComponent }); - } - - private static void InvokeEnemyProjectileUpdate(Component projectileComponent, float elapseSeconds, - float realElapseSeconds) - { - EnemyProjectileOnUpdateMethod.Invoke(projectileComponent, new object[] { elapseSeconds, realElapseSeconds }); - } - - 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 object GetProjectileAt(int index) - { - object projectiles = ProjectilesProperty.GetValue(_worldComponent); - PropertyInfo itemProperty = projectiles.GetType().GetProperty("Item", PublicInstance); - return itemProperty.GetValue(projectiles, new object[] { index }); - } - - private int GetProjectilesCount() - { - object projectiles = ProjectilesProperty.GetValue(_worldComponent); - PropertyInfo countProperty = projectiles.GetType().GetProperty("Count", PublicInstance); - return (int)countProperty.GetValue(projectiles); - } - - private object GetPickupAt(int index) - { - object pickups = PickupsProperty.GetValue(_worldComponent); - PropertyInfo itemProperty = pickups.GetType().GetProperty("Item", PublicInstance); - return itemProperty.GetValue(pickups, new object[] { index }); - } - - private int GetPickupsCount() - { - object pickups = PickupsProperty.GetValue(_worldComponent); - PropertyInfo countProperty = pickups.GetType().GetProperty("Count", PublicInstance); - return (int)countProperty.GetValue(pickups); - } - - private int GetCollisionCandidateCount() - { - return (int)CollisionCandidateCountProperty.GetValue(_worldComponent); - } - - private int GetLastResolvedAreaHitCount() - { - return (int)LastResolvedAreaHitCountProperty.GetValue(_worldComponent); - } - - 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); - } - - private static void SetPrivateField(object target, string fieldName, object value) - { - Type type = target.GetType(); - while (type != null) - { - FieldInfo field = type.GetField(fieldName, NonPublicInstance); - if (field != null) - { - field.SetValue(target, value); - return; - } - - type = type.BaseType; - } - - Assert.Fail($"Field '{fieldName}' was not found on type '{target.GetType().FullName}'."); - } - - private sealed class TrackingGameState : GameStateBase - { - public TrackingGameState(GameStateType gameStateType) - { - GameStateType = gameStateType; - } - - public override GameStateType GameStateType { get; } - - public int EnterCount { get; private set; } - - public int LeaveCount { get; private set; } - - public override void OnInit(ProcedureGame master) - { - } - - public override void OnEnter(IFsm procedureOwner) - { - EnterCount++; - } - - public override void OnUpdate(IFsm procedureOwner, float elapseSeconds, - float realElapseSeconds) - { - } - - public override void OnLeave(IFsm procedureOwner) - { - LeaveCount++; - } - - public override void OnDestroy(IFsm procedureOwner) - { - } + Assert.NotNull(UpsertProjectileMethod); + UpsertProjectileMethod.Invoke(_world, new object[] { simData }); } } } diff --git a/Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs b/Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs index 1405bc5..3d6a58f 100644 --- a/Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs +++ b/Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs @@ -1,932 +1,135 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Reflection; +using Entity; +using Entity.EntityData; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; -using Object = UnityEngine.Object; namespace Simulation.Tests.PlayMode { public class SimulationWorldPlayModeTests { - private const string GameAssemblyName = "VampireLike"; - private const string RuntimeAssemblyName = "UnityGameFramework.Runtime"; - 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 BindingFlags NonPublicInstance = + BindingFlags.Instance | BindingFlags.NonPublic; - private static readonly System.Type SimulationWorldType = - System.Type.GetType($"Simulation.SimulationWorld, {GameAssemblyName}"); + private static readonly FieldInfo ProjectileDataField = + typeof(EnemyProjectile).GetField("_projectileData", NonPublicInstance); - private static readonly System.Type SimulationTickContextType = - System.Type.GetType($"Simulation.SimulationTickContext, {GameAssemblyName}"); + private static readonly FieldInfo ProjectileIsActiveField = + typeof(EnemyProjectile).GetField("_isActive", NonPublicInstance); - private static readonly System.Type EnemySimDataType = - System.Type.GetType($"Simulation.EnemySimData, {GameAssemblyName}"); - - private static readonly System.Type ProjectileSimDataType = - System.Type.GetType($"Simulation.ProjectileSimData, {GameAssemblyName}"); - - private static readonly System.Type EnemyProjectileType = - System.Type.GetType($"Entity.EnemyProjectile, {GameAssemblyName}"); - - private static readonly System.Type EnemyProjectileDataType = - System.Type.GetType($"Entity.EntityData.EnemyProjectileData, {GameAssemblyName}"); - - private static readonly System.Type CampTypeType = - System.Type.GetType($"Definition.Enum.CampType, {GameAssemblyName}"); - - private static readonly System.Type GameEntryType = - System.Type.GetType($"GameEntry, {GameAssemblyName}"); - - private static readonly System.Type EnemySeparationSolverProviderType = - System.Type.GetType($"CustomUtility.EnemySeparationSolverProvider, {GameAssemblyName}"); - - private static readonly System.Type EnemyManagerComponentType = - System.Type.GetType($"CustomComponent.EnemyManagerComponent, {GameAssemblyName}"); - - private static readonly System.Type PlayerType = - System.Type.GetType($"Entity.Player, {GameAssemblyName}"); - - private static readonly System.Type EntityBaseType = - System.Type.GetType($"Entity.EntityBase, {GameAssemblyName}"); - - private static readonly System.Type HealthComponentType = - System.Type.GetType($"Components.HealthComponent, {GameAssemblyName}"); - - private static readonly System.Type ProcedureComponentType = - System.Type.GetType($"UnityGameFramework.Runtime.ProcedureComponent, {RuntimeAssemblyName}"); - - private static readonly System.Type ProcedureGameType = - System.Type.GetType($"Procedure.ProcedureGame, {GameAssemblyName}"); - - private static readonly System.Type GameStateTypeType = - System.Type.GetType($"Procedure.GameStateType, {GameAssemblyName}"); - - private static readonly System.Type ProcedureManagerType = - System.Type.GetType("GameFramework.Procedure.ProcedureManager, GameFramework"); - - private static readonly System.Type ProcedureManagerInterfaceType = - System.Type.GetType("GameFramework.Procedure.IProcedureManager, GameFramework"); - - private static readonly System.Type FsmOpenGenericType = - System.Type.GetType("GameFramework.Fsm.Fsm`1, GameFramework"); - - private static readonly MethodInfo UpsertEnemyMethod = - SimulationWorldType?.GetMethod("UpsertEnemy", NonPublicInstance); - - private static readonly MethodInfo RemoveEnemyByEntityIdMethod = - SimulationWorldType?.GetMethod("RemoveEnemyByEntityId", NonPublicInstance); - - private static readonly MethodInfo UpsertProjectileMethod = - SimulationWorldType?.GetMethod("UpsertProjectile", NonPublicInstance); - - private static readonly MethodInfo RemoveProjectileByEntityIdMethod = - SimulationWorldType?.GetMethod("RemoveProjectileByEntityId", NonPublicInstance); - - private static readonly MethodInfo TryGetEnemyDataMethod = - SimulationWorldType?.GetMethod("TryGetEnemyData", NonPublicInstance); - - private static readonly MethodInfo TickMethod = - SimulationWorldType?.GetMethod("Tick", PublicInstance); - - private static readonly MethodInfo TryGetNearestEnemyEntityIdMethod = - SimulationWorldType?.GetMethod("TryGetNearestEnemyEntityId", PublicInstance); - - private static readonly MethodInfo TryRequestAreaCollisionMethod = - SimulationWorldType?.GetMethod("TryRequestAreaCollision", PublicInstance); - - private static readonly MethodInfo ClearSimulationStateMethod = - SimulationWorldType?.GetMethod("ClearSimulationState", PublicInstance); - - private static readonly MethodInfo UseGridBucketSolverMethod = - EnemySeparationSolverProviderType?.GetMethod("UseGridBucketSolver", PublicStatic); - - private static readonly FieldInfo EntitySyncField = - SimulationWorldType?.GetField("_entitySync", NonPublicInstance); - - private static readonly FieldInfo TransformSyncField = - SimulationWorldType?.GetField("_transformSync", NonPublicInstance); - - private static readonly FieldInfo HitPresentationField = - SimulationWorldType?.GetField("_hitPresentation", NonPublicInstance); - - private static readonly FieldInfo UseSimulationMovementField = - SimulationWorldType?.GetField("_useSimulationMovement", NonPublicInstance); - - private static readonly PropertyInfo EnemiesProperty = - SimulationWorldType?.GetProperty("Enemies", PublicInstance); - - private static readonly PropertyInfo ProjectilesProperty = - SimulationWorldType?.GetProperty("Projectiles", PublicInstance); - - private static readonly PropertyInfo CollisionCandidateCountProperty = - SimulationWorldType?.GetProperty("CollisionCandidateCount", PublicInstance); - - private static readonly PropertyInfo UseSimulationMovementProperty = - SimulationWorldType?.GetProperty("UseSimulationMovement", PublicInstance); - - private static readonly PropertyInfo LastResolvedAreaHitCountProperty = - SimulationWorldType?.GetProperty("LastResolvedAreaHitCount", PublicInstance); - - private static readonly FieldInfo CollisionQueryInputsField = - SimulationWorldType?.GetField("_collisionQueryInputs", NonPublicInstance); - - private static readonly FieldInfo AreaCollisionRequestsField = - SimulationWorldType?.GetField("_areaCollisionRequests", NonPublicInstance); + private static readonly FieldInfo ProjectileDirectionField = + typeof(EnemyProjectile).GetField("_direction", NonPublicInstance); private static readonly MethodInfo EnemyProjectileOnUpdateMethod = - EnemyProjectileType?.GetMethod("OnUpdate", NonPublicInstance); + typeof(EnemyProjectile).GetMethod("OnUpdate", NonPublicInstance); - private static readonly FieldInfo EnemyProjectileDataField = - EnemyProjectileType?.GetField("_projectileData", NonPublicInstance); - - private static readonly FieldInfo EnemyProjectileIsActiveField = - EnemyProjectileType?.GetField("_isActive", NonPublicInstance); - - private static readonly FieldInfo EnemyProjectileIsSimulationDrivenField = - EnemyProjectileType?.GetField("_isSimulationDriven", NonPublicInstance); - - private static readonly PropertyInfo GameEntrySimulationWorldProperty = - GameEntryType?.GetProperty("SimulationWorld", PublicStatic); - - private static readonly MethodInfo GameEntryGetSimulationWorldMethod = - GameEntrySimulationWorldProperty?.GetGetMethod(true); - - private static readonly MethodInfo GameEntrySetSimulationWorldMethod = - GameEntrySimulationWorldProperty?.GetSetMethod(true); - - private static readonly PropertyInfo GameEntryEnemyManagerProperty = - GameEntryType?.GetProperty("EnemyManager", PublicStatic); - - private static readonly MethodInfo GameEntryGetEnemyManagerMethod = - GameEntryEnemyManagerProperty?.GetGetMethod(true); - - private static readonly MethodInfo GameEntrySetEnemyManagerMethod = - GameEntryEnemyManagerProperty?.GetSetMethod(true); - - private static readonly PropertyInfo GameEntryProcedureProperty = - GameEntryType?.GetProperty("Procedure", PublicStatic); - - private static readonly MethodInfo GameEntryGetProcedureMethod = - GameEntryProcedureProperty?.GetGetMethod(true); - - private static readonly MethodInfo GameEntrySetProcedureMethod = - GameEntryProcedureProperty?.GetSetMethod(true); - - private static readonly MethodInfo HealthComponentOnInitMethod = - HealthComponentType?.GetMethod("OnInit", PublicInstance); - - private static readonly FieldInfo ProjectileMaxDistanceFromPlayerField = - SimulationWorldType?.GetField("_projectileMaxDistanceFromPlayer", NonPublicInstance); - - private static readonly FieldInfo ProjectileMaxVerticalOffsetFromPlayerField = - SimulationWorldType?.GetField("_projectileMaxVerticalOffsetFromPlayer", NonPublicInstance); - - private GameObject _worldGameObject; - private Component _worldComponent; + private GameObject _worldObject; + private SimulationWorld _world; [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(ProjectileSimDataType, "ProjectileSimData type lookup failed."); - Assert.NotNull(EnemyProjectileType, "EnemyProjectile type lookup failed."); - Assert.NotNull(EnemyProjectileDataType, "EnemyProjectileData type lookup failed."); - Assert.NotNull(CampTypeType, "CampType type lookup failed."); - Assert.NotNull(GameEntryType, "GameEntry type lookup failed."); - Assert.NotNull(EnemySeparationSolverProviderType, "EnemySeparationSolverProvider type lookup failed."); - Assert.NotNull(EnemyManagerComponentType, "EnemyManagerComponent type lookup failed."); - Assert.NotNull(PlayerType, "Player type lookup failed."); - Assert.NotNull(EntityBaseType, "EntityBase type lookup failed."); - Assert.NotNull(HealthComponentType, "HealthComponent type lookup failed."); - Assert.NotNull(ProcedureComponentType, "ProcedureComponent type lookup failed."); - Assert.NotNull(ProcedureGameType, "ProcedureGame type lookup failed."); - Assert.NotNull(GameStateTypeType, "GameStateType type lookup failed."); - Assert.NotNull(ProcedureManagerType, "ProcedureManager type lookup failed."); - Assert.NotNull(ProcedureManagerInterfaceType, "IProcedureManager type lookup failed."); - Assert.NotNull(FsmOpenGenericType, "Fsm`1 type lookup failed."); - Assert.NotNull(UpsertEnemyMethod, "UpsertEnemy reflection lookup failed."); - Assert.NotNull(RemoveEnemyByEntityIdMethod, "RemoveEnemyByEntityId reflection lookup failed."); - Assert.NotNull(UpsertProjectileMethod, "UpsertProjectile reflection lookup failed."); - Assert.NotNull(RemoveProjectileByEntityIdMethod, "RemoveProjectileByEntityId reflection lookup failed."); - Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed."); - Assert.NotNull(TickMethod, "Tick reflection lookup failed."); - Assert.NotNull(TryGetNearestEnemyEntityIdMethod, "TryGetNearestEnemyEntityId reflection lookup failed."); - Assert.NotNull(TryRequestAreaCollisionMethod, "TryRequestAreaCollision reflection lookup failed."); - Assert.NotNull(ClearSimulationStateMethod, "ClearSimulationState reflection lookup failed."); - Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed."); - Assert.NotNull(EnemiesProperty, "Enemies property reflection lookup failed."); - Assert.NotNull(ProjectilesProperty, "Projectiles property reflection lookup failed."); - Assert.NotNull(CollisionCandidateCountProperty, "CollisionCandidateCount property reflection lookup failed."); - Assert.NotNull(UseSimulationMovementProperty, "UseSimulationMovement property reflection lookup failed."); - Assert.NotNull(UseSimulationMovementField, "_useSimulationMovement field reflection lookup failed."); - Assert.NotNull(LastResolvedAreaHitCountProperty, "LastResolvedAreaHitCount property reflection lookup failed."); - Assert.NotNull(CollisionQueryInputsField, "Collision query inputs field reflection lookup failed."); - Assert.NotNull(AreaCollisionRequestsField, "Area collision requests field reflection lookup failed."); - Assert.NotNull(ProjectileMaxDistanceFromPlayerField, - "Projectile max distance field reflection lookup failed."); - Assert.NotNull(ProjectileMaxVerticalOffsetFromPlayerField, - "Projectile max vertical offset field reflection lookup failed."); - Assert.NotNull(EnemyProjectileOnUpdateMethod, "EnemyProjectile.OnUpdate reflection lookup failed."); - Assert.NotNull(EnemyProjectileDataField, "EnemyProjectile _projectileData reflection lookup failed."); - Assert.NotNull(EnemyProjectileIsActiveField, "EnemyProjectile _isActive reflection lookup failed."); - Assert.NotNull(EnemyProjectileIsSimulationDrivenField, - "EnemyProjectile _isSimulationDriven reflection lookup failed."); - Assert.NotNull(GameEntrySimulationWorldProperty, "GameEntry.SimulationWorld property lookup failed."); - Assert.NotNull(GameEntryGetSimulationWorldMethod, - "GameEntry.SimulationWorld getter reflection lookup failed."); - Assert.NotNull(GameEntrySetSimulationWorldMethod, - "GameEntry.SimulationWorld setter reflection lookup failed."); - Assert.NotNull(GameEntryEnemyManagerProperty, "GameEntry.EnemyManager property lookup failed."); - Assert.NotNull(GameEntryGetEnemyManagerMethod, "GameEntry.EnemyManager getter reflection lookup failed."); - Assert.NotNull(GameEntrySetEnemyManagerMethod, "GameEntry.EnemyManager setter reflection lookup failed."); - Assert.NotNull(GameEntryProcedureProperty, "GameEntry.Procedure property lookup failed."); - Assert.NotNull(GameEntryGetProcedureMethod, "GameEntry.Procedure getter reflection lookup failed."); - Assert.NotNull(GameEntrySetProcedureMethod, "GameEntry.Procedure setter reflection lookup failed."); - Assert.NotNull(HealthComponentOnInitMethod, "HealthComponent.OnInit reflection lookup failed."); - - _worldGameObject = new GameObject("SimulationWorldPlayModeTests"); - _worldComponent = _worldGameObject.AddComponent(SimulationWorldType); - - // Isolate PlayMode regression to simulation behavior only. - EntitySyncField?.SetValue(_worldComponent, null); - TransformSyncField?.SetValue(_worldComponent, null); - HitPresentationField?.SetValue(_worldComponent, null); - - SetUseSimulationMovement(true); - UseGridBucketSolverMethod.Invoke(null, new object[] { 1f }); + _worldObject = new GameObject("SimulationWorldPlayModeTests"); + _world = _worldObject.AddComponent(); yield return null; } [UnityTearDown] public IEnumerator TearDown() { - if (_worldComponent != null) + if (_worldObject != null) { - EntitySyncField?.SetValue(_worldComponent, null); - TransformSyncField?.SetValue(_worldComponent, null); - HitPresentationField?.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; - } - - [UnityTest] - public IEnumerator TickEnemies_ChasesPlayer_WhenJobSimulationChannelEnabled() - { - UpsertEnemy(CreateEnemy(entityId: 3201, 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_MatchesOutput_AfterClearSimulationState() - { - UpsertEnemy(CreateEnemy(entityId: 3251, position: Vector3.zero, speed: 2f, attackRange: 1f)); - InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: new Vector3(10f, 0f, 0f)); - - object nonBurstEnemy = GetEnemyAt(0); - int nonBurstState = (int)GetField(nonBurstEnemy, "State"); - Vector3 nonBurstPosition = (Vector3)GetField(nonBurstEnemy, "Position"); - Vector3 nonBurstForward = (Vector3)GetField(nonBurstEnemy, "Forward"); - - ClearSimulationStateMethod.Invoke(_worldComponent, null); - - SetUseSimulationMovement(true); - UpsertEnemy(CreateEnemy(entityId: 3251, position: Vector3.zero, speed: 2f, attackRange: 1f)); - InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: new Vector3(10f, 0f, 0f)); - - object burstEnemy = GetEnemyAt(0); - int burstState = (int)GetField(burstEnemy, "State"); - Vector3 burstPosition = (Vector3)GetField(burstEnemy, "Position"); - Vector3 burstForward = (Vector3)GetField(burstEnemy, "Forward"); - - Assert.That(burstState, Is.EqualTo(nonBurstState)); - Assert.That((burstPosition - nonBurstPosition).sqrMagnitude, Is.LessThanOrEqualTo(1e-8f)); - Assert.That((burstForward - nonBurstForward).sqrMagnitude, Is.LessThanOrEqualTo(1e-8f)); - yield break; - } - - [UnityTest] - public IEnumerator TryGetNearestEnemyEntityId_SelectsNearestBucketCandidate_WhenJobSimulationEnabled() - { - UpsertEnemy(CreateEnemy(entityId: 3301, position: new Vector3(1f, 0f, 0f), speed: 0f, attackRange: 1f)); - UpsertEnemy(CreateEnemy(entityId: 3302, position: new Vector3(6f, 0f, 0f), speed: 0f, attackRange: 1f)); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - - object[] parameters = { Vector3.zero, 100f, 0 }; - bool found = (bool)TryGetNearestEnemyEntityIdMethod.Invoke(_worldComponent, parameters); - int nearestEntityId = (int)parameters[2]; - - Assert.IsTrue(found); - Assert.That(nearestEntityId, Is.EqualTo(3301)); - yield break; - } - - [UnityTest] - public IEnumerator TickEnemies_SeparatesOverlappedEnemies_WhenJobSimulationEnabled() - { - UpsertEnemy(CreateEnemy(entityId: 3401, position: new Vector3(0f, 0f, 0f), speed: 1f, attackRange: 0.1f, - avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 2)); - UpsertEnemy(CreateEnemy(entityId: 3402, position: new Vector3(0.1f, 0f, 0f), speed: 1f, attackRange: 0.1f, - avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 2)); - - InvokeTick(deltaTime: 0.1f, realDeltaTime: 0.1f, playerPosition: new Vector3(10f, 0f, 0f)); - - object enemyA = GetEnemyAt(0); - object enemyB = GetEnemyAt(1); - Vector3 posA = (Vector3)GetField(enemyA, "Position"); - Vector3 posB = (Vector3)GetField(enemyB, "Position"); - posA.y = 0f; - posB.y = 0f; - float distance = Vector3.Distance(posA, posB); - Assert.That(distance, Is.GreaterThanOrEqualTo(0.89f)); - yield break; - } - - [UnityTest] - public IEnumerator TickEnemies_SeparatesOverlappedEnemies_WhenPlayerIsStaticAndInRange() - { - UpsertEnemy(CreateEnemy(entityId: 3411, position: new Vector3(0f, 0f, 0f), speed: 1f, attackRange: 10f, - avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 3)); - UpsertEnemy(CreateEnemy(entityId: 3412, position: new Vector3(0.05f, 0f, 0f), speed: 1f, attackRange: 10f, - avoidEnemyOverlap: true, enemyBodyRadius: 0.45f, separationIterations: 3)); - - InvokeTick(deltaTime: 0.1f, realDeltaTime: 0.1f, playerPosition: Vector3.zero); - - object enemyA = GetEnemyAt(0); - object enemyB = GetEnemyAt(1); - Assert.That((int)GetField(enemyA, "State"), Is.EqualTo(2)); - Assert.That((int)GetField(enemyB, "State"), Is.EqualTo(2)); - - Vector3 posA = (Vector3)GetField(enemyA, "Position"); - Vector3 posB = (Vector3)GetField(enemyB, "Position"); - posA.y = 0f; - posB.y = 0f; - float distance = Vector3.Distance(posA, posB); - Assert.That(distance, Is.GreaterThanOrEqualTo(0.5f)); - yield break; - } - - [UnityTest] - public IEnumerator TickProjectiles_MovesAndUpdatesLifetime_WhenJobSimulationEnabled() - { - UpsertProjectile(CreateProjectile(entityId: 5401, position: Vector3.zero, forward: Vector3.right, - velocity: new Vector3(2f, 0f, 0f), speed: 0f, lifeTime: 2f, age: 0f, active: true, - remainingLifetime: 2f, state: 0)); - - InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero); - - Assert.That(GetProjectilesCount(), Is.EqualTo(1)); - object projectile = GetProjectileAt(0); - Vector3 position = (Vector3)GetField(projectile, "Position"); - float age = (float)GetField(projectile, "Age"); - float remainingLifetime = (float)GetField(projectile, "RemainingLifetime"); - bool active = (bool)GetField(projectile, "Active"); - - Assert.That(position.x, Is.EqualTo(1f).Within(0.0001f)); - Assert.That(age, Is.EqualTo(0.5f).Within(0.0001f)); - Assert.That(remainingLifetime, Is.EqualTo(1.5f).Within(0.0001f)); - Assert.IsTrue(active); - yield break; - } - - [UnityTest] - public IEnumerator TickProjectiles_ContinuesFromLatestState_AcrossConsecutiveTicks() - { - UpsertProjectile(CreateProjectile(entityId: 5410, position: Vector3.zero, forward: Vector3.right, - velocity: new Vector3(2f, 0f, 0f), speed: 0f, lifeTime: 5f, age: 0f, active: true, - remainingLifetime: 5f, state: 0)); - - InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero); - object afterJobEnabled = GetProjectileAt(0); - Vector3 positionAfterJobEnabled = (Vector3)GetField(afterJobEnabled, "Position"); - float ageAfterJobEnabled = (float)GetField(afterJobEnabled, "Age"); - Assert.That(positionAfterJobEnabled.x, Is.EqualTo(1f).Within(0.0001f)); - Assert.That(ageAfterJobEnabled, Is.EqualTo(0.5f).Within(0.0001f)); - - InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero); - object afterSecondTick = GetProjectileAt(0); - Vector3 positionAfterSecondTick = (Vector3)GetField(afterSecondTick, "Position"); - float ageAfterSecondTick = (float)GetField(afterSecondTick, "Age"); - float remainingLifetimeAfterSecondTick = (float)GetField(afterSecondTick, "RemainingLifetime"); - bool activeAfterSecondTick = (bool)GetField(afterSecondTick, "Active"); - - Assert.That(positionAfterSecondTick.x, Is.EqualTo(2f).Within(0.0001f)); - Assert.That(ageAfterSecondTick, Is.EqualTo(1f).Within(0.0001f)); - Assert.That(remainingLifetimeAfterSecondTick, Is.EqualTo(4f).Within(0.0001f)); - Assert.IsTrue(activeAfterSecondTick); - yield break; - } - - [UnityTest] - public IEnumerator EnemyProjectile_TogglesCollider_WhenSimulationMovementSwitchesAtRuntime() - { - SetUseSimulationMovement(false); - - GameObject projectileObject = new GameObject("EnemyProjectileColliderTogglePlayMode"); - Component projectileComponent = null; - try - { - projectileComponent = projectileObject.AddComponent(EnemyProjectileType); - Collider projectileCollider = projectileObject.AddComponent(); - projectileCollider.enabled = true; - - object previousSimulationWorld = GetGameEntrySimulationWorld(); - SetGameEntrySimulationWorld(_worldComponent); - try - { - object neutralCamp = System.Enum.Parse(CampTypeType, "Neutral"); - object projectileData = System.Activator.CreateInstance( - EnemyProjectileDataType, - BindingFlags.Public | BindingFlags.Instance, - null, - new object[] { 8001, 1, neutralCamp, 1, 0f, 10f, Vector3.forward }, - null); - - EnemyProjectileDataField.SetValue(projectileComponent, projectileData); - EnemyProjectileIsActiveField.SetValue(projectileComponent, true); - EnemyProjectileIsSimulationDrivenField.SetValue(projectileComponent, false); - - InvokeEnemyProjectileUpdate(projectileComponent, 0.016f, 0.016f); - Assert.IsTrue(projectileCollider.enabled); - - SetUseSimulationMovement(true); - InvokeEnemyProjectileUpdate(projectileComponent, 0.016f, 0.016f); - Assert.IsFalse(projectileCollider.enabled); - - SetUseSimulationMovement(false); - InvokeEnemyProjectileUpdate(projectileComponent, 0.016f, 0.016f); - Assert.IsTrue(projectileCollider.enabled); - } - finally - { - SetGameEntrySimulationWorld(previousSimulationWorld); - } - } - finally - { - if (projectileObject != null) - { - Object.Destroy(projectileObject); - } + Object.Destroy(_worldObject); } yield return null; } [UnityTest] - public IEnumerator RemoveProjectileByEntityId_RemapIndex_ForMovedProjectile() + public IEnumerator Tick_AppliesSyncedPlayerMovementInput() { - UpsertProjectile(CreateProjectile(entityId: 5405, position: new Vector3(0f, 0f, 0f), - forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true, - remainingLifetime: 3f, state: 0)); - UpsertProjectile(CreateProjectile(entityId: 5406, position: new Vector3(1f, 0f, 0f), - forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true, - remainingLifetime: 3f, state: 0)); - UpsertProjectile(CreateProjectile(entityId: 5407, position: new Vector3(2f, 0f, 0f), - forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true, - remainingLifetime: 3f, state: 0)); - - bool removed = RemoveProjectileByEntityId(5406); - bool removedMoved = RemoveProjectileByEntityId(5407); - - Assert.IsTrue(removed); - Assert.That(GetProjectilesCount(), Is.EqualTo(1)); - Assert.IsTrue(removedMoved); - Assert.That((int)GetField(GetProjectileAt(0), "EntityId"), Is.EqualTo(5405)); - yield break; - } - - [UnityTest] - public IEnumerator TickProjectiles_RecyclesWhenExceedingPlayerDistance_WhenJobSimulationEnabled() - { - ProjectileMaxDistanceFromPlayerField.SetValue(_worldComponent, 5f); - ProjectileMaxVerticalOffsetFromPlayerField.SetValue(_worldComponent, 1000f); - UpsertProjectile(CreateProjectile(entityId: 5408, position: new Vector3(6f, 0f, 0f), - forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true, - remainingLifetime: 3f, state: 0)); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - - Assert.That(GetProjectilesCount(), Is.EqualTo(0)); - yield break; - } - - [UnityTest] - public IEnumerator TickProjectiles_RecyclesWhenExceedingVerticalOffset_WhenJobSimulationEnabled() - { - ProjectileMaxDistanceFromPlayerField.SetValue(_worldComponent, 0f); - ProjectileMaxVerticalOffsetFromPlayerField.SetValue(_worldComponent, 1f); - UpsertProjectile(CreateProjectile(entityId: 5409, position: new Vector3(0f, 2f, 0f), - forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true, - remainingLifetime: 3f, state: 0)); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - - Assert.That(GetProjectilesCount(), Is.EqualTo(0)); - yield break; - } - - [UnityTest] - public IEnumerator TickProjectiles_RecyclesExpiredProjectile_WhenJobSimulationEnabled() - { - UpsertProjectile(CreateProjectile(entityId: 5402, position: Vector3.zero, forward: Vector3.forward, - velocity: Vector3.zero, speed: 0f, lifeTime: 1f, age: 0.95f, active: true, remainingLifetime: 0.05f, - state: 0)); - - InvokeTick(deltaTime: 0.1f, realDeltaTime: 0.1f, playerPosition: Vector3.zero); - - Assert.That(GetProjectilesCount(), Is.EqualTo(0)); - yield break; - } - - [UnityTest] - public IEnumerator TickProjectiles_BuildsCollisionCandidatesAgainstEnemies_WhenJobSimulationEnabled() - { - UpsertEnemy(CreateEnemy(entityId: 5501, position: Vector3.zero, speed: 0f, attackRange: 1f)); - UpsertProjectile(CreateProjectile(entityId: 5502, position: Vector3.zero, forward: Vector3.forward, - velocity: Vector3.zero, speed: 0f, lifeTime: 2f, age: 0f, active: true, remainingLifetime: 2f, - state: 0)); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - - Assert.That(GetCollisionCandidateCount(), Is.GreaterThan(0)); - yield break; - } - - [UnityTest] - public IEnumerator TickProjectiles_BuildsCollisionCandidates_WithLatestEnemyMovement_WhenJobSimulationEnabled() - { - UpsertEnemy(CreateEnemy(entityId: 5511, position: new Vector3(2f, 0f, 0f), speed: 1f, attackRange: 0.1f)); - UpsertProjectile(CreateProjectile(entityId: 5512, position: new Vector3(1f, 0f, 0f), - forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 2f, age: 0f, active: true, - remainingLifetime: 2f, state: 0)); - - InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: Vector3.zero); - - Assert.That(GetCollisionCandidateCount(), Is.GreaterThan(0)); - yield break; - } - - [UnityTest] - public IEnumerator TickProjectiles_ExpiresAfterCollisionCandidateConsumed_WhenJobSimulationEnabled() - { - UpsertEnemy(CreateEnemy(entityId: 5503, position: Vector3.zero, speed: 0f, attackRange: 1f)); - UpsertProjectile(CreateProjectile(entityId: 5504, position: Vector3.zero, forward: Vector3.forward, - velocity: Vector3.zero, speed: 0f, lifeTime: 10f, age: 0f, active: true, remainingLifetime: 10f, - state: 0)); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - - Assert.That(GetProjectilesCount(), Is.EqualTo(0)); - yield break; - } - - [UnityTest] - public IEnumerator TickProjectiles_LimitsCandidatesToMaxTargets_IncludingPlayerCandidate() - { - object previousEnemyManager = GetGameEntryEnemyManager(); - GameObject enemyManagerObject = new GameObject("EnemyManagerMaxTargetsPlayMode"); - GameObject playerObject = new GameObject("PlayerTargetMaxTargetsPlayMode"); + GameObject playerObject = new GameObject("Player"); try { - Component enemyManager = enemyManagerObject.AddComponent(EnemyManagerComponentType); - Component player = playerObject.AddComponent(PlayerType); - Component healthComponent = playerObject.AddComponent(HealthComponentType); - HealthComponentOnInitMethod.Invoke(healthComponent, new object[] { 100, null }); - SetPrivateField(player, "_healthComponent", healthComponent); - SetPrivateField(player, "m_CachedTransform", playerObject.transform); - SetPrivateField(player, "m_Available", true); + playerObject.transform.position = Vector3.zero; - object enemyById = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(typeof(int), EntityBaseType)); - enemyById.GetType().GetMethod("Add")?.Invoke(enemyById, new object[] { -1, player }); - object enemies = Activator.CreateInstance(typeof(List<>).MakeGenericType(EntityBaseType)); - enemies.GetType().GetMethod("Add")?.Invoke(enemies, new object[] { player }); - SetPrivateField(enemyManager, "_enemyById", enemyById); - SetPrivateField(enemyManager, "_enemies", enemies); - SetGameEntryEnemyManager(enemyManager); + _world.SyncPlayerMovementInput(playerObject.transform, true, Vector3.right, 6f); + _world.Tick(new SimulationTickContext(0.5f, 0.5f, playerObject.transform.position)); - UpsertEnemy(CreateEnemy(entityId: 5521, position: Vector3.zero, speed: 0f, attackRange: 1f)); - UpsertProjectile(CreateProjectile(entityId: 5522, position: Vector3.zero, forward: Vector3.forward, - velocity: Vector3.zero, speed: 0f, lifeTime: 1f, age: 0f, active: true, remainingLifetime: 1f, - state: 0)); + yield return null; - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - - Assert.That(GetCollisionCandidateCount(), Is.EqualTo(1)); + Assert.That(playerObject.transform.position.x, Is.EqualTo(3f).Within(0.001f)); + Assert.That(playerObject.transform.position.z, Is.EqualTo(0f).Within(0.001f)); } finally { - SetGameEntryEnemyManager(previousEnemyManager); - Object.Destroy(enemyManagerObject); Object.Destroy(playerObject); } + } + + [UnityTest] + public IEnumerator EnemyProjectile_OnUpdate_DoesNotSelfDriveOrExpire() + { + GameObject projectileObject = new GameObject("EnemyProjectile"); + try + { + EnemyProjectile projectile = projectileObject.AddComponent(); + projectileObject.transform.position = Vector3.zero; + + ProjectileDataField.SetValue(projectile, + new EnemyProjectileData(3001, 1001, Definition.Enum.CampType.Enemy, 5, 10f, 0.1f, Vector3.right)); + ProjectileIsActiveField.SetValue(projectile, true); + ProjectileDirectionField.SetValue(projectile, Vector3.right); + + EnemyProjectileOnUpdateMethod.Invoke(projectile, new object[] { 1f, 1f }); + + yield return null; + + Assert.That(projectileObject.transform.position.x, Is.EqualTo(0f).Within(0.0001f)); + Assert.That(projectileObject.transform.position.y, Is.EqualTo(0f).Within(0.0001f)); + Assert.That(projectileObject.transform.position.z, Is.EqualTo(0f).Within(0.0001f)); + Assert.That(projectile.IsActive, Is.True); + } + finally + { + Object.Destroy(projectileObject); + } + } + + [UnityTest] + public IEnumerator Tick_RespectsEnemyMovementSyncFromComponentShell() + { + _world.SyncEnemyMovementInput(4001, false, Vector3.left, 5f, true, 0.45f, 2); + + MethodInfo upsertEnemyMethod = typeof(SimulationWorld).GetMethod("UpsertEnemy", NonPublicInstance); + upsertEnemyMethod.Invoke(_world, new object[] + { + new EnemySimData + { + EntityId = 4001, + Position = Vector3.zero, + Forward = Vector3.forward, + Rotation = Quaternion.identity, + Speed = 5f, + AttackRange = 0.5f, + AvoidEnemyOverlap = true, + EnemyBodyRadius = 0.45f, + SeparationIterations = 2 + } + }); + + _world.SyncEnemyMovementInput(4001, false, Vector3.left, 5f, true, 0.45f, 2); + _world.Tick(new SimulationTickContext(1f, 1f, new Vector3(8f, 0f, 0f))); yield return null; - } - [UnityTest] - public IEnumerator TryRequestAreaCollision_ReturnsFalse_WhenSimulationMovementDisabled() - { - SetUseSimulationMovement(false); - object[] requestArgs = { 5530, 5530, Vector3.zero, 1f, 1 }; - bool requestResult = (bool)TryRequestAreaCollisionMethod.Invoke(_worldComponent, requestArgs); - - Assert.IsFalse(requestResult); - Assert.IsFalse((bool)UseSimulationMovementProperty.GetValue(_worldComponent)); - - yield break; - } - - [UnityTest] - public IEnumerator EnqueueAreaQuery_CapturesInactiveSourceSnapshot_WhenSourceEntityUnavailable() - { - UpsertEnemy(CreateEnemy(entityId: 5531, position: Vector3.zero, speed: 0f, attackRange: 1f)); - - object[] enqueueArgs = { 99999, 99999, Vector3.zero, 1f, 1 }; - bool enqueueResult = (bool)TryRequestAreaCollisionMethod.Invoke(_worldComponent, enqueueArgs); - Assert.IsTrue(enqueueResult); - - object areaCollisionRequests = AreaCollisionRequestsField.GetValue(_worldComponent); - Assert.NotNull(areaCollisionRequests); - PropertyInfo requestCountProperty = areaCollisionRequests.GetType().GetProperty("Count", PublicInstance); - int requestCount = (int)requestCountProperty.GetValue(areaCollisionRequests); - Assert.That(requestCount, Is.GreaterThan(0)); - - PropertyInfo requestItemProperty = areaCollisionRequests.GetType().GetProperty("Item", PublicInstance); - object firstRequest = requestItemProperty.GetValue(areaCollisionRequests, new object[] { 0 }); - FieldInfo requestSnapshotField = - firstRequest.GetType().GetField("SourceWasActiveAtQueryTime", PublicInstance); - Assert.NotNull(requestSnapshotField); - bool requestSnapshot = (bool)requestSnapshotField.GetValue(firstRequest); - Assert.IsFalse(requestSnapshot); - - InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero); - Assert.That(GetLastResolvedAreaHitCount(), Is.EqualTo(0)); - yield break; - } - - private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange, - bool avoidEnemyOverlap = false, float enemyBodyRadius = 0.45f, int separationIterations = 1) - { - 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", avoidEnemyOverlap); - SetField(ref enemy, "EnemyBodyRadius", enemyBodyRadius); - SetField(ref enemy, "SeparationIterations", separationIterations); - SetField(ref enemy, "TargetType", 0); - SetField(ref enemy, "State", 0); - return enemy; - } - - private object CreateProjectile(int entityId, Vector3 position, Vector3 forward, Vector3 velocity, float speed, - float lifeTime, float age, bool active, float remainingLifetime, int state) - { - object projectile = System.Activator.CreateInstance(ProjectileSimDataType); - SetField(ref projectile, "EntityId", entityId); - SetField(ref projectile, "OwnerEntityId", 0); - SetField(ref projectile, "Position", position); - SetField(ref projectile, "Forward", forward); - SetField(ref projectile, "Velocity", velocity); - SetField(ref projectile, "Speed", speed); - SetField(ref projectile, "LifeTime", lifeTime); - SetField(ref projectile, "Age", age); - SetField(ref projectile, "Active", active); - SetField(ref projectile, "RemainingLifetime", remainingLifetime); - SetField(ref projectile, "State", state); - return projectile; - } - - 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 SetUseSimulationMovement(bool enabled) - { - UseSimulationMovementField.SetValue(_worldComponent, enabled); - } - - private void UpsertEnemy(object enemy) - { - UpsertEnemyMethod.Invoke(_worldComponent, new[] { enemy }); - } - - private void UpsertProjectile(object projectile) - { - UpsertProjectileMethod.Invoke(_worldComponent, new[] { projectile }); - } - - private bool RemoveEnemyByEntityId(int entityId) - { - return (bool)RemoveEnemyByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId }); - } - - private bool RemoveProjectileByEntityId(int entityId) - { - return (bool)RemoveProjectileByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId }); - } - - private static object GetGameEntrySimulationWorld() - { - return GameEntryGetSimulationWorldMethod.Invoke(null, null); - } - - private static void SetGameEntrySimulationWorld(object simulationWorld) - { - GameEntrySetSimulationWorldMethod.Invoke(null, new[] { simulationWorld }); - } - - private static object GetGameEntryEnemyManager() - { - return GameEntryGetEnemyManagerMethod.Invoke(null, null); - } - - private static void SetGameEntryEnemyManager(object enemyManager) - { - GameEntrySetEnemyManagerMethod.Invoke(null, new[] { enemyManager }); - } - - private static object GetGameEntryProcedure() - { - return GameEntryGetProcedureMethod.Invoke(null, null); - } - - private static void SetGameEntryProcedure(object procedureComponent) - { - GameEntrySetProcedureMethod.Invoke(null, new[] { procedureComponent }); - } - - private static void InvokeEnemyProjectileUpdate(Component projectileComponent, float elapseSeconds, - float realElapseSeconds) - { - EnemyProjectileOnUpdateMethod.Invoke(projectileComponent, new object[] { elapseSeconds, realElapseSeconds }); - } - - 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 object GetProjectileAt(int index) - { - object projectiles = ProjectilesProperty.GetValue(_worldComponent); - PropertyInfo itemProperty = projectiles.GetType().GetProperty("Item", PublicInstance); - return itemProperty.GetValue(projectiles, new object[] { index }); - } - - private int GetProjectilesCount() - { - object projectiles = ProjectilesProperty.GetValue(_worldComponent); - PropertyInfo countProperty = projectiles.GetType().GetProperty("Count", PublicInstance); - return (int)countProperty.GetValue(projectiles); - } - - private int GetCollisionCandidateCount() - { - return (int)CollisionCandidateCountProperty.GetValue(_worldComponent); - } - - private int GetLastResolvedAreaHitCount() - { - return (int)LastResolvedAreaHitCountProperty.GetValue(_worldComponent); - } - - 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); - } - - private static void SetPrivateField(object target, string fieldName, object value) - { - Type type = target.GetType(); - while (type != null) - { - FieldInfo field = type.GetField(fieldName, NonPublicInstance); - if (field != null) - { - field.SetValue(target, value); - return; - } - - type = type.BaseType; - } - - Assert.Fail($"Field '{fieldName}' was not found on type '{target.GetType().FullName}'."); + Assert.That(_world.Enemies.Count, Is.EqualTo(1)); + Assert.That(_world.Enemies[0].Position.x, Is.EqualTo(0f).Within(0.0001f)); + Assert.That(_world.Enemies[0].Position.y, Is.EqualTo(0f).Within(0.0001f)); + Assert.That(_world.Enemies[0].Position.z, Is.EqualTo(0f).Within(0.0001f)); + Assert.That(_world.Enemies[0].State, Is.EqualTo(0)); } } } diff --git a/docs/P1.5 Simulation-Supplement.md b/docs/P1.5 Simulation-Supplement.md index 8bf72ea..f04a1ee 100644 --- a/docs/P1.5 Simulation-Supplement.md +++ b/docs/P1.5 Simulation-Supplement.md @@ -40,11 +40,11 @@ - 当前主瓶颈明确:`MoveSeperation` 是绝对热点(约 `43%~47%` 帧占比),P2 优先并行化该阶段。 - 评估口径可复现:Android 端受 `60 fps` 上限影响,性能判断以 CPU `ms` 为准。 -## 回滚开关说明 -- 开关字段:`SimulationWorld._useSimulationMovement`(序列化私有字段) -- 对外接口:`UseSimulationMovement` / `SetUseSimulationMovement(bool)` -- 回滚方式:将开关置 `false`,敌人立即回退到旧 `MovementComponent` 更新路径。 -- 验证建议:同场景同刷怪参数下执行 A/B 对比,确认行为一致与性能差异。 +## 路线收敛说明 +- `SimulationWorld.Tick(...)` 已收敛为战斗内唯一仿真执行入口。 +- `UseSimulationMovement` 不再承担运行时双路径路由职责,不应再作为回滚到旧 `MovementComponent` 路径的开关理解。 +- 敌人、投射物与目标查询的运行时行为统一以 `SimulationWorld` 主容器和 Burst Job 管线为准。 +- 验证建议:聚焦单一路径下的敌人移动、投射物生命周期、最近敌查询和 area hit 结果,而不是做旧路径 A/B 对照。 ## P2 交接建议 - Job/Burst 第一优先级:`MoveSeperation` 阶段并行化。 diff --git a/docs/P2 Job System + Burst 落地.md b/docs/P2 Job System + Burst 落地.md index 19ddd10..4303496 100644 --- a/docs/P2 Job System + Burst 落地.md +++ b/docs/P2 Job System + Burst 落地.md @@ -6,7 +6,7 @@ 目标: - 固化压测口径(0.5k/1k/1.5k/2k) - 给出回归验证结论 -- 给出开关/回滚策略 +- 给出单一路径架构下的验证策略 - 给出最终验收判定(通过/不通过) ## 2. 验收标准(对齐 TodoList) @@ -23,21 +23,17 @@ - Profiler 口径:以 CPU `ms` 为主,`fps` 仅作辅助(Android 端存在 60fps 上限) - Profiler 配置:`Call Stacks = Off` -## 4. P2 开关与回滚策略 +## 4. P2 路线收敛说明 -### 4.1 运行开关 -- `UseSimulationMovement` -- `UseJobSimulation` -- `UseBurstJobs` +### 4.1 当前运行时语义 +- `SimulationWorld.Tick(...)` 是战斗内唯一仿真执行入口。 +- 敌人移动、敌人分离、投射物推进、碰撞 broad-phase、最近敌查询统一走 Burst/Job 管线。 +- 文档中的 `UseJobSimulation`、`UseBurstJobs` 当前没有代码实现,不应再作为实际回滚方案描述。 -### 4.2 生效时机约束 -- `UseSimulationMovement` / `UseJobSimulation`:战斗内不支持热切换,需在 Battle 外修改后生效。 -- `UseBurstJobs`:可切换,但建议仅用于战斗外 A/B。 - -### 4.3 回滚策略(建议) -1. 切回非 Job 路径:`UseJobSimulation = false` -2. 若仍异常,切回旧移动:`UseSimulationMovement = false` -3. 保留 `UseBurstJobs` 仅在 Job 路径 A/B 对照 +### 4.2 验证重点 +1. 验证单一路径下的敌人移动、投射物生命周期、碰撞候选与 area hit 结果。 +2. 验证 `Battle -> LevelUp -> Shop -> Battle` 与清场流程不会留下脏的仿真状态。 +3. 验证 Debug/测试表面不再暴露旧 solver 或双路径开关语义。 ## 5. 回归验证(Checkpoint 9) @@ -57,7 +53,7 @@ #### 用例 1:10 分钟连续战斗 - 执行时间:待填 - 场景/波次参数:待填 -- 运行开关:`UseSimulationMovement = true`,`UseJobSimulation = true`,`UseBurstJobs = true` +- 运行路径:`SimulationWorld` Burst/Job 单一路径 - 结果:待填 - 日志/录屏:待填 - 备注:待填 @@ -66,7 +62,7 @@ - 执行时间:已执行,见 `Logs/editmode-test-results.xml` - 操作步骤:由 EditMode 测试 `ProcedureGame_TransitionsBattleToLevelUpShopAndBackToBattle` 覆盖 - 执行方式:自动化测试 -- 运行开关:`UseSimulationMovement = true`,`UseJobSimulation = true`,`UseBurstJobs = true` +- 运行路径:`SimulationWorld` Burst/Job 单一路径 - 结果:通过 - 日志/录屏:`Logs/editmode-test-results.xml` - 备注:验证 `ProcedureGame` 可从 `Battle` 正确切换到 `LevelUp`、再到 `Shop`,并最终返回 `Battle` @@ -75,7 +71,7 @@ - 执行时间:已执行,见 `Logs/editmode-test-results.xml` - 验证范围:掉落注册 / 更新 / 回收 - 执行方式:自动化测试 -- 运行开关:`UseSimulationMovement = true`,`UseJobSimulation = true`,`UseBurstJobs = true` +- 运行路径:`SimulationWorld` Burst/Job 单一路径 - 结果:通过 - 日志/录屏:`Logs/editmode-test-results.xml` - 备注:由 EditMode 测试 `PickupLifecycle_UpsertAndRemove_KeepsBindingsConsistent` 覆盖,验证掉落在 `SimulationWorld` 中的生命周期与 binding remap 正常 diff --git a/docs/TodoList.md b/docs/TodoList.md index 7a6a302..6aa9d80 100644 --- a/docs/TodoList.md +++ b/docs/TodoList.md @@ -40,8 +40,8 @@ - [x] Checkpoint 3:建立 Simulation 主更新入口并接入 Battle 状态 - 在 `GameStateBattle.OnUpdate` 中增加 `SimulationWorld.Tick(...)` 调用。 - 先只接“敌人移动/追踪”系统,其他逻辑保持原路径。 - - 增加开关(建议 `UseSimulationMovement`)用于 A/B 对比与回滚。 - - 完成标准:关闭开关与当前行为一致;开启开关后敌人仍能正常追踪玩家。 + - 路线已收敛:不再维护 `UseSimulationMovement` 作为运行时 A/B 与回滚开关。 + - 完成标准:`SimulationWorld.Tick(...)` 成为唯一执行入口,敌人仍能正常追踪玩家。 - [x] Checkpoint 4:迁移敌人核心移动逻辑到 Simulation(去 MonoBehaviour 核心逻辑) - 将 `MeleeEnemy/RemoteEnemy` 的目标追踪、移动方向、攻击距离判定迁至 Simulation。 @@ -117,11 +117,9 @@ - `com.unity.jobs`(已废弃并并入 `com.unity.collections`,Unity 2022.3 不再单独锁定包) - `com.unity.burst` - `com.unity.mathematics` - - 增加 P2 运行开关(建议): - - `UseJobSimulation` - - `UseBurstJobs` - - 约束:默认可一键回退到 P1.5 路径,避免全量切换导致定位困难。 - - 完成标准:Editor/Development Build 均可编译运行;关闭开关时行为与 P1.5 一致。 + - 文档中的 `UseJobSimulation` / `UseBurstJobs` 当前未落代码实现,不再作为运行时方案前提。 + - 约束:以当前 `SimulationWorld` Burst/Job 单一路径为唯一验收对象。 + - 完成标准:Editor/Development Build 均可编译运行;单一路径行为稳定。 - [x] Checkpoint 2:Simulation 与 Job 数据通道打通(仅建通道,不改行为) - 为敌人/投射物建立 Job 输入输出结构(纯数据,不含 `Transform`/托管引用)。 diff --git a/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/.openspec.yaml b/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/.openspec.yaml new file mode 100644 index 0000000..6a5db8c --- /dev/null +++ b/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-02 diff --git a/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/design.md b/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/design.md new file mode 100644 index 0000000..e97dd39 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/design.md @@ -0,0 +1,61 @@ +## Context + +当前战斗运行时已经把 `SimulationWorld.Tick(...)` 接入主循环,并由 Burst Job 管线承担敌人移动、敌人分离、投射物推进与碰撞 broad-phase 的主体计算。但组件与实体层仍保留旧路径,包括 `MovementComponent.OnUpdate()` 直接改位移、`EnemyProjectile.OnUpdate()` 自驱动、`NearestTargetSelector` fallback 查询,以及 `EnemySeparationSolverProvider` 的旧互斥职责。这使运行时行为、调试入口和测试方式都围绕双路径假设展开。 + +本次变更横跨 `SimulationWorld`、实体/组件、战斗入口、调试面板、测试和文档,属于一次架构收敛,而不是单点修补。约束是:必须保留现有 Burst/Job 管线与 `_enemies/_projectiles/_pickups` 作为正式状态源,避免重新引入另一套仿真框架。 + +## Goals / Non-Goals + +**Goals:** +- 让 `SimulationWorld` 成为战斗中的唯一仿真执行入口。 +- 把实体与组件职责收敛为输入提交、注册同步和表现消费。 +- 删除旧双路径分支、旧互斥 solver 依赖和与之绑定的调试入口。 +- 将测试改为验证可观察行为,而不是依赖私有字段反射或 Native 通道细节。 +- 同步文档,使其准确反映单一路径架构。 + +**Non-Goals:** +- 不重写现有 Burst/Job 算法本身。 +- 不在本次变更中扩展新武器或新仿真能力。 +- 不恢复或保留可切换的 P1.5 / 非 `SimulationWorld` 执行路径。 +- 不以兼容旧测试为目标保留多余字段或调试钩子。 + +## Decisions + +### 1. 保留现有 Burst Job 管线,删除双路径路由语义 +- 决策:以 `SimulationWorld.TickSimulationPipeline(in SimulationTickContext context)` 作为唯一执行面,移除 `UseSimulationMovement` 的运行时路由职责;如果保留该字段,也只能作为全局停机开关而不是分支入口。 +- 原因:现有可工作的完整路径只有 Burst Job 管线,继续维护组件驱动 fallback 没有等价能力,只会增加漂移。 +- 备选方案:恢复旧路径并保留切换开关。否决原因是旧路径已不完整,恢复成本高且会持续制造测试矩阵和调试噪音。 + +### 2. 以 sim state 作为唯一真相源,实体侧只提交输入与消费输出 +- 决策:`_enemies`、`_projectiles`、`_pickups` 继续作为正式运行时状态源;`MovementComponent`、敌人、玩家、投射物只维护输入态、注册态和表现同步所需数据,不再直接推进世界位置或互斥。 +- 原因:状态集中后,Job 输入/输出和 presentation write-back 才能形成闭环,避免实体私自改写坐标导致状态撕裂。 +- 备选方案:保留实体局部自驱动,再由 `SimulationWorld` 尽量同步。否决原因是会持续产生写冲突和追责困难。 + +### 3. 保留 `SimulationWorld` 查询/结算能力,移除实体 fallback 查询 +- 决策:碰撞 broad-phase、area/sector/projectile 命中查询统一由 `SimulationWorld` 提供;目标选择器与武器逻辑直接依赖该能力,不再回退到遍历或组件侧查询。 +- 原因:查询能力只有与仿真状态源共用同一空间索引时才一致,fallback 查询会导致命中范围与真实运行时不同步。 +- 备选方案:保留 fallback 作为调试或兜底。否决原因是这会掩盖真实问题,并让生产行为与测试行为不一致。 + +### 4. 测试与调试面板跟随单一路径重建 +- 决策:移除旧 solver 切换 UI 和依赖私有字段的测试模式,改为验证敌人移动、投射物生命周期、范围命中、实体 hide/remove 等外部行为,并仅展示当前 `SimulationWorld` 的指标。 +- 原因:单一路径架构下,私有 Native 通道结构属于实现细节,不应成为长期契约。 +- 备选方案:继续保留反射测试以降低短期改动量。否决原因是会固化错误抽象边界。 + +## Risks / Trade-offs + +- [风险] 删除旧分支后,部分依赖 `MovementComponent.OnUpdate()` 或 projectile 自驱动的实体可能短期失效。 → 缓解:优先收敛战斗入口和输入同步接口,再逐类替换敌人、玩家、投射物的调用点。 +- [风险] 测试从白盒切到黑盒后,定位 Native 通道回归会更慢。 → 缓解:保留必要运行时指标与日志,但不把私有字段暴露为长期契约。 +- [风险] 文档和代码阶段性不同步会误导后续开发。 → 缓解:将文档同步列为同一 change 的完成条件,而不是后续补做。 +- [权衡] 放弃双路径意味着失去旧逻辑的快速兜底。 → 缓解:通过更稳定的单路径测试覆盖和可观测指标替代兜底分支。 + +## Migration Plan + +1. 先收敛战斗主入口与 `SimulationWorld` tick 语义,明确单一路径。 +2. 再将 `MovementComponent`、敌人、玩家、投射物改为输入/注册/表现职责,移除旧 solver 与 fallback 查询。 +3. 最后清理调试面板、重建测试、更新文档,确保仓库不再暴露旧路径语义。 +4. 回滚策略仅限于回退整个 change;不设计运行时开关回滚。 + +## Open Questions + +- `UseSimulationMovement` 是否完全删除,还是保留为仅用于全局停机/诊断的只读配置,需要在实现前最终确认。 +- 玩家位移是否已经有完整的 `SimulationWorld` 输入同步接口;若没有,需要先补足最小接口再移除 `MovementComponent.OnUpdate()` 调用。 diff --git a/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/proposal.md b/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/proposal.md new file mode 100644 index 0000000..cd86e83 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/proposal.md @@ -0,0 +1,24 @@ +## Why + +`SimulationWorld` 当前已经以 Burst Job 管线承担主要仿真职责,但运行时仍残留组件自驱动、实体 fallback 和旧调试/测试路径,导致移动、碰撞和生命周期逻辑在两套语义之间分叉。继续维护这些旧路径只会放大行为漂移和测试脆弱性,因此需要把运行时彻底收敛到单一路径。 + +## What Changes + +- 将 `SimulationWorld` Burst/Job 管线固化为战斗中的唯一仿真执行入口。 +- 删除 `UseSimulationMovement` 一类双路径路由语义,以及实体、组件中的自驱动移动和 fallback 查询逻辑。 +- 将 `MovementComponent`、敌人/投射物实体、目标选择器收敛为输入同步、注册管理和表现消费层。 +- 清理 `EnemySeparationSolverProvider` 及其调试面板入口,移除旧互斥 solver 的运行时依赖。 +- 重建测试与文档,使其面向外部可观察行为,而不是旧私有字段和 Native 通道反射。 + +## Capabilities + +### New Capabilities +- `simulationworld-runtime-convergence`: Define `SimulationWorld` as the sole runtime simulation path for movement, projectile stepping, collision queries, presentation sync, and related battle integration. + +### Modified Capabilities + +## Impact + +- Affected code: `Assets/GameMain/Scripts/Simulation/**`, `Assets/GameMain/Scripts/Components/MovementComponent.cs`, `Assets/GameMain/Scripts/Entity/EntityLogic/**`, `Assets/GameMain/Scripts/Procedure/Game/**`, `Assets/GameMain/Scripts/CustomComponent/DebugPanel/RuntimeDebugPanelComponent.cs`, `Assets/Tests/Simulation/**`, `docs/P1.5 Simulation-Supplement.md`, `docs/P2 Job System + Burst 落地.md`, `docs/TodoList.md`. +- Runtime impact: battle update order, enemy/player/projectile movement, collision candidate queries, target selection, presentation write-back, and simulation lifecycle reset behavior. +- Breaking impact: removes legacy runtime branches and debug affordances that assume a non-`SimulationWorld` execution path exists. diff --git a/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/specs/simulationworld-runtime-convergence/spec.md b/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/specs/simulationworld-runtime-convergence/spec.md new file mode 100644 index 0000000..77ede11 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/specs/simulationworld-runtime-convergence/spec.md @@ -0,0 +1,45 @@ +## ADDED Requirements + +### Requirement: SimulationWorld SHALL be the sole battle simulation executor +The battle runtime MUST execute movement, projectile stepping, collision broad-phase, and related simulation state updates through `SimulationWorld`, and it MUST NOT route these responsibilities through an alternative non-`SimulationWorld` runtime path. + +#### Scenario: Battle tick advances through SimulationWorld +- **WHEN** the battle update loop advances a gameplay frame +- **THEN** `SimulationWorld` executes the simulation pipeline for that frame as the authoritative runtime update path + +#### Scenario: Legacy routing switch does not select an alternate executor +- **WHEN** runtime configuration related to simulation movement is evaluated +- **THEN** it does not select or re-enable a separate legacy movement execution path + +### Requirement: Runtime entities SHALL submit input and consume simulation output only +Enemy entities, the player entity, projectile entities, and `MovementComponent` MUST submit movement or behavior input into `SimulationWorld` state and MUST consume position, facing, hit, or lifecycle results from simulation output, rather than computing world-space advancement independently. + +#### Scenario: MovementComponent no longer advances transforms directly +- **WHEN** movement input, speed, or separation parameters change on an entity +- **THEN** the component synchronizes those values to `SimulationWorld` state without directly moving the entity transform + +#### Scenario: Entity presentation follows simulation output +- **WHEN** simulation state is committed for enemies or projectiles +- **THEN** entity presentation updates consume the committed output to refresh transforms, facing, visibility, or removal state + +### Requirement: Spatial queries SHALL use SimulationWorld-owned data and services +Target selection, projectile hits, area hits, and sector hits MUST use `SimulationWorld` spatial indexing and query services, and MUST NOT fall back to legacy entity-side traversal or solver-specific query paths. + +#### Scenario: Target selection uses simulation spatial data +- **WHEN** weapon or AI logic requests nearby or nearest targets +- **THEN** the query resolves against `SimulationWorld` maintained spatial data for the current frame + +#### Scenario: Projectile and area hit evaluation stay on simulation path +- **WHEN** projectile, area, or sector hit logic runs during combat +- **THEN** hit candidates are produced from `SimulationWorld` collision and query capabilities instead of a fallback entity-side path + +### Requirement: Runtime surfaces SHALL reflect the single-path architecture +Runtime debug surfaces, automated tests, and architecture documents MUST reflect that the project supports one authoritative `SimulationWorld` execution path rather than dual-path behavior. + +#### Scenario: Debug panel omits legacy solver controls +- **WHEN** runtime simulation debugging is displayed +- **THEN** it shows current `SimulationWorld` metrics without exposing legacy solver switching or dual-path controls + +#### Scenario: Regression tests validate observable single-path behavior +- **WHEN** simulation regression coverage is maintained +- **THEN** tests validate observable outcomes such as movement, projectile lifetime, hit results, and hide/remove lifecycle instead of asserting private compatibility fields for legacy paths diff --git a/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/tasks.md b/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/tasks.md new file mode 100644 index 0000000..9a8ea73 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/tasks.md @@ -0,0 +1,23 @@ +## 1. Battle Runtime Convergence + +- [x] 1.1 Update `SimulationWorld` and battle entry points so the Burst/Job simulation pipeline is the only runtime execution path. +- [x] 1.2 Remove or redefine `UseSimulationMovement` and related semantics so it can no longer route combat through a legacy non-simulation path. +- [x] 1.3 Confirm `ClearSimulationState()` and battle bootstrap/reset flows only clear simulation-owned state without reintroducing dual-path behavior. + +## 2. Entity and Component Path Cleanup + +- [x] 2.1 Refactor `MovementComponent` into an input/register/sync layer and remove direct transform advancement plus `EnemySeparationSolverProvider` runtime dependence. +- [x] 2.2 Remove enemy and player calls that self-advance movement or branch on `UseSimulationMovement`, replacing them with simulation input submission and presentation consumption. +- [x] 2.3 Remove projectile self-driven movement/lifetime logic and make projectile presentation follow `SimulationWorld` output only. + +## 3. Query, Debug, and Regression Alignment + +- [x] 3.1 Route target selection and hit queries exclusively through `SimulationWorld` spatial data and remove legacy fallback traversal paths. +- [x] 3.2 Clean the runtime debug panel so it exposes current `SimulationWorld` metrics without legacy solver or dual-path controls. +- [x] 3.3 Rewrite simulation regression tests to validate observable behavior such as movement, projectile lifetime, hit results, and hide/remove lifecycle. + +## 4. Documentation and Verification + +- [x] 4.1 Update simulation architecture documents and todo notes to describe the single authoritative `SimulationWorld` path and remove stale switch descriptions. +- [x] 4.2 Run targeted verification for battle movement, projectile flow, query behavior, and updated tests to confirm no legacy execution path remains. + diff --git a/openspec/specs/simulationworld-runtime-convergence/spec.md b/openspec/specs/simulationworld-runtime-convergence/spec.md new file mode 100644 index 0000000..75acd12 --- /dev/null +++ b/openspec/specs/simulationworld-runtime-convergence/spec.md @@ -0,0 +1,51 @@ +# simulationworld-runtime-convergence + +## Purpose + +Define the battle runtime contract that `SimulationWorld` is the single authoritative simulation path for movement, projectile stepping, collision/query execution, and presentation-driven output consumption. + +## Requirements + +### Requirement: SimulationWorld SHALL be the sole battle simulation executor +The battle runtime MUST execute movement, projectile stepping, collision broad-phase, and related simulation state updates through `SimulationWorld`, and it MUST NOT route these responsibilities through an alternative non-`SimulationWorld` runtime path. + +#### Scenario: Battle tick advances through SimulationWorld +- **WHEN** the battle update loop advances a gameplay frame +- **THEN** `SimulationWorld` executes the simulation pipeline for that frame as the authoritative runtime update path + +#### Scenario: Legacy routing switch does not select an alternate executor +- **WHEN** runtime configuration related to simulation movement is evaluated +- **THEN** it does not select or re-enable a separate legacy movement execution path + +### Requirement: Runtime entities SHALL submit input and consume simulation output only +Enemy entities, the player entity, projectile entities, and `MovementComponent` MUST submit movement or behavior input into `SimulationWorld` state and MUST consume position, facing, hit, or lifecycle results from simulation output, rather than computing world-space advancement independently. + +#### Scenario: MovementComponent no longer advances transforms directly +- **WHEN** movement input, speed, or separation parameters change on an entity +- **THEN** the component synchronizes those values to `SimulationWorld` state without directly moving the entity transform + +#### Scenario: Entity presentation follows simulation output +- **WHEN** simulation state is committed for enemies or projectiles +- **THEN** entity presentation updates consume the committed output to refresh transforms, facing, visibility, or removal state + +### Requirement: Spatial queries SHALL use SimulationWorld-owned data and services +Target selection, projectile hits, area hits, and sector hits MUST use `SimulationWorld` spatial indexing and query services, and MUST NOT fall back to legacy entity-side traversal or solver-specific query paths. + +#### Scenario: Target selection uses simulation spatial data +- **WHEN** weapon or AI logic requests nearby or nearest targets +- **THEN** the query resolves against `SimulationWorld` maintained spatial data for the current frame + +#### Scenario: Projectile and area hit evaluation stay on simulation path +- **WHEN** projectile, area, or sector hit logic runs during combat +- **THEN** hit candidates are produced from `SimulationWorld` collision and query capabilities instead of a fallback entity-side path + +### Requirement: Runtime surfaces SHALL reflect the single-path architecture +Runtime debug surfaces, automated tests, and architecture documents MUST reflect that the project supports one authoritative `SimulationWorld` execution path rather than dual-path behavior. + +#### Scenario: Debug panel omits legacy solver controls +- **WHEN** runtime simulation debugging is displayed +- **THEN** it shows current `SimulationWorld` metrics without exposing legacy solver switching or dual-path controls + +#### Scenario: Regression tests validate observable single-path behavior +- **WHEN** simulation regression coverage is maintained +- **THEN** tests validate observable outcomes such as movement, projectile lifetime, hit results, and hide/remove lifecycle instead of asserting private compatibility fields for legacy paths