欢迎光临散文网 会员登陆 & 注册

从零开始独立游戏开发学习笔记(七十二)--Godot 学习笔记(五)--指南(三)-2D

2023-12-04 15:50 作者:oyishyi  | 我要投稿

本来想着给我的游戏做了键盘输入后,接着开始做动画 sprite。那就学一下动画。但是谁又能想到,Godot 指南里的 Animation 并不是这个 animation sprite。而是“动起来的画”,用关键帧制作的动画那种。然后我看了一下 Godot 的界面,还真是两个不同的界面。。。

So,今天学的是 2D 版块。

1. Canvas Layer

1.1 ViewPort 和 Canvas Items

CanvasItem 是所有 2D 节点的基础。无论是 Node2D 还是 Control 都是继承自此。

CanvasItem 节点肯定是 viewport 节点的直接或者间接子节点。(因为 root 节点本身就是一个 viewport,所以平时不会对这件事有感觉),viewport 会展示 CanvasItem 节点。

Viewport 的属性 Viewport.canvas_transform 属性,改变这个属性可以用于改变其内部所有 canvasItem 节点在屏幕上的位置。Camera2D 正是运用这个特性来调整屏幕。

但是注意,想达成像是滚动等效果,比起调整屏幕,调整 canvas 的 transform 性能上更加高效。

1.2 CanvasLayers

很多时候,我们并不想游戏中的所有东西都跟随 canvas transform,比如:

  • Parallax Backgrounds:背景移动的速度比其他地方慢一些。

  • UI:尤其是 HUD。

  • Transition 场景过渡:有些过渡效果(淡入淡出等)我们希望在屏幕上位置不变。

如何在同一个场景树中达成上面的效果? CanvasLayers 节点会添加一个完全分离的 2d 渲染层。完全可以把不想随 Viewport 移动的东西放这个节点下面。该节点有一个 layer 属性,普通视角的layer 是 0。CanvasLayers 可以根据需求决定放哪一层。CanvasLayers 的 tranform 不会受到别的影响,所以能固定在屏幕上。

一个比较常见的场景如下:

因为背景需要慢速移动,HUD 需要固定,所以用 CanvasLayer 放在了不同层上。

因为 CanvasLayer 和在场景树上的位置无关,所以可以随时随地根据需求实例化。

虽然可以做到,但是 CanvasLayer 本意并不是用来调整场景前后位置的。正确标准的做法应该是认真调整你的 scene 里节点的位置关系。在一个 scene 中,和直觉相反,最上面的节点绘制在最后面的图层。或者通过 CanvasItem.z_index 来调整绘制顺序。

有时候觉得,Godot 很多概念真的有点像前端。比如输入事件的处理像是冒泡,这里z_index也是和 css 属性一样的名字,以及父节点可以获取子节点的属性但反过来却不好取,有点像 react。也许我真的选对了引擎?

2. Viewport 和 Canvas transform

这一节讲的是比较底层的东西,就是节点如何被绘制到屏幕上。

2.1 Canvas Transform

上一节提到过,每一个 canvasItem 都有一个自己的 Canvas Layer(不是 CanvasLayer 节点)。每一个 Canvas Layer 都有自己的 transform。

默认所有节点都被渲染在 Layer 0 上。

2.2 全局 canvas transform

前面说过,Viewport 也有一个 canvas transform,这个是全局 canvas transform,这个 transform 会影响其他的 canvas 的 transform。

2.3 拉伸 transform

Viewport 还有一个 stretch transform,用于对屏幕尺寸进行改变。后面会讲.

2.4 Window transform

这里的 window 不是 windows 系统的窗口,而是 Godot 的一个节点类型。为了能够对窗口内容进行拉伸,每一个 Window 都包含一个 Window tansform。用于处理类似于窗口黑边之类的东西。

2.5 transform order

为了把一个 CanvasItem 的坐标转化为实际屏幕上的坐标,大概是下图的机制:

2.6 transform function

上面的图中用到了许多 transform function。从右往左,乘以一个 transform function 就往左一步。

你可以在 CanvasItem 中使用这些函数,虽然大部分时间你用不上。

2.7 传递输入事件

有了这些知识,可以自己发送输入事件了,即使用户并没有真的按下事件。

js

var local_pos = Vector2(10, 20)var ie = InputEventMouseButton.new()ie.button_index = MOUSE_BUTTON_LEFTie.position = get_viewport().get_screen_transform() * get_global_transform_with_canvas() * local_posInput.parse_input_event(ie)

这样子会在这个节点的 (10, 20) 的位置执行一个鼠标左键点击的事件。

上面讲了很多告诉你如何获取屏幕的精确坐标,但大部分时间你应该用本地坐标来完成,因为后者会自适应屏幕分辨率。

3. 渲染

3.1 2D 灯光

默认情况下,Godot 里的 2D 场景是未着色的,且没有灯光也没有阴影。Godot 提供了实时 2D 光照和阴影。可以很好地增加游戏的景深感。
一套完整的 2D 光照系统,包含如下节点:

  • CanvasModulate:能够给场景整个着色,类似于“环境光”的感觉。如果一个地方没有收到其他 2D 光照,那就是这个光来着色了。这个光照会让整个场景变暗,如果不加的话,2D 光照会让场景变得特别亮。

  • Sprite2D,TileMap:用于接受光照。

  • PointLight2D/DirectionalLight2D:用于照亮场景。

  • LightOccluder2D:用于告诉着色器哪一部分需要投射阴影。Occluder 可以作为一个单独的节点放置,也可以作为 TileMap 的一部分。只有有光源的地方才会有阴影。虽然像是废话,但是毕竟没有光源也可以看得见各个物体。

