Files
Cielonos/Assets/Scripts/MainGame/UI/PlayerUI/MainGamePages/Map/MapUIPage.cs
SoulliesOfficial 649b7a5ddc 更新
2026-05-23 08:27:50 -04:00

424 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using Cielonos.MainGame.Map;
using SLSUtilities.UI;
using UnityEngine;
using UnityEngine.UI.Extensions;
namespace Cielonos.MainGame.UI
{
/// <summary>
/// 地图 UI 页面。根据 RunMapData 动态生成节点和连接线元素。
/// 监听 RunManager 阶段变化,自动在 MapSelection 阶段打开/刷新,其他阶段关闭。
/// 节点点击后调用 RunManager.SelectNode 执行传送。
/// 支持通过 MapPanZoom 组件进行拖拽平移和滚轮缩放。
/// </summary>
public class MapUIPage : UIPageBase
{
// ================================================================
// 常量
// ================================================================
private const float CONNECTION_LINE_THICKNESS = 3f;
private static readonly Color CONNECTION_LINE_COLOR = new Color(0.5f, 0.5f, 0.5f, 0.8f);
// ================================================================
// 序列化字段
// ================================================================
[Header("Containers")]
public RectTransform mapContainer;
[SerializeField] private RectTransform nodeContainer;
[SerializeField] private RectTransform connectionContainer;
[Header("Prefabs")]
[SerializeField] private GameObject nodePrefab;
[Header("Pan & Zoom")]
[SerializeField] private MapPanZoom panZoom;
// ================================================================
// 运行时状态
// ================================================================
/// <summary>已生成的节点元素,按网格坐标索引。</summary>
private readonly Dictionary<Vector2Int, MapNodeElement> _nodeElements =
new Dictionary<Vector2Int, MapNodeElement>();
/// <summary>已生成的连接线 GameObject 列表。</summary>
private readonly List<GameObject> _connectionObjects = new List<GameObject>();
/// <summary>当前绑定的地图数据。</summary>
private RunMapData _mapData;
// ================================================================
// 生命周期
// ================================================================
protected override void Start()
{
base.Start();
// 订阅 RunManager 阶段切换事件,自动控制地图页面的开关
if (RunManager.Instance != null)
{
RunManager.Instance.OnPhaseChanged += OnRunPhaseChanged;
}
else
{
Debug.LogWarning("[MapUIPage] RunManager.Instance 不存在,无法自动切换地图页面。");
}
}
private void OnDestroy()
{
// 取消订阅,防止 RunManager 被先销毁时的空引用
if (RunManager.Instance != null)
{
RunManager.Instance.OnPhaseChanged -= OnRunPhaseChanged;
}
}
// ================================================================
// 阶段切换响应
// ================================================================
/// <summary>
/// RunManager 阶段切换回调。
/// 离开 MapSelection 阶段时自动关闭地图;打开由玩家手动操作(地图按钮)。
/// </summary>
private void OnRunPhaseChanged(RunPhase phase)
{
if (phase != RunPhase.MapSelection && IsOpen)
{
Close();
}
}
/// <summary>
/// 页面打开时自动刷新节点显示状态并居中到当前节点。
/// </summary>
protected override void OnPageOpened()
{
base.OnPageOpened();
if (RunManager.Instance != null && RunManager.Instance.currentRun != null)
{
RefreshDisplayStates(RunManager.Instance.currentRun);
CenterOnCurrentNode();
}
}
// ================================================================
// 公共 API
// ================================================================
/// <summary>
/// 根据 RunMapData 生成所有地图 UI 元素(节点 + 连接线)。
/// 若之前已生成则先清除。生成完成后自动应用初始显示状态。
/// </summary>
/// <param name="mapData">要显示的地图数据。</param>
public void Populate(RunMapData mapData)
{
if (mapData == null)
{
Debug.LogWarning("[MapUIPage] 传入的 RunMapData 为 null无法生成地图。");
return;
}
_mapData = mapData;
ClearAll();
CreateNodes(mapData);
CreateConnections(mapData);
// 生成完成后立即应用初始显示状态(避免短暂显示原始外观)
if (RunManager.Instance != null && RunManager.Instance.currentRun != null)
{
RefreshDisplayStates(RunManager.Instance.currentRun);
}
}
/// <summary>
/// 根据 RunState 更新所有节点的显示状态(迷雾、交互、使用状态、边框颜色)。
/// </summary>
/// <param name="state">当前 Run 的状态。</param>
public void RefreshDisplayStates(RunState state)
{
Dictionary<Vector2Int, NodeDisplayState> displayStates = MapFogCalculator.Calculate(state);
foreach (KeyValuePair<Vector2Int, NodeDisplayState> kvp in displayStates)
{
if (_nodeElements.TryGetValue(kvp.Key, out MapNodeElement element))
{
element.ApplyDisplayState(kvp.Value);
}
}
// 更新连接线的可见性(所有连接线始终可见,因为所有节点至少显示为 Silhouette
RefreshConnectionVisibility(displayStates);
}
// ================================================================
// 节点点击处理
// ================================================================
/// <summary>
/// 节点被玩家点击后的回调。调用 RunManager.SelectNode 执行传送逻辑。
/// RunManager 内部会验证可达性并处理阶段切换和场景加载。
/// </summary>
/// <param name="gridPosition">被点击节点的网格坐标。</param>
private void OnNodeClicked(Vector2Int gridPosition)
{
if (RunManager.Instance == null)
{
Debug.LogWarning("[MapUIPage] RunManager 不存在,无法处理节点点击。");
return;
}
//Debug.Log($"[MapUIPage] 玩家点击节点 {gridPosition},请求传送。");
RunManager.Instance.SelectNode(gridPosition);
}
// ================================================================
// 视角控制
// ================================================================
/// <summary>
/// 将地图视角居中到玩家当前所在节点的 UI 位置。
/// </summary>
private void CenterOnCurrentNode()
{
if (panZoom == null || _mapData == null) return;
if (RunManager.Instance == null || RunManager.Instance.currentRun == null) return;
Vector2Int currentPos = RunManager.Instance.currentRun.currentPosition;
if (_mapData.nodes.TryGetValue(currentPos, out RunMapNode node))
{
panZoom.CenterOn(node.position);
}
}
// ================================================================
// 节点生成
// ================================================================
/// <summary>
/// 为每个 RunMapNode 实例化一个节点 UI 元素,并绑定点击事件。
/// </summary>
private void CreateNodes(RunMapData mapData)
{
if (nodePrefab == null)
{
Debug.LogError("[MapUIPage] nodePrefab 未赋值,无法生成节点。");
return;
}
Transform parent = nodeContainer != null ? nodeContainer : mapContainer;
foreach (KeyValuePair<Vector2Int, RunMapNode> kvp in mapData.nodes)
{
RunMapNode node = kvp.Value;
GameObject nodeGo = Instantiate(nodePrefab, parent);
nodeGo.name = $"Node_{node.gridPosition.x}_{node.gridPosition.y}";
MapNodeElement element = nodeGo.GetComponent<MapNodeElement>();
if (element == null)
{
Debug.LogWarning($"[MapUIPage] nodePrefab 缺少 MapNodeElement 组件,跳过节点 {node.gridPosition}。");
Destroy(nodeGo);
continue;
}
element.Setup(node);
// 绑定点击事件 → OnNodeClicked → RunManager.SelectNode
element.OnClicked += OnNodeClicked;
_nodeElements[kvp.Key] = element;
}
}
// ================================================================
// 连接线生成
// ================================================================
/// <summary>
/// 为所有节点之间的连接创建 UILineRenderer。
/// 使用 HashSet 去重,确保双向连接只生成一条线。
/// </summary>
private void CreateConnections(RunMapData mapData)
{
Transform parent = connectionContainer != null ? connectionContainer : mapContainer;
HashSet<long> processedEdges = new HashSet<long>();
foreach (KeyValuePair<Vector2Int, RunMapNode> kvp in mapData.nodes)
{
Vector2Int fromPos = kvp.Key;
RunMapNode fromNode = kvp.Value;
foreach (Vector2Int toPos in fromNode.connectedPositions)
{
// 使用排序后的 hash 去重双向边
long edgeKey = GetEdgeKey(fromPos, toPos);
if (!processedEdges.Add(edgeKey)) continue;
// 确保目标节点存在
if (!mapData.nodes.TryGetValue(toPos, out RunMapNode toNode)) continue;
CreateConnectionLine(parent, fromNode.position, toNode.position, fromPos, toPos);
}
}
}
/// <summary>
/// 创建一条从 startPos 到 endPos 的 UILineRenderer 连接线。
/// </summary>
private void CreateConnectionLine(Transform parent, Vector2 startUIPos, Vector2 endUIPos,
Vector2Int fromGrid, Vector2Int toGrid)
{
GameObject lineGo = new GameObject($"Connection_{fromGrid.x},{fromGrid.y}_to_{toGrid.x},{toGrid.y}");
lineGo.transform.SetParent(parent, false);
RectTransform rt = lineGo.AddComponent<RectTransform>();
// 锚定到容器中心,不拉伸
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
rt.anchoredPosition = Vector2.zero;
rt.sizeDelta = mapContainer != null ? mapContainer.sizeDelta : new Vector2(1200f, 675f);
UILineRenderer lineRenderer = lineGo.AddComponent<UILineRenderer>();
lineRenderer.LineThickness = CONNECTION_LINE_THICKNESS;
lineRenderer.color = CONNECTION_LINE_COLOR;
lineRenderer.raycastTarget = false;
// UILineRenderer 使用相对于自身 RectTransform 的坐标
// 由于 RectTransform 的 pivot 在 (0.5, 0.5),节点的 position 是相对于 mapContainer 中心的坐标
lineRenderer.Points = new Vector2[] { startUIPos, endUIPos };
_connectionObjects.Add(lineGo);
}
/// <summary>
/// 生成一条边的唯一标识 key与方向无关
/// </summary>
private static long GetEdgeKey(Vector2Int a, Vector2Int b)
{
// 确保小的坐标在前,保证 (a→b) 和 (b→a) 生成相同的 key
Vector2Int min, max;
if (a.x < b.x || (a.x == b.x && a.y < b.y))
{
min = a;
max = b;
}
else
{
min = b;
max = a;
}
// 将两个 Vector2Int 打包为 long
// 每个坐标分量占 16 位,总共 64 位
return ((long)(min.x + 32768) << 48)
| ((long)(min.y + 32768) << 32)
| ((long)(max.x + 32768) << 16)
| (long)(max.y + 32768);
}
// ================================================================
// 连接线可见性
// ================================================================
/// <summary>
/// 根据节点的显示状态更新连接线的可见性。
/// 两端节点都隐藏时,连接线也隐藏。
/// </summary>
private void RefreshConnectionVisibility(Dictionary<Vector2Int, NodeDisplayState> displayStates)
{
foreach (GameObject connectionGo in _connectionObjects)
{
if (connectionGo == null) continue;
// 从命名中解析连接的两个节点坐标
if (!TryParseConnectionName(connectionGo.name, out Vector2Int fromPos, out Vector2Int toPos))
continue;
bool fromVisible = displayStates.TryGetValue(fromPos, out NodeDisplayState fromState)
&& fromState.visibility != MapNodeVisibility.Hidden;
bool toVisible = displayStates.TryGetValue(toPos, out NodeDisplayState toState)
&& toState.visibility != MapNodeVisibility.Hidden;
connectionGo.SetActive(fromVisible || toVisible);
}
}
/// <summary>
/// 从连接线 GameObject 名称中解析两端节点坐标。
/// 命名格式Connection_{x1},{y1}_to_{x2},{y2}
/// </summary>
private static bool TryParseConnectionName(string name, out Vector2Int from, out Vector2Int to)
{
from = Vector2Int.zero;
to = Vector2Int.zero;
// Connection_{x1},{y1}_to_{x2},{y2}
if (!name.StartsWith("Connection_")) return false;
string body = name.Substring("Connection_".Length);
string[] parts = body.Split(new[] { "_to_" }, System.StringSplitOptions.None);
if (parts.Length != 2) return false;
return TryParseCoord(parts[0], out from) && TryParseCoord(parts[1], out to);
}
private static bool TryParseCoord(string s, out Vector2Int result)
{
result = Vector2Int.zero;
string[] components = s.Split(',');
if (components.Length != 2) return false;
if (!int.TryParse(components[0], out int x)) return false;
if (!int.TryParse(components[1], out int y)) return false;
result = new Vector2Int(x, y);
return true;
}
// ================================================================
// 清理
// ================================================================
/// <summary>
/// 清除所有已生成的节点和连接线。
/// </summary>
private void ClearAll()
{
ClearNodes();
ClearConnections();
}
private void ClearNodes()
{
foreach (KeyValuePair<Vector2Int, MapNodeElement> kvp in _nodeElements)
{
if (kvp.Value != null)
{
// 取消订阅点击事件,防止内存泄漏
kvp.Value.OnClicked -= OnNodeClicked;
Destroy(kvp.Value.gameObject);
}
}
_nodeElements.Clear();
}
private void ClearConnections()
{
foreach (GameObject go in _connectionObjects)
{
if (go != null) Destroy(go);
}
_connectionObjects.Clear();
}
}
}