什么是IMGUI
我们平常使用的GUI,像是QT和wxWidgets这种,属于保留型GUI(Retianed Mode GUI
,简称RMGUI
),即它们将和GUI有关的状态都保存在内部,如果你想要改变状态的话,需要使用一系列的Set函数去更改。
而本文要介绍的是另一种老旧的,在CLI时代就存在的绘制GUI的方法:立即型GUI(immediate Mode GUI
,简称IMGUI
)。这类GUI不采用OO方式实现,而是过程式编程,并且也不会保留内部的状态,在每次绘制的时候都需要用户传入状态。这种类型的更多的适用于显示区域实时刷新的程序里面,例如游戏和CAD等。
举个栗子,如果是RMGUI创建按钮,并且在按钮按下设定事件的代码类似这样:
|
|
这种就是控件所有的状态都在内部,是典型的OO设计。
如果是IMGUI,则可能是这样的代码:
|
|
没有关于button的对象或者结构体,只有一个Button函数用于绘制Button(控件即是函数),并且返回触发在button上发生的事件。如果你不想显示button只需要不调用Button函数即可。
有关IMGUI最火的库就是Dear IMGUI,可以先去试着用一用。
IMGUI在目前游戏的应用属于“文艺复兴”,因为这种GUI方式最早是在CLI界面中使用的,直到后面的OO出现才被用的比较少。但是仍旧很有应用价值。
一般的RMGUI虽然好用,但是存在如下的缺点:
- 一般都很很庞大
- 自己从0开始造轮子的话很困难,当游戏中只需要一些小的GUI时,花时间去造RMGUI费时费力。
- 存储额外的,重复的状态。比如我要共享一份文本在各个TextEditor中,我就需要调用每个TextEditor的SetText方法让他们将这份文本拷贝一份到自己的内部。
而IMGUI实现起来则非常简单(通过本文即可实现一个小的IMGUI),而且各个控件之间的耦合非常低(毕竟控件不存储状态,自然就不需要控件本身和其他控件主动交互,耦合度大大的低),很容易拓展。
当然IMGUI也有缺点,那就是写的代码很杂乱:因为IMGUI自己不存储状态,这意味着所有的状态需要用户自己设置,写到最后代码中各种if让代码很凌乱。而且各种控件的代码摆在一起,让人头晕目眩。而且很难使用布局文件来自动布局。当然,用得好也是很强的,Unity的GUI就是IMGUI。
我的建议是,如果你的游戏一开始并没有考虑到使用GUI,但是后面出现了不多的GUI需求,也找不到合适的RMGUI搭配游戏的话(我就是这样),可以自己造一个小的IMGUI。如果是确定游戏或引擎中有大量的GUI需求,还是推荐用RMGUI。
开始编写IMGUI
对于IMGUI的介绍就到此为止,接下来我们着手用SDL2实现一个小的IMGUI。这部分内容也可以容易地使用其他绘图库实现。我这里用C++17作为编程语言,不过不会过多地涉及到C++的语法,大部分的语法是和C相通的(毕竟不使用OO),并且假设你对SDL2很熟悉。
本文的参考教程是Sol:IMGUI Turorial,但是他是用SDL1写的。
GUIState
首先我们需要一个GUIState结构体来存储整个IMGUI的状态:
|
|
里面包含了当前鼠标的位置,鼠标按键是否按下(我这里只检测鼠标左键,其他的按键请按需添加)。hot_item
是当前鼠标悬浮在上面的控件ID,active_item
是当前被按下的控件ID,不管鼠标现在是否在上面,只要在上面按下了没松开就记录下来。
然后我们要在我们的游戏循环里设置这些值:
|
|
第一个控件,Button
现在我们来创建第一个控件:Button,这代表着一个按钮。
|
|
我们的Button有一个ID,标识其位置和大小的xywh,显示的颜色color以及被按下时显示的颜色press_color。
这个函数的实现如下:
|
|
这个函数做了三件事:
- 通过鼠标的状态更改了GUIState
- 绘制了Button
- 返回了触发的Button事件
那么这个时候我们就可以这样使用我们的button:
|
|
这个时候窗口上会有一个button,按下之后会变成蓝色的。不过不会变回去。我们需要在游戏循环的开头和结尾进行一些处理:
|
|
就完成了,效果如下:
这里有个缺点:当一直按着Button的时候会一直输出"button clicked"。你可以在Button函数里对一直按下的情况作出处理。这里就不进行处理了。
再来一个控件,SlidBar
通过上面的Button的例子,想必你已经了解了基本控件的编写方法,我们这里再编写一个滑动条作为例子:
|
|
效果如下:
自动生成的ID
每次增加控件都需要声明一个全局的ID,着很麻烦。接下来就编写一个自动生成ID的宏:
|
|
这个宏使用__LINE__
获得行号,然后将行号变为ID。如果你定义了一个START_ID
,我们将从START_ID处开始计数,这个START_ID可以有效避免其他模块的ID的重复。
这个限制就是不能在一行内使用多次GEN_ID()
,不过对于我们这种简单的足够了。
按键的处理
最后我们尝试添加按键的处理。首先需要在UIState中增加有关成员:
|
|
kbd_item
记录了按键焦点所在的item。key
则记录了当前按键。
然后在游戏循环中注册一下新的成员:
|
|
这里要注意一个缺点:如果帧率低的话会导致按键的遗漏(因为我们是在事件循环外面处理的按键)。
然后更改一下SlidBar(这里不打算给Button添加按键事件了):
|
|
效果如下:
结束
本文到这里就结束了,如果需要更多的控件你可以自己再去编写。可以看到IMGUI写起来是很快的,我也十分乐意在游戏中去使用它。
所有的代码
下面是本文涉及的所有代码,只用到了SDL2,没有用到其他附加库。
|
|