diff --git a/Assets/GameMain/Entities/MeleeEnemy.prefab b/Assets/GameMain/Entities/MeleeEnemy.prefab index 4c3a5ed..6099332 100644 --- a/Assets/GameMain/Entities/MeleeEnemy.prefab +++ b/Assets/GameMain/Entities/MeleeEnemy.prefab @@ -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: diff --git a/Assets/GameMain/Entities/Player.prefab b/Assets/GameMain/Entities/Player.prefab index 1b69932..bc58f51 100644 --- a/Assets/GameMain/Entities/Player.prefab +++ b/Assets/GameMain/Entities/Player.prefab @@ -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 diff --git a/Assets/GameMain/Scripts/Base/GameEntry.Custom.cs b/Assets/GameMain/Scripts/Base/GameEntry.Custom.cs index 456e71c..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/ @@ -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(); HPBar = UnityGameFramework.Runtime.GameEntry.GetComponent(); DamageText = UnityGameFramework.Runtime.GameEntry.GetComponent(); - if (DamageText == null && Base != null) - { - DamageText = Base.gameObject.AddComponent(); - } + #if UNITY_EDITOR || DEVELOPMENT_BUILD RuntimeDebugPanel = UnityGameFramework.Runtime.GameEntry.GetComponent(); - if (RuntimeDebugPanel == null && Base != null) - { - RuntimeDebugPanel = Base.gameObject.AddComponent(); - } #endif + 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(); } diff --git a/Assets/GameMain/Scripts/Components/MovementComponent.cs b/Assets/GameMain/Scripts/Components/MovementComponent.cs index 51c618c..2fc3696 100644 --- a/Assets/GameMain/Scripts/Components/MovementComponent.cs +++ b/Assets/GameMain/Scripts/Components/MovementComponent.cs @@ -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); } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/EnemyManager/EnemyManagerComponent.cs b/Assets/GameMain/Scripts/CustomComponent/EnemyManager/EnemyManagerComponent.cs index 2bc455a..f954cf8 100644 --- a/Assets/GameMain/Scripts/CustomComponent/EnemyManager/EnemyManagerComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/EnemyManager/EnemyManagerComponent.cs @@ -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 } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Debugger/ProfilerMarker.cs b/Assets/GameMain/Scripts/Debugger/ProfilerMarker.cs index 9a90e53..c1e4241 100644 --- a/Assets/GameMain/Scripts/Debugger/ProfilerMarker.cs +++ b/Assets/GameMain/Scripts/Debugger/ProfilerMarker.cs @@ -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"); diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyBase.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyBase.cs index a835d96..f7dadeb 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyBase.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/EnemyBase.cs @@ -9,4 +9,10 @@ public abstract class EnemyBase : TargetableObject public abstract override ImpactData GetImpactData(); public virtual void SetTarget(Transform target) => _target = target; -} \ No newline at end of file + + protected bool IsSimulationMovementEnabled() + { + var simulationWorld = GameEntry.SimulationWorld; + return simulationWorld != null && simulationWorld.UseSimulationMovement; + } +} diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/MeleeEnemy.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/MeleeEnemy.cs index e85e709..948599f 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/MeleeEnemy.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/MeleeEnemy.cs @@ -54,6 +54,11 @@ namespace Entity protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) { + if (IsSimulationMovementEnabled()) + { + return; + } + base.OnUpdate(elapseSeconds, realElapseSeconds); if (_target == null) diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/RemoteEnemy.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/RemoteEnemy.cs index b3472e6..f0fdf07 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/RemoteEnemy.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/RemoteEnemy.cs @@ -50,6 +50,11 @@ namespace Entity protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) { + if (IsSimulationMovementEnabled()) + { + return; + } + base.OnUpdate(elapseSeconds, realElapseSeconds); if (_target == null) diff --git a/Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs b/Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs index 89a68c0..e7785b3 100644 --- a/Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs +++ b/Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs @@ -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)); } diff --git a/Assets/GameMain/Scripts/Procedure/Game/ProcedureGame.cs b/Assets/GameMain/Scripts/Procedure/Game/ProcedureGame.cs index 01dc50f..bcb12aa 100644 --- a/Assets/GameMain/Scripts/Procedure/Game/ProcedureGame.cs +++ b/Assets/GameMain/Scripts/Procedure/Game/ProcedureGame.cs @@ -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 _procedureOwner = null; - private PlayerData _currentPlayerData = null; + private IFsm _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); diff --git a/Assets/GameMain/Scripts/Simulation.meta b/Assets/GameMain/Scripts/Simulation.meta new file mode 100644 index 0000000..089d035 --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7f519a6e4d7b4cb4ab5eabf95fb55e1b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Simulation/EntityBinding.cs b/Assets/GameMain/Scripts/Simulation/EntityBinding.cs new file mode 100644 index 0000000..d3c089c --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/EntityBinding.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; + +namespace Simulation +{ + public sealed class EntityBinding + { + private readonly Dictionary _entityIdToSimulationIndex = new Dictionary(); + private readonly Dictionary _simulationIndexToEntityId = new Dictionary(); + + 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(); + } + } +} diff --git a/Assets/GameMain/Scripts/Simulation/EntityBinding.cs.meta b/Assets/GameMain/Scripts/Simulation/EntityBinding.cs.meta new file mode 100644 index 0000000..b3aafca --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/EntityBinding.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c4f6d93561db4b9092ab61182f2983d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: 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/SimData/EnemySimData.cs b/Assets/GameMain/Scripts/Simulation/SimData/EnemySimData.cs new file mode 100644 index 0000000..69d2ddb --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimData/EnemySimData.cs @@ -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; + } +} diff --git a/Assets/GameMain/Scripts/Simulation/SimData/EnemySimData.cs.meta b/Assets/GameMain/Scripts/Simulation/SimData/EnemySimData.cs.meta new file mode 100644 index 0000000..448e55c --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimData/EnemySimData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ef2eea85685544189eef2d9f2cece080 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Simulation/SimData/PickupSimData.cs b/Assets/GameMain/Scripts/Simulation/SimData/PickupSimData.cs new file mode 100644 index 0000000..61dbf77 --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimData/PickupSimData.cs @@ -0,0 +1,12 @@ +using UnityEngine; + +namespace Simulation +{ + public struct PickupSimData + { + public int EntityId; + public Vector3 Position; + public float PickupRadius; + public int State; + } +} diff --git a/Assets/GameMain/Scripts/Simulation/SimData/PickupSimData.cs.meta b/Assets/GameMain/Scripts/Simulation/SimData/PickupSimData.cs.meta new file mode 100644 index 0000000..695d645 --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimData/PickupSimData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ff6e8b75e4af4c3ca9e3b732b1383f95 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Simulation/SimData/ProjectileSimData.cs b/Assets/GameMain/Scripts/Simulation/SimData/ProjectileSimData.cs new file mode 100644 index 0000000..fa23db6 --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimData/ProjectileSimData.cs @@ -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; + } +} diff --git a/Assets/GameMain/Scripts/Simulation/SimData/ProjectileSimData.cs.meta b/Assets/GameMain/Scripts/Simulation/SimData/ProjectileSimData.cs.meta new file mode 100644 index 0000000..54e9a43 --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimData/ProjectileSimData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2eec1f4389004d69bdce8c4dd95d255e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Simulation/SimulationTickContext.cs b/Assets/GameMain/Scripts/Simulation/SimulationTickContext.cs new file mode 100644 index 0000000..2428dcd --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimulationTickContext.cs @@ -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; } + } +} diff --git a/Assets/GameMain/Scripts/Simulation/SimulationTickContext.cs.meta b/Assets/GameMain/Scripts/Simulation/SimulationTickContext.cs.meta new file mode 100644 index 0000000..6516687 --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimulationTickContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c4c57fcf285f488b821c9141a6d0ad09 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: 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 new file mode 100644 index 0000000..ecb27dd --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs @@ -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 _enemies = new List(); + private readonly List _projectiles = new List(); + private readonly List _pickups = new List(); + private readonly Dictionary _enemyTransforms = new Dictionary(); + + 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; + public IReadOnlyList 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; + } + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs.meta b/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs.meta new file mode 100644 index 0000000..0b8861a --- /dev/null +++ b/Assets/GameMain/Scripts/Simulation/SimulationWorld.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8a558ebbc9cb4d94946ac9f4f27914d8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs b/Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs index 67f160a..619b08b 100644 --- a/Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs +++ b/Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs @@ -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 Registrations = new(); + private static readonly Dictionary 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); } } } diff --git a/Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs b/Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs index a233214..1b66b03 100644 --- a/Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs +++ b/Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs @@ -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 _agents = new(); + private readonly System.Collections.Generic.Dictionary _agents = new(); - private readonly System.Collections.Generic.Dictionary> + private readonly System.Collections.Generic.Dictionary> _buckets = new(); - private readonly System.Collections.Generic.List _recycle = new(); + private readonly System.Collections.Generic.List _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(8); + list = new System.Collections.Generic.List(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); diff --git a/Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs b/Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs index 3e8ec6e..a304a49 100644 --- a/Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs +++ b/Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs @@ -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); } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Utility/EnemySeperator/NaiveEnemySeparationSolver.cs b/Assets/GameMain/Scripts/Utility/EnemySeperator/NaiveEnemySeparationSolver.cs index 466f878..63e9256 100644 --- a/Assets/GameMain/Scripts/Utility/EnemySeperator/NaiveEnemySeparationSolver.cs +++ b/Assets/GameMain/Scripts/Utility/EnemySeperator/NaiveEnemySeparationSolver.cs @@ -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 _agents = new(); - private readonly System.Collections.Generic.List _agentKeys = new(); + private readonly System.Collections.Generic.Dictionary _agents = new(); + private readonly System.Collections.Generic.List _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; diff --git a/Assets/Launcher.unity b/Assets/Launcher.unity index abbaee2..06b4865 100644 --- a/Assets/Launcher.unity +++ b/Assets/Launcher.unity @@ -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 diff --git a/docs/P1 Simulation 分层.md b/docs/P1 Simulation 分层.md new file mode 100644 index 0000000..a7e56aa --- /dev/null +++ b/docs/P1 Simulation 分层.md @@ -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 | + diff --git a/docs/TodoList.md b/docs/TodoList.md index b5fa759..695c682 100644 --- a/docs/TodoList.md +++ b/docs/TodoList.md @@ -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`