背景色不会接受任何光照信息,如果希望阴影投射在背景上,需要用一个 visual representation 来表示背景,比如 Sprite2D。

3.1.1 点光源 PointLight2D

点光源又叫位置光源,是最常见的 2D 光源。
在 Inspector 中,点光源具有以下属性:

  • Texture:用于光源的纹理。Texture 的大小就是光的大小。如果纹理有 ALpha 通道,那在 Mix 混合模式中会很有用。不过如果用 add 或者 subtract 模式则不需要。

  • Offset:和直接改光的位置有区别。offset 不会更改阴影的位置。

  • Texture Scale:增大灯光的尺寸。越大越耗费性能。

  • Height:虚拟的高度,当有法线的时候,灯光的虚拟高度。默认情况下,light 与物体表面里的非常近,所以如果使用了法线,灯光会变得很难看见,所以需要增加这个高度。没有法线的表面不会受到这个值的影响。

3.1.2 定向光 DirectionLight2D

这是 4.0 新增的功能。一般用于表现阳光或者月光。光线是平行投射的。

DirectionLight2D 提供以下属性:

  • 高度:虚拟高度,效果和点光源一样。高度不会影响阴影的样子。

  • 最大距离:为了避免计算在相机外的物体投射阴影诞生了这个,这个距离是离相机中心的距离,超过这个距离的物体不会被计算阴影。不然在场景开始就算计算场景终点的阴影不划算。这个属性不会受到相机的缩放的影响,因为它计算的只是和相机中心的距离。

定向光源的阴影永远都是无限长,这个是定向光线的渲染方式造成的缺陷。想要投射正常的阴影,应该取消定向光源的阴影。转而使用自定义的 shader。

3.1.3 一些共享的属性

点光源和定向光源都具有这些属性,当然这指的就是 Ligh2D 这个基类的属性。

  • Enabled:是否开启光源。关闭这个和直接隐藏节点的区别在于,关闭这个不会对子节点有影响。

  • Editor Only:开启后,光源仅会在编辑器中起作用。游戏中不会起效果。

  • Color:颜色。

  • Energy:光线强度。

  • Blend Mode:与被照亮物体的颜色计算方式。默认是 add,光线叠加,会变亮。subtract 一般用于特殊效果,会变暗。mix 则是线性叠加,不会变亮也不会变暗。

  • Range:Zmin, Zmax,Layer Min,Layer Max,决定了光线会影响哪些 zindex 和 layer 的物体。

  • Range - Item Cull Mask:两个作用。①通过被照射物体上的 canvasItem -> Light Mask 来控制体是都会受到光照影响。(注意:这里有一个坑,就是如果你创建了一个 Player,然后改变其 Light Mask,你会发现还是受到光线影响,这是因为你要改的不是 Player 的 Light Mask,你应该进入 Player Scene,然后更改 Player 下面的 Sprite 或者 Animated2D 节点,更改其 Light Mask 才行。(所以也不能叫坑吧,算是一个小知识,这就是 How it Works)) ②如果有 Occluder,那么还可以用 Occluder Light Mask 来控制是否投射阴影。

3.1.4 设置阴影

光是开启光源里的阴影是看不到阴影的,因为要有阴影就要有阻挡物,也就是 Occluders。

2D 阴影必须要有 LightOccluder2D 才能形成,这个节点需要靠多边形来匹配 sprite 的形状。此外,还具有两个属性:

  • SDF Collision:用于着色器,不过开启了也不会影响性能,所以干脆默认开启。

  • Occluder Light Mask:与光源的 Item Cull Mask 一起控制物体是否会形成阴影。

3.1.5 如何创建 LightOccluder2D

两种方式,一种手动,一种自动。

自动创建

选中一个 Sprite2D 节点,编辑器上方(就是选择/缩放/网格的工具栏那里)会多出一个选项可以自动生成 LightOccluder2D 或者 Polygon2D。

手动创建

创建 Polygon2D,自己手动一点点画出来。

阴影属性

阴影可以配置各种属性:

  • Color:阴影的颜色。

  • Filter,三种方式,其实就是硬阴影,软阴影。PCF5 和 PCF13 区别可以简单的通过更改 Filter Smoooth 看到,5 就是边缘 5 层不同的透明度来模拟,13 就是 13 层。PCF13 尽量少用,因为性能消耗大。

  • Filter Smooth:调一下就知道了。

  • Item Cull Mask:和 Range->Item Cull Mask 不一样,这个只会影响阴影,但不会影响光源。

3.1.5 阴影绘制顺序

LighOccluder2D 遵循通常的 2D 绘制顺序。借此您可以控制遮挡物怎么遮挡。(就像之前说的,尽量用 Scene 里节点的顺序来控制图层,少用 layer,z_index 这些),好的架构最重要。

如果 LightOccluder2D 节点是 Sprite 的 sibling,且如果 Occluder 节点在下面,那么就会遮挡这个 sprite。

如果遮挡物是 sprite 的 child,就会遮挡 sprite。但如果开启了 Show Behind Parent,那就不会遮挡,不过默认是关闭的。

3.1.6 法线贴图和高光贴图

这两个可以应用于所有的 2D 元素。提高真实感。

