Cleanup 1

This commit is contained in:
SepComet 2026-04-02 11:21:07 +08:00
parent 5ed792b09f
commit 1052cc0136
26 changed files with 600 additions and 2057 deletions

View File

@ -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);
}
}
}
}
}

View File

@ -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;
}

View File

@ -13,7 +13,6 @@ public abstract class EnemyBase : TargetableObject
protected bool IsSimulationMovementEnabled()
{
var simulationWorld = GameEntry.SimulationWorld;
return simulationWorld != null && simulationWorld.UseSimulationMovement;
return true;
}
}

View File

@ -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);
@ -137,4 +105,4 @@ namespace Entity
}
}
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -212,7 +212,6 @@ namespace Entity
_inputComponent.OnUpdate(elapseSeconds, realElapseSeconds);
_movementComponent.SetDirection(_inputComponent.Direction);
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
_absorbComponent.OnUpdate(elapseSeconds, realElapseSeconds);
}

View File

@ -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;
}
}
}
}

View File

@ -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;

View File

@ -16,7 +16,7 @@ namespace Simulation
public void OnLateUpdate()
{
if (_world == null || !_world.UseSimulationMovement)
if (_world == null)
{
return;
}
@ -104,4 +104,4 @@ namespace Simulation
}
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 417eac38d86047d096eb9f74647b6cf7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
}
}
}

View File

@ -14,11 +14,6 @@ namespace Simulation
return false;
}
if (!_useSimulationMovement)
{
return false;
}
BuildEnemyTargetSpatialIndexIfNeeded();
float cellSize = GetTargetSelectionCellSize();

View File

@ -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<EnemySimData> Enemies => _enemies;
public IReadOnlyList<ProjectileSimData> Projectiles => _projectiles;
public IReadOnlyList<PickupSimData> 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);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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` 阶段并行化。

View File

@ -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 @@
#### 用例 110 分钟连续战斗
- 执行时间:待填
- 场景/波次参数:待填
- 运行开关:`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 正常

View File

@ -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 2Simulation 与 Job 数据通道打通(仅建通道,不改行为)
- 为敌人/投射物建立 Job 输入输出结构(纯数据,不含 `Transform`/托管引用)。

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-02

View File

@ -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()` 调用。

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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