Checkpoint 5 & Checkpoint 6:

- SimulationWorld 只做逻辑计算与数据输出,不再直接写 Transform
- 在 ShowEntitySuccess 中接入 Pickup/Projectile 占位注册:
	- Drop 走 UpsertPickup(...)
	- Bullet/Projectile 走 UpsertProjectile(...)
- 在 HideEntityComplete 中接入对应回收:
	- Drop 走 RemovePickupByEntityId(...)
	- Bullet/Projectile 走 RemoveProjectileByEntityId(...)
- 新增占位数据构造:
	- CreatePickupSimData(...)
	- CreateProjectileSimData(...)
- 调整 EnemyManagerComponent 职责,现在只管“敌人相关”:
	- 敌人刷怪节奏
	- 敌人列表缓存与计数
	- 玩家引用与 enemy.SetTarget(...)
- 新增嵌套类 SimulationWorld.Presentation,专门消费 EnemySimData 并回写 position/rotation
- 新增嵌套类 SimulationWorld.EntitySync 负责“Simulation 同步”:
	- 监听 ShowEntitySuccess/HideEntityComplete
	- 同步 Enemy/Drop/Projectile 到 SimulationWorld
	- 敌人 SimData 构建、Pickup/Projectile 占位数据构建
This commit is contained in:
SepComet 2026-02-20 20:41:22 +08:00
parent 6494ebc5fd
commit 31fe7a4d61
16 changed files with 339 additions and 98 deletions

View File

@ -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<EnemyManagerComponent>();
SimulationWorld = UnityGameFramework.Runtime.GameEntry.GetComponent<SimulationWorld>();
if (SimulationWorld == null && Base != null)
{
SimulationWorld = Base.gameObject.AddComponent<SimulationWorld>();
}
SpriteCache = UnityGameFramework.Runtime.GameEntry.GetComponent<SpriteCacheComponent>();
UIRouter = UnityGameFramework.Runtime.GameEntry.GetComponent<UIRouterComponent>();
}
}
}

View File

@ -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<MovementComponent>() : 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--)

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e2d0f6135bae4675af78194b2272e280
timeCreated: 1771590468

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<EnemySimData> _enemies = new List<EnemySimData>();
private readonly List<ProjectileSimData> _projectiles = new List<ProjectileSimData>();
private readonly List<PickupSimData> _pickups = new List<PickupSimData>();
private readonly Dictionary<int, Transform> _enemyTransforms = new Dictionary<int, Transform>();
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<EnemySimData> Enemies => _enemies;
public IReadOnlyList<ProjectileSimData> 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;
}
}
}
}
}

View File

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

View File

@ -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` 容器与绑定关系。
- 先不迁移完整行为,只保证创建、回收、索引同步路径可用。
- 完成标准:投射物/掉落物实体生命周期正常,无索引越界与回收遗漏。