Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

GDScript 参考

GDScript 是一种面向对象的高级指令式渐进类型编程语言,专为 Godot 构建。GDScript 的语法基于缩进,与 Python 等语言类似。设计 GDScript 这门语言旨在对 Godot 引擎进行优化,与 Godot 引擎紧密集成,从而为内容的创建与继承提供灵活的手段。

GDScript 是完全独立于 Python 存在的,二者之间并不不存在继承与扩展关系。

历史

备注

关于 GDScript 历史的文档已移至常见问题

GDScript 示例

考虑到部分开发者了解过编程语法,学起GDScript来可能会快些,因此这里给出一个 GDScript 的简单示例供参考学习。

# Everything after "#" is a comment.
# A file is a class!

# (optional) icon to show in the editor dialogs:
@icon("res://path/to/optional/icon.svg")

# (optional) class definition:
class_name MyClass

# Inheritance:
extends BaseClass


# Member variables.
var a = 5
var s = "Hello"
var arr = [1, 2, 3]
var dict = {"key": "value", 2: 3}
var other_dict = {key = "value", other_key = 2}
var typed_var: int
var inferred_type := "String"

# Constants.
const ANSWER = 42
const THE_NAME = "Charly"

# Enums.
enum {UNIT_NEUTRAL, UNIT_ENEMY, UNIT_ALLY}
enum Named {THING_1, THING_2, ANOTHER_THING = -1}

# Built-in vector types.
var v2 = Vector2(1, 2)
var v3 = Vector3(1, 2, 3)


# Functions.
func some_function(param1, param2, param3):
    const local_const = 5

    if param1 < local_const:
        print(param1)
    elif param2 > 5:
        print(param2)
    else:
        print("Fail!")

    for i in range(20):
        print(i)

    while param2 != 0:
        param2 -= 1

    match param3:
        3:
            print("param3 is 3!")
        _:
            print("param3 is not 3!")

    var local_var = param1 + 3
    return local_var


# Functions override functions with the same name on the base/super class.
# If you still want to call them, use "super":
func something(p1, p2):
    super(p1, p2)


# It's also possible to call another function in the super class:
func other_something(p1, p2):
    super.something(p1, p2)


# Inner class
class Something:
    var a = 10


# Constructor
func _init():
    print("Constructed!")
    var lv = Something.new()
    print(lv.a)

如果你以前有过使用 C、 C++ 或 C# 之类的静态类型语言编程经验,却从未使用过动态类型编程语言,建议阅读此教程: GDScript:动态语言入门

语言

下面是有关 GDScript 的一些概述。对于 GDScript 更为详细的信息,如哪些方法可用于数组或其他对象,可以在链接的类描述中查找到。

标识符

仅限于含字母字符( azAZ )、 数字( 09 )和下划线 _ 的字符串可作为标识符。同时,标识符不能以数字开头,且大小写敏感(如 fooFOO 就是两个不同的标识符)。

标识符现在也允许包含 UAX#31 所提供的部分 Unicode 字符,也就是说,你现在也可以将非英文字符作为标识符使用。然而, Unicode 字符中易与 ASCII 字符混淆的字符以及颜文字则是无法作为标识符使用的。

关键字

以下是该语言支持的关键字列表。由于关键字是保留字(记号),故不能用作标识符。操作符(如 innotandor)及下列内置类型的名称也是保留字。

想深入了解关键字的话,可在 GDScript 词法分析器中找到对于关键字的定义。

关键字

描述

if

if/else/elif

elif

if/else/elif

else

if/else/elif

for

for

while

while

match

match

break

退出当前 forwhile 循环的执行。

continue

立即跳到 forwhile 循环的下一个迭代。

pass

语法上要求在不希望执行代码的语句中使用,例如在空函数中使用。

return

从函数当中返回一个值。

class

定义一个内部类。见 内部类

class_name

将脚本定义为具有指定名称的全局可访问类。见 注册具名类

extends

定义当前类继承什么类。

is

检测变量是否继承自给定的类,或检测该变量是否为给定的内置类型。

in

通常情况下用来检查字符串、列表、范围、字典、节点中是否存在某个值,而和 for 关键字连用时,则用于遍历字符串、列表、范围、字典、节点中的内容。

as

尝试将值转换为给定类型的值。

self

引用当前类实例。

signal

定义信号。

func

定义函数。

static

将一个函数声明为静态函数,或将一个成员变量声明为静态成员变量。

const

定义常量。

enum

定义枚举。

var

定义变量。

breakpoint

用来设置脚本编辑器辅助调试断点的关键字。与在脚本编辑器每行最左侧点击红点创建断点不同, breakpoint 关键字可以储存在脚本内部。在不同机器上使用版本工具时,由 breakpoint 关键字创建的断点仍旧有效。

preload

预加载一个类或变量,参见 类作为资源

await

等待信号或协程完成,参见等待信号和协程

yield

以前的版本中用于协程,现保留为关键字,方便迁移。

assert

断言条件,如果失败则记录错误。在非调试版本中忽略掉断言语法。参见 Assert 关键字

void

用于代表函数不返回任何值。

PI

PI(π)常数。

TAU

TAU(τ)常数。

INF

无穷常量,用于比较和计算结果。

NAN

NAN(非数)常量,用作计算后不可能得到的结果。

运算符

下面是支持的运算符列表及其优先级(越靠上运算优先级越高)。

运算符

描述

( )

分组(优先级最高)

括号其实不是运算符,但是能够让你显式指定运算的优先级。

x[index]

下标

x.attribute

属性引用

foo()

函数调用

await x

等待信号和协程

x is Node

类型检查

另见 is_instance_of() 函数。

x ** y

幂(乘方)

x 与其自身相乘 y 次,类似于调用 pow() 函数。

注意:在 GDScript 中,** 运算符是左结合的,详情见表后。

~x

按位取反

+x
-x

一致 / 求负

x * y
x / y
x % y

乘法/除法/余数

% 运算符也用于字符串的格式化

注意:这些运算符的行为与 C++ 一致,对于来自 Python、JavaScript 等语言的用户可能存在意外的行为,详情见表后。

x + y
x - y

加法(或连接)/减法

x << y
x >> y

位移位

x & y

按位与

x ^ y

按位异或

x | y

按位或

x == y
x != y
x < y
x > y
x <= y
x >= y

比较

详情见表后。

x in y
x not in y

包含检查

in 也在 for 关键字的语法中使用。

not x
!x

布尔“非”,以及不推荐使用的别名

x and y
x && y

布尔“与”,以及不推荐使用的别名

x or y
x || y

布尔“或”,以及不推荐使用的别名

真表达式 if 条件 else 假表达式

三元运算符 if/else

x as Node

类型转换

x = y
x += y
x -= y
x *= y
x /= y
x **= y
x %= y
x &= y
x |= y
x ^= y
x <<= y
x >>= y

赋值(优先级最低)

表达式中不能使用赋值运算符。

备注

一些运算符的运算机制可能会与你的预期有所不符:

  1. 若运算符 / 两端的数值均为 int,则进行整数除法而非浮点数除法。例如: 5 /2 == 22 为该算式的结果而非 2.5 为结果。若希望进行浮点数运算,请将该运算符两端的其中一个数值的类型改为 float 。例如:直接使用浮点数( x / 2.0 )、转换类型( float(x) / y )、乘以 1.0x * 1.0 / y )等。

  2. 运算符 % 仅适用于整型数值的取余运算。对于小数的取余运算,请使用 fmod() 方法。

  3. 对于负值,% 运算符和 fmod() 函数使用 截断算法 而非向负无穷大舍入,此时余数会带有符号(即余数可能为负)。如果你需要数学意义上的余数,请改用 posmod()fposmod() 函数。

  4. 运算符 **左结合运算符 ,也就是说, 2 ** 2 ** 3 这个运算等价于 (2 ** 2) ** 3 。对此,请使用括号来处理该运算的优先级,如 2 ** (2 ** 3)

  5. ==!= 运算符在有些情况下允许你比较不同类型的值(例如,1 == 1.0 的结果为真),但在其他情况下可能会发生运行时错误。如果你不能确定操作数的类型,可以安全地使用 is_same() 函数(但请注意,该函数对类型和引用更加严格)。要比较浮点数,请改用 is_equal_approx()is_zero_approx() 函数。