这里有一个自动生成 2D 法线的工具,叫做 Laigter。

简单的就不错:

比如说对于 Sprite2D 来说,加 CanvasTexture ,然后就可以配置 Normal Map,Specture Map 等。

3.1.7 附加 Sprite 来使 2D 灯光渲染更快

使用 2D 灯光会造成一些性能问题,所以有时候可以附加 Sprite 来作为替代。原理很简单,就是用一个灯光颜色的 sprite,然后选择 blend mode 为 add。反正 2D 灯光的本质其实也就是改变颜色而已。

这个方法显然比用灯光性能高很多很多,因为不需要通过一个单独的渲染管线。此外,这个方法还可以用来和 AnimatedSprite2D 结合使用,这样就有创造了一个动画的“灯光”。

当然,附加 Sprite 自然有它的缺点:

  • 首先混合模式附加的颜色,相比真正的灯光而言并不正确。如果场景光线很充足这倒无所谓,但如果光线比较昏暗就能看到明显区别。最极端的案例是完全黑暗的场景,附加的 Sprite 并不能用来照亮区域。

  • 不能投射阴影。当然了,它又不是灯光。它只是看起来像灯光的颜色而已。

  • 不受法线,高光贴图的影响。

所以使用场景应该是在不太需要灯光的真实效果的地方。

这种方式经常用于短暂的动态效果,比如子弹或者爆炸。因为他们很快,不需要很真实的光效。

使用方法

创建一个 Sprite2D 节点中,先添加 Texture(就像 PointLight2D 里的那个一样),在 CanvasItem->Material 里添加 CanvasItemMaterial 材质,然后选择混合模式为 Add 就行了,现在这个 Sprite2D 就可以作为。如下图:

和原生灯光对比

这是原生灯光:

这是附加灯光

可以看到效果还是不错的,因为这里光线充足,我也没有添加法线,两者唯一区别就是没有阴影。所以有些时候对细节要求不高确实可以用用。

3.2 2D 网格(2D Meshes)

2D 中用的图片比较多,很少用网格。注意 2D 网格是二维几何体并不是 3D。生成 2D 的方式一个是导入 OBJ 文件,还有一个是通过 Sprite2D 创建。

3.2.1 为啥要用 2D 网格

这是一种优化方式。Godot 会渲染图片的所有像素,即使透明的也是如此。如果你有一个尺寸很大的图片,但大部分边缘面积都是透明的,会很耗费性能。尤其是使用 ParallaxBackground 将多个具有大透明区域的图片分层时。

而转成 2D Mesh 则会忽略透明部分。

3.2.2 如何将 Sprite2D 转成 2D Mesh

Sprite2D 的节点的编辑器工具栏上有一个按钮(就是之前生成 Occluder 的地方),调整下参数很方便就能生成了。

3.3 2D Sprite Animation

终于到这了,本来只是为了学这个,怎么前面这么多啊。

3.3.1 添加动画

这个之前教程里说了,没什么多的东西。

3.3.2 控制动画

同样

3.3.3 SpriteSheet

Godot 可以读取 Spreadsheet 格式的动画。

3.3.4 带有 AnimationPlayer 的 SpriteSheet

还有一种方式是先用一个普通的 Sprite2D 展示 Tetxture,然后使用 AnimationPlayer 来控制动画。

  1. 先创建一个 Sprite2D,把 SpriteSheet 拖到 Texture 上,在 Animation 部分选择 Hframes,VFrames 来自动切割,就和 AnimatedSprite2D 里做的一样。

  2. 然后选择 AnimationPlayer,选择下方的动画。点击“动画”按钮(看起来像是个不能点的文字框,但其实是个按钮),新建动画。右边把时间改成 0.6(这个是教程的值,实际上应该是下方 snap 的值(默认是 0.1) * 你有多少动画帧), 然后勾选 loop 成为循环动画。

  3. 回到 Sprite2D 节点,点击 frame 右边的一个钥匙的按钮。我说为啥是钥匙呢,原来是关键帧。那就简单了,AE 没有白学。

AnimationPlayer 使用方式和 AnimatedPlayer2D 一模一样,Script 里的方法也一模一样。

如果同时动画和某个属性同时改变,play() 并不会立刻反应,要下一个动画帧才能改变。所以会造成故障帧。比如说你在做一个转身的动画,你先改变 h_flip 来转身,然后 play() 播放转身动画。但是实际上,人物会转身然后保持跑动的动画过一帧,然后才变成转身动画。要解决这个问题,请在 play() 之后调用 advanced(0) 来更新动画。

3.3.5 选哪个?AnimationPlayer 还是 AnimatedSprite2D

用过了就会发现,AnimationPlayer 更复杂,而 AnimatedSprite2D 连简单的一次性播放动画都做不到。

所以一般来说动画复杂的,比如说玩家,肯定是要用 AnimationPlayer 的。简单的小动画那就用 AnimatedSprite2D 就可以了。

当然还有更狠的,给 AnimatedSprite2D 配上 AnimationPlayer,因为前者也有 frame 属性也可以加关键帧。反正 AnimationPlayer 主打一个万能配。

AnimatedSprite2D 配 AnimationPlayer

就是在 animatedSprite2D 里弄好动画后,在 AnimationPlayer 里切换 animation 和 frame 来控制动画,而不用 animatedSprite2D 自身的动画。个人感觉这样更清晰,如果用 animationPlayer 的话,所有的 sprite 帧都要放在一个大的 Sprite Sheet 里面,区分动画靠帧数,还要去专门记攻击动画是 15-36 帧这种东西。

