Cleanup 2

This commit is contained in:
SepComet 2026-04-02 12:36:01 +08:00
parent 1052cc0136
commit ffcd4e6b54
22 changed files with 246 additions and 598 deletions

View File

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

View File

@ -1,4 +1,4 @@
using Definition.DataStruct; using Definition.DataStruct;
using Entity; using Entity;
using UnityEngine; using UnityEngine;
@ -10,9 +10,5 @@ public abstract class EnemyBase : TargetableObject
public virtual float AttackRange => 1f; public virtual float AttackRange => 1f;
public virtual void SetTarget(Transform target) => _target = target; public virtual void SetTarget(Transform target) => _target = target;
protected bool IsSimulationMovementEnabled()
{
return true;
}
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using CustomDebugger; using CustomDebugger;
using UnityEngine; using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
@ -8,28 +8,28 @@ namespace Simulation
public sealed partial class SimulationWorld : GameFrameworkComponent public sealed partial class SimulationWorld : GameFrameworkComponent
{ {
// Partial layout: // Partial layout:
// - SimulationWorld.cs: 核心状态、常量和 Unity 生命周期入口点。 // - SimulationWorld.cs: 鏍稿績鐘舵€併€佸父閲忓拰 Unity 鐢熷懡鍛ㄦ湡鍏ュ彛鐐广€?
// - SimulationWorld.RuntimeModules.cs: 运行时域对象、配置和状态代理。 // - SimulationWorld.RuntimeModules.cs: 杩愯鏃跺煙瀵硅薄銆侀厤缃拰鐘舵€佷唬鐞嗐€?
// - SimulationWorld.SimEntityState.cs: 模拟状态的增删改查和生命周期注册。 // - SimulationWorld.SimEntityState.cs: 妯℃嫙鐘舵€佺殑澧炲垹鏀规煡鍜岀敓鍛藉懆鏈熸敞鍐屻€?
// - SimulationWorld.EntityToSimData.cs: Unity 实体到 sim data 的初始化适配。 // - SimulationWorld.EntityToSimData.cs: Unity 瀹炰綋鍒?sim data 鐨勫垵濮嬪寲閫傞厤銆?
// - SimulationWorld.EntitySync.cs: GameFramework 实体 show/hide 事件桥。 // - SimulationWorld.EntitySync.cs: GameFramework 瀹炰綋 show/hide 浜嬩欢妗ャ€?
// - SimulationWorld.TargetSelectionSpatialIndex.cs: 最近敌空间索引查询。 // - SimulationWorld.TargetSelectionSpatialIndex.cs: 鏈€杩戞晫绌洪棿绱㈠紩鏌ヨ銆?
// - Presentation/SimulationWorld.TransformSync.cs: late-update transform 同步桥。 // - Presentation/SimulationWorld.TransformSync.cs: late-update transform 鍚屾妗ャ€?
// - Presentation/SimulationWorld.HitPresentation.cs: 投射物命中事件表现桥。 // - Presentation/SimulationWorld.HitPresentation.cs: 鎶曞皠鐗╁懡涓簨浠惰〃鐜版ˉ銆?
// - DataChannel/SimulationWorld.JobDataChannel.cs: Job 通道共享字段、常量和运行时状态。 // - DataChannel/SimulationWorld.JobDataChannel.cs: Job 閫氶亾鍏变韩瀛楁銆佸父閲忓拰杩愯鏃剁姸鎬併€?
// - DataChannel/SimulationWorld.JobDataLifecycle.cs: Native 通道初始化、清理和 clear。 // - DataChannel/SimulationWorld.JobDataLifecycle.cs: Native 閫氶亾鍒濆鍖栥€佹竻鐞嗗拰 clear銆?
// - DataChannel/SimulationWorld.JobDataConversion.cs: sim/job 数据转换与输入输出缓冲准备。 // - DataChannel/SimulationWorld.JobDataConversion.cs: sim/job 鏁版嵁杞崲涓庤緭鍏ヨ緭鍑虹紦鍐插噯澶囥€?
// - DataChannel/SimulationWorld.CollisionTransient.cs: 碰撞临时通道和运行时统计。 // - DataChannel/SimulationWorld.CollisionTransient.cs: 纰版挒涓存椂閫氶亾鍜岃繍琛屾椂缁熻銆?
// - DataChannel/SimulationWorld.EnemySeparationTemporal.cs: 敌人分离的帧间临时状态。 // - DataChannel/SimulationWorld.EnemySeparationTemporal.cs: 鏁屼汉鍒嗙鐨勫抚闂翠复鏃剁姸鎬併€?
// - DataChannel/SimulationWorld.JobOutputCommit.cs: Job 输出回写主容器。 // - DataChannel/SimulationWorld.JobOutputCommit.cs: Job 杈撳嚭鍥炲啓涓诲鍣ㄣ€?
// - Jobs/SimulationWorld.EnemyJobs.cs: 模拟通道 编排 + 敌人移动/分离 顺序执行 // - Jobs/SimulationWorld.EnemyJobs.cs: 妯℃嫙閫氶亾 缂栨帓 + 鏁屼汉绉诲姩/鍒嗙 椤哄簭鎵ц
// - Jobs/SimulationWorld.ProjectileJobs.cs: 投射物移动与回收 // - Jobs/SimulationWorld.ProjectileJobs.cs: 鎶曞皠鐗╃Щ鍔ㄤ笌鍥炴敹
// - Jobs/SimulationWorld.CollisionPipeline.cs: 碰撞管线共享配置和状态 // - Jobs/SimulationWorld.CollisionPipeline.cs: 纰版挒绠$嚎鍏变韩閰嶇疆鍜岀姸鎬?
// - Jobs/SimulationWorld.CollisionRequests.cs: area/sector 请求缓冲 // - Jobs/SimulationWorld.CollisionRequests.cs: area/sector 璇锋眰缂撳啿
// - Jobs/SimulationWorld.CollisionBroadPhase.cs: broad-phase 候选构建和 Job 调度 // - Jobs/SimulationWorld.CollisionBroadPhase.cs: broad-phase 鍊欓€夋瀯寤哄拰 Job 璋冨害
// - Jobs/SimulationWorld.CollisionResolve.cs: 主线程命中结算与 area settle // - Jobs/SimulationWorld.CollisionResolve.cs: 涓荤嚎绋嬪懡涓粨绠椾笌 area settle
// - Jobs/SimulationWorld.CollisionPresentation.cs: 命中表现事件和实体/impact 解析 // - Jobs/SimulationWorld.CollisionPresentation.cs: 鍛戒腑琛ㄧ幇浜嬩欢鍜屽疄浣?impact 瑙f瀽
// - JobStruct/*.cs: burst job 内核和面向 job 的数据结构 // - JobStruct/*.cs: burst job 鍐呮牳鍜岄潰鍚?job 鐨勬暟鎹粨鏋?
private const float DefaultAttackRange = 1f; private const float DefaultAttackRange = 1f;
private const int EnemyStateIdle = 0; private const int EnemyStateIdle = 0;
private const int EnemyStateChasing = 1; private const int EnemyStateChasing = 1;
@ -44,7 +44,6 @@ namespace Simulation
public IReadOnlyList<EnemySimData> Enemies => _enemies; public IReadOnlyList<EnemySimData> Enemies => _enemies;
public IReadOnlyList<ProjectileSimData> Projectiles => _projectiles; public IReadOnlyList<ProjectileSimData> Projectiles => _projectiles;
public IReadOnlyList<PickupSimData> Pickups => _pickups; public IReadOnlyList<PickupSimData> Pickups => _pickups;
public bool UseSimulationMovement => true;
#region Lifecycle #region Lifecycle
@ -92,3 +91,4 @@ namespace Simulation
#endregion #endregion
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
using System.Reflection; using System.Reflection;
using Components; using Components;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine; using UnityEngine;
@ -88,7 +88,7 @@ namespace Simulation.Tests.Editor
} }
[Test] [Test]
public void SyncEnemyMovementInput_DisablesEnemyMovementOnSimulationPath() public void SyncEnemyMovementInput_DisablesEnemyMovement()
{ {
UpsertEnemy(new EnemySimData UpsertEnemy(new EnemySimData
{ {
@ -166,3 +166,4 @@ namespace Simulation.Tests.Editor
} }
} }
} }

