U3D編輯器開發&粒子特效/動畫預覽器範例

2023-01-29 15:00:59

概述


U3D提供了一套拓展編輯器的介面,可以用於直接在編輯器非播放模式執行程式。常用於執行一些工具程式,例如資源管理。在做技能編輯器等工具程式時,也可以使用執行模式介面會比較簡單(這樣也方便開放遊戲創意工坊給玩家)。使用編輯器去做一些渲染相關的預覽(如粒子系統,動畫預覽)會麻煩一點,有時候需要查詢和反射使用U3D引擎未暴露的介面。
U3D編輯器相關官方檔案查詢連結:https://docs.unity3d.com/cn/current/Manual/GUIScriptingGuide.html

業務需求分析


這是常用的一些需求和介面,多數的用法比較簡單的在這裡簡單介紹一下。

OnGUI

程式碼化在視口繪製UI,常用於繪製一些DebugUI,如官方範例:
OnGUI官方介紹

using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour {
            
    void OnGUI ()
    {
        // 建立背景框
        GUI.Box(new Rect(10,10,100,90), "Loader Menu");
    
        // 建立第一個按鈕。如果按下此按鈕,則會執行 Application.Loadlevel (1)
        if(GUI.Button(new Rect(20,40,80,20), "Level 1"))
        {
            Application.LoadLevel(1);
        }
    
        // 建立第二個按鈕。
        if(GUI.Button(new Rect(20,70,80,20),"Level 2")) 
        {
            Application.LoadLevel(2);
        }
    }
}

Editor和ExecuteInEditMode特性

  • ExecuteInEditMode特性:讓指令碼在編輯器模式執行。
  • Editor:封裝一些UI繪製的介面,如(重新OnInspectorGUI以在Inspector視窗繪製UI介面)。

Editor和ExecuteInEditMode特性相關官方介紹

EditorWindow

繼承EditorWindow重寫以在編輯器建立繪製一個獨立的編輯器視窗。

using UnityEditor;
using UnityEngine;

public class MyWindow : EditorWindow
{
    string myString = "Hello World";
    bool groupEnabled;
    bool myBool = true;
    float myFloat = 1.23f;
    
    // 將名為"My Window"的選單項新增到 Window 選單
    [MenuItem("Window/My Window")]
    public static void ShowWindow()
    {
        //顯示現有視窗範例。如果沒有,請建立一個。
        EditorWindow.GetWindow(typeof(MyWindow));
    }
    
    void OnGUI()
    {
        GUILayout.Label ("Base Settings", EditorStyles.boldLabel);
        myString = EditorGUILayout.TextField ("Text Field", myString);
        
        groupEnabled = EditorGUILayout.BeginToggleGroup ("Optional Settings", groupEnabled);
            myBool = EditorGUILayout.Toggle ("Toggle", myBool);
            myFloat = EditorGUILayout.Slider ("Slider", myFloat, -3, 3);
        EditorGUILayout.EndToggleGroup ();
    }
}

EditorWindow官方範例

Serializable屬性繪製器

Serializable U3D官方翻譯為屬性繪製器,字面意思是可被序列化的。大概的效果是被Serializable特性標記可以被U3D序列化並在U3D進行繪製和提供更方便的操作,如下:

using System;
using UnityEngine;

enum IngredientUnit { Spoon, Cup, Bowl, Piece }

// 自定義 Serializable 類
[Serializable]
public class Ingredient
{
    public string name;
    public int amount = 1;
    public IngredientUnit unit;
}

public class Recipe : MonoBehaviour
{
    public Ingredient potionResult;
    public Ingredient[] potionIngredients;
}

屬性繪製器官方介紹

外觀風格/面板

GUIStyle和GUISkin提供了介面來客製化化UI繪製的外觀風格,區別在於GUIStyle常用於某個控制元件的風格繪製,而GUISkin則會應用於其下文中所有控制元件的繪製,具體使用細節見官方介紹
GUISkin官方介紹
GUIStyle官方介紹

PreviewRenderUtility 預覽視窗渲染工具

PreviewRenderUtility 個人認為它是一個預覽視窗的渲染工具類,官方對其介紹幾乎沒有。但是在EditorWindow、Inspector視窗預覽模型、動畫、粒子特效會用到。例如這個特效的預覽下文會具體介紹:

常用控制元件速查表