3.4 2D 粒子系统

3.4.1 粒子节点

Godot 有两个粒子节点,GPUParticles2D 和 CPUParticles2D。能选肯定选 GPU,性能更好。唯一可能选 CPU 的原因可能只是 GPU 遇到瓶颈了帮忙分担点。

3.4.2 ParticleProcessMaterial

加了一个节点,但是啥都没有点。因为没有加 ParticleProcessMaterial,也没有添加 Texture。
直接创建一个 ParticleProcessMaterial。
Texture 同理。

3.4.3 Animation flipbook

Animation flipbook 是一种特殊的 Texture,可以看作是粒子的 SpriteSheet。是一系列的粒子动画帧,如下图。

使用 flipbook 比起单个 Texture 需要更多的设置。

首先把一个 flipbook 图片加载进 Texture,然后在这个节点的 CanvasItem->Material 里添加一个 CanvasItemMaterial(之前你们用来模拟灯光的那个),然后在生成的 Material 里可以看到有一个 Particle Animation 的选项,勾选后会提供 hframe 和vframe。熟悉的操作。(如果 flipback 背景是黑色的而不是透明的,则可以把混合模式调成 add,当然最好的是事先修改好图片)

这个时候,再回到 ParticleProcessMaterial 里,有一个 animation 部分就可以对动画进行控制了。

粒子系统玩的主要是各种调参,所以这里对每个参数进行介绍(前面部分是粒子节点本身的参数,后面部分是介绍 ParticleProcessMaterial 的参数):

3.4.4 Time 参数

Lifetime

每个粒子的存活时间,每个粒子消失的时候就会有新的来代替。

One Shot

很好懂,发射一次就结束。

Preprocess

假设粒子已经运行的时间。
比如说你想做一个雾的场景,但是粒子系统生成雾也是要从某个点开始往外喷发直到某个时间段稳定了才形成雾的样子。但你不可能让玩家看到这个过程,所以要使用 preprocess 来跳过这个生成阶段。

Speed Scale

直接明了

Explosiveness

粒子系统默认是平均生成,i.e. 如果 lifetime 为 1,粒子数量为 10.那么会每 0.1 秒生成一个粒子。
如果把 Explosiveness 设置为 1,那么就会一次性生成 10 个粒子,等 1秒再生成 10 个粒子。

且这个值是可以取中间值的,提供一些灵活性。

Randomness

直接明了

Fixed FPS

改变粒子系统的 fps,注意这一选项不会改变粒子系统的速度,只是平稳和卡,物理计算该到哪里还是哪里。
如果改成 0,那么就是按照实际游戏帧率来渲染了,游戏多少帧粒子就多少帧(同上不会影响物理效果,不用担心)。

Fract Delta

一种计算方式,说是开了会更平滑。

3.4.5 Drawing 参数

Visibility Rect

这个矩形控制是否渲染粒子,如果这个矩形在视口之外,则不会渲染粒子。
W 和 H 控制矩形宽度和高度。X 和 Y 则是矩形的左上角的坐标(相对粒子发射器的坐标)。

Godot 可以自动生成这个矩形(就像你生成 Occulder 和 Mesh 那样,在关卡编辑器上方),Godot 会模拟粒子系统跑一阵子,然后生成适合的矩形。

Local Coords

开启意味着所有被发射的粒子都与该节点绑定,移动节点的时候,所有已经喷出的粒子也会跟着一起移动。
关闭意味着已经发射的粒子位置不会再改变了。

Draw Order

这个绘制顺序指的不是先后,而是图层前后。

3.4.6 ParticleProcessMaterial

Initial velocity

初速度的大小,粒子刚发射时候的速度大小 Pixels/Sec。

Angular Velocity

初始角速度。

Spin Velocity

旋转速度。

Orbit Velocity

沿着中心旋转的速度。上面的 spin 是粒子的中心,这个是粒子发射器的中心。

Direction

粒子刚发射时候的初速度的方向,默认是 (1,0,0) 按理来说是向右,但实际上粒子动画看起来是向下,因为这是初速度。粒子默认还有一个向下的重力。

Spread

顾名思义,沿着初速度,向两边的角度,粒子会在这个区域发射。因为是两边,所以最大值是 180,拉满就是全方向了。

Flatness

仅对 3D 有用,其实就是另一方向的 Spread。

Gravity

就刚刚提到的重力。

Linear Acceleration

线性加速度

Radial Accl

径向加速度,就是粒子和发射器之间的方向。正值会远离发射器,负值会靠近发射器。

Tangential Accl

切向加速度,就是和径向垂直的方向。(这里有一个英语的问题,虽然 Tangential 翻译为切向,但其实所谓的切向速度,并不一定和速度相切,叫横向加速度更好理解,因为一定是和径向垂直的)(以下图为例,切向从定义来讲应该是 Velocity,但实际上指的是垂直径向的那个)

至于上面有一个 orbit Velocity,应该才是上图中的那个 Velocity,轨道速度,但在 godot 里,我感觉指的估计还是横向速度。

Damping

阻尼,用于创造摩擦力。对于像是爆炸,火花等一开始速度快,最后速度慢的效果来说很好用。

Angle

角度,最经常被用来随机的属性。毕竟角度随机比较容易看出来随机的感觉。

