Godot 教程与开发流程初步总结
本总结主要涉及到的教程有:
- 【中字】Godot 3.2像素风ARPG制作教程(全集)_哔哩哔哩_bilibili
- 合集·Godot 3.x 平台跳跃游戏教程(暂时搁置,有机会再补充)
- 合集·《迷失岛2》游戏框架
- 合集·《勇者传说》Godot 4教程(可以参考Godot 4 教程《勇者传说》编程笔记)
【补充】其实《迷失岛2》的教程非常好,涉及到很多知识点;但当时学的时候囫囵吞枣,跟着做一遍就算了,没有仔细去分析背后的原理。后续应该会再重新复习一遍教程并给出笔记,当然,应该避免像 2DAdventure 的教程笔记一样事无巨细,代码更改的每一步都做记录;而是对项目结构和代码进行分析思考后,给出自己的心得体会。
基本知识点
脚本/节点与场景
- 新建脚本会继承自某一个节点(如果声明了 class_name,则可以在节点树面板显示)
- 可以选择继承自脚本,相当于进一步继承(节点就是类,实例化节点就是类创建对象)
场景:由节点组成,作为游戏的加载单位
需要注意的是:
- 使用
MyNode.new()
后,创建的是实例化的节点,而不是实例化的场景!(也就是说,如果你创建了一个场景,有多个节点,新建根节点并不会实例化整个场景) - 实例化场景,必须要用如下的代码才能实现:
1 |
|
场景树与父子节点
场景树一定有一个根节点,向场景树插入节点可以视作是一种“用组合替代继承”的设计哲学。
一些函数的返回类型和返回值含义(初学极容易搞混):
函数/属性 | 返回类型 | 返回值内涵 |
---|---|---|
Node.get_tree() | SceneTree | 当前场景(循环单位) |
Node.get_tree().current_scene | Node | 当前场景(根节点) |
Node.get_tree().root | Window | 游戏运行的根目录 |
PackedScene.instantiate() | Node | 实例化场景的根节点 |
Node.add_child(Node) | void | - |
Node.get_children() | Array[Node] | 子节点列表(默认不含孙子节点) |
Node.get_parent() | Node | 父节点(没有则返回null) |
各类型的实际含义:
- SceneTree:由 MainLoop 派生而来,是游戏循环的单位
- Node:节点,开发者视角下的“场景”指代的就是根节点(根节点是“对象”,子节点是“属性”)
- Window:游戏窗口,游戏运行的根目录(通常通过 get_tree().root 访问)
- PackedScene:继承自 Resource 节点,通过 load/preload 函数加载场景文件返回
【个人理解】场景树是放在一个可以循环运行的“容器”里的,这个“容器”就是 SceneTree;开发者访问场景树,实际上是访问场景树的根节点,而不是这个“容器”(其实就是「场景树」和「节点树」的区别)
补充:add_child
函数只会在场景中临时生成节点,不会设置父节点为 owner;owner 表示新增的节点会作为场景文件的一部分(谨慎使用 owner!你很可能不希望游戏运行通过脚本临时新增的节点被持久化到场景文件中)
信号和组
Godot 中一般遵循这样的编程规范:向下调用,向上信号——即,在同一个节点树中,父节点访问子节点,可以调用子节点的函数;子节点访问父节点,就需要传递信号。
特别地,可以在脚本中使用 @export 引入需要的对象类型,然后将场景树中的节点(可以是父节点或父节点的其他子节点)拖进检查器中,实现不使用信号的调用。
一个对组的简要介绍:Godot 4 | groups overview - YouTube
组主要用于对同一类或需要同样操作的对象,比如敌人检测到玩家等。将场景的根节点加入组,实例化后都归属于这个组中,可以通过 get_tree().call_group("objects", "function")
调用对应函数,或者通过 get_tree().get_nodes_in_group("objects")
获取成员列表。
(更详细的介绍可以参考官方文档和相关视频,这里不再赘述)
状态机(逻辑与动画的处理)
ARPG 项目的做法
(注:该做法虽然基于 uheartbeast 的 ARPG 项目,但思想是从别人教程中借鉴的)
将逻辑与动画分离,逻辑部分通过 _physics_process
每帧轮询,动画部分通过 _process
每帧轮询。两个函数都监听玩家的输入,然后分别做逻辑和动画的处理。
代码形如:
1 |
|
这种做法是自己早期编程探索出来的,现在看来稍显幼稚,难以支撑大项目的开发。
timothyqiu 的做法
编写一个可以复用的状态机脚本,项目中所有需要切换状态的对象都可以引用此节点,并实现所需的函数;所有的对象实现状态切换的模式都是一样的,也便于管理。
状态机脚本(这里直接复制 2D Adventure 的代码):
1 |
|
需要实现的函数:
- get_next_state - 获取下一个状态(每帧都调用,状态相同返回 -1)
- 内容:根据当前状态和操作,判断下一个状态是什么
- 形如
State.A: if ... : return State.B
- transition_state - 状态改变时的操作(只在状态前后不同的时候才调用)
- 内容:状态切换的瞬间应该做什么(包括动画播放和其他一些操作)
- 形如
if -> State.A: animation_player.player(...) do xxx
- tick_physics - 调用状态机的对象,用来替代
_physics_process
的函数- 运动逻辑,形如
match state: State.A: move(...)
- 运动逻辑,形如
注意:最好不要把其他的逻辑塞进这三个函数中(比如,如果想要实现动画播放结束后执行一些操作,最好不要在 get_next_state 函数中实现,毕竟这个函数只用于返回下一状态,如果状态不变,就不应该有多余的操作;可以在动画轨道中调用,或者写一个回调函数)
节点状态机初探
高级状态机参考文章:Building a more advanced state machine in Godot – The Shaggy Dev
YouTube 教程:
- Finite State Machines in Godot 4 in Under 10 Minutes - YouTube
- State Machine Setup for 2D Platformer Character ~ Godot 4 GameDev Tutorial - YouTube
基础的 State 类包括如下的方法:
- Enter:进入此状态的操作
- Exit:离开此状态的操作
- Update:与画面和逻辑有关的操作(如动画、计时器)
- Physics Update:与运动物理有关的操作(如速度)
往往需要一个状态机/管理来控制状态的转换并记录当前状态,通常包含:
- states:字典或列表,用于记录各个状态(使用 get_children 方法获取)
- current_state:记录当前的状态,便于直接调用节点的方法
(这里不作深究,这种方法更适合 C# 大项目的规范)
UI 设计(Panel/回合制/解谜)
节点补间动画
基本的逻辑如下:
1 |
|
其中,缓动和过渡效果的参数可以参考(颜色表示缓动,不同曲线组表示过渡):
- 移动动画举例
1 |
|
- 旋转动画举例
1 |
|
脚本生成多节点
以“在棋盘上生成棋子”的效果为例:
1 |
|
【注意】需要为生成的节点声明类名称(class_name)
场景转换效果
场景的转换有内置的 change_scene_to_file
函数,但这个函数是 deferred 的,第二帧才能看到效果,为此,可以自行编写函数处理:
1 |
|
对每个场景,也可专门设计一个场景的进入效果:
1 |
|
物品获取处理
玩家点击物品 -> 播放物品获取动画(这期间不允许玩家再交互)-> 物品消失
参考代码如下:
1 |
|
【吐槽】锈湖系列经常会出现连续点击一个物品后位置偏移的 bug,很可能就是没有很好地处理这部分的逻辑,没有确保物品交互后播放动画期间不能被玩家交互。
多对象交互(动作)
攻击与无敌时间
攻击的本质,是攻击方的 Hitbox 与受击方的 Hurtbox 重叠,大致的流程是:
- 攻击方进入攻击状态,开启 Hitbox
- 如果攻击状态中,Hitbox 与 Hurtbox 重叠:
- 受击方进行相应的操作,如扣血、添加受击效果等(可通过传递自定义信号,或使用 area_entered 信号传进的 area 的属性实现)
- 如果没有重叠,无事发生(miss)
- 攻击状态结束,关闭 Hitbox
- (注:自始至终,对象的 Hurtbox 都是默认开启的,除非进入无敌时间或游戏需要)
无敌时间,一般使用计时器来实现:受击时,开启无敌时间,设置 hurtbox 的可见性为 false;timeout 后,设置 hurtbox 的可见性为 true
补充:uheartbeast 的项目中,是直接在 area 的信号函数中扣血,而 timothyqiu 的项目是在 transition_state 中的 Hurt 状态下扣血(后者尽可能把状态转换的操作都放进函数 case 中)
需要注意的问题:
- 通常 Hitbox 和 Hurtbox 关于重叠的处理,只在其中一个里面实现:如果在 Hitbox 中实现,Hurtbox 的可见性与 monitorable 挂钩;如果在 Hurtbox 中实现,Hurtbox 的可见性与 monitoring 挂钩(这也是 uheartbeast 的项目中的一个问题:搞混了两个参数)
- 如果有受击动画,并且无敌时间在受击动画之间,可以考虑在受击动画的轨道上设置变量值
补充:击退效果(以 ARPG 项目为例)
1 |
|
敌人追踪玩家
- 使用 RayCast 实现:常用于横板动作游戏中地面人物的寻物 AI
RayCast 从敌人的眼睛发出,检测敌人前方的玩家(不包括后方,所以玩家可以背后偷袭)
1 |
|
- 使用 Area2D 实现:常用于俯视角 2D 游戏,或横板动作游戏的飞行敌人寻物 AI
为敌人设置一个检测玩家的 Area,玩家进入敌人的 DetectionArea 时,敌人登记玩家;玩家离开后,变量恢复空值。敌人的脚本在需要追踪玩家时,调用函数判断是否检测到玩家:
1 |
|
敌人软碰撞
我们希望多个敌人同时追赶玩家时,不要互相重叠;但如果直接设置敌人间相互碰撞(即设置敌人的根节点的碰撞 mask 为自己所在的层 layer),又会显得生硬,所以需要软碰撞。
软碰撞的本质是,当敌人相互碰撞时(此时已经有重叠了,如果使用普通的碰撞就不会有重叠的可能),给敌人之间一个对彼此相反的“力”,即一个运动方向向量,然后处理赋值给 velocity,代码的实现大致如下:
1 |
|
其他机制简要列举
2D 平台游戏(设计方面可以参考:【游戏制作工具箱】《蔚蓝》的手感为何迷人?Why Does Celeste Feel So Good to Play? | GMTK_哔哩哔哩_bilibili 9’30” 处,与教程相呼应)
- 狼跳:设置郊狼时间,走出地面开启,倒计时内仍然可跳跃
- 跳跃预判:快要着陆时按下跳跃键也可以在落地后起跳(按下跳跃键后开始计时,在时间内着陆后自动起跳)
- 长短跳:松开跳跃键后,若向上速度仍然很大,立即减速(短跳)
- 滑墙/蹬墙跳等(注意落地接水平输入需要立刻切换状态)
2D 俯视角游戏(也含 2D 横板动作游戏)
- 敌人追踪 AI:结合前面“追踪玩家”的实现,如果发现玩家,进入追逐状态(向玩家方向行进);如果玩家远离探测区域,恢复站立/游荡状态(可以配合寻路算法,避免敌人卡墙的情况)
- 敌人游荡状态:编写脚本专门实现,进入游荡状态后随机获取一个游荡时间,在此期间,朝着一个随机的方向移动随机距离(需要保证距离不能过小或过大)
1 |
|
其他常见游戏机制实现思路:
- 存档:通过全局脚本控制,关键性的操作要实时调用该脚本保存,关闭游戏后写入文件,开启游戏后读取文件(可以使用字典保存成 json 文件,不过很多开发者建议自定义格式;json 的文件格式过于麻烦,web 可能需要这种统一的文件格式,游戏开发就没必要刻意遵守了)
- 音乐/音效:通过全局脚本控制,在需要的时候调用播放音乐的函数即可
- 读档的场景变化:创建 FlagSwitch 脚本,可以结合存档的全局脚本使用;当场景需要变化时,先将变化传递给存档脚本,存档脚本再发出信号到 FlagSwitch 中,调用节点变化函数
参考:《迷失岛2》复刻项目
1 |
|
(以上只是对参考教程中提到的要素做简要整理,远远达不到全面的程度)