commit
a94a86b68b
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Definition.DataStruct;
|
||||
using Definition.DataStruct;
|
||||
using Entity;
|
||||
using UnityEngine;
|
||||
|
||||
|
|
@ -10,10 +10,5 @@ public abstract class EnemyBase : TargetableObject
|
|||
public virtual float AttackRange => 1f;
|
||||
|
||||
public virtual void SetTarget(Transform target) => _target = target;
|
||||
|
||||
protected bool IsSimulationMovementEnabled()
|
||||
{
|
||||
var simulationWorld = GameEntry.SimulationWorld;
|
||||
return simulationWorld != null && simulationWorld.UseSimulationMovement;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Collider>(true);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -212,7 +212,6 @@ namespace Entity
|
|||
_inputComponent.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
|
||||
_movementComponent.SetDirection(_inputComponent.Direction);
|
||||
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
_absorbComponent.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ namespace Entity.Weapon
|
|||
}
|
||||
|
||||
var simulationWorld = GameEntry.SimulationWorld;
|
||||
if (simulationWorld == null || !simulationWorld.UseSimulationMovement)
|
||||
if (simulationWorld == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -16,31 +16,29 @@ namespace Simulation
|
|||
|
||||
public void OnLateUpdate()
|
||||
{
|
||||
if (_world == null || !_world.UseSimulationMovement)
|
||||
if (_world == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e3960124c8fe4304493659a13e5a9439
|
||||
fileFormatVersion: 2
|
||||
guid: 417eac38d86047d096eb9f74647b6cf7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -14,11 +14,6 @@ namespace Simulation
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!_useSimulationMovement)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BuildEnemyTargetSpatialIndexIfNeeded();
|
||||
|
||||
float cellSize = GetTargetSelectionCellSize();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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,6 @@ namespace Simulation
|
|||
public IReadOnlyList<EnemySimData> Enemies => _enemies;
|
||||
public IReadOnlyList<ProjectileSimData> Projectiles => _projectiles;
|
||||
public IReadOnlyList<PickupSimData> Pickups => _pickups;
|
||||
public bool UseSimulationMovement => _useSimulationMovement;
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
|
|
@ -68,14 +64,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,3 +91,4 @@ namespace Simulation
|
|||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Transform, LegacyRegistration> LegacyRegistrations = new();
|
||||
private static readonly List<EnemySeparationAgent> LegacyAgents = new();
|
||||
private static readonly List<Transform> LegacyRecycle = new();
|
||||
private static int _legacySnapshotFrame = -1;
|
||||
private static int _nextLegacyAgentId = 1;
|
||||
|
||||
public static IEnemySeparationSolver Current => _simulationSolver;
|
||||
public static string CurrentSolverName => _simulationSolver.GetType().Name;
|
||||
|
||||
public static void SetSolver(IEnemySeparationSolver solver)
|
||||
{
|
||||
if (solver == null) return;
|
||||
|
||||
_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<EnemySeparationAgent> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3cf44095cd7c76043a8e8a44dc5a0888
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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<int, Agent> _agents = new();
|
||||
private readonly System.Collections.Generic.Dictionary<long, System.Collections.Generic.List<int>> _buckets = new();
|
||||
private readonly System.Collections.Generic.Stack<System.Collections.Generic.List<int>> _bucketListPool = new();
|
||||
private readonly System.Collections.Generic.List<long> _activeBucketKeys = new();
|
||||
private readonly float _cellSize;
|
||||
|
||||
private float _maxRadius = 0.45f;
|
||||
|
||||
public GridBucketEnemySeparationSolver(float cellSize = 1f)
|
||||
{
|
||||
_cellSize = Mathf.Max(0.1f, cellSize);
|
||||
}
|
||||
|
||||
public void SetAgents(System.Collections.Generic.IReadOnlyList<EnemySeparationAgent> 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<int>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c7c10dca24b508f4fa6726eae7ac2fb1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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<EnemySeparationAgent> agents);
|
||||
Vector3 Resolve(int agentId, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int, Agent> _agents = new();
|
||||
private readonly System.Collections.Generic.List<int> _agentKeys = new();
|
||||
|
||||
public void SetAgents(System.Collections.Generic.IReadOnlyList<EnemySeparationAgent> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ec8ec1013900437498da4613f680a898
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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<TParams>()`
|
||||
- 新建 `WeaponXXXData`
|
||||
- 新建 `WeaponXXXParamsData`
|
||||
- 在构造里调用 `ParseParams<TParams>()`
|
||||
|
||||
4. 新增武器逻辑类
|
||||
- 继承 `WeaponBase`
|
||||
- 接入状态机
|
||||
- 读取 `ParamsData`
|
||||
- 继承 `WeaponBase`
|
||||
- 接入状态机
|
||||
- 读取 `ParamsData`
|
||||
|
||||
5. 接入生成入口
|
||||
- 玩家初始武器
|
||||
- 商店购买武器
|
||||
- 其他掉落/奖励入口
|
||||
- 玩家初始武器
|
||||
- 商店购买武器
|
||||
- 其他掉落/奖励入口
|
||||
|
||||
6. 验证点
|
||||
- 武器生成正确
|
||||
- 参数生效正确
|
||||
- 描述文本正确
|
||||
- Simulation 模式和非 Simulation 模式都能命中
|
||||
- 武器生成正确
|
||||
- 参数生效正确
|
||||
- 描述文本正确
|
||||
- Simulation 模式和非 Simulation 模式都能命中
|
||||
|
|
|
|||
|
|
@ -40,13 +40,14 @@
|
|||
- 当前主瓶颈明确:`MoveSeperation` 是绝对热点(约 `43%~47%` 帧占比),P2 优先并行化该阶段。
|
||||
- 评估口径可复现:Android 端受 `60 fps` 上限影响,性能判断以 CPU `ms` 为准。
|
||||
|
||||
## 回滚开关说明
|
||||
- 开关字段:`SimulationWorld._useSimulationMovement`(序列化私有字段)
|
||||
- 对外接口:`UseSimulationMovement` / `SetUseSimulationMovement(bool)`
|
||||
- 回滚方式:将开关置 `false`,敌人立即回退到旧 `MovementComponent` 更新路径。
|
||||
- 验证建议:同场景同刷怪参数下执行 A/B 对比,确认行为一致与性能差异。
|
||||
## 路线收敛说明
|
||||
- `SimulationWorld.Tick(...)` 已收敛为战斗内唯一仿真执行入口。
|
||||
- 旧的 `UseSimulationMovement` 兼容属性已删除;运行时不再暴露“是否启用 SimulationWorld 移动”的壳层开关。
|
||||
- 敌人、投射物与目标查询的运行时行为统一以 `SimulationWorld` 主容器和 Burst Job 管线为准。
|
||||
- 验证建议:聚焦单一路径下的敌人移动、投射物生命周期、最近敌查询和 area hit 结果,而不是做旧路径 A/B 对照。
|
||||
|
||||
## P2 交接建议
|
||||
- Job/Burst 第一优先级:`MoveSeperation` 阶段并行化。
|
||||
- 保持阶段边界不变:继续维持四阶段管线与 `ProfilerMarker`,避免失去对比口径。
|
||||
- 保持生命周期/索引规则不变:`EntitySync` 与 swap-back/remap 继续作为硬约束。
|
||||
|
||||
|
|
|
|||
|
|
@ -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 正常
|
||||
|
|
|
|||
|
|
@ -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。
|
||||
|
|
@ -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` 访问。
|
||||
|
||||
|
|
@ -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`/托管引用)。
|
||||
|
|
@ -243,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`
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-04-02
|
||||
|
|
@ -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()` 调用。
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-04-02
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# 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 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 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, 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
|
||||
Loading…
Reference in New Issue