地图初步

This commit is contained in:
SoulliesOfficial
2026-04-30 07:06:38 -04:00
parent 8ad26129b2
commit 47125f95f4
98 changed files with 2237 additions and 20524 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 97d3a4f682d0e3249b7dd2ab98f42943
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 343955b29f79f424d866df1483cdef22

View 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;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3fc5f071bddbe134ab39c8fdf1f21bc3

View 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)];
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 07f5101bb66f25042a8cec2b0b5e7c86

View 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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5969481483110a649a81f36c8c205fb3

View 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
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1909a1a45fa45594e94a89aa02e3dfe2

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d1db6362878a52c4196ad6df9a373b11
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bc9f3701417662c4789e8d8ef866a7e5

View 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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 228f5129b3e4fab4b9738801fd21d9e7

View 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);
}
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ef86af874de4c0842925ab52256e256e

View File

@@ -0,0 +1,10 @@
using RootMotion;
using UnityEngine;
namespace Cielonos.MainGame
{
public class RunManager : Singleton<RunManager>
{
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8222c6b4127177848ab9c9e8fdc0650d

View 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; // 节点总数
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cc9a61a4bf301a349bb971b3362a79f8

View 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; // 是否通关
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 83f1dcc80a3c27c4a9340d8cb779c9c7