Files
Cielonos/Assets/Scripts/MainGame/GameRun/Map/MapGenerator.cs
SoulliesOfficial 47125f95f4 地图初步
2026-04-30 07:06:38 -04:00

466 lines
18 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 System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Cielonos.MainGame.Map
{
/// <summary>
/// 全联通地图生成器,生成类似"以撒的结合"/"挺进地牢"的 2D 网格地图。
/// 所有节点双向互通,玩家可以自由走遍地图中的每一个房间。
/// </summary>
public static class MapGenerator
{
/// <summary>网格四方向偏移。</summary>
private static readonly Vector2Int[] Directions =
{
Vector2Int.up,
Vector2Int.down,
Vector2Int.left,
Vector2Int.right
};
/// <summary>生成失败时允许的最大重试次数。</summary>
private const int MAX_GENERATION_RETRIES = 10;
/// <summary>
/// 根据配置生成一份完整的全联通 RunMapData。
/// 若特殊节点配额无法满足,则重新生成,最多重试 <see cref="MAX_GENERATION_RETRIES"/> 次。
/// 所有重试均失败时返回 null 并输出警告。
/// </summary>
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;
}
/// <summary>
/// 执行单次地图生成。若特殊节点配额无法完全满足则返回 null。
/// </summary>
private static RunMapData TryGenerate(MapGenerationConfig config)
{
RunMapData mapData = new RunMapData
{
nodes = new Dictionary<Vector2Int, RunMapNode>()
};
// --- 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;
}
// ================================================================
// 房间扩展(类以撒算法)
// ================================================================
/// <summary>
/// 从起点开始,使用队列式随机扩展在网格上铺开房间。
/// 每次从已有房间中随机选一个,向随机方向尝试扩展。
/// </summary>
private static void ExpandRooms(RunMapData mapData, MapGenerationConfig config)
{
int targetCount = config.targetRoomCount + 1; // +1 包含起点
List<Vector2Int> existingPositions = new List<Vector2Int>(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} 可能过小)。");
}
}
/// <summary>扩展时允许的最大已有邻居数,避免生成过于密集的块状区域。</summary>
private const int MAX_ADJACENT_FOR_EXPANSION = 2;
// ================================================================
// 连接建立
// ================================================================
/// <summary>
/// 遍历所有节点,为每对相邻节点建立双向连接。
/// </summary>
private static void ConnectAllAdjacentNodes(RunMapData mapData)
{
foreach (KeyValuePair<Vector2Int, RunMapNode> 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 放置
// ================================================================
/// <summary>
/// 将距离起点最远的死胡同节点设为 Boss 节点。
/// 若没有足够远的死胡同,则选距离最远的任意节点。
/// </summary>
private static void PlaceBossNode(RunMapData mapData, MapGenerationConfig config)
{
Dictionary<Vector2Int, int> distances = ComputeBfsDistances(mapData, mapData.startPosition);
// 优先选择满足最小距离的死胡同(度数 == 1
Vector2Int bestPos = mapData.startPosition;
int bestDist = -1;
foreach (KeyValuePair<Vector2Int, int> 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<Vector2Int, int> 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;
}
// ================================================================
// 特殊节点分配
// ================================================================
/// <summary>
/// 按配额分配特殊节点。优先放在死胡同,其次放在低度数节点。
/// 所有特殊节点需满足:与起点的最小距离约束、同类节点间的最小距离约束。
/// </summary>
/// <returns>所有类型的配额均完全满足时返回 true任意类型放置数量不足时返回 false。</returns>
private static bool AssignSpecialNodes(RunMapData mapData, MapGenerationConfig config)
{
Dictionary<Vector2Int, int> distancesFromStart =
ComputeBfsDistances(mapData, mapData.startPosition);
// 收集可用候选节点(排除起点和 Boss
List<Vector2Int> 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<MapNodeType, List<Vector2Int>> placedByType =
new Dictionary<MapNodeType, List<Vector2Int>>();
foreach (KeyValuePair<MapNodeType, int> quota in config.specialNodeCounts)
{
MapNodeType nodeType = quota.Key;
int targetCount = quota.Value;
if (!placedByType.ContainsKey(nodeType))
placedByType[nodeType] = new List<Vector2Int>();
int placed = 0;
List<Vector2Int> toRemove = new List<Vector2Int>();
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;
}
/// <summary>
/// 检查候选位置与所有已放置的同类节点之间的 BFS 距离是否满足最小距离约束。
/// </summary>
private static bool SatisfiesSameTypeDistance(
RunMapData mapData,
Vector2Int candidate,
List<Vector2Int> placedSameType,
int minDistance)
{
if (placedSameType.Count == 0 || minDistance <= 1) return true;
// 从候选位置出发做一次 BFS只需要探索到 minDistance 深度即可
Dictionary<Vector2Int, int> distances = ComputeBfsDistancesWithLimit(
mapData, candidate, minDistance);
foreach (Vector2Int placedPos in placedSameType)
{
if (distances.TryGetValue(placedPos, out int dist) && dist < minDistance)
{
return false;
}
}
return true;
}
/// <summary>
/// 限深度 BFS仅探索到 maxDepth 步即停止,用于距离约束检查以减少不必要的遍历。
/// </summary>
private static Dictionary<Vector2Int, int> ComputeBfsDistancesWithLimit(
RunMapData mapData, Vector2Int start, int maxDepth)
{
Dictionary<Vector2Int, int> distances = new Dictionary<Vector2Int, int>();
Queue<Vector2Int> queue = new Queue<Vector2Int>();
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 分配
// ================================================================
/// <summary>
/// 为所有战斗类型节点分配 ZoneData 资产名。
/// </summary>
private static void AssignZoneData(RunMapData mapData, MapGenerationConfig config)
{
foreach (RunMapNode node in mapData.nodes.Values)
{
node.zoneDataAssetName = ResolveZoneDataName(node.nodeType, config);
}
}
/// <summary>
/// 根据节点类型从配置中随机抽取对应的 ZoneData 资产名。
/// 非战斗类型节点返回空字符串。
/// </summary>
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 坐标
// ================================================================
/// <summary>
/// 根据网格坐标计算每个节点的 UI 位置。
/// </summary>
private static void AssignUIPositions(RunMapData mapData, MapGenerationConfig config)
{
foreach (KeyValuePair<Vector2Int, RunMapNode> kvp in mapData.nodes)
{
kvp.Value.position = new Vector2(
kvp.Key.x * config.nodeSpacing,
kvp.Key.y * config.nodeSpacing
);
}
}
// ================================================================
// 工具方法
// ================================================================
/// <summary>在指定位置创建并放置一个节点。</summary>
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<Vector2Int>()
};
mapData.nodes[pos] = node;
}
/// <summary>检查坐标是否在网格范围内。</summary>
private static bool IsWithinGrid(Vector2Int pos, int gridRadius)
{
return Mathf.Abs(pos.x) <= gridRadius && Mathf.Abs(pos.y) <= gridRadius;
}
/// <summary>计算候选位置周围已有节点的数量。</summary>
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;
}
/// <summary>
/// BFS 计算从起点到所有节点的最短步数距离。
/// </summary>
private static Dictionary<Vector2Int, int> ComputeBfsDistances(
RunMapData mapData, Vector2Int start)
{
Dictionary<Vector2Int, int> distances = new Dictionary<Vector2Int, int>();
Queue<Vector2Int> queue = new Queue<Vector2Int>();
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;
}
/// <summary>从列表中随机取一个元素,列表为空时返回空字符串。</summary>
private static string PickRandom(List<string> list)
{
if (list == null || list.Count == 0) return string.Empty;
return list[Random.Range(0, list.Count)];
}
}
}