View File

@ -1,4 +1,4 @@
using System.Collections; using System.Collections;
using System.Reflection; using System.Reflection;
using Entity; using Entity;
using Entity.EntityData; using Entity.EntityData;
@ -99,7 +99,7 @@ namespace Simulation.Tests.PlayMode
} }
[UnityTest] [UnityTest]
public IEnumerator Tick_RespectsEnemyMovementSyncFromComponentShell() public IEnumerator Tick_RespectsEnemyMovementSync()
{ {
_world.SyncEnemyMovementInput(4001, false, Vector3.left, 5f, true, 0.45f, 2); _world.SyncEnemyMovementInput(4001, false, Vector3.left, 5f, true, 0.45f, 2);
@ -133,3 +133,4 @@ namespace Simulation.Tests.PlayMode
} }
} }
} }

View File

@ -187,21 +187,21 @@
### 推荐实施顺序 ### 推荐实施顺序
1. 先确认唯一执行路径 1. 先确认唯一执行路径
- `SimulationWorld` Burst 管线作为唯一运行时执行路径 - `SimulationWorld` Burst 管线作为唯一运行时执行路径
- 实体和组件只保留输入、注册、表现职责 - 实体和组件只保留输入、注册、表现职责
2. 再处理战斗入口 2. 再处理战斗入口
- 先改 `GameStateBattle` / `GameEntry` / `ProcedureGame` - 先改 `GameStateBattle` / `GameEntry` / `ProcedureGame`
- 让运行时明确依赖当前 Burst 管线,不再保留双路径语义 - 让运行时明确依赖当前 Burst 管线,不再保留双路径语义
3. 再清理旧组件驱动路径 3. 再清理旧组件驱动路径
- 先收敛 `MovementComponent` - 先收敛 `MovementComponent`
- 再删敌人/玩家/投射物实体里的自驱动移动 - 再删敌人/玩家/投射物实体里的自驱动移动
- 再删旧 fallback 查询和旧互斥 solver - 再删旧 fallback 查询和旧互斥 solver
4. 最后重建测试和文档 4. 最后重建测试和文档
- 先让行为稳定 - 先让行为稳定
- 再补新的回归测试和文档 - 再补新的回归测试和文档
### 当前建议 ### 当前建议
@ -355,72 +355,72 @@
优先顺序建议: 优先顺序建议:
1. 长枪 / 刺剑 1. 长枪 / 刺剑
- 基于 `WeaponKnife` - 基于 `WeaponKnife`
- 重点调: - 重点调:
- 前刺距离 - 前刺距离
- 命中半径 - 命中半径
- 冷却 - 冷却
2. 大剑 / 半月斩 2. 大剑 / 半月斩
- 基于 `WeaponSlash` - 基于 `WeaponSlash`
- 重点调: - 重点调:
- `SectorAngle` - `SectorAngle`
- 攻击范围 - 攻击范围
- 动画时长 - 动画时长
3. 战锤 / 震地锤 3. 战锤 / 震地锤
- 基于 `WeaponLightning``WeaponKnife` - 基于 `WeaponLightning``WeaponKnife`
- 重点调: - 重点调:
- 落点半径 - 落点半径
- 前摇 - 前摇
- 低频高伤 - 低频高伤
4. 霰弹枪 4. 霰弹枪
- 基于参数化后的 `WeaponHandgun` - 基于参数化后的 `WeaponHandgun`
- 重点调: - 重点调:
- 散射 - 散射
- 多 pellet - 多 pellet
- 近距离爆发 - 近距离爆发
5. 狙击枪 5. 狙击枪
- 基于参数化后的 `WeaponHandgun` - 基于参数化后的 `WeaponHandgun`
- 重点调: - 重点调:
- 单发高伤 - 单发高伤
- 超远射程 - 超远射程
- 慢冷却 - 慢冷却
6. 陨石杖 / 圣光柱 6. 陨石杖 / 圣光柱
- 基于 `WeaponLightning` - 基于 `WeaponLightning`
- 重点调: - 重点调:
- `HoverHeight` - `HoverHeight`
- 爆炸半径 - 爆炸半径
- 冷却 - 冷却
### P3: 中成本扩展 ### P3: 中成本扩展
1. 链式闪电 1. 链式闪电
- 在首目标命中后,继续寻找附近目标 - 在首目标命中后,继续寻找附近目标
- 需要新增: - 需要新增:
- 连锁次数 - 连锁次数
- 连锁半径 - 连锁半径
- 每跳衰减 - 每跳衰减
2. 穿透弹 / 火球 2. 穿透弹 / 火球
- 复用现有 projectile/simulation 基础 - 复用现有 projectile/simulation 基础
- 需要明确: - 需要明确:
- 穿透次数 - 穿透次数
- 命中后是否爆炸 - 命中后是否爆炸
3. 地雷 / 陷阱 3. 地雷 / 陷阱
- 本质是延时触发 area hit - 本质是延时触发 area hit
- 需要新增: - 需要新增:
- 布置后触发时机 - 布置后触发时机
- 持续时间 - 持续时间
- 触发半径 - 触发半径
4. 回旋镖 4. 回旋镖
- 需要双阶段投射物状态 - 需要双阶段投射物状态
- 成本高于普通枪械/范围武器 - 成本高于普通枪械/范围武器
### P4: 暂缓项 ### P4: 暂缓项
@ -439,29 +439,29 @@
## 新武器接入步骤模板 ## 新武器接入步骤模板
1. 在 `Weapon.txt` 新增一行 1. 在 `Weapon.txt` 新增一行
- 配好基础字段 - 配好基础字段
- `Params` 写 JSON 对象 - `Params` 写 JSON 对象
2. 新增 `WeaponType` 2. 新增 `WeaponType`
- 在 `Assets/GameMain/Scripts/Definition/Enum/WeaponType.cs` - 在 `Assets/GameMain/Scripts/Definition/Enum/WeaponType.cs`
3. 新增武器数据子类 3. 新增武器数据子类
- 新建 `WeaponXXXData` - 新建 `WeaponXXXData`
- 新建 `WeaponXXXParamsData` - 新建 `WeaponXXXParamsData`
- 在构造里调用 `ParseParams<TParams>()` - 在构造里调用 `ParseParams<TParams>()`
4. 新增武器逻辑类 4. 新增武器逻辑类
- 继承 `WeaponBase` - 继承 `WeaponBase`
- 接入状态机 - 接入状态机
- 读取 `ParamsData` - 读取 `ParamsData`
5. 接入生成入口 5. 接入生成入口
- 玩家初始武器 - 玩家初始武器
- 商店购买武器 - 商店购买武器
- 其他掉落/奖励入口 - 其他掉落/奖励入口
6. 验证点 6. 验证点
- 武器生成正确 - 武器生成正确
- 参数生效正确 - 参数生效正确
- 描述文本正确 - 描述文本正确
- Simulation 模式和非 Simulation 模式都能命中 - Simulation 模式和非 Simulation 模式都能命中

