少女祈祷中...

开场白

半夜忽惊起,写下一个绝妙的游戏点子。

那就开始做吧。

希望在开学前做出一个游戏DEMO

开始学习Godot

o((>ω< ))o

Godot

Godot 关键概念

Godot 中,游戏就是一棵由节点构成的,树又可以结合起来构成场景。然后你还可以将这些节点连起来,让它们通过信号进行通信。

场景

场景可以是一个角色、一件武器、用户界面中的一个菜单、一座房子、整个关卡,或者任何你能想到的东西。

场景可以嵌套。

节点

场景由若干节点组成。节点是游戏最小的构件,排列成树状。

所有节点都具备以下特性:

  • 名称。
  • 可编辑的属性。
  • 每帧都可以接收回调以进行更新。
  • 你可以使用新的属性和函数来进行扩展。
  • 你可以将它们添加为其他节点的子节点。

场景树

游戏的所有场景都汇集在场景树中。由于场景是节点树,因此场景树也是节点树。但是,从场景的角度来考虑你的游戏更容易,因为它们可以代表角色、武器、门或你的用户界面。

信号

节点在发生某些事件时发出信号。此功能无需在代码中硬连接它们就能让节点相互通信。它为你提供了构建场景的灵活性。

快捷键

快捷键 功能
F5 开始运行项目
F8 结束运行项目
Ctrl + D 创建副本
F6 运行当前场景

GDScript

基础知识

函数

1
2
3
func name(p1, p2):
instruction_1
instruction_2

函数内容不能为空,可以像python一样使用pass语句进行占位。

变量

1
var name = 10

变量的使用类似于标签。

1
2
3
4
var name = 10
name = "text"
print(name)
# 输出“text”

变量或值之间的比较只有“>”、“<”、“==”、“!=”符号,没有“<=”和“>=”。

GameLoop 特殊函数 _process(delta)

1
2
func _process(delta):			# delta是唯一的参数
...

在执行完set_process(true)语句后该函数会一直循环执行。

每个游戏中的对象都有一个各自的_process(delta)

Godot引擎会尝试尽量让该函数执行更快更频繁,使得画面更流畅。

每次运行的时间间隔就是 帧(Frame)

delta表示是从上一次执行_process()开始到当前时刻的时间间隔。(应用时其实就是相邻两帧之间的时间。)

因为帧之间的时间间隔是随时改变的,那么在不使用delta的情况下,场景的变换速率也是随时改变的:

1
2
3
func _process(delta):
rotate(3.0)
# 这是一个速度不稳定的旋转

这时给它的参数乘上一个delta就可以使它能够匀速旋转,即使游戏的帧数正在改变:

1
2
3
func _process(delta):
rotate(3.0 * delta)
# 这是一个速度稳定的旋转

这其实就是所谓的让 像“自旋速率”这样与时间相关的变量 从“帧独立(frame-dependent)”转变成“时间独立(time-dependent)”。

那么与delta相乘的值其实就是每秒的速率。

if语句和while语句

GDScriptif语句、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
2
3
4
position -= Vector2(50,0)

position.x -= 50
position.y -= 0

“=”、“+=”、“*=”、“/=”类似。

array

array的使用与python中的列表基本一致。

String类型就是储存字符的array

array用作队列:append()pop_front()

array用作栈:append()pop_back()

通过.size()访问大小。

dictionary

dictionary的使用与python中的字典基本一致。

1
2
3
4
var dict = {
"key" : val,
...
}

向字典中添加键值对的方法:

1
dict[key] += val			# 使用“=”也可

使用.keys()遍历所有的key

1
2
for item_name in dict.keys():
print(item_name)

实际上,可以省略.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 节点的所有属性和方法,包括它继承的 Node2DCanvasItemNode 等类。

_init()

1
2
func _init():
...

让我们把它分解一下。 func 关键字定义了一个名为 _init 的新函数。这是类构造函数的一个特殊名称。如果你定义了这个函数,引擎会在内存中创建每个对象或节点时调用 _init()

_process(delta)

参考上文基础知识。

请注意 _process()_init() 一样都是以下划线开头的。按照约定,这是 Godot 的虚函数,也就是你可以覆盖的与引擎通信的内置函数。

