Files
XericLibrary-WebGL-Publish/Runtime/Blueprint/GraphTheoryBlueprintComponent.cs
2026-08-10 14:17:25 +08:00

345 lines
11 KiB
C#
Raw Permalink 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 UnityEngine;
using UnityEngine.EventSystems;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
using XericLibrary.Runtime.Blueprint.Canvas;
using XericLibrary.Runtime.Blueprint.Element;
namespace XericLibrary.Runtime.Blueprint
{
/// <summary>
/// 图论蓝图组件:Unity Inspector 配置、画布创建及调试入口。
/// 帧驱动完全由 <see cref="BlueprintGraphSystem"/> 承担。
/// </summary>
[DisallowMultipleComponent]
[ExecuteAlways]
public class GraphTheoryBlueprintComponent : MonoBehaviour
{
[Header("Canvas Settings")]
[Tooltip("勾选则为世界空间画布,否则为屏幕空间画布")]
[SerializeField] private bool _useWorldSpaceCanvas = false;
[Tooltip("世界空间画布的相机引用")]
[SerializeField] private Camera _worldCamera;
[Tooltip("屏幕空间模式实际可见区域;留空时使用蓝图宿主 RectTransform。")]
[SerializeField] private RectTransform _viewportRectTransform;
[Header("Blueprint Context")]
[Tooltip("嵌入式图配置:主题与按 TargetToolType 解析的工具配置资产。")]
[SerializeField] private BlueprintGraphContext _context = new BlueprintGraphContext();
[Header("Input")]
[Tooltip("新输入系统 InputActionAsset。留空则仅支持鼠标基础操作(中键拖动平移、滚轮缩放)。")]
[SerializeField] private Object _inputActions;
public Object InputActions
{
get { return _inputActions; }
set
{
if (_inputActions == value) return;
#if ENABLE_INPUT_SYSTEM
if (Graph != null && _inputActions is InputActionAsset oldAsset)
{
var oldTool = BlueprintToolProvider.GetToolByType<BlueprintInputTool_InputSystem>(Graph);
if (oldTool != null && oldTool.InputActionAsset == oldAsset)
oldTool.InputActionAsset = null;
}
#endif
_inputActions = value;
#if ENABLE_INPUT_SYSTEM
if (Graph != null)
{
var newAsset = value as InputActionAsset;
EnsureEventSystem(newAsset);
var newTool = BlueprintToolProvider.GetToolByType<BlueprintInputTool_InputSystem>(Graph);
if (newTool != null)
newTool.InputActionAsset = newAsset;
}
#endif
}
}
[Header("Test")]
[Tooltip("是否自动生成测试节点")]
[SerializeField] public bool _autoGenerateTestNodes = true;
[Header("Debug")]
[Tooltip("显示所有隐藏的运行时对象(HideFlags),用于调试。默认关闭。")]
[SerializeField] private bool _showDebugObjects = false;
/// <summary>当前蓝图实例</summary>
public BlueprintGraph Graph { get; private set; }
/// <summary>当前画布</summary>
public IBlueprintCanvas Canvas { get; private set; }
public BlueprintGraphContext Context => _context;
public string ThemeName
{
get { return _context?.ThemeName ?? string.Empty; }
set
{
if (_context == null)
_context = new BlueprintGraphContext();
_context.ThemeName = value ?? string.Empty;
Graph?.RequestContextRefresh(_context);
}
}
// ===== 初始化 =====
private void Awake()
{
if (_context == null)
_context = new BlueprintGraphContext();
CreateCanvas();
CreateGraph();
SetupInput();
}
private void CreateCanvas()
{
if (_useWorldSpaceCanvas)
{
Canvas = new WorldSpaceBlueprintCanvas(transform, _worldCamera);
}
else
{
// 1) 找父级 Canvas(不在自身挂 Canvas
var parentCanvas = GetComponentInParent<UnityEngine.Canvas>();
if (parentCanvas == null)
{
// 场景中无 Canvas → 创建全屏 Canvas,将自身移入
var canvasGo = new GameObject("Blueprint Canvas",
typeof(UnityEngine.Canvas),
typeof(UnityEngine.UI.CanvasScaler),
typeof(UnityEngine.UI.GraphicRaycaster));
var cc = canvasGo.GetComponent<UnityEngine.Canvas>();
cc.renderMode = RenderMode.ScreenSpaceOverlay;
canvasGo.transform.SetParent(transform.parent, false);
canvasGo.transform.SetAsLastSibling();
transform.SetParent(canvasGo.transform, false);
// 将组件自身的 RectTransform 撑满整个 Canvas
// 确保 __Bp_RenderRoot 的尺寸与屏幕一致,
// 避免 GetViewportRect() 基于默认 (100x100) 尺寸错误裁剪节点。
var selfRt = GetComponent<RectTransform>();
if (selfRt == null)
selfRt = gameObject.AddComponent<RectTransform>();
selfRt.anchorMin = Vector2.zero;
selfRt.anchorMax = Vector2.one;
selfRt.offsetMin = Vector2.zero;
selfRt.offsetMax = Vector2.zero;
parentCanvas = cc;
}
// 2) 创建渲染根子项(所有蓝图层挂载于此,由外层的 Mask 裁剪)
var renderRootGo = new GameObject("__Bp_RenderRoot",
typeof(RectTransform));
renderRootGo.hideFlags = HideFlags.DontSave;
var renderRootRt = renderRootGo.GetComponent<RectTransform>();
renderRootRt.SetParent(transform, false);
renderRootRt.anchorMin = Vector2.zero;
renderRootRt.anchorMax = Vector2.one;
renderRootRt.offsetMin = Vector2.zero;
renderRootRt.offsetMax = Vector2.zero;
// 3) 以父级 Canvas + 渲染根构建画布适配器;实际视口默认是宿主自身而非 RenderRoot。
var hostRect = GetComponent<RectTransform>();
Canvas = new ScreenSpaceBlueprintCanvas(parentCanvas, renderRootRt,
_viewportRectTransform != null ? _viewportRectTransform : hostRect);
}
}
private void CreateGraph()
{
// System 是唯一帧驱动;Context 在 Initialize/工具构建之前绑定。
Graph = BlueprintGraphSystem.GlobalInstance.CreateGraph(Canvas, _context);
}
private void SetupInput()
{
#if ENABLE_INPUT_SYSTEM
var asset = _inputActions as InputActionAsset;
EnsureEventSystem(asset);
// 将 InputActionAsset 传递给输入工具(由工具自身管理绑定)。
var tool = BlueprintToolProvider.GetToolByType<BlueprintInputTool_InputSystem>(Graph);
if (tool != null)
tool.InputActionAsset = asset;
#endif
}
#if ENABLE_INPUT_SYSTEM
private void EnsureEventSystem(InputActionAsset actionsAsset)
#else
private void EnsureEventSystem(Object _)
#endif
{
var es = FindObjectOfType<EventSystem>();
EventSystem eventSys;
if (es != null)
{
eventSys = es;
}
else
{
var esGo = new GameObject("EventSystem");
eventSys = esGo.AddComponent<EventSystem>();
}
#if ENABLE_INPUT_SYSTEM
var uiModule = eventSys.GetComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>();
if (uiModule == null)
{
var standalone = eventSys.GetComponent<StandaloneInputModule>();
if (standalone != null)
DestroyImmediate(standalone);
uiModule = eventSys.gameObject.AddComponent<UnityEngine.InputSystem.UI.InputSystemUIInputModule>();
}
if (actionsAsset != null)
ConfigureBlueprintUiActions(uiModule, actionsAsset);
else
{
// 未提供蓝图资产时使用 Input System 内置 UI 动作,仍可产生指针事件。
uiModule.actionsAsset = null;
uiModule.AssignDefaultActions();
}
#else
if (eventSys.GetComponent<StandaloneInputModule>() == null)
{
eventSys.gameObject.AddComponent<StandaloneInputModule>();
}
#endif
}
#if ENABLE_INPUT_SYSTEM
/// <summary>
/// 将 Blueprint map 的鼠标动作显式映射到 UI 模块。
/// InputSystemUIInputModule 不会仅凭 actionsAsset 自动推断这些引用。
/// </summary>
private static void ConfigureBlueprintUiActions(
UnityEngine.InputSystem.UI.InputSystemUIInputModule uiModule,
InputActionAsset actionsAsset)
{
var map = actionsAsset.FindActionMap(BlueprintInputConstants.MapName);
var point = map?.FindAction("Point");
var leftClick = map?.FindAction("LeftClick");
var rightClick = map?.FindAction("RightClick");
var middleClick = map?.FindAction("MiddleClick");
var scrollWheel = map?.FindAction("ScrollWheel");
if (point == null || leftClick == null || rightClick == null ||
middleClick == null || scrollWheel == null)
{
uiModule.actionsAsset = null;
uiModule.AssignDefaultActions();
return;
}
uiModule.actionsAsset = actionsAsset;
uiModule.point = InputActionReference.Create(point);
uiModule.leftClick = InputActionReference.Create(leftClick);
uiModule.rightClick = InputActionReference.Create(rightClick);
uiModule.middleClick = InputActionReference.Create(middleClick);
uiModule.scrollWheel = InputActionReference.Create(scrollWheel);
}
#endif
// ===== 调试可见性 =====
private void SyncDebugVisibility()
{
if (Graph == null) return;
var targetFlags = _showDebugObjects
? HideFlags.None | HideFlags.DontSave
: HideFlags.HideAndDontSave;
var root = Graph.Canvas?.GetRootTransform();
if (root == null) return;
for (int i = 0; i < root.childCount; i++)
{
var child = root.GetChild(i);
if (child.name.StartsWith("__BpLayer_") || child.name == "__Bp_GridBackground")
child.gameObject.hideFlags = targetFlags;
}
}
// ===== 测试节点 =====
private void Start()
{
if (_autoGenerateTestNodes)
GenerateTestNodes();
}
// 组件仅承担 Inspector 配置与调试入口;图的 Update/LateUpdate 只由 BlueprintGraphSystem 驱动。
private void OnValidate()
{
if (_context == null)
_context = new BlueprintGraphContext();
Graph?.RequestContextRefresh(_context);
SyncDebugVisibility();
}
// ===== 测试节点生成 =====
public void GenerateTestNodes()
{
if (Graph == null) return;
if (Graph.Nodes.Count > 0)
return;
float startX = 200f;
float centerY = 300f;
float spacing = 200f;
float branchOffset = 200f;
var node1 = new GraphTheoryNode("Node 1", Color.red);
node1.Position = new Vector2(startX, centerY);
var node2 = new GraphTheoryNode("Node 2", Color.green);
node2.Position = new Vector2(startX + spacing, centerY);
var node3 = new GraphTheoryNode("Node 3", Color.blue);
node3.Position = new Vector2(startX + spacing * 2, centerY);
Graph.AddNode(node1);
Graph.AddNode(node2);
Graph.AddNode(node3);
Graph.TryConnectPorts(node1.OutputPorts[0], node2.InputPorts[0]);
Graph.TryConnectPorts(node2.OutputPorts[0], node3.InputPorts[0]);
var node4 = new GraphTheoryNode("Node 4", Color.yellow);
node4.Position = new Vector2(startX + spacing * 2, centerY + branchOffset);
var node5 = new GraphTheoryNode("Node 5", Color.cyan);
node5.Position = new Vector2(startX + spacing * 2, centerY - branchOffset);
Graph.AddNode(node4);
Graph.AddNode(node5);
Graph.TryConnectPorts(node2.OutputPorts[0], node4.InputPorts[0]);
Graph.TryConnectPorts(node2.OutputPorts[0], node5.InputPorts[0]);
}
// ===== 清理 =====
private void OnDestroy()
{
if (Graph != null)
{
var system = BlueprintGraphSystem.LazyInstance;
system?.DestroyGraph(Graph);
Graph = null;
}
}
}
}