View File

@ -42,7 +42,7 @@
## 路线收敛说明 ## 路线收敛说明
- `SimulationWorld.Tick(...)` 已收敛为战斗内唯一仿真执行入口。 - `SimulationWorld.Tick(...)` 已收敛为战斗内唯一仿真执行入口。
- `UseSimulationMovement` 不再承担运行时双路径路由职责,不应再作为回滚到旧 `MovementComponent` 路径的开关理解 - 旧的 `UseSimulationMovement` 兼容属性已删除;运行时不再暴露“是否启用 SimulationWorld 移动”的壳层开关
- 敌人、投射物与目标查询的运行时行为统一以 `SimulationWorld` 主容器和 Burst Job 管线为准。 - 敌人、投射物与目标查询的运行时行为统一以 `SimulationWorld` 主容器和 Burst Job 管线为准。
- 验证建议:聚焦单一路径下的敌人移动、投射物生命周期、最近敌查询和 area hit 结果,而不是做旧路径 A/B 对照。 - 验证建议:聚焦单一路径下的敌人移动、投射物生命周期、最近敌查询和 area hit 结果,而不是做旧路径 A/B 对照。
@ -50,3 +50,4 @@
- Job/Burst 第一优先级:`MoveSeperation` 阶段并行化。 - Job/Burst 第一优先级:`MoveSeperation` 阶段并行化。
- 保持阶段边界不变:继续维持四阶段管线与 `ProfilerMarker`,避免失去对比口径。 - 保持阶段边界不变:继续维持四阶段管线与 `ProfilerMarker`,避免失去对比口径。
- 保持生命周期/索引规则不变:`EntitySync` 与 swap-back/remap 继续作为硬约束。 - 保持生命周期/索引规则不变:`EntitySync` 与 swap-back/remap 继续作为硬约束。

