Unity行为脚本与编辑器脚本的解耦例子之一

一、需求说明

复制代码
   (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();
     }            
 }
相关推荐
小蜗 strong4 小时前
Unity中MRTK下载相关功能配置(适用HoloLens 2 部署)
unity·游戏引擎·hololens
ok406lhq21 小时前
unity游戏调用SDK支付返回游戏会出现画面移位的问题
android·游戏·unity·游戏引擎·sdk
小股虫21 小时前
从Tair虚拟桶到数据库分库分表:解耦逻辑与物理的架构艺术
数据库·架构·解耦
ellis19701 天前
toLua[八] main场景分析
unity·lua
CreasyChan1 天前
unity四元数 - “处理旋转的大师”
unity·c#·游戏引擎
野区捕龙为宠1 天前
unity 实现3D空间音效特性
3d·unity·游戏引擎
老朱佩琪!1 天前
Unity外观模式
unity·游戏引擎·外观模式
程序员茶馆1 天前
【unity】Shader艺术之unity内置变量个性化控制
unity·游戏引擎
CreasyChan1 天前
unity射线与几何检测 - “与世界的交互”
算法·游戏·3d·unity·数学基础