深入了解IMGUI和编辑器自定义

The new Unity UI system has now been out for over a year. So I thought I’d do a blog post about the old UI system, IMGUI.

新的Unity UI系统现已推出一年多了。 所以我想写一篇关于旧UI系统IMGUI的博客文章。

Strange timing, you might think. Why care about the old UI system now that the new one is available? Well, while the new UI system is intended to cover every in-game user interface situation you might want to throw at it, IMGUI is still used, particularly in one very important situation: the Unity Editor itself. If you’re interested in extending the Unity Editor with custom tools and features, it’s very likely that one of the things you’ll need to do is go toe-to-toe with IMGUI.

您可能会认为计时奇怪。 既然新的UI系统可用,为什么还要关心旧的UI系统? 好吧,尽管新的UI系统旨在涵盖您可能想扔给它的每种游戏内用户界面情况,但仍使用IMGUI,尤其是在一种非常重要的情况下:Unity编辑器本身。 如果您对使用自定义工具和功能扩展Unity编辑器感兴趣,则很有可能需要做的一件事情就是与IMGUI保持一致。

立即进行 (Proceeding Immediately)

First question, then: Why is it called ‘IMGUI’? IMGUI is short for Immediate Mode GUI. OK, so, what’s that? Well, there’s two major approaches to GUI systems: ‘immediate’ and ‘retained.’

那么,第一个问题是:为什么将其称为“ IMGUI”? IMGUI是立即模式GUI的缩写。 好,那是什么? 嗯,GUI系统有两种主要方法:“立即”和“保留”。

A retained mode GUI is one in which the GUI system ‘retains’ information about your GUI: you set up your various GUI widgets – labels, buttons, sliders, text fields, etc – and then that information is kept around and used by the system to render the screen, respond to events, and so on. When you want to change the text on a label, or move a button, then you’re manipulating some information which is stored somewhere, and when you’ve made your change then the system carries on working in its new state. As the user changes values and moves sliders, the system simply stores their changes, and it’s up to you to query the values or respond to callbacks. The new Unity UI system is an example of a retained mode GUI; you create your UI.Labels, UI.Buttons and so on as components, set up them up, and then just let them sit there, and the new UI system will take care of the rest.

保留模式GUI是一种GUI系统,其中GUI系统“保留”有关您的GUI的信息:您设置各种GUI小部件(标签,按钮,滑块,文本字段等),然后该信息将保留并由系统使用渲染屏幕,响应事件等。 当您想更改标签上的文本或移动按钮时,您将需要处理一些存储在某处的信息,当您进行更改后,系统将以新状态继续工作。 当用户更改值并移动滑块时,系统仅存储其更改,这取决于您查询值或响应回调。 新的Unity UI系统是保留模式GUI的示例; 您可以将UI.Labels,UI.Buttons等创建为组件,对其进行设置,然后将它们放在那里,新的UI系统将负责其余的工作。

Meanwhile, an immediate mode GUI is one in which the GUI system generally does not retain information about your GUI, but instead, repeatedly asks you to re-specify what your controls are, and where they are, and so on. As you specify each part of the UI in the form of function calls, it is processed immediately – drawn, clicked, etc – and the consequences of any user interaction returned to you straight away, instead of you needing to query for it. This is inefficient for a game UI – and inconvenient for artists to work with, as everything becomes very code-dependent – but it turns out to be very handy for non-realtime situations (like Editor panels) which are heavily code-driven (like Editor panels) and want to change the displayed controls easily in response to current state (like Editor panels!) so it’s a good choice for things like heavy construction equipment. No, wait. I meant, it’s a good choice for Editor panels.

同时, 即时模式GUI是一个在GUI系统一般不会保留你的GUI的信息,而是反复要求您重新指定您的控件是什么,他们在哪里,等等。 当您以函数调用的形式指定UI的每个部分时,会立即对其进行处理(绘制,单击等),并且任何用户交互的后果都会立即返回给您,而无需您进行查询。 这对于游戏UI而言效率低下-由于一切都变得非常依赖于代码,因此对于艺术家来说不方便使用-但事实证明,这在很大程度上受代码驱动的非实时情况(如“编辑器”面板)非常方便编辑器面板),并希望根据当前状态轻松更改显示的控件(例如编辑器面板!),因此对于重型建筑设备等而言,它是一个不错的选择。 不,等等 我的意思是,这是“编辑器”面板的不错选择。

If you want to know more, Casey Muratori has a great video where he discusses some of the upsides and principles of an Immediate Mode GUI. Or you can just keep reading!