Scale

简单明了

Color

简单明了

Hue Variation

简单明了

Animation

只有用 canbaseItem Material 加 flipbook 的时候才有用。
如果关闭循环,且在粒子生命周期内动画播放完了,会持续显示最后一帧(如果你发现不是,也许你最后一帧是透明的)。

如果 speed 设置为 1,则粒子动画会刚好播放一整个生命周期。

如果 flipbook spritesheet 里并不是一个粒子的全程动画,而是你想用这个 spritesheet 达成每个粒子从中挑选一个动画来播放。那么请把 speed 调整为 0,然后 offset max 调整为 1(min 为 0)。那么每个粒子就会从中随机挑选一个来使用了。

3.4.7 发射形状

有一个叫发射遮罩的东西,决定了粒子从哪里发射。
在关卡编辑器上方(对,还是那里)前期,有一个加载发射遮罩的选项。

有三种类型:

  • Solid Pixels(实体像素):粒子会从 Texture 的所有非透明的地方发射。

  • Border Pixels(边界像素):粒子只会从 Texture 的外边界发射。

  • Directed Border Pixels(有向边界像素):和 Border Pixels 相似,但是粒子可以从边界往外发射。选择了这个会导致部分属性失效,我自己测的是初始方向和Spread会无效,因为会按照这个 Texture 来。

选择捕获像素颜色,那么发射的粒子会继承 Texture 发射点像素的的颜色。

生成的 Mask 会在 ParticleProcessMaterial 的 Spwan->Position 里。
刚才生成的 Mask 会在这里的 Point Texture 和 Normal Texture 里,如果勾选了颜色则会在 Color Texture 里。这三个东西如果你不是特别清楚原理那就别自己换,最好全靠自动生成。

3.5 2D 抗锯齿(2D antialiasing)

因为分辨率的原因,2D 场景经常会出现锯齿现象。 尤其是在 Linea2D,Polygon2D,TextureProgressBar 中最为明显,部分 Custom Drawing 也会。

3.5.1 Line2D,Polygon2D 和 Custom Drawing 上的抗锯齿属性

部分节点会提供一个 Antiliased 的属性,这个属性对性能影响很小,是最推荐的方式。

这个属性并不要求打开 MSAA,也就是说基础性能消耗就很少。

不过,因为这个方法通过生成几何体来起作用。所以如果你的几何体是一个很复杂,且一直在变化的话,这个方式的性能消耗就会上来。

3.5.2 多重采样抗锯齿(MSAA/Multisample Antialiasing)

MSAA 在 2D 的使用范围是有限制的:

  • 几何边缘,比如 Line,Polygon 的边缘,

  • Sprite 的边缘。但这里指的是整个 Texture 的边缘,而不是由透明度产生的边缘。见下面的图。

MSAA 的缺点很明显,就是只适用于边缘。MSAA 会增加 Coverage 采样的数量,但不会增加 Color 采样的数量,fragment shader 仍然只对每个像素执行一次。因此 MSAA 不会以任何方式影响以下的锯齿:

  • 应用了 nearest-neightbor filter 的 texture(像素画)

  • 自定义 2D shader 产生的锯齿。

  • 使用 Light2D 产生的 Specular 锯齿。

  • 字体渲染时候的锯齿

MSAA 可以在项目设置里设置。

MSAA 的效果如下。重点看下面的像素画,可以看到 godot 的 logo 没有任何区别,它们虽然是边缘,但这种边缘是透明度带来的,不是真正的边缘。真正的边缘是后面的绿色背景的正方形边缘,可以看到在第 2,3 个的正方形边缘上确实有抗锯齿的效果。

3.6 Custom Drawing in 2D

godot 提供了 sprite,line,polygon,particles 等东西。如果这都不够你表达所需的话,你可以使用任意 2D 节点(Node2D 或者 Control)为基础来自定义绘制。

自定义绘制有很多使用场景,比如:

  • 绘制现有节点无法完成的形状和逻辑。比如特殊的动画多边形,移动残影等。

  • 特殊的视觉效果。

  • 绘制大量的简单物体。因为自定义绘制可以避免一次性生成多个节点。可以提高性能。

  • 自定义 UI 控制。虽然 godot 提供了尽量多的方式,但是游戏不是 web 应用,总有很多特殊的控制方式是独特的。

3.6.1 绘制

首先添加脚本(一定得是继承 CanvasItem 节点),通常是 Node2D 和 Control。然后重写 _draw() 函数。
里面能使用很多绘制方法,在 CanvasItem 的 class reference 里有详细说明,这些方法都只能在 _draw() 里使用。

3.6.2 更新

_draw() 只会被调用一次,然后不会再被调用。这是为了节省性能,避免每帧都重绘。

如果想要重新绘制,要手动调用 CanvasItem.queue_redraw() 函数,然后 _draw() 就会被重新调用。

所以,如果想要每帧都重绘,在 _process() 里调用 queue_draw() 即可。

3.6.3 坐标

draw 方法使用的是 CanvasItem 的坐标系统,会跟着 CanvasItem 的 transform 而变换(简单来说就是 local transform)。当然,也有像是 draw_set_transform() 等方法来自定义 transform。

使用 draw_line() ,draw_rect() 等方法的时候,注意如果宽度是奇数,那么线的位置应该移动 0.5 来保持中心,如下图:

如果从 (1,0) 到 (4,0),但宽度是 3,那么只能是左 2 右 1 或者左 1 右 2,这样线不会是中心点。会造成各种问题。

