Files
Gen_Hack-and-Slash-Roguelite/Client/Assets/Scripts/Map/Landform.cs

470 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
/// <summary>
/// 初始化实体碰撞数组。
/// </summary>
/// <param name="size">数组的大小。</param>
/// <param name="shift">实体地图偏移量。</param>
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;
}
/// <summary>
/// 获取当前地形所属的维度ID。
/// </summary>
/// <returns>维度ID字符串。</returns>
public string GetDimensionID()
{
if (dimension) return dimension.DimensionId;
Debug.LogWarning("<color=orange>Landform未分配Dimension对象返回默认ID。</color>");
return "dimension_not_set";
}
/// <summary>
/// 获取基础瓦片地图的尺寸。
/// </summary>
/// <returns>瓦片地图的尺寸 (Vector3Int)。</returns>
public Vector3Int GetSize()
{
return baseTilemap.size;
}
/// <summary>
/// 获取基础瓦片地图的原点。
/// </summary>
/// <returns>瓦片地图的原点 (Vector3Int)。</returns>
public Vector3Int GetOrigin()
{
return baseTilemap.origin;
}
/// <summary>
/// 将格子坐标转换为世界坐标。
/// </summary>
/// <param name="coord">格子坐标。</param>
/// <returns>对应的世界坐标。</returns>
public Vector3 GetWorldCoordinates(Vector3Int coord)
{
return baseTilemap.CellToWorld(coord);
}
/// <summary>
/// 将世界坐标转换为格子坐标。
/// </summary>
/// <param name="worldPosition">世界坐标。</param>
/// <returns>对应的格子坐标。</returns>
public Vector3Int GetCellCoordinates(Vector3 worldPosition)
{
return baseTilemap.WorldToCell(worldPosition);
}
/// <summary>
/// 清除所有瓦片地图上的所有瓦片。
/// </summary>
public void Clear()
{
baseTilemap.ClearAllTiles();
buildingTilemap.ClearAllTiles();
plantTilemap.ClearAllTiles();
}
/// <summary>
/// 私有辅助方法安全地从指定Tilemap层获取TileDef。
/// </summary>
/// <param name="tilemap">要查询的Tilemap。</param>
/// <param name="cellPosition">格子坐标。</param>
/// <returns>找到的TileDef如果没有瓦片或定义则为null。</returns>
private TileDef GetTileDef(Tilemap tilemap, Vector3Int cellPosition)
{
var tile = tilemap.GetTile(cellPosition);
return tile ? DefineManager.Instance.FindDefine<TileDef>(tile.name) : null;
}
/// <summary>
/// 私有辅助方法从指定的Tilemap层获取瓦片的通行信息。
/// 可以确定瓦片是否导致不可通行,并返回其通行成本。
/// </summary>
/// <param name="tilemap">要查询的Tilemap。</param>
/// <param name="cellPosition">格子坐标。</param>
/// <returns>一个元组,包含可空成本 (null表示无特定成本) 和是否不可通行。</returns>
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<TileDef>(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); // 可通行,并返回其成本
}
/// <summary>
/// 获取指定格子坐标上的瓦片状态,包括碰撞体类型和通行成本。
/// 优先级:实体 &gt; buildingTilemap &gt; plantTilemap &gt; baseTilemap。
/// 此函数经优化以减少GC。
/// </summary>
/// <param name="cellPosition">要查询的格子坐标。</param>
/// <returns>包含瓦片碰撞体类型和通行成本的TileState结构。</returns>
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);
}
/// <summary>
/// 获取指定世界坐标上的瓦片状态,包括碰撞体类型和通行成本。
/// </summary>
/// <param name="worldPosition">要查询的世界坐标。</param>
/// <returns>包含瓦片碰撞体类型和通行成本的TileState结构。</returns>
public TileState GetTileState(Vector3 worldPosition)
{
var cellPosition = baseTilemap.WorldToCell(worldPosition);
return GetTileState(cellPosition);
}
/// <summary>
/// 获取指定格子坐标的通行成本。
/// 优先级:实体 &gt; buildingTilemap &gt; plantTilemap &gt; baseTilemap。
/// 区域外、存在碰撞体或通行成本达到1f的区域均视为不可通行 (返回1f)。
/// 此函数为路径查找优化避免GC。
/// </summary>
/// <param name="cellPosition">要查询的格子坐标。</param>
/// <returns>通行成本速度降低率1f表示不可通行。</returns>
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;
}
/// <summary>
/// 获取指定世界坐标的通行成本。
/// </summary>
/// <param name="worldPosition">世界坐标。</param>
/// <returns>通行成本速度降低率1f表示不可通行。</returns>
public float GetTravelCost(Vector3 worldPosition)
{
return GetTravelCost(GetCellCoordinates(worldPosition));
}
/// <summary>
/// 获取实体所占区域的通行成本。
/// 如果区域内任何一个格子不可通行 (GetTravelCost返回1f),则整个区域不可通行 (返回1f)。
/// 否则,取实体中心格子的通行成本。
/// 此函数适用于具有体积的实体进行路径查找。
/// </summary>
/// <param name="entityCenterCell">实体的中心格子坐标。</param>
/// <param name="size">实体的尺寸,以格子为单位 (例如Vector3Int(2, 2, 1) 表示一个 2x2 的实体)。</param>
/// <returns>区域的通行成本速度降低率1f表示不可通行。</returns>
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);
}
/// <summary>
/// 获取实体所占区域的通行成本。
/// 如果区域内任何一个格子不可通行 (GetTravelCost返回1f),则整个区域不可通行 (返回1f)。
/// 否则,取实体中心格子的通行成本。
/// 此函数适用于具有体积的实体进行路径查找。
/// </summary>
/// <param name="worldPosition">实体的世界坐标 (假定为中心点)。</param>
/// <param name="size">实体的尺寸,以格子为单位 (例如Vector3Int(2, 2, 1) 表示一个 2x2 的实体)。</param>
/// <returns>区域的通行成本速度降低率1f表示不可通行。</returns>
public float GetTravelCostForArea(Vector3 worldPosition, Vector3Int size)
{
// 将实体的世界坐标转换为其所在的格子坐标,然后调用基于格子坐标的重载函数
var entityCenterCell = baseTilemap.WorldToCell(worldPosition);
return GetTravelCostForArea(entityCenterCell, size);
}
/// <summary>
/// 设置基础瓦片。
/// </summary>
/// <param name="tile">要设置的瓦片。</param>
/// <param name="position">瓦片的位置。</param>
/// <returns>当前位置的旧瓦片。</returns>
public TileBase SetBaseTile(TileBase tile, Vector3Int position)
{
var current = baseTilemap.GetTile(position);
baseTilemap.SetTile(position, tile);
return current;
}
/// <summary>
/// 检查指定位置是否有实体。
/// </summary>
/// <param name="position">要检查的地图格子坐标。</param>
/// <returns>如果存在实体则返回true否则返回false。</returns>
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;
}
/// <summary>
/// 当实体被创建时触发的回调用于更新hasEntity网格中对应位置的值。
/// </summary>
/// <param name="entity">被创建的实体。</param>
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;
}
/// <summary>
/// 当实体被销毁时触发的回调用于将hasEntity网格中对应位置的值-1。
/// </summary>
/// <param name="entity">被销毁的实体。</param>
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(
$"<color=red>hasEntity在 (地图坐标: {x}, {y} 实际数组索引:{arrayX},{arrayY}) 变为负数。这表明建筑 {building.name} 的计数错误。可能发生了销毁事件而没有相应的创建,或者存在重复的销毁调用。</color>");
hasEntity[arrayX, arrayY] = 0; // 避免负数,可能是个临时的修复
}
}
else
{
Debug.LogWarning(
$"<color=orange>建筑 {building.name} ({position}) 试图在网格边界外 (地图坐标: {x}, {y} 实际数组索引:{arrayX},{arrayY}) 销毁实体。这可能表明创建或销毁逻辑存在错误。</color>");
}
}
}
/// <summary>
/// 定义瓦片状态结构体
/// </summary>
public struct TileState
{
// 瓦片的世界坐标
public Vector3Int position;
// 碰撞体类型
public Tile.ColliderType colliderType;
// 通行成本
public float travelCost;
/// <summary>
/// 构造函数,初始化瓦片状态。
/// </summary>
/// <param name="pos">瓦片的世界坐标。</param>
/// <param name="collider">碰撞体类型。</param>
/// <param name="cost">通行成本。</param>
public TileState(Vector3Int pos, Tile.ColliderType collider, float cost)
{
position = pos;
colliderType = collider;
travelCost = cost;
}
}
}
}