【1】下图为节点图的构成
【2】节点基类实现
using System.Collections; using System.Collections.Generic; using UnityEngine; using XNode; /// <summary> /// 定义一个自己的节点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 /// </summary> public class myNode : Node { /* * flowNode Class:【游戏关卡】或者【仿真作业流程】节点 * 功能分类: * 一、编辑器状态下的功能 * 1、编辑功能 * 2、调试功能 * 3、提供预览功能【所见即所得的模块行为】 * 二、发布状态下的功能 * * 三、节点功能的【预览】或者【测试】的说明 * 1、目标:实现所见即所得的功能 * 2、最好在Editor的playing状态下测试或者预览,不然容易更改GameObject的初始状态,也就不容易破坏刚搭建好的场景 * */ /// <summary> /// Start,初始化,执行时序和功能与Monobehaviour相同 /// </summary> public virtual void Start() { } /// <summary> /// Update,每帧调用,Start,执行时序和功能与Monobehaviour相同 /// </summary> public virtual void Update() { } /// <summary> /// 流程进入节点的时候调用 /// </summary> public virtual void EnterNode() { } /// <summary> /// 节点执行完毕,流程退出该节点的时候调用 /// </summary> public virtual void ExitNode() { //判断是否有后续节点,有则激活 List<NodePort> nextNodes = 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); //在下一帧里面激活下一节点 } } } /// <summary> /// 编辑器的playing模式下,测试节点的功能 /// 限定为playing的原因,playing状态下的操作,等stop后可以自动回撤 /// </summary> 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; /// <summary> /// Non-operation node 没有操作的节点:用于流程节点的连接,导通的作用 /// 使用场景:假定第一步操作结束后,要进行第二步操作,第二步操作有5个node需要同时启动,不使用nop节点的情况下,第一步操作的最后一个节点要 /// 连接5根线到第二步操作的5个节点中,如果流程变动,第一步的最后一个操作节点需要切换,那么要重新连接5根线。 /// 如果第二个节点的开始处是用nop节点作为起始节点,上面的情况,只需修改一根连接。 /// </summary> public class NopNode : myNode { //========通用参数设置区 ========begin /// <summary> /// 该Node的功能说明 /// </summary> [Header("功能备注")] [TextArea] public string tooltip; [HideInInspector] [Input] public Empty Enter; [HideInInspector] [Output] public Empty Exit; /// <summary> /// 当前节点是不是处于【Enter状态】,Enter状态的意思就是该节点正在执行当中。 /// 说明:一个Graph中,同一时刻里,处于Enter状态的节点,不止一个。 /// </summary> [HideInInspector] public bool isEnter; /// <summary> /// 节点用到的node class 脚本 /// </summary> [HideInInspector] public string scriptName; //========通用参数设置区 ========end /// <summary> /// 初始化 /// </summary> protected override void Init() { scriptName = this.GetType().Name; } public override void Update() { } /// <summary> /// 执行流程进入节点,这个节点开始执行 /// </summary> public override void EnterNode() { base.EnterNode(); Debug.Log($"流程进入节点:{this.name}"); isEnter = true; ExitNode(); } /// <summary> /// 节点执行完毕后,流程退出该节点,进入后续节点 /// </summary> 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; /// <summary> /// 等待所有消息:所有的消息等到后,才执行后面的节点 /// </summary> public class waitAllMessagesNode : myNode { //========通用参数设置区 ========begin /// <summary> /// 该Node的功能说明 /// </summary> [Header("功能备注")] [TextArea] public string tooltip; [HideInInspector] [Input] public Empty Enter; [HideInInspector] [Output] public Empty Exit; /// <summary> /// 当前节点是不是处于【Enter状态】,Enter状态的意思就是该节点正在执行当中。 /// 说明:一个Graph中,同一时刻里,处于Enter状态的节点,不止一个。 /// </summary> [HideInInspector] public bool isEnter; /// <summary> /// 节点用到的node class 脚本 /// </summary> [HideInInspector] public string scriptName; //========通用参数设置区 ========end //========自定义参数设置区========begin [Header("等待的消息列表")] public string[] messages; //[Header("参数(多个参数中间用[#]隔开)")] //public string msgArg; /// <summary> /// 要等待的消息,初始化的时候,存入一个字典里面,收到一个消息则从字典里面清除该消息,字典item为0的时候,代表所有的消息都收到 /// </summary> private Dictionary<string, string> msgDict = new Dictionary<string, string>(); //========自定义参数设置区========end /// <summary> /// 初始化 /// </summary> 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) { /* * 不同的消息指令合用该方法,如何区分是哪个消息指令触发了该方法,需要用【@】split后取参数 * arg = [消息名]@[参数1#参数2#...#参数n] */ var msg = msgArg.Split('@')[0]; //解析消息名称 //Debug.Log("执行了WaitMsg方法"); //Debug.Log("inCurrentFlow = " + inCurrentFlow); if (isEnter) { msgDict.Remove(msg); if (msgDict.Count == 0) { ExitNode(); } } } /// <summary> /// 执行流程进入节点,这个节点开始执行 /// </summary> public override void EnterNode() { base.EnterNode(); Debug.Log($"流程进入节点:{this.name}"); isEnter = true; //消息装入字典里面。收到一个消息,则删除该消息,等字典为空的时候,代表所有消息都收到,重复执行的时候有bug, foreach (var msg in messages) { msgDict.Add(msg, ""); } } /// <summary> /// 节点执行完毕后,流程退出该节点,进入后续节点 /// </summary> 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; /* * 为一个节点定制它的外观。 * 1、这里的外观是指在Graph上的外观,而不是inspector面板上的外观 * 2、定制外观的目的,是让Graph上的节点占地面积小一点,防止后期节点太多,装不下 * 3、所有节点的editor代码都相同,能不能用一个脚本来处理 * 4、快速更替class的名字,本例中的moveCameraNode,快速修改成需要的class */ [CustomNodeEditor(typeof(moveCameraNode))] public class moveCameraNodeEditor : NodeEditor { private moveCameraNode myFlowNode; //定义了一个类型的节点,在绘制节点body的时候用 /// <summary> /// Node header的绘制 /// </summary> 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; } /// <summary> /// 功能:Draws standard field editors for all public fields /// 疑问:绘制public的字段,谁的public fields;绘制到哪里,是绘制到graph中的node GUI上,还是node的inspector上 /// 这个函数是每帧调用? /// </summary> public override void OnBodyGUI() { /* 说明: * 1、如果simpleNode为空,那么初始化simpleNode * 2、【serializedObject.Update()】:更新【系列化的物体】的representation(表现,表象) * 3、【PropertyField()】:Make a field for a serialized property. Automatically displays relevant node port. * 为【序列化属性】创建一个字段。 并把这个字段显示在与它相对应的端口上。 * 4、【LabelField()】:创建一个标签字段。 (用于显示只读信息。) */ if (myFlowNode == null) myFlowNode = target as moveCameraNode; //as - 引用类型之间的转变 //Update serialized object's representation。更新【系列化的物体】的representation(表现,表象) //与【serializedObject.ApplyModifiedProperties()】配对使用 serializedObject.Update(); //模块功能设置 /* * ====函数说明==== * UnityEditor.EditorGUILayout.LabelField(myFlowNode.tooltip) // Make a label field. (Useful for showing read-only info.) * serializedObject.FindProperty("Enter") // Find serialized property by name. */ 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面板上的外观如下:
一共有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"); } /* * Get the ItsMagic method and invoke with a parameter value of 100 * MethodInfo magicMethod = magicType.GetMethod("ItsMagic"); * object magicValue = magicMethod.Invoke(magicClassObject, new object[]{100}); */ } 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 { /// <summary> Override graph inspector to show an 'Open Graph' button at the top </summary> [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"); } /* * Get the ItsMagic method and invoke with a parameter value of 100 * MethodInfo magicMethod = magicType.GetMethod("ItsMagic"); * object magicValue = magicMethod.Invoke(magicClassObject, new object[]{100}); */ } var myfunc = go.GetType().GetMethod("TestNode"); myfunc.Invoke(go, null); } } // ======= 添加的代码 end serializedObject.ApplyModifiedProperties(); } } #endif }
(1)如何在一个节点里面调用协程(协程只能在monobehaviour,而节点继承scriptableObject)
(2)节点里面如何引用scene的对象