3.6.4 使用案例:绘制圆弧(重要)

Godot 里有一个 draw_circle() 来画整圆,但是如果我们想要画的是圆弧怎么办?那就只能自己写代码了。(笑死,现在 Godot 已经提供了 draw_arc 来画圆弧,但是重点不是这个,重点是你要自己写代码来完成 Godot 没有提供的功能,我们假设 Godot 没有提供这个方法好了)

圆弧的基本功能

为了画圆弧,我们需要圆心位置,半径,起始角度和终止角度,这里我们花哨一点,再加一个颜色。

重点来了,在屏幕上绘制形状,其本质是一个一个点/边互相链接。对于圆弧这种曲线而言,点越多就越平滑,点越少就越棱角。没错,如果你要用 _draw(),你就需要考虑这些事情了。如果你要更细节一点,那么这里可以做 LOD(Levels of Detail),如果形状很大,那么需要的点肯定也会很多才能看起来平滑,如果形状小,那么很少的点就可以表现很平滑了。(在 3D 中,这个表现为离相机近和远,其实所谓的近和远也就是大和小的区别了) 。不过,说是这么说,为了方便,我们这里就不搞 LOD 了,全用固定点。

先上代码:

js

func draw_circle_arc(center, radius, angle_from, angle_to, color): var nb_edges = 32 var points_arc = PackedVector2Array() for i in range(nb_edges + 1): var angle_edge = deg_to_rad(angle_from + i * (angle_to-angle_from) / nb_edges - 90) points_arc.push_back(center + Vector2(cos(angle_edge), sin(angle_edge)) * radius) for index_point in range(nb_points): draw_line(points_arc[index_point], points_arc[index_point + 1], color)

  1. nb_edges 是组成形状的边的个数,为了图方便我们用固定的 32。

  2. 然后初始化一个 Vector2 的 Array 用来存放点的位置数据。

  3. 32 个边由 33 个点组成,在第一个 for 循环里,我们计算这 33 个点应该存在的位置。使用 deg_to_rad 把角度转换成弧度。但是因为人的直觉来看角度的 0 度是 12 点钟方向,但是弧度实际上是把 3 点钟方向作为 0 度。所以这里减 90 度,这样子以后就可以用 12 点钟作为角度的 0 度了,比较利于人类思考。

  4. Vector2(cos(angle_edge), sin(angle_edge)) 就是圆心到这个点的标准向量。乘以半径就是带长度的向量。加上 center 就是点的实际位置了。

  5. 拿到 33 个点的实际位置后,我们只需要在每个点之间 draw_line() 画线就行了。这就是第二个 for 循环做的事情。

  6. 然后,您只需要在 _draw() 里调用这个方法就行了。

如果想变成实心,把 draw_line 改成 draw_polygon 即可。

3.6.5 动态自定义绘图

我们现在学会了画静态图,那么我们要画动态图了。
也很简单,假设我们想让上面的圆弧动起来,那么我们只需要把 angle_from 和 angle_to 改成全局变量即可(或者从其他节点里获取等)。这样我们就可以在 _process() 里更改变量,然后调用 queue_draw() 就行了。

注意:如果你是通过增大角度来旋转,这里角度可能会无限增长,比如 425°,虽然 godot 依然能够计算。但是随着时间增长,这个度数可能会达到 2**31 - 1,也就是整数的最大值,造成 bug 的产生。所以要用 wrap()/wrapf() 来标准化。

速度

别忘了 delta,因为渲染必须在 process 里使用,没法依赖 physical_process。所以我们要给这些速度乘以 delta。

3.6.6 抗锯齿

部分绘制方法提供了抗锯齿,比如 draw_line。如果不提供的话,那你只能用 MSAA 了,但别忘了这一开就是影响整个视口。并且只针对特定元素(请回顾上一大节)。

4. 物理与移动

4.1 2D 移动

介绍一些常用的方法。

4.1.1 基本设置

一般都是以 CharacterBody2D 开始,配上 Sprite2D 和 CollisionShape2D 两个子节点。
然后在项目设置里设置 InputMap。

在 _physics_process 里改变 velocity,然后调用 move_and_slide() 。

4.1.2 朝向鼠标的方向

调用 look_at() 函数,可以改变节点的旋转方向,参数使用鼠标的位置,那就变成了看向鼠标的方向了。

4.1.3 移动到鼠标点击的位置

获取鼠标的位置后,有两个有用的方法:

  • Vector2.direction_to():获取方向,一般这么用 velocity = position.direction_to(target) * speed

  • Vector2.distance_to():获取到某点的距离。一般这么用 var distance = position.distance_to(target)

5. 工具

5.1 TileSets

TileMap 用于创建游戏布局场景的好工具,可以用图块来绘制场景,好处一个是不用创建大量的 Sprite,二个是比用大量的 Sprite 性能要更好。TileMap 可以添加各种功能。

不过,在使用 TileMap 前,需要创建 TileSet,TileSet 就是在 TileMap 里使用的图块的几何。创建好一个 TileSet 后,在 TileMap 编辑器里来使用它们。

TileSet 的创建需要一个和 Spritesheet,Filpbook 类似的东西,叫做 TileSheet。

5.1.1 创建 Tileset

先创建一个 TileMap,然后在检查器里新建一个 TileSet 资源。
展开后,可以发现,TileSet 的图块形状除了方形还有很多种。
设置好图块的大小。

