9

U3D编辑器开发&粒子特效/动画预览器示例 - 寡人正在Coding

 1 year ago
source link: https://www.cnblogs.com/hggzhang/p/17020747.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
image

U3D提供了一套拓展编辑器的接口,可以用于直接在编辑器非播放模式运行程序。常用于运行一些工具程序,例如资源管理。在做技能编辑器等工具程序时,也可以使用运行模式接口会比较简单(这样也方便开放游戏创意工坊给玩家)。使用编辑器去做一些渲染相关的预览(如粒子系统,动画预览)会麻烦一点,有时候需要查询和反射使用U3D引擎未暴露的接口。
U3D编辑器相关官方文档查询链接:https://docs.unity3d.com/cn/current/Manual/GUIScriptingGuide.html

业务需求分析

image

这是常用的一些需求和接口,多数的用法比较简单的在这里简单介绍一下。

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);
        }
    }
}
image

Editor和ExecuteInEditMode特性

  • ExecuteInEditMode特性:让脚本在编辑器模式运行。
  • Editor:封装一些UI绘制的接口,如(重新OnInspectorGUI以在Inspector窗口绘制UI界面)。

Editor和ExecuteInEditMode特性相关官方介绍

EditorWindow

继承EditorWindow重写以在编辑器创建绘制一个独立的编辑器窗口。

image
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进行绘制和提供更方便的操作,如下:

image
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窗口预览模型、动画、粒子特效会用到。例如这个特效的预览下文会具体介绍:

image

常用控件速查表

名称 代码 示例
Label GUI.Label (new Rect (25, 25, 100, 30), "Label"); image
Button if (GUI.Button (new Rect (25, 25, 100, 30), "Button")) image
TextField 单行输入框 str = GUI.TextField (new Rect (25, 25, 100, 30), str); image
TextArea 多行输入框 str = GUI.TextArea (new Rect (25, 25, 100, 30), str); image
Toggle 勾选框 b = GUI.Toggle (new Rect (25, 25, 100, 30), toggleBool, "Toggle"); image
HorizontalSlider 水平滑块 h = GUI.HorizontalSlider (new Rect (25, 25, 100, 30), hSliderValue, 0.0f, 10.0f); image
Toolbar 页签 Idx = GUI.Toolbar (new Rect (25, 25, 250, 30), Idx, BtnNames); image
SelectionGrid 平铺列表 Idx = GUI.SelectionGrid (new Rect (25, 25, 300, 60), Idx, Names, Cow); image
ScrollView 滚动列表 pos = GUI.BeginScrollView (rect, scrollViewVector, rectContent);
//XXX
GUI.EndScrollView();
image
Window 小窗口 w = GUI.Window (0, wRect, drawDele, "win"); image

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

布局速查表

布局的一般用法是,例如组Group

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

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

布局 用法 预览
组 Group 固定相对位置 image
区域 Area 用于自动布局一组控件,与组类似 -
水平布局 Horizontal 水平布局一组控件 image
垂直布局 Vertical 垂直布局一组控件 image

GUILayoutOption可以某些重写自动布局参数,例如:
GUILayout.Button ("My width has been overridden", GUILayout.Width (95));
GUILayout.Width重写了自动布局这个按钮的宽

特效预览窗口

U3D自带的特效预览方式是拖到Scene窗口上,有的时候做编辑器(例如技能编辑器)时需要预览的特效,切来切去太麻烦了。而且U3D拖到Scene才能预览这种方式也很麻烦,也可以自己拓展选中特效在Hir上预览。

image
  • 使用反射调用库函数绑定特效
  • 使用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();
        }
    }
}
image

动画预览相对会简单一点,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插件,不过是收费的
  • 新手在开发布局比较复杂的编辑器可以考虑参考下老司机的代码,给出两个供参考:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK