GDScript 参考
GDScript 是一种面向对象的高级指令式渐进类型编程语言,专为 Godot 构建,以与 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 相关的详细内容,如哪些方法可用于数组或其他对象,可在相关链接所指向的类描述页面中查询。
标识符
标识符仅限于含字母字符( 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 | 通常情况下用来检测字符串、列表、范围、字典、节点中是否存在某个值,而和 |
as | 尝试将值转换为给定类型的值。 |
self | 引用当前类实例。 |
super | 解析父类作用域内的方法,参见 继承 。 |
signal | 定义信号。 |
func | 定义函数。 |
static | 将一个函数声明为静态函数,或将一个成员变量声明为静态成员变量。 |
const | 定义常量。 |
enum | 定义枚举。 |
var | 定义变量。 |
breakpoint | 用来设置脚本编辑器辅助调试断点的关键字。与在脚本编辑器每行最左侧点击红点所创建的断点不同, |
preload | 预加载一个类或变量,参见 类作为资源 。 |
await | 等待信号或协程完成,参见等待信号和协程。 |
yield | 以前的版本中用于协程,现保留为关键字,以便旧版本迁移至新版本。 |
assert | 断言条件,若断言失败则记录错误,在非调试版本中会忽略断言语法。参见 Assert 关键字。 |
void | 用于代表函数不返回任何值。 |
PI | PI(π)常数。 |
TAU | TAU(τ)常数。 |
INF | 无穷常量,用于比较和计算结果。 |
NAN | NAN(非数)常量,用作计算后不可能得到的结果。 |
运算符
下列为 GDScript 所支持的运算符及其运算优先级。所有二进制运算符均为 左结合运算符 ,其中就包括 **
,即 2 ** 2 ** 3
等价于 (2 ** 2) ** 3
。为避免运算歧义,请使用括号来处理该运算的优先级,如 2 ** (2 ** 3)
。
运算符 | 描述 |
---|---|
| 分组(优先级最高) 括号其实不是运算符,但是能够让你显式指定运算的优先级。 |
| 下标 |
| 属性引用 |
| 函数调用 |
| |
x is Node x is not Node | 类型检查 另见 is_instance_of() 函数。 |
| 幂(乘方) 将 |
| 按位取反 |
+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() 函数。==
和!=
运算符在有些情况下允许比较不同类型的值(例如,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 中的一类特殊标记,用来修饰脚本或脚本中的代码,影响 Godot 引擎或编辑器对该脚本或代码的所产生的效果。
每个注解均以 @
符号开头,加以注解名称而构成。有关注解的详细说明及其使用范例,请参考 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 的脚本编辑器中,一些特殊关键字会在注释中高亮显示以提醒用户:
关键提示 (标红):
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 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 警告系统。
初始化顺序
成员变量的初始化顺序如下:
变量根据其静态类型,取值为
null
(无类型变量和对象)或类型的默认值(int
为0
、bool
为false
等)。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.
- (Only for
所有非
@onready
成员变量均完成定义时调用_init()
方法。初始化场景和资源时,赋导出的值。
(Only for
Node
-derived classes)@onready
variables are initialized.(Only for
Node
-derived classes) If defined, the_ready()
method is called.
警告
复杂表达式也能够作为变量的初始化器,其中也包括函数调用。请确保初始化变量时变量的声明顺序正确,否则对应的值可能会被覆盖。例如:
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)
会在控制台中打印出:
{ "a": 1 }
{ "a": 1, "b": 2 }
{ }
解决这个问题只需将 _data
变量的定义移动到 a
的定义之前,或者移除空字典的赋值(={}
)。
静态变量
成员变量可以声明为静态成员变量:
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
类型转换
赋予给指定了类型的变量的值必须具有与其类型相兼容的类型。若需要将值强制转换为特定类型,特别是对于对象类型而言要进行转型,则可以使用强制转型运算符 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 不同,在 GDScript 中不应该将 self
作为函数的第一个参数传递)。
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
表示该函数不返回任何东西。这种函数称为 void 函数,可以使用 return
关键字提前返回,但不能返回任何值。
func void_function() -> void:
return # Can't return a value.
备注
非 void 函数 必须 返回一个值,如果你的代码具有分支语句(例如 if
/else
构造),则所有可能的路径都必须有返回值。例如,如果在 if
块内有一个 return
,但在其后没有,则编辑器将抛出一个错误,因为如果该代码块未执行,那么该函数将没有值进行有效返回。
引用函数
Functions are first-class values in terms of the Callable object. Referencing a function by name without calling it will automatically generate the proper callable. This can be used to pass functions as arguments.
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]`.
备注
Callables must be called with the call() method. You cannot use the ()
operator directly. This behavior is implemented to avoid performance issues on direct function calls.
Lambda 函数
Lambda functions allow you to declare functions that do not belong to a class. Instead, a Callable object is created and assigned to a variable directly. This can be useful to create callables to pass around without polluting the class scope.
var lambda = func (x):
print(x)
To call the created lambda you can use the call() method:
lambda.call(42) # Prints `42`.
Lambda functions can be named for debugging purposes (the name is displayed in the Debugger):
var lambda = func my_lambda(x):
print(x)
You can specify type hints for lambda functions in the same way as for regular ones:
var lambda := func (x: int) -> void:
print(x)
Note that if you want to return a value from a lambda function, an explicit return
is required (you can’t omit return
):
var lambda = func (x): return x ** 2
print(lambda.call(2)) # Prints `4`.
Lambda functions capture the local environment:
var x = 42
var lambda = func ():
print(x) # Prints `42`.
lambda.call()
警告
Local variables are captured by value once, when the lambda is created. So they won’t be updated in the lambda if reassigned in the outer function:
var x = 42
var lambda = func (): print(x)
lambda.call() # Prints `42`.
x = "Hello"
lambda.call() # Prints `42`.
Also, a lambda cannot reassign an outer local variable. After exiting the lambda, the variable will be unchanged, because the lambda capture implicitly shadows it:
var x = 42
var lambda = func ():
print(x) # Prints `42`.
x = "Hello" # Produces the `CONFUSABLE_CAPTURE_REASSIGNMENT` warning.
print(x) # Prints `Hello`.
lambda.call()
print(x) # Prints `42`.
However, if you use pass-by-reference data types (arrays, dictionaries, and objects), then the content changes are shared until you reassign the variable:
var a = []
var lambda = func ():
a.append(1)
print(a) # Prints `[1]`.
a = [2] # Produces the `CONFUSABLE_CAPTURE_REASSIGNMENT` warning.
print(a) # Prints `[2]`.
lambda.call()
print(a) # Prints `[1]`.
静态函数
函数可以声明为静态函数。静态函数不能访问实例成员变量,也不能使用 self
,非常适用于创建辅助函数库:
static func sum2(a, b):
return a + b
Lambda functions cannot be declared static.
语句与流程控制
标准的语句可以是赋值、函数调用以及流程控制结构等(见下方)。 ;
为语句分隔符,在使用时可写可略。
表达式
表达式是运算符和操作数的有序排列,尽管表达式本身可以构成一个语句,但仅函数调用才适合作为语句使用,因为其他类型的表达式通常不会产生副作用。
表达式返回的数值可赋值给有效目标,而某些运算符的操作数也可以变成一条表达式。赋值语句因无返回值而不能作为表达式使用。
以下是一些表达式的示例:
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
小技巧
SVG图片在用作自定义节点图标时,需要在该图片的 导入选项 中将 编辑器 > 依照编辑器比例缩放 与 编辑器 > 依照编辑器主题转换颜色 勾选,这样才能让有跟 Godot 图标色调相同的图标在编辑器中能够同步其缩放、同步其主题设置。
这是一个类文件示例:
# 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 所覆写。信号、通知也可用于自定义引擎行为。
类的构造函数
The class constructor, called on class instantiation, is named _init
. If you want to call the base class constructor, you can also use the super
syntax. Note that every class has an implicit constructor that is always called (defining the default values of class variables). super
is used to call the explicit constructor:
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
构造函数不接受任何参数,即便该构造函数即便什么也不做,也仍然需要将一些值传递给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()
类作为资源
存储为文件的类将会视为 GDScript <class_GDScript>`资源,且必须从磁盘加载这些文件之后才能在其他类中访问它们,可以通过调用 ``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 函数)语句块里定义代码,在该成员被读写时执行之。
示例(以匿名 setter/getter 函数为例):
var milliseconds: int = 0
var seconds: int:
get:
return milliseconds / 1000
set(value):
milliseconds = value * 1000
备注
Unlike setget
in previous Godot versions, set
and get
methods are always called (except as noted below), even when accessed inside the same class (with or without prefixing with self.
). This makes the behavior consistent. If you need direct access to the value, use another variable for direct access and make the property code use that name.
替代方案(具名 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
备注
信号是一种回调机制,同时还充当观察者的角色,这是一种常见的编程模式。有关更多信息,请阅读《游戏编程模式》电子书中的观察者教程。
你可以将这些信号连接到方法,就像连接 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.bind(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!")