GDScript 参考
GDScript 是一种高级面向对象的指令式编程语言,使用渐进类型,专为 Godot 构建。GDScript 的语法基于缩进,与 Python 等语言类似。它的目标是针对 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 更为详细的信息,如哪些方法可用于数组或其他对象,可以在链接的类描述中查找到。
标识符
任何仅限于字母字符( a
到 z
和 A
到 Z
)、 数字( 0
到 9
)和下划线 _
的字符串都可以作为标识符。此外,标识符不能以数字开头,并且区分大小写( foo
和 FOO
是两个不同的标识符)。
标识符现在也允许包含 UAX#31 所提供的部分 Unicode 字符,也就是说,你现在也可以将非英文字符作为标识符使用。然而, Unicode 字符中易与 ASCII 字符混淆的字符以及颜文字则是无法作为标识符使用的。
关键字
以下是该语言支持的关键字列表。由于关键字是保留字(记号),因此不能用作标识符。操作符(如 in
、not
、and
、or
)及下列内置类型的名称也是保留字。
想深入了解关键字的话,可在 GDScript 词法分析器中找到对于关键字的定义。
关键字 | 描述 |
---|---|
if | 见 if/else/elif 。 |
elif | 见 if/else/elif 。 |
else | 见 if/else/elif 。 |
for | 见 for 。 |
while | 见 while 。 |
match | 见 match 。 |
break | 退出当前 |
continue | 立即跳到 |
pass | 语法上要求在不希望执行代码的语句中使用,例如在空函数中使用。 |
return | 从函数当中返回一个值。 |
class | 定义一个内部类。见 内部类。 |
class_name | 将脚本定义为具有指定名称的全局可访问类。见 注册具名类。 |
extends | 定义当前类继承什么类。 |
is | 检测变量是否继承自给定的类,或检测该变量是否为给定的内置类型。 |
in | Tests whether a value is within a string, array, range, dictionary, or node. When used with |
as | 尝试将值转换为给定类型的值。 |
self | 引用当前类实例。 |
signal | 定义信号。 |
func | 定义函数。 |
static | 将一个函数声明为静态函数,或将一个成员变量声明为静态成员变量。 |
const | 定义常量。 |
enum | 定义枚举。 |
var | 定义变量。 |
breakpoint | 用来设置脚本编辑器辅助调试断点的关键字。与在脚本编辑器每行最左侧点击红点创建断点不同, |
preload | 预加载一个类或变量,参见 类作为资源 。 |
await | 等待信号或协程完成,参见等待信号和协程。 |
yield | 以前的版本中用于协程,现保留为关键字,方便迁移。 |
assert | 断言条件,如果失败则记录错误。在非调试版本中忽略掉断言语法。参见 Assert 关键字。 |
void | 用于代表函数不返回任何值。 |
PI | PI(π)常数。 |
TAU | TAU(τ)常数。 |
INF | 无穷常量,用于比较和计算结果。 |
NAN | NAN(非数)常量,用作计算后不可能得到的结果。 |
运算符
下面是支持的运算符列表及其优先级(越靠上运算优先级越高)。
运算符 | 描述 |
---|---|
| 分组(优先级最高) 括号其实不是运算符,但是能够让你显式指定运算的优先级。 |
| 下标 |
| 属性引用 |
| 函数调用 |
| |
| 类型检查 另见 is_instance_of() 函数。 |
| 幂(乘方) 将 注意:在 GDScript 中, |
| 按位取反 |
+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 in y x not in y | 包含检查
|
not x !x | 布尔“非”,以及不推荐使用的别名 |
x and y x && y | 布尔“与”,以及不推荐使用的别名 |
x or y x || y | 布尔“或”,以及不推荐使用的别名 |
| 三元运算符 if/else |
| |
x = y x += y x -= y x = y x /= y x **= y x %= y x &= y x |= y x ^= y x <<= y x >>= y | 赋值(优先级最低) 表达式中不能使用赋值运算符。 |
备注
一些运算符的运算机制可能会与你的预期有所不符:
若运算符
/
两端的数值均为 int,则进行整数除法而非浮点数除法。例如:5 /2 == 2
中2
为该算式的结果而非2.5
为结果。若希望进行浮点数运算,请将该运算符两端的其中一个数值的类型改为 float 。例如:直接使用浮点数(x / 2.0
)、转换类型(float(x) / y
)、乘以1.0
(x * 1.0 / y
)等。运算符
%
仅适用于整型数值的取余运算。对于小数的取余运算,请使用 fmod() 方法。对于负值,
%
运算符和fmod()
函数使用 截断 而不是向负无穷大舍入。这意味着余数有符号。如果你需要数学意义上的余数,请改用 posmod() 和 fposmod() 函数。运算符
**
是 左结合运算符 ,也就是说,2 ** 2 ** 3
这个运算等价于(2 ** 2) ** 3
。对此,请使用括号来处理该运算的优先级,如2 ** (2 ** 3)
。==
和!=
运算符有时允许你比较不同类型的值(例如,1 == 1.0
为真),但在其他情况下可能会产生一个运行时错误 。如果你不确定操作数的类型,可以安全地使用 is_same() 函数(但请注意,它对类型和引用更加严格)。要比较浮点数,请改用 is_equal_approx() 和 is_zero_approx() 函数。
字面量
Example(s) | 描述 |
| 空值 |
| 布尔值 |
| 十进制整数 |
| 十六进制整数 |
| 二进制整数 |
| 浮点数(实数) |
| 常规字符串 |
| 常规字符串(用三对引号括住) |
| 原始字符串 |
| 原始字符串(用三对引号括住) |
| |
|
也有两种长得像字面量,但实际上不是字面量的量:
示例 | 描述 |
|
|
|
|
整数和浮点数可以用 _
来分隔,使其更加易读。以下表示数字的方法都是有效的:
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.
常规字符串字面量 内可包含以下转义序列:
转义序列 | 转义为 |
| 换行(line feed,LF) |
| 水平制表符(tab) |
| 回车(carriage return,CR) |
| 警报(蜂鸣/响铃) |
| 退格键(Backspace) |
| 换页符(form feed,FF) |
| 垂直制表符(tab) |
| 双引号 |
| 单引号 |
| 反斜杠 |
| Unicode UTF-16 码位 |
| Unicode UTF-32 码位 |
有两种方法可以表示 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")
这样一来,节点和外部引用需要先在成员变量中声明,然后在 Node._ready()
中定义其具体值。这种操作比较麻烦,而且随着引用变量越来越多,要定义的引用数量也会越来越多,操作起来就会显得十分不便。为此,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 的脚本编辑器内,一些特殊关键字会在注释中高亮显示以提醒用户:
关键提示 (标红):
ALERT
、ATTENTION
、CAUTION
、CRITICAL
、DANGER
、SECURITY
警告提示 (标黄):
BUG
、DEPRECATED
、FIXME
、HACK
、TASK
、TBD
、TODO
、WARNING
一般提示 (标绿):
INFO
、NOTE
、NOTICE
、TEST
、TESTING
这些关键字均大小写敏感,故需要全大写以保证能被引擎所识别:
# 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
代码区块可以将大块代码组织成更容易理解的部分。但请注意:外部编辑器通常不支持该特性,因此即使不依赖代码区块,也要确保你的代码易于理解。
备注
单独的函数和被缩进的部分(例如 if
和 for
) 始终 可以在脚本编辑器中折叠,也就是说,应避免使用代码区块来包含单一函数或被缩进的部分,执意使用也并不会带来太多好处。代码区块在将多个元素分组在一起时效果最佳。
行间语句接续
在GDScript中,一行语句可以通过反斜杠( \
)来接续到下一行。将反斜杠加在语句行末尾可以衔接该语句行的代码与下一语句行的代码。例如:
var a = 1 + \
2
可以按以下方式对单个语句行进行多行接续:
var a = 1 + \
4 + \
10 + \
4
内置类型
内置类型是分配在栈上的,按值传递,在每次赋值或将其作为参数传递给函数时都会复制其值。例外:对象 Object
、数组 Array
、字典 Dictionary
以及密存数组(如 PackedByteArray
),这些类型的值是按引用进行传递的,其实例的值相互共享。数组、字典以及部分对象( Node
、 Resource
)均有一个 duplicate()
方法,允许对其具体值进行复制操作。
基本内置类型
GDScript 中的变量可以赋值为不同的内置类型。
null
null
是一个空数据类型,既不包含任何信息,也不能赋值为其他任何值。
bool
“boolean”(布尔)的缩写,只能包含 true
或 false
。
int
英文“integer”(整数)的缩写,存储整数(正整数和负整数)。存储的是 64 位值,等效于 C++ 中的 int64_t
。
float
使用浮点值存储实数,包括小数。存储的是 64 位值,等效于 C++ 中的 double
。注意:目前 Vector2
、Vector3
、PackedFloat32Array
等数据结构存储的是 32 位单精度 float
值。
String
Unicode 格式的字符序列。
StringName
不可变字符串,一个实例仅允许拥有一个名称。该类型的实例创建起来较慢,在多线程环境下可能会导致锁等待。不过,该类型的实例比较起来比字符串快,非常适合在字典中作为键名使用。
NodePath
节点或节点属性的预解析路径。它可以轻松地分配给字符串或从字符串分配。它们可用于与树交互以获取节点,亦或用于通过诸如 Tween来影响属性。
内置向量类型
Vector2
2D 向量类型,包含 x
和 y
两个字段,也可以像访问数组元素一样访问这两个字段。
Vector2i
同 Vector2,但其分量均为整型数值,在做2D网格中显示物品时非常实用。
Rect2
2D 矩形类型,包含两个向量字段: position
和 size
。还包含一个 end
字段,即 position + size
。
Vector3
3D 向量类型,包含 x
、 y
和 z
这三个字段,也可以像访问数组元素一样访问这些字段。
Vector3i
同 Vector3 ,但其分量均为整型数值,可用于为 3D 网格中的每个物品编制索引。
Transform2D
用于 2D 线性变换的3x2矩阵。
Plane
3D 平面类型的标准形式,包含一个向量字段 normal
以及一个 标量距离 d
。
Quaternion
四元数是一种用于表示 3D 旋转的数据类型,对于内插旋转十分有用。
AABB
轴对齐边界框(或 3D 边框),包含两个向量字段: position
和 size
. 还包含一个 end
字段, 即 position + size
.
Basis
3×3矩阵,用于 3D 旋转与缩放。其包含3个向量字段( x
, y
和 z
),且可以像3D向量一样按索引访问这些向量字段。
Transform3D
3D 线性变换,包含一个 Basis(基)字段 basis
和一个 Vector3 字段 origin
。
引擎内置类型
Color
颜色数据类型包含 r
、 g
、 b
、 a
四个字段,也可以用 h
、 s
、 v
这三个字段来分别访问色相、饱和度、明度。
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)
Array
( Array[Variant]
)则是例外,这样做可以保证用户使用的便捷性与与旧版本代码的兼容性。不过,非类型化的数组是不安全的。
密存数组
GDScript 数组在内存中通过线性分配以提高速度,但使用大型数组(包含数万个元素)时却可能会导致内存碎片。如果在意这个问题,可以使用特定类型的密存数组,这些数组只接受单个数据类型,避免了内存碎片的同时使用的内存也更少。然而这些特定类型的数组是原子数组,运行起来通常要比通用数组慢,因此建议将这些数组仅用于大型数据集当中:
PackedByteArray:字节(从 0 到 255 的整数)数组。
PackedInt32Array:32位整数数组。
PackedInt64Array:64位整数数组。
PackedFloat32Array:32位浮点数数组。
PackedFloat64Array:64位浮点数数组。
PackedStringArray:字符串数组。
PackedVector2Array:Vector2 类型的数组。
PackedVector3Array:Vector3 类型的数组。
PackedColorArray:Color 类型的数组。
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 order.
变量可进行类型指定。指定类型时,将强制该变量始终容纳与被指定类型相同类型的数据。试图分配与该类型不兼容的值将触发报错。
在变量声明中,在变量名后面使用 :
(冒号)+ 类型名 来指定类型。
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 警告系统。
静态成员变量
成员变量可以声明为静态成员变量:
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_name
和 extends
关键字之前:
@static_unload
class_name MyNode
extends Node
亦可见 Static functions 和 Static 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}
func _ready():
# Access values with Name.KEY, prints '5'
print(State.STATE_JUMP)
# Use constant dictionary functions
# prints '["STATE_IDLE", "STATE_JUMP", "STATE_SHOOT"]'
print(State.keys())
函数
函数始终属于 类 。查找变量时,函数作用域的查找顺序是:局部 → 类成员 → 全局。引擎始终允许用 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)
局部变量是通过值来进行传递的,而 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 variables 和 Static 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
对类型的要求比 ==
运算符更严格。例如 1
和 1.0
是不匹配的。唯一的例外是 String
和 StringName
的匹配:例如会认为字符串 "hello"
和 StringName &"hello"
相等。
基本语法
match <expression>:
<pattern(s)>:
<block>
<pattern(s)> when <guard expression>:
<block>
<...>
给熟悉 switch 语句的人提供的速成课程
将
switch
替换为match
。删除
case
。删除
break
。将
default
替换为单个下划线。
流程控制
将数值按从上到下的顺序为每个语句块条件进行匹配,如果有一个语句块条件匹配,则会执行第一个与之相应的语句块,之后继续执行 match
语句下面的内容。
备注
3.x 版本中, continue
在 match
语句中起着特殊作用,而在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
这是一个类文件示例:
# 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
这里有几点需要牢记:
如果被继承的类(
State.gd
)定义了一个带有参数(此处的e
)的_init
构造函数,那么继承的类(Idle.gd
)也必须定义_init
,并且要将适当的参数从State.gd
传递给_init
。Idle.gd
的构造函数的参数数量可以与基类State.gd
的构造函数的参数数量有所不同。在上面的示例中,传递给
State.gd
构造函数的e
与传递给Idle.gd
的e
是相同的。如果
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,必须从磁盘加载这些文件之后才能在其他类中访问它们,可以通过调用 load
或 preload
函数来完成(后述)。一个加载的类资源通过调用类对象上的 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 提供了一套特别的语法,通过在变量定义后使用 set
、 get
关键字来对类成员属性的读写进行封装。这样一来,你就可以在 set
(setter 函数)、 get
(getter 函数)语句块里定义代码,在该成员被读写时执行之。
示例:
var milliseconds: int = 0
var seconds: int:
get:
return milliseconds / 1000
set(value):
milliseconds = value * 1000
备注
与之前的 Godot 版本中的 setget
不同,属性 setter 和 getter 总是 被调用(除非下面指出),即使在同一个类中访问时(有或没有前缀 self.
) 。这使得行为一致。如果你需要直接访问该值,请使用另一个变量进行直接访问,并使属性代码使用那个名称。
替代方案
若想从变量声明中分离 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 函数在给一个变量定义时必须使用相同的定义格式,不允许混合使用这两种定义格式。
备注
You cannot specify type hints for inline setters and getters. This is done on purpose to reduce the boilerplate. If the variable is typed, then the setter’s argument is automatically of the same type, and the getter’s return value must match it. Separated setter/getter functions can have type hints, and the type must match the variable’s type or be a wider type.
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 通过实现引用计数来释放某些不再使用的实例,而不是通过垃圾收集器,或者需要纯手动管理内存释放。RefCounted 类(或继承它的任何类,例如 Resource)的任何实例在不再使用时将自动释放。对于不是 RefCounted 的类(例如 Node 或基本 Object 类型)的实例,它将保留在内存中,直到使用 free()
(或用于节点的 queue_free()
)删除它。
备注
如果通过 free()
或 queue_free()
删除 Node,则它的所有子节点也将被递归删除。
为了避免造成无法释放的循环引用,提供了用于创建弱引用的 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
备注
信号是一种回调机制,同时还充当观察者的角色,这是一种常见的编程模式。有关更多信息,请阅读《游戏编程模式》电子书中的观察者教程。
你可以将这些信号连接到方法,就像连接 Button 或 RigidBody3D 等节点的内置信号一样。
在下面的示例中,我们将 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
节点中,我们同时获得 Character
和 Lifebar
节点,然后将发出信号的 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 会在生成回调函数时自动为你添加这些参数。但是,在发出信号时仍然可以发出任意数量的参数,需要由你来确定该信号正确发出的值。
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
就会变成协程,调用方也需要对它进行 await:
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!")
© 版权所有 2014-present Juan Linietsky, Ariel Manzur and the Godot community (CC BY 3.0). Revision b1c660f7
.
Built with Sphinx using a theme provided by Read the Docs.