名稱 程式碼 範例
Label GUI.Label (new Rect (25, 25, 100, 30), "Label");
Button if (GUI.Button (new Rect (25, 25, 100, 30), "Button"))
TextField 單行輸入框 str = GUI.TextField (new Rect (25, 25, 100, 30), str);
TextArea 多行輸入框 str = GUI.TextArea (new Rect (25, 25, 100, 30), str);
Toggle 勾選框 b = GUI.Toggle (new Rect (25, 25, 100, 30), toggleBool, "Toggle");
HorizontalSlider 水平滾軸 h = GUI.HorizontalSlider (new Rect (25, 25, 100, 30), hSliderValue, 0.0f, 10.0f);
Toolbar 頁籤 Idx = GUI.Toolbar (new Rect (25, 25, 250, 30), Idx, BtnNames);
SelectionGrid 平鋪列表 Idx = GUI.SelectionGrid (new Rect (25, 25, 300, 60), Idx, Names, Cow);
ScrollView 捲動列表 pos = GUI.BeginScrollView (rect, scrollViewVector, rectContent);
//XXX
GUI.EndScrollView();
Window 小視窗 w = GUI.Window (0, wRect, drawDele, "win");

注意手動排版使用GUI.XXX,自動排版使用GUILayout.XXX

佈局速查表

佈局的一般用法是,例如組Group

GUI.BeginGroup
//控制元件A
//控制元件B ...
GUI.EndGroup

using (new GroupScope)
{
//控制元件A
//控制元件B ...
}

佈局 用法 預覽
組 Group 固定相對位置
區域 Area 用於自動佈局一組控制元件,與組類似 -
水平佈局 Horizontal 水平佈局一組控制元件
垂直佈局 Vertical 垂直佈局一組控制元件

GUILayoutOption可以某些重寫自動佈局引數,例如:
GUILayout.Button ("My width has been overridden", GUILayout.Width (95));
GUILayout.Width重寫了自動佈局這個按鈕的寬

特效預覽視窗

U3D自帶的特效預覽方式是拖到Scene視窗上,有的時候做編輯器(例如技能編輯器)時需要預覽的特效,切來切去太麻煩了。而且U3D拖到Scene才能預覽這種方式也很麻煩,也可以自己拓展選中特效在Hir上預覽。

關鍵點

  • 使用反射呼叫庫函數繫結特效
  • 使用prevRU渲染GameObj

程式碼範例

LibUtil 呼叫庫函數

public class LibUtil
{
    public static Type ParticleSystemEditorUtils
    {
        get
        {
            var assembly = Assembly.GetAssembly(typeof(Editor));
            return assembly.GetType("UnityEditor.ParticleSystemEditorUtils");
        }
    }

    public static ParticleSystem lockedParticleSystem
    {
        get
        {
            var info = ParticleSystemEditorUtils.GetProperty("lockedParticleSystem", BindingFlags.Static | BindingFlags.NonPublic);
            return (ParticleSystem)info.GetValue(null, null);
        }
        set
        {
            var info = ParticleSystemEditorUtils.GetProperty("lockedParticleSystem", BindingFlags.Static | BindingFlags.NonPublic);
            info.SetValue(null, value, null);
        }
    }

    public static bool editorIsScrubbing
    {
        set
        {
            var info = ParticleSystemEditorUtils.GetProperty("playbackIsScrubbing", BindingFlags.Static | BindingFlags.NonPublic);
            info.SetValue(null, value, null);
        }
    }

    public static float editorPlaybackTime
    {
        get
        {
            var info = ParticleSystemEditorUtils.GetProperty("playbackTime", BindingFlags.Static | BindingFlags.NonPublic);
            return (float)info.GetValue(null, null);
        }
        set
        {
            var info = ParticleSystemEditorUtils.GetProperty("playbackTime", BindingFlags.Static | BindingFlags.NonPublic);
            info.SetValue(null, value, null);
        }
    }

    public static void StopEffect()
    {
        var assembly = Assembly.GetAssembly(typeof(Editor));
        var util = assembly.GetType("UnityEditor.ParticleSystemEffectUtils");
        var info = util.GetMethod("StopEffect", BindingFlags.Static | BindingFlags.NonPublic, null, new Type[] { }, new ParameterModifier[] { });
        info.Invoke(null, null);
    }
}

