diff --git a/Assets/GameMain/Scripts/Base/GameEntry.Custom.cs b/Assets/GameMain/Scripts/Base/GameEntry.Custom.cs index 41ac84e..f4b23b9 100644 --- a/Assets/GameMain/Scripts/Base/GameEntry.Custom.cs +++ b/Assets/GameMain/Scripts/Base/GameEntry.Custom.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------ +//------------------------------------------------------------ // Game Framework // Copyright © 2013-2021 Jiang Yin. All rights reserved. // Homepage: https://gameframework.cn/ @@ -46,7 +46,11 @@ public partial class GameEntry : MonoBehaviour EnemyManager = UnityGameFramework.Runtime.GameEntry.GetComponent(); SimulationWorld = UnityGameFramework.Runtime.GameEntry.GetComponent(); + if (SimulationWorld == null && Base != null) + { + SimulationWorld = Base.gameObject.AddComponent(); + } SpriteCache = UnityGameFramework.Runtime.GameEntry.GetComponent(); UIRouter = UnityGameFramework.Runtime.GameEntry.GetComponent(); } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/CustomComponent/EnemyManager/EnemyManagerComponent.cs b/Assets/GameMain/Scripts/CustomComponent/EnemyManager/EnemyManagerComponent.cs index f917d59..f954cf8 100644 --- a/Assets/GameMain/Scripts/CustomComponent/EnemyManager/EnemyManagerComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/EnemyManager/EnemyManagerComponent.cs @@ -1,12 +1,10 @@ using System.Collections.Generic; -using Components; using DataTable; using Definition.Enum; using Entity; using Entity.EntityData; using GameFramework.Event; using Procedure; -using Simulation; using StarForce; using UnityEngine; using UnityGameFramework.Runtime; @@ -17,6 +15,7 @@ namespace CustomComponent public class EnemyManagerComponent : GameFrameworkComponent { private const float MinSpawnRateScale = 0.1f; + private const string EnemyGroupName = "Enemy"; private EntityComponent _entity; @@ -211,21 +210,14 @@ namespace CustomComponent { if (!(e is ShowEntitySuccessEventArgs ne)) return; - if (ne.Entity.Logic is EnemyBase enemy) + string entityGroupName = ne.Entity?.EntityGroup?.Name; + + if (entityGroupName == EnemyGroupName && ne.Entity.Logic is EnemyBase enemy) { _currentEnemyCount++; enemy.SetTarget(_player); RemoveEnemyFromCache(enemy.Id); _enemies.Add(enemy); - - if (ne.UserData is EnemyData enemyData) - { - GameEntry.SimulationWorld?.UpsertEnemy(CreateEnemySimData(enemy, enemyData)); - } - else - { - GameEntry.SimulationWorld?.UpsertEnemy(CreateEnemySimData(enemy, null)); - } } if (ne.EntityLogicType == typeof(Player)) @@ -238,7 +230,8 @@ namespace CustomComponent { if (e is HideEntityCompleteEventArgs ne) { - if (ne.EntityGroup.Name == "Enemy") + string entityGroupName = ne.EntityGroup.Name; + if (entityGroupName == EnemyGroupName) { if (_currentEnemyCount > 0) { @@ -246,30 +239,10 @@ namespace CustomComponent } RemoveEnemyFromCache(ne.EntityId); - GameEntry.SimulationWorld?.RemoveEnemyByEntityId(ne.EntityId); } } } - private static EnemySimData CreateEnemySimData(EnemyBase enemy, EnemyData enemyData) - { - MovementComponent movementComponent = enemy != null ? enemy.GetComponent() : null; - - return new EnemySimData - { - EntityId = enemy.Id, - Position = enemy.CachedTransform.position, - Forward = enemy.CachedTransform.forward, - Speed = enemyData != null ? enemyData.SpeedBase : 0f, - AttackRange = 1f, - AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap, - EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f, - SeparationIterations = movementComponent != null ? movementComponent.SeparationIterations : 2, - TargetType = 0, - State = 0 - }; - } - private void RemoveEnemyFromCache(int entityId) { for (int i = _enemies.Count - 1; i >= 0; i--) diff --git a/Assets/GameMain/Scripts/Simulation/SimData.meta b/Assets/GameMain/Scripts/Simulation/SimData.meta new file mode 100644 index 0000000..6601a23 --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimData.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e2d0f6135bae4675af78194b2272e280 +timeCreated: 1771590468 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Simulation/EnemySimData.cs b/Assets/GameMain/Scripts/Simulation/SimData/EnemySimData.cs similarity index 92% rename from Assets/GameMain/Scripts/Simulation/EnemySimData.cs rename to Assets/GameMain/Scripts/Simulation/SimData/EnemySimData.cs index 8d0d1e2..69d2ddb 100644 --- a/Assets/GameMain/Scripts/Simulation/EnemySimData.cs +++ b/Assets/GameMain/Scripts/Simulation/SimData/EnemySimData.cs @@ -7,6 +7,7 @@ namespace Simulation public int EntityId; public Vector3 Position; public Vector3 Forward; + public Quaternion Rotation; public float Speed; public float AttackRange; public bool AvoidEnemyOverlap; diff --git a/Assets/GameMain/Scripts/Simulation/EnemySimData.cs.meta b/Assets/GameMain/Scripts/Simulation/SimData/EnemySimData.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Simulation/EnemySimData.cs.meta rename to Assets/GameMain/Scripts/Simulation/SimData/EnemySimData.cs.meta diff --git a/Assets/GameMain/Scripts/Simulation/PickupSimData.cs b/Assets/GameMain/Scripts/Simulation/SimData/PickupSimData.cs similarity index 100% rename from Assets/GameMain/Scripts/Simulation/PickupSimData.cs rename to Assets/GameMain/Scripts/Simulation/SimData/PickupSimData.cs diff --git a/Assets/GameMain/Scripts/Simulation/PickupSimData.cs.meta b/Assets/GameMain/Scripts/Simulation/SimData/PickupSimData.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Simulation/PickupSimData.cs.meta rename to Assets/GameMain/Scripts/Simulation/SimData/PickupSimData.cs.meta diff --git a/Assets/GameMain/Scripts/Simulation/ProjectileSimData.cs b/Assets/GameMain/Scripts/Simulation/SimData/ProjectileSimData.cs similarity index 100% rename from Assets/GameMain/Scripts/Simulation/ProjectileSimData.cs rename to Assets/GameMain/Scripts/Simulation/SimData/ProjectileSimData.cs diff --git a/Assets/GameMain/Scripts/Simulation/ProjectileSimData.cs.meta b/Assets/GameMain/Scripts/Simulation/SimData/ProjectileSimData.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Simulation/ProjectileSimData.cs.meta rename to Assets/GameMain/Scripts/Simulation/SimData/ProjectileSimData.cs.meta diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs b/Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs new file mode 100644 index 0000000..fa5bdab --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs @@ -0,0 +1,138 @@ +using Components; +using Entity; +using Entity.EntityData; +using GameFramework.Event; +using UnityGameFramework.Runtime; + +namespace Simulation +{ + public partial class SimulationWorld + { + public sealed class EntitySync + { + private const string EnemyGroupName = "Enemy"; + private const string DropGroupName = "Drop"; + private const string BulletGroupName = "Bullet"; + private const string ProjectileGroupName = "Projectile"; + + private readonly SimulationWorld _world; + + public EntitySync(SimulationWorld world) + { + _world = world; + } + + public void OnStart() + { + GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); + GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); + } + + public void OnDestroy() + { + GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); + GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); + } + + private void OnShowEntitySuccess(object sender, GameEventArgs e) + { + if (e is not ShowEntitySuccessEventArgs args) return; + if (args.Entity == null) return; + if (_world == null) return; + + string groupName = args.Entity.EntityGroup?.Name; + if (string.IsNullOrEmpty(groupName)) return; + + if (groupName == EnemyGroupName && args.Entity.Logic is EnemyBase enemy) + { + _world.RegisterEnemyTransform(enemy.Id, enemy.CachedTransform); + _world.UpsertEnemy(CreateEnemySimData(enemy, args.UserData as EnemyData)); + return; + } + + if (groupName == DropGroupName && args.Entity.Logic is EntityBase pickupEntity) + { + _world.UpsertPickup(CreatePickupSimData(pickupEntity)); + return; + } + + if ((groupName == BulletGroupName || groupName == ProjectileGroupName) && + args.Entity.Logic is EntityBase projectileEntity) + { + _world.UpsertProjectile(CreateProjectileSimData(projectileEntity)); + } + } + + private void OnHideEntityComplete(object sender, GameEventArgs e) + { + if (e is not HideEntityCompleteEventArgs args) return; + if (args.EntityGroup == null) return; + if (_world == null) return; + + string groupName = args.EntityGroup.Name; + if (groupName == EnemyGroupName) + { + _world.UnregisterEnemyTransform(args.EntityId); + _world.RemoveEnemyByEntityId(args.EntityId); + return; + } + + if (groupName == DropGroupName) + { + _world.RemovePickupByEntityId(args.EntityId); + return; + } + + if (groupName == BulletGroupName || groupName == ProjectileGroupName) + { + _world.RemoveProjectileByEntityId(args.EntityId); + } + } + + private static EnemySimData CreateEnemySimData(EnemyBase enemy, EnemyData enemyData) + { + MovementComponent movementComponent = enemy != null ? enemy.GetComponent() : null; + + return new EnemySimData + { + EntityId = enemy.Id, + Position = enemy.CachedTransform.position, + Forward = enemy.CachedTransform.forward, + Rotation = enemy.CachedTransform.rotation, + Speed = enemyData != null ? enemyData.SpeedBase : 0f, + AttackRange = 1f, + AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap, + EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f, + SeparationIterations = movementComponent != null ? movementComponent.SeparationIterations : 2, + TargetType = 0, + State = 0 + }; + } + + private static PickupSimData CreatePickupSimData(EntityBase pickupEntity) + { + return new PickupSimData + { + EntityId = pickupEntity.Id, + Position = pickupEntity.CachedTransform.position, + PickupRadius = 0.35f, + State = 0 + }; + } + + private static ProjectileSimData CreateProjectileSimData(EntityBase projectileEntity) + { + return new ProjectileSimData + { + EntityId = projectileEntity.Id, + OwnerEntityId = 0, + Position = projectileEntity.CachedTransform.position, + Forward = projectileEntity.CachedTransform.forward, + Speed = 0f, + RemainingLifetime = 0f, + State = 0 + }; + } + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs.meta b/Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs.meta new file mode 100644 index 0000000..0709c55 --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54d72e7241f04623bcfddfd096086dbb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.Presentation.cs b/Assets/GameMain/Scripts/Simulation/SimulationWorld.Presentation.cs new file mode 100644 index 0000000..6d12d64 --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.Presentation.cs @@ -0,0 +1,69 @@ +using UnityEngine; + +namespace Simulation +{ + public partial class SimulationWorld + { + private sealed class Presentation + { + private readonly SimulationWorld _world; + + public Presentation(SimulationWorld world) + { + _world = world; + } + + public void OnLateUpdate() + { + if (_world == null || !_world.UseSimulationMovement) + { + return; + } + + var enemyManager = GameEntry.EnemyManager; + if (enemyManager == null || enemyManager.Enemies == null) + { + return; + } + + var enemies = enemyManager.Enemies; + for (int i = 0; i < enemies.Count; i++) + { + if (enemies[i] is not EnemyBase enemyEntity || !enemyEntity.Available) + { + continue; + } + + if (!_world.TryGetEnemyData(enemyEntity.Id, out EnemySimData enemyData)) + { + continue; + } + + ApplyEnemyPresentation(enemyEntity, enemyData); + } + } + + private static void ApplyEnemyPresentation(EnemyBase enemyEntity, in EnemySimData enemyData) + { + Transform enemyTransform = enemyEntity.CachedTransform; + enemyTransform.position = enemyData.Position; + + Quaternion rotation = enemyData.Rotation; + float rotationMagnitude = Mathf.Abs(rotation.x) + Mathf.Abs(rotation.y) + Mathf.Abs(rotation.z) + + Mathf.Abs(rotation.w); + if (rotationMagnitude > float.Epsilon) + { + enemyTransform.rotation = rotation; + return; + } + + Vector3 forward = enemyData.Forward; + forward.y = 0f; + if (forward.sqrMagnitude > float.Epsilon) + { + enemyTransform.forward = forward.normalized; + } + } + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.Presentation.cs.meta b/Assets/GameMain/Scripts/Simulation/SimulationWorld.Presentation.cs.meta new file mode 100644 index 0000000..eacff63 --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.Presentation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ed4e018208ef0d042b99372c1f9ae778 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs b/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs index 86c6f34..8f10a92 100644 --- a/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs @@ -1,12 +1,11 @@ using System.Collections.Generic; using CustomUtility; -using Entity; using UnityEngine; using UnityGameFramework.Runtime; namespace Simulation { - public sealed class SimulationWorld : GameFrameworkComponent + public sealed partial class SimulationWorld : GameFrameworkComponent { private const float DefaultAttackRange = 1f; private const int EnemyStateIdle = 0; @@ -15,13 +14,17 @@ namespace Simulation [SerializeField] private bool _useSimulationMovement; + private EntitySync _entitySync; + private Presentation _presentation; + private readonly List _enemies = new List(); private readonly List _projectiles = new List(); private readonly List _pickups = new List(); + private readonly Dictionary _enemyTransforms = new Dictionary(); - public EntityBinding EnemyBinding { get; } = new EntityBinding(); - public EntityBinding ProjectileBinding { get; } = new EntityBinding(); - public EntityBinding PickupBinding { get; } = new EntityBinding(); + private EntityBinding EnemyBinding { get; } = new EntityBinding(); + private EntityBinding ProjectileBinding { get; } = new EntityBinding(); + private EntityBinding PickupBinding { get; } = new EntityBinding(); public IReadOnlyList Enemies => _enemies; public IReadOnlyList Projectiles => _projectiles; @@ -33,7 +36,31 @@ namespace Simulation _useSimulationMovement = enabled; } - public int AddEnemy(in EnemySimData simData) + protected override void Awake() + { + base.Awake(); + _entitySync = new EntitySync(this); + _presentation = new Presentation(this); + } + + private void Start() + { + _entitySync?.OnStart(); + } + + private void OnDestroy() + { + _entitySync?.OnDestroy(); + _entitySync = null; + _presentation = null; + } + + private void LateUpdate() + { + _presentation?.OnLateUpdate(); + } + + private int AddEnemy(in EnemySimData simData) { int simulationIndex = _enemies.Count; _enemies.Add(simData); @@ -41,18 +68,18 @@ namespace Simulation return simulationIndex; } - public int UpsertEnemy(in EnemySimData simData) + private int UpsertEnemy(in EnemySimData simData) { - if (EnemyBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex)) + if (!EnemyBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex)) { - _enemies[simulationIndex] = simData; - return simulationIndex; + return AddEnemy(simData); } - return AddEnemy(simData); + _enemies[simulationIndex] = simData; + return simulationIndex; } - public bool RemoveEnemyByEntityId(int entityId) + private bool RemoveEnemyByEntityId(int entityId) { if (!EnemyBinding.TryGetSimulationIndex(entityId, out int simulationIndex)) { @@ -69,10 +96,40 @@ namespace Simulation _enemies.RemoveAt(lastIndex); EnemyBinding.UnbindByEntityId(entityId); + _enemyTransforms.Remove(entityId); return true; } - public int AddProjectile(in ProjectileSimData simData) + private void RegisterEnemyTransform(int entityId, Transform transform) + { + if (transform == null) + { + _enemyTransforms.Remove(entityId); + return; + } + + _enemyTransforms[entityId] = transform; + } + + private void UnregisterEnemyTransform(int entityId) + { + _enemyTransforms.Remove(entityId); + } + + private bool TryGetEnemyData(int entityId, out EnemySimData enemyData) + { + if (!EnemyBinding.TryGetSimulationIndex(entityId, out int simulationIndex) || simulationIndex < 0 || + simulationIndex >= _enemies.Count) + { + enemyData = default; + return false; + } + + enemyData = _enemies[simulationIndex]; + return true; + } + + private int AddProjectile(in ProjectileSimData simData) { int simulationIndex = _projectiles.Count; _projectiles.Add(simData); @@ -80,18 +137,18 @@ namespace Simulation return simulationIndex; } - public int UpsertProjectile(in ProjectileSimData simData) + private int UpsertProjectile(in ProjectileSimData simData) { - if (ProjectileBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex)) + if (!ProjectileBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex)) { - _projectiles[simulationIndex] = simData; - return simulationIndex; + return AddProjectile(simData); } - return AddProjectile(simData); + _projectiles[simulationIndex] = simData; + return simulationIndex; } - public bool RemoveProjectileByEntityId(int entityId) + private bool RemoveProjectileByEntityId(int entityId) { if (!ProjectileBinding.TryGetSimulationIndex(entityId, out int simulationIndex)) { @@ -111,7 +168,7 @@ namespace Simulation return true; } - public int AddPickup(in PickupSimData simData) + private int AddPickup(in PickupSimData simData) { int simulationIndex = _pickups.Count; _pickups.Add(simData); @@ -119,18 +176,18 @@ namespace Simulation return simulationIndex; } - public int UpsertPickup(in PickupSimData simData) + private int UpsertPickup(in PickupSimData simData) { - if (PickupBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex)) + if (!PickupBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex)) { - _pickups[simulationIndex] = simData; - return simulationIndex; + return AddPickup(simData); } - return AddPickup(simData); + _pickups[simulationIndex] = simData; + return simulationIndex; } - public bool RemovePickupByEntityId(int entityId) + private bool RemovePickupByEntityId(int entityId) { if (!PickupBinding.TryGetSimulationIndex(entityId, out int simulationIndex)) { @@ -165,6 +222,7 @@ namespace Simulation _enemies.Clear(); _projectiles.Clear(); _pickups.Clear(); + _enemyTransforms.Clear(); EnemyBinding.Clear(); ProjectileBinding.Clear(); @@ -180,21 +238,11 @@ namespace Simulation Vector3 playerPosition = context.PlayerPosition; playerPosition.y = 0f; - EntityComponent entityComponent = GameEntry.Entity; for (int i = 0; i < _enemies.Count; i++) { EnemySimData enemy = _enemies[i]; - EnemyBase enemyEntity = null; - Transform enemyTransform = null; - - if (entityComponent != null && - entityComponent.GetGameEntity(enemy.EntityId) is EnemyBase runtimeEnemy && - runtimeEnemy.Available) - { - enemyEntity = runtimeEnemy; - enemyTransform = runtimeEnemy.CachedTransform; - } + _enemyTransforms.TryGetValue(enemy.EntityId, out Transform enemyTransform); Vector3 currentPosition = enemy.Position; currentPosition.y = 0f; @@ -229,32 +277,14 @@ namespace Simulation enemy.Position = desiredPosition; enemy.State = EnemyStateChasing; + if (forward.sqrMagnitude > float.Epsilon) + { + enemy.Rotation = Quaternion.LookRotation(forward, Vector3.up); + } } _enemies[i] = enemy; - - if (enemyEntity != null) - { - ApplyEnemyPresentation(enemyEntity, enemy); - } - } - } - - private static void ApplyEnemyPresentation(EnemyBase enemyEntity, in EnemySimData enemyData) - { - if (enemyEntity == null || !enemyEntity.Available) - { - return; - } - - enemyEntity.CachedTransform.position = enemyData.Position; - - Vector3 forward = enemyData.Forward; - forward.y = 0f; - if (forward.sqrMagnitude > float.Epsilon) - { - enemyEntity.CachedTransform.forward = forward.normalized; } } } -} +} \ No newline at end of file diff --git a/Assets/Launcher.unity b/Assets/Launcher.unity index d990e43..015ef5f 100644 --- a/Assets/Launcher.unity +++ b/Assets/Launcher.unity @@ -1407,7 +1407,7 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1652245190} serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 @@ -1426,6 +1426,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8a558ebbc9cb4d94946ac9f4f27914d8, type: 3} m_Name: m_EditorClassIdentifier: + _useSimulationMovement: 0 --- !u!1 &1852670052 GameObject: m_ObjectHideFlags: 0 diff --git a/docs/TodoList.md b/docs/TodoList.md index f5c5570..5932a70 100644 --- a/docs/TodoList.md +++ b/docs/TodoList.md @@ -49,13 +49,13 @@ - `MeleeEnemy/RemoteEnemy.OnUpdate` 仅保留表现层或空实现(不再做核心移动计算)。 - 完成标准:同等刷怪量下,敌人移动结果与旧逻辑视觉一致,无明显穿模/停滞回归。 -- [ ] Checkpoint 5:拆分“逻辑输出”与“表现层消费” +- [x] Checkpoint 5:拆分“逻辑输出”与“表现层消费” - 逻辑层输出:`position/rotation/state`(必要时含 `isMoving`)。 - 表现层仅消费并回写 `Transform`,动画/特效/UI 不参与逻辑计算。 - 明确边界:HPBar、DamageText、Animator 继续由表现层驱动。 - 完成标准:关闭/开启 Simulation 不影响 UI 事件链(血量、经验、金币、关卡流程)。 -- [ ] Checkpoint 6:补齐 Projectile/Pickup 的 Simulation 占位数据通道 +- [x] Checkpoint 6:补齐 Projectile/Pickup 的 Simulation 占位数据通道 - 在 `SimulationWorld` 中接入 `ProjectileSimData / PickupSimData` 容器与绑定关系。 - 先不迁移完整行为,只保证创建、回收、索引同步路径可用。 - 完成标准:投射物/掉落物实体生命周期正常,无索引越界与回收遗漏。