开场白
半夜忽惊起,写下一个绝妙的游戏点子。
那就开始做吧。
希望在开学前做出一个游戏DEMO。
开始学习Godot!
o((>ω< ))o
Godot
Godot 关键概念
在 Godot 中,游戏就是一棵由节点构成的树,树又可以结合起来构成场景。然后你还可以将这些节点连起来,让它们通过信号进行通信。
场景
场景可以是一个角色、一件武器、用户界面中的一个菜单、一座房子、整个关卡,或者任何你能想到的东西。
场景可以嵌套。
节点
场景由若干节点组成。节点是游戏最小的构件,排列成树状。
所有节点都具备以下特性:
- 名称。
- 可编辑的属性。
- 每帧都可以接收回调以进行更新。
- 你可以使用新的属性和函数来进行扩展。
- 你可以将它们添加为其他节点的子节点。
场景树
游戏的所有场景都汇集在场景树中。由于场景是节点树,因此场景树也是节点树。但是,从场景的角度来考虑你的游戏更容易,因为它们可以代表角色、武器、门或你的用户界面。
信号
节点在发生某些事件时发出信号。此功能无需在代码中硬连接它们就能让节点相互通信。它为你提供了构建场景的灵活性。
快捷键
快捷键 | 功能 |
---|---|
F5 | 开始运行项目 |
F8 | 结束运行项目 |
Ctrl + D | 创建副本 |
F6 | 运行当前场景 |
GDScript
基础知识
函数
1 | func name(p1, p2): |
函数内容不能为空,可以像python一样使用
pass
语句进行占位。
变量
1 | var name = 10 |
变量的使用类似于标签。
1 | var name = 10 |
变量或值之间的比较只有“>”、“<”、“==”、“!=”符号,没有“<=”和“>=”。
GameLoop 特殊函数 _process(delta)
1 | func _process(delta): # delta是唯一的参数 |
在执行完set_process(true)
语句后该函数会一直循环执行。
每个游戏中的对象都有一个各自的_process(delta)
Godot引擎会尝试尽量让该函数执行更快更频繁,使得画面更流畅。
每次运行的时间间隔就是 帧(Frame)。
delta
表示是从上一次执行_process()
开始到当前时刻的时间间隔。(应用时其实就是相邻两帧之间的时间。)
因为帧之间的时间间隔是随时改变的,那么在不使用delta
的情况下,场景的变换速率也是随时改变的:
1 | func _process(delta): |
这时给它的参数乘上一个delta
就可以使它能够匀速旋转,即使游戏的帧数正在改变:
1 | func _process(delta): |
这其实就是所谓的让 像“自旋速率”这样与时间相关的变量 从“帧独立(frame-dependent)”转变成“时间独立(time-dependent)”。
那么与
delta
相乘的值其实就是每秒的速率。
if语句和while语句
GDScript中if语句、while语句用法与python相同。
for语句
1
2
3
4
5
6 for number in range(3):
print(number)
# 0 1 2
# 与 for number in [0, 1, 2]:
# print(number)
# 等价
2D Vectors
下面语句等价:
1 | position -= Vector2(50,0) |
“=”、“+=”、“*=”、“/=”类似。
array
array的使用与python中的列表基本一致。
String类型就是储存字符的array。
将array用作队列:
append()
、pop_front()
将array用作栈:
append()
、pop_back()
通过
.size()
访问大小。
dictionary
dictionary的使用与python中的字典基本一致。
1 | var dict = { |
向字典中添加键值对的方法:
1 | dict[key] += val # 使用“=”也可 |
使用.keys()
遍历所有的key:
1 | for item_name in dict.keys(): |
实际上,可以省略
.keys()
:
1
2 for item_name in dict:
print(item_name)
此时,使用dict[key]
获取对应value。
在保证唯一性的情况下,可以使用任何类型作为key。
而value的类型可能受到的限制就多一些。
数据类型
使用int()
、float()
、str()
进行类型转换。
Vector2类型与单个数字进行乘除是兼容的,但加减不行。
整数除法如"3/2"默认是去除小数部分,需要改为"3/2.0"才会得到1.5。
让一个整数乘1.0即可变为浮点型。
可选的显式数据类型
使用如下显式的数据类型定义来避免类型使用错误:
1 | var variable_name: Type = value |
更进一步可以简写为:
1 | var variable_name := value |
实战知识
extends
1 | extends Sprite2D |
每个 GDScript 文件都是一个隐含的类。extends
关键字定义了这个脚本所继承或扩展的类。本例中为Sprite2D
,意味着我们的脚本将获得 Sprite2D 节点的所有属性和方法,包括它继承的 Node2D
、CanvasItem
、Node
等类。
_init()
1 | func _init(): |
让我们把它分解一下。 func
关键字定义了一个名为 _init
的新函数。这是类构造函数的一个特殊名称。如果你定义了这个函数,引擎会在内存中创建每个对象或节点时调用 _init()
。
_process(delta)
参考上文基础知识。
请注意
_process()
和_init()
一样都是以下划线开头的。按照约定,这是 Godot 的虚函数,也就是你可以覆盖的与引擎通信的内置函数。
我们的 Sprite2D 由于 _process()
函数中的代码而移动。Godot 提供了一种打开和关闭处理的方法:Node.set_process() 。Node 的另一个方法 is_processing()
,如果空闲处理处于活动状态,则返回 true
。我们可以使用 not
关键字来反转该值。
1 | func _on_button_pressed(): |
_on_button_pressed()
是人为设置的按钮给出的信号,这个函数名在将按钮与对象链接时自动生成。(游戏引擎自动给出函数框架,由程序员对函数内容进行补充。)这里是通过编辑器连接信号的方法。
定义velocity
1 | var speed = 400 |
定义一个名为 velocity
的局部变量,该变量是用于表示方向和速度的 2D 向量。要让节点向前移动,我们可以从 Vector2 类的常量 Vector2.UP
入手,这个向量指向上方,调用 Vector2
的 rotated()
方法可以将其进行旋转。表达式 Vector2.UP.rotated(rotation)
表示的是指向图标前方的向量。用这个方向与我们的 speed
属性相乘后,得到的就是用来移动节点的速度。
rotation
、position
是Node2D类的成员变量。PI是GDScript语言的常量。
实现出的效果就是图像在绕圈圈。
两种处理玩家输入途径
-
内置的输入回调,主要是
_unhandled_input()
。和_process()
一样 ,它是一个内置的虚函数,Godot 每次在玩家按下一个键时都会调用。它是你想用来对那些不是每一帧都发生的事件做出反应的工具,比如按Space
来跳跃。更多关于输入回调的信息请参阅 使用 InputEvent 。
-
Input
单例。单例是一个全局可访问的对象。Godot 在脚本中提供对几个对象的访问。它是每一帧检查输入的有效工具。
下面讲解第二种途径
Input
。
1 | var speed = 400 |
这段代码实现出来的效果跟游戏“坦克动荡”的移动类似。
为了检查当前帧玩家是否按下了某个键,我们需要调用 Input.is_action_pressed()
。这个方法使用一个字符串来表示一个输入动作。当该按键被按下时,函数返回 true
,否则这个函数将返回 false
。
上面我们使用的两个动作,“ui_left
”和“ui_right
”,是每个 Godot 项目中预定义的。它们分别在玩家按键盘上的左右箭头或游戏手柄上的左右键时触发。
我们将 velocity
的值初始化为 Vector2.ZERO
,这是内置 Vector
类型的一个常量,代表长度为 0 的二维向量。
如果玩家按下“ui_up
”动作,我们就会更新速度的值,使对象向前移动。
打开“项目 -> 项目设置”并点击“输入映射”选项卡,就可以查看并编辑项目中的输入动作。
连接信号的两种方法
上文中使用到的_on_button_pressed()
是通过编辑器连接信号的方法。
除此之外,还可以通过代码连接信号:
核心思路:获取发出信号的节点的引用,并储存到目标节点中的本地变量。然后通过这个变量的成员信号调用connect
方法。
要使用代码来连接信号,你需要调用所需监听节点信号的
connect()
方法。这里我们要监听的是 Timer 的“timeout
”信号。
我们想要在场景实例化时连接信号,我们可以使用 Node._ready() 内置函数来实现这一点,当节点完全实例化时,引擎会自动调用该函数。
为了获取相对于当前节点的引用,我们使用方法 Node.get_node()。我们可以将引用存储在变量中。
1 | func _ready(): |
该行读起来是这样的:我们将计时器的“timeout
”信号连接到脚本附加到的节点上。当计时器发出timeout
时,去调用我们需要定义的函数_on_timer_timeout()
。让我们将其定义添加到脚本的底部,并使用它来切换对象的可见性。
1 | func _on_timer_timeout(): |
visible
属性是一个布尔值,用于控制节点的可见性。
如果你现在运行 Node2D 场景,就会看到对象在闪啊闪的,间隔为一秒。(因为默认计时为一秒,并启用了Autostart功能。)
自定义信号
你可以在脚本中定义自定义信号。例如,假设你希望在玩家的生命值为零时通过屏幕显示游戏结束。为此,当他们的生命值达到 0 时,你可以定义一个名为“died
”或“health_depleted
”的信号。
1 | extends Node2D |
自定义信号的工作方式与内置信号相同:它们显示在“节点”选项卡中(在表示脚本的.gd
的特殊栏目下),你可以像连接其他信号一样连接到它们。
要通过代码发出信号,请调用信号的 emit()
方法。
1 | func take_damage(amount): |
信号还可以选择声明一个或多个参数。在括号之间指定参数的名称:
1 | signal health_changed(old_value, new_value) |
要在发出信号的同时传值,请将它们添加为 emit()
函数的额外参数:
1 | func take_damage(amount): |