commit
e4b85e61aa
|
|
@ -151,7 +151,10 @@ MonoBehaviour:
|
|||
m_EditorClassIdentifier:
|
||||
_isMoving: 0
|
||||
_direction: {x: 0, y: 0, z: 0}
|
||||
_cachedTransform: {fileID: 0}
|
||||
_cachedTransform: {fileID: 7683855655592166216}
|
||||
_avoidEnemyOverlap: 0
|
||||
_enemyBodyRadius: 0.45
|
||||
_separationIterations: 2
|
||||
_speedBase: 0
|
||||
--- !u!114 &6353753365317756414
|
||||
MonoBehaviour:
|
||||
|
|
|
|||
|
|
@ -191,8 +191,8 @@ Camera:
|
|||
near clip plane: 0.3
|
||||
far clip plane: 100
|
||||
field of view: 80
|
||||
orthographic: 0
|
||||
orthographic size: 5
|
||||
orthographic: 1
|
||||
orthographic size: 15
|
||||
m_Depth: 0
|
||||
m_CullingMask:
|
||||
serializedVersion: 2
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//------------------------------------------------------------
|
||||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
//------------------------------------------------------------
|
||||
|
||||
using CustomComponent;
|
||||
using Simulation;
|
||||
using StarForce;
|
||||
using UI;
|
||||
using UnityEngine;
|
||||
|
|
@ -20,12 +21,15 @@ public partial class GameEntry : MonoBehaviour
|
|||
public static HPBarComponent HPBar { get; private set; }
|
||||
|
||||
public static DamageTextComponent DamageText { get; private set; }
|
||||
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
public static RuntimeDebugPanelComponent RuntimeDebugPanel { get; private set; }
|
||||
#endif
|
||||
|
||||
|
||||
public static EnemyManagerComponent EnemyManager { get; private set; }
|
||||
|
||||
|
||||
public static SimulationWorld SimulationWorld { get; private set; }
|
||||
|
||||
public static SpriteCacheComponent SpriteCache { get; private set; }
|
||||
|
||||
public static UIRouterComponent UIRouter { get; private set; }
|
||||
|
|
@ -35,18 +39,17 @@ public partial class GameEntry : MonoBehaviour
|
|||
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
|
||||
HPBar = UnityGameFramework.Runtime.GameEntry.GetComponent<HPBarComponent>();
|
||||
DamageText = UnityGameFramework.Runtime.GameEntry.GetComponent<DamageTextComponent>();
|
||||
if (DamageText == null && Base != null)
|
||||
{
|
||||
DamageText = Base.gameObject.AddComponent<DamageTextComponent>();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
RuntimeDebugPanel = UnityGameFramework.Runtime.GameEntry.GetComponent<RuntimeDebugPanelComponent>();
|
||||
if (RuntimeDebugPanel == null && Base != null)
|
||||
{
|
||||
RuntimeDebugPanel = Base.gameObject.AddComponent<RuntimeDebugPanelComponent>();
|
||||
}
|
||||
#endif
|
||||
|
||||
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>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ namespace Components
|
|||
[SerializeField] private int _separationIterations = 2;
|
||||
|
||||
public float Speed => (_speedBase + _movementStat.Value) * _movementStat.Percent;
|
||||
public bool AvoidEnemyOverlap => _avoidEnemyOverlap;
|
||||
public float EnemyBodyRadius => _enemyBodyRadius;
|
||||
public int SeparationIterations => _separationIterations;
|
||||
[SerializeField] private float _speedBase;
|
||||
|
||||
private StatComponent _statComponent;
|
||||
|
|
@ -61,6 +64,8 @@ namespace Components
|
|||
|
||||
public void OnReset()
|
||||
{
|
||||
Transform transformToUnregister = _cachedTransform;
|
||||
|
||||
_speedBase = 0;
|
||||
_cachedTransform = null;
|
||||
_direction = Vector3.zero;
|
||||
|
|
@ -77,7 +82,7 @@ namespace Components
|
|||
|
||||
_statComponent = null;
|
||||
|
||||
UnregisterEnemyMover();
|
||||
UnregisterEnemyMover(transformToUnregister);
|
||||
}
|
||||
|
||||
private void Move(float deltaTime = 0)
|
||||
|
|
@ -91,7 +96,7 @@ namespace Components
|
|||
if (_avoidEnemyOverlap)
|
||||
{
|
||||
nextPosition = EnemySeparationSolverProvider.Resolve(
|
||||
this,
|
||||
_cachedTransform,
|
||||
nextPosition,
|
||||
_direction,
|
||||
_separationIterations);
|
||||
|
|
@ -108,12 +113,12 @@ namespace Components
|
|||
{
|
||||
UnregisterEnemyMover();
|
||||
if (!_avoidEnemyOverlap) return;
|
||||
EnemySeparationSolverProvider.Register(this, _cachedTransform, _enemyBodyRadius);
|
||||
EnemySeparationSolverProvider.Register(_cachedTransform, _enemyBodyRadius);
|
||||
}
|
||||
|
||||
private void UnregisterEnemyMover()
|
||||
private void UnregisterEnemyMover(Transform transform = null)
|
||||
{
|
||||
EnemySeparationSolverProvider.Unregister(this);
|
||||
EnemySeparationSolverProvider.Unregister(transform ?? _cachedTransform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace CustomComponent
|
|||
public class EnemyManagerComponent : GameFrameworkComponent
|
||||
{
|
||||
private const float MinSpawnRateScale = 0.1f;
|
||||
private const string EnemyGroupName = "Enemy";
|
||||
|
||||
private EntityComponent _entity;
|
||||
|
||||
|
|
@ -209,10 +210,13 @@ 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);
|
||||
}
|
||||
|
||||
|
|
@ -226,13 +230,31 @@ namespace CustomComponent
|
|||
{
|
||||
if (e is HideEntityCompleteEventArgs ne)
|
||||
{
|
||||
if (ne.EntityGroup.Name == "Enemy")
|
||||
string entityGroupName = ne.EntityGroup.Name;
|
||||
if (entityGroupName == EnemyGroupName)
|
||||
{
|
||||
_currentEnemyCount--;
|
||||
if (_currentEnemyCount > 0)
|
||||
{
|
||||
_currentEnemyCount--;
|
||||
}
|
||||
|
||||
RemoveEnemyFromCache(ne.EntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveEnemyFromCache(int entityId)
|
||||
{
|
||||
for (int i = _enemies.Count - 1; i >= 0; i--)
|
||||
{
|
||||
EntityBase cachedEnemy = _enemies[i];
|
||||
if (cachedEnemy == null || cachedEnemy.Id == entityId)
|
||||
{
|
||||
_enemies.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace CustomDebugger
|
|||
{
|
||||
public static class CustomProfilerMarker
|
||||
{
|
||||
public static readonly ProfilerMarker TickEnemies = new ProfilerMarker("TickEnemies");
|
||||
public static readonly ProfilerMarker Movement_Update = new ProfilerMarker("Movement_Update");
|
||||
public static readonly ProfilerMarker ShopUI_Update = new("UGF.ShopUI.Update");
|
||||
public static readonly ProfilerMarker Inventory_Refresh = new("UGF.Inventory.Refresh");
|
||||
|
|
|
|||
|
|
@ -9,4 +9,10 @@ public abstract class EnemyBase : TargetableObject
|
|||
public abstract override ImpactData GetImpactData();
|
||||
|
||||
public virtual void SetTarget(Transform target) => _target = target;
|
||||
}
|
||||
|
||||
protected bool IsSimulationMovementEnabled()
|
||||
{
|
||||
var simulationWorld = GameEntry.SimulationWorld;
|
||||
return simulationWorld != null && simulationWorld.UseSimulationMovement;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ namespace Entity
|
|||
|
||||
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (IsSimulationMovementEnabled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
|
||||
if (_target == null)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ namespace Entity
|
|||
|
||||
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (IsSimulationMovementEnabled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
|
||||
if (_target == null)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ using DataTable;
|
|||
using Entity;
|
||||
using GameFramework.Fsm;
|
||||
using GameFramework.Procedure;
|
||||
using UnityGameFramework.Runtime;
|
||||
using Simulation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Procedure
|
||||
{
|
||||
|
|
@ -13,15 +14,15 @@ namespace Procedure
|
|||
{
|
||||
public override GameStateType GameStateType => GameStateType.Battle;
|
||||
|
||||
private EnemyManagerComponent _enemyManager = null;
|
||||
private EnemyManagerComponent _enemyManager;
|
||||
|
||||
private int _currentLevel = 0;
|
||||
private int _currentLevel;
|
||||
|
||||
private float _levelTimeLeft = 0;
|
||||
private float _levelTimeLeft;
|
||||
|
||||
private Player Player => _procedureGame.Player;
|
||||
|
||||
private ProcedureGame _procedureGame = null;
|
||||
private ProcedureGame _procedureGame;
|
||||
|
||||
public void AddBattleDuration(float seconds)
|
||||
{
|
||||
|
|
@ -65,6 +66,13 @@ namespace Procedure
|
|||
|
||||
_enemyManager.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
|
||||
SimulationWorld simulationWorld = GameEntry.SimulationWorld;
|
||||
if (simulationWorld != null)
|
||||
{
|
||||
Vector3 playerPosition = Player != null ? Player.CachedTransform.position : Vector3.zero;
|
||||
simulationWorld.Tick(new SimulationTickContext(elapseSeconds, realElapseSeconds, playerPosition));
|
||||
}
|
||||
|
||||
_levelTimeLeft -= elapseSeconds;
|
||||
GameEntry.Event.Fire(this, LevelProcessEventArgs.Create((int)_levelTimeLeft));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using CustomEvent;
|
||||
using DataTable;
|
||||
using Definition.Enum;
|
||||
using Entity;
|
||||
|
|
@ -24,11 +23,11 @@ namespace Procedure
|
|||
{
|
||||
public override bool UseNativeDialog => false;
|
||||
|
||||
private HudForm _hudForm = null;
|
||||
private bool _hudInitialized = false;
|
||||
private HudForm _hudForm;
|
||||
private bool _hudInitialized;
|
||||
|
||||
private IFsm<IProcedureManager> _procedureOwner = null;
|
||||
private PlayerData _currentPlayerData = null;
|
||||
private IFsm<IProcedureManager> _procedureOwner;
|
||||
private PlayerData _currentPlayerData;
|
||||
public int CurrentLevel = 1;
|
||||
|
||||
private GameStateType _currentGameState = GameStateType.None;
|
||||
|
|
@ -88,6 +87,7 @@ namespace Procedure
|
|||
base.OnEnter(procedureOwner);
|
||||
|
||||
_procedureOwner = procedureOwner;
|
||||
GameEntry.SimulationWorld?.Clear();
|
||||
|
||||
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
|
||||
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, ShowEntitySuccess);
|
||||
|
|
@ -135,6 +135,7 @@ namespace Procedure
|
|||
Player = null;
|
||||
|
||||
_procedureOwner = null;
|
||||
GameEntry.SimulationWorld?.Clear();
|
||||
|
||||
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
|
||||
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, ShowEntitySuccess);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7f519a6e4d7b4cb4ab5eabf95fb55e1b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed class EntityBinding
|
||||
{
|
||||
private readonly Dictionary<int, int> _entityIdToSimulationIndex = new Dictionary<int, int>();
|
||||
private readonly Dictionary<int, int> _simulationIndexToEntityId = new Dictionary<int, int>();
|
||||
|
||||
public int Count => _entityIdToSimulationIndex.Count;
|
||||
|
||||
public void Bind(int entityId, int simulationIndex)
|
||||
{
|
||||
if (_entityIdToSimulationIndex.TryGetValue(entityId, out int oldSimulationIndex))
|
||||
{
|
||||
_simulationIndexToEntityId.Remove(oldSimulationIndex);
|
||||
}
|
||||
|
||||
if (_simulationIndexToEntityId.TryGetValue(simulationIndex, out int oldEntityId))
|
||||
{
|
||||
_entityIdToSimulationIndex.Remove(oldEntityId);
|
||||
}
|
||||
|
||||
_entityIdToSimulationIndex[entityId] = simulationIndex;
|
||||
_simulationIndexToEntityId[simulationIndex] = entityId;
|
||||
}
|
||||
|
||||
public void RemapIndex(int entityId, int newSimulationIndex)
|
||||
{
|
||||
if (!_entityIdToSimulationIndex.TryGetValue(entityId, out int oldSimulationIndex))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_simulationIndexToEntityId.TryGetValue(newSimulationIndex, out int oldEntityId) &&
|
||||
oldEntityId != entityId)
|
||||
{
|
||||
_entityIdToSimulationIndex.Remove(oldEntityId);
|
||||
}
|
||||
|
||||
_simulationIndexToEntityId.Remove(oldSimulationIndex);
|
||||
_entityIdToSimulationIndex[entityId] = newSimulationIndex;
|
||||
_simulationIndexToEntityId[newSimulationIndex] = entityId;
|
||||
}
|
||||
|
||||
public bool TryGetSimulationIndex(int entityId, out int simulationIndex)
|
||||
{
|
||||
return _entityIdToSimulationIndex.TryGetValue(entityId, out simulationIndex);
|
||||
}
|
||||
|
||||
public bool TryGetEntityId(int simulationIndex, out int entityId)
|
||||
{
|
||||
return _simulationIndexToEntityId.TryGetValue(simulationIndex, out entityId);
|
||||
}
|
||||
|
||||
public bool UnbindByEntityId(int entityId)
|
||||
{
|
||||
if (!_entityIdToSimulationIndex.TryGetValue(entityId, out int simulationIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_entityIdToSimulationIndex.Remove(entityId);
|
||||
_simulationIndexToEntityId.Remove(simulationIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool UnbindBySimulationIndex(int simulationIndex)
|
||||
{
|
||||
if (!_simulationIndexToEntityId.TryGetValue(simulationIndex, out int entityId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_simulationIndexToEntityId.Remove(simulationIndex);
|
||||
_entityIdToSimulationIndex.Remove(entityId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_entityIdToSimulationIndex.Clear();
|
||||
_simulationIndexToEntityId.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c4f6d93561db4b9092ab61182f2983d7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e2d0f6135bae4675af78194b2272e280
|
||||
timeCreated: 1771590468
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public struct EnemySimData
|
||||
{
|
||||
public int EntityId;
|
||||
public Vector3 Position;
|
||||
public Vector3 Forward;
|
||||
public Quaternion Rotation;
|
||||
public float Speed;
|
||||
public float AttackRange;
|
||||
public bool AvoidEnemyOverlap;
|
||||
public float EnemyBodyRadius;
|
||||
public int SeparationIterations;
|
||||
public int TargetType;
|
||||
public int State;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ef2eea85685544189eef2d9f2cece080
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public struct PickupSimData
|
||||
{
|
||||
public int EntityId;
|
||||
public Vector3 Position;
|
||||
public float PickupRadius;
|
||||
public int State;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ff6e8b75e4af4c3ca9e3b732b1383f95
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public struct ProjectileSimData
|
||||
{
|
||||
public int EntityId;
|
||||
public int OwnerEntityId;
|
||||
public Vector3 Position;
|
||||
public Vector3 Forward;
|
||||
public float Speed;
|
||||
public float RemainingLifetime;
|
||||
public int State;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2eec1f4389004d69bdce8c4dd95d255e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public readonly struct SimulationTickContext
|
||||
{
|
||||
public SimulationTickContext(float deltaTime, float realDeltaTime, Vector3 playerPosition)
|
||||
{
|
||||
DeltaTime = deltaTime;
|
||||
RealDeltaTime = realDeltaTime;
|
||||
PlayerPosition = playerPosition;
|
||||
}
|
||||
|
||||
public float DeltaTime { get; }
|
||||
public float RealDeltaTime { get; }
|
||||
public Vector3 PlayerPosition { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c4c57fcf285f488b821c9141a6d0ad09
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 54d72e7241f04623bcfddfd096086dbb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ed4e018208ef0d042b99372c1f9ae778
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
using System.Collections.Generic;
|
||||
using CustomDebugger;
|
||||
using CustomUtility;
|
||||
using Unity.Profiling;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld : GameFrameworkComponent
|
||||
{
|
||||
private const float DefaultAttackRange = 1f;
|
||||
private const int EnemyStateIdle = 0;
|
||||
private const int EnemyStateChasing = 1;
|
||||
private const int EnemyStateInAttackRange = 2;
|
||||
|
||||
[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>();
|
||||
|
||||
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;
|
||||
public IReadOnlyList<PickupSimData> Pickups => _pickups;
|
||||
public bool UseSimulationMovement => _useSimulationMovement;
|
||||
|
||||
public void SetUseSimulationMovement(bool enabled)
|
||||
{
|
||||
_useSimulationMovement = enabled;
|
||||
}
|
||||
|
||||
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);
|
||||
EnemyBinding.Bind(simData.EntityId, simulationIndex);
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
private int UpsertEnemy(in EnemySimData simData)
|
||||
{
|
||||
if (!EnemyBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
|
||||
{
|
||||
return AddEnemy(simData);
|
||||
}
|
||||
|
||||
_enemies[simulationIndex] = simData;
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
private bool RemoveEnemyByEntityId(int entityId)
|
||||
{
|
||||
if (!EnemyBinding.TryGetSimulationIndex(entityId, out int simulationIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int lastIndex = _enemies.Count - 1;
|
||||
if (simulationIndex != lastIndex)
|
||||
{
|
||||
EnemySimData movedData = _enemies[lastIndex];
|
||||
_enemies[simulationIndex] = movedData;
|
||||
EnemyBinding.RemapIndex(movedData.EntityId, simulationIndex);
|
||||
}
|
||||
|
||||
_enemies.RemoveAt(lastIndex);
|
||||
EnemyBinding.UnbindByEntityId(entityId);
|
||||
_enemyTransforms.Remove(entityId);
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
ProjectileBinding.Bind(simData.EntityId, simulationIndex);
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
private int UpsertProjectile(in ProjectileSimData simData)
|
||||
{
|
||||
if (!ProjectileBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
|
||||
{
|
||||
return AddProjectile(simData);
|
||||
}
|
||||
|
||||
_projectiles[simulationIndex] = simData;
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
private bool RemoveProjectileByEntityId(int entityId)
|
||||
{
|
||||
if (!ProjectileBinding.TryGetSimulationIndex(entityId, out int simulationIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int lastIndex = _projectiles.Count - 1;
|
||||
if (simulationIndex != lastIndex)
|
||||
{
|
||||
ProjectileSimData movedData = _projectiles[lastIndex];
|
||||
_projectiles[simulationIndex] = movedData;
|
||||
ProjectileBinding.RemapIndex(movedData.EntityId, simulationIndex);
|
||||
}
|
||||
|
||||
_projectiles.RemoveAt(lastIndex);
|
||||
ProjectileBinding.UnbindByEntityId(entityId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private int AddPickup(in PickupSimData simData)
|
||||
{
|
||||
int simulationIndex = _pickups.Count;
|
||||
_pickups.Add(simData);
|
||||
PickupBinding.Bind(simData.EntityId, simulationIndex);
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
private int UpsertPickup(in PickupSimData simData)
|
||||
{
|
||||
if (!PickupBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
|
||||
{
|
||||
return AddPickup(simData);
|
||||
}
|
||||
|
||||
_pickups[simulationIndex] = simData;
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
private bool RemovePickupByEntityId(int entityId)
|
||||
{
|
||||
if (!PickupBinding.TryGetSimulationIndex(entityId, out int simulationIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int lastIndex = _pickups.Count - 1;
|
||||
if (simulationIndex != lastIndex)
|
||||
{
|
||||
PickupSimData movedData = _pickups[lastIndex];
|
||||
_pickups[simulationIndex] = movedData;
|
||||
PickupBinding.RemapIndex(movedData.EntityId, simulationIndex);
|
||||
}
|
||||
|
||||
_pickups.RemoveAt(lastIndex);
|
||||
PickupBinding.UnbindByEntityId(entityId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Tick(in SimulationTickContext context)
|
||||
{
|
||||
if (!_useSimulationMovement)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies.Auto())
|
||||
{
|
||||
TickEnemies(in context);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_enemies.Clear();
|
||||
_projectiles.Clear();
|
||||
_pickups.Clear();
|
||||
_enemyTransforms.Clear();
|
||||
|
||||
EnemyBinding.Clear();
|
||||
ProjectileBinding.Clear();
|
||||
PickupBinding.Clear();
|
||||
}
|
||||
|
||||
private void TickEnemies(in SimulationTickContext context)
|
||||
{
|
||||
if (_enemies.Count == 0 || context.DeltaTime <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 playerPosition = context.PlayerPosition;
|
||||
playerPosition.y = 0f;
|
||||
|
||||
for (int i = 0; i < _enemies.Count; i++)
|
||||
{
|
||||
EnemySimData enemy = _enemies[i];
|
||||
_enemyTransforms.TryGetValue(enemy.EntityId, out Transform enemyTransform);
|
||||
|
||||
Vector3 currentPosition = enemy.Position;
|
||||
currentPosition.y = 0f;
|
||||
|
||||
Vector3 toPlayer = playerPosition - currentPosition;
|
||||
float sqrDistance = toPlayer.sqrMagnitude;
|
||||
float attackRange = enemy.AttackRange > 0f ? enemy.AttackRange : DefaultAttackRange;
|
||||
float attackRangeSqr = attackRange * attackRange;
|
||||
|
||||
if (sqrDistance <= attackRangeSqr)
|
||||
{
|
||||
enemy.State = EnemyStateInAttackRange;
|
||||
}
|
||||
else if (enemy.Speed <= 0f || sqrDistance <= float.Epsilon)
|
||||
{
|
||||
enemy.State = EnemyStateIdle;
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 forward = toPlayer.normalized;
|
||||
enemy.Forward = forward;
|
||||
Vector3 desiredPosition = enemy.Position + forward * enemy.Speed * context.DeltaTime;
|
||||
if (enemy.AvoidEnemyOverlap && enemyTransform != null)
|
||||
{
|
||||
int separationIterations = enemy.SeparationIterations > 0 ? enemy.SeparationIterations : 1;
|
||||
desiredPosition = EnemySeparationSolverProvider.Resolve(
|
||||
enemyTransform,
|
||||
desiredPosition,
|
||||
forward,
|
||||
separationIterations);
|
||||
}
|
||||
|
||||
enemy.Position = desiredPosition;
|
||||
enemy.State = EnemyStateChasing;
|
||||
if (forward.sqrMagnitude > float.Epsilon)
|
||||
{
|
||||
enemy.Rotation = Quaternion.LookRotation(forward, Vector3.up);
|
||||
}
|
||||
}
|
||||
|
||||
_enemies[i] = enemy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8a558ebbc9cb4d94946ac9f4f27914d8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
|
||||
using System.Collections.Generic;
|
||||
using Components;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CustomUtility
|
||||
|
|
@ -9,12 +8,11 @@ namespace CustomUtility
|
|||
{
|
||||
private struct Registration
|
||||
{
|
||||
public Transform Transform;
|
||||
public float BodyRadius;
|
||||
}
|
||||
|
||||
private static IEnemySeparationSolver _current = new GridBucketEnemySeparationSolver();
|
||||
private static readonly Dictionary<MovementComponent, Registration> Registrations = new();
|
||||
private static readonly Dictionary<Transform, Registration> Registrations = new();
|
||||
|
||||
public static IEnemySeparationSolver Current => _current;
|
||||
public static string CurrentSolverName => _current.GetType().Name;
|
||||
|
|
@ -36,42 +34,41 @@ namespace CustomUtility
|
|||
SetSolver(new NaiveEnemySeparationSolver());
|
||||
}
|
||||
|
||||
public static void Register(MovementComponent mover, Transform transform, float bodyRadius)
|
||||
public static void Register(Transform transform, float bodyRadius)
|
||||
{
|
||||
if (mover == null || transform == null) return;
|
||||
if (transform == null) return;
|
||||
|
||||
var registration = new Registration
|
||||
{
|
||||
Transform = transform,
|
||||
BodyRadius = bodyRadius
|
||||
};
|
||||
Registrations[mover] = registration;
|
||||
_current.Register(mover, transform, bodyRadius);
|
||||
Registrations[transform] = registration;
|
||||
_current.Register(transform, bodyRadius);
|
||||
}
|
||||
|
||||
public static void Unregister(MovementComponent mover)
|
||||
public static void Unregister(Transform transform)
|
||||
{
|
||||
if (mover == null) return;
|
||||
if (transform == null) return;
|
||||
|
||||
_current.Unregister(mover);
|
||||
Registrations.Remove(mover);
|
||||
_current.Unregister(transform);
|
||||
Registrations.Remove(transform);
|
||||
}
|
||||
|
||||
public static Vector3 Resolve(MovementComponent mover, Vector3 desiredPosition, Vector3 fallbackDirection,
|
||||
public static Vector3 Resolve(Transform transform, Vector3 desiredPosition, Vector3 fallbackDirection,
|
||||
int iterations)
|
||||
{
|
||||
return _current.Resolve(mover, desiredPosition, fallbackDirection, iterations);
|
||||
return _current.Resolve(transform, desiredPosition, fallbackDirection, iterations);
|
||||
}
|
||||
|
||||
private static void ReRegisterAll()
|
||||
{
|
||||
foreach (var pair in Registrations)
|
||||
{
|
||||
MovementComponent mover = pair.Key;
|
||||
Transform transform = pair.Key;
|
||||
Registration registration = pair.Value;
|
||||
if (mover == null || registration.Transform == null) continue;
|
||||
if (transform == null) continue;
|
||||
|
||||
_current.Register(mover, registration.Transform, registration.BodyRadius);
|
||||
_current.Register(transform, registration.BodyRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Components;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CustomUtility
|
||||
|
|
@ -14,12 +13,12 @@ namespace CustomUtility
|
|||
public int CellZ;
|
||||
}
|
||||
|
||||
private readonly System.Collections.Generic.Dictionary<MovementComponent, Agent> _agents = new();
|
||||
private readonly System.Collections.Generic.Dictionary<Transform, Agent> _agents = new();
|
||||
|
||||
private readonly System.Collections.Generic.Dictionary<long, System.Collections.Generic.List<MovementComponent>>
|
||||
private readonly System.Collections.Generic.Dictionary<long, System.Collections.Generic.List<Transform>>
|
||||
_buckets = new();
|
||||
|
||||
private readonly System.Collections.Generic.List<MovementComponent> _recycle = new();
|
||||
private readonly System.Collections.Generic.List<Transform> _recycle = new();
|
||||
private readonly float _cellSize;
|
||||
|
||||
private int _snapshotFrame = -1;
|
||||
|
|
@ -30,14 +29,14 @@ namespace CustomUtility
|
|||
_cellSize = Mathf.Max(0.1f, cellSize);
|
||||
}
|
||||
|
||||
public void Register(MovementComponent mover, Transform transform, float bodyRadius)
|
||||
public void Register(Transform transform, float bodyRadius)
|
||||
{
|
||||
if (mover == null || transform == null) return;
|
||||
if (transform == null) return;
|
||||
|
||||
if (!_agents.TryGetValue(mover, out var agent))
|
||||
if (!_agents.TryGetValue(transform, out var agent))
|
||||
{
|
||||
agent = new Agent();
|
||||
_agents.Add(mover, agent);
|
||||
_agents.Add(transform, agent);
|
||||
}
|
||||
|
||||
agent.Transform = transform;
|
||||
|
|
@ -50,22 +49,22 @@ namespace CustomUtility
|
|||
_snapshotFrame = -1;
|
||||
}
|
||||
|
||||
public void Unregister(MovementComponent mover)
|
||||
public void Unregister(Transform transform)
|
||||
{
|
||||
if (mover == null) return;
|
||||
if (!_agents.TryGetValue(mover, out var agent)) return;
|
||||
if (transform == null) return;
|
||||
if (!_agents.TryGetValue(transform, out var agent)) return;
|
||||
|
||||
RemoveFromBucket(mover, agent.CellX, agent.CellZ);
|
||||
_agents.Remove(mover);
|
||||
RemoveFromBucket(transform, agent.CellX, agent.CellZ);
|
||||
_agents.Remove(transform);
|
||||
RecalculateMaxRadius();
|
||||
_snapshotFrame = -1;
|
||||
}
|
||||
|
||||
public Vector3 Resolve(MovementComponent mover, Vector3 desiredPosition, Vector3 fallbackDirection,
|
||||
public Vector3 Resolve(Transform transform, Vector3 desiredPosition, Vector3 fallbackDirection,
|
||||
int iterations)
|
||||
{
|
||||
if (mover == null) return desiredPosition;
|
||||
if (!_agents.TryGetValue(mover, out var self)) return desiredPosition;
|
||||
if (transform == null) return desiredPosition;
|
||||
if (!_agents.TryGetValue(transform, out var self)) return desiredPosition;
|
||||
|
||||
EnsureSnapshot();
|
||||
|
||||
|
|
@ -90,9 +89,9 @@ namespace CustomUtility
|
|||
|
||||
for (int i = 0; i < bucket.Count; i++)
|
||||
{
|
||||
MovementComponent otherMover = bucket[i];
|
||||
if (otherMover == mover) continue;
|
||||
if (!_agents.TryGetValue(otherMover, out var other)) continue;
|
||||
Transform otherTransform = bucket[i];
|
||||
if (otherTransform == transform) continue;
|
||||
if (!_agents.TryGetValue(otherTransform, out var other)) continue;
|
||||
|
||||
Vector3 toSelf = candidate - other.Position;
|
||||
float minDistance = self.Radius + other.Radius;
|
||||
|
|
@ -115,7 +114,7 @@ namespace CustomUtility
|
|||
}
|
||||
}
|
||||
|
||||
SyncAgentPosition(mover, self, candidate);
|
||||
SyncAgentPosition(transform, self, candidate);
|
||||
|
||||
candidate.y = desiredPosition.y;
|
||||
return candidate;
|
||||
|
|
@ -132,11 +131,11 @@ namespace CustomUtility
|
|||
|
||||
foreach (var pair in _agents)
|
||||
{
|
||||
MovementComponent mover = pair.Key;
|
||||
Transform transform = pair.Key;
|
||||
Agent agent = pair.Value;
|
||||
if (mover == null || agent.Transform == null)
|
||||
if (transform == null || agent.Transform == null)
|
||||
{
|
||||
_recycle.Add(mover);
|
||||
_recycle.Add(transform);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -146,7 +145,7 @@ namespace CustomUtility
|
|||
|
||||
agent.CellX = ToCell(position.x);
|
||||
agent.CellZ = ToCell(position.z);
|
||||
AddToBucket(mover, agent.CellX, agent.CellZ);
|
||||
AddToBucket(transform, agent.CellX, agent.CellZ);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _recycle.Count; i++)
|
||||
|
|
@ -155,15 +154,15 @@ namespace CustomUtility
|
|||
}
|
||||
}
|
||||
|
||||
private void SyncAgentPosition(MovementComponent mover, Agent agent, Vector3 position)
|
||||
private void SyncAgentPosition(Transform transform, Agent agent, Vector3 position)
|
||||
{
|
||||
int newCellX = ToCell(position.x);
|
||||
int newCellZ = ToCell(position.z);
|
||||
|
||||
if (agent.CellX != newCellX || agent.CellZ != newCellZ)
|
||||
{
|
||||
RemoveFromBucket(mover, agent.CellX, agent.CellZ);
|
||||
AddToBucket(mover, newCellX, newCellZ);
|
||||
RemoveFromBucket(transform, agent.CellX, agent.CellZ);
|
||||
AddToBucket(transform, newCellX, newCellZ);
|
||||
agent.CellX = newCellX;
|
||||
agent.CellZ = newCellZ;
|
||||
}
|
||||
|
|
@ -171,24 +170,24 @@ namespace CustomUtility
|
|||
agent.Position = position;
|
||||
}
|
||||
|
||||
private void AddToBucket(MovementComponent mover, int cellX, int cellZ)
|
||||
private void AddToBucket(Transform transform, int cellX, int cellZ)
|
||||
{
|
||||
long key = CellKey(cellX, cellZ);
|
||||
if (!_buckets.TryGetValue(key, out var list))
|
||||
{
|
||||
list = new System.Collections.Generic.List<MovementComponent>(8);
|
||||
list = new System.Collections.Generic.List<Transform>(8);
|
||||
_buckets.Add(key, list);
|
||||
}
|
||||
|
||||
list.Add(mover);
|
||||
list.Add(transform);
|
||||
}
|
||||
|
||||
private void RemoveFromBucket(MovementComponent mover, int cellX, int cellZ)
|
||||
private void RemoveFromBucket(Transform transform, int cellX, int cellZ)
|
||||
{
|
||||
long key = CellKey(cellX, cellZ);
|
||||
if (!_buckets.TryGetValue(key, out var list)) return;
|
||||
|
||||
list.Remove(mover);
|
||||
list.Remove(transform);
|
||||
if (list.Count == 0)
|
||||
{
|
||||
_buckets.Remove(key);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
using Components;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CustomUtility
|
||||
{
|
||||
public interface IEnemySeparationSolver
|
||||
{
|
||||
void Register(MovementComponent mover, Transform transform, float bodyRadius);
|
||||
void Unregister(MovementComponent mover);
|
||||
Vector3 Resolve(MovementComponent mover, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations);
|
||||
void Register(Transform transform, float bodyRadius);
|
||||
void Unregister(Transform transform);
|
||||
Vector3 Resolve(Transform transform, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Components;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CustomUtility
|
||||
|
|
@ -11,33 +10,33 @@ namespace CustomUtility
|
|||
public float Radius;
|
||||
}
|
||||
|
||||
private readonly System.Collections.Generic.Dictionary<MovementComponent, Agent> _agents = new();
|
||||
private readonly System.Collections.Generic.List<MovementComponent> _agentKeys = new();
|
||||
private readonly System.Collections.Generic.Dictionary<Transform, Agent> _agents = new();
|
||||
private readonly System.Collections.Generic.List<Transform> _agentKeys = new();
|
||||
|
||||
public void Register(MovementComponent mover, Transform transform, float bodyRadius)
|
||||
public void Register(Transform transform, float bodyRadius)
|
||||
{
|
||||
if (mover == null || transform == null) return;
|
||||
if (transform == null) return;
|
||||
|
||||
if (!_agents.TryGetValue(mover, out var agent))
|
||||
if (!_agents.TryGetValue(transform, out var agent))
|
||||
{
|
||||
agent = new Agent();
|
||||
_agents.Add(mover, agent);
|
||||
_agents.Add(transform, agent);
|
||||
}
|
||||
|
||||
agent.Transform = transform;
|
||||
agent.Radius = Mathf.Max(0.01f, bodyRadius);
|
||||
}
|
||||
|
||||
public void Unregister(MovementComponent mover)
|
||||
public void Unregister(Transform transform)
|
||||
{
|
||||
if (mover == null) return;
|
||||
_agents.Remove(mover);
|
||||
if (transform == null) return;
|
||||
_agents.Remove(transform);
|
||||
}
|
||||
|
||||
public Vector3 Resolve(MovementComponent mover, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations)
|
||||
public Vector3 Resolve(Transform transform, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations)
|
||||
{
|
||||
if (mover == null) return desiredPosition;
|
||||
if (!_agents.TryGetValue(mover, out var self)) return desiredPosition;
|
||||
if (transform == null) return desiredPosition;
|
||||
if (!_agents.TryGetValue(transform, out var self)) return desiredPosition;
|
||||
|
||||
Vector3 candidate = desiredPosition;
|
||||
candidate.y = 0f;
|
||||
|
|
@ -56,9 +55,9 @@ namespace CustomUtility
|
|||
{
|
||||
for (int i = 0; i < _agentKeys.Count; i++)
|
||||
{
|
||||
MovementComponent otherMover = _agentKeys[i];
|
||||
if (otherMover == mover) continue;
|
||||
if (!_agents.TryGetValue(otherMover, out var other)) continue;
|
||||
Transform otherTransform = _agentKeys[i];
|
||||
if (otherTransform == transform) continue;
|
||||
if (!_agents.TryGetValue(otherTransform, out var other)) continue;
|
||||
if (other.Transform == null) continue;
|
||||
|
||||
Vector3 otherPosition = other.Transform.position;
|
||||
|
|
|
|||
|
|
@ -241,6 +241,8 @@ Transform:
|
|||
- {fileID: 472081678}
|
||||
- {fileID: 2050832067}
|
||||
- {fileID: 534968532}
|
||||
- {fileID: 477326942}
|
||||
- {fileID: 1652245191}
|
||||
m_Father: {fileID: 1852670053}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &120093239
|
||||
|
|
@ -802,6 +804,50 @@ MonoBehaviour:
|
|||
m_EditorClassIdentifier:
|
||||
_pixelsPerUnit: 100
|
||||
_defaultPivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &477326941
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 477326942}
|
||||
- component: {fileID: 477326943}
|
||||
m_Layer: 0
|
||||
m_Name: RuntimeDebugPanel
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &477326942
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 477326941}
|
||||
serializedVersion: 2
|
||||
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
|
||||
m_Children: []
|
||||
m_Father: {fileID: 119167776}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &477326943
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 477326941}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 1d8ada5157a04921a6e543a040e57960, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
--- !u!1 &513208572
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -1336,6 +1382,51 @@ MonoBehaviour:
|
|||
_hpBarItemTemplate: {fileID: 11414536, guid: 96d2e77dd9853514da336717bd5627c0, type: 3}
|
||||
_hpBarInstanceRoot: {fileID: 1454214587}
|
||||
_instancePoolCapacity: 16
|
||||
--- !u!1 &1652245190
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1652245191}
|
||||
- component: {fileID: 1652245192}
|
||||
m_Layer: 0
|
||||
m_Name: SimulationWorld
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1652245191
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1652245190}
|
||||
serializedVersion: 2
|
||||
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
|
||||
m_Children: []
|
||||
m_Father: {fileID: 119167776}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1652245192
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1652245190}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 8a558ebbc9cb4d94946ac9f4f27914d8, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_useSimulationMovement: 1
|
||||
--- !u!1 &1852670052
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
## 测试机性能
|
||||
iQOO Neo8
|
||||
|
||||
CPU: 第一代骁龙 8+ 八核
|
||||
|
||||
内存: 12 GB
|
||||
|
||||
系统: OriginOS 6 (Android 16)
|
||||
|
||||
## CPU
|
||||
| 怪物数量 | 帧率 | TickEnemies 占比 | TickEnemies GC | Movement_Update 占比 |
|
||||
|--------|-----------------------|--------------------|----------------|--------------------|
|
||||
| `500` | `60.8 fps (16.43 ms)` | `37.6% (6.18 ms)` | `27.4 KB` | `0.0% (0 ms)` |
|
||||
| `1000` | `40.2 fps (24.85 ms)` | `51.5% (12.8 ms)` | `54.5 KB` | `0.0% (0 ms)` |
|
||||
| `1500` | `28.0 fps (35.62 ms)` | `56.4% (20.11 ms)` | `82.1 KB` | `0.0% (0 ms)` |
|
||||
| `2000` | `18.8 fps (53.04 ms)` | `55.8% (29.62 ms)` | `107.6 KB` | `0.0% (0 ms)` |
|
||||
|
||||
tip:
|
||||
1. 60 fps 为 Android 端帧率上限,具体可参考 CPU ms 耗时
|
||||
2. 注意 Profiler 里会产生性能损耗的配置(Call Stacks)
|
||||
|
||||
### Memory
|
||||
| 怪物数量 | GC Used Memory | GC Allocated In Frame |
|
||||
|------|----------------|-----------------------|
|
||||
| 500 | 7.8 MB | 29.5 KB |
|
||||
| 1000 | 8.8 MB | 56.6 KB |
|
||||
| 1500 | 10.0 MB | 84.2 KB |
|
||||
| 2000 | 11.8 MB | 109.7 KB |
|
||||
|
||||
|
||||
### Render
|
||||
| 维度 | 500 enemies | 1000 enemies | 1500 enemies | 2000 enemies |
|
||||
|----------------------|-------------|--------------|--------------|--------------|
|
||||
| SetPass Calls | 41 | 42 | 43 | 44 |
|
||||
| Draw Calls | 46 | 46 | 48 | 49 |
|
||||
| Batches | 46 | 46 | 48 | 49 |
|
||||
| **Static Batching:** | | | | |
|
||||
| Batched Draw Call | 0 | 0 | 0 | 0 |
|
||||
| Batched | 0 | 0 | 0 | 0 |
|
||||
| **Instancing:** | | | | |
|
||||
| Batched Draw Call | 420 | 505 | 584 | 962 |
|
||||
| Batched | 5 | 5 | 7 | 8 |
|
||||
|
||||
|
|
@ -25,18 +25,91 @@
|
|||
- 以上问题修正后,核心流程可稳定连续跑 10 分钟无异常日志。
|
||||
|
||||
## 2. P1 Simulation 分层(为 Job/Burst 做结构准备)
|
||||
- [ ] 新建 `Simulation` 层(建议目录:`Assets/GameMain/Scripts/Simulation`):
|
||||
- `SimulationWorld`:统一持有敌人/投射物/掉落物的纯数据容器。
|
||||
- `EnemySimData / ProjectileSimData / PickupSimData`:结构化、连续内存友好的数据定义。
|
||||
- `EntityBinding`:维护 `EntityId <-> SimulationIndex` 映射。
|
||||
- [ ] 将“逻辑计算”和“表现层(Transform/Animator/特效/UI)”拆离:
|
||||
- 逻辑层输出 position/rotation/state。
|
||||
- 表现层只消费结果做显示。
|
||||
- [ ] 先保持现有 GameFramework 实体生命周期不变,仅替换更新路径。
|
||||
- [x] Checkpoint 1:搭建 Simulation 基础骨架(仅新增,不改行为)
|
||||
- 新建目录:`Assets/GameMain/Scripts/Simulation`。
|
||||
- 新建 `SimulationWorld`,统一持有 `EnemySimData / ProjectileSimData / PickupSimData` 容器。
|
||||
- 新建 `EntityBinding`,维护 `EntityId <-> SimulationIndex` 双向映射。
|
||||
- 新建 `SimulationTickContext`(至少包含 `deltaTime`、`playerPosition`)。
|
||||
- 完成标准:工程可编译,场景运行行为与当前一致(只加结构,不切链路)。
|
||||
|
||||
- [x] Checkpoint 2:敌人生命周期接入 Simulation(保持 GameFramework 生命周期不变)
|
||||
- 在敌人 `Show/Hide` 时同步注册/反注册到 `SimulationWorld` 与 `EntityBinding`。
|
||||
- `EnemyManagerComponent` 继续负责刷怪与实体显隐,不改外部调用方式。
|
||||
- 完成标准:敌人数量统计与当前一致,无重复注册、无悬空索引。
|
||||
|
||||
- [x] Checkpoint 3:建立 Simulation 主更新入口并接入 Battle 状态
|
||||
- 在 `GameStateBattle.OnUpdate` 中增加 `SimulationWorld.Tick(...)` 调用。
|
||||
- 先只接“敌人移动/追踪”系统,其他逻辑保持原路径。
|
||||
- 增加开关(建议 `UseSimulationMovement`)用于 A/B 对比与回滚。
|
||||
- 完成标准:关闭开关与当前行为一致;开启开关后敌人仍能正常追踪玩家。
|
||||
|
||||
- [x] Checkpoint 4:迁移敌人核心移动逻辑到 Simulation(去 MonoBehaviour 核心逻辑)
|
||||
- 将 `MeleeEnemy/RemoteEnemy` 的目标追踪、移动方向、攻击距离判定迁至 Simulation。
|
||||
- `EnemySimData` 至少包含:`position`、`forward`、`speed`、`attackRange`、`targetType`、`state`。
|
||||
- `MeleeEnemy/RemoteEnemy.OnUpdate` 仅保留表现层或空实现(不再做核心移动计算)。
|
||||
- 完成标准:同等刷怪量下,敌人移动结果与旧逻辑视觉一致,无明显穿模/停滞回归。
|
||||
|
||||
- [x] Checkpoint 5:拆分“逻辑输出”与“表现层消费”
|
||||
- 逻辑层输出:`position/rotation/state`(必要时含 `isMoving`)。
|
||||
- 表现层仅消费并回写 `Transform`,动画/特效/UI 不参与逻辑计算。
|
||||
- 明确边界:HPBar、DamageText、Animator 继续由表现层驱动。
|
||||
- 完成标准:关闭/开启 Simulation 不影响 UI 事件链(血量、经验、金币、关卡流程)。
|
||||
|
||||
- [x] Checkpoint 6:补齐 Projectile/Pickup 的 Simulation 占位数据通道
|
||||
- 在 `SimulationWorld` 中接入 `ProjectileSimData / PickupSimData` 容器与绑定关系。
|
||||
- 先不迁移完整行为,只保证创建、回收、索引同步路径可用。
|
||||
- 完成标准:投射物/掉落物实体生命周期正常,无索引越界与回收遗漏。
|
||||
|
||||
- [x] Checkpoint 7:P1 阶段回归与性能记录
|
||||
- 回归用例:战斗 10 分钟、`Battle -> LevelUp -> Shop -> Battle` 循环、掉落吸附与拾取。
|
||||
- Profiling 对比:记录 1k/2k/3k 敌人下 Main Thread、GC Alloc、敌人更新耗时。
|
||||
- 输出文档:`P1 Simulation 分层设计 + 回滚开关说明 + 对比数据`。
|
||||
- 完成标准:核心流程稳定,无新增 Error/Exception;可一键回滚到旧更新路径。
|
||||
|
||||
**验收标准**
|
||||
- 敌人移动/追踪由 Simulation 统一调度,不再逐个 Enemy MonoBehaviour 执行核心逻辑。
|
||||
|
||||
## 2.5 P1.5 Simulation 收尾(P2 前置)
|
||||
- [ ] Checkpoint 1:清理 `TickEnemies` 侧 GC(优先级最高)
|
||||
- 目标:将 `TickEnemies GC` 从当前 `27~108 KB` 降到 `< 5 KB / frame`。
|
||||
- 重点文件:`Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs`。
|
||||
- 处理方式:桶容器与临时列表复用(包含 bucket list 复用池),避免每帧重建集合。
|
||||
- 完成标准:`2000` 敌人压测下 `TickEnemies GC` 稳定 `< 5 KB / frame`。
|
||||
|
||||
- [ ] Checkpoint 2:解耦 Simulation 核心与 `Transform` 运行时依赖
|
||||
- 目标:`SimulationWorld.TickEnemies` 不直接读取或写入 `Transform`。
|
||||
- 重点文件:`Assets/GameMain/Scripts/Simulation/SimulationWorld.cs`、`Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs`、`Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs`。
|
||||
- 处理方式:互斥求解输入改为纯数据(位置/半径/索引),`Transform` 仅在 Presentation 阶段回写。
|
||||
- 完成标准:`TickEnemies` 热路径中不出现 `Transform` 访问。
|
||||
|
||||
- [ ] Checkpoint 3:收口 `EntitySync` 职责边界
|
||||
- 目标:`EntitySync` 仅处理生命周期映射,不承担运行时移动逻辑。
|
||||
- 重点文件:`Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs`。
|
||||
- 处理方式:保留注册/反注册与初值同步,移除 Tick 过程依赖。
|
||||
- 完成标准:`OnShow/OnHide` 逻辑稳定,且不引入运行时分配热点。
|
||||
|
||||
- [ ] Checkpoint 4:拆分 Simulation Tick 阶段,为 Job 化铺路
|
||||
- 目标:将敌人 Tick 拆分为稳定阶段,便于后续迁移 `IJobParallelFor`。
|
||||
- 建议阶段:`BuildInput -> Move/Separation -> StateUpdate -> WriteBack`。
|
||||
- 重点文件:`Assets/GameMain/Scripts/Simulation/SimulationWorld.cs`。
|
||||
- 完成标准:每阶段有独立 `ProfilerMarker`,可明确观测耗时占比。
|
||||
|
||||
- [ ] Checkpoint 5:补最小回归测试(P1.5 重构保护)
|
||||
- 目标:确保重构不改变战斗行为。
|
||||
- 建议目录:`Assets/Tests/Simulation/`。
|
||||
- 用例范围:追踪玩家、攻击距离停下、实体移除后的索引重映射。
|
||||
- 完成标准:EditMode/PlayMode 相关用例通过,主流程手测无回归。
|
||||
|
||||
- [ ] Checkpoint 6:补充 P1.5 结项文档
|
||||
- 输出:`P1.5 收尾说明 + 对比数据 + 回滚开关`。
|
||||
- 明确记录:Android 60fps 上限、Profiler 采样配置(Call Stacks 开关状态)、评估以 CPU ms 为主。
|
||||
- 完成标准:文档可复现实验结论,并可作为 P2 输入基线。
|
||||
|
||||
**验收标准**
|
||||
- `Movement_Update` 持续维持 `0 ms`(或可忽略占比)。
|
||||
- `TickEnemies` 在目标敌人数下 GC 与 CPU 耗时均有明显下降,并可复现。
|
||||
- Simulation 层与表现层边界清晰,可无缝衔接 P2 Job/Burst 改造。
|
||||
|
||||
## 3. P2 Job System + Burst 落地(核心性能阶段)
|
||||
- [ ] 引入并锁定依赖版本(Unity 2022.3 对应):
|
||||
- `com.unity.collections`
|
||||
|
|
|
|||
Loading…
Reference in New Issue