@游戏开发者,ImGUI 能成为 GUI 的未来吗?

640?wx_fmt=gif

对于游戏开发者来说,在开发过程中,加入 UI 的支持是不可或缺的一环,不过想要自己动手敲代码实现 UI 实属一件难事,后来 ImGUI 诞生为开发者们带来直接拿来用般的便利,而这是否意味着 ImGUI 即将去掉传统的 GUI 设计?

@游戏开发者,ImGUI 能成为 GUI 的未来吗?_第1张图片

作者 | nil

译者 | 苏本如,责编 | 屠敏

出品 | CSDN(ID:CSDNnews)

一个即兴的、未经深思熟虑的想法是:像Dear ImGUI这样的东西有可能是主流UI库的未来吗?

对于那些不知道什么是即时模式图形用户界面或ImGUI的人,可以看看Casey Muratori在2005年制作的一个还算有名的视频:https://youtu.be/Z1qyvQsjK5Y(需科学上网)。     

大多数使用ImGUI风格的程序员发现,使用ImGUI来创建用户界面比使用传统的保留模式图形用户界面(GUI)要容易得多。而且性能会得到显著地提高。

 

640?wx_fmt=png

 

典型的保留模式,面向对象的GUI框架是一个系统。在该系统中,你基本上创建了一个GUI框架控件(widget)的“场景图”(窗口、网格、滑块、按钮、复选框等等)。你将你的数据复制到这些控件中,等待事件(event)或回调(callback)在控件被编辑时接收到通知。然后查询控件的新值并将其复制回你的数据中。

这种模式几乎应用于所有的GUI系统中。Windows、WFP、HTML DOM、Apple UIKit、Qt,你能叫出名字的99%的GUI框架都属于保留模式的,面向对象的,“场景图”式的GUI。

这种模式的GUI存在的问题是:

  • 必须编写大量代码来管理GUI对象的创建和销毁。

    设想你有一个滚动列表,你经常需要创建100多个或1000多个GUI控件(就像HTML,创建一个TR,然后是TD,然后是每个TD的内容,等等)。如果数据真的很大,你最终不得不创建一些控件的虚拟窗口,要么在用户滚动时创建新的窗口并且删除旧窗口,要么将旧窗口从后面拉出来,然后将其添加到前面。其结果是:你需要写的代码太多了。

  • 创建和销毁对象导致UI反应迟缓。

    由于GUI对象的创建和销毁速度很慢(通常它们是非常大的对象),因此通常需要编写大量的代码来帮助寻找和设计解决方案,以最小化需要创建和销毁的对象数量。

    想想React如何使用虚拟DOM来识别差异,然后将这些差异应用到实际的GUI控件和DOM树/场景图中。

  • 你必须编制数据传入/传出控件。

    这就需要先将数据复制到控件中,然后对事件做出响应,并将控件中的新数据读回。需要编写更多的代码。

与此相反,ImGUI中没有对象,也几乎没有状态。大多数ImGUI的简单做法是像下面这样调用函数:

 
// draw a buttonif (ImGUI::Button("Click Me")) {  IWasClickedSoDoSomething();}// draw sliderImGUI::SliderFloat("Speed:" &someInstance.speed, 0.0f, 100.0f);
if (ImGUI::Button("Click Me")) {
  IWasClickedSoDoSomething();
}
// draw slider
ImGUI::SliderFloat("Speed:" &someInstance.speed, 0.0f, 100.0f);

这里的Button和Slider做了两件事:

  1. 它们将绘制控件所需的位置和纹理坐标添加到一个向量(数组)中。如果控件被裁剪出屏幕或在当前窗口/裁剪矩形之外,则坐标不被添加。

  2. 它们检查鼠标指针的位置、键盘状态等,以操作该控件。如果数据发生变化,它们会立即返回。

所以,这样做有如下优点:

  • 丝毫不需要分配内存,也即需要的内存为零!

  • 速度很快。即使使用非常复杂的UI并且只有单线程的情况下,大多数(如果不是全部)ImGUI在60fps(帧)的速度下运行没有任何问题。

  • 不需要对必须管理的对象进行创建和销毁操作。

  • 没有状态,因为没有对象来存储状态。

  • 基本不需要编制数据。

  • 没有需要注册或响应的事件或回调。

