using Data;
using Entity;
using Managers;
using UnityEngine;
using UnityEngine.Tilemaps;
// Keep this, as it's used for DefineManager.Instance and EntityManager.Instance
namespace Map
{
public class Landform : MonoBehaviour
{
// 基础地形瓦片地图
public Tilemap baseTilemap;
// 建筑瓦片地图
public Tilemap buildingTilemap;
// 植物瓦片地图
public Tilemap plantTilemap;
public Vector2Int entityMapShift;
// 维度信息
public Dimension dimension;
public int[,] hasEntity;
private void OnDestroy()
{
// Unsubscribe to prevent memory leaks or errors if EntityManager outlives Landform
if (EntityManager.Instance !=
null) // This check is allowed for OnDestroy as the singleton might be destroyed before this object.
EntityManager.Instance.OnCreateEntity -= OnHaveEntityCreate;
}
///
/// 初始化实体碰撞数组。
///
/// 数组的大小。
/// 实体地图偏移量。
public void InitEntityCollisionArray(Vector2Int size, Vector2Int shift)
{
hasEntity = new int[size.x, size.y];
entityMapShift = shift;
// Ensure we don't subscribe multiple times
EntityManager.Instance.OnCreateEntity -= OnHaveEntityCreate;
EntityManager.Instance.OnCreateEntity += OnHaveEntityCreate;
}
///
/// 获取当前地形所属的维度ID。
///
/// 维度ID字符串。
public string GetDimensionID()
{
if (dimension) return dimension.DimensionId;
Debug.LogWarning("Landform未分配Dimension对象,返回默认ID。");
return "dimension_not_set";
}
///
/// 获取基础瓦片地图的尺寸。
///
/// 瓦片地图的尺寸 (Vector3Int)。
public Vector3Int GetSize()
{
return baseTilemap.size;
}
///
/// 获取基础瓦片地图的原点。
///
/// 瓦片地图的原点 (Vector3Int)。
public Vector3Int GetOrigin()
{
return baseTilemap.origin;
}
///
/// 将格子坐标转换为世界坐标。
///
/// 格子坐标。
/// 对应的世界坐标。
public Vector3 GetWorldCoordinates(Vector3Int coord)
{
return baseTilemap.CellToWorld(coord);
}
///
/// 将世界坐标转换为格子坐标。
///
/// 世界坐标。
/// 对应的格子坐标。
public Vector3Int GetCellCoordinates(Vector3 worldPosition)
{
return baseTilemap.WorldToCell(worldPosition);
}
///
/// 清除所有瓦片地图上的所有瓦片。
///
public void Clear()
{
baseTilemap.ClearAllTiles();
buildingTilemap.ClearAllTiles();
plantTilemap.ClearAllTiles();
}
///
/// 私有辅助方法:安全地从指定Tilemap层获取TileDef。
///
/// 要查询的Tilemap。
/// 格子坐标。
/// 找到的TileDef,如果没有瓦片或定义则为null。
private TileDef GetTileDef(Tilemap tilemap, Vector3Int cellPosition)
{
var tile = tilemap.GetTile(cellPosition);
return tile ? DefineManager.Instance.FindDefine(tile.name) : null;
}
///
/// 私有辅助方法:从指定的Tilemap层获取瓦片的通行信息。
/// 可以确定瓦片是否导致不可通行,并返回其通行成本。
///
/// 要查询的Tilemap。
/// 格子坐标。
/// 一个元组,包含可空成本 (null表示无特定成本) 和是否不可通行。
private (float? cost, bool isImpassable) _GetTileCostInfo(Tilemap tilemap, Vector3Int cellPosition)
{
var tile = tilemap.GetTile(cellPosition);
if (!tile) return (null, false); // 该层无瓦片
var tileDef = DefineManager.Instance.FindDefine(tile.name);
if (tileDef == null)
// 如果瓦片存在但无定义,视为其不影响通行成本,也不导致路径阻塞。
return (null, false);
// 如果瓦片有碰撞体或者其定义通行成本已经达到或超过 1f,则该瓦片使得格子不可通行。
if (tileDef.collider != Tile.ColliderType.None || tileDef.tileCost >= 1f) return (1f, true); // 不可通行
return (tileDef.tileCost, false); // 可通行,并返回其成本
}
///
/// 获取指定格子坐标上的瓦片状态,包括碰撞体类型和通行成本。
/// 优先级:实体 > buildingTilemap > plantTilemap > baseTilemap。
/// 此函数经优化以减少GC。
///
/// 要查询的格子坐标。
/// 包含瓦片碰撞体类型和通行成本的TileState结构。
public TileState GetTileState(Vector3Int cellPosition)
{
// 优先检查是否有实体占据该格子
if (HasEntity(new Vector2Int(cellPosition.x, cellPosition.y)))
// 如果有实体,则视为不可通行,碰撞体类型为 Grid (或自定义的实体碰撞类型)
return new TileState(cellPosition, Tile.ColliderType.Grid, 1f);
var buildingTileDef = GetTileDef(buildingTilemap, cellPosition);
var plantTileDef = GetTileDef(plantTilemap, cellPosition);
var baseTileDef = GetTileDef(baseTilemap, cellPosition);
var finalColliderType = Tile.ColliderType.None;
var finalTravelCost = 0f;
// 确定最终的碰撞体类型 (优先级 Building > Plant > Base)
if (buildingTileDef != null && buildingTileDef.collider != Tile.ColliderType.None)
finalColliderType = buildingTileDef.collider;
else if (plantTileDef != null && plantTileDef.collider != Tile.ColliderType.None)
finalColliderType = plantTileDef.collider;
else if (baseTileDef != null && baseTileDef.collider != Tile.ColliderType.None)
finalColliderType = baseTileDef.collider;
// 确定最终的通行成本
if (finalColliderType != Tile.ColliderType.None)
{
finalTravelCost = 1f; // 有碰撞体,则通行成本为1 (不可通过)
}
else
{
// 如果没有碰撞体,则取优先级最高的瓦片的tileCost作为最终通行成本。
// 优先级 Building > Plant > Base
if (buildingTileDef != null)
finalTravelCost = buildingTileDef.tileCost;
else if (plantTileDef != null)
finalTravelCost = plantTileDef.tileCost;
else if (baseTileDef != null) finalTravelCost = baseTileDef.tileCost;
// 如果没有任何有效的瓦片定义,finalTravelCost 保持默认值 0f
}
return new TileState(cellPosition, finalColliderType, finalTravelCost);
}
///
/// 获取指定世界坐标上的瓦片状态,包括碰撞体类型和通行成本。
///
/// 要查询的世界坐标。
/// 包含瓦片碰撞体类型和通行成本的TileState结构。
public TileState GetTileState(Vector3 worldPosition)
{
var cellPosition = baseTilemap.WorldToCell(worldPosition);
return GetTileState(cellPosition);
}
///
/// 获取指定格子坐标的通行成本。
/// 优先级:实体 > buildingTilemap > plantTilemap > baseTilemap。
/// 区域外、存在碰撞体或通行成本达到1f的区域均视为不可通行 (返回1f)。
/// 此函数为路径查找优化,避免GC。
///
/// 要查询的格子坐标。
/// 通行成本(速度降低率),1f表示不可通行。
public float GetTravelCost(Vector3Int cellPosition)
{
// 0. 检查格子是否在基本地图区域(baseTilemap.cellBounds)之外。
// 假设baseTilemap.cellBounds定义了陆地的总体逻辑边界。
if (!baseTilemap.cellBounds.Contains(cellPosition)) return 1f; // 区域外始终为1 (不可通行)
// 0.5. 检查该格子是否被实体占据,实体通常是更高优先级的阻碍
if (HasEntity(new Vector2Int(cellPosition.x, cellPosition.y))) return 1f; // 有实体占据,视为不可通行
float? highestPriorityTraversableCost = null; // 用于存储最高优先级的可通行成本
// 1. 检查 Building 层 (现在只检查瓦片本身,实体碰撞已在前面处理)
var buildingResult = _GetTileCostInfo(buildingTilemap, cellPosition);
if (buildingResult.isImpassable) return 1f; // Building 层瓦片导致不可通行
if (buildingResult.cost.HasValue) highestPriorityTraversableCost = buildingResult.cost.Value;
// 2. 检查 Plant 层 (仅当 Building 层未导致不可通行且未提供通行成本时)
if (!highestPriorityTraversableCost.HasValue) // Building层未提供有效成本时,才考虑Plant层
{
var plantResult = _GetTileCostInfo(plantTilemap, cellPosition);
if (plantResult.isImpassable) return 1f; // Plant 层瓦片导致不可通行
if (plantResult.cost.HasValue) highestPriorityTraversableCost = plantResult.cost.Value;
}
// 3. 检查 Base 层 (仅当 Building 和 Plant 层均未导致不可通行且未提供通行成本时)
if (!highestPriorityTraversableCost.HasValue) // Building和Plant层都未提供有效成本时,才考虑Base层
{
var baseResult = _GetTileCostInfo(baseTilemap, cellPosition);
if (baseResult.isImpassable) return 1f; // Base 层瓦片导致不可通行
if (baseResult.cost.HasValue) highestPriorityTraversableCost = baseResult.cost.Value;
}
// 如果没有发现不可通行的条件,返回最高优先级的可通行成本,如果都没有则为 0f (完全可通行)。
var finalCost = highestPriorityTraversableCost ?? 0f;
// 再次确认最终成本,如果达到或超过1f,依然视为不可通行。 (这部分逻辑在 _GetTileCostInfo 和前面实体检测已经处理,这里只是额外兜底)
if (finalCost >= 1f) return 1f;
return finalCost;
}
///
/// 获取指定世界坐标的通行成本。
///
/// 世界坐标。
/// 通行成本(速度降低率),1f表示不可通行。
public float GetTravelCost(Vector3 worldPosition)
{
return GetTravelCost(GetCellCoordinates(worldPosition));
}
///
/// 获取实体所占区域的通行成本。
/// 如果区域内任何一个格子不可通行 (GetTravelCost返回1f),则整个区域不可通行 (返回1f)。
/// 否则,取实体中心格子的通行成本。
/// 此函数适用于具有体积的实体进行路径查找。
///
/// 实体的中心格子坐标。
/// 实体的尺寸,以格子为单位 (例如:Vector3Int(2, 2, 1) 表示一个 2x2 的实体)。
/// 区域的通行成本(速度降低率),1f表示不可通行。
public float GetTravelCostForArea(Vector3Int entityCenterCell, Vector3Int size)
{
var minX = entityCenterCell.x - size.x / 2;
var minY = entityCenterCell.y - size.y / 2;
var maxX = minX + size.x;
var maxY = minY + size.y;
// 遍历实体覆盖的所有格子
for (var x = minX; x < maxX; x++)
for (var y = minY; y < maxY; y++)
{
// 对于当前遍历的每个格子,其Z坐标使用实体中心格子的Z坐标
var currentCell = new Vector3Int(x, y, entityCenterCell.z);
// 使用优化过的 GetTravelCost 函数判断每个格子的通行成本
// 假设 GetTravelCost(Vector3Int cellPosition) 是最底层的、直接获取格子通行成本的函数
var cellCost = GetTravelCost(currentCell);
if (cellCost >= 1f) return 1f; // 区域内任何一个格子不可通行,则整个区域不可通行 (快速失败原则)
}
// 如果所有覆盖的格子都可通行,则返回实体中心格子的通行成本
return GetTravelCost(entityCenterCell);
}
///
/// 获取实体所占区域的通行成本。
/// 如果区域内任何一个格子不可通行 (GetTravelCost返回1f),则整个区域不可通行 (返回1f)。
/// 否则,取实体中心格子的通行成本。
/// 此函数适用于具有体积的实体进行路径查找。
///
/// 实体的世界坐标 (假定为中心点)。
/// 实体的尺寸,以格子为单位 (例如:Vector3Int(2, 2, 1) 表示一个 2x2 的实体)。
/// 区域的通行成本(速度降低率),1f表示不可通行。
public float GetTravelCostForArea(Vector3 worldPosition, Vector3Int size)
{
// 将实体的世界坐标转换为其所在的格子坐标,然后调用基于格子坐标的重载函数
var entityCenterCell = baseTilemap.WorldToCell(worldPosition);
return GetTravelCostForArea(entityCenterCell, size);
}
///
/// 设置基础瓦片。
///
/// 要设置的瓦片。
/// 瓦片的位置。
/// 当前位置的旧瓦片。
public TileBase SetBaseTile(TileBase tile, Vector3Int position)
{
var current = baseTilemap.GetTile(position);
baseTilemap.SetTile(position, tile);
return current;
}
///
/// 检查指定位置是否有实体。
///
/// 要检查的地图格子坐标。
/// 如果存在实体则返回true,否则返回false。
public bool HasEntity(Vector2Int position)
{
// 将地图格子坐标转换为hasEntity数组的索引
position += entityMapShift;
// 检查转换后的索引是否在hasEntity数组的有效范围内
if (position.x < 0 || position.y < 0 ||
position.x >= hasEntity.GetLength(0) ||
position.y >= hasEntity.GetLength(1))
return true; // 数组越界视为有实体(或地图外部),通常是不可通行的
return hasEntity[position.x, position.y] > 0;
}
///
/// 当实体被创建时触发的回调,用于更新hasEntity网格中对应位置的值。
///
/// 被创建的实体。
private void OnHaveEntityCreate(Entity.Entity entity)
{
if (entity is not Building building)
return;
if(building.currentDimensionId!=GetDimensionID())
return;
var size = building.Size;
var offset = building.entityPrefab.outline.GetOffset();
var position = (Vector2)building.Position;
// 1. 计算建筑在世界坐标中的左下角 (或起始点)
// position 是中心点。
// Building.Size 是以网格单元为单位的大小,需要转换为世界坐标。
// offset 是轮廓相对于中心点的偏移。
// 计算建筑的半边长(世界坐标)
var halfSizeWorld = new Vector2(size.x * 0.5f, size.y * 0.5f);
// 计算建筑的左下角世界坐标(考虑中心点和偏移)
var bottomLeftWorld = position - halfSizeWorld + offset;
// 2. 将世界坐标转换为网格坐标
var startGrid = GetCellCoordinates(bottomLeftWorld);
// 3. 计算建筑在网格上覆盖的结束点(不包括结束点本身)
// 这里我们直接用 Building.Size 作为网格上的宽度和高度。
// 因为 Building.Size 已经是以网格单元为单位了。
var endGrid = new Vector2Int((int)(startGrid.x + size.x), (int)(startGrid.y + size.y));
// 4. 遍历覆盖的网格范围,将hasEntity对应位置的值+1
for (var x = startGrid.x; x < endGrid.x; x++)
for (var y = startGrid.y; y < endGrid.y; y++)
{
// 转换到hasEntity数组的实际索引
var arrayX = x + entityMapShift.x;
var arrayY = y + entityMapShift.y;
// 确保坐标在hasEntity数组的边界内
if (arrayX >= 0 && arrayX < hasEntity.GetLength(0) && arrayY >= 0 && arrayY < hasEntity.GetLength(1))
hasEntity[arrayX, arrayY]++;
}
entity.OnEntityDiedEvent += OnHaveEntityDestroy;
}
///
/// 当实体被销毁时触发的回调,用于将hasEntity网格中对应位置的值-1。
///
/// 被销毁的实体。
public void OnHaveEntityDestroy(Entity.Entity entity)
{
if (entity is not Building building)
return;
var size = building.Size;
var offset = building.entityPrefab.outline.GetOffset();
var position = (Vector2)building.Position;
// 1. 计算建筑在世界坐标中的左下角 (或起始点)
var halfSizeWorld = new Vector2(size.x * 0.5f, size.y * 0.5f);
var bottomLeftWorld = position - halfSizeWorld + offset;
// 2. 将世界坐标转换为网格坐标
var startGrid = GetCellCoordinates(bottomLeftWorld);
// 3. 计算建筑在网格上覆盖的结束点(不包括结束点本身)
// 与OnHaveEntityCreate中保持一致的计算方式。
var endGrid = new Vector2Int((int)(startGrid.x + size.x), (int)(startGrid.y + size.y));
// 4. 遍历覆盖的网格范围,将hasEntity对应位置的值-1
for (var x = startGrid.x; x < endGrid.x; x++)
for (var y = startGrid.y; y < endGrid.y; y++)
{
// 转换到hasEntity数组的实际索引
var arrayX = x + entityMapShift.x;
var arrayY = y + entityMapShift.y;
// 确保坐标在hasEntity数组的边界内
if (arrayX >= 0 && arrayX < hasEntity.GetLength(0) && arrayY >= 0 && arrayY < hasEntity.GetLength(1))
{
hasEntity[arrayX, arrayY]--;
// 可选:添加断言或错误检查,确保hasEntity不会是负数
if (hasEntity[arrayX, arrayY] < 0)
{
Debug.LogError(
$"hasEntity在 (地图坐标: {x}, {y} 实际数组索引:{arrayX},{arrayY}) 变为负数。这表明建筑 {building.name} 的计数错误。可能发生了销毁事件而没有相应的创建,或者存在重复的销毁调用。");
hasEntity[arrayX, arrayY] = 0; // 避免负数,可能是个临时的修复
}
}
else
{
Debug.LogWarning(
$"建筑 {building.name} ({position}) 试图在网格边界外 (地图坐标: {x}, {y} 实际数组索引:{arrayX},{arrayY}) 销毁实体。这可能表明创建或销毁逻辑存在错误。");
}
}
}
///
/// 定义瓦片状态结构体
///
public struct TileState
{
// 瓦片的世界坐标
public Vector3Int position;
// 碰撞体类型
public Tile.ColliderType colliderType;
// 通行成本
public float travelCost;
///
/// 构造函数,初始化瓦片状态。
///
/// 瓦片的世界坐标。
/// 碰撞体类型。
/// 通行成本。
public TileState(Vector3Int pos, Tile.ColliderType collider, float cost)
{
position = pos;
colliderType = collider;
travelCost = cost;
}
}
}
}