拓展ObjectPreview

public class MyView : ObjectPreview
{
    PreviewRenderUtility prevRU;
    GameObject ins;

    Mesh fm;
    Texture2D ft;
    Material fma;

    static int prevCulLay = 31;

    bool isRunning = false;

    Editor e;

    ParticleSystem ps
    {
        get
        {
            var p = ins.GetComponent<ParticleSystem>();
            return p;
        }
    }

    public void BindEditor(Editor editor)
    {
        e = editor;
    }

    public void Repaint()
    {
        if (e != null)
            e.Repaint();
    }

    void DrawPlayBtn()
    {
        if (GUILayout.Button("Play"))
            Play();
        if (GUILayout.Button("Stop"))
            Stop();
    }

    public override void OnPreviewSettings()
    {
        using (new EditorGUILayout.HorizontalScope())
        {
            DrawPlayBtn();
        }
    }

    public void Play()
    {
        var p = ps;
        Stop();
        if (p != null && isRunning == false)
        {
            isRunning = true;
            LibUtil.lockedParticleSystem = p;
            p.Play();
            LibUtil.editorIsScrubbing = false;
            EditorApplication.update += Update;
        }
    }

    public void Stop()
    {
        var p = ps;
        if (p != null && isRunning == true)
        {
            LibUtil.editorIsScrubbing = false;
            LibUtil.editorPlaybackTime = 0f;
            LibUtil.StopEffect();
            isRunning = false;
            p.Stop();
            EditorApplication.update -= Update;
        }
    }

	//PreviewRenderUtility渲染
    public void Render()
    {
        var flag = StartLight();

        var viewDir = new Vector2(120f, -20f);
        var cam = prevRU.camera;

        var zoomFactor = 1.0f;
        var avatarScale = 1.0f;

        cam.nearClipPlane = 0.5f * zoomFactor;
        cam.farClipPlane = 100f * avatarScale;
        Quaternion rot = Quaternion.Euler(-viewDir.y, -viewDir.x, 0f);
        var camPos = rot * (Vector3.forward * -5.5f * zoomFactor); // + bodyPos + pivotPosOff;
        cam.transform.position = camPos;
        cam.transform.rotation = rot;

        var refIns = ins;

        Matrix4x4 mat = Matrix4x4.TRS(refIns.transform.position, Quaternion.identity, Vector3.one * 5f * avatarScale);
        var refPos = refIns.transform.position;
        fma.mainTextureOffset = -new Vector2(refPos.x, refPos.z) * 5f * 0.08f * (1f / avatarScale);
        fma.SetVector("_Alphas", new Vector4(0.5f * 1f, 0.3f * 1f, 0f, 0f));
        Graphics.DrawMesh(fm, mat, fma, prevCulLay, cam, 0);

        cam.Render();
        EndLighting(flag);
    }

    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
        if (prevRU == null)
        {
            CreatePrevRU();
        }

