拆分 MapEntity 职责

- TowerSelectionPresenter:选中态 + 攻击范围展示
- CombatSelectInputService:点击事件逻辑(位置计算、点击对象识别、组装 CombatSelectFormUserData)
- TowerPlacementService:建造/升级/销毁与塔映射管理
- MapTopologyService:维护地图拓扑结构与寻路缓存
This commit is contained in:
SepComet
2026-03-02 11:04:33 +08:00
parent 344191a91c
commit 5c7501d4fb
23 changed files with 1105 additions and 702 deletions
@@ -0,0 +1,10 @@
using UnityEngine;
namespace GeometryTD.Map
{
[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.Map
{
[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,255 @@
using System;
using System.Collections.Generic;
using GeometryTD.Pathfinding;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityGameFramework.Runtime;
namespace GeometryTD.Map
{
public sealed class MapTopologyService
{
private const string PathTileName = "Path";
private const string FoundationTileName = "Foundation";
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 bool _hasHousePathCell;
private Vector3Int _housePathCell;
public IReadOnlyList<Vector3Int> PathCells => _pathCells;
public IReadOnlyList<Vector3Int> FoundationCells => _foundationCells;
public void Refresh(Tilemap tilemap, Spawner[] spawners, House house, string mapName, int levelId)
{
Clear();
if (tilemap == null)
{
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(tilemap, spawners, house, mapName);
Log.Info(
"Map '{0}' initialized. LevelId={1}, PathCells={2}, FoundationCells={3}, Spawners={4}, House={5}, Routes={6}.",
mapName,
levelId,
_pathCells.Count,
_foundationCells.Count,
spawners != null ? spawners.Length : 0,
house != null ? house.name : "None",
_defaultPathCellsBySpawner.Count);
}
public void Clear()
{
_pathCells.Clear();
_foundationCells.Clear();
_pathCellSet.Clear();
_foundationCellSet.Clear();
_pathCellBuffer.Clear();
_hasHousePathCell = false;
_housePathCell = default;
_spawnerPathStartByRef.Clear();
_defaultPathCellsBySpawner.Clear();
}
public bool IsPathCell(Vector3Int cellPosition)
{
return _pathCellSet.Contains(cellPosition);
}
public bool IsFoundationCell(Vector3Int cellPosition)
{
return _foundationCellSet.Contains(cellPosition);
}
public bool TryGetNearestPathCell(Tilemap tilemap, 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(Tilemap tilemap, 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(Tilemap tilemap, 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 (Vector3Int pos in _pathCellBuffer)
{
worldPathResult.Add(GetPathCellCenterWorld(tilemap, pos));
}
return true;
}
private void RefreshPathCache(Tilemap tilemap, Spawner[] spawners, House house, string mapName)
{
_hasHousePathCell = false;
_housePathCell = default;
_spawnerPathStartByRef.Clear();
_defaultPathCellsBySpawner.Clear();
if (house == null)
{
Log.Warning("Map '{0}' has no house reference, path cache skipped.", mapName);
return;
}
if (!TryGetNearestPathCell(tilemap, house.Position, out _housePathCell))
{
Log.Warning("Map '{0}' house position can not map to a valid path cell.", mapName);
return;
}
_hasHousePathCell = true;
if (spawners == null)
{
return;
}
foreach (Spawner spawner in spawners)
{
if (spawner == null)
{
continue;
}
if (!TryGetNearestPathCell(tilemap, spawner.Position, out Vector3Int startCell))
{
Log.Warning("Map '{0}' spawner '{1}' can not map to a valid path cell.", mapName, spawner.name);
continue;
}
_spawnerPathStartByRef[spawner] = startCell;
List<Vector3Int> defaultPathCells = new();
if (!_mapPathfinder.TryFindPath(_pathCells, startCell, _housePathCell, null, defaultPathCells))
{
Log.Warning(
"Map '{0}' spawner '{1}' has no path to house cell {2}.",
mapName,
spawner.name,
_housePathCell);
continue;
}
_defaultPathCellsBySpawner[spawner] = defaultPathCells;
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ebb51464c2374165ada752b52283f3ae
timeCreated: 1772419747
@@ -0,0 +1,14 @@
using UnityEngine;
namespace GeometryTD.Map
{
[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:
@@ -0,0 +1,300 @@
using System;
using System.Collections.Generic;
using GeometryTD.Definition;
using GeometryTD.Entity.EntityData;
using GeometryTD.Pathfinding;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityGameFramework.Runtime;
namespace GeometryTD.Map
{
public sealed class TowerPlacementService
{
private const int DefaultTowerTypeId = 401;
private readonly Dictionary<Vector3Int, int> _towerEntityIdByFoundationCell = new();
private readonly Dictionary<int, Vector3Int> _foundationCellByTowerEntityId = new();
private readonly Dictionary<int, DefenseTowerStatsData> _towerStatsByEntityId = new();
private readonly List<int> _towerEntityIdBuffer = new();
public IReadOnlyDictionary<Vector3Int, int> TowerEntityIdByFoundationCell => _towerEntityIdByFoundationCell;
public IReadOnlyDictionary<int, Vector3Int> FoundationCellByTowerEntityId => _foundationCellByTowerEntityId;
public bool TryBuildTower(Vector3Int foundationCell, Func<Vector3Int, bool> isFoundationCell, int buildIndex,
int[] buildTowerCosts, int towerTypeId, Tilemap tilemap, Func<int, bool> tryConsumeCoin,
Action<int> addCoin,
out int towerEntityId)
{
towerEntityId = 0;
if (isFoundationCell == null || !isFoundationCell(foundationCell))
{
return false;
}
if (_towerEntityIdByFoundationCell.ContainsKey(foundationCell))
{
return false;
}
int buildCost = GetBuildTowerCost(buildTowerCosts, buildIndex);
if (tryConsumeCoin != null && !tryConsumeCoin(buildCost))
{
return false;
}
DefenseTowerStatsData towerStats = BuildTowerStats(buildIndex);
if (!TryShowTowerEntity(foundationCell, towerStats, towerTypeId, tilemap, out int newTowerEntityId))
{
addCoin?.Invoke(buildCost);
return false;
}
_towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId;
_foundationCellByTowerEntityId[newTowerEntityId] = foundationCell;
_towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(towerStats);
towerEntityId = newTowerEntityId;
return true;
}
public bool TryUpgradeTower(int towerEntityId, int upgradeCost, int towerTypeId, Tilemap tilemap,
Func<int, bool> tryConsumeCoin, Action<int> addCoin, out int resultTowerEntityId,
out Vector3Int foundationCell)
{
resultTowerEntityId = 0;
foundationCell = default;
if (towerEntityId == 0 || !_foundationCellByTowerEntityId.TryGetValue(towerEntityId, out foundationCell))
{
return false;
}
int requiredUpgradeCost = Mathf.Max(0, upgradeCost);
if (tryConsumeCoin != null && !tryConsumeCoin(requiredUpgradeCost))
{
return false;
}
DefenseTowerStatsData oldStats =
_towerStatsByEntityId.TryGetValue(towerEntityId, out DefenseTowerStatsData cachedStats)
? CloneTowerStats(cachedStats)
: BuildTowerStats(0);
DefenseTowerStatsData upgradedStats = CloneTowerStats(oldStats);
ApplyUpgradeToStats(upgradedStats);
HideTowerEntity(towerEntityId);
_towerEntityIdByFoundationCell.Remove(foundationCell);
_foundationCellByTowerEntityId.Remove(towerEntityId);
_towerStatsByEntityId.Remove(towerEntityId);
if (!TryShowTowerEntity(foundationCell, upgradedStats, towerTypeId, tilemap, out int newTowerEntityId))
{
if (TryShowTowerEntity(foundationCell, oldStats, towerTypeId, tilemap, out int fallbackTowerEntityId))
{
_towerEntityIdByFoundationCell[foundationCell] = fallbackTowerEntityId;
_foundationCellByTowerEntityId[fallbackTowerEntityId] = foundationCell;
_towerStatsByEntityId[fallbackTowerEntityId] = CloneTowerStats(oldStats);
resultTowerEntityId = fallbackTowerEntityId;
}
addCoin?.Invoke(requiredUpgradeCost);
return false;
}
_towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId;
_foundationCellByTowerEntityId[newTowerEntityId] = foundationCell;
_towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(upgradedStats);
resultTowerEntityId = newTowerEntityId;
return true;
}
public bool TryDestroyTower(int towerEntityId, int destroyGain, Action<int> addCoin,
out Vector3Int foundationCell)
{
foundationCell = default;
if (towerEntityId == 0 || !_foundationCellByTowerEntityId.TryGetValue(towerEntityId, out foundationCell))
{
return false;
}
HideTowerEntity(towerEntityId);
_towerEntityIdByFoundationCell.Remove(foundationCell);
_foundationCellByTowerEntityId.Remove(towerEntityId);
_towerStatsByEntityId.Remove(towerEntityId);
addCoin?.Invoke(Mathf.Max(0, destroyGain));
return true;
}
public void HideAndClearAllPlacedTowers()
{
_towerEntityIdBuffer.Clear();
foreach (KeyValuePair<int, Vector3Int> pair in _foundationCellByTowerEntityId)
{
_towerEntityIdBuffer.Add(pair.Key);
}
for (int i = 0; i < _towerEntityIdBuffer.Count; i++)
{
HideTowerEntity(_towerEntityIdBuffer[i]);
}
_towerEntityIdByFoundationCell.Clear();
_foundationCellByTowerEntityId.Clear();
_towerStatsByEntityId.Clear();
_towerEntityIdBuffer.Clear();
}
public void ClearTracking()
{
_towerEntityIdByFoundationCell.Clear();
_foundationCellByTowerEntityId.Clear();
_towerStatsByEntityId.Clear();
_towerEntityIdBuffer.Clear();
}
private static int GetBuildTowerCost(int[] buildTowerCosts, int buildIndex)
{
if (buildTowerCosts == null || buildIndex < 0 || buildIndex >= buildTowerCosts.Length)
{
return 0;
}
return Mathf.Max(0, buildTowerCosts[buildIndex]);
}
private static bool TryShowTowerEntity(Vector3Int foundationCell, DefenseTowerStatsData towerStats,
int towerTypeId,
Tilemap tilemap, out int towerEntityId)
{
towerEntityId = 0;
if (GameEntry.Entity == null)
{
return false;
}
int entityId = GameEntry.Entity.GenerateSerialId();
int typeId = towerTypeId > 0 ? towerTypeId : DefaultTowerTypeId;
Vector3 towerPosition = tilemap != null ? tilemap.GetCellCenterWorld(foundationCell) : foundationCell;
towerPosition.z = 0f;
var towerData = new DefenseTowerData(entityId, typeId, towerPosition, Quaternion.identity, towerStats);
GameEntry.Entity.ShowDefenseTower(towerData);
towerEntityId = entityId;
return true;
}
private static void HideTowerEntity(int towerEntityId)
{
if (towerEntityId == 0 || GameEntry.Entity == null)
{
return;
}
UnityGameFramework.Runtime.Entity towerEntity = GameEntry.Entity.GetEntity(towerEntityId);
if (towerEntity != null)
{
GameEntry.Entity.HideEntity(towerEntity);
}
}
private static DefenseTowerStatsData BuildTowerStats(int buildIndex)
{
switch (buildIndex)
{
case 0:
return new DefenseTowerStatsData
{
AttackDamage = 100,
DamageRandomRate = 0f,
RotateSpeed = 200f,
AttackRange = 4.5f,
AttackSpeed = 1.5f,
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Physics,
Tags = Array.Empty<TagType>()
};
case 1:
return new DefenseTowerStatsData
{
AttackDamage = 13,
DamageRandomRate = 0f,
RotateSpeed = 160f,
AttackRange = 5f,
AttackSpeed = 1.2f,
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Fire,
Tags = Array.Empty<TagType>()
};
case 2:
return new DefenseTowerStatsData
{
AttackDamage = 17,
DamageRandomRate = 0f,
RotateSpeed = 140f,
AttackRange = 5.5f,
AttackSpeed = 0.95f,
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Ice,
Tags = Array.Empty<TagType>()
};
case 3:
return new DefenseTowerStatsData
{
AttackDamage = 22,
DamageRandomRate = 0f,
RotateSpeed = 120f,
AttackRange = 6f,
AttackSpeed = 0.75f,
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Poison,
Tags = Array.Empty<TagType>()
};
default:
return new DefenseTowerStatsData
{
AttackDamage = 100,
DamageRandomRate = 0f,
RotateSpeed = 180f,
AttackRange = 5f,
AttackSpeed = 1f,
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Physics,
Tags = Array.Empty<TagType>()
};
}
}
private static DefenseTowerStatsData CloneTowerStats(DefenseTowerStatsData source)
{
if (source == null)
{
return BuildTowerStats(0);
}
TagType[] copiedTags = source.Tags != null ? (TagType[])source.Tags.Clone() : Array.Empty<TagType>();
return new DefenseTowerStatsData
{
AttackDamage = source.AttackDamage,
DamageRandomRate = source.DamageRandomRate,
RotateSpeed = source.RotateSpeed,
AttackRange = source.AttackRange,
AttackSpeed = source.AttackSpeed,
AttackMethodType = source.AttackMethodType,
AttackPropertyType = source.AttackPropertyType,
Tags = copiedTags
};
}
private static void ApplyUpgradeToStats(DefenseTowerStatsData stats)
{
if (stats == null)
{
return;
}
stats.AttackDamage = Mathf.Max(1, stats.AttackDamage + 3);
stats.AttackSpeed = Mathf.Max(0.01f, stats.AttackSpeed + 0.2f);
stats.AttackRange = Mathf.Max(0.1f, stats.AttackRange + 0.4f);
stats.RotateSpeed = Mathf.Max(1f, stats.RotateSpeed + 10f);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 482aab8887db4be481c207cb9e7449d0
timeCreated: 1772419375
@@ -0,0 +1,134 @@
using System.Collections.Generic;
using GeometryTD.Entity;
using GeometryTD.UI;
using UnityEngine;
namespace GeometryTD.Map
{
public sealed class TowerSelectionPresenter
{
private bool _hasSelectedFoundationCell;
private Vector3Int _selectedFoundationCell;
private int _selectedTowerEntityId;
private int _attackRangeVisibleTowerEntityId;
public void ApplySelectedObject(CombatSelectFormUserData userData)
{
if (userData == null)
{
ClearSelectedObject();
return;
}
switch (userData.ClickObjectType)
{
case CombatSelectClickObjectType.Foundation:
_hasSelectedFoundationCell = true;
_selectedFoundationCell = userData.CellPosition;
_selectedTowerEntityId = 0;
break;
case CombatSelectClickObjectType.Tower:
_hasSelectedFoundationCell = true;
_selectedFoundationCell = userData.CellPosition;
_selectedTowerEntityId = userData.TowerEntityId;
break;
default:
ClearSelectedObject();
return;
}
UpdateTowerAttackRangeDisplay(_selectedTowerEntityId);
}
public void SelectTower(Vector3Int foundationCell, int towerEntityId)
{
_hasSelectedFoundationCell = true;
_selectedFoundationCell = foundationCell;
_selectedTowerEntityId = towerEntityId;
UpdateTowerAttackRangeDisplay(_selectedTowerEntityId);
}
public bool TryGetSelectedFoundationCell(out Vector3Int foundationCell)
{
foundationCell = default;
if (!_hasSelectedFoundationCell)
{
return false;
}
foundationCell = _selectedFoundationCell;
return true;
}
public bool TryGetSelectedTower(IReadOnlyDictionary<int, Vector3Int> foundationCellByTowerEntityId,
out int towerEntityId, out Vector3Int foundationCell)
{
towerEntityId = 0;
foundationCell = default;
if (_selectedTowerEntityId == 0 || foundationCellByTowerEntityId == null)
{
return false;
}
if (!foundationCellByTowerEntityId.TryGetValue(_selectedTowerEntityId, out foundationCell))
{
return false;
}
towerEntityId = _selectedTowerEntityId;
return true;
}
public void ClearSelectedObject()
{
UpdateTowerAttackRangeDisplay(0);
_hasSelectedFoundationCell = false;
_selectedFoundationCell = default;
_selectedTowerEntityId = 0;
}
private void UpdateTowerAttackRangeDisplay(int towerEntityId)
{
if (_attackRangeVisibleTowerEntityId != 0 && _attackRangeVisibleTowerEntityId != towerEntityId)
{
SetTowerAttackRangeVisible(_attackRangeVisibleTowerEntityId, false);
_attackRangeVisibleTowerEntityId = 0;
}
if (towerEntityId == 0)
{
if (_attackRangeVisibleTowerEntityId != 0)
{
SetTowerAttackRangeVisible(_attackRangeVisibleTowerEntityId, false);
_attackRangeVisibleTowerEntityId = 0;
}
return;
}
if (SetTowerAttackRangeVisible(towerEntityId, true))
{
_attackRangeVisibleTowerEntityId = towerEntityId;
}
}
private static bool SetTowerAttackRangeVisible(int towerEntityId, bool visible)
{
if (towerEntityId == 0 || GameEntry.Entity == null)
{
return false;
}
EntityBase gameEntity = GameEntry.Entity.GetGameEntity(towerEntityId);
if (gameEntity is not DefenseTowerEntity towerEntity)
{
return false;
}
towerEntity.SetAttackRangeVisible(visible);
return true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 872f548158cd4474ea0cca599f5ae415
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: