地图初步
This commit is contained in:
8
Assets/Scripts/MainGame/GameRun/Map.meta
Normal file
8
Assets/Scripts/MainGame/GameRun/Map.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97d3a4f682d0e3249b7dd2ab98f42943
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Assets/Scripts/MainGame/GameRun/Map/MapBaseCollection.cs
Normal file
33
Assets/Scripts/MainGame/GameRun/Map/MapBaseCollection.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cielonos.Core;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Map
|
||||
{
|
||||
[CreateAssetMenu(fileName = "MapBaseCollection", menuName = "Cielonos/BaseCollections/MainGameBaseCollection")]
|
||||
public partial class MapBaseCollection : BaseCollection<MapBaseCollection>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public partial class MapBaseCollection
|
||||
{
|
||||
[Title("Spawn Points")]
|
||||
[Tooltip("不同组别对应的颜色,用于编辑器显示。注意,只需要SpawnPoint的groupName包含该字符串即可匹配。")]
|
||||
public Dictionary<string, Color> groupColors = new Dictionary<string, Color>();
|
||||
|
||||
public Color GetGroupColor(string groupName)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(groupName))
|
||||
{
|
||||
foreach (var pair in groupColors.Where(pair => groupName.Contains(pair.Key)))
|
||||
{
|
||||
return pair.Value;
|
||||
}
|
||||
}
|
||||
return Color.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 343955b29f79f424d866df1483cdef22
|
||||
72
Assets/Scripts/MainGame/GameRun/Map/MapGenerationConfig.cs
Normal file
72
Assets/Scripts/MainGame/GameRun/Map/MapGenerationConfig.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Map
|
||||
{
|
||||
[CreateAssetMenu(fileName = "MapGenerationConfig", menuName = "Cielonos/Map/MapGenerationConfig")]
|
||||
public class MapGenerationConfig : SerializedScriptableObject
|
||||
{
|
||||
// ----------------------------------------------------------------
|
||||
// 地图结构
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
[Title("地图结构")]
|
||||
[Tooltip("网格的半径大小(实际网格为 (2*gridRadius+1) x (2*gridRadius+1))")]
|
||||
[MinValue(3), MaxValue(10)]
|
||||
public int gridRadius = 5;
|
||||
|
||||
[Tooltip("期望生成的房间总数(不含起点和 Boss)")]
|
||||
[MinValue(8), MaxValue(80)]
|
||||
public int targetRoomCount = 20;
|
||||
|
||||
[Tooltip("Boss 节点与起点之间的最小距离(曼哈顿距离)")]
|
||||
[MinValue(3)]
|
||||
public int minBossDistance = 5;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 特殊节点数量配额
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
[Title("特殊节点配额")]
|
||||
[Tooltip("每种特殊节点类型在整张地图中的数量上限")]
|
||||
[DictionaryDrawerSettings(KeyLabel = "节点类型", ValueLabel = "数量")]
|
||||
public Dictionary<MapNodeType, int> specialNodeCounts = new Dictionary<MapNodeType, int>
|
||||
{
|
||||
{ MapNodeType.EliteCombat, 3 },
|
||||
{ MapNodeType.MechanicalTable, 2 },
|
||||
{ MapNodeType.LogisticsCenter, 2 },
|
||||
{ MapNodeType.MedicalStation, 2 },
|
||||
};
|
||||
|
||||
[Tooltip("特殊节点与起点之间的最小距离(BFS 步数),避免起点附近出现商店等")]
|
||||
[MinValue(1)]
|
||||
public int specialNodeMinDistanceFromStart = 2;
|
||||
|
||||
[Tooltip("同类特殊节点之间的最小距离(BFS 步数),防止同类型节点扎堆")]
|
||||
[MinValue(1)]
|
||||
public int specialNodeMinSameTypeDistance = 1;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// ZoneData 资产名配置
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
[Title("ZoneData 资产名配置")]
|
||||
[Tooltip("可用的普通战斗 ZoneData 资产名列表(随机抽取分配给 NormalCombat 节点)")]
|
||||
public List<string> normalCombatZoneDataNames = new List<string>();
|
||||
|
||||
[Tooltip("可用的精英战斗 ZoneData 资产名列表(随机抽取分配给 EliteCombat 节点)")]
|
||||
public List<string> eliteCombatZoneDataNames = new List<string>();
|
||||
|
||||
[Tooltip("Boss 战斗 ZoneData 资产名(固定分配给 Boss 节点)")]
|
||||
public string bossCombatZoneDataName;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// UI 布局参数
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
[Title("UI 布局参数")]
|
||||
[Tooltip("节点之间的间距(用于 UI 坐标计算)")]
|
||||
public float nodeSpacing = 120f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3fc5f071bddbe134ab39c8fdf1f21bc3
|
||||
465
Assets/Scripts/MainGame/GameRun/Map/MapGenerator.cs
Normal file
465
Assets/Scripts/MainGame/GameRun/Map/MapGenerator.cs
Normal file
@@ -0,0 +1,465 @@
|
||||
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)];
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/GameRun/Map/MapGenerator.cs.meta
Normal file
2
Assets/Scripts/MainGame/GameRun/Map/MapGenerator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07f5101bb66f25042a8cec2b0b5e7c86
|
||||
90
Assets/Scripts/MainGame/GameRun/Map/MapManager.cs
Normal file
90
Assets/Scripts/MainGame/GameRun/Map/MapManager.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.Core.UI;
|
||||
using Cielonos.MainGame.Map;
|
||||
using DG.Tweening;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Cielonos.MainGame
|
||||
{
|
||||
public partial class MapManager : Singleton<MapManager>
|
||||
{
|
||||
public MapBaseCollection baseCollection;
|
||||
|
||||
public ZoneData currentZoneData;
|
||||
public ZoneManager currentZoneManager;
|
||||
public int zoneIndex;
|
||||
public List<ZoneData> zoneDataList;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
zoneIndex = 0;
|
||||
StartCoroutine(SwitchRoomRoutine(zoneDataList[zoneIndex++]));
|
||||
}
|
||||
|
||||
public void GoToNextZone()
|
||||
{
|
||||
if (zoneIndex >= zoneDataList.Count)
|
||||
{
|
||||
Debug.Log("No more zones to load.");
|
||||
return;
|
||||
}
|
||||
|
||||
ScreenFader.Instance.FadeToBlack(onComplete : () => StartCoroutine(SwitchRoomRoutine(zoneDataList[zoneIndex++]))).Play();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class MapManager
|
||||
{
|
||||
public static MapBaseCollection BaseCollection => Instance.baseCollection;
|
||||
}
|
||||
|
||||
public partial class MapManager
|
||||
{
|
||||
public IEnumerator SwitchRoomRoutine(ZoneData nextZoneData) {
|
||||
// 1. 发送转场开始事件 (UI显示遮挡,特效触发)
|
||||
//EventManager.Trigger("OnTeleportStart");
|
||||
|
||||
// 2. 异步卸载旧场景
|
||||
if (currentZoneData != null) {
|
||||
yield return SceneManager.UnloadSceneAsync(currentZoneData.sceneName);
|
||||
}
|
||||
|
||||
currentZoneData = nextZoneData;
|
||||
|
||||
// 3. 异步加载新场景
|
||||
string nextSceneName = nextZoneData.sceneName;
|
||||
AsyncOperation op = SceneManager.LoadSceneAsync(nextSceneName, LoadSceneMode.Additive);
|
||||
op.allowSceneActivation = false; // 先不激活,等加载到90%
|
||||
|
||||
while (op.progress < 0.9f) {
|
||||
yield return null;
|
||||
}
|
||||
|
||||
op.allowSceneActivation = true; // 允许激活
|
||||
yield return op; // 等待激活完成
|
||||
|
||||
// 4. 设置为活跃场景 (非常重要!)
|
||||
Scene nextScene = SceneManager.GetSceneByName(nextSceneName);
|
||||
SceneManager.SetActiveScene(nextScene);
|
||||
|
||||
// 5. 将玩家放到出生点
|
||||
//Player.Instance.TeleportTo(FindObjectOfType<SpawnPoint>().transform.position);
|
||||
|
||||
yield return new WaitForEndOfFrame();
|
||||
|
||||
BattleManager.AttackAreaSm.Reset();
|
||||
BattleManager.EnemySm.Reset();
|
||||
MainGameManager.Player.transform.position = Vector3.zero;
|
||||
ZoneManager.instance.SetupZone(currentZoneData);
|
||||
// 6. 发送转场结束事件 (特效消失,UI关闭)
|
||||
//EventManager.Trigger("OnTeleportEnd");
|
||||
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
ScreenFader.Instance.FadeToClear().Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/GameRun/Map/MapManager.cs.meta
Normal file
2
Assets/Scripts/MainGame/GameRun/Map/MapManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5969481483110a649a81f36c8c205fb3
|
||||
396
Assets/Scripts/MainGame/GameRun/Map/MapPreviewer.cs
Normal file
396
Assets/Scripts/MainGame/GameRun/Map/MapPreviewer.cs
Normal file
@@ -0,0 +1,396 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cielonos.MainGame.Map;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using Sirenix.Utilities.Editor;
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace Cielonos.MainGame.Map
|
||||
{
|
||||
/// <summary>
|
||||
/// 地图预览器,在 Inspector 中可视化生成并展示全联通网格地图。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "MapPreviewer", menuName = "Cielonos/Map/MapPreviewer")]
|
||||
public class MapPreviewer : SerializedScriptableObject
|
||||
{
|
||||
// ----------------------------------------------------------------
|
||||
// 配置
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
[Title("配置")]
|
||||
[Required("需要指定 MapGenerationConfig")]
|
||||
public MapGenerationConfig config;
|
||||
|
||||
[Title("预览外观")]
|
||||
[Range(300f, 1000f)]
|
||||
public float previewSize = 600f;
|
||||
|
||||
[MinValue(6f), MaxValue(24f)]
|
||||
public float nodeRadius = 12f;
|
||||
|
||||
[Range(1f, 6f)]
|
||||
public float edgeThickness = 2.5f;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 节点颜色映射
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
[Title("节点颜色")]
|
||||
public Color colorStart = new Color(0.4f, 0.9f, 0.4f);
|
||||
public Color colorNormalCombat = new Color(0.85f, 0.35f, 0.35f);
|
||||
public Color colorEliteCombat = new Color(0.9f, 0.55f, 0.15f);
|
||||
public Color colorBossCombat = new Color(0.7f, 0.15f, 0.85f);
|
||||
public Color colorMechanicalTable = new Color(0.3f, 0.7f, 1.0f);
|
||||
public Color colorLogisticsCenter = new Color(1.0f, 0.85f, 0.2f);
|
||||
public Color colorMedicalStation = new Color(0.3f, 0.85f, 0.6f);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 运行时缓存(仅 Editor)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
[HideInInspector]
|
||||
public RunMapData previewData;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 按钮
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
[Title("操作")]
|
||||
[Button("生成随机地图", ButtonSizes.Large), GUIColor(0.4f, 0.85f, 0.5f)]
|
||||
public void GeneratePreview()
|
||||
{
|
||||
if (config == null)
|
||||
{
|
||||
Debug.LogWarning("[MapPreviewer] 请先指定 MapGenerationConfig。");
|
||||
return;
|
||||
}
|
||||
|
||||
previewData = MapGenerator.Generate(config);
|
||||
EditorUtility.SetDirty(this);
|
||||
}
|
||||
|
||||
[Button("清空预览", ButtonSizes.Medium), GUIColor(0.85f, 0.4f, 0.4f)]
|
||||
public void ClearPreview()
|
||||
{
|
||||
previewData = null;
|
||||
EditorUtility.SetDirty(this);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Inspector 绘制
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
[OnInspectorGUI]
|
||||
private void DrawMapCanvas()
|
||||
{
|
||||
if (previewData == null || previewData.nodes == null || previewData.nodes.Count == 0)
|
||||
{
|
||||
SirenixEditorGUI.InfoMessageBox("尚未生成地图,请点击「生成随机地图」按钮。");
|
||||
return;
|
||||
}
|
||||
|
||||
GUILayout.Space(8f);
|
||||
SirenixEditorGUI.Title("地图预览", $"共 {previewData.totalNodes} 个房间", TextAlignment.Center, horizontalLine: true);
|
||||
GUILayout.Space(4f);
|
||||
|
||||
// 预留正方形画布
|
||||
Rect canvasRect = GUILayoutUtility.GetRect(
|
||||
GUIContent.none,
|
||||
GUIStyle.none,
|
||||
GUILayout.ExpandWidth(true),
|
||||
GUILayout.Height(previewSize)
|
||||
);
|
||||
|
||||
// 深色背景
|
||||
SirenixEditorGUI.DrawSolidRect(canvasRect, new Color(0.12f, 0.12f, 0.15f, 1f));
|
||||
|
||||
// 计算屏幕坐标
|
||||
Dictionary<Vector2Int, Vector2> screenPositions = ComputeScreenPositions(canvasRect);
|
||||
|
||||
// 绘制连线(先画)
|
||||
DrawEdges(screenPositions);
|
||||
|
||||
// 绘制节点
|
||||
DrawNodes(screenPositions);
|
||||
|
||||
// 图例
|
||||
DrawLegend(canvasRect);
|
||||
|
||||
// 统计信息
|
||||
DrawStats(canvasRect);
|
||||
|
||||
GUILayout.Space(4f);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 坐标映射
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
private Dictionary<Vector2Int, Vector2> ComputeScreenPositions(Rect canvas)
|
||||
{
|
||||
Dictionary<Vector2Int, Vector2> result = new Dictionary<Vector2Int, Vector2>();
|
||||
|
||||
if (previewData.nodes.Count == 0) return result;
|
||||
|
||||
// 找出网格坐标的边界
|
||||
int minX = int.MaxValue, maxX = int.MinValue;
|
||||
int minY = int.MaxValue, maxY = int.MinValue;
|
||||
|
||||
foreach (Vector2Int pos in previewData.nodes.Keys)
|
||||
{
|
||||
if (pos.x < minX) minX = pos.x;
|
||||
if (pos.x > maxX) maxX = pos.x;
|
||||
if (pos.y < minY) minY = pos.y;
|
||||
if (pos.y > maxY) maxY = pos.y;
|
||||
}
|
||||
|
||||
int gridWidth = Mathf.Max(maxX - minX, 1);
|
||||
int gridHeight = Mathf.Max(maxY - minY, 1);
|
||||
|
||||
const float paddingRatio = 0.08f;
|
||||
float padX = canvas.width * paddingRatio;
|
||||
float padY = canvas.height * paddingRatio;
|
||||
|
||||
float cellW = (canvas.width - padX * 2f) / gridWidth;
|
||||
float cellH = (canvas.height - padY * 2f) / gridHeight;
|
||||
float cellSize = Mathf.Min(cellW, cellH);
|
||||
|
||||
// 居中偏移
|
||||
float totalW = gridWidth * cellSize;
|
||||
float totalH = gridHeight * cellSize;
|
||||
float offsetX = canvas.x + (canvas.width - totalW) * 0.5f;
|
||||
float offsetY = canvas.y + (canvas.height - totalH) * 0.5f;
|
||||
|
||||
foreach (Vector2Int pos in previewData.nodes.Keys)
|
||||
{
|
||||
float sx = offsetX + (pos.x - minX) * cellSize;
|
||||
// Y 轴翻转:网格 Y 越大 → 画布 Y 越小(向上)
|
||||
float sy = offsetY + (maxY - pos.y) * cellSize;
|
||||
result[pos] = new Vector2(sx, sy);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 连线绘制
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
private void DrawEdges(Dictionary<Vector2Int, Vector2> positions)
|
||||
{
|
||||
if (Event.current.type != EventType.Repaint) return;
|
||||
|
||||
Color edgeColor = new Color(0.45f, 0.45f, 0.55f, 0.7f);
|
||||
HashSet<(Vector2Int, Vector2Int)> drawnEdges = new HashSet<(Vector2Int, Vector2Int)>();
|
||||
|
||||
foreach (KeyValuePair<Vector2Int, RunMapNode> kvp in previewData.nodes)
|
||||
{
|
||||
Vector2Int fromPos = kvp.Key;
|
||||
if (!positions.TryGetValue(fromPos, out Vector2 fromScreen)) continue;
|
||||
|
||||
foreach (Vector2Int toPos in kvp.Value.connectedPositions)
|
||||
{
|
||||
// 避免双向连接绘制两次
|
||||
var edgeKey = fromPos.x < toPos.x || (fromPos.x == toPos.x && fromPos.y < toPos.y)
|
||||
? (fromPos, toPos)
|
||||
: (toPos, fromPos);
|
||||
|
||||
if (drawnEdges.Contains(edgeKey)) continue;
|
||||
drawnEdges.Add(edgeKey);
|
||||
|
||||
if (!positions.TryGetValue(toPos, out Vector2 toScreen)) continue;
|
||||
DrawThickLine(fromScreen, toScreen, edgeColor, edgeThickness);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawThickLine(Vector2 from, Vector2 to, Color color, float thickness)
|
||||
{
|
||||
Handles.color = color;
|
||||
Vector2 dir = (to - from).normalized;
|
||||
Vector2 perp = new Vector2(-dir.y, dir.x);
|
||||
|
||||
int steps = Mathf.Max(1, Mathf.RoundToInt(thickness));
|
||||
float halfT = (steps - 1) * 0.5f;
|
||||
|
||||
for (int i = 0; i < steps; i++)
|
||||
{
|
||||
float offset = (i - halfT) * 0.8f;
|
||||
Vector3 f3 = new Vector3(from.x + perp.x * offset, from.y + perp.y * offset, 0f);
|
||||
Vector3 t3 = new Vector3(to.x + perp.x * offset, to.y + perp.y * offset, 0f);
|
||||
Handles.DrawLine(f3, t3);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 节点绘制
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
private void DrawNodes(Dictionary<Vector2Int, Vector2> positions)
|
||||
{
|
||||
if (Event.current.type != EventType.Repaint) return;
|
||||
|
||||
foreach (KeyValuePair<Vector2Int, RunMapNode> kvp in previewData.nodes)
|
||||
{
|
||||
if (!positions.TryGetValue(kvp.Key, out Vector2 center)) continue;
|
||||
|
||||
RunMapNode node = kvp.Value;
|
||||
Color nodeColor = GetNodeColor(node.nodeType);
|
||||
|
||||
// 外圈(深色边框)
|
||||
float outerR = nodeRadius + 2f;
|
||||
Handles.color = new Color(0.05f, 0.05f, 0.05f, 0.9f);
|
||||
Handles.DrawSolidDisc(new Vector3(center.x, center.y, 0f), Vector3.forward, outerR);
|
||||
|
||||
// 节点填充
|
||||
Handles.color = nodeColor;
|
||||
Handles.DrawSolidDisc(new Vector3(center.x, center.y, 0f), Vector3.forward, nodeRadius);
|
||||
|
||||
// 节点标签
|
||||
DrawNodeLabel(node, center);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawNodeLabel(RunMapNode node, Vector2 center)
|
||||
{
|
||||
string label = GetNodeShortLabel(node.nodeType);
|
||||
GUIStyle style = new GUIStyle(EditorStyles.miniLabel)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontStyle = FontStyle.Bold,
|
||||
fontSize = 9,
|
||||
normal = { textColor = Color.white }
|
||||
};
|
||||
|
||||
float labelW = 48f;
|
||||
float labelH = 16f;
|
||||
Rect labelRect = new Rect(center.x - labelW * 0.5f, center.y + nodeRadius + 2f, labelW, labelH);
|
||||
GUI.Label(labelRect, label, style);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 图例
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
private void DrawLegend(Rect canvas)
|
||||
{
|
||||
if (Event.current.type != EventType.Repaint) return;
|
||||
|
||||
var entries = new (MapNodeType type, string label)[]
|
||||
{
|
||||
(MapNodeType.Start, "起点"),
|
||||
(MapNodeType.NormalCombat, "普通战斗"),
|
||||
(MapNodeType.EliteCombat, "精英战斗"),
|
||||
(MapNodeType.BossCombat, "Boss"),
|
||||
(MapNodeType.MechanicalTable, "机械台"),
|
||||
(MapNodeType.LogisticsCenter, "物流中心"),
|
||||
(MapNodeType.MedicalStation, "医疗站"),
|
||||
};
|
||||
|
||||
const float dotSize = 10f;
|
||||
const float itemH = 16f;
|
||||
const float itemW = 90f;
|
||||
const float padLeft = 8f;
|
||||
const float padBottom = 8f;
|
||||
|
||||
float totalH = entries.Length * itemH + 4f;
|
||||
float startY = canvas.yMax - padBottom - totalH;
|
||||
|
||||
Rect bgRect = new Rect(canvas.x + padLeft - 4f, startY - 2f, itemW + 8f, totalH + 4f);
|
||||
SirenixEditorGUI.DrawSolidRect(bgRect, new Color(0f, 0f, 0f, 0.45f));
|
||||
|
||||
GUIStyle labelStyle = new GUIStyle(EditorStyles.miniLabel)
|
||||
{
|
||||
normal = { textColor = new Color(0.85f, 0.85f, 0.85f) }
|
||||
};
|
||||
|
||||
for (int i = 0; i < entries.Length; i++)
|
||||
{
|
||||
float y = startY + i * itemH;
|
||||
|
||||
Handles.color = GetNodeColor(entries[i].type);
|
||||
Handles.DrawSolidDisc(
|
||||
new Vector3(canvas.x + padLeft + dotSize * 0.5f, y + itemH * 0.5f, 0f),
|
||||
Vector3.forward, dotSize * 0.5f);
|
||||
|
||||
Rect textRect = new Rect(canvas.x + padLeft + dotSize + 4f, y, itemW - dotSize - 4f, itemH);
|
||||
GUI.Label(textRect, entries[i].label, labelStyle);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 统计信息
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
private void DrawStats(Rect canvas)
|
||||
{
|
||||
if (Event.current.type != EventType.Repaint) return;
|
||||
|
||||
// 统计各类型数量
|
||||
Dictionary<MapNodeType, int> counts = new Dictionary<MapNodeType, int>();
|
||||
foreach (RunMapNode node in previewData.nodes.Values)
|
||||
{
|
||||
if (!counts.ContainsKey(node.nodeType))
|
||||
counts[node.nodeType] = 0;
|
||||
counts[node.nodeType]++;
|
||||
}
|
||||
|
||||
string stats = string.Join(" | ", counts
|
||||
.OrderBy(kvp => kvp.Key)
|
||||
.Select(kvp => $"{GetNodeShortLabel(kvp.Key)}: {kvp.Value}"));
|
||||
|
||||
GUIStyle style = new GUIStyle(EditorStyles.miniLabel)
|
||||
{
|
||||
alignment = TextAnchor.MiddleRight,
|
||||
normal = { textColor = new Color(0.7f, 0.7f, 0.7f) }
|
||||
};
|
||||
|
||||
const float padRight = 8f;
|
||||
const float padTop = 6f;
|
||||
Rect statsRect = new Rect(canvas.x, canvas.y + padTop, canvas.width - padRight, 16f);
|
||||
GUI.Label(statsRect, stats, style);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 工具方法
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
private Color GetNodeColor(MapNodeType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MapNodeType.Start => colorStart,
|
||||
MapNodeType.NormalCombat => colorNormalCombat,
|
||||
MapNodeType.EliteCombat => colorEliteCombat,
|
||||
MapNodeType.BossCombat => colorBossCombat,
|
||||
MapNodeType.MechanicalTable => colorMechanicalTable,
|
||||
MapNodeType.LogisticsCenter => colorLogisticsCenter,
|
||||
MapNodeType.MedicalStation => colorMedicalStation,
|
||||
_ => Color.white,
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetNodeShortLabel(MapNodeType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MapNodeType.Start => "Start",
|
||||
MapNodeType.NormalCombat => "Combat",
|
||||
MapNodeType.EliteCombat => "Elite",
|
||||
MapNodeType.BossCombat => "BOSS",
|
||||
MapNodeType.MechanicalTable => "Chest",
|
||||
MapNodeType.LogisticsCenter => "Shop",
|
||||
MapNodeType.MedicalStation => "Med",
|
||||
_ => "?",
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/GameRun/Map/MapPreviewer.cs.meta
Normal file
2
Assets/Scripts/MainGame/GameRun/Map/MapPreviewer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1909a1a45fa45594e94a89aa02e3dfe2
|
||||
8
Assets/Scripts/MainGame/GameRun/Map/Zone.meta
Normal file
8
Assets/Scripts/MainGame/GameRun/Map/Zone.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1db6362878a52c4196ad6df9a373b11
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
85
Assets/Scripts/MainGame/GameRun/Map/Zone/SpawnPoint.cs
Normal file
85
Assets/Scripts/MainGame/GameRun/Map/Zone/SpawnPoint.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using Sirenix.Utilities.Editor;
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace Cielonos.MainGame.Map
|
||||
{
|
||||
public partial class SpawnPoint : MonoBehaviour
|
||||
{
|
||||
[HorizontalGroup("SpawnPointInfo", 0.5f)]
|
||||
[LabelText("Identification")]
|
||||
public string groupName = "Default";
|
||||
|
||||
[HorizontalGroup("SpawnPointInfo", 0.2f, MarginLeft = 0.05f), ReadOnly, HideLabel]
|
||||
[SerializeField] private int index = -1;
|
||||
public int Index => index;
|
||||
|
||||
[HorizontalGroup("SpawnPointInfo", 0.1f, MarginLeft = 0.05f), Button(Icon = SdfIconType.Command), HideLabel]
|
||||
public void Refresh()
|
||||
{
|
||||
ZoneManager.Instance.RebuildMapData();
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
Color color = Color.white;
|
||||
if (MapBaseCollection.Instance != null)
|
||||
{
|
||||
color = MapBaseCollection.Instance.GetGroupColor(groupName);
|
||||
}
|
||||
|
||||
Gizmos.color = color;
|
||||
Gizmos.DrawSphere(transform.position, 0.25f);
|
||||
|
||||
//Draw Direction
|
||||
Vector3 forward = transform.forward * 0.25f;
|
||||
Gizmos.DrawCube(transform.position + forward, new Vector3(0.1f, 0.1f, 0.25f));
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Draw text label using Handles - automatically faces camera
|
||||
string text = $"{groupName}_{index}";
|
||||
|
||||
// Create a custom style if needed, or use default skin
|
||||
GUIStyle style = new GUIStyle();
|
||||
style.normal.textColor = color;
|
||||
|
||||
float distance = Vector3.Distance(SceneView.lastActiveSceneView.camera.transform.position, transform.position);
|
||||
|
||||
if (distance > 100f)
|
||||
{
|
||||
return; // 太远了不显示
|
||||
}
|
||||
|
||||
float sizeFactor = Mathf.Clamp(100 / distance, 0.2f, 1f);
|
||||
style.fontSize = Mathf.RoundToInt(10 * sizeFactor);
|
||||
style.fontStyle = FontStyle.Bold;
|
||||
style.alignment = TextAnchor.MiddleCenter;
|
||||
Handles.Label(transform.position + Vector3.up * 0.5f, text, style);
|
||||
#endif
|
||||
}
|
||||
|
||||
// 被 Manager 调用
|
||||
public void SetData(int newIndex)
|
||||
{
|
||||
index = newIndex;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public partial class SpawnPoint
|
||||
{
|
||||
public void GetTransform(out Vector3 position, out Quaternion rotation)
|
||||
{
|
||||
position = transform.position;
|
||||
rotation = transform.rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc9f3701417662c4789e8d8ef866a7e5
|
||||
384
Assets/Scripts/MainGame/GameRun/Map/Zone/ZoneData.cs
Normal file
384
Assets/Scripts/MainGame/GameRun/Map/Zone/ZoneData.cs
Normal file
@@ -0,0 +1,384 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Sirenix.OdinInspector;
|
||||
using Sirenix.Utilities;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using Sirenix.OdinInspector.Editor;
|
||||
using Sirenix.Utilities.Editor;
|
||||
#endif
|
||||
|
||||
namespace Cielonos.MainGame.Map
|
||||
{
|
||||
[CreateAssetMenu(fileName = "ZoneData", menuName = "Cielonos/MainGame/Map/ZoneData", order = 1)]
|
||||
public partial class ZoneData : SerializedScriptableObject
|
||||
{
|
||||
[InlineProperty]
|
||||
public struct SpawnPointKey : System.IEquatable<SpawnPointKey>
|
||||
{
|
||||
[HorizontalGroup("Row")] [LabelText("Spawn Point")] [LabelWidth(90)]
|
||||
public string group;
|
||||
|
||||
[HorizontalGroup("Row", Width = 50, MarginLeft = 10)] [HideLabel]
|
||||
public int index;
|
||||
|
||||
public SpawnPointKey(string group, int index)
|
||||
{
|
||||
this.group = group;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public bool Equals(SpawnPointKey other)
|
||||
{
|
||||
return group == other.group && index == other.index;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is SpawnPointKey other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return ((group != null ? group.GetHashCode() : 0) * 397) ^ index;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool operator ==(SpawnPointKey left, SpawnPointKey right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(SpawnPointKey left, SpawnPointKey right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
|
||||
public override string ToString() => $"{group}_{index}";
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
[InlineProperty]
|
||||
[HideReferenceObjectPicker]
|
||||
public class EnemySpawnMapping
|
||||
{
|
||||
[HideInInspector] [System.NonSerialized]
|
||||
public ZoneData parent;
|
||||
|
||||
[FormerlySerializedAs("Group")] [HorizontalGroup("SpawnInfo", 0.25f), HideLabel] [ReadOnly]
|
||||
public string group;
|
||||
|
||||
[FormerlySerializedAs("Index")] [HorizontalGroup("SpawnInfo", 0.15f), HideLabel] [ReadOnly]
|
||||
public int index;
|
||||
|
||||
[FormerlySerializedAs("EnemyID")]
|
||||
[HorizontalGroup("SpawnInfo", 0.5f), HideLabel]
|
||||
[ValueDropdown("GetEnemyKeys")]
|
||||
[OnValueChanged("OnEnemyIDChanged")]
|
||||
public string enemyID;
|
||||
|
||||
public EnemySpawnMapping()
|
||||
{
|
||||
}
|
||||
|
||||
public EnemySpawnMapping(SpawnPointKey key, string val, ZoneData parent)
|
||||
{
|
||||
group = key.group;
|
||||
index = key.index;
|
||||
enemyID = val;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnEnemyIDChanged()
|
||||
{
|
||||
if (parent != null)
|
||||
{
|
||||
parent.UpdateDictionaryEntry(group, index, enemyID);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
private IEnumerable<string> GetEnemyKeys()
|
||||
{
|
||||
return MainGameBaseCollection.Instance.enemiesCollection.Keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ZoneData
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
private static void AddButtonOnTitleBarGUI(Action buttonAction, EditorIcon icon = null)
|
||||
{
|
||||
icon ??= EditorIcons.MagnifyingGlass;
|
||||
if (SirenixEditorGUI.ToolbarButton(icon))
|
||||
{
|
||||
buttonAction?.Invoke();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public partial class ZoneData
|
||||
{
|
||||
[Title("Scene Info")]
|
||||
[ValueDropdown("GetAllSceneNames"), PropertyOrder(-1)]
|
||||
[InfoBox("当前场景和ZoneData的关联场景不匹配!", InfoMessageType.Error, VisibleIf = "@!IsCurrentSceneMatch")]
|
||||
public string sceneName;
|
||||
|
||||
public bool IsCurrentSceneMatch => UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == sceneName;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private List<string> GetAllSceneNames()
|
||||
{
|
||||
List<string> sceneNames = new List<string>();
|
||||
foreach (var scene in EditorBuildSettings.scenes)
|
||||
{
|
||||
if (scene.enabled)
|
||||
{
|
||||
string name = System.IO.Path.GetFileNameWithoutExtension(scene.path);
|
||||
sceneNames.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return sceneNames;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
public partial class ZoneData
|
||||
{
|
||||
private void CollectSpawnPoints(List<SpawnPointKey> list, string groupName)
|
||||
{
|
||||
ZoneManager zoneManager = ZoneManager.Instance;
|
||||
zoneManager.RebuildMapData();
|
||||
list.Clear();
|
||||
if (zoneManager.spawnPoints.TryGetValue(groupName, out List<SpawnPoint> points))
|
||||
{
|
||||
for (int i = 0; i < points.Count; i++)
|
||||
{
|
||||
list.Add(new SpawnPointKey(groupName, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ZoneData
|
||||
{
|
||||
[Title("Spawn Settings")]
|
||||
[Title("Player", HorizontalLine = false, Bold = false)]
|
||||
[ListDrawerSettings(ShowIndexLabels = false, AddCopiesLastElement = true), PropertyOrder(10), LabelText("Player Spawns")]
|
||||
public List<SpawnPointKey> playerSpawns = new List<SpawnPointKey>() { new SpawnPointKey("Player", 0) };
|
||||
}
|
||||
|
||||
public partial class ZoneData
|
||||
{
|
||||
[SerializeField, HideInInspector] public Dictionary<SpawnPointKey, string> enemySpawns = new Dictionary<SpawnPointKey, string>();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[HideInInspector] private List<string> selectedGroupsToSync = new List<string>();
|
||||
|
||||
[Title("Enemy", HorizontalLine = false)]
|
||||
[ShowInInspector]
|
||||
[ListDrawerSettings(
|
||||
OnTitleBarGUI = "DrawEnemySpawnTitleBarGUI",
|
||||
ShowFoldout = true,
|
||||
CustomRemoveIndexFunction = "RemoveEnemySpawnsIndex",
|
||||
CustomRemoveElementFunction = "RemoveEnemySpawnsElement")]
|
||||
[LabelText("Enemy Spawns"), PropertyOrder(12)]
|
||||
private List<EnemySpawnMapping> EnemySpawnsInEditor
|
||||
{
|
||||
get => GetSortedMappings();
|
||||
set
|
||||
{
|
||||
enemySpawns = new Dictionary<SpawnPointKey, string>();
|
||||
foreach (EnemySpawnMapping item in value)
|
||||
{
|
||||
enemySpawns[new SpawnPointKey(item.group, item.index)] = item.enemyID;
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnemySyncSettingsChanged()
|
||||
{
|
||||
ZoneManager zoneManager = ZoneManager.Instance;
|
||||
zoneManager.RebuildMapData();
|
||||
Dictionary<SpawnPointKey, string> newDict = new Dictionary<SpawnPointKey, string>();
|
||||
foreach (string group in selectedGroupsToSync)
|
||||
{
|
||||
if (zoneManager.spawnPoints.TryGetValue(group, out List<SpawnPoint> points))
|
||||
{
|
||||
for (int i = 0; i < points.Count; i++)
|
||||
{
|
||||
SpawnPointKey key = new SpawnPointKey(group, i);
|
||||
newDict[key] = enemySpawns != null ? enemySpawns.GetValueOrDefault(key, "") : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enemySpawns = newDict;
|
||||
EditorUtility.SetDirty(this);
|
||||
}
|
||||
|
||||
private List<EnemySpawnMapping> GetSortedMappings()
|
||||
{
|
||||
List<EnemySpawnMapping> list = new List<EnemySpawnMapping>();
|
||||
if (enemySpawns == null) return list;
|
||||
|
||||
list.AddRange(enemySpawns.Select(kvp => new EnemySpawnMapping(kvp.Key, kvp.Value, this)));
|
||||
list.Sort((a, b) =>
|
||||
{
|
||||
int comp = string.Compare(a.group, b.group, System.StringComparison.Ordinal);
|
||||
if (comp != 0) return comp;
|
||||
return a.index.CompareTo(b.index);
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private void RemoveEnemySpawnsElement(EnemySpawnMapping element)
|
||||
{
|
||||
SpawnPointKey key = new SpawnPointKey(element.group, element.index);
|
||||
if (enemySpawns.Remove(key))
|
||||
{
|
||||
EditorUtility.SetDirty(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveEnemySpawnsIndex(int index)
|
||||
{
|
||||
var list = GetSortedMappings();
|
||||
if (index >= 0 && index < list.Count)
|
||||
{
|
||||
EnemySpawnMapping item = list[index];
|
||||
SpawnPointKey key = new SpawnPointKey(item.group, item.index);
|
||||
if (enemySpawns.Remove(key))
|
||||
{
|
||||
EditorUtility.SetDirty(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDictionaryEntry(string group, int index, string newEnemyID)
|
||||
{
|
||||
SpawnPointKey key = new SpawnPointKey(group, index);
|
||||
if (enemySpawns.ContainsKey(key))
|
||||
{
|
||||
enemySpawns[key] = newEnemyID;
|
||||
EditorUtility.SetDirty(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawEnemySpawnTitleBarGUI() => AddButtonOnTitleBarGUI(OpenEnemySyncSelector);
|
||||
|
||||
private void OpenEnemySyncSelector()
|
||||
{
|
||||
ZoneManager zoneManager = ZoneManager.Instance;
|
||||
zoneManager.RebuildMapData();
|
||||
GroupSyncPopup popup = new GroupSyncPopup(zoneManager.spawnPoints.Keys, selectedGroupsToSync, (result) =>
|
||||
{
|
||||
selectedGroupsToSync = result;
|
||||
OnEnemySyncSettingsChanged();
|
||||
});
|
||||
|
||||
OdinEditorWindow window = OdinEditorWindow.InspectObject(popup);
|
||||
float width = 400;
|
||||
float height = 200;
|
||||
var centerRect = GUIHelper.GetEditorWindowRect().AlignCenter(width, height);
|
||||
window.position = centerRect;
|
||||
window.ShowPopup();
|
||||
}
|
||||
|
||||
private class GroupSyncPopup
|
||||
{
|
||||
[LabelText("Available Groups")]
|
||||
[ListDrawerSettings(IsReadOnly = true, ShowIndexLabels = false, HideAddButton = true, HideRemoveButton = true)]
|
||||
[TableList(AlwaysExpanded = true, HideToolbar = true)]
|
||||
[ShowInInspector, PropertyOrder(1)]
|
||||
public List<GroupItem> toggles = new List<GroupItem>();
|
||||
|
||||
private readonly System.Action<List<string>> onConfirm;
|
||||
|
||||
public GroupSyncPopup(IEnumerable<string> available, List<string> current, System.Action<List<string>> onConfirm)
|
||||
{
|
||||
this.onConfirm = onConfirm;
|
||||
foreach (string name in available)
|
||||
{
|
||||
toggles.Add(new GroupItem(name, current.Contains(name)));
|
||||
}
|
||||
}
|
||||
|
||||
[Button, PropertyOrder(2)]
|
||||
public void Confirm()
|
||||
{
|
||||
List<string> result = toggles.Where(t => t.sync).Select(t => t.name).ToList();
|
||||
this.onConfirm?.Invoke(result);
|
||||
GUIHelper.CurrentWindow?.Close();
|
||||
}
|
||||
|
||||
public class GroupItem
|
||||
{
|
||||
[HideLabel, TableColumnWidth(30, Resizable = false)]
|
||||
public bool sync;
|
||||
|
||||
[HideLabel, ReadOnly] public string name;
|
||||
|
||||
public GroupItem(string name, bool sync)
|
||||
{
|
||||
this.name = name;
|
||||
this.sync = sync;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public partial class ZoneData
|
||||
{
|
||||
[Title("Specified Locations", HorizontalLine = false)]
|
||||
[PropertyOrder(20)]
|
||||
[LabelText("Exit Spawns")]
|
||||
[ListDrawerSettings(ShowIndexLabels = false, OnTitleBarGUI = "DrawExitSpawnsTitleBarGUI")]
|
||||
public List<SpawnPointKey> exitSpawns = new List<SpawnPointKey>() { new SpawnPointKey("Exit", 0) };
|
||||
#if UNITY_EDITOR
|
||||
private void DrawExitSpawnsTitleBarGUI() =>
|
||||
AddButtonOnTitleBarGUI(() => CollectSpawnPoints(exitSpawns, "Exit"), EditorIcons.Refresh);
|
||||
#endif
|
||||
|
||||
[PropertySpace]
|
||||
[PropertyOrder(21)]
|
||||
[LabelText("Tech Center Spawns")]
|
||||
[ListDrawerSettings(ShowIndexLabels = false, OnTitleBarGUI = "DrawTechCenterSpawnsTitleBarGUI")]
|
||||
public List<SpawnPointKey> techCenterSpawns = new List<SpawnPointKey>();
|
||||
#if UNITY_EDITOR
|
||||
private void DrawTechCenterSpawnsTitleBarGUI() =>
|
||||
AddButtonOnTitleBarGUI(() => CollectSpawnPoints(techCenterSpawns, "TechCenter"), EditorIcons.Refresh);
|
||||
#endif
|
||||
}
|
||||
|
||||
public partial class ZoneData
|
||||
{
|
||||
[Title("Random Locations", HorizontalLine = false)]
|
||||
[ListDrawerSettings(ShowIndexLabels = false, AddCopiesLastElement = true), PropertyOrder(50), LabelText("Random Location Spawns")]
|
||||
public List<SpawnPointKey> randomLocationSpawns = new List<SpawnPointKey>();
|
||||
|
||||
[ValueDropdown("GetLocationIDs"), PropertyOrder(51), LabelText("Available Random Locations")]
|
||||
public List<string> availableRandomLocations = new List<string>();
|
||||
|
||||
public List<string> GetLocationIDs()
|
||||
{
|
||||
return new List<string>() { "Factory", "Warehouse", "Office", "Laboratory", "Hangar", "Storage" }
|
||||
.Exclude(availableRandomLocations).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 228f5129b3e4fab4b9738801fd21d9e7
|
||||
84
Assets/Scripts/MainGame/GameRun/Map/Zone/ZoneManager.cs
Normal file
84
Assets/Scripts/MainGame/GameRun/Map/Zone/ZoneManager.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Map
|
||||
{
|
||||
public partial class ZoneManager : Singleton<ZoneManager>
|
||||
{
|
||||
[Title("Editor Tools")]
|
||||
[Button("Rebuild Spawn Points"), PropertyOrder(-100)]
|
||||
public void RebuildMapData()
|
||||
{
|
||||
if(spawnPointContainer == null)
|
||||
{
|
||||
Debug.LogWarning("[ZoneManager] Spawn Point Container is not assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
spawnPoints.Clear();
|
||||
|
||||
// 1. 查找场景中所有生成点
|
||||
SpawnPoint[] allPoints = spawnPointContainer.GetComponentsInChildren<SpawnPoint>(true);
|
||||
|
||||
// 2. 按 groupName 分组,并按 Hierarchy 顺序排序 (保证 Index 确定性)
|
||||
var grouped = allPoints
|
||||
.GroupBy(p => p.groupName)
|
||||
.OrderBy(g => g.Key);
|
||||
|
||||
foreach (var group in grouped)
|
||||
{
|
||||
// 按在 Hierarchy 中的顺序排序列表
|
||||
var list = group.OrderBy(p => p.transform.GetSiblingIndex()).ToList();
|
||||
spawnPoints[group.Key] = list;
|
||||
|
||||
// 3. 将计算好的 Index 写入到具体物体上(持久化)
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
list[i].SetData(i); // 调用子物体的方法设置数据
|
||||
}
|
||||
}
|
||||
|
||||
//Debug.Log($"[ZoneManager] Rebuilt map: Found {allPoints.Length} points in {spawnPoints.Count} groups.");
|
||||
}
|
||||
}
|
||||
|
||||
// 继承 Singleton 保持运行时单例特性
|
||||
public partial class ZoneManager
|
||||
{
|
||||
// 运行时查找用的字典
|
||||
[Title("Runtime Data")]
|
||||
[Required]
|
||||
public GameObject spawnPointContainer;
|
||||
public Dictionary<string, List<SpawnPoint>> spawnPoints = new Dictionary<string, List<SpawnPoint>>();
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
RebuildMapData();
|
||||
}
|
||||
|
||||
public void SetupZone(ZoneData data)
|
||||
{
|
||||
if (data.enemySpawns != null)
|
||||
{
|
||||
foreach (KeyValuePair<ZoneData.SpawnPointKey, string> enemySpawn in data.enemySpawns)
|
||||
{
|
||||
string group = enemySpawn.Key.group;
|
||||
int index = enemySpawn.Key.index;
|
||||
|
||||
if (spawnPoints.TryGetValue(group, out List<SpawnPoint> points))
|
||||
{
|
||||
SpawnPoint point = points[index];
|
||||
point.GetTransform(out Vector3 position, out Quaternion rotation);
|
||||
GameObject enemyPrefab = MainGameBaseCollection.Instance.enemiesCollection[enemySpawn.Value];
|
||||
Instantiate(enemyPrefab, position, rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef86af874de4c0842925ab52256e256e
|
||||
10
Assets/Scripts/MainGame/GameRun/RunManager.cs
Normal file
10
Assets/Scripts/MainGame/GameRun/RunManager.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using RootMotion;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame
|
||||
{
|
||||
public class RunManager : Singleton<RunManager>
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/GameRun/RunManager.cs.meta
Normal file
2
Assets/Scripts/MainGame/GameRun/RunManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8222c6b4127177848ab9c9e8fdc0650d
|
||||
42
Assets/Scripts/MainGame/GameRun/RunMapData.cs
Normal file
42
Assets/Scripts/MainGame/GameRun/RunMapData.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame
|
||||
{
|
||||
[Serializable]
|
||||
public class RunMapNode
|
||||
{
|
||||
public Vector2Int gridPosition; // 节点在网格中的坐标
|
||||
public MapNodeType nodeType;
|
||||
public Vector2 position; // UI 定位用
|
||||
public string sceneName; // 对应的 ZoneData.sceneName(战斗节点)
|
||||
public string zoneDataAssetName; // 关联的 ZoneData 资产名
|
||||
public List<Vector2Int> connectedPositions; // 双向连接的相邻节点坐标列表
|
||||
|
||||
/// <summary>
|
||||
/// 该节点的连接数(度数)。度数为 1 的节点为"死胡同"。
|
||||
/// </summary>
|
||||
public int Degree => connectedPositions?.Count ?? 0;
|
||||
}
|
||||
|
||||
public enum MapNodeType
|
||||
{
|
||||
Start, // 起始节点
|
||||
NormalCombat, // 普通战斗
|
||||
EliteCombat, // 精英战斗(更强的敌人配置)
|
||||
BossCombat, // Boss 战
|
||||
MechanicalTable, // 机械台(宝箱房,可获取装备)
|
||||
LogisticsCenter, // 物流中心(商店)
|
||||
MedicalStation, // 医疗站点(恢复HP)
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class RunMapData
|
||||
{
|
||||
public Dictionary<Vector2Int, RunMapNode> nodes; // 所有节点,按网格坐标索引
|
||||
public Vector2Int startPosition; // 起始节点坐标
|
||||
public Vector2Int bossPosition; // Boss 节点坐标
|
||||
public int totalNodes; // 节点总数
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/GameRun/RunMapData.cs.meta
Normal file
2
Assets/Scripts/MainGame/GameRun/RunMapData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc9a61a4bf301a349bb971b3362a79f8
|
||||
18
Assets/Scripts/MainGame/GameRun/RunState.cs
Normal file
18
Assets/Scripts/MainGame/GameRun/RunState.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame
|
||||
{
|
||||
[Serializable]
|
||||
public class RunState
|
||||
{
|
||||
public RunMapData mapData; // 本局生成的地图
|
||||
public Vector2Int currentPosition; // 当前所在节点的网格坐标
|
||||
public HashSet<Vector2Int> visitedNodes; // 已访问节点坐标集合
|
||||
public float elapsedTime; // 本局已用时间
|
||||
public int roomsCleared; // 已清理房间数
|
||||
public int enemiesDefeated; // 已击杀敌人数
|
||||
public bool isCompleted; // 是否通关
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MainGame/GameRun/RunState.cs.meta
Normal file
2
Assets/Scripts/MainGame/GameRun/RunState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83f1dcc80a3c27c4a9340d8cb779c9c7
|
||||
Reference in New Issue
Block a user