然后我们前往编辑器底部的 TileSet,添加图集(atlas),图集的作用是确定那些部分会被作为 tileset,因为不是图片的所有部分都会作为 tileset。会提示你是否自动创建,如果你已经正确设置了图块的大小,则点击确认。一个 atlas 可以创建多个 atlas,以便使用多个 tilesheet 图片

如果有一些 tiles 不需要,可以用橡皮擦擦掉。或者右键删除也是一样的作用。

左边的 setup 是创建和删除图块(如果自动创建会自动全添加为图块了)。这里可以调整 atlas 的属性。

  • ID:用于排序

  • Name:名字,最好用一些描述性名字,比如“区域”,“装饰物”等

  • Margins:有些 tilesheet 周围有些边距,导致按照图块大小分分不完。一般这种事网络上下载的比较多,如果不想改图片的话,那就这里改。

  • Separation:如果图块之间有间隔,可以用这个。

  • Texture Region Size:图块分割大小。tilesheet 会按照这个大小区域分割。

  • Use Texture Padding:勾选后,所有的 tile 都会有 1 像素大小的透明边缘。防止在使用 filter 的时候发生纹理渗色现象。一般来说除非这个导致渲染出事了,不然保持开启最好。

橡皮擦右边的三个点里有一些自动创建图块的选项,创建 atlas 时的自动创建和这个效果一样。

5.1.2 使用场景合集

Godot 4.0 开始可以把场景作为 tiles 了。比如你创建了一个商店场景,那就可以批量放置了。当然节点不限,像是音频啊,粒子啊,各种都可以放。

但是注意,这个并不会带来什么性能好处。如果非必要的话,还是用 atlas 比较好。不要做出什么把 sprite2D 节点作为场景来安放的傻事,除非有什么特殊需求。

5.1.3 把多个 atlas 合并为一个 atlas

添加 atlas 的右边有三个点,这里可以选择合并。

Tileset 有一个 tile proxies 的特性。用于替换 tilemap 里的 tileset。当合并 atlas 的时候,会自动替换。如果你想替换 atlas 为另一个 atlas,也可以使用这个。

5.1.4 给 tileset 添加碰撞,导航,和遮挡物

我们现在成功创建了一个基础 TileSet,已经可以使用 tilemap 了。不过我们还想添加一些额外功能像是碰撞,导航,遮挡物等功能。

为了添加碰撞,我们在 tilemap 检查器中添加一个 physical layer。导航和遮挡物同理也有对应的 layer。

在 tileset 面板中,调整到“选择”工具,选中图块,可以看到物理层属性了,下面有一个多边形编辑器,这里可以编辑碰撞区域。你也可以按 f 来快速创建区域。

5.1.5 Custom Data Layer

使用 Custom Data Layer,可以按照图块来分配自定义数据,可以用于储存类似于玩家触碰图块是否会受伤,或者是否可摧毁图块等信息。

不过由于数据是和 tileset 相关联而不是 tilemap,所以每个实例图块都共享相同的数据。也可以构建 Alternative Tile 来为某个图块专门储存信息,这个后面会讲。

注意 tileset 里数据层属性不显示名字,而是按照定义的顺序。

属性绘制也可以适用于自定义数据

5.1.6 创建地形集(以前的自动填充)

很多地形集都有这种地形--在边缘,角落处会有特殊变体,手动放置会很麻烦,先放头,然后放一堆中间,再放尾。

可以使用地形来自动执行这种连接。地形被分组为地形集,每个地形集都可以分配为“匹配角和边”,“匹配角”,“匹配边”模式。

先在检查器里创建一个地形集,创建地形集后还要创建一个或者多个地形。
创建完后,在 tileset 里设置属性,也可以在绘制里设置。 设置完地形集和地形后,会出现一个地形邻接位(绘制属性列里不会出现这个,必须在选择列里配置)。

地形邻接位

地形邻接位用来控制放置哪个图块。数值是地形的 index。比如:

这么设置的意思是,第一张图因为是在左侧,所以只能右侧放 index 为 0 的 tile。所以设置右边缘为 0,其他设置为 -1。第二张图因为在中间,左右两边都可以放,所以左右侧边缘都设置为 0,其他设置为 -1。

设置完后,在 tilemap 里地形板块就可以很方便绘制了。

5.1.7 多个 tile 属性配置

选择多个

在 “选择”列,可以选择多个 tile,再配置,这样就可以批量配置了。需要注意的是,在这里,多选是 shift 而不是 ctrl,按住 shift 再点击可以多选。

属性绘制

很明显这个指南的顺序是瞎配的,这个明明前面都提到无数次了,结果真正提在这么老后面,我真服了。
这个不难,捣鼓一下就会了。

5.1.8 创建备选图块 Alternative Tile

有时候你希望某个 tile 实例具有单独的配置,而不想影响到其他实例。

创建很简单,选中 tile,右键即可创建。如果处于选择模式,新创建的备选图块已经处于选择状态可以编辑了(注意备选图块不会继承基础图块的属性,你需要一个个添加)。如果不是,依然可以创建,但想要更改就要去选择模式了。

备选图块的属性和基础图块是不同的,可以自己试试。

5.2 TileMap

如果想让多个 TileMap 重用同一个 TileSet,最好的方法是把 TileSet 保存为外部资源。

5.2.1 创建 TileMap 图层

