
读完本节,请自觉掌握以下内容[自动狗头]:
如果读完还没掌握,请接着读xNode的第二篇
Unity可视化脚本之——xNode【2】官方wiki文档试读
【1】下图为节点图的构成
【2】节点基类实现
using System.Collections; using System.Collections.Generic; using UnityEngine; using XNode; ////// 定义一个自己的节点class,在原有Node class 的基础上,增加了虚方法:Start() 、 Update() 、TestNode()。 /// 设计的初衷:因为Node Class中不能使用monobehaviour的Start和Update,因为一个节点已经继承了Node Class(它的基类是 ScriptableObject),所以不能再继承MonoBehaviour Class /// 【注意】Start和Update这两个方法与Monobehavior中的Start和Update行为类似,但它是一个自定义的虚方法,子节点中如果用到,需要重写。 /// myNode.Start() ——用于节点的初始化,图加载的时候调用,调用的入口在SceneGraphManager的Start方法里,SceneGraphManager是一个monobehavior脚本 /// myNode.Update() ——用于节点的每帧更新,调用的入口在SceneGraphManager的Update方法里 /// myNode.TestNode()——编辑器的playing模式下,测试节点的功能 /// /// 最后修改日期:2021-11-04 /// public class myNode : Node { ////// Start,初始化,执行时序和功能与Monobehaviour相同 /// public virtual void Start() { } ////// Update,每帧调用,Start,执行时序和功能与Monobehaviour相同 /// public virtual void Update() { } ////// 流程进入节点的时候调用 /// public virtual void EnterNode() { } ////// 节点执行完毕,流程退出该节点的时候调用 /// public virtual void ExitNode() { //判断是否有后续节点,有则激活 ListnextNodes = GetOutputPort("Exit").GetConnections(); if (nextNodes != null) { Debug.Log($"当前节点:{this.name},后续节点有"); for (int i = 0; i < nextNodes.Count; i++) { myNode nextNode = nextNodes[i].node as myNode; //此处的as必用 Debug.Log($"{i}----" + nextNode.name + "---------"); SceneGraphManager.CallAfterFramesCoroutine(1, nextNode.EnterNode); //在下一帧里面激活下一节点 } } } /// /// 编辑器的playing模式下,测试节点的功能 /// 限定为playing的原因,playing状态下的操作,等stop后可以自动回撤 /// public virtual void TestNode() { } }
【3】mono管理脚本的实现
void Start()
{
//图中的所有节点进行初始化
foreach (myNode nd in graph.nodes)
{
nd.Start();
}
//脚本单例判断
attachedCount++;
if (attachedCount > 1) Debug.LogError("【mySceneGraph.cs】脚本只能被挂载1次");
}
void Update()
{
totalTime += Time.deltaTime;
//每帧都调用所有节点的Update函数
foreach(myNode nd in graph.nodes)
{
nd.Update();
}
}
三、一个简单的普通的流程节点是什么样的
using System.Collections; using System.Collections.Generic; using UnityEngine; using XNode; ///四、【等待消息】节点是如何实现的/// Non-operation node 没有操作的节点:用于流程节点的连接,导通的作用 /// 使用场景:假定第一步操作结束后,要进行第二步操作,第二步操作有5个node需要同时启动,不使用nop节点的情况下,第一步操作的最后一个节点要 /// 连接5根线到第二步操作的5个节点中,如果流程变动,第一步的最后一个操作节点需要切换,那么要重新连接5根线。 /// 如果第二个节点的开始处是用nop节点作为起始节点,上面的情况,只需修改一根连接。 /// public class NopNode : myNode { //========通用参数设置区 ========begin ////// 该Node的功能说明 /// [Header("功能备注")] [TextArea] public string tooltip; [HideInInspector] [Input] public Empty Enter; [HideInInspector] [Output] public Empty Exit; ////// 当前节点是不是处于【Enter状态】,Enter状态的意思就是该节点正在执行当中。 /// 说明:一个Graph中,同一时刻里,处于Enter状态的节点,不止一个。 /// [HideInInspector] public bool isEnter; ////// 节点用到的node class 脚本 /// [HideInInspector] public string scriptName; //========通用参数设置区 ========end ////// 初始化 /// protected override void Init() { scriptName = this.GetType().Name; } public override void Update() { } ////// 执行流程进入节点,这个节点开始执行 /// public override void EnterNode() { base.EnterNode(); Debug.Log($"流程进入节点:{this.name}"); isEnter = true; ExitNode(); } ////// 节点执行完毕后,流程退出该节点,进入后续节点 /// public override void ExitNode() { base.ExitNode(); isEnter = false; } //端口的类型,【注意】:port必须能够系列化,不然graph上渲染的时候不能全部显示,会出错 [System.Serializable] public class Empty { }; //编辑器模式,才显示【测试功能】菜单,鼠标点击header,右键显示菜单【测试功能】 #if UNITY_EDITOR [ContextMenu("测试功能")] #endif public override void TestNode() { if (!(Application.isEditor && Application.isPlaying)) { Debug.Log("编辑器运行模式下才能进行测试!"); return; } Debug.Log($"开始测试{this.name}模块的功能......"); //具体的测试 } }
【1】图上的等待节点
【2】Inspector面板上的参数
【3】节点的实现
using System.Collections; using System.Collections.Generic; using UnityEngine; using XNode; ///五、节点的外观怎么定制 1、如何让节点的外观简洁明:/// 等待所有消息:所有的消息等到后,才执行后面的节点 /// public class waitAllMessagesNode : myNode { //========通用参数设置区 ========begin ////// 该Node的功能说明 /// [Header("功能备注")] [TextArea] public string tooltip; [HideInInspector] [Input] public Empty Enter; [HideInInspector] [Output] public Empty Exit; ////// 当前节点是不是处于【Enter状态】,Enter状态的意思就是该节点正在执行当中。 /// 说明:一个Graph中,同一时刻里,处于Enter状态的节点,不止一个。 /// [HideInInspector] public bool isEnter; ////// 节点用到的node class 脚本 /// [HideInInspector] public string scriptName; //========通用参数设置区 ========end //========自定义参数设置区========begin [Header("等待的消息列表")] public string[] messages; //[Header("参数(多个参数中间用[#]隔开)")] //public string msgArg; ////// 要等待的消息,初始化的时候,存入一个字典里面,收到一个消息则从字典里面清除该消息,字典item为0的时候,代表所有的消息都收到 /// private DictionarymsgDict = new Dictionary (); //========自定义参数设置区========end /// /// 初始化 /// protected override void Init() { //脚本的名字:class名 scriptName = this.GetType().Name; } public override void Start() { base.Start(); //等待的消息注册 if (messages.Length > 0) { foreach (string msg in messages) { MessageManager.AddMsgFunc("{msg}@", WaitMsg); } } } void WaitMsg(string msgArg) { var msg = msgArg.Split('@')[0]; //解析消息名称 //Debug.Log("执行了WaitMsg方法"); //Debug.Log("inCurrentFlow = " + inCurrentFlow); if (isEnter) { msgDict.Remove(msg); if (msgDict.Count == 0) { ExitNode(); } } } ////// 执行流程进入节点,这个节点开始执行 /// public override void EnterNode() { base.EnterNode(); Debug.Log($"流程进入节点:{this.name}"); isEnter = true; //消息装入字典里面。收到一个消息,则删除该消息,等字典为空的时候,代表所有消息都收到,重复执行的时候有bug, foreach (var msg in messages) { msgDict.Add(msg, ""); } } ////// 节点执行完毕后,流程退出该节点,进入后续节点 /// public override void ExitNode() { base.ExitNode(); isEnter = false; } //端口的类型,【注意】:port必须能够系列化,不然graph上渲染的时候不能全部显示,会出错 [System.Serializable] public class Empty { }; //编辑器模式,才显示【测试功能】菜单,鼠标点击header,右键显示菜单【测试功能】 #if UNITY_EDITOR [ContextMenu("测试功能")] #endif public override void TestNode() { if (!(Application.isEditor && Application.isPlaying)) { Debug.Log("编辑器运行模式下才能进行测试!"); return; } Debug.Log($"开始测试{this.name}模块的功能......"); //具体的测试 } }
以篮圈中的节点为例介绍
(1)Enter:流程进入的连线
(2)Exit:流程退出,next node的连线
(3)模块的功能:
(4)具体的功能描述
(5)使用到脚本
给这个脚本编写一个继承NodeEditor的脚本,用于定制node在graph上的外观,下面是【相机移动(moveCameraNode)】的NodeEditor脚本
(1)定义header的显示方式
public override void OnHeaderGUI(){...}
(2)定义body的显示方式
public override void OnBodyGUI(){...}
(3)务必记得把更新的内容进行apply,以便持久化
serializedObject.ApplyModifiedProperties();
(4)完整代码
using System;
using UnityEditor;
using UnityEngine;
using XNode;
using XNodeEditor;
using static XNodeEditor.NodeEditor;
[CustomNodeEditor(typeof(moveCameraNode))]
public class moveCameraNodeEditor : NodeEditor
{
private moveCameraNode myFlowNode; //定义了一个类型的节点,在绘制节点body的时候用
///
/// Node header的绘制
///
public override void OnHeaderGUI()
{
GUI.color = Color.white;
moveCameraNode node = target as moveCameraNode; //获取node引用对象
flowGraph graph = node.graph as flowGraph; //获取graph引用对象
if (node.isEnter)
{
GUI.color = Color.red; //如果当前节点是current节点,GUI.color 设置为蓝色
}
GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30));
GUI.color = Color.white;
}
///
/// 功能:Draws standard field editors for all public fields
/// 疑问:绘制public的字段,谁的public fields;绘制到哪里,是绘制到graph中的node GUI上,还是node的inspector上
/// 这个函数是每帧调用?
///
public override void OnBodyGUI()
{
if (myFlowNode == null) myFlowNode = target as moveCameraNode; //as - 引用类型之间的转变
//Update serialized object's representation。更新【系列化的物体】的representation(表现,表象)
//与【serializedObject.ApplyModifiedProperties()】配对使用
serializedObject.Update();
//模块功能设置
UnityEditor.EditorGUILayout.LabelField(myFlowNode.tooltip);
UnityEditor.EditorGUILayout.LabelField("script:" + myFlowNode.scriptName);
NodeEditorGUILayout.PropertyField(serializedObject.FindProperty("Enter"));
NodeEditorGUILayout.PropertyField(serializedObject.FindProperty("Exit"));
// Apply property modifications。修改完毕后,应用这些修改。
serializedObject.ApplyModifiedProperties();
}
}
六、节点在inspector面板上的外观定制
比如【移动相机】节点在inspector面板上的外观如下:
一共有5个交互的元素,其中还包括一个定制的button——【测试节点功能】
1、实现的方法:
修改以下脚本
2、修改的内容
在 GlobalNodeEditor的 OnInspectorGUI()方法中添加代码,注意代码的位置,需要放在
serializedObject.ApplyModifiedProperties()语句之前。
(1)添加的代码
// ======= 添加的代码 begin
if (GUILayout.Button("测试节点功能", GUILayout.Height(40)))
{
Debug.Log("调用对应节点的测试方法进行测试!");
foreach (var go in serializedObject.targetObjects)
{
Debug.Log(go.name);
Debug.Log(go.GetType());
foreach (var m in go.GetType().GetMethods()) //用到了反射
{
//Debug.Log(m.Name);
if (m.Name == "TestNode")
{
Debug.Log("侦测到测试节点的方法TestNode");
}
}
var myfunc = go.GetType().GetMethod("TestNode");
myfunc.Invoke(go, null);
}
}
// ======= 添加的代码 end
(2)完整的代码
using UnityEditor;
using UnityEngine;
#if ODIN_INSPECTOR
using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities;
using Sirenix.Utilities.Editor;
#endif
namespace XNodeEditor
{
/// Override graph inspector to show an 'Open Graph' button at the top
[CustomEditor(typeof(XNode.NodeGraph), true)]
#if ODIN_INSPECTOR
public class GlobalGraphEditor : OdinEditor {
public override void OnInspectorGUI() {
if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph);
}
base.OnInspectorGUI();
}
}
#else
[CanEditMultipleObjects]
public class GlobalGraphEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
if (GUILayout.Button("Edit graph", GUILayout.Height(40)))
{
NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph);
}
GUILayout.Space(EditorGUIUtility.singleLineHeight);
GUILayout.Label("Raw data", "BoldLabel");
DrawDefaultInspector(); //Inspector绘制,Unity核心
serializedObject.ApplyModifiedProperties();
}
}
#endif
[CustomEditor(typeof(XNode.Node), true)]
#if ODIN_INSPECTOR
public class GlobalNodeEditor : OdinEditor {
public override void OnInspectorGUI() {
if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
SerializedProperty graphProp = serializedObject.FindProperty("graph");
NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
w.Home(); // Focus selected node
}
base.OnInspectorGUI();
}
}
#else
[CanEditMultipleObjects]
public class GlobalNodeEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
if (GUILayout.Button("Edit graph", GUILayout.Height(40)))
{
SerializedProperty graphProp = serializedObject.FindProperty("graph");
NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
w.Home(); // Focus selected node
}
GUILayout.Space(EditorGUIUtility.singleLineHeight);
GUILayout.Label("Raw data", "BoldLabel");
// Now draw the node itself.
DrawDefaultInspector();
// ======= 添加的代码 begin
if (GUILayout.Button("测试节点功能", GUILayout.Height(40)))
{
Debug.Log("调用对应节点的测试方法进行测试!");
foreach (var go in serializedObject.targetObjects)
{
Debug.Log(go.name);
Debug.Log(go.GetType());
foreach (var m in go.GetType().GetMethods()) //用到了反射
{
//Debug.Log(m.Name);
if (m.Name == "TestNode")
{
Debug.Log("侦测到测试节点的方法TestNode");
}
}
var myfunc = go.GetType().GetMethod("TestNode");
myfunc.Invoke(go, null);
}
}
// ======= 添加的代码 end
serializedObject.ApplyModifiedProperties();
}
}
#endif
}
七、其它:
(1)如何在一个节点里面调用协程(协程只能在monobehaviour,而节点继承scriptableObject)
(2)节点里面如何引用scene的对象