添加 CombatNodeComponent 及相关类
- CombatNodeComponent:节点组件,负责提取并开始关卡 - CombatScheduler:关卡调度器,负责关卡的生命周期调度 - MapEntity:地图实体,提供地图相关的数据 - Spawner:出生点,负责标记地图上的敌人出生点坐标 - House:基地,负责标记地图上的敌人目标点坐标
This commit is contained in:
@@ -17,8 +17,6 @@ public partial class GameEntry : MonoBehaviour
|
||||
public static BuiltinDataComponent BuiltinData { get; private set; }
|
||||
|
||||
public static HPBarComponent HPBar { get; private set; }
|
||||
|
||||
public static EnemyManagerComponent EnemyManager { get; private set; }
|
||||
|
||||
public static UIRouterComponent UIRouter { get; private set; }
|
||||
|
||||
@@ -30,7 +28,6 @@ public partial class GameEntry : MonoBehaviour
|
||||
{
|
||||
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
|
||||
HPBar = UnityGameFramework.Runtime.GameEntry.GetComponent<HPBarComponent>();
|
||||
EnemyManager = UnityGameFramework.Runtime.GameEntry.GetComponent<EnemyManagerComponent>();
|
||||
UIRouter = UnityGameFramework.Runtime.GameEntry.GetComponent<UIRouterComponent>();
|
||||
EventNode = UnityGameFramework.Runtime.GameEntry.GetComponent<EventNodeComponent>();
|
||||
CombatNode = UnityGameFramework.Runtime.GameEntry.GetComponent<CombatNodeComponent>();
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c942b083720d4d588063c5f1dde93428
|
||||
timeCreated: 1772165636
|
||||
+123
-13
@@ -8,7 +8,7 @@ using UnityGameFramework.Runtime;
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// 战斗节点组件
|
||||
/// 鎴樻枟鑺傜偣缁勪欢
|
||||
/// </summary>
|
||||
public class CombatNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
@@ -20,19 +20,25 @@ namespace GeometryTD.CustomComponent
|
||||
|
||||
// LevelPhase.Id => LevelSpawnEntries
|
||||
private readonly Dictionary<int, List<DRLevelSpawnEntry>> _spawnEntriesByPhaseId = new();
|
||||
|
||||
private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _selectedSpawnEntriesByPhaseId = new();
|
||||
private readonly List<int> _levelIdBuffer = new();
|
||||
private readonly CombatScheduler _combatScheduler = new CombatScheduler();
|
||||
|
||||
private bool _runtimeInitialized;
|
||||
|
||||
public LevelThemeType CurrentThemeType { get; private set; }
|
||||
public DRLevel CurrentLevel { get; private set; }
|
||||
|
||||
public void OnInit(LevelThemeType themeType)
|
||||
{
|
||||
ShutdownBattleRuntime();
|
||||
|
||||
CurrentThemeType = themeType;
|
||||
CurrentLevel = null;
|
||||
_levelsById.Clear();
|
||||
_phasesByLevelId.Clear();
|
||||
_spawnEntriesByPhaseId.Clear();
|
||||
_selectedSpawnEntriesByPhaseId.Clear();
|
||||
_levelIdBuffer.Clear();
|
||||
|
||||
IDataTable<DRLevel> dtLevel = GameEntry.DataTable.GetDataTable<DRLevel>();
|
||||
@@ -45,8 +51,9 @@ namespace GeometryTD.CustomComponent
|
||||
}
|
||||
|
||||
DRLevel[] levels = dtLevel.GetAllDataRows();
|
||||
foreach (var level in levels)
|
||||
for (int i = 0; i < levels.Length; i++)
|
||||
{
|
||||
DRLevel level = levels[i];
|
||||
if (level.LevelThemeType != themeType)
|
||||
{
|
||||
continue;
|
||||
@@ -58,8 +65,9 @@ namespace GeometryTD.CustomComponent
|
||||
}
|
||||
|
||||
DRLevelPhase[] levelPhases = dtLevelPhase.GetAllDataRows();
|
||||
foreach (var phase in levelPhases)
|
||||
for (int i = 0; i < levelPhases.Length; i++)
|
||||
{
|
||||
DRLevelPhase phase = levelPhases[i];
|
||||
int levelId = phase.Id / 1000;
|
||||
if (!_levelsById.ContainsKey(levelId))
|
||||
{
|
||||
@@ -96,28 +104,130 @@ namespace GeometryTD.CustomComponent
|
||||
entries.Add(spawnEntry);
|
||||
}
|
||||
|
||||
foreach (List<DRLevelPhase> phaseList in _phasesByLevelId.Values)
|
||||
{
|
||||
phaseList.Sort((left, right) => left.Id.CompareTo(right.Id));
|
||||
}
|
||||
|
||||
foreach (List<DRLevelSpawnEntry> phaseSpawnEntries in _spawnEntriesByPhaseId.Values)
|
||||
{
|
||||
phaseSpawnEntries.Sort((left, right) =>
|
||||
{
|
||||
int timeCompare = left.StartTime.CompareTo(right.StartTime);
|
||||
if (timeCompare != 0)
|
||||
{
|
||||
return timeCompare;
|
||||
}
|
||||
|
||||
return left.Id.CompareTo(right.Id);
|
||||
});
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
"CombatNodeComponent initialized. Theme={0}, Levels={1}, Phases={2}, Entries={3}",
|
||||
"CombatNodeComponent initialized. Theme={0}, Levels={1}, Phases={2}, Entries={3}.",
|
||||
themeType,
|
||||
_levelsById.Count,
|
||||
CountPhases(),
|
||||
CountEntries());
|
||||
|
||||
EnsureBattleRuntimeInitialized();
|
||||
}
|
||||
|
||||
public void StartCombat()
|
||||
{
|
||||
if (_levelIdBuffer.Count <= 0)
|
||||
if (!EnsureBattleRuntimeInitialized())
|
||||
{
|
||||
Log.Warning("CombatNodeComponent start failed. Missing scheduler runtime.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TrySelectRandomLevel(out DRLevel selectedLevel))
|
||||
{
|
||||
Log.Warning("CombatNodeComponent has no level cache. Call OnInit(levelThemeType) first.");
|
||||
return;
|
||||
}
|
||||
|
||||
int randomIndex = Random.Range(0, _levelIdBuffer.Count);
|
||||
int randomLevelId = _levelIdBuffer[randomIndex];
|
||||
CurrentLevel = _levelsById[randomLevelId];
|
||||
if (!_phasesByLevelId.TryGetValue(selectedLevel.Id, out List<DRLevelPhase> phaseList) ||
|
||||
phaseList == null ||
|
||||
phaseList.Count <= 0)
|
||||
{
|
||||
Log.Warning("CombatNodeComponent start failed. Level '{0}' has no phase data.", selectedLevel.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 在这里接入真实战斗节点玩法启动流程(场景进入、敌人生成、结算等)。
|
||||
Log.Info("StartCombat selected level '{0}' (Theme={1}).", CurrentLevel.Id, CurrentThemeType);
|
||||
_selectedSpawnEntriesByPhaseId.Clear();
|
||||
foreach (var phase in phaseList)
|
||||
{
|
||||
if (_spawnEntriesByPhaseId.TryGetValue(phase.Id, out List<DRLevelSpawnEntry> entries) &&
|
||||
entries != null)
|
||||
{
|
||||
_selectedSpawnEntriesByPhaseId[phase.Id] = entries;
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedSpawnEntriesByPhaseId[phase.Id] = new List<DRLevelSpawnEntry>();
|
||||
}
|
||||
}
|
||||
|
||||
CurrentLevel = selectedLevel;
|
||||
_combatScheduler.Start(selectedLevel, phaseList, _selectedSpawnEntriesByPhaseId);
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (!_runtimeInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_combatScheduler.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
}
|
||||
|
||||
public void OnShutdown()
|
||||
{
|
||||
CurrentLevel = null;
|
||||
ShutdownBattleRuntime();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
OnShutdown();
|
||||
}
|
||||
|
||||
private bool EnsureBattleRuntimeInitialized()
|
||||
{
|
||||
if (_runtimeInitialized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_combatScheduler.OnInit();
|
||||
_runtimeInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ShutdownBattleRuntime()
|
||||
{
|
||||
if (!_runtimeInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_combatScheduler.OnDestroy();
|
||||
_runtimeInitialized = false;
|
||||
}
|
||||
|
||||
private bool TrySelectRandomLevel(out DRLevel level)
|
||||
{
|
||||
level = null;
|
||||
if (_levelIdBuffer.Count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int selectedIndex = Random.Range(0, _levelIdBuffer.Count);
|
||||
int selectedLevelId = _levelIdBuffer[selectedIndex];
|
||||
return _levelsById.TryGetValue(selectedLevelId, out level);
|
||||
}
|
||||
|
||||
private int CountPhases()
|
||||
@@ -134,7 +244,7 @@ namespace GeometryTD.CustomComponent
|
||||
private int CountEntries()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var list in _spawnEntriesByPhaseId.Values)
|
||||
foreach (List<DRLevelSpawnEntry> list in _spawnEntriesByPhaseId.Values)
|
||||
{
|
||||
count += list.Count;
|
||||
}
|
||||
@@ -142,4 +252,4 @@ namespace GeometryTD.CustomComponent
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using GameFramework.Event;
|
||||
using GameFramework.Resource;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class CombatScheduler
|
||||
{
|
||||
private enum SchedulerState : byte
|
||||
{
|
||||
Idle = 0,
|
||||
WaitingForMap = 1,
|
||||
RunningPhase = 2,
|
||||
Completed = 3,
|
||||
Failed = 4
|
||||
}
|
||||
|
||||
private readonly List<DRLevelPhase> _phaseBuffer = new List<DRLevelPhase>();
|
||||
private readonly Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>> _spawnEntriesByPhaseId =
|
||||
new Dictionary<int, IReadOnlyList<DRLevelSpawnEntry>>();
|
||||
|
||||
private readonly EnemyManager _enemyManager = new EnemyManager();
|
||||
|
||||
private EntityComponent _entity;
|
||||
private DRLevel _currentLevel;
|
||||
private DRLevelPhase _currentPhase;
|
||||
private int _currentPhaseIndex;
|
||||
private float _currentPhaseElapsed;
|
||||
private SchedulerState _state = SchedulerState.Idle;
|
||||
private int _loadingMapEntityId;
|
||||
private int _loadedMapEntityId;
|
||||
private bool _initialized;
|
||||
private bool _isEntityEventSubscribed;
|
||||
private MapEntity _currentMap;
|
||||
|
||||
public bool IsRunning => _state == SchedulerState.WaitingForMap || _state == SchedulerState.RunningPhase;
|
||||
public bool IsCompleted => _state == SchedulerState.Completed;
|
||||
public DRLevel CurrentLevel => _currentLevel;
|
||||
public DRLevelPhase CurrentPhase => _currentPhase;
|
||||
public MapEntity CurrentMap => _currentMap;
|
||||
|
||||
public void OnInit()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
_entity = GameEntry.Entity;
|
||||
EnsureEntityEventSubscribed();
|
||||
_enemyManager.OnInit(this);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
ResetRuntime();
|
||||
}
|
||||
|
||||
public void Start(
|
||||
DRLevel level,
|
||||
IReadOnlyList<DRLevelPhase> phases,
|
||||
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId)
|
||||
{
|
||||
if (!_initialized || _entity == null)
|
||||
{
|
||||
_state = SchedulerState.Failed;
|
||||
Log.Warning("CombatScheduler start failed. Runtime is not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (level == null || phases == null || phases.Count <= 0)
|
||||
{
|
||||
_state = SchedulerState.Failed;
|
||||
Log.Warning("CombatScheduler start failed. Invalid level or phase data.");
|
||||
return;
|
||||
}
|
||||
|
||||
_enemyManager.EndPhase();
|
||||
HideCurrentMapIfNeeded();
|
||||
ResetRuntime();
|
||||
|
||||
_currentLevel = level;
|
||||
foreach (var phase in phases)
|
||||
{
|
||||
_phaseBuffer.Add(phase);
|
||||
}
|
||||
|
||||
if (spawnEntriesByPhaseId != null)
|
||||
{
|
||||
foreach (var pair in spawnEntriesByPhaseId)
|
||||
{
|
||||
_spawnEntriesByPhaseId[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (_phaseBuffer.Count <= 0)
|
||||
{
|
||||
_state = SchedulerState.Failed;
|
||||
Log.Warning("CombatScheduler start failed. Level '{0}' has no phase data.", level.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryShowMap(level))
|
||||
{
|
||||
_state = SchedulerState.Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
_currentPhase = null;
|
||||
_currentPhaseIndex = -1;
|
||||
_currentPhaseElapsed = 0f;
|
||||
_state = SchedulerState.WaitingForMap;
|
||||
|
||||
Log.Info("CombatScheduler started. Level={0}, PhaseCount={1}.", _currentLevel.Id, _phaseBuffer.Count);
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
switch (_state)
|
||||
{
|
||||
case SchedulerState.WaitingForMap:
|
||||
if (_currentMap == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BeginNextPhase();
|
||||
return;
|
||||
case SchedulerState.RunningPhase:
|
||||
UpdateCurrentPhase(elapseSeconds, realElapseSeconds);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_enemyManager.OnDestroy();
|
||||
HideCurrentMapIfNeeded();
|
||||
ResetRuntime();
|
||||
UnsubscribeEntityEvents();
|
||||
_entity = null;
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
private bool TryShowMap(DRLevel level)
|
||||
{
|
||||
string mapAssetName = level.Id.ToString();
|
||||
string mapAssetPath = AssetUtility.GetLevelMapAsset(mapAssetName);
|
||||
if (GameEntry.Resource.HasAsset(mapAssetPath) == HasAssetResult.NotExist)
|
||||
{
|
||||
Log.Warning(
|
||||
"CombatScheduler start failed. Level '{0}' map asset not found: '{1}'.",
|
||||
level.Id,
|
||||
mapAssetPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
_loadingMapEntityId = _entity.GenerateSerialId();
|
||||
_entity.ShowMap(new MapData(_loadingMapEntityId, level.Id, Vector3.zero), mapAssetName);
|
||||
|
||||
Log.Info(
|
||||
"CombatScheduler loading map. Level={0}, Asset='{1}', EntityId={2}.",
|
||||
level.Id,
|
||||
mapAssetPath,
|
||||
_loadingMapEntityId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateCurrentPhase(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (_currentPhase == null)
|
||||
{
|
||||
_state = SchedulerState.Failed;
|
||||
Log.Warning("CombatScheduler update failed. Current phase is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
_currentPhaseElapsed += elapseSeconds;
|
||||
_enemyManager.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
|
||||
if (!ShouldEndCurrentPhase())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CompleteCurrentPhase();
|
||||
}
|
||||
|
||||
private bool ShouldEndCurrentPhase()
|
||||
{
|
||||
switch (_currentPhase.EndType)
|
||||
{
|
||||
case PhaseEndType.TimeElapsed:
|
||||
return _currentPhaseElapsed >= ResolveTimeElapsedThreshold(_currentPhase);
|
||||
case PhaseEndType.EnemiesCleared:
|
||||
case PhaseEndType.BossDead:
|
||||
return _enemyManager.IsPhaseSpawnCompleted && _enemyManager.AliveEnemyCount <= 0;
|
||||
case PhaseEndType.None:
|
||||
default:
|
||||
if (_currentPhase.DurationSeconds > 0 && _currentPhaseElapsed >= _currentPhase.DurationSeconds)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _enemyManager.IsPhaseSpawnCompleted && _enemyManager.AliveEnemyCount <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
private float ResolveTimeElapsedThreshold(DRLevelPhase phase)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(phase.EndParam) &&
|
||||
float.TryParse(phase.EndParam, NumberStyles.Float, CultureInfo.InvariantCulture,
|
||||
out float parsedSeconds))
|
||||
{
|
||||
return parsedSeconds;
|
||||
}
|
||||
|
||||
if (phase.DurationSeconds > 0)
|
||||
{
|
||||
return phase.DurationSeconds;
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private void CompleteCurrentPhase()
|
||||
{
|
||||
_enemyManager.EndPhase();
|
||||
Log.Info(
|
||||
"CombatScheduler phase completed. Level={0}, Phase={1}, Elapsed={2:F2}s.",
|
||||
_currentLevel != null ? _currentLevel.Id : 0,
|
||||
_currentPhase != null ? _currentPhase.Id : 0,
|
||||
_currentPhaseElapsed);
|
||||
|
||||
BeginNextPhase();
|
||||
}
|
||||
|
||||
private void BeginNextPhase()
|
||||
{
|
||||
_currentPhaseIndex++;
|
||||
if (_currentPhaseIndex >= _phaseBuffer.Count)
|
||||
{
|
||||
_state = SchedulerState.Completed;
|
||||
_currentPhase = null;
|
||||
Log.Info("CombatScheduler level completed. Level={0}.", _currentLevel != null ? _currentLevel.Id : 0);
|
||||
return;
|
||||
}
|
||||
|
||||
_currentPhase = _phaseBuffer[_currentPhaseIndex];
|
||||
_currentPhaseElapsed = 0f;
|
||||
|
||||
IReadOnlyList<DRLevelSpawnEntry> spawnEntries = null;
|
||||
_spawnEntriesByPhaseId.TryGetValue(_currentPhase.Id, out spawnEntries);
|
||||
_enemyManager.BeginPhase(_currentPhase, spawnEntries);
|
||||
_state = SchedulerState.RunningPhase;
|
||||
|
||||
Log.Info(
|
||||
"CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.",
|
||||
_currentLevel != null ? _currentLevel.Id : 0,
|
||||
_currentPhase.Id,
|
||||
_currentPhase.EndType,
|
||||
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()
|
||||
{
|
||||
if (_isEntityEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
_isEntityEventSubscribed = true;
|
||||
}
|
||||
|
||||
private void UnsubscribeEntityEvents()
|
||||
{
|
||||
if (!_isEntityEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
_isEntityEventSubscribed = false;
|
||||
}
|
||||
|
||||
private void ResetRuntime()
|
||||
{
|
||||
_phaseBuffer.Clear();
|
||||
_spawnEntriesByPhaseId.Clear();
|
||||
_currentLevel = null;
|
||||
_currentPhase = null;
|
||||
_currentPhaseIndex = -1;
|
||||
_currentPhaseElapsed = 0f;
|
||||
_state = SchedulerState.Idle;
|
||||
}
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is ShowEntitySuccessEventArgs ne))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ne.EntityLogicType != typeof(MapEntity) || ne.Entity.Id != _loadingMapEntityId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_loadedMapEntityId = _loadingMapEntityId;
|
||||
_loadingMapEntityId = 0;
|
||||
_currentMap = ne.Entity.Logic as MapEntity;
|
||||
if (_currentMap == null)
|
||||
{
|
||||
_state = SchedulerState.Failed;
|
||||
Log.Error("Loaded map entity logic is invalid. EntityId={0}.", ne.Entity.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
"Map ready. LevelId={0}, PathCells={1}, FoundationCells={2}, Spawners={3}, House={4}.",
|
||||
_currentLevel != null ? _currentLevel.Id : 0,
|
||||
_currentMap.PathCells.Count,
|
||||
_currentMap.FoundationCells.Count,
|
||||
_currentMap.Spawners.Length,
|
||||
_currentMap.House != null ? _currentMap.House.name : "None");
|
||||
}
|
||||
|
||||
private void OnShowEntityFailure(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is ShowEntityFailureEventArgs ne))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ne.EntityLogicType != typeof(MapEntity) || ne.EntityId != _loadingMapEntityId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_loadingMapEntityId = 0;
|
||||
_currentMap = null;
|
||||
_state = SchedulerState.Failed;
|
||||
Log.Error(
|
||||
"Map load failed. LevelId={0}, Asset='{1}', Error='{2}'.",
|
||||
_currentLevel != null ? _currentLevel.Id : 0,
|
||||
ne.EntityAssetName,
|
||||
ne.ErrorMessage);
|
||||
}
|
||||
|
||||
private void OnHideEntityComplete(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is HideEntityCompleteEventArgs ne))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ne.EntityId == _loadedMapEntityId)
|
||||
{
|
||||
_loadedMapEntityId = 0;
|
||||
_currentMap = null;
|
||||
}
|
||||
|
||||
if (ne.EntityId == _loadingMapEntityId)
|
||||
{
|
||||
_loadingMapEntityId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f43fd8050fd4239b4407a4fedcf0b7a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,465 @@
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GameFramework.Event;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class EnemyManager
|
||||
{
|
||||
private sealed class SpawnEntryRuntime
|
||||
{
|
||||
public DRLevelSpawnEntry Entry;
|
||||
public bool Completed;
|
||||
public float NextTriggerTime;
|
||||
public float EndTime;
|
||||
public int RemainingCount;
|
||||
}
|
||||
|
||||
private const int DefaultEnemyConfigId = 1;
|
||||
private const float MinStreamInterval = 0.05f;
|
||||
private const float MinBurstGap = 0.01f;
|
||||
|
||||
private readonly List<Spawner> _spawners = new List<Spawner>();
|
||||
private readonly Dictionary<int, Spawner> _spawnerByOrder = new Dictionary<int, Spawner>();
|
||||
private readonly List<Vector3> _pathBuffer = new List<Vector3>();
|
||||
private readonly List<SpawnEntryRuntime> _spawnRuntimes = new List<SpawnEntryRuntime>();
|
||||
|
||||
private CombatScheduler _combatScheduler;
|
||||
private EntityComponent _entity;
|
||||
private IDataTable<DREnemy> _drEnemy;
|
||||
|
||||
private int _spawnEnemyMaxCount = 5000;
|
||||
private int _currentEnemyCount;
|
||||
private int _nextSpawnerIndex;
|
||||
private int _currentMapEntityId;
|
||||
private bool _initialized;
|
||||
private bool _enemyConfigMissingLogged;
|
||||
|
||||
private float _phaseElapsed;
|
||||
private bool _isPhaseRunning;
|
||||
|
||||
public int AliveEnemyCount => _currentEnemyCount;
|
||||
public bool IsPhaseSpawnCompleted { get; private set; } = true;
|
||||
public bool IsPhaseRunning => _isPhaseRunning;
|
||||
|
||||
public void OnInit(CombatScheduler combatScheduler)
|
||||
{
|
||||
_combatScheduler = combatScheduler;
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_entity = GameEntry.Entity;
|
||||
_drEnemy = GameEntry.DataTable.GetDataTable<DREnemy>();
|
||||
_currentEnemyCount = 0;
|
||||
_nextSpawnerIndex = 0;
|
||||
_currentMapEntityId = 0;
|
||||
_enemyConfigMissingLogged = false;
|
||||
_spawners.Clear();
|
||||
_spawnerByOrder.Clear();
|
||||
_pathBuffer.Clear();
|
||||
_spawnRuntimes.Clear();
|
||||
_phaseElapsed = 0f;
|
||||
_isPhaseRunning = false;
|
||||
IsPhaseSpawnCompleted = true;
|
||||
|
||||
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public void BeginPhase(DRLevelPhase phase, IReadOnlyList<DRLevelSpawnEntry> spawnEntries)
|
||||
{
|
||||
if (!_initialized || _combatScheduler == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = phase;
|
||||
EndPhase();
|
||||
|
||||
_phaseElapsed = 0f;
|
||||
_isPhaseRunning = true;
|
||||
IsPhaseSpawnCompleted = false;
|
||||
RefreshSpawnerCache(true);
|
||||
|
||||
if (spawnEntries != null)
|
||||
{
|
||||
for (int i = 0; i < spawnEntries.Count; i++)
|
||||
{
|
||||
SpawnEntryRuntime runtime = BuildSpawnRuntime(spawnEntries[i]);
|
||||
if (runtime != null)
|
||||
{
|
||||
_spawnRuntimes.Add(runtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IsPhaseSpawnCompleted = _spawnRuntimes.Count <= 0;
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (!_initialized || _combatScheduler == null || !_isPhaseRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshSpawnerCache(false);
|
||||
_phaseElapsed += elapseSeconds;
|
||||
UpdateSpawnRuntimes();
|
||||
}
|
||||
|
||||
public void EndPhase()
|
||||
{
|
||||
_isPhaseRunning = false;
|
||||
_phaseElapsed = 0f;
|
||||
_spawnRuntimes.Clear();
|
||||
IsPhaseSpawnCompleted = true;
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
_combatScheduler = null;
|
||||
return;
|
||||
}
|
||||
|
||||
EndPhase();
|
||||
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
|
||||
_spawners.Clear();
|
||||
_spawnerByOrder.Clear();
|
||||
_pathBuffer.Clear();
|
||||
_currentMapEntityId = 0;
|
||||
_nextSpawnerIndex = 0;
|
||||
_combatScheduler = null;
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
private SpawnEntryRuntime BuildSpawnRuntime(DRLevelSpawnEntry entry)
|
||||
{
|
||||
if (entry == null || entry.EntryType == EntryType.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
SpawnEntryRuntime runtime = new SpawnEntryRuntime
|
||||
{
|
||||
Entry = entry,
|
||||
Completed = false,
|
||||
NextTriggerTime = Mathf.Max(0f, entry.StartTime),
|
||||
EndTime = Mathf.Max(0f, entry.StartTime),
|
||||
RemainingCount = Mathf.Max(0, entry.Count)
|
||||
};
|
||||
|
||||
switch (entry.EntryType)
|
||||
{
|
||||
case EntryType.Stream:
|
||||
{
|
||||
float duration = Mathf.Max(0f, entry.Duration);
|
||||
runtime.EndTime = duration > 0f ? runtime.NextTriggerTime + duration : runtime.NextTriggerTime;
|
||||
runtime.Completed = entry.Count <= 0;
|
||||
return runtime;
|
||||
}
|
||||
case EntryType.Burst:
|
||||
case EntryType.Boss:
|
||||
runtime.Completed = runtime.RemainingCount <= 0;
|
||||
return runtime;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSpawnRuntimes()
|
||||
{
|
||||
bool allCompleted = true;
|
||||
for (int i = 0; i < _spawnRuntimes.Count; i++)
|
||||
{
|
||||
SpawnEntryRuntime runtime = _spawnRuntimes[i];
|
||||
if (runtime.Completed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (runtime.Entry.EntryType)
|
||||
{
|
||||
case EntryType.Stream:
|
||||
ProcessStreamRuntime(runtime);
|
||||
break;
|
||||
case EntryType.Burst:
|
||||
case EntryType.Boss:
|
||||
ProcessBurstRuntime(runtime);
|
||||
break;
|
||||
default:
|
||||
runtime.Completed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!runtime.Completed)
|
||||
{
|
||||
allCompleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
IsPhaseSpawnCompleted = allCompleted;
|
||||
}
|
||||
|
||||
private void ProcessStreamRuntime(SpawnEntryRuntime runtime)
|
||||
{
|
||||
if (_phaseElapsed < runtime.NextTriggerTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int countPerWave = Mathf.Max(0, runtime.Entry.Count);
|
||||
if (countPerWave <= 0)
|
||||
{
|
||||
runtime.Completed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
float interval = runtime.Entry.Interval > 0f ? runtime.Entry.Interval : MinStreamInterval;
|
||||
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.NextTriggerTime <= runtime.EndTime)
|
||||
{
|
||||
SpawnEnemies(runtime.Entry, countPerWave);
|
||||
runtime.NextTriggerTime += interval;
|
||||
}
|
||||
|
||||
if (runtime.NextTriggerTime > runtime.EndTime)
|
||||
{
|
||||
runtime.Completed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessBurstRuntime(SpawnEntryRuntime runtime)
|
||||
{
|
||||
if (_phaseElapsed < runtime.NextTriggerTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (runtime.RemainingCount <= 0)
|
||||
{
|
||||
runtime.Completed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
float gap = runtime.Entry.Gap;
|
||||
if (gap <= 0f)
|
||||
{
|
||||
SpawnEnemies(runtime.Entry, runtime.RemainingCount);
|
||||
runtime.RemainingCount = 0;
|
||||
runtime.Completed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
gap = Mathf.Max(gap, MinBurstGap);
|
||||
while (_phaseElapsed >= runtime.NextTriggerTime && runtime.RemainingCount > 0)
|
||||
{
|
||||
SpawnEnemies(runtime.Entry, 1);
|
||||
runtime.RemainingCount--;
|
||||
runtime.NextTriggerTime += gap;
|
||||
}
|
||||
|
||||
runtime.Completed = runtime.RemainingCount <= 0;
|
||||
}
|
||||
|
||||
private void SpawnEnemies(DRLevelSpawnEntry entry, int spawnCount)
|
||||
{
|
||||
if (spawnCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Spawner spawner = ResolveSpawner(entry.SpawnPointId);
|
||||
if (spawner == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MapEntity currentMap = _combatScheduler != null ? _combatScheduler.CurrentMap : null;
|
||||
if (currentMap == null ||
|
||||
!currentMap.TryFindPathWorldPoints(spawner, null, _pathBuffer) ||
|
||||
_pathBuffer.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DREnemy enemyConfig = GetEnemyConfig(entry.EnemyId);
|
||||
if (enemyConfig == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < spawnCount; i++)
|
||||
{
|
||||
if (_currentEnemyCount >= _spawnEnemyMaxCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int enemyEntityId = _entity.GenerateSerialId();
|
||||
EnemyData enemyData = new EnemyData(
|
||||
enemyEntityId,
|
||||
enemyConfig.EntityId,
|
||||
_pathBuffer[0],
|
||||
enemyConfig.BaseHp,
|
||||
enemyConfig.Speed,
|
||||
_pathBuffer);
|
||||
_entity.ShowEnemy(enemyData);
|
||||
}
|
||||
}
|
||||
|
||||
private DREnemy GetEnemyConfig(int enemyId)
|
||||
{
|
||||
if (_drEnemy == null)
|
||||
{
|
||||
_drEnemy = GameEntry.DataTable.GetDataTable<DREnemy>();
|
||||
if (_drEnemy == null)
|
||||
{
|
||||
if (!_enemyConfigMissingLogged)
|
||||
{
|
||||
Log.Warning("EnemyManagerComponent can not find DREnemy data table.");
|
||||
_enemyConfigMissingLogged = true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (enemyId > 0)
|
||||
{
|
||||
DREnemy targetConfig = _drEnemy.GetDataRow(enemyId);
|
||||
if (targetConfig != null)
|
||||
{
|
||||
return targetConfig;
|
||||
}
|
||||
}
|
||||
|
||||
DREnemy defaultConfig = _drEnemy.GetDataRow(DefaultEnemyConfigId);
|
||||
if (defaultConfig != null)
|
||||
{
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
DREnemy[] allConfigs = _drEnemy.GetAllDataRows();
|
||||
if (allConfigs.Length > 0)
|
||||
{
|
||||
return allConfigs[0];
|
||||
}
|
||||
|
||||
if (!_enemyConfigMissingLogged)
|
||||
{
|
||||
Log.Warning("EnemyManagerComponent found no enemy configs.");
|
||||
_enemyConfigMissingLogged = true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Spawner ResolveSpawner(int spawnPointId)
|
||||
{
|
||||
if (spawnPointId > 0 && _spawnerByOrder.TryGetValue(spawnPointId, out Spawner mappedSpawner))
|
||||
{
|
||||
return mappedSpawner;
|
||||
}
|
||||
|
||||
if (_spawners.Count <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Spawner fallbackSpawner = _spawners[_nextSpawnerIndex % _spawners.Count];
|
||||
_nextSpawnerIndex++;
|
||||
return fallbackSpawner;
|
||||
}
|
||||
|
||||
private void RefreshSpawnerCache(bool force)
|
||||
{
|
||||
MapEntity currentMap = _combatScheduler != null ? _combatScheduler.CurrentMap : null;
|
||||
if (currentMap == null)
|
||||
{
|
||||
_spawners.Clear();
|
||||
_spawnerByOrder.Clear();
|
||||
_currentMapEntityId = 0;
|
||||
_nextSpawnerIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force && _currentMapEntityId == currentMap.Id && _spawners.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_spawners.Clear();
|
||||
_spawnerByOrder.Clear();
|
||||
_nextSpawnerIndex = 0;
|
||||
_currentMapEntityId = currentMap.Id;
|
||||
|
||||
Spawner[] mapSpawners = currentMap.Spawners;
|
||||
for (int i = 0; i < mapSpawners.Length; i++)
|
||||
{
|
||||
Spawner spawner = mapSpawners[i];
|
||||
if (spawner == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!currentMap.TryGetDefaultPathCells(spawner, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_spawners.Add(spawner);
|
||||
if (spawner.SpawnOrder > 0 && !_spawnerByOrder.ContainsKey(spawner.SpawnOrder))
|
||||
{
|
||||
_spawnerByOrder[spawner.SpawnOrder] = spawner;
|
||||
}
|
||||
}
|
||||
|
||||
_spawners.Sort((left, right) => left.SpawnOrder.CompareTo(right.SpawnOrder));
|
||||
}
|
||||
|
||||
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is ShowEntitySuccessEventArgs ne)
|
||||
{
|
||||
if (ne.EntityLogicType == typeof(EnemyEntity))
|
||||
{
|
||||
_currentEnemyCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShowEntityFailure(object sender, GameEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
private void OnHideEntityComplete(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is HideEntityCompleteEventArgs ne))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ne.EntityGroup.Name != "Enemy")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using GameFramework.Event;
|
||||
using GeometryTD;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class EnemyManagerComponent : GameFrameworkComponent
|
||||
{
|
||||
private EntityComponent _entity;
|
||||
|
||||
private float _spawnEnemyInterval = 5f;
|
||||
private float _spawnEnemyTimer;
|
||||
private float _spawnEnemyAccelerate = 0.5f;
|
||||
private int _spawnEnemyCount = 5;
|
||||
private int _spawnEnemyMaxCount = 5000;
|
||||
private int _currentEnemyCount;
|
||||
private int _spawnDistanceFromPlayer = 10;
|
||||
private int _currentSpawnEnemyId = 0;
|
||||
|
||||
private Transform _player;
|
||||
|
||||
public void OnInit()
|
||||
{
|
||||
_entity = GameEntry.Entity;
|
||||
|
||||
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
}
|
||||
|
||||
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
_spawnEnemyTimer += elapseSeconds;
|
||||
if (_spawnEnemyTimer < _spawnEnemyInterval) return;
|
||||
SpawnEnemy();
|
||||
_spawnEnemyTimer = 0;
|
||||
_spawnEnemyInterval = Mathf.Max(0.1f, _spawnEnemyInterval - _spawnEnemyAccelerate);
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess);
|
||||
GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure);
|
||||
GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete);
|
||||
}
|
||||
|
||||
private void SpawnEnemy()
|
||||
{
|
||||
if (_player == null) return;
|
||||
for (int i = 0; i < _spawnEnemyCount; i++)
|
||||
{
|
||||
if (_currentEnemyCount >= _spawnEnemyMaxCount) break;
|
||||
_entity.ShowEnemy(new EnemyData(_currentSpawnEnemyId % _spawnEnemyMaxCount, 1001, _player,
|
||||
GetRandomPosition(), 10, 2));
|
||||
_currentSpawnEnemyId++;
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 GetRandomPosition()
|
||||
{
|
||||
float x = Random.Range(-1f, 1f);
|
||||
float z = Random.Range(-1f, 1f);
|
||||
Vector3 dir = new Vector3(x, 0, z).normalized;
|
||||
return _player.position + dir * _spawnDistanceFromPlayer;
|
||||
}
|
||||
|
||||
private void OnShowEntitySuccess(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is ShowEntitySuccessEventArgs ne)
|
||||
{
|
||||
if (ne.EntityLogicType == typeof(Enemy))
|
||||
{
|
||||
_currentEnemyCount++;
|
||||
}
|
||||
|
||||
if (ne.EntityLogicType == typeof(Player))
|
||||
{
|
||||
_player = ne.Entity.transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShowEntityFailure(object sender, GameEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
private void OnHideEntityComplete(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is HideEntityCompleteEventArgs ne)
|
||||
{
|
||||
if (ne.EntityGroup.Name == "Enemy")
|
||||
{
|
||||
_currentEnemyCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ namespace GeometryTD
|
||||
public const int BulletAsset = 80;
|
||||
public const int AsteroiAsset = 80;
|
||||
public const int EnemyAsset = 80;
|
||||
public const int MapAsset = 85;
|
||||
public const int EffectAsset = 80;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -14,6 +15,8 @@ namespace GeometryTD.Entity.EntityData
|
||||
[SerializeField] private float _speed = 0;
|
||||
|
||||
[SerializeField] private Transform _player = null;
|
||||
|
||||
[SerializeField] private List<Vector3> _pathPoints = new List<Vector3>();
|
||||
|
||||
public EnemyData(int entityId, int typeId, Transform player, Vector3 pos, int maxHp, float speed) : base(
|
||||
entityId, typeId)
|
||||
@@ -22,6 +25,17 @@ namespace GeometryTD.Entity.EntityData
|
||||
_speed = speed;
|
||||
Position = pos;
|
||||
_player = player;
|
||||
_pathPoints.Clear();
|
||||
}
|
||||
|
||||
public EnemyData(int entityId, int typeId, Vector3 pos, int maxHp, float speed,
|
||||
IReadOnlyList<Vector3> pathPoints) : base(entityId, typeId)
|
||||
{
|
||||
_maxHealth = maxHp;
|
||||
_speed = speed;
|
||||
Position = pos;
|
||||
_player = null;
|
||||
SetPathPoints(pathPoints);
|
||||
}
|
||||
|
||||
public CampType Camp
|
||||
@@ -47,5 +61,23 @@ namespace GeometryTD.Entity.EntityData
|
||||
get => _player;
|
||||
set => _player = value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<Vector3> PathPoints => _pathPoints;
|
||||
|
||||
public bool HasPath => _pathPoints.Count > 0;
|
||||
|
||||
public void SetPathPoints(IReadOnlyList<Vector3> pathPoints)
|
||||
{
|
||||
_pathPoints.Clear();
|
||||
if (pathPoints == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < pathPoints.Count; i++)
|
||||
{
|
||||
_pathPoints.Add(pathPoints[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,41 +13,37 @@ namespace GeometryTD.Entity.EntityData
|
||||
[Serializable]
|
||||
public abstract class EntityDataBase
|
||||
{
|
||||
[SerializeField]
|
||||
private int m_Id = 0;
|
||||
[SerializeField] private int _id = 0;
|
||||
|
||||
[SerializeField]
|
||||
private int m_TypeId = 0;
|
||||
[SerializeField] private int _typeId = 0;
|
||||
|
||||
[SerializeField]
|
||||
private Vector3 m_Position = Vector3.zero;
|
||||
[SerializeField] private Vector3 _position = Vector3.zero;
|
||||
|
||||
[SerializeField]
|
||||
private Quaternion m_Rotation = Quaternion.identity;
|
||||
[SerializeField] private Quaternion _rotation = Quaternion.identity;
|
||||
|
||||
public EntityDataBase(int entityId, int typeId)
|
||||
{
|
||||
m_Id = entityId;
|
||||
m_TypeId = typeId;
|
||||
_id = entityId;
|
||||
_typeId = typeId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实体编号。
|
||||
/// 生成的实体编号。
|
||||
/// </summary>
|
||||
public int Id => m_Id;
|
||||
public int Id => _id;
|
||||
|
||||
/// <summary>
|
||||
/// 实体类型编号。
|
||||
/// 实体类型编号(外键)。
|
||||
/// </summary>
|
||||
public int TypeId => m_TypeId;
|
||||
public int TypeId => _typeId;
|
||||
|
||||
/// <summary>
|
||||
/// 实体位置。
|
||||
/// </summary>
|
||||
public Vector3 Position
|
||||
{
|
||||
get => m_Position;
|
||||
set => m_Position = value;
|
||||
get => _position;
|
||||
set => _position = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,8 +51,8 @@ namespace GeometryTD.Entity.EntityData
|
||||
/// </summary>
|
||||
public Quaternion Rotation
|
||||
{
|
||||
get => m_Rotation;
|
||||
set => m_Rotation = value;
|
||||
get => _rotation;
|
||||
set => _rotation = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.Entity.EntityData
|
||||
{
|
||||
[Serializable]
|
||||
public class MapData : EntityDataBase
|
||||
{
|
||||
[SerializeField] private int _levelId = 0;
|
||||
|
||||
public MapData(int entityId, int levelId, Vector3 position) : this(entityId, 0, levelId, position)
|
||||
{
|
||||
}
|
||||
|
||||
public MapData(int entityId, int typeId, int levelId, Vector3 position) : base(entityId, typeId)
|
||||
{
|
||||
_levelId = levelId;
|
||||
Position = position;
|
||||
}
|
||||
|
||||
public int LevelId
|
||||
{
|
||||
get => _levelId;
|
||||
set => _levelId = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2635b0451d8c944ea0a5761ebc53d51
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -48,7 +48,25 @@ namespace GeometryTD
|
||||
|
||||
public static void ShowEnemy(this EntityComponent entityComponent, EnemyData data)
|
||||
{
|
||||
entityComponent.ShowEntity(typeof(Enemy), "Enemy", Constant.AssetPriority.EnemyAsset, data);
|
||||
entityComponent.ShowEntity(typeof(EnemyEntity), "Enemy", Constant.AssetPriority.EnemyAsset, data);
|
||||
}
|
||||
|
||||
public static void ShowMap(this EntityComponent entityComponent, MapData data)
|
||||
{
|
||||
ShowMap(entityComponent, data, null);
|
||||
}
|
||||
|
||||
public static void ShowMap(this EntityComponent entityComponent, MapData data, string mapAssetName)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
Log.Warning("Map data is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
string resolvedMapAssetName = string.IsNullOrEmpty(mapAssetName) ? data.LevelId.ToString() : mapAssetName;
|
||||
string mapAssetPath = AssetUtility.GetLevelMapAsset(resolvedMapAssetName);
|
||||
entityComponent.ShowEntity(data.Id, typeof(MapEntity), mapAssetPath, "Map", Constant.AssetPriority.MapAsset, data);
|
||||
}
|
||||
|
||||
private static void ShowEntity(this EntityComponent entityComponent, Type logicType, string entityGroup,
|
||||
@@ -77,4 +95,4 @@ namespace GeometryTD
|
||||
return --s_SerialId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
using Components;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.Entity
|
||||
{
|
||||
public class Enemy : EntityBase
|
||||
{
|
||||
private Transform _target;
|
||||
private float _speed;
|
||||
private MovementComponent _movementComponent;
|
||||
private float _attackRange = 1f;
|
||||
private float _attackRangeSquared;
|
||||
|
||||
protected override void OnInit(object userData)
|
||||
{
|
||||
base.OnInit(userData);
|
||||
|
||||
_movementComponent = GetComponent<MovementComponent>();
|
||||
}
|
||||
|
||||
protected override void OnShow(object userData)
|
||||
{
|
||||
base.OnShow(userData);
|
||||
|
||||
if (userData is EnemyData enemyData)
|
||||
{
|
||||
_speed = enemyData.Speed;
|
||||
_target = enemyData.Player;
|
||||
}
|
||||
|
||||
_movementComponent.OnInit(_speed, this.CachedTransform);
|
||||
_movementComponent.SetMove(true);
|
||||
_attackRangeSquared = _attackRange * _attackRange;
|
||||
}
|
||||
|
||||
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
base.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
|
||||
float distanceSquared = (this.CachedTransform.position - _target.position).sqrMagnitude;
|
||||
if (distanceSquared < _attackRangeSquared)
|
||||
{
|
||||
// 攻击
|
||||
_movementComponent.SetMove(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_movementComponent.SetMove(true);
|
||||
_movementComponent.SetDirection(GetTargetDirection());
|
||||
}
|
||||
|
||||
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
}
|
||||
|
||||
protected override void OnHide(bool isShutdown, object userData)
|
||||
{
|
||||
_movementComponent.SetMove(false);
|
||||
|
||||
base.OnHide(isShutdown, userData);
|
||||
}
|
||||
|
||||
private Vector3 GetTargetDirection()
|
||||
{
|
||||
return new Vector3(_target.position.x - this.CachedTransform.position.x, 0f,
|
||||
_target.position.z - this.CachedTransform.position.z).normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 112f2a17ab82438484782c9a763fdee1
|
||||
timeCreated: 1768892870
|
||||
@@ -0,0 +1,134 @@
|
||||
using Components;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.Entity
|
||||
{
|
||||
public class EnemyEntity : EntityBase
|
||||
{
|
||||
private const float WaypointReachDistance = 0.05f;
|
||||
|
||||
private Transform _target;
|
||||
private float _speed;
|
||||
private MovementComponent _movementComponent;
|
||||
private readonly List<Vector3> _pathPoints = new List<Vector3>();
|
||||
private int _pathPointIndex;
|
||||
private bool _isDespawnRequested;
|
||||
|
||||
protected override void OnInit(object userData)
|
||||
{
|
||||
base.OnInit(userData);
|
||||
|
||||
_movementComponent = GetComponent<MovementComponent>();
|
||||
}
|
||||
|
||||
protected override void OnShow(object userData)
|
||||
{
|
||||
base.OnShow(userData);
|
||||
|
||||
_target = null;
|
||||
_pathPoints.Clear();
|
||||
_pathPointIndex = 0;
|
||||
_isDespawnRequested = false;
|
||||
if (userData is EnemyData enemyData)
|
||||
{
|
||||
_speed = enemyData.Speed;
|
||||
_target = enemyData.Player;
|
||||
if (enemyData.HasPath)
|
||||
{
|
||||
IReadOnlyList<Vector3> pathPoints = enemyData.PathPoints;
|
||||
for (int i = 0; i < pathPoints.Count; i++)
|
||||
{
|
||||
_pathPoints.Add(pathPoints[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_movementComponent.OnInit(_speed, CachedTransform);
|
||||
_movementComponent.SetMove(true);
|
||||
}
|
||||
|
||||
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
base.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
|
||||
if (_pathPoints.Count > 0)
|
||||
{
|
||||
UpdatePathMovement(elapseSeconds, realElapseSeconds);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnHide(bool isShutdown, object userData)
|
||||
{
|
||||
_movementComponent.SetMove(false);
|
||||
_pathPoints.Clear();
|
||||
_pathPointIndex = 0;
|
||||
_isDespawnRequested = false;
|
||||
|
||||
base.OnHide(isShutdown, userData);
|
||||
}
|
||||
|
||||
private void UpdatePathMovement(float elapseSeconds, float realElapseSeconds)
|
||||
{
|
||||
if (_isDespawnRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pathPointIndex >= _pathPoints.Count)
|
||||
{
|
||||
DespawnOnReachHouse();
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 direction = GetDirectionToPathPoint(_pathPoints[_pathPointIndex], out float distanceSquared);
|
||||
if (distanceSquared <= WaypointReachDistance * WaypointReachDistance)
|
||||
{
|
||||
_pathPointIndex++;
|
||||
if (_pathPointIndex >= _pathPoints.Count)
|
||||
{
|
||||
DespawnOnReachHouse();
|
||||
return;
|
||||
}
|
||||
|
||||
direction = GetDirectionToPathPoint(_pathPoints[_pathPointIndex], out _);
|
||||
}
|
||||
|
||||
if (direction.sqrMagnitude <= Mathf.Epsilon)
|
||||
{
|
||||
_movementComponent.SetMove(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_movementComponent.SetMove(true);
|
||||
_movementComponent.SetDirection(direction);
|
||||
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
}
|
||||
|
||||
private Vector3 GetDirectionToPathPoint(Vector3 worldPoint, out float distanceSquared)
|
||||
{
|
||||
Vector3 delta = worldPoint - CachedTransform.position;
|
||||
distanceSquared = delta.sqrMagnitude;
|
||||
if (distanceSquared <= Mathf.Epsilon)
|
||||
{
|
||||
return Vector3.zero;
|
||||
}
|
||||
|
||||
return delta.normalized;
|
||||
}
|
||||
|
||||
private void DespawnOnReachHouse()
|
||||
{
|
||||
if (_isDespawnRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDespawnRequested = true;
|
||||
_movementComponent.SetMove(false);
|
||||
GameEntry.Entity.HideEntity(Entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec17059949d1140428bd47809397a01f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,300 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using GeometryTD.Pathfinding;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Tilemaps;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.Entity
|
||||
{
|
||||
public class MapEntity : EntityBase
|
||||
{
|
||||
private const string PathTileName = "Path";
|
||||
private const string FoundationTileName = "Foundation";
|
||||
private static readonly Spawner[] EmptySpawners = Array.Empty<Spawner>();
|
||||
|
||||
private readonly List<Vector3Int> _pathCells = new();
|
||||
private readonly List<Vector3Int> _foundationCells = new();
|
||||
private readonly HashSet<Vector3Int> _pathCellSet = new();
|
||||
private readonly HashSet<Vector3Int> _foundationCellSet = new();
|
||||
private readonly IMapPathfinder _mapPathfinder = new GridMapPathfinder();
|
||||
private readonly List<Vector3Int> _pathCellBuffer = new();
|
||||
private readonly Dictionary<Spawner, Vector3Int> _spawnerPathStartByRef = new();
|
||||
private readonly Dictionary<Spawner, List<Vector3Int>> _defaultPathCellsBySpawner = new();
|
||||
|
||||
private MapDataRefs _mapDataRefs;
|
||||
private MapData _mapData;
|
||||
private bool _hasHousePathCell;
|
||||
private Vector3Int _housePathCell;
|
||||
|
||||
public IReadOnlyList<Vector3Int> PathCells => _pathCells;
|
||||
public IReadOnlyList<Vector3Int> FoundationCells => _foundationCells;
|
||||
public Tilemap Tilemap => _mapDataRefs != null ? _mapDataRefs.Tilemap : null;
|
||||
public Spawner[] Spawners => _mapDataRefs?.Spawners ?? EmptySpawners;
|
||||
public House House => _mapDataRefs?.House;
|
||||
|
||||
public bool IsPathCell(Vector3Int cellPosition)
|
||||
{
|
||||
return _pathCellSet.Contains(cellPosition);
|
||||
}
|
||||
|
||||
public bool IsFoundationCell(Vector3Int cellPosition)
|
||||
{
|
||||
return _foundationCellSet.Contains(cellPosition);
|
||||
}
|
||||
|
||||
public bool TryGetNearestPathCell(Vector3 worldPosition, out Vector3Int pathCell)
|
||||
{
|
||||
pathCell = default;
|
||||
if (_pathCells.Count <= 0 || Tilemap == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector3Int directCell = Tilemap.WorldToCell(worldPosition);
|
||||
if (_pathCellSet.Contains(directCell))
|
||||
{
|
||||
pathCell = directCell;
|
||||
return true;
|
||||
}
|
||||
|
||||
float minDistance = float.MaxValue;
|
||||
for (int i = 0; i < _pathCells.Count; i++)
|
||||
{
|
||||
Vector3Int candidate = _pathCells[i];
|
||||
float distance = (Tilemap.GetCellCenterWorld(candidate) - worldPosition).sqrMagnitude;
|
||||
if (distance >= minDistance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
minDistance = distance;
|
||||
pathCell = candidate;
|
||||
}
|
||||
|
||||
return minDistance < float.MaxValue;
|
||||
}
|
||||
|
||||
public Vector3 GetPathCellCenterWorld(Vector3Int pathCell)
|
||||
{
|
||||
return Tilemap != null ? Tilemap.GetCellCenterWorld(pathCell) : Vector3.zero;
|
||||
}
|
||||
|
||||
public bool TryGetDefaultPathCells(Spawner spawner, out IReadOnlyList<Vector3Int> pathCells)
|
||||
{
|
||||
pathCells = null;
|
||||
if (spawner == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_defaultPathCellsBySpawner.TryGetValue(spawner, out List<Vector3Int> cachedPathCells))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pathCells = cachedPathCells;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryFindPathCells(Spawner spawner, IReadOnlyCollection<Vector3Int> blockedCells,
|
||||
List<Vector3Int> pathResult)
|
||||
{
|
||||
if (pathResult == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pathResult.Clear();
|
||||
if (spawner == null || !_hasHousePathCell)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_spawnerPathStartByRef.TryGetValue(spawner, out Vector3Int startCell))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _mapPathfinder.TryFindPath(_pathCells, startCell, _housePathCell, blockedCells, pathResult);
|
||||
}
|
||||
|
||||
public bool TryFindPathWorldPoints(Spawner spawner, IReadOnlyCollection<Vector3Int> blockedCells,
|
||||
List<Vector3> worldPathResult)
|
||||
{
|
||||
if (worldPathResult == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
worldPathResult.Clear();
|
||||
if (Tilemap == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryFindPathCells(spawner, blockedCells, _pathCellBuffer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var pos in _pathCellBuffer)
|
||||
{
|
||||
worldPathResult.Add(GetPathCellCenterWorld(pos));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnInit(object userData)
|
||||
{
|
||||
base.OnInit(userData);
|
||||
|
||||
_mapDataRefs = GetComponent<MapDataRefs>();
|
||||
if (_mapDataRefs == null)
|
||||
{
|
||||
Log.Error("MapDataRefs is missing on map entity '{0}'.", name);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnShow(object userData)
|
||||
{
|
||||
base.OnShow(userData);
|
||||
|
||||
_mapData = userData as MapData;
|
||||
if (_mapData == null)
|
||||
{
|
||||
Log.Warning("MapData is invalid for map entity '{0}'.", Id);
|
||||
}
|
||||
|
||||
RefreshTiles();
|
||||
}
|
||||
|
||||
protected override void OnHide(bool isShutdown, object userData)
|
||||
{
|
||||
ClearRuntimeData();
|
||||
base.OnHide(isShutdown, userData);
|
||||
}
|
||||
|
||||
private void RefreshTiles()
|
||||
{
|
||||
ClearRuntimeData();
|
||||
|
||||
if (_mapDataRefs == null)
|
||||
{
|
||||
_mapDataRefs = GetComponent<MapDataRefs>();
|
||||
if (_mapDataRefs == null)
|
||||
{
|
||||
Log.Error("MapDataRefs is missing on map entity '{0}'.", name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Tilemap tilemap = _mapDataRefs.Tilemap;
|
||||
if (tilemap == null)
|
||||
{
|
||||
Log.Error("Tilemap reference is missing in MapDataRefs on '{0}'.", name);
|
||||
return;
|
||||
}
|
||||
|
||||
BoundsInt bounds = tilemap.cellBounds;
|
||||
foreach (Vector3Int cellPosition in bounds.allPositionsWithin)
|
||||
{
|
||||
TileBase tile = tilemap.GetTile(cellPosition);
|
||||
if (tile == null || string.IsNullOrEmpty(tile.name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(tile.name, PathTileName, StringComparison.Ordinal))
|
||||
{
|
||||
_pathCells.Add(cellPosition);
|
||||
_pathCellSet.Add(cellPosition);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(tile.name, FoundationTileName, StringComparison.Ordinal))
|
||||
{
|
||||
_foundationCells.Add(cellPosition);
|
||||
_foundationCellSet.Add(cellPosition);
|
||||
}
|
||||
}
|
||||
|
||||
RefreshPathCache();
|
||||
Log.Info(
|
||||
"Map '{0}' initialized. LevelId={1}, PathCells={2}, FoundationCells={3}, Spawners={4}, House={5}, Routes={6}.",
|
||||
name,
|
||||
_mapData != null ? _mapData.LevelId : 0,
|
||||
_pathCells.Count,
|
||||
_foundationCells.Count,
|
||||
Spawners.Length,
|
||||
House != null ? House.name : "None",
|
||||
_defaultPathCellsBySpawner.Count);
|
||||
}
|
||||
|
||||
private void RefreshPathCache()
|
||||
{
|
||||
_hasHousePathCell = false;
|
||||
_housePathCell = default;
|
||||
_spawnerPathStartByRef.Clear();
|
||||
_defaultPathCellsBySpawner.Clear();
|
||||
|
||||
if (House == null)
|
||||
{
|
||||
Log.Warning("Map '{0}' has no house reference, path cache skipped.", name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryGetNearestPathCell(House.Position, out _housePathCell))
|
||||
{
|
||||
Log.Warning("Map '{0}' house position can not map to a valid path cell.", name);
|
||||
return;
|
||||
}
|
||||
|
||||
_hasHousePathCell = true;
|
||||
Spawner[] spawners = Spawners;
|
||||
foreach (var spawner in spawners)
|
||||
{
|
||||
if (spawner == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryGetNearestPathCell(spawner.Position, out Vector3Int startCell))
|
||||
{
|
||||
Log.Warning("Map '{0}' spawner '{1}' can not map to a valid path cell.", name, spawner.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
_spawnerPathStartByRef[spawner] = startCell;
|
||||
|
||||
List<Vector3Int> defaultPathCells = new List<Vector3Int>();
|
||||
if (!_mapPathfinder.TryFindPath(_pathCells, startCell, _housePathCell, null, defaultPathCells))
|
||||
{
|
||||
Log.Warning(
|
||||
"Map '{0}' spawner '{1}' has no path to house cell {2}.",
|
||||
name,
|
||||
spawner.name,
|
||||
_housePathCell);
|
||||
continue;
|
||||
}
|
||||
|
||||
_defaultPathCellsBySpawner[spawner] = defaultPathCells;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearRuntimeData()
|
||||
{
|
||||
_pathCells.Clear();
|
||||
_foundationCells.Clear();
|
||||
_pathCellSet.Clear();
|
||||
_foundationCellSet.Clear();
|
||||
_pathCellBuffer.Clear();
|
||||
_hasHousePathCell = false;
|
||||
_housePathCell = default;
|
||||
_spawnerPathStartByRef.Clear();
|
||||
_defaultPathCellsBySpawner.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d45242aa35912c46a0260f67e3b2ae0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,7 +1,6 @@
|
||||
using GameFramework.Fsm;
|
||||
using GameFramework.Procedure;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.Procedure
|
||||
@@ -10,8 +9,6 @@ namespace GeometryTD.Procedure
|
||||
{
|
||||
public override bool UseNativeDialog => false;
|
||||
|
||||
private RepoFormUseCase _repoFormUseCase;
|
||||
|
||||
public bool GameStart { get; set; }
|
||||
|
||||
#region FSM
|
||||
@@ -28,12 +25,13 @@ namespace GeometryTD.Procedure
|
||||
GameStart = false;
|
||||
GameEntry.EventNode.OnInit();
|
||||
GameEntry.CombatNode.OnInit(LevelThemeType.Plain);
|
||||
GameEntry.CombatNode.StartCombat();
|
||||
|
||||
//GameEntry.EventNode.StartEvent();
|
||||
|
||||
_repoFormUseCase = new RepoFormUseCase();
|
||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase);
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.RepoForm);
|
||||
// _repoFormUseCase = new RepoFormUseCase();
|
||||
// GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase);
|
||||
// GameEntry.UIRouter.OpenUI(UIFormType.RepoForm);
|
||||
}
|
||||
|
||||
protected override void OnUpdate(IFsm<IProcedureManager> procedureOwner, float elapseSeconds,
|
||||
@@ -45,14 +43,18 @@ namespace GeometryTD.Procedure
|
||||
{
|
||||
procedureOwner.SetData<VarInt32>("NextSceneId", (int)SceneType.Main);
|
||||
ChangeState<ProcedureChangeScene>(procedureOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.CombatNode.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
}
|
||||
|
||||
protected override void OnLeave(IFsm<IProcedureManager> procedureOwner, bool isShutdown)
|
||||
{
|
||||
GameEntry.CombatNode.OnShutdown();
|
||||
base.OnLeave(procedureOwner, isShutdown);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
//------------------------------------------------------------
|
||||
// Game Framework
|
||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
||||
// Homepage: https://gameframework.cn/
|
||||
// Feedback: mailto:ellan@gameframework.cn
|
||||
//------------------------------------------------------------
|
||||
|
||||
using GameFramework;
|
||||
using GameFramework;
|
||||
using GameFramework.Event;
|
||||
using GameFramework.Resource;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
@@ -43,7 +35,7 @@ namespace GeometryTD.Procedure
|
||||
"UISound",
|
||||
};
|
||||
|
||||
private Dictionary<string, bool> _loadedFlag = new Dictionary<string, bool>();
|
||||
private readonly Dictionary<string, bool> _loadedFlag = new();
|
||||
|
||||
public override bool UseNativeDialog => true;
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class House : MonoBehaviour
|
||||
{
|
||||
public Vector3 Position => transform.position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1a168ec7442cbb4ebe72bd5a8140a9c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,37 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Tilemaps;
|
||||
|
||||
namespace GeometryTD
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class MapDataRefs : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Tilemap _tilemap = null;
|
||||
[SerializeField] private Spawner[] _spawners = null;
|
||||
[SerializeField] private House _house = null;
|
||||
|
||||
public Tilemap Tilemap => _tilemap;
|
||||
public Spawner[] Spawners => _spawners;
|
||||
public House House => _house;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
if (_tilemap == null)
|
||||
{
|
||||
_tilemap = GetComponentInChildren<Tilemap>(true);
|
||||
}
|
||||
|
||||
if (_spawners == null || _spawners.Length == 0)
|
||||
{
|
||||
_spawners = GetComponentsInChildren<Spawner>(true);
|
||||
}
|
||||
|
||||
if (_house == null)
|
||||
{
|
||||
_house = GetComponentInChildren<House>(true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34140eda5b81ee44dafb450b3d53d9c3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3c0f83d0aecadf4d82856eea8b5260c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,134 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.Pathfinding
|
||||
{
|
||||
public class GridMapPathfinder : IMapPathfinder
|
||||
{
|
||||
private static readonly Vector3Int[] NeighborOffsets =
|
||||
{
|
||||
new Vector3Int(1, 0, 0),
|
||||
new Vector3Int(-1, 0, 0),
|
||||
new Vector3Int(0, 1, 0),
|
||||
new Vector3Int(0, -1, 0)
|
||||
};
|
||||
|
||||
private readonly HashSet<Vector3Int> _walkableSet = new HashSet<Vector3Int>();
|
||||
private readonly Queue<Vector3Int> _openQueue = new Queue<Vector3Int>();
|
||||
private readonly Dictionary<Vector3Int, Vector3Int> _cameFrom = new Dictionary<Vector3Int, Vector3Int>();
|
||||
|
||||
public bool TryFindPath(
|
||||
IReadOnlyCollection<Vector3Int> walkableCells,
|
||||
Vector3Int startCell,
|
||||
Vector3Int destinationCell,
|
||||
IReadOnlyCollection<Vector3Int> blockedCells,
|
||||
List<Vector3Int> pathResult)
|
||||
{
|
||||
if (pathResult == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pathResult.Clear();
|
||||
if (walkableCells == null || walkableCells.Count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BuildWalkableSet(walkableCells);
|
||||
if (!_walkableSet.Contains(startCell) || !_walkableSet.Contains(destinationCell))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_openQueue.Clear();
|
||||
_cameFrom.Clear();
|
||||
_openQueue.Enqueue(startCell);
|
||||
_cameFrom[startCell] = startCell;
|
||||
|
||||
while (_openQueue.Count > 0)
|
||||
{
|
||||
Vector3Int currentCell = _openQueue.Dequeue();
|
||||
if (currentCell == destinationCell)
|
||||
{
|
||||
BuildPath(destinationCell, pathResult);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < NeighborOffsets.Length; i++)
|
||||
{
|
||||
Vector3Int nextCell = currentCell + NeighborOffsets[i];
|
||||
if (!_walkableSet.Contains(nextCell) || _cameFrom.ContainsKey(nextCell))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsBlocked(nextCell, blockedCells, startCell, destinationCell))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_cameFrom[nextCell] = currentCell;
|
||||
_openQueue.Enqueue(nextCell);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void BuildWalkableSet(IReadOnlyCollection<Vector3Int> walkableCells)
|
||||
{
|
||||
_walkableSet.Clear();
|
||||
foreach (Vector3Int cell in walkableCells)
|
||||
{
|
||||
_walkableSet.Add(cell);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildPath(Vector3Int destinationCell, List<Vector3Int> pathResult)
|
||||
{
|
||||
pathResult.Clear();
|
||||
Vector3Int current = destinationCell;
|
||||
while (true)
|
||||
{
|
||||
pathResult.Add(current);
|
||||
Vector3Int parent = _cameFrom[current];
|
||||
if (parent == current)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
current = parent;
|
||||
}
|
||||
|
||||
pathResult.Reverse();
|
||||
}
|
||||
|
||||
private static bool IsBlocked(
|
||||
Vector3Int cell,
|
||||
IReadOnlyCollection<Vector3Int> blockedCells,
|
||||
Vector3Int startCell,
|
||||
Vector3Int destinationCell)
|
||||
{
|
||||
if (blockedCells == null || blockedCells.Count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cell == startCell || cell == destinationCell)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (Vector3Int blockedCell in blockedCells)
|
||||
{
|
||||
if (blockedCell == cell)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a92a95ffefd4a6082255420a184ee34
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.Pathfinding
|
||||
{
|
||||
public interface IMapPathfinder
|
||||
{
|
||||
bool TryFindPath(
|
||||
IReadOnlyCollection<Vector3Int> walkableCells,
|
||||
Vector3Int startCell,
|
||||
Vector3Int destinationCell,
|
||||
IReadOnlyCollection<Vector3Int> blockedCells,
|
||||
List<Vector3Int> pathResult);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5540589b31b39ee4db4e77033a511a0c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class Spawner : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private int _spawnOrder = 0;
|
||||
|
||||
public int SpawnOrder => _spawnOrder;
|
||||
|
||||
public Vector3 Position => transform.position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 444d6b38c3d704a4b9c4692ff53e76b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -69,7 +69,7 @@ namespace GeometryTD.CustomUtility
|
||||
|
||||
public static string GetLevelMapAsset(string assetName)
|
||||
{
|
||||
return Utility.Text.Format("Assets/GameMain/LevelMap/Level{0}.prefab", assetName);
|
||||
return Utility.Text.Format("Assets/GameMain/Entities/LevelMap/Level{0}.prefab", assetName);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user