导出变量的功能在3.x版本中也是有的,但是4.0版本对其进行了语法上的改进。
导出变量在日常的游戏制作中提供节点的自定义参数化调节功能时非常有用,除此之外还用于自定义资源。
本文是笔者(Bilibili@巽星石)在4.0官方文档《GDScript的导出》一文基础上机翻和增补而来的。希望对大家学习4.0的语法有所帮助。
Godot 4.0提供了改进版本的导出变量写法,新的写法比原来3.X版本的更简单优雅,实际上导出变量这部分基本是换汤不换药,如果熟悉3.X版本的,那么4.0的也就是改换下语法而已。
4.0增加了一种新语法称为“annotation”,引擎内部现在的中文翻译叫“注解”。是一些以@
打头的特殊命令。新的导出变量相关的语法都属于“注解”形式,除了涉及导出变量之外,“注解”还包含各种其他用途的命令。
Godot4.0将有关导出在内的“注解”全部纳入到了内置文档的@GDScript
部分,和全局常量和函数放在一起,你可以在官方文档或内置文档的@GDScript
中找到它们的说明。
和3.X一样,导出变量修改后的值会被自动保存到场景文件(.tscn)或资源文件(.tres)中,用于二次打开时复原。你可以在检视器面板自由的修改它们,然后按一下Ctrl+S
,它们就自动随场景或资源文件保存了。
导出变量需要使用@export
语句来实现。基本上可以认为是在3.x的export
关键字之前加了个@
。
@export var number: int = 5
在上面的代码,保存后当前节点的检视器面板就会出现一个自定义的属性number,并且默认值为5
。
此时你就可以直接在检视器面板上修改这个自定义属性的值了。比如这里改为7
,可以看到只要与默认值不一样,属性输入框的前方就会出现一个环形箭头,点击它可以快速将值恢复为默认值。
修改完自定义属性后,按Ctrl+S
保存当前场景,它的值就会被保存到场景文件中了。
如果给导出变量赋默认值,GDScript将自动进行类型推断,并在检视器面板显示相应的属性编辑控件。
@export var number = 5 # 根据给定的默认值5,number将被推断为int类型
@export var number2 = 5.0 # 根据给定的默认值5.0,number2将被推断为float类型
@export var string1 = "你好,戈多4.0" # 根据给定的默认值"你好,戈多4.0",string1将被推断为String类型
也可只标明类型而不赋默认值。
@export var number: int # 没有给定默认值,则初始值为0
以上两种方式你只能选其一,要么赋初始值让GDScript自己去判断类型,要么现实的申明类型,只申明属性名称将会报错。
你也可以导出资源和节点类型的变量。
@export var resource: Resource # 指定抽象的Resource资源类型
@export var node: Node # 指定抽象的Node节点类型
上面的代码导出了两个变量,分别是Resource
资源类型和Node
节点类型。
Node
和NodePath
注意,Node
节点类型和NodePath
在检视器面板的属性编辑控件和行为几乎一模一样,但是两者存储的数据是不同的。
@export var node: Node # 指定抽象的Node节点类型
@export var node_path: NodePath # 节点路径
func _ready():
print(node) # 输出:btn1:<Button#26407339008>
print(node_path) # 输出:Window/btn1
Node
类型导出变量存储的是对节点的引用,而NodePath
类型导出变量存储的是节点的相对路径。
用NodePath
类型变量获取节点路径后,可以使用get_node()
方法将路径转换为对节点的引用。
@export var node_path: NodePath
var node = get_node(node_path)
如果要限制节点路径的节点类型,可以使用@export_node_path
语句:
@export_node_path("Button") var some_button # 限制可以选择的节点类型为Button
在检视器面板点击属性值编辑器的“指定…”后,在弹出的对话框中可以看到,除了Button类型的节点,其余节点均处于不可选择的灰色状态。
当然你可以传入多个参数,用于限定更多的可选节点类型。
@export_node_path("Button","Label") var some_button # 限制可以选择的节点类型为Button、Label
图片是比较常用的一种资源,我们可以使用preload()
加载一张图片来作为导出属性的默认值。
@export var face = preload("res://icon.svg") # 图片
这种加载的图片,在3.x中类型为StreamTexture
,在4.0中叫做CompressedTexture2D
。
@export var face:CompressedTexture2D
指定场景也是导出变量很常用的方法,只需要将导出变量的类型设定为PackedScene
类型就可以。
@export var scene2:PackedScene
在脚本内部就可以使用变量scene2
来实例化和动态加载外部的另一个场景了。
事实上,导出变量允许的类型相当广泛,包括所有的基本数据类型、内置节点以及资源类型,自定义资源类型等。其中也有一些是特殊的用法,比如字符串作为路径等。初学者建议先学习基本数据类型的导出和一些常用的特殊用法,等到感觉需要深入学习一下时再进行专门的学习和练习。
默认用@export
导出的String
类型变量在检视器面板显示一个单行文本框作为属性值编辑器,如果想要多行文本框则需要使用@export_multiline
进行申明。
@export var text1:String # 默认,单行文本
@export_multiline var text2:String # 显示为多行文本
字符串类型导出的特殊用法之一就是申明和保存路径。Godot会为其提供特殊的属性编辑控件,来方便路径的引用和修改。
需要注意的一点是,导出变量提供两种路径,一种基于res://
的相对路径(或叫局部路径),另一种是基于整个电脑(也可以其他设备)的全局路径。前者只提供资源目录下的视野,而后者则可以在电脑的多个盘符之间游走。
@export_file var f_path # 文件路径(基于res://)
@export_dir var current_dir # 文件夹路径(基于res://)
@export_file
用于设置和保存一个基于res://
的文件路径,而@export_dir
可以设置和保存一个基于res://
的文件夹路径。变量类型其实还是String
类型,只是特殊化的用于存储路径了。你可以显式的申明变量类型为String
或赋默认值。
# 显式申明为字符串类型
@export_file var f_path:String
@export_dir var current_dir:String
# 赋默认值
@export_file var f_path = "res://22.tscn"
@export_dir var current_dir = "res://addons/"
@export_file
可以采用类似3.x的括号语法,传入一个字符串参数用于筛选文件类型。
@export_file("*.txt") var text # “*.txt”用于在文件对话框中过滤文件类型。
使用@export_global_
前缀的两个关键字,就可以将导出变量申明为全局文件系统中的路径, 但仅在工具模式下的脚本中起效。
@export_global_file("*.png") var tool_image # 全局文件系统,PNG图片类型
@export_global_dir var tool_dir # 全局文件系统,文件夹
默认情况下,申明的整数和浮点数导出变量,只会显示一个简单的SpinBox
或只允许输入数值的单行文本框作为属性值编辑器。
@export var num1 = 2 # 整数
@export var num2 = 15.2 # 浮点数
但是Godot允许在基础申明之上,对数值的范围进行限制。
使用@export_range
将可以实现,基本语法是@export_range(min,max,step)
,三个参数分别为最小值、最大值和单次调整的最小步幅。
@export_range(0, 20) var i:int # 允许0到20之间的整数值
@export_range(-10, 20) var j:int # 允许-10到20之间的整数值
@export_range(-10, 20, 0.2) var k: float # 允许从-10到20的浮点数,并将值捕捉为0.2的倍数
以下部分未验证成功:
只有添加提示“or_greater”和/或“or_lesser”时,才能对滑块进行限制。
@export_range(0, 100, 1, "or_greater", "or_lesser")
用@export_exp_easing
申明一个浮点数导出变量,将在检视器面板显示一个显示为缓动曲线的特殊属性值编辑器,通过左右拖动可以改变数值和对应的曲线形状。显示ease()
函数的可视化表示。
@export_exp_easing var transition_speed
日常使用,直接用@export
申明,并指定变量类型为Color
类型即可,下面的一些具体的用例。
@export var bg_color:Color # 没有赋默认值,则默认显示纯黑色
@export var font_color = Color(1.0,0.0,0.0) # RGB形式,红色
@export var font_border_color = Color(1.0,0.0,0.0,0.5) # RGBA形式,红色,50%不透明
@export var border_color = Color("green") # 颜色名形式,无Alpha值,绿色
@export var border_color2 = Color("green",0.4) # 颜色名+Alpha值形式,绿色,40%不透明度
@export var another_color1 = Color.BLACK # 常量形式,无Alpha值,黑色
@export var another_color2 = Color(Color.RED) # 副本形式,无Alpha值,红色
@export var another_color3 = Color(Color.RED,0.4) # 副本+Alpha值形式,红色,40%不透明
给定颜色值的方式是使用Color()
类型的构造函数进行构造,可以采用浮点数的RGB、RGBA形式,带或不带Alpha值的颜色名形式,还有Color
类型的内置常量形式,从一个颜色构造副本的形式等等。
也可以使用@export_color_no_alpha
申明一个Alpha值始终为1的颜色变量。
@export_color_no_alpha var col: Color # 颜色将以RGB形式编辑和使用,alpha值将始终为 1
资料
bit flags在某些编程语言中也被叫做“位枚举”,在Godot中大致可以理解为一种特殊的枚举类型,它的特殊点在于它用于存储多个可选值的多选状态。底层是用位运算之类的实现的,但是这都不重要,重要的是,每一种选择状态都会对应唯一的一个数值,比如,全不选对应0,其他单个选择或多个选择状态下的值计算可能比较复杂,但是其结果在整个选择中是对应唯一的一个整数值的。
我们要了解的是如何用它来判断一种复杂的选择状态。
用作位标志(bit flags)的整数可以在一个属性中存储多个true
/false
(布尔值) 值。通过@export_flags
可以将变量申明为位标志:
@export_flags("Fire", "Water", "Earth", "Wind","Gold") var spell_elements = 0
其中每个字符串对应的数值为其位置(从0开始)的2的次方。
文本 | 数值 |
---|---|
Fire | 20 = 1 |
Water | 21 = 2 |
Earth | 22 = 4 |
Wind | 23 = 8 |
Gold | 24 = 16 |
… | |
最大值 | 232=4294967296 |
单选一个值的话,最终结果就是该值对应的数值,Fire
的值为1
,Water
为 2
,Earth
为4
,Wind
对应于值8
。
如果是多选,则为这些数值之间相加,下面是spell_elements
这个位枚举所对应的一些状态以及计算方法和最终结果。
选择 | 计算 | 最终值 |
---|---|---|
什么都不选 | 0 | 0 |
Fire | 20 | 1 |
Water | 21 | 2 |
Earth | 22 | 4 |
Wind | 23 | 8 |
Gold | 24 | 16 |
Fire+Water | 1 + 2 | 3 |
Fire+Water+Gold | 1+2+16 | 19 |
Fire+Water+Earth+Wind+Gold | 1+2+4+8+16 | 31 |
可以看到这种枚举方式,在表示和判断多选状态时非常有用。
必须为每个标志提供字符串说明。在此示例中,``。通常,应相应地定义常量(例如const ELEMENT_WIND = 8
等等)。
你也可以使用冒号显式的设定值:
@export_flags("Self:4", "Allies:8", "Foes:16") var spell_targets = 0
基于张学徒的说明,大致知道需要用如下的代码形式进行某种组合的判断,但是对位运算相关的知识不太熟悉,导致尚不能明白为何是如此做的。
enum Spell_Elements{
Fire = 1 << 0,
Water = 1 << 1,
Earth = 1 << 2,
Wind = 1 << 3,
Gold = 1 << 4
}
@export_flags("Fire", "Water", "Earth", "Wind","Gold") var spell_elements = 0
func _ready():
print(spell_elements == Spell_Elements.Fire | Spell_Elements.Water)
pass
Spell_Elements.Fire | Spell_Elements.Water
判断是不是同时选择了Fire
和Water
。
允许的最低值为1
,而0
表示未选择任何内容。
你也可以显示的申明每个值的数值(注意,只有2的次方才有效):
@export_flags("Self:4", "Allies:8", "Self and Allies:12", "Foes:16") var spell_targets = 0
Godot4.0为项目设置中定义的物理层、渲染和导航层提供了导出注解:
@export_flags_2d_physics var layers_2d_physics
@export_flags_2d_render var layers_2d_render
@export_flags_2d_navigation var layers_2d_navigation
@export_flags_3d_physics var layers_3d_physics
@export_flags_3d_render var layers_3d_render
@export_flags_3d_navigation var layers_3d_navigation
使用位标志需要对按位运算有一定的了解。 如有疑问,请改用布尔变量。
可以将导出属性的类型指定为一个已经申明的枚举类型,比如下面这样:
enum NamedEnum {THING_1, THING_2, ANOTHER_THING = -1}
@export var x: NamedEnum
func _ready():
print(x == NamedEnum.THING_1)
此时,编辑器将在检视器中为导出变量x
创建一个下拉选框,将NamedEnum的可选值列举为为导出变量x
的可选值。因为枚举本质上是多个整型常量,所以导出变量x
选择后也是返回相应的枚举常量值。
也可以使用@export_enum
将整数和字符串属性限制为特定的值列表。编辑将在检查器中创建一个小部件,列举以下内容:“Warrior”, “Magician"和"Thief”。该值将存储为整数,对应于所选选项的索引(即0、1或2)。
@export_enum("Warrior", "Magician", "Thief") var character_class: int
func _ready():
print(character_class == 0)
可以看到相比先申明枚举,然后声明导出变量的形式,@export_enum
无法提供优雅的代码提示和判断语句。
你也可以使用冒号添加显式值:
@export_enum("Slow:30", "Average:60", "Very Fast:200") var character_speed: int
如果将导出变量类型申明为String
,则存储的值也是String
类型。
@export_enum("Warrior", "Magician", "Thief") var character_class: String
func _ready():
print(character_class == "Warrior")
导出的数组可以具有初始值设定项,但它们必须是常量表达式。
如果导出的数组指定了从 Resource 继承的类型,则该数组 可以通过拖放多个文件在检查器中设置值 立即从文件系统停靠站。
默认值必须是常量表达式。
@export var a = [1, 2, 3]
导出的数组可以指定类型(使用与以前相同的提示)。
@export var ints: Array[int] = [1, 2, 3]
# 目前尚不支持`Array[Array[float]]`等嵌套类型数组。
@export var two_dimensional: Array[Array] = [[1.0, 2.0], [3.0, 4.0]]
您可以省略缺省值,但如果不赋值,则该值将为null
。
@export var b: Array
@export var scenes: Array[PackedScene]
从资源继承的具有指定类型的数组可以通过以下方式设置,从文件系统停靠站拖放多个文件。
@export var textures: Array[Texture] = []
@export var scenes: Array[PackedScene] = []
Packed类型数组也可以工作,但只初始化为空:
@export var vector3s = PackedVector3Array()
@export var strings = PackedStringArray()
可以用@export_group("分组名称")
语法在检查器中对导出的属性进行分组 。参数传入分组名称即可。此关键字之后的每个导出属性都将添加到该组,直到另一个@export_group("分组名称")
语句的出现。
@export_group("My Properties") # 创建属性分组
@export var number = 3 # 该属性将自动归于My Properties分组下
@export_group("My Properties2") # 创建另一个属性分组,也就意为着上一个属性分组自动中断
@export var number2 = 4 # 该属性将自动归于My Properties2分组下
使用@export_group("")
可以中断上一个分组,之后的导出属性因为不再属于任何分组,会直接出现在属性分类下。
@export_group("My Properties") # 创建属性分组
@export var number = 3 # 该属性将自动归于My Properties分组下
@export_group("") # 中断上一分组
@export var number2 = 4 # 该属性将提升到所有组之前
@export_group
还有第2个参数,用于指定分组下属性的前缀 。
@export_group("Prefixed Properties", "prefix_") # 创建属性分组,属性前缀为“prefix_”
# 以下3个属性都以“prefix_”作为前缀,自动归入“Prefixed Properties”分组下
@export var prefix_number = 3
@export var prefix_string = ""
@export var prefix_color = Color("red")
# 出现一个不符合该分组前缀的导出属性,则自动中断分组
@export var aa = 12
# 中断分组后,出现“prefix_”前缀的导出属性则不会继续添加到组
@export var prefix_color2 = Color("green")
可以看到,在检视器上,相同前缀的属性会自动隐藏统一的前缀,让检视器面板看起来更简洁,但是其实际对应的属性名是不变的。
属性分组不能嵌套,但可以使用@export_subgroup
在属性分组内创建子分组。
# 分组
@export_group("Prefixed Properties", "prefix_")
@export var prefix_number = 3
@export var prefix_string = ""
@export var prefix_color = Color("red")
# 子分组
@export_subgroup("Extra Properties")
@export var string = ""
@export var flag = false
@export var aa = 12
@export var prefix_color2 = Color("green")
子分组也不支持嵌套,也就是说在同一个@export_group()
之后,下一个@export_group()
之前,所有的@export_subgroup()
将始终是并列关系,而不会出现从属关系。
# 分组
@export_group("Prefixed Properties", "prefix_")
@export var prefix_number = 3
@export var prefix_string = ""
@export var prefix_color = Color("red")
# 子分组2
@export_subgroup("Extra Properties")
@export var string = ""
@export var flag = false
# 子分组2
@export_subgroup("group2")
@export var aa = 12
@export var prefix_color2 = Color("green")
同样可以使用@export_subgroup("")
来中断子分组,但是实际结果是,直接中断了整个属性分组。之后的导出变量会提升到属性分类下。
# 分组
@export_group("Prefixed Properties", "prefix_")
@export var prefix_number = 3
@export var prefix_string = ""
@export var prefix_color = Color("red")
# 子分组
@export_subgroup("Extra Properties")
@export var string = ""
@export var flag = false
@export_subgroup("") # 中断子分组
@export var aa = 12
@export var prefix_color2 = Color("green")
默认情况下,导出变量的属性分类名称是其所在代码文件的文件名。
使用@export_category()
可以自定义属性类别名称,甚至你可以创建多个属性类别。
@export_category("Main Category") # 定义属性分类
@export var number = 3
@export var string = ""
@export_category("Extra Category") # 定义属性分类2
@export var flag = false
注意
默认检视器面板也就是属性列表,是根据节点的类继承关系(或称“继承链”)显示属性的,每个类型以属性分类的形式单独呈现,由上到下,首先是节点自身的类型,然后是其继承的父类型,然后是父类型的类型…以此类推。
而导出变量是在节点的脚本中定义的,脚本继承自节点的类型,所以可以算得上是该节点类型的一个子类型,所以脚本变量(又称“导出变量”或“导出属性”)就会出现在节点类型之前。
虽然自定义属性分类很好用,但是对于在某些情况下有可能干扰继承链的查看,所以谨慎和收敛的使用是必要的。
在工具脚本模式下从脚本更改导出变量的值时,检查器中的值不会自己更新。要更新它,请在设置导出变量的值后调用 notify_property_list_changed()
。
不是每种类型的导出都可以在语言本身的级别上提供,以避免不必要的设计复杂性。下面介绍一些或多或少可以用低级API实现的常见导出功能。
在进一步阅读之前,您应该熟悉处理属性的方式,以及如何使用_set()、_get()和_get_Property_list()方法自定义属性,如从对象访问数据或逻辑中所述。
2023年3月14日21:15:19
关于评论区提问,我也是在学习“文档注释”的部分,无意间发现的。
原文是:
通注释和文档注释之间有两个区别。首先,文档注释应该以双哈希符号##开头。其次,它必须紧跟在脚本成员之前,或者对于脚本描述,它必须放在脚本的顶部。 如果记录了导出的变量,则其描述将用作编辑器中的工具提示。 该文档可以由编辑器生成为XML文件。
所以要为导出变量添加编辑器的ToolTip,也就是鼠标提示文本,可以借助“文档注释”功能实现。其实也很简单就是将描述写成##
开头的文档注释就可以了,比如下面的例子:
extends Control
## 页面的背景色,默认为50%不透明度白色
@export var bg_color = Color("white",0.5):
set(val):
bg_color = val
get:
return bg_color
此时,查看“检视器”面板的导出变量,鼠标经过时就会出现相应的描述。
根据文档注释的语法,你可以采用某些支持的BBcode代码来实现诸如加粗、斜体、下划线以及强制换行、代码块等效果。
这里我们先实现多行代码功能,使用BBcode代码[br]
可以创建多行文档描述。
extends Control
## 页面的背景色,默认为50%不透明度白色[br]
## 第二行[br]
## 第三行[br]
## 更多...
@export var bg_color = Color("blue",0.5):
set(val):
bg_color = val
get:
return bg_color
##
号之后的空格是非必须的使用[codeblock]
和[/codeblock]
这一对标签,可以在描述中创建代码示例效果。
##页面的背景色,默认为50%不透明度白色[br]
## 示例:
## [codeblock]
## bg_color = Color("green",0.4)
## [/codeblock]
@export var bg_color = Color("blue",0.5):
set(val):
bg_color = val
get:
return bg_color
重要提示
当你连续修改导出变量的文档注释时,检视器面板的描述并不会实时刷新,关闭场景重新打开或者在不同场景间切换再回来,也不会刷新,目前已知的一种刷新方法是直接重新加载当前项目。