如果您想了解更多,Casey Muratori会提供一个精彩的视频 ,他在其中讨论即时模式GUI的一些优点和原理。 或者,您可以继续阅读!

每一个事件 (Every Event-uality)

Whenever IMGUI code is running, there is a current ‘Event’ being handled – this could be something like ‘user has clicked the mouse button,’ or something like ‘the GUI needs to be repainted.’ You can find out what the current event is by checking Event.current.type.

每当运行IMGUI代码时,都会处理当前的“事件” ,这可能是“用户单击鼠标按钮”或“需要重新绘制GUI”之类的内容。 您可以通过检查Event.current.type来了解当前事件是什么。

Imagine what it might look like if you’re doing a set of buttons in a window somewhere and you had to write separate code to respond to ‘user has clicked the mouse button’ and ‘the GUI needs to be repainted.’ At a block level it might look like this:

想象一下,如果您正在某个窗口的某个窗口中执行一组按钮,并且必须编写单独的代码来响应“用户单击了鼠标按钮”和“需要重新绘制GUI”,那会是什么样子。 在块级别,它可能看起来像这样:

深入了解IMGUI和编辑器自定义_第1张图片

Writing these functions for each separate GUI event is kinda tedious; but you’ll notice that there’s a certain structural similarity between the functions. Each step of the way, we are doing something relating to the same control (button 1, button 2, or button 3). Exactly what we’re doing depends on the event, but the structure is the same. What this means is that we can do this instead:

为每个单独的GUI事件编写这些功能有点繁琐。 但是您会注意到这些函数之间存在一定的结构相似性。 在此过程的每个步骤中,我们都在做与同一控件(按钮1,按钮2或按钮3)有关的事情 。 确切地说,我们在做什么取决于事件,但是结构是相同的。 这意味着我们可以改为:

深入了解IMGUI和编辑器自定义_第2张图片

We have a single OnGUI function which calls library functions like GUI.Button, and those library functions do different things depending on which event we’re handling. Simple!

我们只有一个OnGUI函数,该函数调用诸如GUI.Button之类的库函数,并且这些库函数根据我们正在处理的事件来做不同的事情。 简单!

There are 5 event types that are used most of the time:

大多数时间使用5种事件类型 :

EventType.MouseDown Set when the user has just pressed a mouse button.
EventType.MouseUp Set when the user has just released a mouse button.
EventType.KeyDown Set when the user has just pressed a key.
EventType.KeyUp Set when the user has just released a key.
EventType.Repaint Set when IMGUI needs to redraw the screen.
EventType.MouseDown 用户刚刚按下鼠标按钮时设置。
EventType.MouseUp 在用户刚刚释放鼠标按钮时设置。
EventType.KeyDown 当用户按下按键时设置。
EventType.KeyUp 在用户刚刚释放键时设置。
EventType.Repaint 在IMGUI需要重绘屏幕时设置。

That’s not an exhaustive list – check the EventType documentation for more.

那不是一个详尽的列表–有关更多信息,请查看EventType文档 。

How might a standard control, such as GUI.Button, respond to some of these events?

标准控件(如GUI.Button )如何响应其中的某些事件?

EventType.Repaint Draw the button in the provided rectangle.
EventType.MouseDown Check whether the mouse is within the button’s rectangle. If so, flag the button as being down and trigger a repaint so that it gets redrawn as pressed in.
EventType.MouseUp Unflag the button as down and trigger a repaint, then check whether the mouse is still within the button’s rectangle: if so, return true, so that the caller can respond to the button being clicked.
EventType.Repaint 在提供的矩形中绘制按钮。
EventType.MouseDown 检查鼠标是否在按钮的矩形内。 如果是这样,请将按钮标记为按下并触发重新绘制,以便在按下时重新绘制它。
EventType.MouseUp 取消将按钮标记为按下并触发重新绘制,然后检查鼠标是否仍在按钮的矩形内:如果是,则返回true,以便调用者可以响应被单击的按钮。

The reality is more complicated than this – a button also responds to keyboard events, and there is code to ensure that only the button that you initially clicked on can respond to the MouseUp – but this gives you a general idea. As long as you call GUI.Button at the same point in your code for each of these events, with the same position and contents, then the different behaviours will work together to provide all the functionality of a button.

实际情况要比这复杂得多-按钮还可以响应键盘事件,并且有代码可以确保只有您最初单击的按钮才能响应MouseUp但这给了您一个大概的想法。 只要您针对这些事件中的每个事件在代码的同一点调用GUI.Button ,并且它们具有相同的位置和内容,那么不同的行为将协同工作以提供按钮的所有功能。

