【译介】深入理解IMGUI和编辑器扩展

0x00

原文地址

0x01

  • 什么是IMGUI?

0x02

  • IMGUI如何处理Event?
  • Control ID是什么?

Every Event-uality

Event.current.type获得当前事件,Evnet.current.GetTypeForControl()获得当前

0x03

  • Editor、EditorWindow、PropertyDrawer、GUI、EditorGUI都有什么功能?

GUI与EditorGUI区别在于GUI可以作为运行时工具构建到游戏中,而EditorGUI只能在编辑环境下使用。

GUILayout和EditorGUILayout可以用来自动布局。

EditorWindow用来派生创建自定义编辑器窗口。在代码触发显示后,可以实现工具的GUI代码显示窗口内容(OnGUI())。

以下内容来自Unity文档,即时模式GUI(IMGUI)

扩展GameView

运行时可以在继承了MonoBehaviour的脚本中,通过OnGUI()方法启动,Unity每帧都会调用OnGUI()方法,无需显式创建和销毁GUI控件。

坐标系基于左上角(关于屏幕坐标左上角为(0, 0),向右向下为正)。

声明控件的关键信息:Type (Position, Content)

Type

指的是控件类型(Control Type),过调用Unity的GUI类或 GUILayout类中的函数来声明该类型。

Position

是所有GUI控件函数的第一个参数,由Rect(左侧坐标, 顶部坐标, 宽度, 高度)函数描述。

所有这些值都以整数提供,对应于像素值。所有UnityGUI控件均在屏幕空间(Screen Space)中工作,屏幕空间表示发布时的游戏播放器的分辨率尺寸(以像素为单位,可由Screen.widthScreen.height获得)。

Content

指的是与控件一起显示的实际内容,通常是文本或图像(Texture)。

1
GUI.Label (new Rect (0,0,100,50), "This is the text string for a Label Control");

可提供GUIContent对象作为Content参数,并提供更加复杂的显示功能:

  • 定义要在GUIContent中显示的字符串和图像。
1
2
Texture2D icon;
GUI.Box (new Rect (10,10,100,50), new GUIContent("This is text", icon));
  • 定义鼠标悬停时显示的提示(Tooltip)

    GUIContent()创建的Tooltip可以通过GUI.tooltip获取,并通过GUI.Label()显示。

  • 同时显示字符串,图标和提示。

控件类型

以下内容来自Unity文档,控件

  • Label
  • Button
  • RepeatButton(保持点击状态的每一帧都将执行)
  • TextField(单行)
  • TextArea(多行)
  • Toggle
  • Toolbar(一行Button,一次只能有一个Button处于激活状态)
  • SelectionGrid(多行Toolbar)
  • HorizontalSlider(水平滑钮)
  • VerticalSlider(垂直滑钮)
  • HorizontalScrollbar(用于导航ScrollView控件)
  • VerticalScrollbar
  • ScrollView(需要两个Rect和2D矢量作为参数)
  • Window(可拖动的控件容器)

当用户执行了操作时,GUI.changed结果将获得true,因此可以轻松验证用户输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* GUI.changed 示例 */

using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{

private int selectedToolbar = 0;
private string[] toolbarStrings = {"One", "Two"};

void OnGUI ()
{
// 确定哪个按钮处于激活状态,是否在此帧进行了点击
selectedToolbar = GUI.Toolbar (new Rect (50, 10, Screen.width - 100, 30), selectedToolbar, toolbarStrings);

// 如果用户在此帧点击了新的工具栏按钮,我们将处理他们的输入
if (GUI.changed)
{
Debug.Log("The toolbar was clicked");

if (0 == selectedToolbar)
{
Debug.Log("First button was clicked");
}
else
{
Debug.Log("Second button was clicked");
}
}
}

}

0x04

  • 如何实现MyCustomSlider显示自定义滑动条?
  • FocusType?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style)
{
int controlID = GUIUtility.GetControlID(FocusType.Passive);
swich(Event.current.GetTypeForControl(controlID))
{
case EventType.Repaint:
{
// Work out the width of the bar in pixels by lerping
int pixelWidth = (int)Mathf.Lerp(1f, controlRect.width, value);

// Build up the rectangle that the bar will cover
// by copying the whole control rect, and then setting the width
Rect targetRect = new Rect(controlRect){ width = pixelWidth };

// Tint whatever we draw to be red/green depending on value
GUI.color = Color.Lerp(Color.red, Color.green, value);

// Draw the texture from the GUIStyle, applying the tint
GUI.DrawTexture(targetRect, style.normal.background);

// Reset the tint back to white, i.e. untinted
GUI.color = Color.white;

break;
}
case EventType.MouseDown:
{
// If the click is actually on us...
if (controlRect.Contains(Event.current.mousePosition)
// ...and the click is with the left mouse button (button 0)
&& Event.current.button == 0)
{
// ...then capture the mouse by setting the hotControl.
GUIUtility.hotControl = controlID;
}
break;
}
case EventType.MouseUp:
{
// If we were the hotControl, we aren't any more.
if (GUIUtility.hotControl == controlID)
{
GUIUtility.hotControl = 0;
}
break;
}
}

if (Event.current.isMouse && GUIUtility.hotControl == controlID)
{
// Get mouse X position relative to left edge of the control
float relativeX = Event.current.mousePosition.x - controlRect.x;

// Divide by control width to get a value between 0 and 1
value = Mathf.Clamp01(relativeX / controlRect.width);

// Report that the data in the GUI has changed
GUI.changed = true;

// Mark event as 'used' so other controls don't respond to it, and to trigger an automatic repaint.
Event.current.Use();
}

return value;
}

最后,简单的IMGUI control就实现好了,MyCustomSlider可以用在自定义Editors、PropertyDrawers、editorWindows等等。

0x05

  • SceneView的辅助UI元素,编辑器工具制作?

Handles类,用来在SceneView中绘制3D GUI。

0x06

  • PropertyDrawer如何使用?
  • 自定义Serializable类的每个实例的GUI。
  • 通过自定义的属性特性来自定义脚本成员的GUI。

对现有的可显示字段进行排版。

0x07

  • TreeView如何使用?

Unity文档-TreeView

TreeView不是树(数据结构)。可以使用所需的任何树数据结构来构造TreeView(c#或Unity Transform层级视图)

  • TreeView
  • TreeViewItem
  • TreeViewState - 可序列化的状态

  • BuildRoot - 创建TreeView的抽象方法,每次调用Reload时都会调用此方法

  • BuildRows - 虚拟方法,处理展开的行

0x08

  • Unity DrawDefaultInspector();是怎么绘制Editor的?

可以使用VS反编译出代码,通过serializedObject.GetIterator()得到的迭代器,使用EditorGUILayout.PropertyField(iterator, true)

通过ScriptAttributeUtility.GetHandler(property).OnGUILayout()方法绘制