引导流程需求 目前已经有了节点和引导的遮罩,为了实现编辑步骤,还要把每一步需要高亮的位置或者需要高亮跟随的GameObject放入节点数据中,并根据节点的先后顺序逐一运行逻辑。所以就需要有3个事情: 1.引导节点。目前节点编辑器只有开始节点,无法存储引导节点的数据,所以需要继承于NodeBase和数据,实现编辑器的可视化功能。 2.引导节点逻辑处理脚本。为了之后的拓展性,所以针对每一种引导节点都有一个特定的处理脚本。通过维护一个状态机的生命周期,在脚本中拿到节点数据进行具体的逻辑处理。 3.引导系统。我把这个写成了单例。负责加载引导数据并反序列化出来,同时也要提供根据引导名字开启引导的接口。
引导节点实现 引导数据 数据类继承于原来的,引导需要知道高亮指定的GameObject,数据就需要知道GameObject的ID(这里通过给对应的物体挂载脚本),引导数据传入ID。
1 2 3 4 5 6 7 8 9 10 [Serializable ] public class GuideData :NodeSerializationData { [SerializeField ] public string GuideDesc; [SerializeField ] public long GuideID; }
引导Node 引导节点继承NodeBase,需要重写InitComponent添加两个输入框编辑数据,并把数据保存起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 public class GuideNode : NodeBase { public GuideData guideData; private TextField textField; private bool m_isPreview; public override void Init (NodeSoData nodeData, NodeGraphView.NodeGraphView nodeGraphView ) { base .Init(nodeData, nodeGraphView); } protected override void InitData () { m_isPreview = false ; AddPortData(); if (nodeData.userData != null ) { guideData = nodeData.userData as GuideData; } else { guideData = new GuideData(); guideData.GuideDesc = "测试文本1233" ; guideData.GuideID = 10086 ; nodeData.userData = guideData; } } public override NodeSerializationData GetSerializationData () { GuideData serializationData = new GuideData(); serializationData.GuideDesc = guideData.GuideDesc; serializationData.GuideID = guideData.GuideID; serializationData.nodeID = nodeData.nodeID; serializationData.outputPortDataList = nodeData.outputPortDataList; serializationData.intputPortDataList = nodeData.intputPortDataList; return serializationData; } private void AddPortData () { if (nodeData.intputPortDataList == null || nodeData.intputPortDataList.Count <= 0 ) { nodeData.AddInputPortData(); } if (nodeData.outputPortDataList == null || nodeData.outputPortDataList.Count <= 0 ) { nodeData.AddOutputPortData(); } } protected override void InitComponent () { base .InitComponent(); Label label1 = new Label("高亮区域ID" ); var textField1 = new TextField(); textField1.value = guideData.GuideID.ToString(); textField1.RegisterCallback<ChangeEvent<string >>(OnGuideIDChangeValue); extensionContainer.Add(label1); extensionContainer.Add(textField1); Label label2 = new Label("引导文本" ); extensionContainer.Add(label2); textField = new TextField(); textField.value = guideData.GuideDesc; textField.RegisterCallback<ChangeEvent<string >>(OnTextFieldChangeValue); extensionContainer.Add(textField); RefreshExpandedState(); } private void OnGuideIDChangeValue (ChangeEvent<string > evt ) { if (long .TryParse(evt.newValue, out long id)) { guideData.GuideID = long .Parse(evt.newValue); Debug.Log(evt.newValue); EditorUtility.SetDirty(nodeData); AssetDatabase.SaveAssets(); } } private void OnTextFieldChangeValue (ChangeEvent<string > evt ) { guideData.GuideDesc = evt.newValue; Debug.Log(evt.newValue); EditorUtility.SetDirty(nodeData); AssetDatabase.SaveAssets(); } }
引导节点处理脚本 为了实现解耦,所以给每一个引导节点数据类都写一个唯一的数据处理脚本。
脚本基类 类中维护4个状态和节点数据。4个状态初始化、运行中、结束和完成,子类通过实现4个状态内的对数据处理的具体逻辑,完成具体需求的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 public class GuideHandlerBase { protected GuideState m_guideState; protected NodeSerializationData m_guideData; protected GuideCollection m_guideCollection; public NodeSerializationData guideData => m_guideData; public GuideHandlerBase (NodeSerializationData data ) { m_guideData = data; } public void SetGuideCollection (GuideCollection guideCollection ) { m_guideCollection = guideCollection; } public void ChangeState (GuideState guideState ) { if (m_guideState == GuideState.Done) { return ; } m_guideState = guideState; switch (guideState) { case GuideState.None: break ; case GuideState.Init: { OnInitState(); } break ; case GuideState.Running: { OnRunningState(); } break ; case GuideState.Finish: { OnFinishState(); } break ; case GuideState.Done: { break ; } default : break ; } } protected virtual void OnInitState () { Debug.Log($"节点ID={m_guideData.nodeID} 初始化" ); m_guideCollection.AddRunningNodeID(m_guideData.nodeID); } protected virtual void OnRunningState () { Debug.Log($"节点ID={m_guideData.nodeID} 运行中" ); } protected virtual void OnFinishState () { Debug.Log($"节点ID={m_guideData.nodeID} 结束" ); m_guideCollection.RemoveRunningNodeID(m_guideData.nodeID); RunNextNode(); m_guideState = GuideState.Done; } public void RunNextNode () { if (m_guideCollection == null ) { Debug.LogError("guideCollectio is null!" ); return ; } foreach (var portData in m_guideData.outputPortDataList) { foreach (var linkData in portData.linkData) { m_guideCollection.RunNode(linkData.linkNodeID); } } } }
引导处理类 引导类的实现比较简单,初始化的时候,实例化一个遮罩,并根据传入的引导数据内的ID找到对应高亮的GameObject进行跟随。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 public class HeightLightRectGuideHandler : GuideHandlerBase { private RectMaskVertex m_RectMask; private Transform targetObj; private GuideData m_guideData; public HeightLightRectGuideHandler (NodeSerializationData data ) : base (data ) { if (data is GuideData guidedata) { m_guideData = guidedata; } } protected override void OnInitState () { base .OnInitState(); InitGuide(); ChangeState(GuideState.Running); } protected override void OnRunningState () { base .OnRunningState(); if (m_RectMask == null || targetObj == null ) { return ; } m_RectMask.SetFollowTarget(targetObj.gameObject, true ); } protected override void OnFinishState () { FinishGuide(); base .OnFinishState(); } private void InitGuide () { m_RectMask = GuideSystem.Instance.InstantiateRectMask(); m_RectMask.name = "RectMask" + m_guideData.nodeID; SetHighLight(); MessageSystem.Instance.AddMessage(MessageDefine.MASK_HIGH_LINGHT_CLICK, OnMaskHighLightClick); MessageSystem.Instance.AddMessage(MessageDefine.MASK_CLICK, OnMaskClick); } private void OnMaskClick () { Debug.Log("OnMaskClick" ); } private void OnMaskHighLightClick () { ChangeState(GuideState.Finish); } private void SetHighLight () { if (guideData == null ) { Debug.Log($"guideData is null" ); return ; } targetObj = GuideSystem.Instance.GetComponent<Transform>(m_guideData.GuideID); m_RectMask.SetFollowTarget(targetObj.gameObject, true ); } private void FinishGuide () { MessageSystem.Instance.RemoveMessage(MessageDefine.MASK_CLICK, OnMaskClick); MessageSystem.Instance.RemoveMessage(MessageDefine.MASK_HIGH_LINGHT_CLICK, OnMaskHighLightClick); if (m_RectMask != null ) { GameObject.Destroy(m_RectMask.gameObject); m_RectMask = null ; } } }
引导系统实现 引导系统作为了一个单例存在,内部的两个重要功能:1、负责引导数据的记载。2、维护一个GuideRuner负责引导的运行。
加载和反序列化数据 加载数据的函数比较简单,我这里直接使用了Resource.Load把对应的文件加载并通过字符串分割(分割和保存的逻辑相互对应)反序列成一个List数据。
1 2 3 4 5 6 7 8 9 private List<GuideHandlerBase> LoadData (string guideName ){ string fileName = guideName + ".json" ; var bytesData = Resources.Load<TextAsset>(guideName); var strList = bytesData.text.Split("|" ).ToList(); var handlerList = DeserializeObjectAndHandler(strList); m_guideDataDic.Add(guideName, handlerList); return handlerList; }
开启引导接口 引导系统中通过一个GuideRuner类负责开启。GuideRuner是一个继承于Mono的,挂载在场景的引导根节点上。内部维护了一个字典,记录着当前运行着的所有引导,并实现开始引导和移除引导的接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 public class GuideRuner : MonoBehaviour { public EventSystem eventSystem; public RectMaskVertex rectMsask; private Dictionary<string , GuideCollection> m_RunningGuideDic = new Dictionary<string , GuideCollection>(); private void Start () { GuideSystem.Instance.StartGuide("NewGraph0" ); } private void OnEnable () { GuideSystem.Instance.SetGuideRoot(this ); } public void RunningGuide (string guideName, List<GuideHandlerBase> guideData ) { if (m_RunningGuideDic.TryGetValue(guideName, out GuideCollection guideCollection)) { guideCollection.ChangeState(GuideState.Init); return ; } var newGuide = new GuideCollection(guideName, guideData,this ); m_RunningGuideDic.Add(guideName, newGuide); } private void LateUpdate () { } public void RemoveGuideCollection (string guideName ) { if (m_RunningGuideDic == null || m_RunningGuideDic.Count <= 0 ) { return ; } if (m_RunningGuideDic.ContainsKey(guideName)) { m_RunningGuideDic.Remove(guideName); } } public RectMaskVertex InstantiateRectMask () { if (rectMsask == null ) { return null ; } RectMaskVertex rectMaskVertex = Instantiate<RectMaskVertex>(rectMsask, transform); rectMaskVertex.SetEventSystem(eventSystem); return rectMaskVertex; } }
引导系统中通过调用GuideRunner的接口开启引导。
1 2 3 4 5 6 7 8 9 10 11 12 public void StartGuide (string guideName ){ if (string .IsNullOrEmpty(guideName)) { return ; } var guideData = GetGuideListData(guideName); m_guideRuner.RunningGuide(guideName, guideData); }
GuideCollection 因为可能存在多个引导,所以又抽象出了一个GuideCollection的数据结构。GuideCollection其实就是保存了一个引导蓝图所有的节点处理类。在构造函数中初始化,并找到蓝图中的开始节点运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 public class GuideCollection { private string m_guideName; private List<GuideHandlerBase> m_guideHandlerData; private GuideState m_guideState; private GuideRuner m_guideRuner; private List<long > m_runningNodeID; public GuideCollection (string guideName, List<GuideHandlerBase> guideHandlerData, GuideRuner guideRuner ) { m_guideName = guideName; m_guideHandlerData = guideHandlerData; m_guideRuner = guideRuner; m_runningNodeID = new List<long >(); ChangeState(GuideState.Init); } private bool CheckHasNodeRunning () { return m_runningNodeID.Count > 0 ; } public void Update () { if (m_guideState != GuideState.Running) { return ; } if (!CheckHasNodeRunning()) { ChangeState(GuideState.Finish); } } #region 对外接口 public void ChangeState (GuideState guideState ) { m_guideState = guideState; switch (guideState) { case GuideState.None: break ; case GuideState.Init: { OnInitState(); } break ; case GuideState.Running: { OnRunningState(); } break ; case GuideState.Finish: { OnFinishState(); } break ; default : break ; } } public void AddRunningNodeID (long nodeID ) { if (m_runningNodeID.Contains(nodeID)) { return ; } m_runningNodeID.Add(nodeID); } public void RemoveRunningNodeID (long nodeID ) { for (int i = m_runningNodeID.Count - 1 ; i >= 0 ; i--) { if (m_runningNodeID[i] == nodeID) { m_runningNodeID.Remove(i); break ; } } } #endregion protected virtual void OnInitState () { var startNode = GuideStartNode(); RunNode(startNode.guideData.nodeID); } protected virtual void OnRunningState () { } protected virtual void OnFinishState () { m_guideRuner.RemoveGuideCollection(m_guideName); Debug.Log("guide over!" ); } public void RunNode (long nodeID ) { var handlerBase = GetGuideNodeDataByID(nodeID); if (handlerBase == null ) { Debug.LogError($"GuideCollection:linkNode ID ={nodeID} is NULL!" ); return ; } handlerBase.SetGuideCollection(this ); handlerBase.ChangeState(GuideState.Init); } private GuideHandlerBase GuideStartNode () { if (m_guideHandlerData == null ) { return null ; } GuideHandlerBase result = null ; foreach (var guideNode in m_guideHandlerData) { if (guideNode.guideData.typeFullName == typeof (StartNodeData).FullName) { result = guideNode; break ; } } return result; } private GuideHandlerBase GetGuideNodeDataByID (long guideID ) { if (m_guideHandlerData == null || m_guideHandlerData.Count <= 0 ) { return null ; } foreach (var guideNode in m_guideHandlerData) { if (guideNode.guideData.nodeID == guideID) { return guideNode; } } Debug.LogError($"GudieHandler:GuideID={guideID} is not found" ); return null ; } }
通过节点数据获取对应的处理脚本 目前有节点数据和处理脚本,但是还有一个问题,无法将数据和对应的处理脚本一一对应起来。为了解决这个问题,我记录了一个字典,key为数据的类型,value是具体的处理脚本。在读取每个节点数据的时候,先用了NewtonJson的一个函数,统一反序列化成基类,再通过的数据中记录类名与字典的key的类型比较是否相同,从而获得对应的处理脚本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 private List<GuideHandlerBase> LoadData (string guideName ){ string fileName = guideName + ".json" ; var bytesData = Resources.Load<TextAsset>(guideName); var strList = bytesData.text.Split("|" ).ToList(); var handlerList = DeserializeObjectAndHandler(strList); m_guideDataDic.Add(guideName, handlerList); return handlerList; } private List<GuideHandlerBase> DeserializeObjectAndHandler (List<string > objectStrList ){ if (objectStrList == null || objectStrList.Count <= 0 ) { return null ; } var nodeDataList = new List<GuideHandlerBase>(); foreach (var objectStr in objectStrList) { var baseData = Newtonsoft.Json.JsonConvert.DeserializeObject<NodeSerializationData>(objectStr); if (baseData == null ) { Debug.LogError("无法反序列化基础数据" ); continue ; } Type targetType = null ; foreach (var typeItem in m_guideTypeMap) { if (typeItem.Key.FullName.Equals(baseData.typeFullName)) { targetType = typeItem.Key; break ; } } if (targetType == null ) { Debug.LogError($"未找到匹配的类型: {baseData.typeFullName} " ); continue ; } var saveData = Newtonsoft.Json.JsonConvert.DeserializeObject(objectStr, targetType); if (saveData == null ) { Debug.LogError($"无法反序列化为目标类型: {targetType.FullName} " ); continue ; } if (!m_guideTypeMap.TryGetValue(targetType, out Type handlerType)) { Debug.LogError($"未找到类型 {targetType.FullName} 对应的处理器类型" ); continue ; } var handler = Activator.CreateInstance(handlerType, saveData) as GuideHandlerBase; if (handler == null ) { Debug.LogError($"无法创建处理器实例: {handlerType.FullName} " ); continue ; } nodeDataList.Add(handler); } return nodeDataList; }