View File

@ -40,7 +40,7 @@
- [x] Checkpoint 3建立 Simulation 主更新入口并接入 Battle 状态 - [x] Checkpoint 3建立 Simulation 主更新入口并接入 Battle 状态
- 在 `GameStateBattle.OnUpdate` 中增加 `SimulationWorld.Tick(...)` 调用。 - 在 `GameStateBattle.OnUpdate` 中增加 `SimulationWorld.Tick(...)` 调用。
- 先只接“敌人移动/追踪”系统,其他逻辑保持原路径。 - 先只接“敌人移动/追踪”系统,其他逻辑保持原路径。
- 路线已收敛:不再维护 `UseSimulationMovement` 作为运行时 A/B 与回滚开关 - 路线已收敛:`UseSimulationMovement` 兼容属性已移除,运行时不再保留 A/B 与回滚开关壳层
- 完成标准:`SimulationWorld.Tick(...)` 成为唯一执行入口,敌人仍能正常追踪玩家。 - 完成标准:`SimulationWorld.Tick(...)` 成为唯一执行入口,敌人仍能正常追踪玩家。
- [x] Checkpoint 4迁移敌人核心移动逻辑到 Simulation去 MonoBehaviour 核心逻辑) - [x] Checkpoint 4迁移敌人核心移动逻辑到 Simulation去 MonoBehaviour 核心逻辑)
@ -72,13 +72,13 @@
## 2.5 P1.5 Simulation 收尾P2 前置) ## 2.5 P1.5 Simulation 收尾P2 前置)
- [x] Checkpoint 1清理 `TickEnemies` 侧 GC优先级最高 - [x] Checkpoint 1清理 `TickEnemies` 侧 GC优先级最高
- 目标:将 `TickEnemies GC` 从当前 `27~108 KB` 降到 `< 5 KB / frame` - 目标:将 `TickEnemies GC` 从当前 `27~108 KB` 降到 `< 5 KB / frame`
- 重点文件:`Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs` - 历史热点已收口到 `SimulationWorld` 内部敌人分离管线,不再维护独立 legacy solver 文件
- 处理方式:桶容器与临时列表复用(包含 bucket list 复用池),避免每帧重建集合。 - 处理方式:桶容器与临时列表复用(包含 bucket list 复用池),避免每帧重建集合。
- 完成标准:`2k` 敌人压测下 `TickEnemies GC` 稳定 `< 5 KB / frame` - 完成标准:`2k` 敌人压测下 `TickEnemies GC` 稳定 `< 5 KB / frame`
- [x] Checkpoint 2解耦 Simulation 核心与 `Transform` 运行时依赖 - [x] Checkpoint 2解耦 Simulation 核心与 `Transform` 运行时依赖
- 目标:`SimulationWorld.TickEnemies` 不直接读取或写入 `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 阶段回写。 - 处理方式:互斥求解输入改为纯数据(位置/半径/索引),`Transform` 仅在 Presentation 阶段回写。
- 完成标准:`TickEnemies` 热路径中不出现 `Transform` 访问。 - 完成标准:`TickEnemies` 热路径中不出现 `Transform` 访问。
@ -241,3 +241,4 @@
## 测试命令 ## 测试命令
- PlayMode: `& "C:\UnityProjects\Unity Editor\2022.3.62f3c1\Editor\Unity.exe" -batchmode -nographics -projectPath . -runTests -testPlatform PlayMode -testResults Logs/playmode-test-results.xml -logFile Logs/playmode-tests.log` - 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` - 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`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ Define the battle runtime contract that `SimulationWorld` is the single authorit
## Requirements ## Requirements
### Requirement: SimulationWorld SHALL be the sole battle simulation executor ### Requirement: SimulationWorld SHALL be the sole battle simulation executor
The battle runtime MUST execute movement, projectile stepping, collision broad-phase, and related simulation state updates through `SimulationWorld`, and it MUST NOT route these responsibilities through an alternative non-`SimulationWorld` runtime path. The battle runtime MUST execute movement, projectile stepping, collision broad-phase, and related simulation state updates through `SimulationWorld`, and it MUST NOT route these responsibilities through an alternative non-`SimulationWorld` runtime path or expose compatibility switches that imply such a runtime path still exists.
#### Scenario: Battle tick advances through SimulationWorld #### Scenario: Battle tick advances through SimulationWorld
- **WHEN** the battle update loop advances a gameplay frame - **WHEN** the battle update loop advances a gameplay frame
@ -17,6 +17,10 @@ The battle runtime MUST execute movement, projectile stepping, collision broad-p
- **WHEN** runtime configuration related to simulation movement is evaluated - **WHEN** runtime configuration related to simulation movement is evaluated
- **THEN** it does not select or re-enable a separate legacy movement execution path - **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 ### 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. Enemy entities, the player entity, projectile entities, and `MovementComponent` MUST submit movement or behavior input into `SimulationWorld` state and MUST consume position, facing, hit, or lifecycle results from simulation output, rather than computing world-space advancement independently.
@ -40,7 +44,7 @@ Target selection, projectile hits, area hits, and sector hits MUST use `Simulati
- **THEN** hit candidates are produced from `SimulationWorld` collision and query capabilities instead of a fallback entity-side path - **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 ### Requirement: Runtime surfaces SHALL reflect the single-path architecture
Runtime debug surfaces, automated tests, and architecture documents MUST reflect that the project supports one authoritative `SimulationWorld` execution path rather than dual-path behavior. Runtime debug surfaces, automated tests, architecture documents, and compatibility-facing runtime APIs MUST reflect that the project supports one authoritative `SimulationWorld` execution path rather than dual-path behavior, and MUST NOT preserve legacy solver provider abstractions that imply an alternate runtime separation path is still supported.
#### Scenario: Debug panel omits legacy solver controls #### Scenario: Debug panel omits legacy solver controls
- **WHEN** runtime simulation debugging is displayed - **WHEN** runtime simulation debugging is displayed
@ -49,3 +53,7 @@ Runtime debug surfaces, automated tests, and architecture documents MUST reflect
#### Scenario: Regression tests validate observable single-path behavior #### Scenario: Regression tests validate observable single-path behavior
- **WHEN** simulation regression coverage is maintained - **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 - **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