下面两点可能是这样做的缺点:

  • 可能需要更多的CPU。

    我还不能确信这一点总是对的。保留模式GUI设计的初衷是为了尽量减少工作量。假设你有一个类似微软Excel的用户界面。它有75个工具栏按钮和显示300个单元格的电子表格。输入光标位于单元格E7中,并且在闪烁。如果回到Windows 3.0(及更早版本),CPU将绘制像素(GPU那时不存在)。GUI系统确定只需要重新绘制光标本身大小的一些小区域,并且只需要将这些像素直接重新绘制到屏幕内存中。同样,如果键入字母,系统只能确定单元格E7已被修改,只需重新绘制单元格E7。

    在1993-1994年的计算机上,这点很重要。因为计算机无法以每秒60帧的速度绘制整个屏幕。

    因此,对于传统的基于“场景图”的面向对象的保留模式GUI来说,这是最好的做法。

    需要注意的是,系统仍然需要检查图形用户界面的大部分地方来计算最小的影响区域是什么。这可能不如重新绘制每个像素的工作量大,但需要的工作量也不少。

    ImGUI则相反,任何时候你想更改任何内容,整个图形用户界面就要重新绘制。即使是光标。以我们进入Excel示例,所有75个工具栏控件和300个单元格都将因为一个闪烁的光标而重新绘制。

    这是ImGUI的最坏情况。大量的CPU被浪费了。

    再拿滚动电子表格作个对比。

    在基于场景图的保留模式的图形用户界面中,假设您按下page down键,很可能300个单元格控件会被删除,300个新的单元格控件会被创建,每个单元格的数据将被复制到每个单元格控件中。从所有这些来看,GUI系统将遍历所有300个单元并将它们绘制出来。

    相反,在ImGUI的情况下,不会删除任何旧控件,不会创建任何新控件,也不复制任何数据, 300个单元格要像先前一样绘制出来。在这种情况下,ImGUI为更新整个显示页面所需要的CPU工作量仅仅是保留模式GUI系统工作量的十分之一至百分之一。

    哪种情况更常见呢?对于一个文本编辑器来说,通常只有很小的变化,所以场景图式的GUI会获胜。但是对于Instagram或Facebook应用程序,人们几乎经常滚动页面,在这种情况下,ImGUI以压倒性优势获胜。

  • 可访问性问题

    使用保留模式GUI,所有控件的数据都已复制到GUI的场景图中。这意味着GUI系统本身可以查看这些数据并提供不同的接口(比如放大,说出它,变成盲文,等等)。

    而使用ImGUI的情况下,通常GUI不保留任何数据,所以它可能做不了保留模式GUI能够做的那些事情。

    这可能是一个值得研究的地方。可能存在一些方案可以使ImGUI能够比传统方法更好地处理可访问性问题。大多数ImGUI用于游戏开发,它针对的对象是同一团队中的游戏开发人员,而不是最终用户。也就是说,没有动力去推动对这些解决方案的探索。

下面两点感觉是缺点,但可能不是:

  • 样式

    我不太确定大多数保留模式GUI是如何支持换肤的。可能是使用最具样式风格的,且包含了所有1000多个CSS选项的HTML DOM。

    对于ImGUI来说,样式是由你来设计的。添加更多的样式选项,甚至是几乎所有的CSS或者至少是好的那部分CSS,可能是相对容易实现的,而且能保持好的性能。更好的地方在于:你可以很容易选择需要这些样式或者不需要这些样式。所以如果你的应用程序不需要这样的样式,为什么要浪费内存或CPU时间来处理它呢?为什么要像大多数保留模式GUI那样,不管你使用如否,都要将所有的样式数据嵌入到每个控件中呢?考虑一下HTML,如果每个元素都有100个样式设置(毫不夸张确实有100个设置),那是一件多么可怕的事。

  • 动画

    大多数ImGUI都是无状态的,所以所有的动画都取决于应用程序。虽然很容易想到使用存有少量动画状态数据的包装器(wrapper)可以很容易地将UI动画放回。但是事实上,包装器可以让你选择只在重要的地方支持动画,比如样式。大多数保留模式的GUI都保存有大量的数据、状态和每个控件的设置,无论你使用如否。

对这点我真的很好奇。我知道大多数GUI框架作者都怀疑ImGUI是一个好的模式。而且据我所知,没有人真的尝试过它。如前所述,大多数的ImGUI用于游戏开发。而要想找到合适的模式来完全复制像苹果的UIKit这样的精致奢华的东西,需要各方的共同努力。这能做到并保持好的性能吗?把所有功能/特性添加回去会让它失去性能优势吗?ImGUI的基本设计是否意味着它最终将保持性能和易用性?如果没有场景图式的GUI,我们是否会发现某些功能/特性不可能真正地实现?

 

640?wx_fmt=png

 

我还要补充一点,在某种程度上,React在使用上类似于ImGUI。React有JSX,但它只是函数调用的简化。最大的区别在于:

  • 不需要渲染器,因为每个组件都会立即渲染。。

  • 不需要隐藏的虚拟DOM。

  • 不需要设置状态,因为它是无状态的。

  • 不需要附加事件或处理像componentWillMount (组件将要加载), componentDidMount(组件加载), componentWillUnmount(组件将卸载)等事件,因为没有组件,只有函数,也没有控件(DOM元素、本地对象等)。

如果我们把上面的代码翻译成假想的ImReact语言,它看上去可能像下面这样:

 
const Button = (props) => {  return ImGUI:Button(props.caption);};const SliderFloat = (props) => {  return ImGUI:SliderFloat(props.caption, props.value, props.min, props.max);};const Form = (props) => {  if () {    DoSomething();  }  };
  return ImGUI:Button(props.caption);
};