To help with tying these different behaviours together under different events, IMGUI has the concept of a ‘control ID.’ The idea of a control ID is to give a consistent way to refer to a given control across every event type. Each distinct part of the UI that has non-trivial interactive behaviour will request a control ID; it’s used to keep track of things like which control currently has keyboard focus, or to store a small amount of information associated with a control. The control IDs are simply awarded to controls in the order that they ask for them, so, again, as long as you’re calling the same GUI functions in the same order under different events, they’ll end up being awarded the same control IDs and the different events will sync up.

为了帮助在不同事件下将这些不同行为联系在一起,IMGUI具有“控件ID”的概念。 控件ID的想法是提供一种一致的方式来引用每种事件类型的给定控件。 UI中具有非平凡的交​​互行为的每个不同部分都将请求控件ID。 它用于跟踪当前哪个控件具有键盘焦点的事物,或存储与控件相关的少量信息。 控件ID只是按其要求的顺序授予控件,因此,再一次,只要您在不同事件下以相同顺序调用相同的GUI函数,它们最终将被授予相同的控件ID和其他事件将同步。

定制控制难题 (Custom Control Conundrum)

If you want to create your own custom Editor classes, your own EditorWindow classes, or your own PropertyDrawer classes, the GUI class – as well as the EditorGUI class – provides a library of useful standard controls that you’ll see used throughout Unity.

如果要创建自己的自定义Editor类, EditorWindow类或PropertyDrawer类,则GUI类以及EditorGUI类均提供了有用的标准控件库,您将在整个Unity中使用它们。

(It’s a common mistake for newbie Editor coders to overlook the GUI class – but the controls in that class can be used when extending the Editor just as freely as the controls in EditorGUI. There’s nothing particularly special about GUI vs EditorGUI – they’re just two libraries of controls for you to use – but the difference is that the controls in EditorGUI cannot be used in game builds, because the code for them is part of the Editor, while GUI is a part of the engine itself).

(这是一个常见的错误为新手编辑程序员忽略了GUI类-但在类的控件可以用来一样自由伸展的编辑器作为控件时EditorGUI没有什么特别之处。 GUI VS EditorGUI -他们只是两个控件库供您使用–但不同之处在于, EditorGUI中的控件无法在游戏版本中使用,因为它们的代码是Editor的一部分,而GUI是引擎本身的一部分)。

But what if you want to do something that goes beyond what’s available in the standard library?

但是,如果您想做一些超出标准库可用范围的事情该怎么办?

Let’s explore how we might create a custom user interface control. Try clicking and dragging the coloured boxes in this little demo:

让我们探讨如何创建自定义用户界面控件。 尝试单击并拖动此小演示中的彩色框:

(You’ll need a browser with WebGL support to see the demo, like current versions of Firefox).

(您需要使用支持WebGL的浏览器来观看演示,例如当前版本的Firefox)。

These custom sliders each drive a separate ‘float’ value between 0 and 1. You might want to use such a thing in the Inspector as another way of displaying, say, hull integrity for different parts of a spaceship object, where 1 represents ‘no damage’ and 0 represents ‘totally destroyed’ – having the bars represent the values as colours may make it easier to tell, at a glance, what state the ship is in. The code for building this as a custom IMGUI control that you can use like any other control is pretty easy, so let’s walk through it.

这些自定义滑块每个都驱动一个介于0和1之间的单独的“ float”值。您可能想在Inspector中使用这种方式,作为另一种显示飞船对象不同部分的船体完整性的方式,其中1表示“否”。损坏”,0表示“完全破坏”-用颜色表示颜色的条形图可以使您一眼便知道船处于什么状态。将其构建为可自定义IMGUI控件的代码就像其他任何控件一样,它非常简单,因此让我们逐步了解一下。

The first step is to decide upon our function signature. In order to cover all the different event types, our control is going to need three things:

第一步是确定我们的功能签名。 为了涵盖所有不同的事件类型,我们的控件将需要三件事:

  • a Rect which defines where it should draw itself and where it should respond to mouse clicks.

    一个Rect ,它定义应该在哪里绘制以及应该对鼠标单击做出响应的位置。

  • the current float value that the bar is representing.

    条形图表示的当前float值。

  • a GUIStyle, which contains any necessary information about spacing, fonts, textures, and so on that the control will need. In our case that includes the texture that we’ll use to draw the bar. More on this parameter later.

    GUIStyle ,其中包含GUIStyle需要的有关间距,字体,纹理等的任何必要信息。 在我们的示例中,包括用于绘制条的纹理。 稍后将详细介绍此参数。

