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

592 lines
25 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 UnityEngine;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; // 引入 System.Threading.Tasks
using Managers;
namespace Map
{
/// <summary>
/// 定义图案中单个瓦片的数据。
/// </summary>
public class PatternTileData
{
/// <summary>
/// 相对于图案锚点的X坐标偏移。
/// </summary>
[JsonProperty("relativeX", Required = Required.Always)]
public int RelativeX { get; set; }
/// <summary>
/// 相对于图案锚点的Y坐标偏移。
/// </summary>
[JsonProperty("relativeY", Required = Required.Always)]
public int RelativeY { get; set; }
/// <summary>
/// 该位置放置的瓦片定义名称。
/// </summary>
[JsonProperty("tileDefName", Required = Required.Always)]
public string TileDefName { get; set; } = "DefaultBuildingTile";
}
/// <summary>
/// 定义一个可重复使用的瓦片图案。
/// </summary>
public class PatternDefinition
{
/// <summary>
/// 图案的唯一ID。
/// </summary>
[JsonProperty("patternId", Required = Required.Always)]
public string PatternId { get; set; } = "DefaultPattern";
/// <summary>
/// 构成图案的瓦片列表。
/// </summary>
[JsonProperty("tiles", Required = Required.Always)]
public List<PatternTileData> Tiles { get; set; } = new List<PatternTileData>();
/// <summary>
/// 图案的宽度从最小到最大X相对坐标的范围
/// </summary>
[JsonIgnore] public int Width { get; private set; }
/// <summary>
/// 图案的高度从最小到最大Y相对坐标的范围
/// </summary>
[JsonIgnore] public int Height { get; private set; }
/// <summary>
/// 相对于图案锚点的最小X坐标。
/// </summary>
[JsonIgnore] public int MinRelativeX { get; private set; }
/// <summary>
/// 相对于图案锚点的最大X坐标。
/// </summary>
[JsonIgnore] public int MaxRelativeX { get; private set; }
/// <summary>
/// 相对于图案锚点的最小Y坐标。
/// </summary>
[JsonIgnore] public int MinRelativeY { get; private set; }
/// <summary>
/// 相对于图案锚点的最大Y坐标。
/// </summary>
[JsonIgnore] public int MaxRelativeY { get; private set; }
/// <summary>
/// 计算图案的边界和尺寸。应在Init解析后调用一次。
/// </summary>
public void CalculateBounds()
{
if (Tiles == null || Tiles.Count == 0)
{
MinRelativeX = 0;
MaxRelativeX = 0;
Width = 1;
MinRelativeY = 0;
MaxRelativeY = 0;
Height = 1;
return;
}
MinRelativeX = int.MaxValue;
MaxRelativeX = int.MinValue;
MinRelativeY = int.MaxValue;
MaxRelativeY = int.MinValue;
foreach (var tileData in Tiles)
{
MinRelativeX = Mathf.Min(MinRelativeX, tileData.RelativeX);
MaxRelativeX = Mathf.Max(MaxRelativeX, tileData.RelativeX);
MinRelativeY = Mathf.Min(MinRelativeY, tileData.RelativeY);
MaxRelativeY = Mathf.Max(MaxRelativeY, tileData.RelativeY);
}
Width = MaxRelativeX - MinRelativeX + 1;
Height = MaxRelativeY - MinRelativeY + 1;
}
/// <summary>
/// 缓存已加载的TileBase对象避免重复从TileManager获取。
/// </summary>
[JsonIgnore] private Dictionary<string, UnityEngine.Tilemaps.TileBase> _tileCache;
/// <summary>
/// 获取或缓存指定定义名称的TileBase。
/// </summary>
/// <param name="defName">瓦片定义名称。</param>
/// <returns>对应的TileBase对象如果未找到则为null。</returns>
public UnityEngine.Tilemaps.TileBase GetCachedTile(string defName)
{
_tileCache ??= new Dictionary<string, UnityEngine.Tilemaps.TileBase>();
if (!_tileCache.TryGetValue(defName, out var tile))
{
tile = TileManager.Instance.GetTile(defName);
if (tile != null)
{
_tileCache[defName] = tile;
}
}
return tile;
}
}
/// <summary>
/// 抽象基类,用于定义图案的放置指令。
/// </summary>
[JsonConverter(typeof(PlacementInstructionConverter))]
public abstract class PlacementInstruction
{
/// <summary>
/// 要放置的图案ID。
/// </summary>
[JsonProperty("patternId", Required = Required.Always)]
public string PatternId { get; set; } = string.Empty;
/// <summary>
/// (可选) 必须放置在其上的基础瓦片名称。
/// </summary>
[JsonProperty("requiredBaseTileDefName")]
public string RequiredBaseTileDefName { get; set; }
/// <summary>
/// 放置指令的类型字段用于JsonConverter识别。
/// </summary>
[JsonProperty("type", Required = Required.Always)]
public string Type { get; protected set; }
/// <summary>
/// 构造函数。
/// </summary>
/// <param name="type">指令类型字符串。</param>
protected PlacementInstruction(string type)
{
Type = type;
}
}
/// <summary>
/// 用于反序列化PlacementInstruction及其派生类的JSON转换器。
/// </summary>
public class PlacementInstructionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(PlacementInstruction);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
var jsonObject = Newtonsoft.Json.Linq.JObject.Load(reader);
var type = jsonObject["type"]?.ToString();
if (type == null)
{
throw new JsonSerializationException("放置指令JSON中必须包含'type'字段。");
}
PlacementInstruction instruction;
switch (type.ToLowerInvariant())
{
case "fixed":
instruction = new FixedPlacementInstruction();
break;
case "random":
instruction = new RandomPlacementInstruction();
break;
default:
throw new JsonSerializationException($"未知的放置指令类型: {type}");
}
serializer.Populate(jsonObject.CreateReader(), instruction);
return instruction;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value, value?.GetType() ?? typeof(object));
}
}
/// <summary>
/// 指定固定位置放置图案的指令。
/// </summary>
public class FixedPlacementInstruction : PlacementInstruction
{
/// <summary>
/// 放置的绝对X坐标。
/// </summary>
[JsonProperty("x", Required = Required.Always)]
public int X { get; set; }
/// <summary>
/// 放置的绝对Y坐标。
/// </summary>
[JsonProperty("y", Required = Required.Always)]
public int Y { get; set; }
/// <summary>
/// 构造函数。
/// </summary>
public FixedPlacementInstruction() : base("fixed")
{
}
}
/// <summary>
/// 指定随机位置放置图案的指令。
/// </summary>
public class RandomPlacementInstruction : PlacementInstruction
{
/// <summary>
/// 尝试放置的次数。
/// </summary>
[JsonProperty("count", Required = Required.Always)]
public int Count { get; set; } = 1;
/// <summary>
/// 随机放置区域的最小X坐标。
/// </summary>
[JsonProperty("minX")] public int MinX { get; set; }
/// <summary>
/// 随机放置区域的最大X坐标。
/// </summary>
[JsonProperty("maxX")] public int MaxX { get; set; } = 99;
/// <summary>
/// 随机放置区域的最小Y坐标。
/// </summary>
[JsonProperty("minY")] public int MinY { get; set; }
/// <summary>
/// 随机放置区域的最大Y坐标。
/// </summary>
[JsonProperty("maxY")] public int MaxY { get; set; } = 99;
/// <summary>
/// 为每个图案尝试放置的最大次数,防止无限循环。
/// </summary>
[JsonProperty("maxAttemptsPerPattern")]
public int MaxAttemptsPerPattern { get; set; } = 20;
/// <summary>
/// 构造函数。
/// </summary>
public RandomPlacementInstruction() : base("random")
{
}
}
/// <summary>
/// 建筑/图案生成器的配置类。
/// </summary>
public class BuildingPatternGeneratorConfig
{
/// <summary>
/// 所有预定义的图案列表。
/// </summary>
[JsonProperty("patterns", Required = Required.Always)]
public List<PatternDefinition> Patterns { get; set; } = new List<PatternDefinition>();
/// <summary>
/// 放置图案的指令列表。
/// </summary>
[JsonProperty("placementInstructions", Required = Required.Always)]
public List<PlacementInstruction> PlacementInstructions { get; set; } =
new List<PlacementInstruction>();
/// <summary>
/// 是否防止图案互相重叠。
/// </summary>
[JsonProperty("preventOverlap")] public bool PreventOverlap { get; set; } = true;
/// <summary>
/// 地图生成区域的宽度(单元格数量)。
/// </summary>
[JsonProperty("mapCellSizeX")] public int MapCellSizeX { get; set; } = 100;
/// <summary>
/// 地图生成区域的高度(单元格数量)。
/// </summary>
[JsonProperty("mapCellSizeY")] public int MapCellSizeY { get; set; } = 100;
}
/// <summary>
/// 负责根据JSON配置生成建筑和复杂图案的地图生成器。
/// </summary>
public class BuildingPatternMapGenerator : MapGeneratorWorkClassBase
{
/// <summary>
/// 建筑/图案生成器的配置。
/// </summary>
private BuildingPatternGeneratorConfig _config;
/// <summary>
/// 存储图案ID到PatternDefinition的查找表用于快速访问。
/// </summary>
private Dictionary<string, PatternDefinition> _patternLookup;
/// <summary>
/// 初始化建筑/图案生成器解析JSON配置字符串。
/// </summary>
/// <param name="value">包含生成参数的JSON字符串。</param>
public override void Init(string value)
{
try
{
_config = JsonConvert.DeserializeObject<BuildingPatternGeneratorConfig>(value);
if (_config == null)
{
Debug.LogError(
"建筑图案地图生成器配置反序列化为空。请检查JSON格式。中止初始化。");
_config = new BuildingPatternGeneratorConfig();
return;
}
_patternLookup = new Dictionary<string, PatternDefinition>();
foreach (var pattern in _config.Patterns)
{
if (string.IsNullOrWhiteSpace(pattern.PatternId))
{
Debug.LogWarning(
"建筑图案地图生成器图案定义中存在空的图案ID。已跳过。");
continue;
}
if (_patternLookup.ContainsKey(pattern.PatternId))
{
Debug.LogWarning(
$"建筑图案地图生成器发现重复的图案ID '{pattern.PatternId}'。将覆盖现有定义。");
}
pattern.CalculateBounds();
_patternLookup[pattern.PatternId] = pattern;
}
if (_config.MapCellSizeX <= 0) _config.MapCellSizeX = 100;
if (_config.MapCellSizeY <= 0) _config.MapCellSizeY = 100;
}
catch (JsonSerializationException ex)
{
Debug.LogError(
$"建筑图案地图生成器JSON反序列化错误{ex.Message}。输入JSON: '{value}'");
_config = null;
}
catch (Exception ex)
{
Debug.LogError(
$"建筑图案地图生成器:初始化过程中发生未预期错误:{ex.Message}。输入JSON: '{value}'");
_config = null;
}
}
/// <summary>
/// 根据配置参数在 `MapGenerator` 的 `buildingTilemap` 上生成建筑和图案。
/// 此方法现在是异步的,以避免阻塞主线程。
/// </summary>
/// <param name="map">MapGenerator实例包含要操作的Tilemap。</param>
public override async Task Process(MapGenerator map) // 标记为 async Task
{
if (_config == null || _patternLookup == null)
{
Debug.LogError(
"建筑图案地图生成器在初始化之前或初始化失败后调用Process。中止生成。");
return; // 返回一个已完成的Task
}
if (map == null || map.buildingTilemap == null)
{
Debug.LogError(
"建筑图案地图生成器MapGenerator或建筑瓦片地图为空。无法生成建筑。中止。");
return; // 返回一个已完成的Task
}
if (map.baseTilemap == null &&
_config.PlacementInstructions.Any(p => !string.IsNullOrEmpty(p.RequiredBaseTileDefName)))
{
Debug.LogWarning(
"建筑图案地图生成器:某些放置指令需要基础瓦片,但基础瓦片地图为空。基础瓦片要求将被忽略或导致错误。");
}
// 迭代每个放置指令
foreach (var instruction in _config.PlacementInstructions)
{
if (!_patternLookup.TryGetValue(instruction.PatternId, out var patternDef))
{
Debug.LogError(
$"建筑图案地图生成器在已定义的图案中找不到图案ID '{instruction.PatternId}'。跳过该指令。");
continue;
}
if (instruction is FixedPlacementInstruction fixedInstruction)
{
// 固定放置通常很快,不需要 await Task.Yield()
PlacePattern(map, patternDef, fixedInstruction.X, fixedInstruction.Y,
instruction.RequiredBaseTileDefName, _config.PreventOverlap);
}
else if (instruction is RandomPlacementInstruction randomInstruction)
{
// 随机放置可能会尝试多次,因此内部添加 await Task.Yield()
// 标记了 private async Task并await它。
await AttemptRandomPlacement(map, patternDef, randomInstruction, instruction.RequiredBaseTileDefName,
_config.PreventOverlap);
}
// 每处理一个指令后,暂停一下,以防有大量指令
await Task.Yield();
}
}
/// <summary>
/// 尝试在地图上随机位置放置一个图案。此方法现在是异步的,以避免阻塞主线程。
/// </summary>
/// <param name="map">MapGenerator实例。</param>
/// <param name="patternDef">要放置的图案定义。</param>
/// <param name="instruction">随机放置指令。</param>
/// <param name="requiredBaseTileDefName">所需的基础瓦片定义名称。</param>
/// <param name="preventOverlap">是否防止重叠。</param>
private async Task AttemptRandomPlacement(MapGenerator map, PatternDefinition patternDef, // 标记为 async Task
RandomPlacementInstruction instruction, string requiredBaseTileDefName, bool preventOverlap)
{
for (var attempt = 0; attempt < instruction.MaxAttemptsPerPattern; attempt++)
{
// 确保随机放置的范围在地图尺寸内且能容纳图案
// 注意:`randomX`, `randomY`在此逻辑中被计算为图案的“左上角”世界坐标,而不是锚点(0,0)的世界坐标。
var rangeMinX = Mathf.Max(instruction.MinX, 0);
var rangeMaxX =
Mathf.Min(instruction.MaxX,
_config!.MapCellSizeX - patternDef.Width); // 确保图案水平方向能放下
var rangeMinY = Mathf.Max(instruction.MinY, 0);
var rangeMaxY =
Mathf.Min(instruction.MaxY,
_config!.MapCellSizeY - patternDef.Height); // 确保图案垂直方向能放下
if (rangeMinX > rangeMaxX || rangeMinY > rangeMaxY)
{
Debug.LogWarning(
$"建筑图案地图生成器:图案 '{patternDef.PatternId}' 的随机放置范围 ({instruction.MinX}-{instruction.MaxX}, {instruction.MinY}-{instruction.MaxY}) 无效或小于图案尺寸 ({patternDef.Width}x{patternDef.Height})。跳过随机放置尝试。");
return; // 返回一个已完成的Task
}
var randomX =
UnityEngine.Random.Range(rangeMinX,
rangeMaxX + 1); // +1 因为Range(int, int)对于max是排他性的
var randomY = UnityEngine.Random.Range(rangeMinY, rangeMaxY + 1);
// 根据现有逻辑,`CanPlacePatternAt`和`PlacePattern`的`worldAnchorX, worldAnchorY`参数需要是图案(0,0)相对点的世界坐标。
// 而这里计算的`randomX, randomY`是图案边界的左上角。
// 故将`randomX + patternDef.MinRelativeX`和`randomY + patternDef.MinRelativeY`作为传递给方法的锚点。
if (CanPlacePatternAt(map, patternDef, randomX + patternDef.MinRelativeX,
randomY + patternDef.MinRelativeY, requiredBaseTileDefName, preventOverlap))
{
PlacePattern(map, patternDef, randomX + patternDef.MinRelativeX, randomY + patternDef.MinRelativeY, requiredBaseTileDefName, preventOverlap);
return; // 放置成功退出尝试循环返回一个已完成的Task
}
// 在每次尝试后,暂停一下,避免一个随机放置尝试次数过多导致阻塞
await Task.Yield();
}
Debug.LogWarning(
$"建筑图案地图生成器:在尝试 {instruction.MaxAttemptsPerPattern} 次后,未能随机放置图案 '{patternDef.PatternId}'。");
}
/// <summary>
/// 检查一个图案是否可以在给定位置放置,同时满足基础瓦片和重叠要求。
/// 这个方法执行的是快速检查,不需要异步。
/// </summary>
/// <param name="map">MapGenerator实例。</param>
/// <param name="patternDef">要放置的图案定义。</param>
/// <param name="worldAnchorX">图案锚点(相对坐标0,0)的世界X坐标。</param>
/// <param name="worldAnchorY">图案锚点(相对坐标0,0)的世界Y坐标。</param>
/// <param name="requiredBaseTileDefName">所需的基础瓦片定义名称。</param>
/// <param name="preventOverlap">是否防止重叠。</param>
/// <returns>如果可以放置则为true否则为false。</returns>
private bool CanPlacePatternAt(MapGenerator map, PatternDefinition patternDef, int worldAnchorX,
int worldAnchorY, string requiredBaseTileDefName, bool preventOverlap)
{
foreach (var tileData in patternDef.Tiles)
{
var currentWorldX = worldAnchorX + tileData.RelativeX;
var currentWorldY = worldAnchorY + tileData.RelativeY;
var tilePosition = new Vector3Int(currentWorldX, currentWorldY, 0);
if (currentWorldX < 0 || currentWorldX >= _config!.MapCellSizeX ||
currentWorldY < 0 || currentWorldY >= _config.MapCellSizeY)
{
return false;
}
if (preventOverlap)
{
var existingBuildingTile = map.buildingTilemap.GetTile(tilePosition);
if (existingBuildingTile != null)
{
return false;
}
}
if (!string.IsNullOrEmpty(requiredBaseTileDefName))
{
if (map.baseTilemap == null)
{
Debug.LogWarning(
$"建筑图案地图生成器:已配置基础瓦片名称 ('{requiredBaseTileDefName}') 但基础瓦片地图为空。将忽略基础瓦片要求。");
continue; // 忽略此瓦片的基础检查,继续其他检查
}
var baseTile = map.baseTilemap.GetTile(tilePosition);
// 注意GetTile().name 可能不是最优方式,推荐使用 ScriptableObject 的引用比较或唯一ID
// 但如果 requiredBaseTileDefName 确实是 GetTile().name则保持此逻辑。
if (baseTile == null || baseTile.name != TileManager.Instance.GetTile(requiredBaseTileDefName)?.name) // 比较名称
{
// 考虑缓存 requiredBaseTile 的引用以防止每次循环都查找
return false;
}
}
}
return true;
}
/// <summary>
/// 实际在地图上放置一个图案。该方法假设CanPlacePatternAt已确认可以放置。
/// 这个方法执行的是快速设置瓦片的操作,不需要异步。
/// </summary>
/// <param name="map">MapGenerator实例。</param>
/// <param name="patternDef">要放置的图案定义。</param>
/// <param name="worldAnchorX">图案锚点(相对坐标0,0)的世界X坐标。</param>
/// <param name="worldAnchorY">图案锚点(相对坐标0,0)的世界Y坐标。</param>
/// <param name="requiredBaseTileDefName">所需的基础瓦片定义名称。</param>
/// <param name="preventOverlap">是否防止重叠。</param>
private void PlacePattern(MapGenerator map, PatternDefinition patternDef, int worldAnchorX, int worldAnchorY,
string requiredBaseTileDefName, bool preventOverlap)
{
// 再次检查放置可行性,确保在多线程或异步上下文中没有竞态条件导致的状态变更
// 但如果 PlacePattern 立即在 CanPlacePatternAt 之后调用,且都在主线程,这个检查主要用于防御性编程。
if (!CanPlacePatternAt(map, patternDef, worldAnchorX, worldAnchorY, requiredBaseTileDefName,
preventOverlap))
{
Debug.LogWarning(
$"建筑图案地图生成器:尝试在 ({worldAnchorX},{worldAnchorY}) 放置图案 '{patternDef.PatternId}' 失败,原因是非法放置。此情况本不应发生,请检查放置逻辑或异步执行中的竞态条件。");
return;
}
foreach (var tileData in patternDef.Tiles)
{
var currentWorldX = worldAnchorX + tileData.RelativeX;
var currentWorldY = worldAnchorY + tileData.RelativeY;
var tilePosition = new Vector3Int(currentWorldX, currentWorldY, 0);
var tileToPlace = patternDef.GetCachedTile(tileData.TileDefName);
if (tileToPlace != null)
{
map.buildingTilemap.SetTile(tilePosition, tileToPlace);
}
else
{
Debug.LogError(
$"建筑图案地图生成器:未能获取定义名称为 '{tileData.TileDefName}' 的瓦片,用于图案 '{patternDef.PatternId}' 在 ({currentWorldX},{currentWorldY}) 处的放置。");
}
}
}
}
}