- 调整优化了 CombatNodeComponent 相关实体的生命周期管理
- 总结了一份 CombatNodeComponent 文档 CombatNodeArchitecture.md
This commit is contained in:
parent
dfd37778b6
commit
26dc1a5600
|
|
@ -7,6 +7,6 @@
|
||||||
101 设置 SettingForm Default False True
|
101 设置 SettingForm Default False True
|
||||||
102 关于 AboutForm Default False True
|
102 关于 AboutForm Default False True
|
||||||
103 主UI MainForm Default False True
|
103 主UI MainForm Default False True
|
||||||
150 测试UI TestMenuForm Default False False
|
150 测试UI TestMenuForm Test False False
|
||||||
200 事件UI EventForm Default False False
|
200 事件UI EventForm Default False False
|
||||||
201 背包UI RepoForm Default False True
|
201 背包UI RepoForm Default False True
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using GameFramework.DataTable;
|
using GameFramework.DataTable;
|
||||||
|
using GeometryTD.CustomEvent;
|
||||||
using GeometryTD.DataTable;
|
using GeometryTD.DataTable;
|
||||||
using GeometryTD.Definition;
|
using GeometryTD.Definition;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
@ -65,9 +66,8 @@ namespace GeometryTD.CustomComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
DRLevelPhase[] levelPhases = dtLevelPhase.GetAllDataRows();
|
DRLevelPhase[] levelPhases = dtLevelPhase.GetAllDataRows();
|
||||||
for (int i = 0; i < levelPhases.Length; i++)
|
foreach (var phase in levelPhases)
|
||||||
{
|
{
|
||||||
DRLevelPhase phase = levelPhases[i];
|
|
||||||
int levelId = phase.Id / 1000;
|
int levelId = phase.Id / 1000;
|
||||||
if (!_levelsById.ContainsKey(levelId))
|
if (!_levelsById.ContainsKey(levelId))
|
||||||
{
|
{
|
||||||
|
|
@ -85,9 +85,8 @@ namespace GeometryTD.CustomComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
DRLevelSpawnEntry[] spawnEntries = dtSpawnEntry.GetAllDataRows();
|
DRLevelSpawnEntry[] spawnEntries = dtSpawnEntry.GetAllDataRows();
|
||||||
for (int i = 0; i < spawnEntries.Length; i++)
|
foreach (var spawnEntry in spawnEntries)
|
||||||
{
|
{
|
||||||
DRLevelSpawnEntry spawnEntry = spawnEntries[i];
|
|
||||||
int phaseId = spawnEntry.Id / 1000;
|
int phaseId = spawnEntry.Id / 1000;
|
||||||
int levelId = phaseId / 1000;
|
int levelId = phaseId / 1000;
|
||||||
if (!_levelsById.ContainsKey(levelId))
|
if (!_levelsById.ContainsKey(levelId))
|
||||||
|
|
@ -171,6 +170,12 @@ namespace GeometryTD.CustomComponent
|
||||||
|
|
||||||
CurrentLevel = selectedLevel;
|
CurrentLevel = selectedLevel;
|
||||||
_combatScheduler.Start(selectedLevel, phaseList, _selectedSpawnEntriesByPhaseId);
|
_combatScheduler.Start(selectedLevel, phaseList, _selectedSpawnEntriesByPhaseId);
|
||||||
|
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndCombat()
|
||||||
|
{
|
||||||
|
GameEntry.Event.Fire(this, NodeCompleteEventArgs.Create());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||||
|
|
@ -252,4 +257,4 @@ namespace GeometryTD.CustomComponent
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -23,9 +23,9 @@ namespace GeometryTD.CustomComponent
|
||||||
Failed = 4
|
Failed = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly List<DRLevelPhase> _phaseBuffer = new List<DRLevelPhase>();
|
private readonly List<DRLevelPhase> _phaseBuffer = new();
|
||||||
private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _spawnEntriesByPhaseId =
|
|
||||||
new Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>>();
|
private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _spawnEntriesByPhaseId = new();
|
||||||
|
|
||||||
private readonly EnemyManager _enemyManager = new EnemyManager();
|
private readonly EnemyManager _enemyManager = new EnemyManager();
|
||||||
|
|
||||||
|
|
@ -79,8 +79,8 @@ namespace GeometryTD.CustomComponent
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CleanupBattleEntities();
|
||||||
_enemyManager.EndPhase();
|
_enemyManager.EndPhase();
|
||||||
HideCurrentMapIfNeeded();
|
|
||||||
ResetRuntime();
|
ResetRuntime();
|
||||||
|
|
||||||
_currentLevel = level;
|
_currentLevel = level;
|
||||||
|
|
@ -145,8 +145,8 @@ namespace GeometryTD.CustomComponent
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CleanupBattleEntities();
|
||||||
_enemyManager.OnDestroy();
|
_enemyManager.OnDestroy();
|
||||||
HideCurrentMapIfNeeded();
|
|
||||||
ResetRuntime();
|
ResetRuntime();
|
||||||
UnsubscribeEntityEvents();
|
UnsubscribeEntityEvents();
|
||||||
_entity = null;
|
_entity = null;
|
||||||
|
|
@ -253,7 +253,12 @@ namespace GeometryTD.CustomComponent
|
||||||
{
|
{
|
||||||
_state = SchedulerState.Completed;
|
_state = SchedulerState.Completed;
|
||||||
_currentPhase = null;
|
_currentPhase = null;
|
||||||
|
if (_currentMap != null)
|
||||||
|
{
|
||||||
|
_entity.HideEntity(_currentMap);
|
||||||
|
}
|
||||||
Log.Info("CombatScheduler level completed. Level={0}.", _currentLevel != null ? _currentLevel.Id : 0);
|
Log.Info("CombatScheduler level completed. Level={0}.", _currentLevel != null ? _currentLevel.Id : 0);
|
||||||
|
GameEntry.CombatNode.EndCombat();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,39 +278,6 @@ namespace GeometryTD.CustomComponent
|
||||||
spawnEntries != null ? spawnEntries.Count : 0);
|
spawnEntries != null ? spawnEntries.Count : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HideCurrentMapIfNeeded()
|
|
||||||
{
|
|
||||||
if (_entity == null)
|
|
||||||
{
|
|
||||||
_currentMap = null;
|
|
||||||
_loadingMapEntityId = 0;
|
|
||||||
_loadedMapEntityId = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_loadingMapEntityId != 0)
|
|
||||||
{
|
|
||||||
EntityBase loadingMap = _entity.GetGameEntity(_loadingMapEntityId);
|
|
||||||
if (loadingMap != null)
|
|
||||||
{
|
|
||||||
_entity.HideEntity(loadingMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_loadedMapEntityId != 0)
|
|
||||||
{
|
|
||||||
EntityBase loadedMap = _entity.GetGameEntity(_loadedMapEntityId);
|
|
||||||
if (loadedMap != null)
|
|
||||||
{
|
|
||||||
_entity.HideEntity(loadedMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadingMapEntityId = 0;
|
|
||||||
_loadedMapEntityId = 0;
|
|
||||||
_currentMap = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsureEntityEventSubscribed()
|
private void EnsureEntityEventSubscribed()
|
||||||
{
|
{
|
||||||
if (_isEntityEventSubscribed)
|
if (_isEntityEventSubscribed)
|
||||||
|
|
@ -340,9 +312,40 @@ namespace GeometryTD.CustomComponent
|
||||||
_currentPhase = null;
|
_currentPhase = null;
|
||||||
_currentPhaseIndex = -1;
|
_currentPhaseIndex = -1;
|
||||||
_currentPhaseElapsed = 0f;
|
_currentPhaseElapsed = 0f;
|
||||||
|
_loadingMapEntityId = 0;
|
||||||
|
_loadedMapEntityId = 0;
|
||||||
|
_currentMap = null;
|
||||||
_state = SchedulerState.Idle;
|
_state = SchedulerState.Idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CleanupBattleEntities()
|
||||||
|
{
|
||||||
|
if (_entity == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_loadingMapEntityId != 0 && _entity.IsLoadingEntity(_loadingMapEntityId))
|
||||||
|
{
|
||||||
|
_entity.HideEntity(_loadingMapEntityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentMap != null)
|
||||||
|
{
|
||||||
|
_entity.HideEntity(_currentMap);
|
||||||
|
}
|
||||||
|
else if (_loadedMapEntityId != 0)
|
||||||
|
{
|
||||||
|
UnityGameFramework.Runtime.Entity loadedMapEntity = _entity.GetEntity(_loadedMapEntityId);
|
||||||
|
if (loadedMapEntity != null)
|
||||||
|
{
|
||||||
|
_entity.HideEntity(loadedMapEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_enemyManager.CleanupTrackedEnemies();
|
||||||
|
}
|
||||||
|
|
||||||
#region Event Handlers
|
#region Event Handlers
|
||||||
|
|
||||||
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ namespace GeometryTD.CustomComponent
|
||||||
private readonly Dictionary<int, Spawner> _spawnerByOrder = new Dictionary<int, Spawner>();
|
private readonly Dictionary<int, Spawner> _spawnerByOrder = new Dictionary<int, Spawner>();
|
||||||
private readonly List<Vector3> _pathBuffer = new List<Vector3>();
|
private readonly List<Vector3> _pathBuffer = new List<Vector3>();
|
||||||
private readonly List<SpawnEntryRuntime> _spawnRuntimes = new List<SpawnEntryRuntime>();
|
private readonly List<SpawnEntryRuntime> _spawnRuntimes = new List<SpawnEntryRuntime>();
|
||||||
|
private readonly HashSet<int> _trackedEnemyEntityIds = new HashSet<int>();
|
||||||
|
private readonly List<int> _trackedEnemyIdBuffer = new List<int>();
|
||||||
|
|
||||||
private CombatScheduler _combatScheduler;
|
private CombatScheduler _combatScheduler;
|
||||||
private EntityComponent _entity;
|
private EntityComponent _entity;
|
||||||
|
|
@ -66,6 +68,8 @@ namespace GeometryTD.CustomComponent
|
||||||
_spawnerByOrder.Clear();
|
_spawnerByOrder.Clear();
|
||||||
_pathBuffer.Clear();
|
_pathBuffer.Clear();
|
||||||
_spawnRuntimes.Clear();
|
_spawnRuntimes.Clear();
|
||||||
|
_trackedEnemyEntityIds.Clear();
|
||||||
|
_trackedEnemyIdBuffer.Clear();
|
||||||
_phaseElapsed = 0f;
|
_phaseElapsed = 0f;
|
||||||
_isPhaseRunning = false;
|
_isPhaseRunning = false;
|
||||||
IsPhaseSpawnCompleted = true;
|
IsPhaseSpawnCompleted = true;
|
||||||
|
|
@ -134,6 +138,7 @@ namespace GeometryTD.CustomComponent
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CleanupTrackedEnemies();
|
||||||
EndPhase();
|
EndPhase();
|
||||||
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||||
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||||
|
|
@ -142,12 +147,47 @@ namespace GeometryTD.CustomComponent
|
||||||
_spawners.Clear();
|
_spawners.Clear();
|
||||||
_spawnerByOrder.Clear();
|
_spawnerByOrder.Clear();
|
||||||
_pathBuffer.Clear();
|
_pathBuffer.Clear();
|
||||||
|
_trackedEnemyEntityIds.Clear();
|
||||||
|
_trackedEnemyIdBuffer.Clear();
|
||||||
|
_currentEnemyCount = 0;
|
||||||
_currentMapEntityId = 0;
|
_currentMapEntityId = 0;
|
||||||
_nextSpawnerIndex = 0;
|
_nextSpawnerIndex = 0;
|
||||||
_combatScheduler = null;
|
_combatScheduler = null;
|
||||||
_initialized = false;
|
_initialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CleanupTrackedEnemies()
|
||||||
|
{
|
||||||
|
if (_trackedEnemyEntityIds.Count <= 0)
|
||||||
|
{
|
||||||
|
_currentEnemyCount = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_trackedEnemyIdBuffer.Clear();
|
||||||
|
foreach (int trackedEnemyEntityId in _trackedEnemyEntityIds)
|
||||||
|
{
|
||||||
|
_trackedEnemyIdBuffer.Add(trackedEnemyEntityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_trackedEnemyEntityIds.Clear();
|
||||||
|
_currentEnemyCount = 0;
|
||||||
|
|
||||||
|
if (_entity == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _trackedEnemyIdBuffer.Count; i++)
|
||||||
|
{
|
||||||
|
int trackedEnemyEntityId = _trackedEnemyIdBuffer[i];
|
||||||
|
if (_entity.HasEntity(trackedEnemyEntityId) || _entity.IsLoadingEntity(trackedEnemyEntityId))
|
||||||
|
{
|
||||||
|
_entity.HideEntity(trackedEnemyEntityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private SpawnEntryRuntime BuildSpawnRuntime(DRLevelSpawnEntry entry)
|
private SpawnEntryRuntime BuildSpawnRuntime(DRLevelSpawnEntry entry)
|
||||||
{
|
{
|
||||||
if (entry == null || entry.EntryType == EntryType.None)
|
if (entry == null || entry.EntryType == EntryType.None)
|
||||||
|
|
@ -167,12 +207,12 @@ namespace GeometryTD.CustomComponent
|
||||||
switch (entry.EntryType)
|
switch (entry.EntryType)
|
||||||
{
|
{
|
||||||
case EntryType.Stream:
|
case EntryType.Stream:
|
||||||
{
|
{
|
||||||
float duration = Mathf.Max(0f, entry.Duration);
|
float duration = Mathf.Max(0f, entry.Duration);
|
||||||
runtime.EndTime = duration > 0f ? runtime.NextTriggerTime + duration : runtime.NextTriggerTime;
|
runtime.EndTime = duration > 0f ? runtime.NextTriggerTime + duration : runtime.NextTriggerTime;
|
||||||
runtime.Completed = entry.Count <= 0;
|
runtime.Completed = entry.Count <= 0;
|
||||||
return runtime;
|
return runtime;
|
||||||
}
|
}
|
||||||
case EntryType.Burst:
|
case EntryType.Burst:
|
||||||
case EntryType.Boss:
|
case EntryType.Boss:
|
||||||
runtime.Completed = runtime.RemainingCount <= 0;
|
runtime.Completed = runtime.RemainingCount <= 0;
|
||||||
|
|
@ -311,6 +351,7 @@ namespace GeometryTD.CustomComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
int enemyEntityId = _entity.GenerateSerialId();
|
int enemyEntityId = _entity.GenerateSerialId();
|
||||||
|
_trackedEnemyEntityIds.Add(enemyEntityId);
|
||||||
EnemyData enemyData = new EnemyData(
|
EnemyData enemyData = new EnemyData(
|
||||||
enemyEntityId,
|
enemyEntityId,
|
||||||
enemyConfig.EntityId,
|
enemyConfig.EntityId,
|
||||||
|
|
@ -434,17 +475,28 @@ namespace GeometryTD.CustomComponent
|
||||||
|
|
||||||
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
||||||
{
|
{
|
||||||
if (e is ShowEntitySuccessEventArgs ne)
|
if (!(e is ShowEntitySuccessEventArgs ne)) return;
|
||||||
|
|
||||||
|
if (ne.EntityLogicType == typeof(EnemyEntity) &&
|
||||||
|
_trackedEnemyEntityIds.Contains(ne.Entity.Id))
|
||||||
{
|
{
|
||||||
if (ne.EntityLogicType == typeof(EnemyEntity))
|
_currentEnemyCount++;
|
||||||
{
|
|
||||||
_currentEnemyCount++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnShowEntityFailure(object sender, GameEventArgs e)
|
private void OnShowEntityFailure(object sender, GameEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (!(e is ShowEntityFailureEventArgs ne))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ne.EntityLogicType != typeof(EnemyEntity))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_trackedEnemyEntityIds.Remove(ne.EntityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnHideEntityComplete(object sender, GameEventArgs e)
|
private void OnHideEntityComplete(object sender, GameEventArgs e)
|
||||||
|
|
@ -454,7 +506,7 @@ namespace GeometryTD.CustomComponent
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ne.EntityGroup.Name != "Enemy")
|
if (!_trackedEnemyEntityIds.Remove(ne.EntityId))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -462,4 +514,4 @@ namespace GeometryTD.CustomComponent
|
||||||
_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1);
|
_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using GameFramework.DataTable;
|
using GameFramework.DataTable;
|
||||||
|
using GeometryTD.CustomEvent;
|
||||||
using GeometryTD.DataTable;
|
using GeometryTD.DataTable;
|
||||||
using GeometryTD.Definition;
|
using GeometryTD.Definition;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
@ -72,9 +73,16 @@ namespace GeometryTD.CustomComponent
|
||||||
Log.Warning("EventNodeComponent StartEvent failed. Event form is not initialized.");
|
Log.Warning("EventNodeComponent StartEvent failed. Event form is not initialized.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_eventFormUseCase.SetCurrentEvent(randomEvent);
|
_eventFormUseCase.SetCurrentEvent(randomEvent);
|
||||||
_eventFormController.OpenUI();
|
_eventFormController.OpenUI();
|
||||||
|
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndEvent()
|
||||||
|
{
|
||||||
|
GameEntry.UIRouter.CloseUI(UIFormType.EventForm);
|
||||||
|
GameEntry.Event.Fire(this, NodeCompleteEventArgs.Create());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static EventOption[] ParseOptions(string optionsRaw)
|
private static EventOption[] ParseOptions(string optionsRaw)
|
||||||
|
|
@ -169,4 +177,4 @@ namespace GeometryTD.CustomComponent
|
||||||
return effects.ToArray();
|
return effects.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,6 +74,7 @@ namespace GeometryTD.Procedure
|
||||||
if (!(e is NodeEnterEventArgs)) return;
|
if (!(e is NodeEnterEventArgs)) return;
|
||||||
|
|
||||||
GameEntry.UIRouter.CloseUI(UIFormType.TestMenuForm);
|
GameEntry.UIRouter.CloseUI(UIFormType.TestMenuForm);
|
||||||
|
GameEntry.UIRouter.CloseUI(UIFormType.MainForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnNodeComplete(object sender, GameEventArgs e)
|
private void OnNodeComplete(object sender, GameEventArgs e)
|
||||||
|
|
@ -81,6 +82,7 @@ namespace GeometryTD.Procedure
|
||||||
if (!(e is NodeCompleteEventArgs)) return;
|
if (!(e is NodeCompleteEventArgs)) return;
|
||||||
|
|
||||||
GameEntry.UIRouter.OpenUI(UIFormType.TestMenuForm);
|
GameEntry.UIRouter.OpenUI(UIFormType.TestMenuForm);
|
||||||
|
GameEntry.UIRouter.OpenUI(UIFormType.MainForm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +118,6 @@ namespace GeometryTD.UI
|
||||||
}
|
}
|
||||||
|
|
||||||
m_UseCase.SelectOption(args.SelectedItemId);
|
m_UseCase.SelectOption(args.SelectedItemId);
|
||||||
CloseUI();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using GeometryTD.CustomComponent;
|
||||||
using GeometryTD.CustomEvent;
|
using GeometryTD.CustomEvent;
|
||||||
using GeometryTD.Definition;
|
using GeometryTD.Definition;
|
||||||
using UnityGameFramework.Runtime;
|
using UnityGameFramework.Runtime;
|
||||||
|
|
@ -6,42 +7,42 @@ namespace GeometryTD.UI
|
||||||
{
|
{
|
||||||
public class EventFormUseCase : IUIUseCase
|
public class EventFormUseCase : IUIUseCase
|
||||||
{
|
{
|
||||||
private EventItem m_CurrentEvent;
|
private EventItem _currentEvent;
|
||||||
|
|
||||||
public void SetCurrentEvent(EventItem eventItem)
|
public void SetCurrentEvent(EventItem eventItem)
|
||||||
{
|
{
|
||||||
m_CurrentEvent = eventItem;
|
_currentEvent = eventItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventFormRawData CreateInitialModel()
|
public EventFormRawData CreateInitialModel()
|
||||||
{
|
{
|
||||||
if (m_CurrentEvent == null)
|
if (_currentEvent == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new EventFormRawData
|
return new EventFormRawData
|
||||||
{
|
{
|
||||||
EventItem = m_CurrentEvent
|
EventItem = _currentEvent
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventOption SelectOption(int optionIndex)
|
public EventOption SelectOption(int optionIndex)
|
||||||
{
|
{
|
||||||
if (m_CurrentEvent == null || m_CurrentEvent.Options == null)
|
if (_currentEvent == null || _currentEvent.Options == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (optionIndex < 0 || optionIndex >= m_CurrentEvent.Options.Length)
|
if (optionIndex < 0 || optionIndex >= _currentEvent.Options.Length)
|
||||||
{
|
{
|
||||||
Log.Warning("EventFormUseCase.SelectOption() option index is invalid: {0}", optionIndex);
|
Log.Warning("EventFormUseCase.SelectOption() option index is invalid: {0}", optionIndex);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 执行 Requirements 校验、CostEffects/RewardEffects 结算与后续节点推进。
|
// TODO: 执行 Requirements 校验、CostEffects/RewardEffects 结算与后续节点推进。
|
||||||
GameEntry.Event.Fire(this, NodeCompleteEventArgs.Create());
|
GameEntry.EventNode.EndEvent();
|
||||||
return m_CurrentEvent.Options[optionIndex];
|
return _currentEvent.Options[optionIndex];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -490,7 +490,7 @@ PrefabInstance:
|
||||||
objectReference: {fileID: 1576232203}
|
objectReference: {fileID: 1576232203}
|
||||||
- target: {fileID: 11454530, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11454530, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_UIGroups.Array.size
|
propertyPath: m_UIGroups.Array.size
|
||||||
value: 1
|
value: 2
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11454530, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11454530, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_UIGroupHelperTypeName
|
propertyPath: m_UIGroupHelperTypeName
|
||||||
|
|
@ -502,7 +502,7 @@ PrefabInstance:
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11454530, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11454530, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_UIGroups.Array.data[1].m_Name
|
propertyPath: m_UIGroups.Array.data[1].m_Name
|
||||||
value: Default
|
value: Test
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11454530, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11454530, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_UIGroups.Array.data[0].m_Depth
|
propertyPath: m_UIGroups.Array.data[0].m_Depth
|
||||||
|
|
@ -510,7 +510,7 @@ PrefabInstance:
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11454530, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11454530, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_UIGroups.Array.data[1].m_Depth
|
propertyPath: m_UIGroups.Array.data[1].m_Depth
|
||||||
value: 0
|
value: 2
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11461470, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11461470, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_LocalizationHelperTypeName
|
propertyPath: m_LocalizationHelperTypeName
|
||||||
|
|
@ -522,7 +522,7 @@ PrefabInstance:
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.size
|
propertyPath: m_EntityGroups.Array.size
|
||||||
value: 5
|
value: 4
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[0].m_Name
|
propertyPath: m_EntityGroups.Array.data[0].m_Name
|
||||||
|
|
@ -538,7 +538,7 @@ PrefabInstance:
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[3].m_Name
|
propertyPath: m_EntityGroups.Array.data[3].m_Name
|
||||||
value: Player
|
value: Map
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[4].m_Name
|
propertyPath: m_EntityGroups.Array.data[4].m_Name
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
# CombatNode 架构摘要
|
||||||
|
|
||||||
|
最后更新:2026-02-28
|
||||||
|
|
||||||
|
## 1. 目标与边界
|
||||||
|
|
||||||
|
CombatNode 子系统的目标是把“战斗节点”拆成三个稳定层:
|
||||||
|
|
||||||
|
- `CombatNodeComponent`:节点入口与配置缓存(门面层)
|
||||||
|
- `CombatScheduler`:单局战斗状态机(编排层)
|
||||||
|
- `EnemyManager`:刷怪与敌人生命周期(执行层)
|
||||||
|
|
||||||
|
边界约束:
|
||||||
|
|
||||||
|
- 只负责战斗节点,不负责菜单流程切换,不负责 UI 细节。
|
||||||
|
- 只依赖数据表和 Entity 系统,不直接持有场景流程 FSM。
|
||||||
|
- 地图寻路由 `MapEntity` 提供能力,敌人移动由 `EnemyEntity` 自治。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 模块职责划分
|
||||||
|
|
||||||
|
## 2.1 CombatNodeComponent(门面层)
|
||||||
|
|
||||||
|
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 读取并缓存 `DRLevel / DRLevelPhase / DRLevelSpawnEntry`。
|
||||||
|
- 按主题筛选关卡,维护 `Level -> Phase -> SpawnEntry` 映射。
|
||||||
|
- 提供 `OnInit / StartCombat / OnUpdate / OnShutdown` 外部接口。
|
||||||
|
- 触发节点事件:
|
||||||
|
- `NodeEnterEventArgs`
|
||||||
|
- `NodeCompleteEventArgs`
|
||||||
|
|
||||||
|
不做的事:
|
||||||
|
|
||||||
|
- 不做阶段推进。
|
||||||
|
- 不做刷怪时序。
|
||||||
|
- 不做实体显示/隐藏细节。
|
||||||
|
|
||||||
|
## 2.2 CombatScheduler(编排层)
|
||||||
|
|
||||||
|
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler.cs`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 管理战斗状态机:`Idle -> WaitingForMap -> RunningPhase -> Completed/Failed`。
|
||||||
|
- 加载地图实体,接收地图 show/hide 成功失败事件。
|
||||||
|
- 按 phase 结束条件推进到下一阶段。
|
||||||
|
- 在开始新战斗或销毁时做统一清场(地图 + 本局敌人)。
|
||||||
|
|
||||||
|
不做的事:
|
||||||
|
|
||||||
|
- 不直接计算每条刷怪规则的触发细节(交给 `EnemyManager`)。
|
||||||
|
|
||||||
|
## 2.3 EnemyManager(执行层)
|
||||||
|
|
||||||
|
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager.cs`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 将 `DRLevelSpawnEntry` 转为运行时任务(stream/burst/boss)。
|
||||||
|
- 按时间推进刷怪任务并生成 `EnemyData`。
|
||||||
|
- 维护本局敌人数量 `AliveEnemyCount`。
|
||||||
|
- 维护“本局生成敌人 ID 集合”,用于准确计数与清场。
|
||||||
|
|
||||||
|
关键点:
|
||||||
|
|
||||||
|
- 生命周期归属按 `entityId` 跟踪,不再按实体组名粗粒度统计。
|
||||||
|
- 清场可覆盖“已加载 + 加载中”的本局敌人。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 关键实体职责
|
||||||
|
|
||||||
|
## 3.1 MapEntity
|
||||||
|
|
||||||
|
文件:`Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 读取 Tilemap,识别 Path/Foundation 格子。
|
||||||
|
- 为每个 `Spawner` 缓存默认可达路径。
|
||||||
|
- 提供:
|
||||||
|
- `TryGetDefaultPathCells`
|
||||||
|
- `TryFindPathCells`
|
||||||
|
- `TryFindPathWorldPoints`
|
||||||
|
|
||||||
|
## 3.2 EnemyEntity
|
||||||
|
|
||||||
|
文件:`Assets/GameMain/Scripts/Entity/EntityLogic/EnemyEntity.cs`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 按路径点移动。
|
||||||
|
- 到达终点后调用 `HideEntity` 自行退出。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 运行时时序(简版)
|
||||||
|
|
||||||
|
1. 菜单触发战斗:`TestMenuFormController -> GameEntry.CombatNode.StartCombat()`
|
||||||
|
2. `CombatNodeComponent.StartCombat()` 选关并把关卡配置交给 `CombatScheduler.Start()`
|
||||||
|
3. `CombatScheduler.Start()` 先清理上局残留,再 `ShowMap(...)`,状态进入 `WaitingForMap`
|
||||||
|
4. 地图加载成功事件到达,`_currentMap` 就绪
|
||||||
|
5. `BeginNextPhase()` 进入 `RunningPhase`,`EnemyManager.BeginPhase(...)`
|
||||||
|
6. 每帧 `OnUpdate`:
|
||||||
|
- `CombatScheduler` 更新时间与结束条件
|
||||||
|
- `EnemyManager` 推进刷怪任务并创建敌人
|
||||||
|
7. 当前 phase 满足结束条件,`CompleteCurrentPhase()`,然后进入下一 phase
|
||||||
|
8. 全 phase 完成后隐藏地图,`GameEntry.CombatNode.EndCombat()`,抛 `NodeComplete`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 数据契约与 ID 规则
|
||||||
|
|
||||||
|
数据来源:
|
||||||
|
|
||||||
|
- `DRLevel`
|
||||||
|
- `DRLevelPhase`
|
||||||
|
- `DRLevelSpawnEntry`
|
||||||
|
|
||||||
|
当前约定(依赖 ID 编码):
|
||||||
|
|
||||||
|
- `levelId = phaseId / 1000`
|
||||||
|
- `phaseId = spawnEntryId / 1000`
|
||||||
|
|
||||||
|
影响:
|
||||||
|
|
||||||
|
- 新增配置时必须遵守该编码,否则 phase 和 entry 无法被正确归属。
|
||||||
|
|
||||||
|
建议:
|
||||||
|
|
||||||
|
- 若后续改数据结构,优先改成显式外键字段,减少 ID 推导耦合。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 结束条件模型
|
||||||
|
|
||||||
|
枚举:`PhaseEndType`
|
||||||
|
|
||||||
|
- `TimeElapsed`:按 `EndParam`(可解析 float)或 `DurationSeconds`
|
||||||
|
- `EnemiesCleared`:`IsPhaseSpawnCompleted && AliveEnemyCount <= 0`
|
||||||
|
- `BossDead`:当前与 `EnemiesCleared` 同判定
|
||||||
|
- `None`:有 duration 走 duration,否则退化为清怪判定
|
||||||
|
|
||||||
|
实现点:`CombatScheduler.ShouldEndCurrentPhase()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 生命周期与清场策略(当前实现)
|
||||||
|
|
||||||
|
## 7.1 地图生命周期
|
||||||
|
|
||||||
|
- 通过 `ShowMap` 异步加载。
|
||||||
|
- 通过 `ShowEntitySuccess/Failure` 绑定运行态地图引用。
|
||||||
|
- 完成关卡时隐藏地图。
|
||||||
|
- 新战斗开始和调度器销毁前都会尝试清理地图实体。
|
||||||
|
|
||||||
|
## 7.2 敌人生命周期
|
||||||
|
|
||||||
|
- 创建时登记 `entityId` 到 `_trackedEnemyEntityIds`。
|
||||||
|
- `ShowEntitySuccess` 仅对 tracked id 计数。
|
||||||
|
- `HideEntityComplete` 仅对 tracked id 减计数并移除跟踪。
|
||||||
|
- 清场时按 tracked id 隐藏(覆盖加载中和已加载实体)。
|
||||||
|
|
||||||
|
## 7.3 设计目标
|
||||||
|
|
||||||
|
- 防止跨局残留实体污染新局状态。
|
||||||
|
- 防止其他系统 `Enemy` 组实体干扰战斗计数。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 扩展指南(按需求)
|
||||||
|
|
||||||
|
## 8.1 新增阶段结束条件
|
||||||
|
|
||||||
|
- 改 `PhaseEndType` 枚举。
|
||||||
|
- 在 `ShouldEndCurrentPhase()` 增加分支。
|
||||||
|
|
||||||
|
## 8.2 新增刷怪条目类型
|
||||||
|
|
||||||
|
- 改 `EntryType` 枚举。
|
||||||
|
- 在 `EnemyManager.BuildSpawnRuntime()` 与 `UpdateSpawnRuntimes()` 增加处理。
|
||||||
|
|
||||||
|
## 8.3 改选关策略
|
||||||
|
|
||||||
|
- 当前是随机选关:`TrySelectRandomLevel()`。
|
||||||
|
- 可改为按难度、解锁进度、权重池。
|
||||||
|
|
||||||
|
## 8.4 动态阻挡/重算路径
|
||||||
|
|
||||||
|
- 当前 `SpawnEnemies` 调 `TryFindPathWorldPoints(spawner, null, ...)`。
|
||||||
|
- 若支持塔阻挡,传入 `blockedCells` 并处理“无路可走”策略。
|
||||||
|
|
||||||
|
## 8.5 结算与奖励
|
||||||
|
|
||||||
|
- 推荐挂在 `CombatScheduler` 完成分支(所有 phase 完成处),不要塞进 `EnemyManager`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 维护时的硬性不变量
|
||||||
|
|
||||||
|
- `CombatNodeComponent` 只做缓存和入口,不写战斗细节。
|
||||||
|
- `CombatScheduler` 是唯一 phase 状态机,不在别处推进 phase。
|
||||||
|
- `AliveEnemyCount` 必须由 tracked id 驱动,不回退到组名统计。
|
||||||
|
- 新战斗开始前必须清场。
|
||||||
|
- `ResetRuntime()` 必须清空地图相关运行态引用和 id。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 快速阅读路径(新人 10 分钟)
|
||||||
|
|
||||||
|
1. `CombatNodeComponent.StartCombat()`
|
||||||
|
2. `CombatScheduler.Start() / OnUpdate() / BeginNextPhase()`
|
||||||
|
3. `EnemyManager.BeginPhase() / OnUpdate() / SpawnEnemies()`
|
||||||
|
4. `MapEntity.TryFindPathWorldPoints()`
|
||||||
|
5. `EnemyEntity.DespawnOnReachHouse()`
|
||||||
|
|
||||||
|
这 5 个点读完,基本能覆盖 90% 战斗节点行为。
|
||||||
Loading…
Reference in New Issue