It’s also going to need to return the value that the user has set by dragging the bar. That’s only meaningful on certain events like mouse events, and not on things like repaint events; so by default we’ll return the value that the calling code passed in. The idea is that the calling code can just do “value = MyCustomSlider(... value ...)” without caring about the event that is happening, so if we’re not returning some new value set by the user, we need to preserve the value that currently stands.

还需要通过拖动条来返回用户设置的值。 这仅对某些事件(如鼠标事件)有意义,而对诸如重画事件等事件则无意义。 因此默认情况下,我们将返回调用代码传入的值。这个想法是调用代码可以执行“ value = MyCustomSlider(... value ...) ”而无需关心正在发生的事件,因此如果我们不返回用户设置的一些新值,则需要保留当前保留的值。

So the resulting signature looks like this:

因此,生成的签名如下所示:

public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style) public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style)

1

public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style)

1

public static float MyCustomSlider ( Rect controlRect , float value , GUIStyle style )

Now we begin implementing the function. The first step is to retrieve a control ID. We’ll use this for certain things when responding to the mouse events. However, even if the event being handled isn’t one we actually care about, we must still request an ID anyway, to ensure that it isn’t allocated to some other control for this particular event. Remember that IMGUI just dishes out IDs in the order they’re requested, so if you don’t ask for an ID it’ll end up being given to the next control instead, causing that control to end up with different IDs for different events, which is likely to break it. So, when requesting IDs, it’s all-or-none – either you request an ID for every event type, or you never request it for any of them (which might be OK, if you’re creating a control that is extremely simple or non-interactive).

现在我们开始实现该功能。 第一步是获取控件ID。 在响应鼠标事件时,将在某些事情上使用它。 但是,即使处理的事件不是我们真正关心的事件,我们仍然仍然必须请求一个ID,以确保不会将其分配给该特定事件的其他控件。 请记住,IMGUI只是按照请求的顺序分配ID,因此,如果您不要求提供ID,它将最终被分配给下一个控件,从而导致该控件针对不同的事件以不同的ID结束,这很可能会破坏它。 因此,在请求ID时,它是全有还是无-您要么为每种事件类型请求一个ID,要么从不为任何事件类型请求ID(如果您要创建的控件非常简单,或者非互动)。

{ int controlID = GUIUtility.GetControlID (FocusType.Passive); { int controlID = GUIUtility.GetControlID (FocusType.Passive);

1

2
{
int controlID = GUIUtility.GetControlID (FocusType.Passive);

1

2
{
int controlID = GUIUtility . GetControlID ( FocusType . Passive ) ;

The FocusType.Passive being passed as a parameter there tells IMGUI what role this control plays in keyboard navigation – whether it’s possible for the control to be the current one reacting to keypresses. My custom slider doesn’t respond to key presses at all, so it specifies Passive, but controls that respond to key presses could specify Native or Keyboard. Check the FocusType docs for more info on them.

FocusType.Passive作为参数传递的FocusType.Passive告诉IMGUI该控件在键盘导航中扮演的角色-该控件是否有可能是当前对按键做出React的控件。 我的自定义滑块完全不响应按键,因此它指定了Passive ,但是响应按键的控件可以指定NativeKeyboard 。 查看FocusType文档以获取有关它们的更多信息。

Next, we do what the majority of custom controls will do at some point in their implementation: we branch depending on the event type, using a switch statement. Instead of just using Event.current.type directly, we’ll use Event.current.GetTypeForControl(), passing it our control ID; this filters the event type, to ensure that, for example, keyboard events are not sent to the wrong control in certain situations. It doesn’t filter everything, though, so we will still need to perform some checks of our own as well.

接下来,我们将执行大多数自定义控件在实现过程中的某些操作:我们使用switch语句根据事件类型进行分支。 而不是仅仅使用Event.current.type直接,我们将使用Event.current.GetTypeForControl()通过它我们的控制ID; 这将过滤事件类型,以确保例如在某些情况下不会将键盘事件发送给错误的控件。 但是,它不会过滤所有内容,因此我们仍然需要对自己进行一些检查。

switch (Event.current.GetTypeForControl(controlID)) {​ switch (Event.current.GetTypeForControl(controlID)) {​

1

2
switch (Event.current.GetTypeForControl(controlID))
{

1

2
switch ( Event . current . GetTypeForControl ( controlID ) )
{

Now we can begin implementing the specific behaviours for the different event types. Let’s start with drawing the control:

现在,我们可以开始为不同的事件类型实现特定的行为。 让我们从绘制控件开始:

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.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; }

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;

你可能感兴趣的:(python,java,编程语言,面试,javascript,ViewUI)