字面量

Example(s)

描述

null

空值

false, true

布尔值

45

十进制整数

0x8f51

十六进制整数

0b101010

二进制整数

3.14, 58.1e-10

浮点数(实数)

"Hello", "Hi"

常规字符串

"""Hello""", '''Hi'''

常规字符串(用三对引号括住)

r"Hello", r'Hi'

原始字符串

r"""Hello""", r'''Hi'''

原始字符串(用三对引号括住)

&"name"

StringName

^"Node/Label"

NodePath

也有两种长得像字面量,但实际上不是字面量的量:

示例

描述

$NodePath

get_node("NodePath") 的简写

%UniqueNode

get_node("%UniqueNode") 的简写

整数和浮点数可以用 _ 来分隔,使其更加易读。以下表示数字的方法都是有效的:

12_345_678  # Equal to 12345678.
3.141_592_7  # Equal to 3.1415927.
0x8080_0000_ffff  # Equal to 0x80800000ffff.
0b11_00_11_00  # Equal to 0b11001100.

常规字符串字面量 内可包含以下转义序列:

转义序列

转义为

\n

换行(line feed,LF)

\t

水平制表符(tab)

\r

回车(carriage return,CR)

\a

警报(蜂鸣/响铃)

\b

退格键(Backspace)

\f

换页符(form feed,FF)

\v

垂直制表符(tab)

\"

双引号

\'

单引号

\\

反斜杠

\uXXXX

Unicode UTF-16 码位 XXXX (16进制,不区分大小写)

\UXXXXXX

Unicode UTF-32 码位 XXXXXX (16进制,不区分大小写)

有两种方法可以表示 0xFFFF 以上的转义 Unicode 字符:

  • 使用 UTF-16 代理对 \uXXXX\uXXXX 表示。

  • 使用单个 UTF-32 码位 \UXXXXXX 表示。

此外,在字符串中使用 \ 后换行可以让该符号后的文字自动换行,而无需在字符串中插入换行符。