const SliderFloat = (props) => {
  return ImGUI:SliderFloat(props.caption, props.value, props.min, props.max);
};

const Form = (props) => {
  if ("Click Me">) {
    DoSomething();
  }
  "0" max="100" value="&props.speed" caption="Speed:" />
};

只要看一下React的代码,你会发现把它转换成真实的代码是非常简单的。

我不是非常确定对speed参数的更新是如何工作的,但我猜是将C++(ImGUI)和JavaScript(React)混合一起的作用。典型的ImGUI要么具有一种Javascript所不具有的模式,它能够传递进来一个指向原始值的指针,要么返回新值(代码如下):

 
newValue = ImGUI::SliderFloat(caption, currentValue, min, max);

如果你想使用和你编写的Dear IMGUI C++示例相同的用法,那么代码如下:

 
someInstance.speed = ImGUI::SliderFloat("Speed:", someInstance.speed, 0.0f, 100.0f);0.0f100.0f);

所以,如果我们假设了API的样式,那么代码可能是这样的:

 
const Button = (props) => {  return ImGUI:Button(props.caption);};const SliderFloat = (props) => {  return ImGUI:SliderFloat(props.caption, props.value, props.min, props.max);};const Form = (props) => {  if () {    DoSomething();  }  props.speed = ();};
  return ImGUI:Button(props.caption);
};

const SliderFloat = (props) => {
  return ImGUI:SliderFloat(props.caption, props.value, props.min, props.max);
};

const Form = (props) => {
  if ("Click Me">) {
    DoSomething();
  }
  props.speed = ("0" max="100" value="{props.speed}" caption="Speed:" />);
};

注意到上面的组件没有返回虚拟DOM节点,因为不需要。我们唯一真正要做的是JSX,它只是为了表明,如果你愿意,你可以使用React样式模式。

注意:在这个例子中,不要陷入直接的状态操作中。如何更新状态不应由UI库决定。不管你使用哪个GUI系统,你都可以自由地管理状态。这个例子显示了ImGUI样式是多么地简单。

 
state.value = ImGUI:SliderFloat(caption, value, min, max);value, min, max);

肯定比下面的保留模式GUI的实现要简单得多。

 
// at init timeconst slider = new SliderWidget(caption, state.value, min, max);slider.onChange = function(newValue) {  state.value = newValue;}// if state.value changed slider needs to show the new valuefunction updateSlider(newValue) {  state.value = newValue;}
const slider = new SliderWidget(caption, state.value, min, max);
slider.onChange = function(newValue{
  state.value = newValue;
}

// if state.value changed slider needs to show the new value
function updateSlider(newValue{
  state.value = newValue;
}

更糟糕的是,现在你需要以某种方式调用updateSlider函数,或者在每一处state.value被更新的地方调用它,或者你编写一个复杂的系统,以便所有需要更新state.value的地方都调用一个跟踪所有控件及其状态的函数。

ImGUI库不需要如此复杂。它不需要处理控件。无论状态中的值是什么,每个帧都是控件的内容。这与React的承诺相同。但是,React使用的保留模式GUI库性能很差,它最终被这一点所拖累。


640?wx_fmt=png

 

作为ImGUI可以实现复杂UI的一个例子,就是下面这个内容丰富的Unity编辑器界面。 

@游戏开发者,ImGUI 能成为 GUI 的未来吗?_第2张图片

这个例子表明,ImGUI在面向用户的应用程序中的使用也可以被优先考虑,而不仅仅只用在游戏应用中,即便Unity本身就是一个游戏制作软件。

这里的Readme文件(https://github.com/ocornut/imgui#gallery)中也包含各式各样的使用ImGUI制作的UI截图。

这里是Dear ImGUI库中包含的示例的在线版本:https://greggman.github.io/doodles/glfw-imgui/out/glfw-imgui.html。

@游戏开发者,ImGUI 能成为 GUI 的未来吗?_第3张图片

如果你想试着玩一下这个示例,请注意,它实际上不是为浏览器设计的,因此存在一些需要修复的问题。这些问题很容易解决,所以不要陷入吹毛求疵的小问题中。相反,你应该注意到它的UI非常复杂,但它能够以60帧的高速运行。使用主窗口中的“示例”菜单并打开更多窗口。展开主窗口中的示例,查看各种活动和复杂的控件。现在假想一下你试图使用HTML/DOM/React来制作同样复杂的UI。你会发现,不仅HTML/DOM版本会有很多卡顿,可能不可以60帧速度运行,而且实际实现的代码可能是多个维度代码的5到10倍。一个维度是使用HTML/DOM和/或React(vs. ImGUI)实现UI需要编写的代码量。另一个维度是要在屏幕上获得UI需要执行的代码量。我怀疑在HTML/DOM版本中执行的CPU指令量比ImGUI版本高出100倍。

比较一下ImGUI::Button函数和生成

你可能感兴趣的:(@游戏开发者,ImGUI 能成为 GUI 的未来吗?)