如果要说这一路走来还有什么我们视作理所当然的游戏元素还没出现过,那开始游戏时的主菜单肯定要算一个。
主菜单一般会视作是UI的一部分。我们常说的UI(User Interface,用户界面)一般指的是GUI(Graphical User Interface,图形用户界面)。也就是说用一些视觉元素呈现的用户界面。
与之相对的,像CLI(Conmmand-Line Interface,命令行界面)就没有我们熟悉的这些按钮、窗口等视觉元素。但是它也算界面。
命令行界面一般用文字展示
Interface由inter和face组成,在中文中除了叫“界面”,也称“接口”。这个术语绝非计算机科学专用的,实际上计算机领域中可能接口的说法用得更多。但是无论如何,interface的往往都是作为沟通两个系统的桥梁。
这个也叫interface
下面我们来试着制作一个简单的开始菜单。
这次新建场景时,我们可以先别急着选择Node2D了。这次我们选择User Interface(用户界面)选项。在新场景中,我们的场景根节点是一个Control。Control是Godot中所有UI节点的基类。Control一般翻译为“控件”。Godot中内置了多种常见的控件来帮助我们构建UI,我们也可以自己构建自己的控件(也就是从Control派生新的控件出来)。我们把这个场景的根节点改成MainMenu或者其它你喜欢的名字,然后保存。
简单设计一下,我们可能希望画面上方是我们游戏的标题或者logo。然后下方是堆叠起来几个按钮,它们分别负责常见的开始游戏、继续游戏、设置、退出等工作。
Label
Label控件用于展示简单的文字。其本意是标签。我们先创建一个Label用于展示游戏标题。添加Label节点后可以在检视面板中看到Text属性,它展示了一个文本框用于输入文本内容。你想给游戏取啥名字就输入什么。
Text属性下方有一些常见的文本相关属性。例如可能是最常用的水平/垂直对齐选项。我们调成居中就好。默认情况下,新加入的Label节点应该在左上方。和其它节点一样,我们可以直接按下w或者选择移动工具来调整它在画面中的位置。移动过程中,可以启用网格吸附工具来辅助定位。
你会发现可能并不是那么好居中,但是现在姑且调整个大概位置。我们稍后会讲到如何正确地调整UI元素的位置。
调整字体大小
作为标题的文字我们可能希望它大一点。不过不得不说Godot中要想调整字体大小并不那么直接。Godot中的UI元素都会引用一种叫主题的资源,它们都有一种默认的资源。这种资源中定义了各种UI元素的默认外观。
在检视面板,Control部分的Theme下可以找到主题属性。虽然默认值为空,但是实际上也引用了默认主题。你可以试着新建一个主题,实际上就是默认主题:
当然这里我们暂时不介绍自定义主题,所以它的主题属性留空就好。顺带一提Godot编辑器也是用Godot开发的,你也可以在编辑器设置中找到编辑器用的主题设置。
如果只是想调整某一个控件的字体,我们需要覆盖主题原本的字体大小设置。我们需要在检视面板找到Theme Override,然后勾选Font Size表示我们要无视主题的设置自行调整字体大小:
现在标题看起来应该大一点了。
Button
我可以这么说,但凡是个用来开发GUI的东西,99%的可能性都有叫Button的东西(不过用来展示一般文本的东西在不同的框架里其实有很多不同的名字和变体)。
我们在下面放上三个按钮,分别是开始游戏、继续游戏和退出——就是添加三个Button节点。Button节点也有Text属性,就是按钮上面的文字,把它们修改成想要的文字然后调整位置:
响应按钮的信号
按下按钮之后通常都会发生些什么。我们先来实现最简单的退出游戏。毫无疑问,我们通过连接到按钮的信号来相应相关事件。为主菜单场景添加一个脚本,然后选中退出按钮(有好好给按钮节点命名吗),连接pressed信号,随后编写相关代码。退出游戏需要调用场景树的quit方法。没错就这么简单:
现在可以把主菜单设置为主场景(这里指的是Godot中的设为主场景选项,不是我们那个主场景),启动游戏,然后点击退出按钮我们就可以退出游戏了。
GUI最头疼的问题——画面尺寸变化
现在主流的带GUI的桌面操作系统都支持多窗口、多任务。我们还可以随时调节窗口的大小。如何让GUI中的元素根据环境变化来保持或者调整布局是一个非常复杂的课题。即使你计划开发一个强制全屏运行的游戏,仍然可能需要考虑不同的屏幕比例。
但是,启动游戏并调整窗口大小,我们的主菜单并没有发生离谱的走样。这主要是由于我们在开始学习时调整了游戏的拉伸模式以适应我们对小尺寸像素游戏素材的需求。当时我写道拉伸模式调整为canvas_item,但实际上文档中更推荐使用了像素美术的游戏选择viewport模式。不过如果不进行其他操作的话,viewport拉伸模式下UI的默认字体看起来会很“像素”,所以至少在这一系列文章中我们姑且保持canvas_item模式。启用了拉伸后,我们在开发过程中很多时候可以认为我们的画面会保持我们设定的窗口大小。
但如果你想要进一步了解GUI方面的知识,请先禁用拉伸。在项目设置的Display/Window/Stretch/Mode这里,调整为disabled。
另外不得不吐槽,Godot官方文档这部分很久没更新过。最新版本文档的GUI部分还是以前的老内容。而且API参考中相关部分也缺少很多内容。
现在启动游戏,调整窗口,我们就会发现我们的界面就是一坨大便了。
控件如何决定自己在画面中的位置呢?随便选中一个控件节点,然后在检视面板中找到Control部分下的Layout栏:
Layout是布局的意思,可以说控件的位置和大小主要就是受这些属性控制。
目前,Layout Mode(布局模式)设置为Position(位置)。简单来说在这种模式下,控件的位置就是你给它设置的位置——无论画面怎么变化,它们都不会移动。
选中任意一个控件,在选择模式下(按下快捷键Q或者点击左上方的鼠标图标),控件除了有个调整大小的框,还可以看到一个绿色的由四个针尖模样的东西组成的准星一样的东西:
这个东西就是控件的锚点(锚点是常用说法,但是实际上并不是“一个点”)。在刚才看到的Layout Mode选项中,除了Position,就是Anchors模式。其实在Position模式下,锚点也在发挥作用,只不过作用没那么大。因为Position模式下锚点始终在左上角,也就是(0, 0)处,我们的位置始终以此为基准计算。那么Anchors模式怎么用呢?
在右上方的这里可以找到Godot提供的锚点预设:
使用过Unity和Unreal的GUI系统、以及使用过各种GUI开发/设计工具的读者一定不会对它感到陌生。菜单被两条线分割为四个部分。左上部分是将控件锚定到四角和四边中心。右下部分是让控件延展开来铺满整个画面。右上部分是让控件在垂直方向上的上中下横向展开。左下部分是在水平方向上三个位置展开。
如果选中标题Label节点,然后选择第一行第二列处的预设,我们的标题就会锚定到画面上方中心:
设置锚点预设之后,标题自己就跑过去了。此时在检视面板中也可以看到,布局模式也发生了变化:
此时还多出了一个锚点预设属性,且已经设置为Center Top。
现在启动游戏,调整画面,可以发现我们的标题始终锚定到了画面上方的中间。
现在你依然可以直接调整控件的位置(Position)属性,画面发生变化、需要调整控件位置时,即使有锚点,Godot也会把它纳入考虑范围。
在编辑器中,锚定预设看似只有这么几种(虽然很多时候这些预设就已经够用了),但实际上锚点取值是无限多的。Layout Mode为Anchors时,在Anchor Preset的菜单中选择Custom我们就可以自行编辑锚点和偏移量。同时,如果前面已经设置了Anchor Preset,此时切换到Custom之后就能看到对应的锚点预设到底是怎么定义的。
可见,受锚点控制的控件的位置和尺寸如何变化会由锚点、偏移、增长方向共同决定。我们一个个来看。
Anchor Points
把anchor翻译成锚点的尴尬了。这个才是真正的锚“点”。
锚定点有四个,对应上下左右。在编辑器中可以看出,它们的取值范围均为0到1的小数。
能在代码里操作的东西不一定能在编辑器中操作,但是能在编辑器中操作的东西一定可以在代码中操作。这四个东西分别对应着Control节点的anchor_xx属性:
对于这四个好朋友,文档如是说:
将节点的某某边锚定到父控件的起点、中点、终点。当节点移动或尺寸发生变化时,它会控制某某边的偏移量如何更新。使用Anchor常量(枚举)可以更便捷地设置它们。
某某指的是上下左右,后同。首先我们了解到锚点是相对的,相对于父控件的。在我们这个场景中,所有小东西的父级控件都是根节点那个Control(你可能已经改成MainMenu了)。新建场景作为根节点的控件,锚点预设默认为Full Rect(占满整个矩形区域),也就是说它始终会占满整个画面。因此我们整个场景中的各个小控件的锚点在目前来说可以认为是针对整个画面来说的。
锚点的取值是归一化的(0到1),对应的某一边的起点、中点、终点对应的就是0、0.5、1。文档中提到的Anchor常量(枚举),实际上只定义了两个值:
起点就是0,终点就是1。如果你像我一样是把标题节点的Anchor Preset直接从Top Center改成Custom的的话,你可以在锚点属性中看到Top和Bottom均为0,即垂直方向上锚定到画面上边,Left和Right均为0.5,即水平方向上锚定到中间。造成的结果就是我们可以在2D视口中看到那四个尖尖在画面参考边界的上边的中点位置。
这四个尖尖实际上是可以分别移动的,在2D视口中直接移动控件的绿色尖尖也等于是在编辑锚点:
这里要劳烦你把它调整回去,我们换一个控件来展示它的作用。
无论怎样,四个锚点会确定画面中的一个矩形区域。由于它们是归一化的值,所以画面尺寸变化时这个区域的相对位置是不变的。接下来我们来介绍Anchors Preset下的Anchor Offset(锚点偏移)。
Anchor Offset
我们前面介绍了锚点可以控制控件的位置,但是没有介绍如何影响控件的大小。接下来用开始按钮做实验。如果我们把它的锚点设置为预设的Center,它会始终在画面中心,但是其大小不会变化。
假设,我们希望按钮的宽度始终占据画面宽度的40%,但是垂直方向上依然维持在画面中间。根据前面所学,在设置完锚点预设为Center之后,把预设改为Custom,我们可以把锚点按左、上、右、下依次设置为0.3、0.5、0.7、0.5。现在开始按钮变成了这样:
看起来有点怪。调整锚点的时候怎么大小也自动变了——但是不是我们希望的那样。现在先试着把Anchor Offsets全部改成0试一下。怎样?是不是按钮的高度不变,而宽度按照预期匹配上了锚点指定的画面占比?启动游戏调整窗口大小,可以看到,这个按钮会按照期望始终保持正确的宽度和垂直方向上的位置:
来看锚点偏移在文档中是如何描述的:
在某某边锚点的基础上,节点的某某边和其父级控件(某某边)的距离
也就是说控件在锚定之后,这个偏移量会以锚点为基础再加上去。如果偏移不为零,我们就可以期待它会贴到锚点所决定的边界上。如果我们把左边的偏移设为10,右边的偏移设为-10(控件右边在锚点右边的左边的话这个偏移就是负值),那么就可以达到一种边距的效果:
由于是应用到了锚点的基础之上,所以毫无疑问这个偏移也是动态的。
但此时你还有一个疑问。你说四个锚点决定一个矩形区域,但是如果两条对边的值如果相同的话,那么这个矩形的宽或高不就为0——上面按钮的锚点设置也是如此,那为什么偏移量全部为0时这个按钮还是有高度,并且它的高度自动放到了锚点区域中心(垂直方向上的0.5处)对齐呢?
Grow Direction
答案是Anchor Preset中的第三个选项Grow Direction。增长方向的描述是说如果控件的当前尺寸小于它的最小尺寸时,应当让它沿哪个方向展开。
在GDScript层面控件的默认最小尺寸是实现细节没有暴露出来,我们只能通过custom_minimum_size来自定义最小尺寸。如果不自定义最小尺寸,其最小尺寸还是交给控件自己决定。例如按钮的最小尺寸会根据上面的文字大小来决定。
我们设置锚点上下边的值为同一个值,且把偏移全部设为0时,上下锚点确定的宽度自然为0——但是我们按钮的最小高度要更大,所以此时需要展开来。Grow Direction会决定朝哪个方向展开。如果你前面和我一样是先将按钮的锚点预设设为Center然后直接变成Custom的话,这两个方向应该都被调整为了Both。如果你的情况和我不一样,可以看一下方向的值。
Both就是朝两个方向展开。因此这个按钮就会从画面中心朝上下展开,看起来就是按钮垂直方向的中点和画面垂直方向的中点对齐了。
设置为Bottom的话,它就会向下展开。此时按钮的上边就会和锚点区域对齐。其它方向道理类似。
现在动手实践一下。假设我们改变了UI设计,现在希望把按钮放到画面左侧,但是不完全靠到画面左边,画面变大时,按钮改变宽度,向右延展(毕竟向左可能会超出屏幕左边)。
那么就可以这样设置:
锚定区域的右边位于在横向的0.3处,高度始终在纵向中点。左偏移为20,保证按钮左侧离画面左边有一定距离。水平增长方向设置为右,这样宽度改变时按钮会向右变变宽而左侧不变。
锚点预设菜单中还有一个按钮叫设为当前比例:
点一下之后,锚点区域就变成了控件当前所确定的矩形区域:
有了前面的知识,这个按钮做了什么不言而喻。
这边还有个图标就是个锚的按钮,启用它再移动控件的话,就会带着锚点一起移动。
呼!相信这一段下来你对Godot的UI系统有了一个不错的了解。有了这些基础知识,使用各种控件、调整它们的布局也会更加得心应手。
当然,肯定还是直接设置画面的拉伸模式,然后直接用anchor方便!我们还是调回来继续吧!(至少把拉伸模式调回来)。
Container——控制一系列控件的布局
前面放的三个按钮我们都是随便放的,但是我们希望它们和开始按钮有同样的大小,只是在垂直方向上的位置不同,但是按钮之间又有相同的间距。当然,你可以手动用刚才学到的知识挨个设置锚点。但是我们可以用更方便的控件来管理它们的布局。
Control的继承树上有一系列叫做Container(容器)的控件,它们主要的作用就是调整其子节点的布局。
为了实现垂直方向上等间距放置多个按钮,我们可以使用VBoxContainer(V就是vertical的意思)。我们添加一个VBoxContainer,然后手动调整它的锚点:
纵向增长方向设为Bottom可以使得空间不够时让它向下增长。这里你不必和我一样,你也可以把Bottom设为和Top一样,这样不管怎样都让Container中的内容向下溢出好了。
然后把按钮交给这个Container。把一个控件放到一个Container中之后,它的锚点就无法调整了,它会受Container控制。
太棒了,按钮全部规规矩矩地放到了容器的区域内,并且有一定的间距:
VBoxContainer有一个叫Alignment(对齐)的属性:
说白了,它的意思其实是“从哪里开始排布子节点”。Begin就是从最上面开始放,Center和End不言而喻。类似的容器HBoxContainer是水平方向排列子节点,类似地,它的对齐方式是左、中、右。
Godot内置了很多不同的容器,你可以自行探索。
实现按钮功能
最后来实现按钮的功能。退出按钮已经完成了。我们要来实现开始游戏功能。我们希望按下开始游戏按钮后,进入我们之前做的主场景。
我们首先定义一个export出来的、类型为PackedScene的变量,便于我们在编辑器中设置要加载的主场景。前面的文章中介绍过,这里不单独重复。
具体切换场景的代码要怎么实现呢。Godot的场景树系统很简单直白,我们加入主场景,然后把菜单删掉就行:
Godot甚至还有个专门为这种情况使用的方法,只需要一句话:
有啥区别呢?就我们这里的需要来说,肯定是越简单越好,直接调用change_scene。使用add_child是常规的场景树操作方法,添加新场景之后,旧场景依旧存在,你要销毁它或者隐藏它都是你自己的事情。如果你的这个UI会经常用到,那么还是不要销毁,在必要的时候让它再次显示就好。但是主菜单这种东西,一场游戏中显示多于两三次的情况可能都不会太常见,所以我们直接调用change_scene,它会自动删除旧场景。
change_scene的另外一个版本是加载场景文件而非PackedScene变量。它的参数是场景资源文件的路径(res开头)。调用时Godot编辑器会自动提示文件路径:
现在我们有了一个主菜单,简直越来越像一个游戏了!