一、需求说明
(1)所有的脚本都是挂载在层级面板上的空物体上,执行顺序就是层级面板的父子物体顺序,所有的脚本都是异步流程。
(2)在编辑器状态,我希望看到逻辑流程的执行状态:当前执行到哪里了,有没有报错等等。(虽然有debug信息,但是奔五的人了,近视+老花,看字确实有点老眼昏花了,看图则轻松多了)

说明:层级面板上的gameobject要变色或者添加标签,这个需要编辑器脚本来实现
二、解决的办法
1、在monoBehaviour脚本里强行调用Editor功能
第一种想到的方法就是【行为脚本】里面直接写逻辑,call编辑器脚本,加入编译条件 if UNITY_EDITOR
就像下面的这个图所表示的:在正常的流程脚本里,直接调用了编辑器脚本。看吧,他们是紧紧抱在一起的,耦合太强了,而且代码里面一堆编译条件,灰不溜秋的代码,心里害怕啊。

2、让他们解耦分开
如图,在他们中间添加一个【事件系统】或者【通知系统】,目的就是不让行为脚本直接调用编辑器脚本
如果用事件系统,那么行为脚本激发事件,编辑器脚本订阅事件。如果用消息也是一样,行为脚本发送消息,编辑器脚本负责最终的执行。

