From 1052cc01364f3cfa8222a13cac563f7342e8b547 Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Thu, 2 Apr 2026 11:21:07 +0800 Subject: [PATCH 1/4] Cleanup 1 --- .../Scripts/Components/MovementComponent.cs | 89 +- .../DebugPanel/RuntimeDebugPanelComponent.cs | 20 - .../Entity/EntityLogic/Enemy/EnemyBase.cs | 3 +- .../EntityLogic/Enemy/EnemyProjectile.cs | 36 +- .../Entity/EntityLogic/Enemy/MeleeEnemy.cs | 7 - .../Entity/EntityLogic/Enemy/RemoteEnemy.cs | 10 - .../Scripts/Entity/EntityLogic/Player.cs | 1 - .../TargetSelector/NearestTargetSelector.cs | 25 +- .../Jobs/SimulationWorld.CollisionRequests.cs | 5 - .../SimulationWorld.TransformSync.cs | 4 +- .../SimulationWorld.PlayerMovement.cs | 82 ++ .../SimulationWorld.PlayerMovement.cs.meta | 11 + .../SimulationWorld.SimEntityState.cs | 28 +- ...lationWorld.TargetSelectionSpatialIndex.cs | 5 - .../Scripts/Simulation/SimulationWorld.cs | 15 +- .../EditMode/SimulationWorldTickTests.cs | 1091 ++--------------- .../PlayMode/SimulationWorldPlayModeTests.cs | 967 ++------------- docs/P1.5 Simulation-Supplement.md | 10 +- docs/P2 Job System + Burst 落地.md | 30 +- docs/TodoList.md | 12 +- .../.openspec.yaml | 2 + .../design.md | 61 + .../proposal.md | 24 + .../spec.md | 45 + .../tasks.md | 23 + .../spec.md | 51 + 26 files changed, 600 insertions(+), 2057 deletions(-) create mode 100644 Assets/GameMain/Scripts/Simulation/SimulationWorld.PlayerMovement.cs create mode 100644 Assets/GameMain/Scripts/Simulation/SimulationWorld.PlayerMovement.cs.meta create mode 100644 openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/.openspec.yaml create mode 100644 openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/design.md create mode 100644 openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/proposal.md create mode 100644 openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/specs/simulationworld-runtime-convergence/spec.md create mode 100644 openspec/changes/archive/2026-04-02-converge-simulationworld-runtime-paths/tasks.md create mode 100644 openspec/specs/simulationworld-runtime-convergence/spec.md 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 From ffcd4e6b54a729c9587b82e3fce2af92c318a998 Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Thu, 2 Apr 2026 12:36:01 +0800 Subject: [PATCH 2/4] Cleanup 2 --- AGENTS.md | 3 + .../Entity/EntityLogic/Enemy/EnemyBase.cs | 8 +- .../Scripts/Simulation/SimulationWorld.cs | 48 ++--- .../EnemySeparationSolverProvider.cs | 163 ---------------- .../EnemySeparationSolverProvider.cs.meta | 11 -- .../GridBucketEnemySeparationSolver.cs | 182 ------------------ .../GridBucketEnemySeparationSolver.cs.meta | 11 -- .../EnemySeperator/IEnemySeparationSolver.cs | 17 -- .../IEnemySeparationSolver.cs.meta | 11 -- .../NaiveEnemySeparationSolver.cs | 81 -------- .../NaiveEnemySeparationSolver.cs.meta | 11 -- .../EditMode/SimulationWorldTickTests.cs | 5 +- .../PlayMode/SimulationWorldPlayModeTests.cs | 5 +- docs/CodeX-TODO.md | 142 +++++++------- docs/P1.5 Simulation-Supplement.md | 3 +- docs/TodoList.md | 7 +- .../.openspec.yaml | 2 + .../design.md | 48 +++++ .../proposal.md | 26 +++ .../spec.md | 31 +++ .../tasks.md | 17 ++ .../spec.md | 12 +- 22 files changed, 246 insertions(+), 598 deletions(-) delete mode 100644 Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs delete mode 100644 Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs.meta delete mode 100644 Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs delete mode 100644 Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs.meta delete mode 100644 Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs delete mode 100644 Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs.meta delete mode 100644 Assets/GameMain/Scripts/Utility/EnemySeperator/NaiveEnemySeparationSolver.cs delete mode 100644 Assets/GameMain/Scripts/Utility/EnemySeperator/NaiveEnemySeparationSolver.cs.meta create mode 100644 openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/.openspec.yaml create mode 100644 openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/design.md create mode 100644 openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/proposal.md create mode 100644 openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/specs/simulationworld-runtime-convergence/spec.md create mode 100644 openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/tasks.md diff --git a/AGENTS.md b/AGENTS.md index 3fc76b0..82722d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,3 +31,6 @@ Always keep `.meta` files when adding or moving Unity assets to preserve GUID re Recent history favors concise, descriptive commit messages (often Chinese), e.g. `Feature: add launcher scene and update project settings`. Keep commits focused and include module context (`UI`, `Procedure`, `Entity`) when useful. PRs should include: change summary, affected scenes/modules, test evidence (Test Runner or CLI logs), linked issue/task, and screenshots or short video for UI/visual updates. + +## Encoding +Use UTF8 with BOM diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyBase.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyBase.cs index 03ae712..86c9e9d 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyBase.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyBase.cs @@ -1,4 +1,4 @@ -using Definition.DataStruct; +using Definition.DataStruct; using Entity; using UnityEngine; @@ -10,9 +10,5 @@ public abstract class EnemyBase : TargetableObject public virtual float AttackRange => 1f; public virtual void SetTarget(Transform target) => _target = target; - - protected bool IsSimulationMovementEnabled() - { - return true; - } } + diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs b/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs index 3cfa136..6783643 100644 --- a/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using CustomDebugger; using UnityEngine; using UnityGameFramework.Runtime; @@ -8,28 +8,28 @@ namespace Simulation public sealed partial class SimulationWorld : GameFrameworkComponent { // Partial layout: - // - SimulationWorld.cs: 核心状态、常量和 Unity 生命周期入口点。 - // - SimulationWorld.RuntimeModules.cs: 运行时域对象、配置和状态代理。 - // - SimulationWorld.SimEntityState.cs: 模拟状态的增删改查和生命周期注册。 - // - SimulationWorld.EntityToSimData.cs: Unity 实体到 sim data 的初始化适配。 - // - SimulationWorld.EntitySync.cs: GameFramework 实体 show/hide 事件桥。 - // - SimulationWorld.TargetSelectionSpatialIndex.cs: 最近敌空间索引查询。 - // - Presentation/SimulationWorld.TransformSync.cs: late-update transform 同步桥。 - // - Presentation/SimulationWorld.HitPresentation.cs: 投射物命中事件表现桥。 - // - DataChannel/SimulationWorld.JobDataChannel.cs: Job 通道共享字段、常量和运行时状态。 - // - DataChannel/SimulationWorld.JobDataLifecycle.cs: Native 通道初始化、清理和 clear。 - // - DataChannel/SimulationWorld.JobDataConversion.cs: sim/job 数据转换与输入输出缓冲准备。 - // - DataChannel/SimulationWorld.CollisionTransient.cs: 碰撞临时通道和运行时统计。 - // - DataChannel/SimulationWorld.EnemySeparationTemporal.cs: 敌人分离的帧间临时状态。 - // - DataChannel/SimulationWorld.JobOutputCommit.cs: Job 输出回写主容器。 - // - Jobs/SimulationWorld.EnemyJobs.cs: 模拟通道 编排 + 敌人移动/分离 顺序执行 - // - Jobs/SimulationWorld.ProjectileJobs.cs: 投射物移动与回收 - // - Jobs/SimulationWorld.CollisionPipeline.cs: 碰撞管线共享配置和状态 - // - Jobs/SimulationWorld.CollisionRequests.cs: area/sector 请求缓冲 - // - Jobs/SimulationWorld.CollisionBroadPhase.cs: broad-phase 候选构建和 Job 调度 - // - Jobs/SimulationWorld.CollisionResolve.cs: 主线程命中结算与 area settle - // - Jobs/SimulationWorld.CollisionPresentation.cs: 命中表现事件和实体/impact 解析 - // - JobStruct/*.cs: burst job 内核和面向 job 的数据结构 + // - SimulationWorld.cs: 鏍稿績鐘舵€併€佸父閲忓拰 Unity 鐢熷懡鍛ㄦ湡鍏ュ彛鐐广€? + // - SimulationWorld.RuntimeModules.cs: 杩愯鏃跺煙瀵硅薄銆侀厤缃拰鐘舵€佷唬鐞嗐€? + // - SimulationWorld.SimEntityState.cs: 妯℃嫙鐘舵€佺殑澧炲垹鏀规煡鍜岀敓鍛藉懆鏈熸敞鍐屻€? + // - SimulationWorld.EntityToSimData.cs: Unity 瀹炰綋鍒?sim data 鐨勫垵濮嬪寲閫傞厤銆? + // - SimulationWorld.EntitySync.cs: GameFramework 瀹炰綋 show/hide 浜嬩欢妗ャ€? + // - SimulationWorld.TargetSelectionSpatialIndex.cs: 鏈€杩戞晫绌洪棿绱㈠紩鏌ヨ銆? + // - Presentation/SimulationWorld.TransformSync.cs: late-update transform 鍚屾妗ャ€? + // - Presentation/SimulationWorld.HitPresentation.cs: 鎶曞皠鐗╁懡涓簨浠惰〃鐜版ˉ銆? + // - DataChannel/SimulationWorld.JobDataChannel.cs: Job 閫氶亾鍏变韩瀛楁銆佸父閲忓拰杩愯鏃剁姸鎬併€? + // - DataChannel/SimulationWorld.JobDataLifecycle.cs: Native 閫氶亾鍒濆鍖栥€佹竻鐞嗗拰 clear銆? + // - DataChannel/SimulationWorld.JobDataConversion.cs: sim/job 鏁版嵁杞崲涓庤緭鍏ヨ緭鍑虹紦鍐插噯澶囥€? + // - DataChannel/SimulationWorld.CollisionTransient.cs: 纰版挒涓存椂閫氶亾鍜岃繍琛屾椂缁熻銆? + // - DataChannel/SimulationWorld.EnemySeparationTemporal.cs: 鏁屼汉鍒嗙鐨勫抚闂翠复鏃剁姸鎬併€? + // - DataChannel/SimulationWorld.JobOutputCommit.cs: Job 杈撳嚭鍥炲啓涓诲鍣ㄣ€? + // - Jobs/SimulationWorld.EnemyJobs.cs: 妯℃嫙閫氶亾 缂栨帓 + 鏁屼汉绉诲姩/鍒嗙 椤哄簭鎵ц + // - Jobs/SimulationWorld.ProjectileJobs.cs: 鎶曞皠鐗╃Щ鍔ㄤ笌鍥炴敹 + // - Jobs/SimulationWorld.CollisionPipeline.cs: 纰版挒绠$嚎鍏变韩閰嶇疆鍜岀姸鎬? + // - Jobs/SimulationWorld.CollisionRequests.cs: area/sector 璇锋眰缂撳啿 + // - Jobs/SimulationWorld.CollisionBroadPhase.cs: broad-phase 鍊欓€夋瀯寤哄拰 Job 璋冨害 + // - Jobs/SimulationWorld.CollisionResolve.cs: 涓荤嚎绋嬪懡涓粨绠椾笌 area settle + // - Jobs/SimulationWorld.CollisionPresentation.cs: 鍛戒腑琛ㄧ幇浜嬩欢鍜屽疄浣?impact 瑙f瀽 + // - JobStruct/*.cs: burst job 鍐呮牳鍜岄潰鍚?job 鐨勬暟鎹粨鏋? private const float DefaultAttackRange = 1f; private const int EnemyStateIdle = 0; private const int EnemyStateChasing = 1; @@ -44,7 +44,6 @@ namespace Simulation public IReadOnlyList Enemies => _enemies; public IReadOnlyList Projectiles => _projectiles; public IReadOnlyList Pickups => _pickups; - public bool UseSimulationMovement => true; #region Lifecycle @@ -92,3 +91,4 @@ namespace Simulation #endregion } } + diff --git a/Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs b/Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs deleted file mode 100644 index 040137f..0000000 --- a/Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace CustomUtility -{ - public static class EnemySeparationSolverProvider - { - private enum SolverType - { - GridBucket, - Naive - } - - private struct LegacyRegistration - { - public int AgentId; - public float BodyRadius; - } - - private static SolverType _solverType = SolverType.GridBucket; - private static float _gridCellSize = 1f; - private static IEnemySeparationSolver _legacySolver = CreateSolver(); - private static IEnemySeparationSolver _simulationSolver = CreateSolver(); - - private static readonly Dictionary LegacyRegistrations = new(); - private static readonly List LegacyAgents = new(); - private static readonly List LegacyRecycle = new(); - private static int _legacySnapshotFrame = -1; - private static int _nextLegacyAgentId = 1; - - public static IEnemySeparationSolver Current => _simulationSolver; - public static string CurrentSolverName => _simulationSolver.GetType().Name; - - public static void SetSolver(IEnemySeparationSolver solver) - { - if (solver == null) return; - - _legacySolver = solver; - _simulationSolver = solver; - _legacySnapshotFrame = -1; - } - - public static void UseGridBucketSolver(float cellSize = 1f) - { - _solverType = SolverType.GridBucket; - _gridCellSize = Mathf.Max(0.1f, cellSize); - RecreateSolvers(); - } - - public static void UseNaiveSolver() - { - _solverType = SolverType.Naive; - RecreateSolvers(); - } - - public static void Register(Transform transform, float bodyRadius) - { - if (transform == null) return; - - if (LegacyRegistrations.TryGetValue(transform, out LegacyRegistration registration)) - { - registration.BodyRadius = bodyRadius; - LegacyRegistrations[transform] = registration; - } - else - { - LegacyRegistrations.Add(transform, new LegacyRegistration - { - AgentId = _nextLegacyAgentId++, - BodyRadius = bodyRadius - }); - } - - _legacySnapshotFrame = -1; - } - - public static void Unregister(Transform transform) - { - if (transform == null) return; - if (!LegacyRegistrations.Remove(transform)) return; - - _legacySnapshotFrame = -1; - } - - public static Vector3 Resolve(Transform transform, Vector3 desiredPosition, Vector3 fallbackDirection, - int iterations) - { - if (transform == null) return desiredPosition; - if (!LegacyRegistrations.TryGetValue(transform, out LegacyRegistration registration)) - { - return desiredPosition; - } - - EnsureLegacySnapshot(); - return _legacySolver.Resolve(registration.AgentId, desiredPosition, fallbackDirection, iterations); - } - - public static void SetSimulationAgents(IReadOnlyList agents) - { - _simulationSolver.SetAgents(agents); - } - - public static Vector3 ResolveSimulation(int agentId, Vector3 desiredPosition, Vector3 fallbackDirection, - int iterations) - { - return _simulationSolver.Resolve(agentId, desiredPosition, fallbackDirection, iterations); - } - - private static void EnsureLegacySnapshot() - { - int frame = Time.frameCount; - if (_legacySnapshotFrame == frame) return; - - _legacySnapshotFrame = frame; - LegacyAgents.Clear(); - LegacyRecycle.Clear(); - - foreach (var pair in LegacyRegistrations) - { - Transform transform = pair.Key; - if (transform == null) - { - LegacyRecycle.Add(pair.Key); - continue; - } - - Vector3 position = transform.position; - position.y = 0f; - - LegacyAgents.Add(new EnemySeparationAgent - { - AgentId = pair.Value.AgentId, - Position = position, - Radius = Mathf.Max(0.01f, pair.Value.BodyRadius) - }); - } - - for (int i = 0; i < LegacyRecycle.Count; i++) - { - LegacyRegistrations.Remove(LegacyRecycle[i]); - } - - _legacySolver.SetAgents(LegacyAgents); - } - - private static void RecreateSolvers() - { - _legacySolver = CreateSolver(); - _simulationSolver = CreateSolver(); - _legacySnapshotFrame = -1; - } - - private static IEnemySeparationSolver CreateSolver() - { - if (_solverType == SolverType.Naive) - { - return new NaiveEnemySeparationSolver(); - } - - return new GridBucketEnemySeparationSolver(_gridCellSize); - } - } -} diff --git a/Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs.meta b/Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs.meta deleted file mode 100644 index 5339ad0..0000000 --- a/Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 3cf44095cd7c76043a8e8a44dc5a0888 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs b/Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs deleted file mode 100644 index f83e050..0000000 --- a/Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs +++ /dev/null @@ -1,182 +0,0 @@ -using UnityEngine; - -namespace CustomUtility -{ - public sealed class GridBucketEnemySeparationSolver : IEnemySeparationSolver - { - private struct Agent - { - public float Radius; - public Vector3 Position; - public int CellX; - public int CellZ; - } - - private readonly System.Collections.Generic.Dictionary _agents = new(); - private readonly System.Collections.Generic.Dictionary> _buckets = new(); - private readonly System.Collections.Generic.Stack> _bucketListPool = new(); - private readonly System.Collections.Generic.List _activeBucketKeys = new(); - private readonly float _cellSize; - - private float _maxRadius = 0.45f; - - public GridBucketEnemySeparationSolver(float cellSize = 1f) - { - _cellSize = Mathf.Max(0.1f, cellSize); - } - - public void SetAgents(System.Collections.Generic.IReadOnlyList agents) - { - RecycleBucketsForSnapshot(); - _agents.Clear(); - _maxRadius = 0.01f; - - if (agents == null) return; - - for (int i = 0; i < agents.Count; i++) - { - EnemySeparationAgent input = agents[i]; - Vector3 position = input.Position; - position.y = 0f; - float radius = Mathf.Max(0.01f, input.Radius); - - Agent agent = new Agent - { - Radius = radius, - Position = position, - CellX = ToCell(position.x), - CellZ = ToCell(position.z) - }; - - _agents[input.AgentId] = agent; - AddToBucket(input.AgentId, agent.CellX, agent.CellZ); - - if (radius > _maxRadius) - { - _maxRadius = radius; - } - } - } - - public Vector3 Resolve(int agentId, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations) - { - if (!_agents.TryGetValue(agentId, out var self)) return desiredPosition; - - Vector3 candidate = desiredPosition; - candidate.y = 0f; - - int effectiveIterations = Mathf.Max(1, iterations); - int queryRange = Mathf.Max(1, Mathf.CeilToInt((self.Radius + _maxRadius) / _cellSize)); - Vector3 fallback = fallbackDirection.sqrMagnitude > 0.0001f ? fallbackDirection.normalized : Vector3.right; - fallback.y = 0f; - - for (int iter = 0; iter < effectiveIterations; iter++) - { - int cellX = ToCell(candidate.x); - int cellZ = ToCell(candidate.z); - - for (int dx = -queryRange; dx <= queryRange; dx++) - { - for (int dz = -queryRange; dz <= queryRange; dz++) - { - if (!_buckets.TryGetValue(CellKey(cellX + dx, cellZ + dz), out var bucket)) continue; - - for (int i = 0; i < bucket.Count; i++) - { - int otherAgentId = bucket[i]; - if (otherAgentId == agentId) continue; - if (!_agents.TryGetValue(otherAgentId, out var other)) continue; - - Vector3 toSelf = candidate - other.Position; - float minDistance = self.Radius + other.Radius; - float minDistanceSq = minDistance * minDistance; - float sqrDistance = toSelf.sqrMagnitude; - - if (sqrDistance <= Mathf.Epsilon) - { - candidate += fallback * (self.Radius * 0.25f); - continue; - } - - if (sqrDistance >= minDistanceSq) continue; - - float distance = Mathf.Sqrt(sqrDistance); - float penetration = minDistance - distance; - candidate += (toSelf / distance) * penetration; - } - } - } - } - - SyncAgentPosition(agentId, ref self, candidate); - - candidate.y = desiredPosition.y; - return candidate; - } - - private void RecycleBucketsForSnapshot() - { - for (int i = 0; i < _activeBucketKeys.Count; i++) - { - long key = _activeBucketKeys[i]; - if (!_buckets.TryGetValue(key, out var bucket)) continue; - - bucket.Clear(); - _bucketListPool.Push(bucket); - _buckets.Remove(key); - } - - _activeBucketKeys.Clear(); - } - - private void SyncAgentPosition(int agentId, ref Agent agent, Vector3 position) - { - int newCellX = ToCell(position.x); - int newCellZ = ToCell(position.z); - - if (agent.CellX != newCellX || agent.CellZ != newCellZ) - { - RemoveFromBucket(agentId, agent.CellX, agent.CellZ); - AddToBucket(agentId, newCellX, newCellZ); - agent.CellX = newCellX; - agent.CellZ = newCellZ; - } - - agent.Position = position; - _agents[agentId] = agent; - } - - private void AddToBucket(int agentId, int cellX, int cellZ) - { - long key = CellKey(cellX, cellZ); - if (!_buckets.TryGetValue(key, out var list)) - { - list = _bucketListPool.Count > 0 - ? _bucketListPool.Pop() - : new System.Collections.Generic.List(8); - _buckets.Add(key, list); - _activeBucketKeys.Add(key); - } - - list.Add(agentId); - } - - private void RemoveFromBucket(int agentId, int cellX, int cellZ) - { - long key = CellKey(cellX, cellZ); - if (!_buckets.TryGetValue(key, out var list)) return; - - list.Remove(agentId); - } - - private int ToCell(float value) - { - return Mathf.FloorToInt(value / _cellSize); - } - - private static long CellKey(int x, int z) - { - return ((long)x << 32) ^ (uint)z; - } - } -} diff --git a/Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs.meta b/Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs.meta deleted file mode 100644 index 7df22f7..0000000 --- a/Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c7c10dca24b508f4fa6726eae7ac2fb1 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs b/Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs deleted file mode 100644 index 0d002f1..0000000 --- a/Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs +++ /dev/null @@ -1,17 +0,0 @@ -using UnityEngine; - -namespace CustomUtility -{ - public struct EnemySeparationAgent - { - public int AgentId; - public Vector3 Position; - public float Radius; - } - - public interface IEnemySeparationSolver - { - void SetAgents(System.Collections.Generic.IReadOnlyList agents); - Vector3 Resolve(int agentId, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations); - } -} diff --git a/Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs.meta b/Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs.meta deleted file mode 100644 index bd00780..0000000 --- a/Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e3960124c8fe4304493659a13e5a9439 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Utility/EnemySeperator/NaiveEnemySeparationSolver.cs b/Assets/GameMain/Scripts/Utility/EnemySeperator/NaiveEnemySeparationSolver.cs deleted file mode 100644 index 8d198cf..0000000 --- a/Assets/GameMain/Scripts/Utility/EnemySeperator/NaiveEnemySeparationSolver.cs +++ /dev/null @@ -1,81 +0,0 @@ -using UnityEngine; - -namespace CustomUtility -{ - public sealed class NaiveEnemySeparationSolver : IEnemySeparationSolver - { - private struct Agent - { - public float Radius; - public Vector3 Position; - } - - private readonly System.Collections.Generic.Dictionary _agents = new(); - private readonly System.Collections.Generic.List _agentKeys = new(); - - public void SetAgents(System.Collections.Generic.IReadOnlyList agents) - { - _agents.Clear(); - _agentKeys.Clear(); - if (agents == null) return; - - for (int i = 0; i < agents.Count; i++) - { - EnemySeparationAgent input = agents[i]; - Vector3 position = input.Position; - position.y = 0f; - - Agent agent = new Agent - { - Radius = Mathf.Max(0.01f, input.Radius), - Position = position - }; - - _agents[input.AgentId] = agent; - _agentKeys.Add(input.AgentId); - } - } - - public Vector3 Resolve(int agentId, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations) - { - if (!_agents.TryGetValue(agentId, out var self)) return desiredPosition; - - Vector3 candidate = desiredPosition; - candidate.y = 0f; - - Vector3 fallback = fallbackDirection.sqrMagnitude > 0.0001f ? fallbackDirection.normalized : Vector3.right; - fallback.y = 0f; - - int effectiveIterations = Mathf.Max(1, iterations); - for (int iter = 0; iter < effectiveIterations; iter++) - { - for (int i = 0; i < _agentKeys.Count; i++) - { - int otherAgentId = _agentKeys[i]; - if (otherAgentId == agentId) continue; - if (!_agents.TryGetValue(otherAgentId, out var other)) continue; - - Vector3 toSelf = candidate - other.Position; - float minDistance = self.Radius + other.Radius; - float minDistanceSq = minDistance * minDistance; - float sqrDistance = toSelf.sqrMagnitude; - - if (sqrDistance <= Mathf.Epsilon) - { - candidate += fallback * (self.Radius * 0.25f); - continue; - } - - if (sqrDistance >= minDistanceSq) continue; - - float distance = Mathf.Sqrt(sqrDistance); - float penetration = minDistance - distance; - candidate += (toSelf / distance) * penetration; - } - } - - candidate.y = desiredPosition.y; - return candidate; - } - } -} diff --git a/Assets/GameMain/Scripts/Utility/EnemySeperator/NaiveEnemySeparationSolver.cs.meta b/Assets/GameMain/Scripts/Utility/EnemySeperator/NaiveEnemySeparationSolver.cs.meta deleted file mode 100644 index d17ca1c..0000000 --- a/Assets/GameMain/Scripts/Utility/EnemySeperator/NaiveEnemySeparationSolver.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: ec8ec1013900437498da4613f680a898 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs b/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs index d8ca668..6358291 100644 --- a/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs +++ b/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using Components; using NUnit.Framework; using UnityEngine; @@ -88,7 +88,7 @@ namespace Simulation.Tests.Editor } [Test] - public void SyncEnemyMovementInput_DisablesEnemyMovementOnSimulationPath() + public void SyncEnemyMovementInput_DisablesEnemyMovement() { UpsertEnemy(new EnemySimData { @@ -166,3 +166,4 @@ namespace Simulation.Tests.Editor } } } + diff --git a/Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs b/Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs index 3d6a58f..e08aae1 100644 --- a/Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs +++ b/Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Collections; using System.Reflection; using Entity; using Entity.EntityData; @@ -99,7 +99,7 @@ namespace Simulation.Tests.PlayMode } [UnityTest] - public IEnumerator Tick_RespectsEnemyMovementSyncFromComponentShell() + public IEnumerator Tick_RespectsEnemyMovementSync() { _world.SyncEnemyMovementInput(4001, false, Vector3.left, 5f, true, 0.45f, 2); @@ -133,3 +133,4 @@ namespace Simulation.Tests.PlayMode } } } + diff --git a/docs/CodeX-TODO.md b/docs/CodeX-TODO.md index 46b8f95..d68e737 100644 --- a/docs/CodeX-TODO.md +++ b/docs/CodeX-TODO.md @@ -187,21 +187,21 @@ ### 推荐实施顺序 1. 先确认唯一执行路径 -- `SimulationWorld` Burst 管线作为唯一运行时执行路径 -- 实体和组件只保留输入、注册、表现职责 + - `SimulationWorld` Burst 管线作为唯一运行时执行路径 + - 实体和组件只保留输入、注册、表现职责 2. 再处理战斗入口 -- 先改 `GameStateBattle` / `GameEntry` / `ProcedureGame` -- 让运行时明确依赖当前 Burst 管线,不再保留双路径语义 + - 先改 `GameStateBattle` / `GameEntry` / `ProcedureGame` + - 让运行时明确依赖当前 Burst 管线,不再保留双路径语义 3. 再清理旧组件驱动路径 -- 先收敛 `MovementComponent` -- 再删敌人/玩家/投射物实体里的自驱动移动 -- 再删旧 fallback 查询和旧互斥 solver + - 先收敛 `MovementComponent` + - 再删敌人/玩家/投射物实体里的自驱动移动 + - 再删旧 fallback 查询和旧互斥 solver 4. 最后重建测试和文档 -- 先让行为稳定 -- 再补新的回归测试和文档 + - 先让行为稳定 + - 再补新的回归测试和文档 ### 当前建议 @@ -355,72 +355,72 @@ 优先顺序建议: 1. 长枪 / 刺剑 -- 基于 `WeaponKnife` -- 重点调: - - 前刺距离 - - 命中半径 - - 冷却 + - 基于 `WeaponKnife` + - 重点调: + - 前刺距离 + - 命中半径 + - 冷却 2. 大剑 / 半月斩 -- 基于 `WeaponSlash` -- 重点调: - - `SectorAngle` - - 攻击范围 - - 动画时长 + - 基于 `WeaponSlash` + - 重点调: + - `SectorAngle` + - 攻击范围 + - 动画时长 3. 战锤 / 震地锤 -- 基于 `WeaponLightning` 或 `WeaponKnife` -- 重点调: - - 落点半径 - - 前摇 - - 低频高伤 + - 基于 `WeaponLightning` 或 `WeaponKnife` + - 重点调: + - 落点半径 + - 前摇 + - 低频高伤 4. 霰弹枪 -- 基于参数化后的 `WeaponHandgun` -- 重点调: - - 散射 - - 多 pellet - - 近距离爆发 + - 基于参数化后的 `WeaponHandgun` + - 重点调: + - 散射 + - 多 pellet + - 近距离爆发 5. 狙击枪 -- 基于参数化后的 `WeaponHandgun` -- 重点调: - - 单发高伤 - - 超远射程 - - 慢冷却 + - 基于参数化后的 `WeaponHandgun` + - 重点调: + - 单发高伤 + - 超远射程 + - 慢冷却 6. 陨石杖 / 圣光柱 -- 基于 `WeaponLightning` -- 重点调: - - `HoverHeight` - - 爆炸半径 - - 冷却 + - 基于 `WeaponLightning` + - 重点调: + - `HoverHeight` + - 爆炸半径 + - 冷却 ### P3: 中成本扩展 1. 链式闪电 -- 在首目标命中后,继续寻找附近目标 -- 需要新增: - - 连锁次数 - - 连锁半径 - - 每跳衰减 + - 在首目标命中后,继续寻找附近目标 + - 需要新增: + - 连锁次数 + - 连锁半径 + - 每跳衰减 2. 穿透弹 / 火球 -- 复用现有 projectile/simulation 基础 -- 需要明确: - - 穿透次数 - - 命中后是否爆炸 + - 复用现有 projectile/simulation 基础 + - 需要明确: + - 穿透次数 + - 命中后是否爆炸 3. 地雷 / 陷阱 -- 本质是延时触发 area hit -- 需要新增: - - 布置后触发时机 - - 持续时间 - - 触发半径 + - 本质是延时触发 area hit + - 需要新增: + - 布置后触发时机 + - 持续时间 + - 触发半径 4. 回旋镖 -- 需要双阶段投射物状态 -- 成本高于普通枪械/范围武器 + - 需要双阶段投射物状态 + - 成本高于普通枪械/范围武器 ### P4: 暂缓项 @@ -439,29 +439,29 @@ ## 新武器接入步骤模板 1. 在 `Weapon.txt` 新增一行 -- 配好基础字段 -- `Params` 写 JSON 对象 + - 配好基础字段 + - `Params` 写 JSON 对象 2. 新增 `WeaponType` -- 在 `Assets/GameMain/Scripts/Definition/Enum/WeaponType.cs` + - 在 `Assets/GameMain/Scripts/Definition/Enum/WeaponType.cs` 3. 新增武器数据子类 -- 新建 `WeaponXXXData` -- 新建 `WeaponXXXParamsData` -- 在构造里调用 `ParseParams()` + - 新建 `WeaponXXXData` + - 新建 `WeaponXXXParamsData` + - 在构造里调用 `ParseParams()` 4. 新增武器逻辑类 -- 继承 `WeaponBase` -- 接入状态机 -- 读取 `ParamsData` + - 继承 `WeaponBase` + - 接入状态机 + - 读取 `ParamsData` 5. 接入生成入口 -- 玩家初始武器 -- 商店购买武器 -- 其他掉落/奖励入口 + - 玩家初始武器 + - 商店购买武器 + - 其他掉落/奖励入口 6. 验证点 -- 武器生成正确 -- 参数生效正确 -- 描述文本正确 -- Simulation 模式和非 Simulation 模式都能命中 + - 武器生成正确 + - 参数生效正确 + - 描述文本正确 + - Simulation 模式和非 Simulation 模式都能命中 diff --git a/docs/P1.5 Simulation-Supplement.md b/docs/P1.5 Simulation-Supplement.md index f04a1ee..a28e42d 100644 --- a/docs/P1.5 Simulation-Supplement.md +++ b/docs/P1.5 Simulation-Supplement.md @@ -42,7 +42,7 @@ ## 路线收敛说明 - `SimulationWorld.Tick(...)` 已收敛为战斗内唯一仿真执行入口。 -- `UseSimulationMovement` 不再承担运行时双路径路由职责,不应再作为回滚到旧 `MovementComponent` 路径的开关理解。 +- 旧的 `UseSimulationMovement` 兼容属性已删除;运行时不再暴露“是否启用 SimulationWorld 移动”的壳层开关。 - 敌人、投射物与目标查询的运行时行为统一以 `SimulationWorld` 主容器和 Burst Job 管线为准。 - 验证建议:聚焦单一路径下的敌人移动、投射物生命周期、最近敌查询和 area hit 结果,而不是做旧路径 A/B 对照。 @@ -50,3 +50,4 @@ - Job/Burst 第一优先级:`MoveSeperation` 阶段并行化。 - 保持阶段边界不变:继续维持四阶段管线与 `ProfilerMarker`,避免失去对比口径。 - 保持生命周期/索引规则不变:`EntitySync` 与 swap-back/remap 继续作为硬约束。 + diff --git a/docs/TodoList.md b/docs/TodoList.md index 6aa9d80..740a13a 100644 --- a/docs/TodoList.md +++ b/docs/TodoList.md @@ -40,7 +40,7 @@ - [x] Checkpoint 3:建立 Simulation 主更新入口并接入 Battle 状态 - 在 `GameStateBattle.OnUpdate` 中增加 `SimulationWorld.Tick(...)` 调用。 - 先只接“敌人移动/追踪”系统,其他逻辑保持原路径。 - - 路线已收敛:不再维护 `UseSimulationMovement` 作为运行时 A/B 与回滚开关。 + - 路线已收敛:`UseSimulationMovement` 兼容属性已移除,运行时不再保留 A/B 与回滚开关壳层。 - 完成标准:`SimulationWorld.Tick(...)` 成为唯一执行入口,敌人仍能正常追踪玩家。 - [x] Checkpoint 4:迁移敌人核心移动逻辑到 Simulation(去 MonoBehaviour 核心逻辑) @@ -72,13 +72,13 @@ ## 2.5 P1.5 Simulation 收尾(P2 前置) - [x] Checkpoint 1:清理 `TickEnemies` 侧 GC(优先级最高) - 目标:将 `TickEnemies GC` 从当前 `27~108 KB` 降到 `< 5 KB / frame`。 - - 重点文件:`Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs`。 + - 历史热点已收口到 `SimulationWorld` 内部敌人分离管线,不再维护独立 legacy solver 文件。 - 处理方式:桶容器与临时列表复用(包含 bucket list 复用池),避免每帧重建集合。 - 完成标准:`2k` 敌人压测下 `TickEnemies GC` 稳定 `< 5 KB / frame`。 - [x] Checkpoint 2:解耦 Simulation 核心与 `Transform` 运行时依赖 - 目标:`SimulationWorld.TickEnemies` 不直接读取或写入 `Transform`。 - - 重点文件:`Assets/GameMain/Scripts/Simulation/SimulationWorld.cs`、`Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs`、`Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs`。 + - 当前重点文件:`Assets/GameMain/Scripts/Simulation/SimulationWorld.cs` 及其敌人分离/数据通道实现;legacy provider/interface 已删除。 - 处理方式:互斥求解输入改为纯数据(位置/半径/索引),`Transform` 仅在 Presentation 阶段回写。 - 完成标准:`TickEnemies` 热路径中不出现 `Transform` 访问。 @@ -241,3 +241,4 @@ ## 测试命令 - PlayMode: `& "C:\UnityProjects\Unity Editor\2022.3.62f3c1\Editor\Unity.exe" -batchmode -nographics -projectPath . -runTests -testPlatform PlayMode -testResults Logs/playmode-test-results.xml -logFile Logs/playmode-tests.log` - EditMode: `& "C:\UnityProjects\Unity Editor\2022.3.62f3c1\Editor\Unity.exe" -batchmode -nographics -projectPath . -runTests -testPlatform EditMode -testResults Logs/editmode-test-results.xml -logFile Logs/editmode-tests.log` + diff --git a/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/.openspec.yaml b/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/.openspec.yaml new file mode 100644 index 0000000..6a5db8c --- /dev/null +++ b/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-02 diff --git a/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/design.md b/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/design.md new file mode 100644 index 0000000..f0567e7 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/design.md @@ -0,0 +1,48 @@ +## Context + +`SimulationWorld` 的运行时收敛已经完成,但代码和文档层面仍留有三类误导性遗留:`SimulationWorld.UseSimulationMovement` 这类恒真属性、`EnemyBase.IsSimulationMovementEnabled()` 这类恒真帮助方法,以及 `EnemySeparationSolverProvider` / `IEnemySeparationSolver` 与两个 legacy solver 实现。这些残留类型不再参与真实运行时调度,却继续把当前架构表述成“单路径之上还保留一套可切换兼容层”。 + +本次变更是一次尾部收口,不重新设计仿真数据流,也不扩展新的 solver 能力;目标是让代码表面与已经落地的运行时事实保持一致。 + +## Goals / Non-Goals + +**Goals:** +- 删除仍对外暴露旧路径语义的兼容属性和帮助方法。 +- 删除不再被运行时使用的 enemy separation provider/interface/legacy solver 类型。 +- 让测试和文档表达与当前单一路径实现一致。 +- 把影响收敛在 `SimulationWorld`、enemy runtime、legacy solver 文件和对应回归覆盖内。 + +**Non-Goals:** +- 不在本次 change 中处理 Unity 场景序列化残留字段。 +- 不重做 `SimulationWorld` 的敌人分离算法或数据结构。 +- 不引入新的运行时调试面板、配置项或回滚开关。 + +## Decisions + +### Remove compatibility members instead of renaming them +直接删除 `UseSimulationMovement` 和 `IsSimulationMovementEnabled()`,而不是把它们改名为新的恒真语义成员。原因是这些成员的唯一历史价值就是表达“可选择是否启用 SimulationWorld”,继续保留只会延长错误心智模型。替代方案是保留只读属性并在注释里声明恒真,但这仍会让调用方继续围绕“是否启用”写分支,因此不采用。 + +### Delete legacy solver types rather than keep them as dead abstractions +`EnemySeparationSolverProvider` 与 `IEnemySeparationSolver` 已不再承载运行时能力,继续保留会产生“还有第二套 enemy separation 入口”的假象。本次直接删除 provider、interface 以及两个实现类,而不是把 provider 改成内部空壳。替代方案是保留文件供历史参考,但仓库历史已经足够承担这个角色,不需要源码继续占位。 + +### Tighten regression coverage around absence of legacy entry points +回归重点不是验证某个字段恒真,而是验证调用面已经不再依赖这些兼容入口。因此测试和文档只覆盖单路径可观察行为,并显式移除对旧壳层 API 的引用。替代方案是增加“成员不存在”的反射测试,但那类测试脆弱且价值低,不采用。 + +## Risks / Trade-offs + +- [外部代码仍引用这些壳层成员] → 在实现前用全文检索清理调用点,并通过编译验证所有受影响程序集。 +- [删除 legacy solver 文件后,文档或测试仍残留旧名称] → 同步更新 `docs/` 和 `Assets/Tests/Simulation/` 中的直接引用。 +- [未来有人希望恢复独立 enemy separation 实验入口] → 若确实需要,应以新的 `SimulationWorld` 内部实验点重新设计,而不是恢复旧 provider 抽象。 + +## Migration Plan + +1. 删除 `UseSimulationMovement` 与 `IsSimulationMovementEnabled()` 及其剩余引用。 +2. 删除 `EnemySeparationSolverProvider`、`IEnemySeparationSolver` 与 legacy solver 实现文件。 +3. 更新受影响测试与文档,使其不再依赖这些符号。 +4. 通过编译与相关仿真测试验证仓库仍在单路径语义下工作。 + +无需运行时迁移或数据迁移;这是源码级收口。回滚方式仅为恢复该 change 的提交,不提供运行时开关回退。 + +## Open Questions + +- None. diff --git a/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/proposal.md b/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/proposal.md new file mode 100644 index 0000000..9706adf --- /dev/null +++ b/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/proposal.md @@ -0,0 +1,26 @@ +## Why + +`SimulationWorld` 已经成为唯一运行时执行路径,但仓库里仍保留 `UseSimulationMovement`、`EnemyBase.IsSimulationMovementEnabled()` 以及 `EnemySeparationSolverProvider` / `IEnemySeparationSolver` 这类旧路径壳层。它们不再承载真实运行时能力,却继续制造双路径仍可恢复的错误信号,也增加后续维护和阅读成本。 + +## What Changes + +- 删除 `SimulationWorld.UseSimulationMovement` 这类恒真兼容属性,改为直接暴露单路径语义。 +- 删除 `EnemyBase.IsSimulationMovementEnabled()` 及其调用点,去除敌人运行时代码里残留的旧路径判断壳层。 +- 删除 `EnemySeparationSolverProvider`、`IEnemySeparationSolver` 及其 legacy solver 实现,明确敌人间分离仅由 `SimulationWorld` 负责。 +- 更新测试与文档,确保回归覆盖和架构说明不再引用这些兼容壳层或 legacy solver 接口。 + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `simulationworld-runtime-convergence`: 收紧单路径运行时要求,明确不得保留可被误解为旧路径开关、能力接口或 solver 提供器的兼容壳层。 + +## Impact + +- Affected code: `Assets/GameMain/Scripts/Simulation`, `Assets/GameMain/Scripts/Entity/EntityLogic/Enemy`, `Assets/GameMain/Scripts/Utility/EnemySeperator`, and related tests/docs. +- APIs: removes compatibility-facing members that still imply legacy movement routing or solver substitution. +- Systems: clarifies that enemy separation and movement execution stay exclusively on the `SimulationWorld` path. diff --git a/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/specs/simulationworld-runtime-convergence/spec.md b/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/specs/simulationworld-runtime-convergence/spec.md new file mode 100644 index 0000000..4e0ff51 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/specs/simulationworld-runtime-convergence/spec.md @@ -0,0 +1,31 @@ +## MODIFIED 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 or expose compatibility switches that imply such a runtime path still exists. + +#### 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 + +#### Scenario: Runtime API does not expose legacy movement enablement shims +- **WHEN** gameplay runtime code integrates with movement simulation +- **THEN** it does not depend on compatibility members whose purpose is to report whether `SimulationWorld` movement is enabled + +### Requirement: Runtime surfaces SHALL reflect the single-path architecture +Runtime debug surfaces, automated tests, architecture documents, and compatibility-facing runtime APIs MUST reflect that the project supports one authoritative `SimulationWorld` execution path rather than dual-path behavior, and MUST NOT preserve legacy solver provider abstractions that imply an alternate runtime separation path is still supported. + +#### 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 + +#### Scenario: Runtime codebase omits legacy solver provider abstractions +- **WHEN** enemy separation behavior is implemented or documented +- **THEN** it is described as `SimulationWorld`-owned runtime behavior without referencing `EnemySeparationSolverProvider`, `IEnemySeparationSolver`, or equivalent legacy provider abstractions diff --git a/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/tasks.md b/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/tasks.md new file mode 100644 index 0000000..28ae9b1 --- /dev/null +++ b/openspec/changes/archive/2026-04-02-remove-simulationworld-legacy-movement-shims/tasks.md @@ -0,0 +1,17 @@ +## 1. Runtime API cleanup + +- [x] 1.1 Remove `SimulationWorld.UseSimulationMovement` and any remaining runtime call sites that branch on that compatibility property. +- [x] 1.2 Remove `EnemyBase.IsSimulationMovementEnabled()` and update enemy runtime code to rely directly on single-path `SimulationWorld` behavior. + +## 2. Legacy solver removal + +- [x] 2.1 Delete `EnemySeparationSolverProvider`, `IEnemySeparationSolver`, and the legacy solver implementations from `Assets/GameMain/Scripts/Utility/EnemySeperator/`. +- [x] 2.2 Clean up any compile-time references, comments, or documentation text that still mention the removed legacy solver provider abstractions. + +## 3. Regression and documentation alignment + +- [x] 3.1 Update simulation/runtime tests so they no longer reference removed compatibility members and still cover observable single-path behavior. +- [x] 3.2 Update architecture and roadmap docs to state that no compatibility movement switch or legacy enemy separation provider remains in the runtime codebase. +- [x] 3.3 Run a build and targeted verification for the affected simulation/runtime surface. + + diff --git a/openspec/specs/simulationworld-runtime-convergence/spec.md b/openspec/specs/simulationworld-runtime-convergence/spec.md index 75acd12..4480c54 100644 --- a/openspec/specs/simulationworld-runtime-convergence/spec.md +++ b/openspec/specs/simulationworld-runtime-convergence/spec.md @@ -7,7 +7,7 @@ Define the battle runtime contract that `SimulationWorld` is the single authorit ## 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. +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 or expose compatibility switches that imply such a runtime path still exists. #### Scenario: Battle tick advances through SimulationWorld - **WHEN** the battle update loop advances a gameplay frame @@ -17,6 +17,10 @@ The battle runtime MUST execute movement, projectile stepping, collision broad-p - **WHEN** runtime configuration related to simulation movement is evaluated - **THEN** it does not select or re-enable a separate legacy movement execution path +#### Scenario: Runtime API does not expose legacy movement enablement shims +- **WHEN** gameplay runtime code integrates with movement simulation +- **THEN** it does not depend on compatibility members whose purpose is to report whether `SimulationWorld` movement is enabled + ### 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. @@ -40,7 +44,7 @@ Target selection, projectile hits, area hits, and sector hits MUST use `Simulati - **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. +Runtime debug surfaces, automated tests, architecture documents, and compatibility-facing runtime APIs MUST reflect that the project supports one authoritative `SimulationWorld` execution path rather than dual-path behavior, and MUST NOT preserve legacy solver provider abstractions that imply an alternate runtime separation path is still supported. #### Scenario: Debug panel omits legacy solver controls - **WHEN** runtime simulation debugging is displayed @@ -49,3 +53,7 @@ Runtime debug surfaces, automated tests, and architecture documents MUST reflect #### 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 + +#### Scenario: Runtime codebase omits legacy solver provider abstractions +- **WHEN** enemy separation behavior is implemented or documented +- **THEN** it is described as `SimulationWorld`-owned runtime behavior without referencing `EnemySeparationSolverProvider`, `IEnemySeparationSolver`, or equivalent legacy provider abstractions From af1fb7bf9db87fddf59bb5a5ba2721cca857b6c8 Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Thu, 2 Apr 2026 12:52:23 +0800 Subject: [PATCH 3/4] fix test sample --- .../SimulationWorld.TransformSync.cs | 28 +++++++++---------- .../Simulation/SimulationWorld.EntitySync.cs | 27 +++++++++++++++--- .../EditMode/SimulationWorldTickTests.cs | 6 +--- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/Assets/GameMain/Scripts/Simulation/Presentation/SimulationWorld.TransformSync.cs b/Assets/GameMain/Scripts/Simulation/Presentation/SimulationWorld.TransformSync.cs index 11b80ed..308d556 100644 --- a/Assets/GameMain/Scripts/Simulation/Presentation/SimulationWorld.TransformSync.cs +++ b/Assets/GameMain/Scripts/Simulation/Presentation/SimulationWorld.TransformSync.cs @@ -22,25 +22,23 @@ namespace Simulation } var enemyManager = GameEntry.EnemyManager; - if (enemyManager == null || enemyManager.Enemies == null) + if (enemyManager != null && enemyManager.Enemies != null) { - return; - } - - var enemies = enemyManager.Enemies; - foreach (var enemy in enemies) - { - if (enemy is not EnemyBase enemyEntity || !enemyEntity.Available) + var enemies = enemyManager.Enemies; + foreach (var enemy in enemies) { - continue; - } + if (enemy is not EnemyBase enemyEntity || !enemyEntity.Available) + { + continue; + } - if (!_world.TryGetEnemyData(enemyEntity.Id, out EnemySimData enemyData)) - { - continue; - } + if (!_world.TryGetEnemyData(enemyEntity.Id, out EnemySimData enemyData)) + { + continue; + } - ApplyEnemyPresentation(enemyEntity, enemyData); + ApplyEnemyPresentation(enemyEntity, enemyData); + } } var projectiles = _world._projectiles; diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs b/Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs index d8a3290..ef8bfde 100644 --- a/Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs @@ -16,6 +16,7 @@ namespace Simulation private const string EnemyProjectileGroupName = "EnemyProjectile"; private readonly SimulationWorld _world; + private bool _isEventSubscribed; public EntitySync(SimulationWorld world) { @@ -24,14 +25,32 @@ namespace Simulation public void OnStart() { - GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); - GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); + var eventComponent = GameEntry.Event; + if (eventComponent == null) + { + return; + } + + eventComponent.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); + eventComponent.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); + _isEventSubscribed = true; } public void OnDestroy() { - GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); - GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); + if (!_isEventSubscribed) + { + return; + } + + var eventComponent = GameEntry.Event; + if (eventComponent != null) + { + eventComponent.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); + eventComponent.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); + } + + _isEventSubscribed = false; } private void OnShowEntitySuccess(object sender, GameEventArgs e) diff --git a/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs b/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs index 6358291..2d77ba2 100644 --- a/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs +++ b/Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs @@ -133,11 +133,7 @@ namespace Simulation.Tests.Editor _world.Tick(new SimulationTickContext(0.5f, 0.5f, Vector3.zero)); - 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)); + Assert.That(_world.Projectiles, Is.Empty); } [Test] From 6d6fa89fc5d7cc909773e7ad4e8162fb39d09662 Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Thu, 2 Apr 2026 13:26:27 +0800 Subject: [PATCH 4/4] Update NearestTargetSelector.cs --- .../TargetSelector/NearestTargetSelector.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/TargetSelector/NearestTargetSelector.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/TargetSelector/NearestTargetSelector.cs index b4c7874..1f8f080 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/TargetSelector/NearestTargetSelector.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/TargetSelector/NearestTargetSelector.cs @@ -13,7 +13,26 @@ namespace Entity.Weapon return null; } - return TrySelectFromSpatialIndex(weapon, maxSqrRange, out EntityBase indexedTarget) ? indexedTarget : 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; } private static bool TrySelectFromSpatialIndex(WeaponBase weapon, float maxSqrRange, out EntityBase target)