U3D提供了一套拓展編輯器的介面,可以用於直接在編輯器非播放模式執行程式。常用於執行一些工具程式,例如資源管理。在做技能編輯器等工具程式時,也可以使用執行模式介面會比較簡單(這樣也方便開放遊戲創意工坊給玩家)。使用編輯器去做一些渲染相關的預覽(如粒子系統,動畫預覽)會麻煩一點,有時候需要查詢和反射使用U3D引擎未暴露的介面。
U3D編輯器相關官方檔案查詢連結:https://docs.unity3d.com/cn/current/Manual/GUIScriptingGuide.html
這是常用的一些需求和介面,多數的用法比較簡單的在這裡簡單介紹一下。
程式碼化在視口繪製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特性相關官方介紹
繼承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 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 個人認為它是一個預覽視窗的渲染工具類,官方對其介紹幾乎沒有。但是在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上預覽。
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);
}
}
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;
}
}
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預覽動畫。
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();
}
}