Godot 4.0 开始,可以把多个图层放在单个 TileMap 节点中,这样可以在相同位置放上多个图块。

默认情况下只有一层,如果想添加,在 tilemap 检查器的图层部分添加,每个层都有一些属性。

  • 名字:区分

  • 启用:打开或者关闭图层

  • Modulate:可以调整这一层所有图块的颜色。如果刚才你自己玩的话,会发现其实每个图块也有这个 Modulate(调制) 属性。这些 Modulate 之间是相乘的效果,所以乘的越多会越暗。

  • Y Sort Enabled:根据图块在 tilemap 上的 y 位置来对图块进行排序,可以用于某些排序问题。

  • Y Sort Origin:用于 Y 排序的垂直偏移,开启 Y 排序的时候才有用。

  • Z Index:控制该图层的绘制顺序,越高就越在画面前面。如果相同,那列表最下面的图层会在最顶部绘制。

注意:删除图层会删除该图层上的所有图块。

5.2.2 打开 TileMap 编辑器

在编辑器的底部,tileset 面板的右边,你肯定早就看见也玩过了,如果没玩过那我不知道前面 tileset 部分你是怎么看懂的。

5.2.3 选择用于绘画的图块

在 tileset 上选择图块作为画笔,然后右边选择画在哪一层上。没错,因为 layer 是 tilemap 的属性,和实例有关,只和画出来的图块实例有关。而之前的物理层啥的都是 tileset 的属性,是应用于所有的 tile 的。

选择图块的时候可以多选的,把多个图块一起画,注意多选按钮还是 shift 不是 ctrl。这对于绘制树木啥的很有用。

5.2.4 绘画模式和工具

选择工具

在屏幕上选择 tile,这里要注意,这里快捷键又变了。按 shift 多选,但是移除单个要按住 ctrl,挺迷惑的设计,不过后面会说原因,虽然那个原因也没啥说服力就是了。

选择后按 del 键可以移除。

在绘制工具的时候,按住 ctrl 会临时转成选择模式,这就是为啥 ctrl 是移除。但其实这个临时的选择模式是一个根本连按住 shift 多选都做不到的一个下位选择模式(其实就只是一个取色器)。所以这个说法也没啥说服力。总结就是这段肯定印度人设计的。

选择模式还可以 copy paste,ctrl + c/v 就行。

绘制工具

左键绘制,右键删除。

鼠标点击前按住 Shift 可以画直线。按住 ctrl + shift 可以画矩形。

按住 ctrl 可以在画面上做到取色器的效果,不过取的是 tile。

直线工具

就是标准的直线工具。和按 shift 一模一样。

矩形工具

和按 ctrl + shift 一模一样

油漆桶 Bucket Fill

标准的油漆桶工具,除了颜色和 tile 脑子要转换一下其它没区别。连续就是是否沿着对角线填充。

所有这三个工具,按右键都会有相匹配的删除效果。
另外两个工具拾取器和橡皮擦就不说了,很简单的效果。注意拾取器可以拖动来拾取一个区域。

散布随机绘画

是工具栏上骰子样的那个图标,勾选后会在选取的 tileset 中随机选一个 tile 来绘画(所以要多选后才起作用),或者如果修改散布值大于 0,那么还会有概率不放置任何 tile(一般用于放一些偶尔的非重复的细节,比如草,碎屑等)。

这个可以和所有工具一起用,直线,矩形,油漆都可以。

5.2.5 图案

虽然现在已经可以多选来放置树木这种东西了,但是很多时候还是希望把这些当做一个预制的图案来使用而不是每次都要多选。

切换到图案板块,在屏幕上选择后,拖到下面来,或者选择后按 ctrl + c,鼠标点击图案的部分聚焦后按 ctrl + v。

图案可以当做普通 tile 来使用,所有的工具都可以用。

另外,图案也是属于 tileset 资源,虽然只能在 tilemap 部分创建。但回到 tileset 版块可以看到。这样子可以保证当使用外部资源重用 tileset 的时候,图案也能一并保存过去。

5.2.6 使用地形

前面 tileset 部分我们创建了地形集,现在我们要使用了。

切到地形板块,选择一个地形,可以看到有三种绘制模式(一开始只能看到两种):

  • Connect 模式:图块可以连接到同一个 tilemap 图层的其他图块。

  • Path 模式:图块只会自动连接同一笔画出来的图块(直到鼠标松开)。

  • 特殊图块模式:专门处理某些特殊情况,地形系统无法支持的自定义情况。

Connect 模式很好用方便,Path 模式更加灵活。比如你只是想画两个平行的线,结果 connect 模式自作聪明给你把两个线连在一起了。

至于第三种模式,如果你前面在 tileset 设置地形集的时候,地形邻接位设置的不会有矛盾的地方,那就不会出现。只有某些之间可能会有模糊不清的情况出现的时候,就会自动生成:

5.2.7 缺失 tile

如果你因为删除/修改 tilseset 或者调整 id 啥的,导致 tilemap 找不到 tileset 资源。屏幕会用一个感叹号图块来代替。(有些情况会直接删除,比如说删除图层)

缺失 tile 不会被渲染,如果有新的 tile 匹配上这个 ID 的话,这些缺失会被补全。

缺失占位符只有在选中 tilemap 节点的时候才会出现。


从零开始独立游戏开发学习笔记(七十二)--Godot 学习笔记(五)--指南(三)-2D的评论 (共 条)

分享到微博请遵守国家法律