三、样例代码
1、事件脚本的实现
csharp
using System;
/// <summary>
/// 流程状态事件系统
/// </summary>
/// <remarks>
/// 用于解耦运行时和编辑器功能:
/// - 运行时脚本(如 WhenAll、WhenAny)调用 RaiseStatusChanged() 触发事件
/// - 编辑器脚本(如 HierarchyHighlighter)订阅 OnStatusChanged 事件并回调处理
/// 这样实现了:
/// 1. 运行时代码无需引用编辑器代码
/// 2. 编辑器功能可独立开关(#if UNITY_EDITOR)
/// 3. 职责单一:运行时只负责业务逻辑,编辑器负责可视化反馈
/// </remarks>
public static class FlowStatusEvent
{
/// <summary>
/// 流程状态改变事件
/// </summary>
/// <remarks>
/// 参数1:游戏对象的实例 ID
/// 参数2:新的流程状态(Running/Waiting/Completed/Failed)
/// </remarks>
public static event Action<int, FlowStatus> OnStatusChanged;
/// <summary>
/// 流程销毁时删除步骤信息
/// </summary>
public static event Action<int> OnDestroyRemoveInstanceID;
/// <summary>
/// 触发状态改变事件
/// </summary>
/// <param name="instanceID">游戏对象的实例 ID</param>
/// <param name="newStatus">新的【流程状态】</param>
/// <example>
/// <code>
/// // 运行时代码中调用
/// FlowStatusEvent.RaiseStatusChanged(instanceID, FlowStatus.Running);
/// </code>
/// </example>
public static void RaiseStatusChanged(int instanceID, FlowStatus newStatus)
{
OnStatusChanged?.Invoke(instanceID, newStatus);
}
/// <summary>
/// 触发OnDestroyRemoveInstanceID
/// </summary>
/// <param name="instanceID"></param>
public static void RaiseOnDestroyRemoveInstanceID(int instanceID)
{
OnDestroyRemoveInstanceID?.Invoke(instanceID);
}
}
2、编辑器脚本:给层级面板上的步骤物体变色
放置的Editor文件夹
csharp
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
/// <summary>
/// Hierarchy 步骤状态高亮
/// </summary>
[InitializeOnLoad]
public static class HierarchyHighlighter
{
/// <summary>
/// 状态字典,用于存储每个对象实例(步骤)的当前状态
/// </summary>
private static Dictionary<int, char> m_StatusDict = new Dictionary<int, char>();
/// <summary>
/// 缓存的GUIStyle,避免每次都创建
/// </summary>
private static GUIStyle cachedLabelStyle;
/// <summary>
/// 静态构造函数,用于初始化事件监听和刷新 Hierarchy
/// </summary>
static HierarchyHighlighter()
{
FlowStatusEvent.OnStatusChanged += OnFlowStatusChanged;
FlowStatusEvent.OnDestroyRemoveInstanceID += OnDestroyRemoveInstanceID;
EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;
//监听编辑器状态的改变
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
//监听场景加载事件
EditorSceneManager.sceneOpened += OnSceneOpened;
}
private static void OnDestroyRemoveInstanceID(int instanceID)
{
if (m_StatusDict.ContainsKey(instanceID))
{
m_StatusDict.Remove(instanceID);
//Debug.Log($"[Editor] 已删除物体 {instanceID} 的状态");
}
}
/// <summary>
/// 初始化GUIStyle(延迟初始化,第一次使用时创建)
/// </summary>
private static void InitializeStyles()
{
if (cachedLabelStyle == null)
{
cachedLabelStyle = new GUIStyle(EditorStyles.miniLabel)
{
alignment = TextAnchor.MiddleRight,
fontStyle = FontStyle.Bold
};
}
}
/// <summary>
/// 编辑器播放状态改变回调
/// </summary>
private static void OnPlayModeStateChanged(PlayModeStateChange state)
{
// 当从 Playing 变为 Stopped 时,清空状态字典
if (state == PlayModeStateChange.EnteredEditMode)
{
m_StatusDict.Clear();
EditorApplication.RepaintHierarchyWindow();
Debug.Log("[Editor] 编辑器停止,已清空流程状态");
}
}
/// <summary>
/// 场景加载完成回调
/// </summary>
private static void OnSceneOpened(Scene scene, OpenSceneMode mode)
{
//// 切换场景时清空状态字典
//ClearAllStatus();
//Debug.Log($"[Editor] 场景 '{scene.name}' 已加载,已清空流程状态");
//不能一概而论全删,有的是保留物体不销毁的
}
/// <summary>
/// 清空所有状态(提取通用方法)
/// </summary>
private static void ClearAllStatus()
{
m_StatusDict.Clear();
EditorApplication.RepaintHierarchyWindow();
Debug.Log("[Editor] 已清空所有流程状态");
}
/// <summary>
/// 流程状态改变事件回调
/// </summary>
private static void OnFlowStatusChanged(int instanceID, FlowStatus newStatus)
{
try
{
char statusChar = ConvertToChar(newStatus);
m_StatusDict[instanceID] = statusChar;
// 状态改变了,刷新 Hierarchy,旨在编辑器运行模式起效
if (EditorApplication.isPlaying)
{
EditorApplication.RepaintHierarchyWindow();
Debug.Log($"~~~~~~~~~~~~~~~~~~~~[Editor] 对象 {instanceID} 状态改为 {newStatus}");
}
}
catch (System.Exception e)
{
Debug.LogError($"OnFlowStatusChanged : [Editor] 获取对象 {instanceID} 状态时出错:{e.Message}");
}
}
/// <summary>
/// 将FlowStatus转换为字符
/// </summary>
private static char ConvertToChar(FlowStatus status)
{
return status switch
{
FlowStatus.Running => 'R',
FlowStatus.Waiting => 'W',
FlowStatus.Completed => 'C',
FlowStatus.Failed => 'F',
_ => '\0'
};
}
/// <summary>
/// 获取对象的状态字符
/// </summary>
public static char GetStatus(int instanceID)
{
return m_StatusDict.TryGetValue(instanceID, out char status) ? status : '\0';
}
/// <summary>
/// Hierarchy窗口项绘制回调
/// </summary>
private static void OnHierarchyGUI(int instanceID, Rect selectionRect)
{
char status = GetStatus(instanceID);
//Debug.Log($"OnHierarchyGUI正在执行中:instanceID = {instanceID} status = {status}");
if (status == '\0') return;
Color bgColor;
string label;
Color textColor = Color.white;
switch (status)
{
case 'R':
bgColor = new Color(0.2f, 0.6f, 0.2f, 0.3f);
label = "● RUNNING";
textColor = Color.green;
break;
case 'W':
bgColor = new Color(0.6f, 0.6f, 0.2f, 0.3f);
label = "○ WAITING";
textColor = Color.yellow;
break;
case 'C':
bgColor = new Color(0.2f, 0.6f, 0.6f, 0.3f);
label = "✓ COMPLETED";
textColor = Color.cyan;
break;
case 'F':
bgColor = new Color(0.8f, 0.2f, 0.2f, 0.4f);
label = "✗ FAILED";
textColor = Color.red;
break;
default:
return;
}
EditorGUI.DrawRect(selectionRect, bgColor);
GUIStyle labelStyle = new GUIStyle(EditorStyles.miniLabel)
{
alignment = TextAnchor.MiddleRight,
normal = { textColor = textColor },
fontStyle = FontStyle.Bold
};
EditorGUI.LabelField(selectionRect, label, labelStyle);
}
}
#endif
3、行为脚本:触发状态变更
csharp
public async UniTask FlowAsync(CancellationToken ctk)
{
try
{
FlowStatusEvent.RaiseStatusChanged(instanceID, FlowStatus.Running); //开始运行
//业务逻辑
await target.DoScale(scaleStartObj, scaleEndObj, duration, ctk);
Debug.Log($"++执行完毕:{this_name}");
FlowStatusEvent.RaiseStatusChanged(instanceID, FlowStatus.Completed); //运行结束
}
catch (Exception e)
{
Debug.Log($"{this_name}.DoScale报错:{e.Message}");
Debug.Log($"\n 抛出一个OperationCanceledException");
FlowStatusEvent.RaiseStatusChanged(instanceID, FlowStatus.Failed); //运行失败
throw new OperationCanceledException();
}
}