包含在一种类型的引号(如 ")中的字符串,无需转义即可包含另一种类型的引号(如 ')。三引号字符串允许你避免转义最多两个连续相同类型的引号 (除非这些引号与字符串边缘相邻)。

原始字符串字面量 始终按照源代码中出现的方式对字符串进行编码,对于正则表达式特别有用。原始字符串不处理转义序列,但你可以“转义”引号或反斜杠(引号和反斜杠会替换自身)。

print("\tchar=\"\\t\"")  # Prints `    char="\t"`.
print(r"\tchar=\"\\t\"") # Prints `\tchar=\"\\t\"`.

GDScript 也支持 GDScript 格式字符串

注解

GDScript中有一些特殊标记,虽然起着类似于关键字的作用,但这些标记本身却不是关键字。这些标记就是 注解 ,每个注解均以 @ 符号开头,并配以注解名称。有关注解的详细说明及其使用范例,请参考 GDScript class reference

注解会影响外部工具处理脚本的方式,通常不会更改该脚本的行为。

比如,你可以将变量导出到编辑器中:

@export_range(1, 100, 1, "or_greater")
var ranged_var: int = 50

要获取更多关于导出属性的信息,请阅读 GDScript exports

所有与注解要求传入的参数类型相符、位置相配的常量表达式均可作为该注解参数传入其中:

const MAX_SPEED = 120.0

@export_range(0.0, 0.5 * MAX_SPEED)
var initial_speed: float = 0.25 * MAX_SPEED

注解可以单行修饰,也可以多行修饰,修饰离该注解最近的非注解语句。注解可以携带参数,每个参数均在注解名后的括号内,彼此之间用逗号隔开。

下面两个例子的效果是等价的:

@annotation_a
@annotation_b
var variable

@annotation_a @annotation_b var variable

@onready 注解

使用节点时,经常会需要将场景中某一部分的引用存放在变量中。由于场景只有在进入活动场景树时才会进行正确配置,因此只有在调用 Node._ready() 时才能获得子节点。

var my_label


func _ready():
    my_label = get_node("MyLabel")

这种操作比较麻烦,而且节点和外部引用越多,操作起来越显得不便。为此,GDScript 提供了 @onready 注解 ,将成员变量的初始化推迟到该节点调用 _ready() 的时刻进行。使用该注解,可以用一行代码替换掉上面的代码:

@onready var my_label = get_node("MyLabel")

警告

同时使用 @onready@export 这两个注解去修饰同一个变量,其效果并不会如你预期的那样理想,因为 @onready 注解会使该变量的默认值在 @export 注解起效后被赋值,导致该默认值被 @onready 的效果所覆盖:

@export var a = "init_value_a"
@onready @export var b = "init_value_b"

func _init():
    prints(a, b) # init_value_a <null>

func _notification(what):
    if what == NOTIFICATION_SCENE_INSTANTIATED:
        prints(a, b) # exported_value_a exported_value_b

func _ready():
    prints(a, b) # exported_value_a init_value_b

为此,本引擎提供了 ONREADY_WITH_EXPORT 警告选项,默认作为编辑器错误进行处理。我们并不推荐关闭或忽略该警告选项。

注释

# 所在行的所有内容都会被忽略,会视为注释进行处理。

# This is a comment.

小技巧

在 Godot 的脚本编辑器内,一些特殊关键字会在注释中高亮显示以提醒用户:

  • 关键提示 (标红)ALERTATTENTIONCAUTIONCRITICALDANGERSECURITY

  • 警告提示 (标黄)BUGDEPRECATEDFIXMEHACKTASKTBDTODOWARNING

  • 一般提示 (标绿)INFONOTENOTICETESTTESTING

这些关键字均大小写敏感,故需要全大写以保证能被引擎所识别:

# In the example below, "TODO" will appear in yellow by default.
# The `:` symbol after the keyword is not required, but it's often used.

# TODO: Add more items for the player to choose from.

可以在编辑器设置的 文本编辑器 > 主题 > 注释标记 部分中更改突出显示的关键字列表及其颜色。

代码区块

代码区块是一种特殊类型的注释,脚本编辑器将其理解为 可折叠区块,即在编写代码区块注释后,可以通过点击注释左侧出现的箭头来折叠和展开该区块。该箭头用一个紫色方块包围起来,以区别于标准的代码折叠。

语法如下:

# Important: There must be *no* space between the `#` and `region` or `endregion`.

# Region without a description:
#region
...
#endregion

# Region with a description:
#region Some description that is displayed even when collapsed
...
#endregion

小技巧

要快速创建代码区块,请在脚本编辑器中选择若干行,右键点击选区,然后选择 创建代码区块即可。系统将自动选中区块描述以对其进行编辑。

可以将代码区块嵌套在其他代码区块内。

以下是代码区块的具体使用示例:

# This comment is outside the code region. It will be visible when collapsed.
#region Terrain generation
# This comment is inside the code region. It won't be visible when collapsed.
func generate_lakes():
    pass

func generate_hills():
    pass
#endregion

#region Terrain population
func place_vegetation():
    pass

func place_roads():
    pass
#endregion

代码区块可以将大块代码组织成更容易理解的部分,但请注意,外部编辑器通常不支持该特性。因此即便不依赖代码区块,也要确保你的代码易于理解。

备注

单独的函数和被缩进的部分(例如 iffor始终 可以在脚本编辑器中折叠,故应避免使用代码区块来包含单一函数或被缩进的部分,执意使用也并不会带来太多好处。代码区块在将多个元素分组在一起时效果最佳。

行间语句接续

在GDScript中,一行语句可以通过反斜杠( \ )来接续到下一行。将反斜杠加在语句行末尾可以衔接该语句行的代码与下一语句行的代码。例如:

var a = 1 + \
2

可以按以下方式对单个语句行进行多行接续:

var a = 1 + \
4 + \
10 + \
4

内置类型

内置类型是分配在栈上的,按值传递,在每次赋值或将其作为参数传递给函数时都会复制其值。例外:对象 Object 、数组 Array 、字典 Dictionary 以及密存数组(如 PackedByteArray ),这些类型的值是按引用进行传递的,其实例的值相互共享。数组、字典以及部分对象( NodeResource )均有一个 duplicate() 方法,允许对其具体值进行复制操作。

基本内置类型

GDScript 中的变量可以赋值为不同的内置类型。

null

null 是一个空数据类型,既不包含任何信息,也不能赋值为其他任何值。

bool

“boolean”(布尔)的缩写,只能包含 truefalse

int

英文“integer”(整数)的缩写,存储整数(正整数和负整数)。存储的是 64 位值,等效于 C++ 中的 int64_t

float

使用浮点值存储实数,包括小数。存储的是 64 位值,等效于 C++ 中的 double。注意:目前 Vector2Vector3PackedFloat32Array 等数据结构存储的是 32 位单精度 float 值。

String

Unicode 格式的字符序列。

StringName

不可变字符串,一个实例仅允许拥有一个名称。该类型的实例创建起来较慢,在多线程环境下可能会导致锁等待。不过,该类型的实例比较起来比字符串快,非常适合在字典中作为键名使用。

NodePath

节点或节点属性的预解析路径,可以轻松地赋值成字符串,亦或从字符串中转换为节点路径。节点路径可用于与节点树交互以获取节点,亦或通过诸如 Tween等方式来影响属性。

内置向量类型

Vector2

2D 向量类型,包含 xy 两个字段,也可以像访问数组元素一样访问这两个字段。

Vector2i

同 Vector2,但其分量均为整型数值,在做2D网格中显示物品时非常实用。

Rect2

2D 矩形类型,包含两个向量字段: positionsize。还包含一个 end 字段,即 position + size

Vector3

3D 向量类型,包含 xyz 这三个字段,也可以像访问数组元素一样访问这些字段。

Vector3i

同 Vector3 ,但其分量均为整型数值,可用于为 3D 网格中的每个物品编制索引。

Transform2D

用于 2D 线性变换的3x2矩阵。

Plane

3D 平面类型的标准形式,包含一个向量字段 normal 以及一个 标量距离 d

Quaternion

四元数是一种用于表示 3D 旋转的数据类型,对于内插旋转十分有用。

AABB

轴对齐边界框(或 3D 边框),包含两个向量字段: positionsize. 还包含一个 end 字段, 即 position + size.

Basis

3×3矩阵,用于 3D 旋转与缩放。其包含3个向量字段( x, yz ),且可以像3D向量一样按索引访问这些向量字段。

Transform3D

3D 线性变换,包含一个 Basis(基)字段 basis 和一个 Vector3 字段 origin

引擎内置类型

Color

颜色数据类型包含 rgba 四个字段,也可以用 hsv 这三个字段来分别访问色相、饱和度、明度。

RID

资源ID(RID)。服务使用通用的 RID 来引用不透明数据。

Object

所有非内置类型的基类型。

容器内置类型

Array

任意对象类型的泛型序列,包括其他数组或字典(见下文)。数组可以动态调整大小,其索引从 0 开始,索引为负整数时则表示从数组尾部开始计数。

var arr = []
arr = [1, 2, 3]
var b = arr[1] # This is 2.
var c = arr[arr.size() - 1] # This is 3.
var d = arr[-1] # Same as the previous line, but shorter.
arr[0] = "Hi!" # Replacing value 1 with "Hi!".
arr.append(4) # Array is now ["Hi!", 2, 3, 4].

类型化数组

Godot 4.0 开始支持类型化数组。向类型化数组中写入数据时,Godot 会检查每个元素是否与该数组所指定的类型相匹配,因此类型化数组不能含有无效数据。而诸如 front()back() 等方法,虽然 GDScript 静态分析器会将类型化数组考虑在内,却仍会返回 Variant 类型的数值。

类型化数组通过 Array[Type] 指定,其中类型 Type 可以是 Variant 类型、内置类型,也可以是用户自定义类型、枚举类型等。不支持类型化数组嵌套(如 Array[Array[int]] )。

var a: Array[int]
var b: Array[Node]
var c: Array[MyClass]
var d: Array[MyEnum]
var e: Array[Variant]

Array 等价于 Array[Varaint]

备注

数组是按引用传递的,因此数组元素类型也是运行时变量引用的内存结构的一个属性。变量的静态类型限制了它可以引用的结构。因此,你 不能为数组内的元素赋予不同的元素类型的值,即使该类型是数组所接受类型的子类型。

若需要对类型化数组进行 转型 ,可以创建一个新数组,并使用 Array.assign() 方法:

var a: Array[Node2D] = [Node2D.new()]

# (OK) You can add the value to the array because `Node2D` extends `Node`.
var b: Array[Node] = [a[0]]

# (Error) You cannot assign an `Array[Node2D]` to an `Array[Node]` variable.
b = a

# (OK) But you can use the `assign()` method instead. Unlike the `=` operator,
# the `assign()` method copies the contents of the array, not the reference.
b.assign(a)

ArrayArray[Variant] )则是例外,这样做可以保证用户使用的便捷性与与旧版本代码的兼容性。不过,非类型化的数组是不安全的。

密存数组

GDScript 数组在内存中通过线性分配以提高速度,但使用大型数组(包含数万个元素)时却可能会导致内存碎片。如果在意这个问题,可以使用特定类型的密存数组,这些数组只接受单个数据类型,避免了内存碎片的同时使用的内存也更少。然而这些特定类型的数组是原子数组,运行起来通常要比通用数组慢,因此建议将这些数组仅用于大型数据集当中:

Dictionary

关联容器,其内部数值通过与之对应的唯一的键进行引用。

var d = {4: 5, "A key": "A value", 28: [1, 2, 3]}
d["Hi!"] = 0
d = {
    22: "value",
    "some_key": 2,
    "other_key": [2, 3, 4],
    "more_key": "Hello"
}

字典也支持 Lua 风格的 table 语法。Lua 风格的 GDScript 字典语法在标记字符串键时,使用的是 = 而非 :,且不使用引号(这样要写的东西会稍微少一些)。但请注意,以这种形式编写的键和 GDScript 标识符一样不能以数字开头,且必须为字面量。

var d = {
    test22 = "value",
    some_key = 2,
    other_key = [2, 3, 4],
    more_key = "Hello"
}

若要向现有字典添加键,可以像访问现有键一样访问要添加的键,并给其赋值:

var d = {} # Create an empty Dictionary.
d.waiting = 14 # Add String "waiting" as a key and assign the value 14 to it.
d[4] = "hello" # Add integer 4 as a key and assign the String "hello" as its value.
d["Godot"] = 3.01 # Add String "Godot" as a key and assign the value 3.01 to it.

var test = 4
# Prints "hello" by indexing the dictionary with a dynamic key.
# This is not the same as `d.test`. The bracket syntax equivalent to
# `d.test` is `d["test"]`.
print(d[test])

备注

方括号语法不仅可以用在 Dictionary 上,而且还可以用来存取任何 Object 的属性。不过要注意:尝试读取不存在的属性会引发脚本错误。要避免这一点,可换用 Object.get()Object.set() 方法。

Signal

信号由对象发出,并由对象所监听。 Signal 类型可以用于将信号广播者作为参数进行传递。

信号可以直接从对象实例中进行引用,如 $Button.button_up

Callable

可调用体包含一个对象及其某个函数,适用于将函数作为数值传递(例如:将可调用体用于信号连接)。

像引用成员属性那样引用一个方法的签名会返回可调用体。 如 var x = $Sprite2D.rotate 会将变量 x 赋值为一个可调用体,该可调用体含有对 $Sprite2D 对象的方法 rotate() 的引用。

可以调用 call 方法来调用可调体所指向的方法,如: x.call(PI)

数据

变量

变量可以作为类成员存在,也可以作为函数的局部变量存在,用 var 关键字创建,可以在初始化时指定一个值。

var a # Data type is 'null' by default.
var b = 5
var c = 3.8
var d = b + c # Variables are always initialized in direct order (see below).

变量可进行类型指定。指定类型时,将强制该变量始终容纳与被指定类型相同类型的数据。试图分配与该类型不兼容的值将触发报错。

在变量声明中,在变量名后面使用 :(冒号)+ 类型名 来指定类型。

var my_vector2: Vector2
var my_node: Node = Sprite2D.new()

如果在声明中初始化变量,则可以推断变量类型,在此情况下可省略类型名称:

var my_vector2 := Vector2() # 'my_vector2' is of type 'Vector2'.
var my_node := Sprite2D.new() # 'my_node' is of type 'Sprite2D'.

类型推断只有在指定的值具有定义的类型时才能通过检查,否则将触发报错。

有效的类型有:

  • 内置类型(如 Array 、 Vector2、 int、 String 等)。

  • 引擎自带类型(如 Node 、 Resource 、 Reference 等)。

  • 包含脚本资源的常量名(如 MyScript ,前提是声明了 const MyScript = preload("res://my_script.gd") )。

  • 在同一个脚本中的其他内部类,此时需要注意作用域(比如:在相同作用域内,在 class InnerClass 中声明 class NestedClass 则会得到 InnerClass.NestedClass )。

  • 通过 class_name 关键字声明的脚本类。

  • 自动加载的节点——单例节点。

备注

虽然 Variant 类型被引擎视作有效类型,但其并不是一个确切的类型,只是一个“没有固定类型”的代名词。使用 Variant 类型很有可能会导致报错,因此引擎默认不会对该类型进行推断。

你可以在项目设置中将该检查关闭,或将其设为警告。详见 GDScript 警告系统

Initialization order

Member variables are initialized in the following order:

  1. Depending on the variable's static type, the variable is either null (untyped variables and objects) or has a default value of the type (0 for int, false for bool, etc.).

  2. The specified values are assigned in the order of the variables in the script, from top to bottom. - (Only for ``Node``-derived classes) If the @onready annotation is applied to a variable, its initialization is deferred to step 5.

  3. If defined, the _init() method is called.

  4. When instantiating scenes and resources, the exported values are assigned.

  5. (Only for ``Node``-derived classes) @onready variables are initialized.

  6. (Only for ``Node``-derived classes) If defined, the _ready() method is called.

警告

You can specify a complex expression as a variable initializer, including function calls. Make sure the variables are initialized in the correct order, otherwise your values may be overwritten. For example:

var a: int = proxy("a", 1)
var b: int = proxy("b", 2)
var _data: Dictionary = {}

func proxy(key: String, value: int):
    _data[key] = value
    print(_data)
    return value

func _init() -> void:
    print(_data)

Will print:

{ "a": 1 }
{ "a": 1, "b": 2 }
{  }

To fix this, move the _data variable definition above the a definition or remove the empty dictionary assignment (= {}).

静态成员变量

成员变量可以声明为静态成员变量:

static var a

静态成员变量直属于类而非类的实例,即静态成员变量可以在多个类实例之间共享数据,这一点与一般的成员变量有所区别。

在类内,可以在任何函数中访问静态成员变量和非静态变量;而在类外,则可以通过引用类名或类的实例来访问静态成员变量(不推荐“类外”情况下的第二种使用方法,会导致可读性有所下降)。

备注

@export 注解和 @onready 注解不能修饰静态成员变量。局部变量不能声明为静态局部变量。

下例中,我们定义了一个 Person 类,声明了一个静态成员变量 max_id 。在游戏中,我们可以增加 max_id 这个静态成员变量来让我们更容易追踪游戏中 Person 实例的数量。

# person.gd
class_name Person

static var max_id = 0

var id
var name

func _init(p_name):
    max_id += 1
    id = max_id
    name = p_name

下面我们创建两个 Person 类的实例,会发现类和实例具有相同的 max_id 值,这是因为该成员变量是静态成员变量,能够在每个实例中访问。

# test.gd
extends Node

func _ready():
    var person1 = Person.new("John Doe")
    var person2 = Person.new("Jane Doe")

    print(person1.id) # 1
    print(person2.id) # 2

    print(Person.max_id)  # 2
    print(person1.max_id) # 2
    print(person2.max_id) # 2

静态成员变量可以指定类型,设置 setter 函数和 getter 函数:

static var balance: int = 0

static var debt: int:
    get:
        return -balance
    set(value):
        balance = -value

父类的静态成员变量也可以在子类中访问:

class A:
    static var x = 1

class B extends A:
    pass

func _ready():
    prints(A.x, B.x) # 1 1
    A.x = 2
    prints(A.x, B.x) # 2 2
    B.x = 3
    prints(A.x, B.x) # 3 3

@static_unload 注解

GDScript的类均为资源,静态成员变量会阻止脚本卸载,即便该脚本所对应的类的实例及对其引用并不存在,静态成员变量依旧会阻止脚本卸载。在静态成员变量存储海量数据,还含有对其他对象的引用(比如场景)的情况下,更需要引起格外重视。你可以手动清理掉这些数据,亦或是使用 @static_unload 注解,让静态成员变量在不存储重要数据时得到重置。

警告

目前由于某个漏洞导致含静态成员变量的脚本实例即使使用了 @static_unload 注解也无法被清除的问题。

注意: @static_unload 注解修饰整个脚本(包括内部类),需置于脚本最开头,且位于 class_nameextends 关键字之前:

@static_unload
class_name MyNode
extends Node

亦可见 Static functionsStatic constructor

类型转换

赋予给指定了类型的变量的值必须具有与其类型相兼容的类型。若需要将值强制转换为特定类型,特别是对于对象类型而言要进行转型,则可以使用强制转型运算符 as

如果值是对象类型,且为与目标类型相同的类型,亦或为目标类型的子类型,则进行转型后会得到同一个对象。

var my_node2D: Node2D
my_node2D = $Sprite2D as Node2D # Works since Sprite2D is a subtype of Node2D.

如果该值的类型不是目标类型的子类型,则强制转型操作将产生 null 值。

var my_node2D: Node2D
my_node2D = $Button as Node2D # Results in 'null' since a Button is not a subtype of Node2D.

对于内置类型,如果允许,则将对其进行强制转型,否则将触发报错。

var my_int: int
my_int = "123" as int # The string can be converted to int.
my_int = Vector2() as int # A Vector2 can't be converted to int, this will cause an error.

与场景树进行交互时,强制转型对于获取节点也更加类型安全,十分有用:

# Will infer the variable to be of type Sprite2D.
var my_sprite := $Character as Sprite2D

# Will fail if $AnimPlayer is not an AnimationPlayer, even if it has the method 'play()'.
($AnimPlayer as AnimationPlayer).play("walk")

常量

常量是游戏运行时不可更改的量,其值在编译时必须已知,可使用 const 关键字为常量值赋予名称。尝试为常量重新赋值将会触发报错。

建议使用常量来储存不应更改的值。

const A = 5
const B = Vector2(20, 20)
const C = 10 + 20 # Constant expression.
const D = Vector2(20, 30).x # Constant expression: 20.
const E = [1, 2, 3, 4][0] # Constant expression: 1.
const F = sin(20) # 'sin()' can be used in constant expressions.
const G = x + 20 # Invalid; this is not a constant expression!
const H = A + 20 # Constant expression: 25 (`A` is a constant).

常量的类型虽然可以从赋予的值中推断出来,但也可以通过显式添加类型来指定:

const A: int = 5
const B: Vector2 = Vector2()

赋予与指定的类型不相容的值将触发报错。

也可以在函数内使用常量来声明一些局部魔法值。

备注

由于数组和字典是通过引用进行传递的,因此常量数组、字典是“浅引用”。也就是说,如果你声明了一个常量数组或常量字典,那么你依然可以增删该数组或该字典内部元素,但你不能给该常量重新赋予新的数组或字典。

枚举

枚举基本上是一组常量的浓缩,为某些常量连续分配整数时非常有用。

enum {TILE_BRICK, TILE_FLOOR, TILE_SPIKE, TILE_TELEPORT}

# Is the same as:
const TILE_BRICK = 0
const TILE_FLOOR = 1
const TILE_SPIKE = 2
const TILE_TELEPORT = 3

如果将名称传递给枚举,则该枚举将会把所有键纳入该名称的 Dictionary 中。也就是说,字典中的所有常方法均可用于具名枚举当中。

重要

从 Godot 3.1 开始,不会再将具名枚举的键注册为全局常量,此后,应在枚举常量前缀以枚举名的形式来访问枚举内的枚举常量( Name.KEY );见后面的例子。

enum State {STATE_IDLE, STATE_JUMP = 5, STATE_SHOOT}

# Is the same as:
const State = {STATE_IDLE = 0, STATE_JUMP = 5, STATE_SHOOT = 6}
# Access values with State.STATE_IDLE, etc.

func _ready():
    # Access values with Name.KEY, prints '5'
    print(State.STATE_JUMP)
    # Use dictionary methods:
    # prints '["STATE_IDLE", "STATE_JUMP", "STATE_SHOOT"]'
    print(State.keys())
    # prints '{ "STATE_IDLE": 0, "STATE_JUMP": 5, "STATE_SHOOT": 6 }'
    print(State)
    # prints '[0, 5, 6]'
    print(State.values())

函数

函数始终属于 。查找变量时,函数作用域的查找顺序是:局部 → 类成员 → 全局。引擎始终允许用 self 作为访问本类及本类成员的关键字,但该关键字在一般情况下并无添加的必要(与 Python 不同, 应将其作为类内函数首选的参数传递)。

func my_function(a, b):
    print(a)
    print(b)
    return a + b  # Return is optional; without it 'null' is returned.

函数可以在任何时候用 return 返回值,默认的返回值为 null

若函数体只含一行语句,则可以将函数及其函数体缩在同一行语句内编写:

func square(a): return a * a

func hello_world(): print("Hello World")

func empty_function(): pass

函数也可带有参数,也可声明返回值的类型。可以使用与声明变量类似的方式添加参数的类型:

func my_function(a: int, b: String):
    pass

如果函数参数具有默认值,则可以对该参数的类型进行推断:

func my_function(int_arg := 42, String_arg := "string"):
    pass

可以在参数列表之后使用箭头标记(->)来指定函数的返回值类型:

func my_int_function() -> int:
    return 0

有返回类型的函数 必须 返回与返回值类型相匹配的值。若将返回值类型设置为 void ,则该函数不返回任何内容。无返回值函数可以使用 return 关键字提前返回,但不能返回任何值。

func void_function() -> void:
    return # Can't return a value.

备注

非 void 函数 必须 返回一个值,如果你的代码具有分支语句(例如 if/else 构造),则所有可能的路径都必须有返回值。例如,如果在 if 块内有一个 return,但在其后没有,则编辑器将抛出一个错误,因为如果该块未执行,则该函数将没有有效的值返回。

引用函数

Callable 对象而言,函数是其第一类对象。通过函数名称来引用一个函数,会自动生成指向该函数的可调用体,而非调用该函数。这种操作可用于将函数作为参数传递。

func map(arr: Array, function: Callable) -> Array:
    var result = []
    for item in arr:
        result.push_back(function.call(item))
    return result

func add1(value: int) -> int:
    return value + 1;

func _ready() -> void:
    var my_array = [1, 2, 3]
    var plus_one = map(my_array, add1)
    print(plus_one) # Prints [2, 3, 4].

备注

可调用体 必须 使用 call 方法进行调用,不能直接使用 () 运算符,以避免直接调用可调用体所指向的函数而造成的性能问题。

Lambda 函数(匿名函数)

Lambda 函数允许声明不属于类的函数,直接创建 Callable 对象并将其赋值给变量。该操作对于创建可传递的可调用体而不污染该类的作用范围非常有用。

var lambda = func(x): print(x)
lambda.call(42) # Prints "42"

Lambda 函数可用于代码调试:

var lambda = func my_lambda(x):
    print(x)

Note that if you want to return a value from a lambda, an explicit return is required (you can't omit return):

var lambda = func(x): return x ** 2
print(lambda.call(2)) # Prints `4`.

Lambda 函数的函数体会自动捕获局部函数所在的函数体作用域,而局部变量是按值传递的,因此,这些局部变量即便在其所在的函数体作用域内发生更改,也不会影响其在 Lambda 函数中的效果:

var x = 42
var my_lambda = func(): print(x)
my_lambda.call() # Prints "42"
x = "Hello"
my_lambda.call() # Prints "42"

备注

作用在 Lambda 函数体之外的变量值对该 Lambda 函数而言就像常量一样,因此,如果你给变量声明了一个数组类型或字典类型的值,则在 Lambda 函数的声明后仍可修改这些值。

静态函数

函数可以声明为静态函数。静态函数不能访问实例成员变量,也不能使用 self,非常适用于创建辅助函数库:

static func sum2(a, b):
    return a + b

Lambda 函数不可声明为静态函数。

Static variablesStatic constructor

语句与流程控制

标准语句可以是赋值、函数调用以及流程控制结构等(见下方)。 ; 为语句分隔符,在使用时完全可选。

表达式

表达式是运算符和操作数的有序排列,尽管只有调用时才可以用作语句使用,只要其他表达式没有副作用,其自身也可作为一条语句使用。

表达式返回的数值可赋值给有效目标,而某些运算符的操作数也可以变成一条表达式。赋值语句因无返回值而不能作为表达式使用。

以下是一些表达式的示例:

2 + 2 # Binary operation.
-5 # Unary operation.
"okay" if x > 4 else "not okay" # Ternary operation.
x # Identifier representing variable or constant.
x.a # Attribute access.
x[4] # Subscript access.
x > 2 or x < 5 # Comparisons and logic operators.
x == y + 2 # Equality test.
do_something() # Function call.
[1, 2, 3] # Array definition.
{A = 1, B = 2} # Dictionary definition.
preload("res://icon.png") # Preload builtin function.
self # Reference to current instance.

标识符、对象属性和下标均可视为表达式有效的赋值目标,而在赋值语句中,表达式不能位于赋值等号左侧。

if/else/elif

条件句通过使用 if/else/elif 语法创建。条件中的括号可写可不写。考虑到基于制表符缩进的性质,可以使用 elif 而非 else/if 来保持缩进级别相同。

if (expression):
    statement(s)
elif (expression):
    statement(s)
else:
    statement(s)

短的语句可以与条件句写在同一行内:

if 1 + 1 == 2: return 2 + 2
else:
    var x = 3 + 3
    return x

有时你可能希望基于布尔表达式来赋予不同的初始值,为此,三元表达式将派上用场:

var x = (value) if (expression) else (value)
y += 3 if y < 10 else -1

可以通过嵌套三元 if 表达式来处理的超过两种可能性的情况。嵌套时,推荐把三元 if 表达式拆分为多行进行表达以保证代码的可读性:

var count = 0

var fruit = (
        "apple" if count == 2
        else "pear" if count == 1
        else "banana" if count == 0
        else "orange"
)
print(fruit)  # banana

# Alternative syntax with backslashes instead of parentheses (for multi-line expressions).
# Less lines required, but harder to refactor.
var fruit_alt = \
        "apple" if count == 2 \
        else "pear" if count == 1 \
        else "banana" if count == 0 \
        else "orange"
print(fruit_alt)  # banana

你可能还想要检查某个值是否包含在某些容器之中,可以通过 if 语句与 in 操作符组合来实现:

# Check if a letter is in a string.
var text = "abc"
if 'b' in text: print("The string contains b")

# Check if a variable is contained within a node.
if "varName" in get_parent(): print("varName is defined in parent!")

while

一般的循环通过 while 语法创建,可以使用 break 来跳出整个循环,或者使用 continue 来跳出当前批次的循环并进入下一轮的循环当中(但会将该关键字下方所有在该循环体内的语句全部跳过):

while (expression):
    statement(s)

for

要迭代一个范围,例如数组或表,请使用 for 循环。迭代数组时,当前数组元素被存储在循环变量中。迭代字典时, 被存储在循环变量中。

for x in [5, 7, 11]:
    statement # Loop iterates 3 times with 'x' as 5, then 7 and finally 11.

var dict = {"a": 0, "b": 1, "c": 2}
for i in dict:
    print(dict[i]) # Prints 0, then 1, then 2.

for i in range(3):
    statement # Similar to [0, 1, 2] but does not allocate an array.

for i in range(1, 3):
    statement # Similar to [1, 2] but does not allocate an array.

for i in range(2, 8, 2):
    statement # Similar to [2, 4, 6] but does not allocate an array.

for i in range(8, 2, -2):
    statement # Similar to [8, 6, 4] but does not allocate an array.

for c in "Hello":
    print(c) # Iterate through all characters in a String, print every letter on new line.

for i in 3:
    statement # Similar to range(3).

for i in 2.2:
    statement # Similar to range(ceil(2.2)).

若需要在数组迭代时对数组进行赋值操作,则推荐使用 for i in array.size() 来进行该操作。

for i in array.size():
        array[i] = "Hello World"

循环变量只属于该循环,为其赋值并不会更改数组的值。如果循环变量是通过引用传递的对象(如节点),则仍可通过调用其方法来操作所指向的对象。

for string in string_array:
    string = "Hello World" # This has no effect

for node in node_array:
    node.add_to_group("Cool_Group") # This has an effect

match

match 语句用于分支流程的执行,相当于在许多其他语言中出现的 switch 语句,但提供了一些附加功能。

警告

match 对类型的要求比 == 运算符更严格。例如 11.0不匹配的。唯一的例外是 StringStringName 的匹配:例如会认为字符串 "hello" 和 StringName &"hello" 相等。

基本语法
match <expression>:
    <pattern(s)>:
        <block>
    <pattern(s)> when <guard expression>:
        <block>
    <...>
给熟悉 switch 语句的人提供的速成课程
  1. switch 替换为 match

  2. 删除 case

  3. 删除 break

  4. default 替换为单个下划线。

流程控制

将值按从上到下的顺序为每个分支条件进行匹配,如果有一个分支条件匹配,则会执行第一个与之相应的分支条件,之后继续执行 match 语句下不含该分支条件的其他分支。

备注

3.x 版本中, continuematch 语句中起着特殊作用,而在4.0版本中则移除了这一特殊作用。

可以使用以下模式类型:

  • 字面量模式

    匹配字面量

    match x:
        1:
            print("We are number one!")
        2:
            print("Two are better than one!")
        "test":
            print("Oh snap! It's a string!")
    
  • 表达式模式

    匹配表达式常量、标识符、或属性访问(A.B):

    match typeof(x):
        TYPE_FLOAT:
            print("float")
        TYPE_STRING:
            print("text")
        TYPE_ARRAY:
            print("array")
    
  • 通配符模式

    匹配所有内容,用一个下划线来表示通配内容。

    可以与其他语言的 switch 语句中的 default 等效:

    match x:
        1:
            print("It's one!")
        2:
            print("It's one times two!")
        _:
            print("It's not 1 or 2. I don't care to be honest.")
    
  • 绑定模式

    绑定模式引入一个新的变量,与通配符模式类似匹配所有通配内容,并为该通配值提供一个名称,在数组和字典模式中特别有用:

    match x:
        1:
            print("It's one!")
        2:
            print("It's one times two!")
        var new_var:
            print("It's not 1 or 2, it's ", new_var)
    
  • 数组模式

    匹配一个数组,数组模式的每个元素本身都可以是一个模式,因此可以对其进行嵌套。

    首先检测数组的长度,其长度必须与语句块条件的数组长度相同,否则不匹配。

    开放式数组 : 通过使最后一个子模式为 .. , 可以使被比较数组的长度超过语句块条件的数组的长度.

    每个子模式都必须用逗号分隔开来。

    match x:
        []:
            print("Empty array")
        [1, 3, "test", null]:
            print("Very specific array")
        [var start, _, "test"]:
            print("First element is ", start, ", and the last is \"test\"")
        [42, ..]:
            print("Open ended array")
    
  • 字典模式

    作用方式同数组模式,且每个键必须为一个常量模式。

    首先检测字典的大小,其大小必须与语句块条件的字典大小相同,否则不匹配。

    开放式字典 : 通过将最后一个子字样改为 .. ,使被比较字典可以比语句块条件的字典更大。

    每个子模式都必须用逗号分隔开。

    若不指定键的值,则仅检查键的存在。

    值模式与键模式之间以 : 分隔。

    match x:
        {}:
            print("Empty dict")
        {"name": "Dennis"}:
            print("The name is Dennis")
        {"name": "Dennis", "age": var age}:
            print("Dennis is ", age, " years old.")
        {"name", "age"}:
            print("Has a name and an age, but it's not Dennis :(")
        {"key": "godotisawesome", ..}:
            print("I only checked for one entry and ignored the rest")
    
  • 多重模式

    你还可以用逗号来分隔同一语句块条件里的多个模式,这些模式不允许包含任何绑定。

    match x:
        1, 2, 3:
            print("It's 1 - 3")
        "Sword", "Splash potion", "Fist":
            print("Yep, you've taken damage")
    
模式保护

对含相同分支条件的多个分支而言,每个 match 结构只能执行其中一条分支,一旦有分支通过匹配,那么剩下的相同条件的分支都将不再执行。如果需要执行多条含相同条件的分支,而避免只执行其中一条的话,可以使用模式保护语句,用 when 关键字引导:

match point:
    [0, 0]:
        print("Origin")
    [_, 0]:
        print("Point on X-axis")
    [0, _]:
        print("Point on Y-axis")
    [var x, var y] when y == x:
        print("Point on line y = x")
    [var x, var y] when y == -x:
        print("Point on line y = -x")
    [var x, var y]:
        print("Point (%s, %s)" % [x, y])
  • 若在当前同条件多分支句中找不到匹配的模式,则模式保护语法不会对该同条件多分支句起效,而是转到下一个(同条件多)分支句进行模式匹配。

  • 若当前同条件多分支句的模式与给定值匹配,则执行多分支句里的模式保护语句。

    • 若保护条件结果为真,则执行该分支下的语句并跳出 match 结构的执行。

    • 若保护条件不为真,则对下一个同条件分支句的模式进行检测。

默认情况下,所有脚本文件都是未命名的类,这时只能使用文件的路径来引用这些无名类(相对路径或绝对路径)。如果你将脚本文件命名为 character.gd的话:

# Inherit from 'character.gd'.

extends "res://path/to/character.gd"

# Load character.gd and create a new node instance from it.

var Character = load("res://path/to/character.gd")
var character_node = Character.new()

注册具名类

你也可以使用 class_name 关键字来为你的类起名,将其注册为 Godot 编辑器中的新类型。你还可以配合使用 @icon 注解,向其括号中输入图片的路径,来将该图片作为该类的图标使用。这样,你的类就会和新的图标一起显示在编辑器中:

# item.gd

@icon("res://interface/icons/item.png")
class_name Item
extends Node
../../../_images/class_name_editor_register_example.png

小技巧

SVG images that are used as custom node icons should have the Editor > Scale With Editor Scale and Editor > Convert Icons With Editor Theme import options enabled. This allows icons to follow the editor's scale and theming settings if the icons are designed with the same color palette as Godot's own icons.

这是一个类文件示例:

# Saved as a file named 'character.gd'.

class_name Character


var health = 5


func print_health():
    print(health)


func print_this_script_three_times():
    print(get_script())
    print(ResourceLoader.load("res://character.gd"))
    print(Character)

如果想要在声明类的同时让这个类继承自某个类,则可以将这两个关键字写在同一行内:

class_name MyNode extends Node

备注

由于脚本可以在用户不知情的情况下在单独的线程中初始化,出于线程安全考虑,Godot 在每次创建实例时,引擎都会初始化非静态变量,其中就包括数组和字典。

继承

类(以文件形式保存)可以继承自:

  • 全局类。

  • 另一个类文件。

  • 另一个类文件中的内部类。

不允许多重继承。

继承使用 extends 关键字:

# Inherit/extend a globally available class.
extends SomeClass

# Inherit/extend a named class file.
extends "somefile.gd"

# Inherit/extend an inner class in another file.
extends "somefile.gd".SomeInnerClass

备注

如果没有显式指定继承的类,则默认该类继承自 RefCounted

要检查给定的实例是否继承自给定的类,可以使用 is 关键字:

# Cache the enemy class.
const Enemy = preload("enemy.gd")

# [...]

# Use 'is' to check inheritance.
if entity is Enemy:
    entity.apply_damage()

要调用 基类 (即当前类的 extends 关键字后的类)中的函数,请使用 super 关键字:

super(args)

由于子类中的函数会替换基类中同名的函数,因此若仍然想调用在基类中的该函数,则可以使用 super 关键字:

func some_func(x):
    super(x) # Calls the same function on the super class.

若需要调用父类方法,可在 super 关键字后用英文句点连接父节点方法名(带括号):

func overriding():
    return 0 # This overrides the method in the base class.

func dont_override():
    return super.overriding() # This calls the method as defined in the base class.

警告

目前普遍有人想试图覆盖引擎内置的 非虚 方法,如 get_class()queue_free() 等。出于技术性原因,暂不支持这种操作,理由如下。

Godot 3 里的 GDScript 允许你 隐藏 引擎方法,而这些被隐藏后重新定义的方法却可以被其他 GDScript 脚本所调用,倘若该方法在引擎内部执行,那么引擎并不会执行你所重新定义的方法。

Godot 4 对 GDScript 内置方法调用机制进行了优化,隐藏方法再重新定义一个同名方法这个招数就再也不管用了。鉴于此,我们增添了 NATIVE_METHOD_OVERRIDE 警告选项,默认设置为一种抛错。我们强烈建议保持该选项开启,不要作为警告而忽略之。

不过需要注意:对于虚方法,比如 _ready(), _process() 以及其他虚方法(在文档中被标为 virtual 且以下划线开头的内置方法),则会进行覆盖操作。虚方法是专门用于自定义引擎行为的方法,可被 GDScript 所覆盖。信号、通知也可用于自定义引擎行为。

类的构造函数

类的构造函数在类进行初始化时调用,在 GDScript 中构造函数为虚函数 _init 。若想要在构造函数中调用父类构造函数,同样可以使用 super 语法。需要注意:每个类都有一个隐式构造函数,总是由引擎调用,用于定义类变量的默认值,而 super 则用于调用显式构造函数:

func _init(arg):
   super("some_default", arg) # Call the custom base constructor.

通过示例可以更好地说明这一点。考虑一下这种情况:

# state.gd (inherited class).
var entity = null
var message = null


func _init(e=null):
    entity = e


func enter(m):
    message = m


# idle.gd (inheriting class).
extends "state.gd"


func _init(e=null, m=null):
    super(e)
    # Do something with 'e'.
    message = m

这里有几点需要牢记:

  1. 如果被继承的类(State.gd)定义了一个带有参数(此处的 e)的 _init 构造函数,那么继承的类(Idle.gd)也必须定义 _init ,并且要将适当的参数从 State.gd 传递给 _init

  2. Idle.gd 的构造函数的参数数量可以与基类 State.gd 的构造函数的参数数量有所不同。

  3. 在上面的示例中,传递给 State.gd 构造函数的 e 与传递给 Idle.gde 是相同的。

  4. 如果 Idle.gd_init 构造函数接受 0 个参数,该构造函数即使什么也不做,也仍然需要将一些值传递给 State.gd 父类。当然,我们除了可以给基类构造函数传变量之外,也可以传表达式,例如:

    # idle.gd
    
    func _init():
        super(5)
    

静态构造函数

静态构造函数用虚函数 _static_init 表示,该函数会在类被载入时,静态类成员变量初始化后自动调用:

static var my_static_var = 1

static func _static_init():
    my_static_var = 2

静态构造函数不能含有任何参数,不能返回值。

内部类

类文件可以包含内部类。内部类使用 class 关键字定义,用 类名.new() 函数来进行实例化。

# Inside a class file.

# An inner class in this class file.
class SomeInnerClass:
    var a = 5


    func print_value_of_a():
        print(a)


# This is the constructor of the class file's main class.
func _init():
    var c = SomeInnerClass.new()
    c.print_value_of_a()

类作为资源

存储为文件的类将会视为 Resource,必须从磁盘加载这些文件之后才能在其他类中访问它们,可以通过调用 loadpreload 函数来完成(后述)。一个加载的类资源通过调用类对象上的 new 函数来完成实例化:

# Load the class resource when calling load().
var MyClass = load("myclass.gd")

# Preload the class only once at compile time.
const MyClass = preload("myclass.gd")


func _init():
    var a = MyClass.new()
    a.some_function()

导出

备注

有关导出的文档已移至 GDScript 导出属性

属性(setter 函数与 getter 函数)

有时,你可能不止希望对类成员进行数据存储操作,甚至想要在更改成员值的时候对其进行有效性检查操作或运算操作。你也可能希望以某种方式对该类成员的访问进行封装。

鉴于此, GDScript 提供了一套特别的语法,通过在变量定义后使用 setget 关键字来对类成员属性的读写进行封装。这样一来,你就可以在 set (setter 函数)、 get (getter 函数)语句块里定义代码,在该成员被读写时执行之。

示例(以匿名 setter/getter 函数为例):

var milliseconds: int = 0
var seconds: int:
    get:
        return milliseconds / 1000
    set(value):
        milliseconds = value * 1000

备注

与之前的 Godot 版本中的 setget 不同,即使在同一个类中进行访问(不管有无前缀 self.),属性的 setter 和 getter 始终 会被调用(除非代码下文指出) ,以让 setter 行为一致、getter 行为也一致。如果你需要直接访问该值,请使用另一个变量进行直接访问,并在其属性代码中使用该被访问变量的变量名。

替代方案(具名 setter/getter 函数)

若想从变量声明中分离 setter/getter 代码,亦或想在多个属性中共享这些代码,则可以借助现有的类函数来完成该操作:

var my_prop:
    get = get_my_prop, set = set_my_prop

也可以将这个写法缩在同一行内写:

var my_prop: get = get_my_prop, set = set_my_prop

Setter 函数和 Getter 函数在给一个变量定义时必须使用相同的定义格式,不允许混合使用这两种定义格式。

备注

不允许对 匿名setter 函数和 getter 函数进行类型指定,以减少代码的重复抄写量。若变量含有指定的类型,则其 setter 函数的参数会自动转换到相同的类型,同时其 getter 函数的返回值类型也必须与该类型相配。具名 setter/getter 函数允许指定类型提示,但这些函数的设值/返回类型必须与该属性的类型或该类型的广义类型相配。

Setter/getter 函数不会被调用的情况

变量在进行初始化时,其初始值会直接赋予给该变量,包括 @onready 注解所修饰的变量也是如此。

在一个变量的 setter 函数和 getter 函数内访问该变量的变量名,会直接访问该变量所代表的成员属性,不会导致 setter 函数和 getter 函数被无限次迭代调用,同时避免了显式声明另一个变量:

signal changed(new_value)
var warns_when_changed = "some value":
    get:
        return warns_when_changed
    set(value):
        changed.emit(value)
        warns_when_changed = value

这种情况也同样适用于替代方案:

var my_prop: set = set_my_prop

func set_my_prop(value):
    my_prop = value # No infinite recursion.

警告

在匿名 setter/getter 函数中调用具名 setter/getter 函数会导致无限递归调用,如下面的这个情况:

var my_prop:
    set(value):
        set_my_prop(value)

func set_my_prop(value):
    my_prop = value # Infinite recursion, since `set_my_prop()` is not the setter.

工具模式

默认情况下,脚本不会在编辑器内运行,只有更改导出的属性这一操作会在编辑器内运行。在某些情况下,我们确实希望这些代码能在编辑器中运行(只要这些代码不执行游戏逻辑,也可以手动避免之)。为此可以用 @tool 注解,必须将其写在文件的顶部:

@tool
extends Button

func _ready():
    print("Hello")

详情见 在编辑器中运行代码

警告

由于工具脚本是在编辑器中运行代码的,故在工具脚本中使用 queue_free()free() 释放节点时需要谨慎(尤其是对脚本所有者本身使用的时候更是如此)。对工具脚本滥用释放节点代码可能会导致编辑器崩溃。

内存管理

Godot 通过实现引用计数来释放某些不再使用的实例,而非通过垃圾收集器(GC),或者需要纯手动管理内存释放来实现这一操作。RefCounted 类(或继承该类的任何类,例如 Resource)的任何实例在不再使用时将自动释放。对于非 RefCounted 类(例如 Node 或基本 Object 类型)的实例,这些实例将保留在内存中,直到使用 free() (或用于节点的 queue_free())才会从内存中删除。

备注

如果通过 free()queue_free() 删除 Node,则它的所有子节点也将会被递归删除。

为了避免造成无法释放的循环引用,Godot 提供了用于创建弱引用的 WeakRef 类,可以访问到对象,但是不会阻止 RefCounted 的释放。见下例:

extends Node

var my_file_ref

func _ready():
    var f = FileAccess.open("user://example_file.json", FileAccess.READ)
    my_file_ref = weakref(f)
    # the FileAccess class inherits RefCounted, so it will be freed when not in use

    # the WeakRef will not prevent f from being freed when other_node is finished
    other_node.use_file(f)

func _this_is_called_later():
    var my_file = my_file_ref.get_ref()
    if my_file:
        my_file.close()

在没有使用引用的情况下,也可以用 is_instance_valid(instance) 来检查对象是否已被释放。

信号

信号是从对象中发出消息的工具,其他对象可以对该信号做出反应。要为一个类创建自定义信号,请使用 signal 关键字。

extends Node


# A signal named health_depleted.
signal health_depleted

备注

信号是一种回调机制,同时还充当观察者的角色,这是一种常见的编程模式。有关更多信息,请阅读《游戏编程模式》电子书中的观察者教程

你可以将这些信号连接到方法,就像连接 ButtonRigidBody3D 等节点的内置信号一样。

在下面的示例中,我们将 Character 节点的 health_depleted 信号连接到 Game 节点上。当 Character 节点发出信号时,Game 节点的 _on_character_health_depleted 就会被调用:

# game.gd

func _ready():
    var character_node = get_node('Character')
    character_node.health_depleted.connect(_on_character_health_depleted)


func _on_character_health_depleted():
    get_tree().reload_current_scene()

可以在发出一个信号时给该信号附带任意数量的参数。

下面这个示例就是该特性的一个不错的实现。假设我们希望屏幕上的生命条能够通过动画对生命值做出反应,但我们希望在场景树中让用户界面与游戏角色保持独立。

在我们的 character.gd 脚本中,我们定义了一个 health_changed 信号并使用 Signal.emit() 发出它,并从我们场景树中更高的 Game 节点发出,我们使用 Signal.connect() 方法将其连接到 Lifebar

# character.gd

...
signal health_changed


func take_damage(amount):
    var old_health = health
    health -= amount

    # We emit the health_changed signal every time the
    # character takes damage.
    health_changed.emit(old_health, health)
...
# lifebar.gd

# Here, we define a function to use as a callback when the
# character's health_changed signal is emitted.

...
func _on_Character_health_changed(old_value, new_value):
    if old_value > new_value:
        progress_bar.modulate = Color.RED
    else:
        progress_bar.modulate = Color.GREEN

    # Imagine that `animate` is a user-defined function that animates the
    # bar filling up or emptying itself.
    progress_bar.animate(old_value, new_value)
...

Game 节点中,我们同时获得 CharacterLifebar 节点,然后将发出信号的 Character 连接到接收者节点上,在本例中 Lifebar 为这一接收者节点。

# game.gd

func _ready():
    var character_node = get_node('Character')
    var lifebar_node = get_node('UserInterface/Lifebar')

    character_node.health_changed.connect(lifebar_node._on_Character_health_changed)

这样 Lifebar 就能够对生命值的变化做出反应,无需将其耦合到 Character 节点内。

可以在信号的定义后面添加括号,并在该括号内写入可选的参数名称:

# Defining a signal that forwards two arguments.
signal health_changed(old_value, new_value)

这些参数会显示在编辑器的节点面板中,Godot 会在生成回调函数时自动为你添加这些参数。但是,在发出信号时仍然可以发出任意数量的参数,需要由你来确定该信号需要准确发出的值。

../../../_images/gdscript_basics_signals_node_tab_1.png

GDScript 可以将一组值绑定到信号和方法之间的连接之上。发出信号时,回调方法将会接收这组绑定值。这些绑定参数对于每个连接都是唯一的,且其值均保持不变。

若发出的信号本身不能让你访问所需的所有数据,则可以使用这组数值将额外的常量信息添加到连接当中。

接着上面的示例,我们要在屏幕上显示每个角色受到的伤害,例如 Player1 遭受了 22 伤害。。然而 health_changed 信号并没有给我们提供受到伤害的角色的名称。因此,在我们将信号连接到游戏终端上时,可以在绑定参数这组数据中添加该角色的名称:

# game.gd

func _ready():
    var character_node = get_node('Character')
    var battle_log_node = get_node('UserInterface/BattleLog')

    character_node.health_changed.connect(battle_log_node._on_Character_health_changed, [character_node.name])

我们的 BattleLog 节点接收信号时,将绑定参数这个数组中的每个元素作为额外的参数传入被连接的函数当中:

# battle_log.gd

func _on_Character_health_changed(old_value, new_value, character_name):
    if not new_value <= old_value:
        return

    var damage = old_value - new_value
    label.text += character_name + " took " + str(damage) + " damage."

等待信号或协程函数

await 关键字可以用来创建协程,会等待某个信号发出之后再继续执行下面的代码。对信号或者对同为协程的函数调用使用 await 关键字会立即将控制权返回给调用方。发出信号时(或者调用的协程函数完成时),就会从停止的地方继续往下执行代码。

例如,要暂停代码执行,直到到用户按下某个按钮后才能继续往下执行剩余代码,你就可以这样写:

func wait_confirmation():
    print("Prompting user")
    await $Button.button_up # Waits for the button_up signal from Button node.
    print("User confirmed")
    return true

此时 wait_confirmation 就会变成协程函数,调用方也需要对它进行等待操作:

func request_confirmation():
    print("Will ask the user")
    var confirmed = await wait_confirmation()
    if confirmed:
        print("User confirmed")
    else:
        print("User cancelled")

需要注意:在请求协程函数的返回值时,不带 await 将会触发报错:

func wrong():
    var confirmed = wait_confirmation() # Will give an error.

如果你不需要结果,直接异步调用就可以了,既不会阻止代码的正常运行,也不会让当前的函数变成协程函数:

func okay():
    wait_confirmation()
    print("This will be printed immediately, before the user press the button.")

若对不是信号和协程函数的表达式使用 await,则会立即返回对应的值,函数也不会将控制权转交回调用方:

func no_wait():
    var x = await get_five()
    print("This doesn't make this function a coroutine.")

func get_five():
    return 5

也就是说,如果从非协程函数中返回信号,那么调用方就会等待那个信号:

func get_signal():
    return $Button.button_up

func wait_button():
    await get_signal()
    print("Button was pressed")

备注

与之前版本 Godot 中的 yield 不同,出于类型安全的考虑,现版本无法获取函数状态对象。实现了这种类型安全之后,就不能说函数在返回 int 的同时还可能在运行时返回函数状态对象了。

Assert 关键字

assert 关键字可用于在调试版本中检查断言条件,而在非调试版本中则会忽略掉这些断言,意味着在发布模式下导出的项目中断言语法不会评估作为参数传递的表达式。因此,断言 决不能 包含具有副作用的表达式,否则,脚本的行为将取决于该项目是否在调试版本中运行。

# Check that 'i' is 0. If 'i' is not 0, an assertion error will occur.
assert(i == 0)

在编辑器中运行项目时,如果发生断言错误,则会暂停该项目的运行。

你还可以传入自定义错误消息,这些消息会在断言失败时显示:

assert(enemy_power < 256, "Enemy is too powerful!")