        prevRU.BeginPreview(r, background);
        Render();
        prevRU.EndAndDrawPreview(r);
    }

    public void Update()
    {
        if (!isRunning)
            return;

        if (ps != null)
        {
            Repaint();
        }
    }

    public override void Initialize(UnityEngine.Object[] targets)
    {
        base.Initialize(targets);
    }

    public void CreatePrevRU()
    {
        if (prevRU != null)
            return;

        prevRU = new PreviewRenderUtility(true);
        prevRU.cameraFieldOfView = 30.0f;

        var cam = prevRU.camera;
        cam.cullingMask = 1 << prevCulLay;
        cam.allowHDR = false;
        cam.allowMSAA = false;

        CreateIns();
        CreateFloor();
    }

    void CreateIns()
    {
        DestoryIns();
        ins = GameObject.Instantiate(target) as GameObject;
        SetUpInsArr(ins);
        prevRU.AddSingleGO(ins);
    }

    void CreateFloor()
    {
        fm = Resources.GetBuiltinResource<Mesh>("New-Plane.fbx");
        ft = (Texture2D)EditorGUIUtility.Load("Avatar/Textures/AvatarFloor.png");
        var s = EditorGUIUtility.LoadRequired("Previews/PreviewPlaneWithShadow.shader") as Shader;
        fma = new Material(s);
        fma.mainTexture = ft;
        fma.mainTextureScale = Vector2.one * 20f;
        fma.SetVector("_Alphas", new Vector4(0.5f, 0.3f, 0f, 0f));
        fma.hideFlags = HideFlags.HideAndDontSave;
    }

    bool StartLight()
    {
        Light[] lights = prevRU.lights;

        lights[0].intensity = 1.4f;
        lights[0].transform.rotation = Quaternion.Euler(40f, 40f, 0f);
        lights[1].intensity = 1.4f;
        Color ambient = new Color(0.1f, 0.1f, 0.1f, 0f);
        InternalEditorUtility.SetCustomLighting(lights, ambient);
        bool fog = RenderSettings.fog;
        Unsupported.SetRenderSettingsUseFogNoDirty(false);
        return fog;
    }

    void EndLighting(bool old)
    {
        Unsupported.SetRenderSettingsUseFogNoDirty(old);
        InternalEditorUtility.RemoveCustomLighting();
    }

    public void SetUpInsArr(GameObject go)
    {
        go.hideFlags = HideFlags.HideAndDontSave;
        go.layer = prevCulLay;

        foreach (Transform t in go.transform)
            SetUpInsArr(t.gameObject);
    }

    public void DestoryIns()
    {
        if (ins == null)
            return;

        GameObject.DestroyImmediate(ins);
        ins = null;
    }

    public override void Cleanup()
    {
        DestoryIns();
        if (prevRU != null)
        {
            prevRU.Cleanup();
            prevRU = null;
        }
        base.Cleanup();
    }
}

public class MyEditor : Editor
{
    MyView p;

    MyView preview
    {
        get
        {
            if (p == null)
            {
                p = new MyView();
                p.Initialize(targets);
                p.BindEditor(this);
            }
            return p;
        }
    }

    public override bool HasPreviewGUI()
    {
        return preview.HasPreviewGUI();
    }

    public override void OnPreviewSettings()
    {
        preview.OnPreviewSettings();
    }

    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
        preview.OnPreviewGUI(r, background);
    }

    public void Clearup()
    {
        p.Cleanup();
        p = null;
    }
}

EditorWindow拓展

public class MyWnd : EditorWindow
{
    [MenuItem("編輯器/MyWnd")]
    static public void PopUp()
    {
        var w = EditorWindow.GetWindow<MyWnd>("MyWnd");
        w.minSize = new Vector2(800, 600);
        w.Show();
    }

    MyEditor e;
    GameObject go;
    GameObject pf;

    private void OnGUI()
    {
        EditorGUI.BeginChangeCheck();
        pf = (GameObject)EditorGUILayout.ObjectField(pf, typeof(GameObject), false);
        if (EditorGUI.EndChangeCheck())
        {
            if (e != null)
            {
                e.Cleanup();
                e = null;
            }

            e = (MyEditor)Editor.CreateEditor(pf, typeof(MyEditor));
        }

        if (e)
        {
            e.OnPreviewSettings();
            e.OnPreviewGUI(GUILayoutUtility.GetRect(400, 400), EditorStyles.whiteLabel);
            Repaint();
        }
    }
}

動畫預覽

動畫預覽相對會簡單一點,U3D有一個AnimationClipEditor來預覽動畫,只是封在了庫裡沒有暴露出來。AnimationClipEditor需要一個AnimClip檔案和一個Avatar,比較麻煩。若是Avatar的GameObj上掛載Anim元件可以獲取到AnimClip,可以優化下直接拖入Avatar預覽動畫。

關鍵點

  • 使用AnimationClipEditor預覽動畫,使用反射呼叫庫函數優化直接拖入Avatar預覽動畫
  • AnimationClipEditor類名需要從Debug工具中檢視,比如VS斷點。然後到U3D的官方C#庫函數反射檔案中檢視
  • 跳轉連結:U3D官方C#反射檔案

程式碼範例

反射工具類

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Reflection;

