

U3D编辑器开发&粒子特效/动画预览器示例 - 寡人正在Coding
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.


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 ();
}
}
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插件,不过是收费的
新手在开发布局比较复杂的编辑器可以考虑参考下老司机的代码,给出两个供参考:
Recommend
-
110
作者简介 善叶 蚂蚁金服数据前端 效果演示3D粒子动画效果如下: embed: sky.mov 本示例所构成并不复杂,主要分成以下几部分: 渐变的背景 不断切换的3D粒子模型(入场的散点也是粒子模型的一种形态) 同时切换的文字 实现主要是基于t
-
69
这大概是个序 关于Three.js,网上有不多不少的零碎教程,有些过于初级,有些云里雾里,而Three.js官网上的示例逼格之高又令人望而却步,这些对于没学过计算机图形学的童鞋来说,就像入门迈槛不知先迈左脚还是右脚,兴趣使然,于是我就先双脚蹦了进去试试水...
-
30
2020年01月15日 阅读 5594 【译】带有Flutter的粒子动画 原文链接:
-
41
1.粒子文本的实现原理 粒子文本的实现原理是:使用两张 canvas,一张是用户看不到的canvas1,用来绘制文本;另一张是用户看到的canvas2,用来根据canvas1中绘制的文本数据来生成粒子。 先在canvas1中用如下的语句绘...
-
11
UGUI研究院之UI粒子特效自适应缩放(二十二) 雨松MOMO
-
9
先看一下效果展示:
-
7
HTML动画特效(第一部) 原创 今天我将使用HTML,CSS和JavaScript做一个个性化的登录界面我们先来...
-
9
玩转Qml(14)-动画特效-梯度 本文于 1032 天之前发表,文中内容可能已经过时。...
-
8
这次涛哥将会教大家一些Qml动画相关的知识。 《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick github https://github.com/jaredtao/TaoQuick 访问不了或者速度太慢,可以用...
-
8
PS超炫酷粒子特效插件-Animation Particle Toolbox 8月 13, 2022 发表于: 优秀设计资源.
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK