using System.Collections.Generic; using System.Linq; using UnityEngine; namespace Cielonos.MainGame.Map { /// /// 全联通地图生成器,生成类似"以撒的结合"/"挺进地牢"的 2D 网格地图。 /// 所有节点双向互通,玩家可以自由走遍地图中的每一个房间。 /// public static class MapGenerator { /// 网格四方向偏移。 private static readonly Vector2Int[] Directions = { Vector2Int.up, Vector2Int.down, Vector2Int.left, Vector2Int.right }; /// 生成失败时允许的最大重试次数。 private const int MAX_GENERATION_RETRIES = 10; /// /// 根据配置生成一份完整的全联通 RunMapData。 /// 若特殊节点配额无法满足,则重新生成,最多重试 次。 /// 所有重试均失败时返回 null 并输出警告。 /// public static RunMapData Generate(MapGenerationConfig config) { for (int attempt = 1; attempt <= MAX_GENERATION_RETRIES; attempt++) { RunMapData result = TryGenerate(config); if (result != null) return result; Debug.Log( $"[MapGenerator] 第 {attempt} 次生成失败(特殊节点配额未满足),正在重新生成..."); } Debug.LogWarning( $"[MapGenerator] 连续 {MAX_GENERATION_RETRIES} 次生成均失败,终止地图生成。" + $"请检查 specialNodeCounts / specialNodeMinSameTypeDistance / targetRoomCount 是否过于严格。"); return null; } /// /// 执行单次地图生成。若特殊节点配额无法完全满足则返回 null。 /// private static RunMapData TryGenerate(MapGenerationConfig config) { RunMapData mapData = new RunMapData { nodes = new Dictionary() }; // --- 1. 起点 --- Vector2Int startPos = Vector2Int.zero; mapData.startPosition = startPos; PlaceNode(mapData, startPos, MapNodeType.Start); // --- 2. 扩展房间 --- ExpandRooms(mapData, config); // --- 3. 建立所有相邻节点之间的双向连接 --- ConnectAllAdjacentNodes(mapData); // --- 4. 放置 Boss 节点(最远的死胡同) --- PlaceBossNode(mapData, config); // --- 5. 分配特殊节点类型(配额未满足则本次生成失败)--- if (!AssignSpecialNodes(mapData, config)) return null; // --- 6. 为剩余节点分配 ZoneData --- AssignZoneData(mapData, config); // --- 7. 计算 UI 坐标 --- AssignUIPositions(mapData, config); mapData.totalNodes = mapData.nodes.Count; return mapData; } // ================================================================ // 房间扩展(类以撒算法) // ================================================================ /// /// 从起点开始,使用队列式随机扩展在网格上铺开房间。 /// 每次从已有房间中随机选一个,向随机方向尝试扩展。 /// private static void ExpandRooms(RunMapData mapData, MapGenerationConfig config) { int targetCount = config.targetRoomCount + 1; // +1 包含起点 List existingPositions = new List(mapData.nodes.Keys); int maxAttempts = targetCount * 20; int attempts = 0; while (mapData.nodes.Count < targetCount && attempts < maxAttempts) { attempts++; // 从已有节点中随机选一个作为扩展源 Vector2Int source = existingPositions[Random.Range(0, existingPositions.Count)]; // 随机选择一个方向 Vector2Int dir = Directions[Random.Range(0, Directions.Length)]; Vector2Int candidate = source + dir; // 检查是否已存在、是否在网格范围内 if (mapData.nodes.ContainsKey(candidate)) continue; if (!IsWithinGrid(candidate, config.gridRadius)) continue; // 限制邻居数量,避免地图过于密集(最多 3 个已有邻居) int adjacentCount = CountAdjacentNodes(mapData, candidate); if (adjacentCount > MAX_ADJACENT_FOR_EXPANSION) continue; PlaceNode(mapData, candidate, MapNodeType.NormalCombat); existingPositions.Add(candidate); } if (mapData.nodes.Count < targetCount) { Debug.LogWarning( $"[MapGenerator] 仅生成 {mapData.nodes.Count}/{targetCount} 个房间" + $"(网格半径 {config.gridRadius} 可能过小)。"); } } /// 扩展时允许的最大已有邻居数,避免生成过于密集的块状区域。 private const int MAX_ADJACENT_FOR_EXPANSION = 2; // ================================================================ // 连接建立 // ================================================================ /// /// 遍历所有节点,为每对相邻节点建立双向连接。 /// private static void ConnectAllAdjacentNodes(RunMapData mapData) { foreach (KeyValuePair kvp in mapData.nodes) { Vector2Int pos = kvp.Key; RunMapNode node = kvp.Value; foreach (Vector2Int dir in Directions) { Vector2Int neighborPos = pos + dir; if (mapData.nodes.ContainsKey(neighborPos) && !node.connectedPositions.Contains(neighborPos)) { node.connectedPositions.Add(neighborPos); } } } } // ================================================================ // Boss 放置 // ================================================================ /// /// 将距离起点最远的死胡同节点设为 Boss 节点。 /// 若没有足够远的死胡同,则选距离最远的任意节点。 /// private static void PlaceBossNode(RunMapData mapData, MapGenerationConfig config) { Dictionary distances = ComputeBfsDistances(mapData, mapData.startPosition); // 优先选择满足最小距离的死胡同(度数 == 1) Vector2Int bestPos = mapData.startPosition; int bestDist = -1; foreach (KeyValuePair kvp in distances) { if (kvp.Key == mapData.startPosition) continue; RunMapNode node = mapData.nodes[kvp.Key]; bool isDeadEnd = node.Degree == 1; bool meetsMinDist = kvp.Value >= config.minBossDistance; if (meetsMinDist && isDeadEnd && kvp.Value > bestDist) { bestDist = kvp.Value; bestPos = kvp.Key; } } // 若没找到满足条件的死胡同,退而求其次选最远节点 if (bestDist < 0) { foreach (KeyValuePair kvp in distances) { if (kvp.Key == mapData.startPosition) continue; if (kvp.Value > bestDist) { bestDist = kvp.Value; bestPos = kvp.Key; } } } mapData.nodes[bestPos].nodeType = MapNodeType.BossCombat; mapData.bossPosition = bestPos; } // ================================================================ // 特殊节点分配 // ================================================================ /// /// 按配额分配特殊节点。优先放在死胡同,其次放在低度数节点。 /// 所有特殊节点需满足:与起点的最小距离约束、同类节点间的最小距离约束。 /// /// 所有类型的配额均完全满足时返回 true;任意类型放置数量不足时返回 false。 private static bool AssignSpecialNodes(RunMapData mapData, MapGenerationConfig config) { Dictionary distancesFromStart = ComputeBfsDistances(mapData, mapData.startPosition); // 收集可用候选节点(排除起点和 Boss) List candidates = mapData.nodes.Keys .Where(pos => pos != mapData.startPosition && pos != mapData.bossPosition && distancesFromStart.TryGetValue(pos, out int dist) && dist >= config.specialNodeMinDistanceFromStart) .OrderBy(pos => mapData.nodes[pos].Degree) // 死胡同优先 .ThenByDescending(pos => distancesFromStart[pos]) // 距离远的优先 .ToList(); // 记录每种类型已放置的节点坐标,用于同类距离检查 Dictionary> placedByType = new Dictionary>(); foreach (KeyValuePair quota in config.specialNodeCounts) { MapNodeType nodeType = quota.Key; int targetCount = quota.Value; if (!placedByType.ContainsKey(nodeType)) placedByType[nodeType] = new List(); int placed = 0; List toRemove = new List(); foreach (Vector2Int pos in candidates) { if (placed >= targetCount) break; // 同类距离约束检查 if (!SatisfiesSameTypeDistance(mapData, pos, placedByType[nodeType], config.specialNodeMinSameTypeDistance)) { continue; } mapData.nodes[pos].nodeType = nodeType; placedByType[nodeType].Add(pos); placed++; toRemove.Add(pos); } // 从候选池移除已分配的节点 foreach (Vector2Int pos in toRemove) candidates.Remove(pos); // 配额未满足,本次生成失败 if (placed < targetCount) return false; } return true; } /// /// 检查候选位置与所有已放置的同类节点之间的 BFS 距离是否满足最小距离约束。 /// private static bool SatisfiesSameTypeDistance( RunMapData mapData, Vector2Int candidate, List placedSameType, int minDistance) { if (placedSameType.Count == 0 || minDistance <= 1) return true; // 从候选位置出发做一次 BFS,只需要探索到 minDistance 深度即可 Dictionary distances = ComputeBfsDistancesWithLimit( mapData, candidate, minDistance); foreach (Vector2Int placedPos in placedSameType) { if (distances.TryGetValue(placedPos, out int dist) && dist < minDistance) { return false; } } return true; } /// /// 限深度 BFS,仅探索到 maxDepth 步即停止,用于距离约束检查以减少不必要的遍历。 /// private static Dictionary ComputeBfsDistancesWithLimit( RunMapData mapData, Vector2Int start, int maxDepth) { Dictionary distances = new Dictionary(); Queue queue = new Queue(); distances[start] = 0; queue.Enqueue(start); while (queue.Count > 0) { Vector2Int current = queue.Dequeue(); int currentDist = distances[current]; if (currentDist >= maxDepth) continue; RunMapNode node = mapData.nodes[current]; foreach (Vector2Int neighbor in node.connectedPositions) { if (!distances.ContainsKey(neighbor)) { distances[neighbor] = currentDist + 1; queue.Enqueue(neighbor); } } } return distances; } // ================================================================ // ZoneData 分配 // ================================================================ /// /// 为所有战斗类型节点分配 ZoneData 资产名。 /// private static void AssignZoneData(RunMapData mapData, MapGenerationConfig config) { foreach (RunMapNode node in mapData.nodes.Values) { node.zoneDataAssetName = ResolveZoneDataName(node.nodeType, config); } } /// /// 根据节点类型从配置中随机抽取对应的 ZoneData 资产名。 /// 非战斗类型节点返回空字符串。 /// private static string ResolveZoneDataName(MapNodeType nodeType, MapGenerationConfig config) { switch (nodeType) { case MapNodeType.NormalCombat: return PickRandom(config.normalCombatZoneDataNames); case MapNodeType.EliteCombat: return PickRandom(config.eliteCombatZoneDataNames); case MapNodeType.BossCombat: return config.bossCombatZoneDataName ?? string.Empty; default: return string.Empty; } } // ================================================================ // UI 坐标 // ================================================================ /// /// 根据网格坐标计算每个节点的 UI 位置。 /// private static void AssignUIPositions(RunMapData mapData, MapGenerationConfig config) { foreach (KeyValuePair kvp in mapData.nodes) { kvp.Value.position = new Vector2( kvp.Key.x * config.nodeSpacing, kvp.Key.y * config.nodeSpacing ); } } // ================================================================ // 工具方法 // ================================================================ /// 在指定位置创建并放置一个节点。 private static void PlaceNode(RunMapData mapData, Vector2Int pos, MapNodeType nodeType) { RunMapNode node = new RunMapNode { gridPosition = pos, nodeType = nodeType, position = Vector2.zero, sceneName = string.Empty, zoneDataAssetName = string.Empty, connectedPositions = new List() }; mapData.nodes[pos] = node; } /// 检查坐标是否在网格范围内。 private static bool IsWithinGrid(Vector2Int pos, int gridRadius) { return Mathf.Abs(pos.x) <= gridRadius && Mathf.Abs(pos.y) <= gridRadius; } /// 计算候选位置周围已有节点的数量。 private static int CountAdjacentNodes(RunMapData mapData, Vector2Int pos) { int count = 0; foreach (Vector2Int dir in Directions) { if (mapData.nodes.ContainsKey(pos + dir)) count++; } return count; } /// /// BFS 计算从起点到所有节点的最短步数距离。 /// private static Dictionary ComputeBfsDistances( RunMapData mapData, Vector2Int start) { Dictionary distances = new Dictionary(); Queue queue = new Queue(); distances[start] = 0; queue.Enqueue(start); while (queue.Count > 0) { Vector2Int current = queue.Dequeue(); int currentDist = distances[current]; RunMapNode node = mapData.nodes[current]; foreach (Vector2Int neighbor in node.connectedPositions) { if (!distances.ContainsKey(neighbor)) { distances[neighbor] = currentDist + 1; queue.Enqueue(neighbor); } } } return distances; } /// 从列表中随机取一个元素,列表为空时返回空字符串。 private static string PickRandom(List list) { if (list == null || list.Count == 0) return string.Empty; return list[Random.Range(0, list.Count)]; } } }