我们的 Sprite2D 由于 _process() 函数中的代码而移动。Godot 提供了一种打开和关闭处理的方法:Node.set_process()Node 的另一个方法 is_processing() ,如果空闲处理处于活动状态,则返回 true。我们可以使用 not 关键字来反转该值。

1
2
func _on_button_pressed():
set_process(not is_processing())

_on_button_pressed()是人为设置的按钮给出的信号,这个函数名在将按钮与对象链接时自动生成。(游戏引擎自动给出函数框架,由程序员对函数内容进行补充。)

这里是通过编辑器连接信号的方法。

定义velocity

1
2
3
4
5
6
7
var speed = 400
var angular_speed = PI

func _process(delta):
rotation += angular_speed * delta
var velocity = Vector2.UP.rotated(rotation) * speed
position += velocity * delta

定义一个名为 velocity 的局部变量,该变量是用于表示方向和速度的 2D 向量。要让节点向前移动,我们可以从 Vector2 类的常量 Vector2.UP 入手,这个向量指向上方,调用 Vector2rotated() 方法可以将其进行旋转。表达式 Vector2.UP.rotated(rotation) 表示的是指向图标前方的向量。用这个方向与我们的 speed 属性相乘后,得到的就是用来移动节点的速度。

rotationpositionNode2D类的成员变量。

PIGDScript语言的常量。

实现出的效果就是图像在绕圈圈。

两种处理玩家输入途径

  1. 内置的输入回调,主要是 _unhandled_input()。和 _process()一样 ,它是一个内置的虚函数,Godot 每次在玩家按下一个键时都会调用。它是你想用来对那些不是每一帧都发生的事件做出反应的工具,比如按 Space 来跳跃。

    更多关于输入回调的信息请参阅 使用 InputEvent

  2. Input 单例。单例是一个全局可访问的对象。Godot 在脚本中提供对几个对象的访问。它是每一帧检查输入的有效工具。

下面讲解第二种途径Input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var speed = 400
var angular_speed = PI

func _process(delta):
var direction = 0
if Input.is_action_pressed("ui_left"):
direction = -1
if Input.is_action_pressed("ui_right"):
direction = 1
rotation += angular_speed * direction * delta
var velocity = Vector2.ZERO
if Input.is_action_pressed("ui_up"):
velocity = Vector2.UP.rotated(rotation) * speed
position += velocity * delta

这段代码实现出来的效果跟游戏“坦克动荡”的移动类似。

为了检查当前帧玩家是否按下了某个键,我们需要调用 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
2
3
func _ready():
var timer = get_node("Timer")
timer.timeout.connect(_on_timer_timeout)

该行读起来是这样的:我们将计时器的“timeout”信号连接到脚本附加到的节点上。当计时器发出timeout时,去调用我们需要定义的函数_on_timer_timeout()。让我们将其定义添加到脚本的底部,并使用它来切换对象的可见性。

1
2
func _on_timer_timeout():
visible = not visible

visible 属性是一个布尔值,用于控制节点的可见性。

如果你现在运行 Node2D 场景,就会看到对象在闪啊闪的,间隔为一秒。(因为默认计时为一秒,并启用了Autostart功能。)

自定义信号

你可以在脚本中定义自定义信号。例如,假设你希望在玩家的生命值为零时通过屏幕显示游戏结束。为此,当他们的生命值达到 0 时,你可以定义一个名为“died”或“health_depleted”的信号。

1
2
3
extends Node2D
signal health_depleted
var health = 10

自定义信号的工作方式与内置信号相同:它们显示在“节点”选项卡中(在表示脚本的.gd的特殊栏目下),你可以像连接其他信号一样连接到它们。

要通过代码发出信号,请调用信号的 emit() 方法。

1
2
3
4
func take_damage(amount):
health -= amount
if health <= 0:
health_depleted.emit()

信号还可以选择声明一个或多个参数。在括号之间指定参数的名称:

1
signal health_changed(old_value, new_value)

要在发出信号的同时传值,请将它们添加为 emit() 函数的额外参数:

1
2
3
4
func take_damage(amount):
var old_health = health
health -= amount
health_changed.emit(old_health, health)