在前面的文章中,我们的玩家角色已经大致完成,操作和动画都可以正常工作。可惜我们场景中的地板和柱子还是用的非常粗糙的占位素材,接下来我们就用素材包中的素材打造一个更像样的场景。
添加背景
我们先添加一个背景图片,现在先简单地放一个Sprite2D然后把背景素材PNG/environment/back给它。注意,在2D场景中,默认情况下后加入场景树的节点会显示在更上面。这里的“上面”指的是“离屏幕外面更近”。所以我们要把背景sprite放在最前面(编辑器场景树从上往下的“上面”),保证它在最下面。
用场景视图中的锚点对它进行缩放(最好是用四角上的锚点等比缩放),让它覆盖住表示游戏视口边缘的边框为佳。
目前来说这个背景的位置一旦调整好,我们就不会那么频繁地去修改它。因此可以点击一下这个锁定按钮防止无意中移动了它:
锁定之后就无法在编辑器中被选中和移动。
最后可以直接删掉之前的Platform场景节点。
Tile和TileMap
很多2D游戏(不管是不是像素风格)都会用到一种很有历史气息、很简单粗暴,但是很好用的方法来构造场景。它们会把场景中的很多东西视作是一些基本元素排列组合构造起来的。这些(通常)大小固定的小方块被称为tile,tile的本意是修房子时贴在地板、墙壁、屋顶上的那些各种材质的砖(或者瓦)。在游戏引擎中一般会提供各种工具来处理含有tile的spritesheet,开发者可以直接在编辑器中用这些tile来绘制场景。
在Godot中,我们需要一个叫TileMap的节点来完成这样的工作。为场景添加一个TileMap节点。注意其在节点树的位置,让它在背景上面,玩家下面。
TileMap需要一种叫TileSet的资源,由于我们可能会在多个场景中用到TileMap,并且它们都会重复用到一些TileSet,因此这里我们不直接在检视面板中新建(当然你也可以这样做),而是直接在文件系统中新建TileSet便于后续复用。
简单复习一下。在文件系统任意你喜欢的位置打开右键菜单选择Create New(新建),然后选择Resource(资源)。搜索找到TileSet并创建。名字可以叫他environment。然后我们就可以把它拖到场景中的TileMap节点的TileSet属性栏中。
选中TileMap节点的时候,下方的面板中会出现两个TileMap特有的面板,一个是TileSet,一个是TileMap。我们首先需要在TileSet面板中配置TileSet,它们会为TileMap提供可用的tile。
打开TileSet面板,点击左侧的加号,选择Atlas(本意为地图册)选项,在弹出的窗口中选择要用到的spritesheet。这里选择素材包中的PNG/layers/environment中的tileset。Godot会自动处理素材识别其中的tile,弹出的窗口直接选Yes。
此时一组新的tile已经加入tileset,右侧可以看到切分好的各个tile。如果觉得这个面板太小不好操作,可以点击右下方这个像“此面向上”的图标展开面板:
上方的几个按钮可以对TileSet进行不同的操作。目前选中的是Setup(配置),是对整个atlas的基本设置。可以调整tile的大小和边距等属性。由于这个素材是比较规整的,所以无需额外调整。
Select(选择)选项启用后可以选中某一个tile进行一些属性的调整。Paint(绘制)选项可以为各个tile绘制额外的属性。我们在后面会用到。
现在TileMap面板中也出现了刚才添加的tileset。我们可以用这些tile来绘制场景了。默认情况下选中TileMap节点时场景中会出现以tile大小为格子大小的网格(grid)。要绘制场景,在上方选择一个需要的绘制工具即可在格子上绘制。这些工具都是各种图像编辑软件中很常见的操作,这里一句话介绍一下。
铅笔图标是最简单直白的,选中一个tile,用铅笔在格子上点一下就画上去。右边的线段和方框是绘制一条线和绘制一个矩形。油桶就是填充一个空白。滴管,可以选取已经绘制的tile然后用它继续绘制。橡皮擦,嗯。顺时针逆时针就是撤消重做。两个箭头一个是水平翻转一个是垂直翻转。骰子图标是随机放一个tile。
如果你不确定某个tile是啥东西,可以参考素材包中的environment-preview图片。随心所欲地绘制自己的场景吧。不过大概要画一个类似平台的东西便于后续内容的学习。
碰撞
现在启动游戏,之前在tilemap上绘制的tile并不会和玩家发生碰撞,玩家会直接掉下去。这是因为我们还没有给tileset中的tile添加和物理碰撞相关的数据。选中TileMap节点,在检视面板中点击当前的TileSet,可以看到Physics Layers(物理层)部分目前为空。我们先了解一下什么是物理层。
物理层
Godot中和涉及物理系统的节点一般都有一个用于控制碰撞检测的层次的属性。在不同节点中名字可能不一样,但是都以这样的形式在检视面板中示人,比如选中Player场景中的CharacterBody2D节点打开Collision部分:
CharacterBody2D的这两个属性实际上是来自CollisionObject2D的`collision_layer`(碰撞层,下文简称layer或层)和`collision_mask`(碰撞掩码,下文简称mask或掩码)。
layer指的是该物件“所在”的层。mask指的是该物件要“扫描”(检测)的层。文档中总结地很好,引用之:
物件A可以检测到和物件B发生接触,当且仅当B位于任意一个A所检测的层中。
无论是从文档描述还是编辑器界面上都可以看出,无论是层还是掩码,都可以选择多个。一个东西可以位于多个层,也可以检测来自多个层的其他东西。默认状态下,layer和mask都设置在了1层。之前我们用来撞的柱子默认的layer和mask也是1,因此肯定会被玩家检测到而发生配置。
例如这样的场景中。玩家的layer和mask都是默认的1。第一个柱子layer为2,mask为1,但是玩家角色的mask为1,所以它扫描不到这个柱子。碰撞没有发生。第二个柱子layer为1,mask为空,但是玩家的layer为1,因此还是没有碰撞。第三个柱子,mask为2,但是layer为1,所以被玩家扫描到了,发生了碰撞。
在Area2D中由于需要发生重叠,这种层次设置可能会更加有用。我们可以设置层次让某个东西可以检测到指定的一类东西进入了范围中。
位运算光速入门
如果查看文档,我们会发现layer和mask的类型都是int,但是为什么编辑器中有32个层呢?
实际上涉及层的操作时都会按照位运算(bitwise operation)来进行操作。Godot中的int类型是64位有符号整数。这里的“64位”不是说它的最大值是“99(中间省略60个9)99”,而是说它的二进制表示最多可以有64位。它的取值范围是`-2^63`到`2^63 - 1`,即`-9223372036854775808` 到`9223372036854775807`。
我们平时用的十进制,怎么把一个数用二进制表示呢?简单起见我们只用比较小的正整数来举例。以十进制数11为例,它是什么意思呢?它实际上是1x10¹+1x10⁰。任意非0数的零次幂就是1。也就是说十进制数的每一位上的数字,就代表几个10的n-1次幂,n表示它从右往左数在第几位(1开始)上。
按照类似的道理,二进制数11的意思是就是说1x2¹+1x2⁰,对应的十进制数就是3。
十进制数11等于是8+0+2+1,也就是1x2³+0x2²+1x2¹+1x2⁰,二进制就从左往右按照每一位上的因数写作1011。
在GDScript中,整数字面量除了可以按照我们日常习惯的十进制来书写,也可以用二进制格式来书写。二进制格式的整数字面量以0b开头:
assert是什么函数你可能忘了,它是所谓断言函数,它的参数是一个布尔类型的表达式,其值为false时会直接报错中断游戏运行。
位运算最终要在二进制表示的数上操作,由于二进制只有1和0两个数字,所以它的位运算和布尔运算非常类似。布尔运算的与、或、非、反在位运算中也存在:
注意位运算符和布尔运算符的区别。Godot建议在布尔运算中建议使用英文单词and、or、not,但是它也支持在其他编程语言中常用的&&、||、!。这些运算在位运算中类似的运算使用的是&、|、~。
按位或(|),相当于每个二进制位上的数相加(不发生进位),也就是说两者中任一一个为1结果为1,同时为1也位1。
按位与(&),相当于每个二进制位上相乘,任一一个为0结果也为0。两者都是1才为1。
按位取反(~),每一位都把1变成0,把0变成1。~0为啥结果为-1呢?说白了它就是一个64位都为1的二进制数。计算机中的整数是用补码表示的。简单来说就是,负数的最高位为1,这一位代表-(2^63),后续位正常计算。当然如果要人来算一个负数的二进制表示要用到一些简便算法,那就是“取反加一”。
例如十进制+2,二进制表示为0b10,取反得(62个1)01。加1得(62个1)10(进一位,不是按位与运算),取反得到(前面62个1)01:
注意“加一”和“按位与”的区别,按位与不会出现进位,但是算术加法会产生进位。
这样一来就可以理解这个layer和mask了。它们会按照其值的后(低)32位来进行操作,编辑器中某个编号的层是否点亮就是对应某个位的1和0。
那掩码为啥叫掩码(mask),说白了就是要把它和另外一个值重合上去、覆盖上去进行比对(还记得冒险小虎队那个解密卡吗)。当一个东西要和另一个东西碰上时,要发生什么事,就是拿另一个东西的mask来和这个东西的layer进行按位与,对上的地方依然是1,如果一个位都没对上,那么结果就是0,也就不需要进行后续的操作了!
实际上Godot(以及其他各种游戏引擎)中都会用类似的原理来处理各种层,并且不只是物理系统的交互。
小知识:关于bit的中文说法
bit在(简体)中文中一般有两种常用说法。一种就是“位”,另一种是“比特”,繁体中文一般称“位元”。比特毫无疑问是音译,比如办宽带给你说“100兆宽带”指的是100Mbps(百万比特每秒),这里的b就是比特。注意这里的b和B是不一样的。一般来说B指的是Byte(字节),一字节为8比特。
不过我们谈论二进制算术的时候一般都是说“位”。
绘制TileSet属性
言归正传。TileSet可以定义多个物理层,然后将其作为数据附加到各个tile上(这个过程也被视作绘制)。点击tileset资源后选择Physics Layers栏目,点击Add Element新建一个物理层,layer和mask默认为1就好。
接下来就可以给具体的tile绘制物理层属性了。在TileSet面板中,启用Paint来绘制属性。点击Select a property editor选择一种属性编辑器。这里我们选择Physics Layer 0,这就是我们刚才新建的物理层。然后点击我们想要附加属性的tile,就能够给它加上物理层属性了。绘制后的tile会覆盖上一层绿(蓝)色:
这个和CollisionShape2D(准确地说是CollisionPolygon2D)类似,定义了一个检测碰撞的形状。绘制工具默认会给tile画上一个四边形覆盖整个tile。对于一些复杂的形状你也可以自行编辑多边形。
这里的图标分别用于添加顶点、编辑顶点、删除顶点。编辑完形状之后可以给tile画上更复杂的碰撞形状。如果怕画得不准可以启用吸附:
经过编辑,我可以给这样的tile画上更贴合的形状:
被画上Physics Layer属性的tile就相当于位于对应物理层,拥有对应的layer和mask属性。
绘制完成之后,再次启动游戏,我们的玩家角色就可以站在这些tile上了。
图案
在一个平台跳跃游戏中,类似的平台必然会重复出现多次。如果我们每次都要重复选择这几个tile然后挨个画就会很麻烦。
在Godot中可以把一系列tile组合成一个图案(pattern)。如你所见pattern要翻译成中文不止一个意思。前面提到的模式匹配的模式也叫pattern。找规律的规律也叫pattern。
由于之前你可能使用了绘制工具在场景中绘制,因此要选择已绘制的tile就要切换到选择工具:
选中的tile会显示为蓝色边框框住。不过我们的背景也是浅色的不太清楚。选择时可以拖动选择多个。选中后按下Ctrl+C复制,然后在Patterns标签页下面粘贴过去。
这样一来我们就可以直接把图案放到TileMap上了。选中Pattern然后选中需要的绘制工具就可以复用图案了。
地形
平台跳跃游戏中的这种平台往往是长短不一的。图案可以用,但是每个图案都是固定的,没法调整长度
Godot的解决方案是地形(terrain)。它可以配置相互关联的tile,让它们能够根据周围的情况自动调整。
先来看个简单的例子。这样的平台是由三个不同的tile构成。最左侧,中间,最右侧。要加长它,左右两端的tile不需要变化,只需要把中间的tile多重复几次即可。
为了避免每次都手动绘制,我们创建一个新的地形。地形数据是TileSet的一个属性,选中TileMap的TileSet,我们可以看到TerrainSet(地形集合)部分。TerrainSet由多个Terrain组成。一个TileSet可以有多个TerrainSet。
新建TerrainSet后再新建一个Terrain:
地形的Mode(模式)有三种选项。默认是“匹配侧面和角落”,另外两个选项是只匹配侧面和只匹配角落。模式会决定一个地形中tile依照哪里的情况来调整周围八个格子中的tile。目前默认也没有问题。
而Terrain Sets中的各个地形有两个属性可以调整,一个是名字一个是颜色。颜色建议调整成泥巴色以外的颜色,因为素材中的很多东西就是泥巴色的。
我们需要为TileSet中的各个tile设置它属于哪个地形。找到代表平台的这三个tile。我们准备把它们全部设为同一种地形中的tile,因此可以把它们全部选中然后批量编辑。此处我们在Select和Paint模式下都可以操作。
在Select模式下选中这三个tile,左侧面板中找到Terrain栏目后就可以选择Terrain Set和其下的具体Terrain。不过这里地形集合相关的数据显示为数字。-1表示“没有设置”。上面提到TileSet的Terrain Sets是一系列Terrain Set,各set中有一系列Terrain。这里的数字就是各元素的索引。我们目前只有一个地形集合,其中也只有一个地形,所以都调整为0就表示将它们设置为了对应地形中的tile:
如果你已经跟着我像这样设置了它们的所属地形数据,那么现在切换到Paint模式就可以看到它们身上有个数字显示它们属于0号地形。
如果使用绘制模式,在左侧选中Terrains属性,然后选择地形集合和地形即可像绘制物理碰撞形状那样把地形数据一个一个点给它们。
设置地形匹配位
只是设置tile所属地形还没法让它们自动调整。只有在设置好tile的“地形匹配位”(terrain peering bits)之后它们才能自动调整。
地形匹配位会决定一个tile在其周围八个格子处于何种状态时才会出现。尽管称作“位”,但实际上对应位置上的匹配位是一个数字,它对应的是地形编号。说起来有点抽象,我们实际来看一下。
地形匹配位也可以在Select和Paint两种模式下设置,不过在绘制模式中要直观一些。
在绘制模式中,将鼠标指针放到已经设置了所属地形的tile上面会出现用于指示匹配位的小方块。左键单击可以设置匹配位,右键可以取消:
可以设置的匹配位会根据地形集合的匹配模式不同而不同。“仅角落”和“仅侧面”模式会只有四个可以设置的地方,分别对应四个角和四个侧面:
现在把这三个tile的匹配位设置为如图所示的样子:
左端tile仅设置右侧,中间设置左右两侧,右端设置左侧。
那么这是什么意思呢?点亮的匹配位意味着要这个方向出现了对应地形中的tile,该tile才能出现。以最左端的tile为例,由于它是最左端的tile,是起始tile,它必须要只有右侧有tile时才能出现,而中间的tile要左右两侧有tile时才能出现。右端tile同理。
接下来实际看一下地形如何工作。选中TileMap面板,选择Terrains标签页。可以看到左侧已经出现了Terrain0(如果你修改了地形名称,这里在索引0左侧会显示为名称)。选中它,右侧出现了其中包含的tile:
如果选择单个tile进行绘制,实际上和之前的绘制方式没啥区别。但是要注意到右侧像“格斗游戏出招表中的摇杆方向”和“双头贪吃蛇”一样的两个图标。
“摇杆”图标是连接绘制工具,它会工具所选绘制位置周围的tile来确定要绘制哪个tile并调整周围的tile。贪吃蛇是路径绘制工具,会根据“笔触”连接路径上绘制的tile。接下来具体来看。
我们先用路径模式绘制平台。现在我们只需要选中一个格子,然后横向拉一条线出来,平台的左端、中间、右端就自动调整好了:
连接模式对于这个简单的、横向的平台来说用处不大,不过也可以演示一下:
可以看到首先放置了一个tile,启用连接模式后,选中空的格子时会根据周围情况自动绘制符合条件的tile。在后面绘制右侧tile时也会自动调整左侧的tile。
怎么样,是不是很好玩。接下来看一个更复杂的例子。
虽然这几个tile貌似不是这么用的,但是用来演示还是很合适。它们大致可以组成一个方框形状的地形,中间是泥巴填起来的。这样设置匹配位:
我们就可以随便拖一个矩形区域出来:
设置tile的地形属性时,可以给它的匹配位设置为和它自身不同的地形。另外在Select模式中也可以手动填写:
这里有对应八个方向的条目。其中的值是一个整数,对应地形编号。
Godot 4中的TileMap的地形功能实际上和3.x中类似的auto tiling功能有很大的差异,有些地方甚至显得不如之前灵活。比如由于匹配位是一个“位”,导致它没法匹配多种地形中的tile,而一个tile也没法设置属于多种地形。相关缺失的功能需要插件来补充,但插件的使用不在本文的介绍范围内,有兴趣可以自行研究。关于地形和TileMap的相关内容也可以参考文档。
本篇文章中我们学习了如何用素材结合TileMap节点来绘制场景,也学会了如何调整各tile的属性让它们跟易于使用。动手绘制一个自己的场景吧!