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();
     }            
 }
相关推荐
mxwin3 小时前
Unity Shader 半透明物体为什么不能写入深度缓冲?
unity·游戏引擎·shader
晚枫歌F4 小时前
三层时间轮的实现
网络·unity·游戏引擎
咸鱼永不翻身5 小时前
Lua脚本事件检查工具
unity·lua·工具
leo__5207 小时前
单载波中继系统资源分配算法MATLAB仿真程序
算法·matlab·unity
努力长头发的程序猿8 小时前
Unity使用ScriptableObject序列化资源
unity·游戏引擎
mxwin8 小时前
Unity Shader 手写基于 PBR 的 URP Lit Shader 核心光照计算
unity·游戏引擎·shader
小贺儿开发8 小时前
Unity3D 智能云端数字标牌系统
unity·阿里云·人机交互·视频·oss·广告·互动
魔士于安9 小时前
Unity windows 同步 异步 打开文件文件夹工具
游戏·unity·游戏引擎·贴图·模型
魔士于安10 小时前
unity lowpoly 风格 城市 建筑 道路 交通标志
游戏·unity·游戏引擎·贴图·模型
mxwin10 小时前
Unity GPU Shader 性能优化指南
unity·游戏引擎·shader