public class CSRefUtil
{
    static public void SetValuePublic(object obj, string name, params object[] param)
    {
        var filed = obj.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public);
        filed.SetValue(obj, param);
    }

    static public void SetValuePrivate(object obj, string name, params object[] param)
    {
        var filed = obj.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
        filed.SetValue(obj, param);
    }

    static public T GetValuePublic<T>(object obj, string name, params object[] param)
    {
        return (T)obj.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public).GetValue(obj);
    }

    static public T GetValuePrivate<T>(object obj, string name, params object[] param)
    {
        return (T)obj.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic).GetValue(obj);
    }

    static public void SetPropertyPublic(object obj, string name, params object[] param)
    {
        var property = obj.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public);
        property.SetMethod.Invoke(obj, param);
    }

    static public void SetPropertyPrivate(object obj, string name, params object[] param)
    {
        var property = obj.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.NonPublic);
        property.SetMethod.Invoke(obj, param);
    }

    static public T GetPropertyPublic<T>(object obj, string name)
    {
        var property = obj.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public);
        return (T)property.GetMethod.Invoke(obj, new object[] { });
    }

    static public T GetPropertyPrivate<T>(object obj, string name, params object[] param)
    {
        var property = obj.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.NonPublic);
        return (T)property.GetMethod.Invoke(obj, new object[] { });
    }

    static public T CallMethodPublic<T>(object obj, string name, params object[] param)
    {
        var method = obj.GetType().GetMethod(name, BindingFlags.Instance | BindingFlags.Public);
        return (T)method.Invoke(obj, param);
    }

    static public T CallMethodPrivate<T>(object obj, string name, params object[] param)
    {
        var method = obj.GetType().GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic);
        return (T)method.Invoke(obj, param);
    }
}

編輯器視窗拓展

public class MyEditor : EditorWindow
{
    Editor previewAnimWnd;
    AnimationClip previewAnim;
    bool isPreviewAnimDirty = false;
    object avatarWnd;
    Vector2 AnimViewSize = new Vector2(800, 600);
    GameObject animGo;

    [MenuItem("編輯器/MyEditor")]
    public static void PopUp()
    {
        var win = GetWindow<MyEditor>();
        win.minSize = new Vector2(800, 800);
        win.Show();
    }

    void OnGUI()
    {
        DrawPreviewAnim();
    }
	// 清理資源
    void OnDisable()
    {
        animGo = null;
        previewAnim = null;

        if (previewAnimWnd != null)
            previewAnimWnd = null;

        if (avatarWnd != null)
            avatarWnd = null;
    }

    void SetPreviewAnim(AnimationClip anim)
    {
        previewAnim = anim;
        isPreviewAnimDirty = true;
    }
    

    public void DrawPreviewAnim()
    {
        EditorGUI.BeginChangeCheck();
        animGo = (GameObject)EditorGUILayout.ObjectField(animGo, typeof(GameObject), false);
        if (EditorGUI.EndChangeCheck())
        {
            if (animGo != null)
            {
                var animator = animGo.GetComponent<Animator>();
                var anim = animator.runtimeAnimatorController.animationClips[0];
                SetPreviewAnim(anim);
            }
        }

        if (isPreviewAnimDirty)
        {
            isPreviewAnimDirty = false;
            if (previewAnim != null)
            {
                previewAnimWnd = Editor.CreateEditor(previewAnim);
                previewAnimWnd.OnInspectorGUI();
                avatarWnd = CSRefUtil.GetValuePrivate<object>(previewAnimWnd, "m_AvatarPreview");
                CSRefUtil.CallMethodPrivate<object>(avatarWnd, "SetPreview", animGo);
            }
        }

        EditorGUILayout.BeginVertical(GUILayout.Width(AnimViewSize.x), GUILayout.Height(AnimViewSize.y));
		
		//繪製
        if (previewAnimWnd != null)
        {
            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.FlexibleSpace();
                previewAnimWnd.OnPreviewSettings();
            }
            AnimationMode.StartAnimationMode();
            previewAnimWnd.OnInteractivePreviewGUI(GUILayoutUtility.GetRect(AnimViewSize.x, AnimViewSize.y), EditorStyles.whiteLabel);
            AnimationMode.StopAnimationMode();
        }
        EditorGUILayout.EndVertical();
    }
}

備註

  • U3D對編輯器的很多功能都沒有介紹清楚,碰到問題建議Google搜尋或去討論群去問下。
  • 還有佈局縮排等不是很常見的功能,建議用到的時候去Google或者瀏覽官方檔案。
  • U3D有對編輯器UI樹形控制元件支援
  • 編輯器拓展開發量大可以考慮使用Unity編輯器擴充套件Odin外掛,不過是收費的
  • 新手在開發佈局比較複雜的編輯器可以考慮參考下老司機的程式碼,給出兩個供參考: