Godot 4 GDScript 教程 / 类型化 GDScript(@export/@onready)
类型化 GDScript(@export/@onready)
类型化 GDScript 是 Godot 4 的重大改进之一,通过类型注解、@export 导出、@onready 延迟初始化和 @tool 编辑器脚本,让代码更安全、更高效。本章深入讲解类型化 GDScript 的各种用法和最佳实践。
1. 类型注解语法
1.1 变量类型注解
# 显式类型注解
var health: int = 100
var speed: float = 300.0
var name: String = "Player"
var is_alive: bool = true
var position: Vector2 = Vector2.ZERO
var color: Color = Color.WHITE
# 类型推断(使用 :=)
var damage := 50 # 推断为 int
var ratio := 0.75 # 推断为 float
var label := "Hello" # 推断为 String
var vector := Vector2.ONE # 推断为 Vector2
# 可以不指定初始值(默认值为类型零值)
var score: int # 默认 0
var velocity: Vector2 # 默认 Vector2(0, 0)
var active: bool # 默认 false
var texture: Texture2D # 默认 null
# 可选类型(允许 null)
var target: Node2D = null # 可以为 null
var weapon: Weapon = null # 自定义类型也可以是 null
⚠️ 注意: 使用类型注解后,赋值不匹配类型的值会触发编译错误。这是类型安全的保障。
1.2 函数类型注解
# 参数类型 + 返回类型
func calculate_damage(base: int, multiplier: float) -> int:
return int(base * multiplier)
# 无返回值用 void
func heal(amount: int) -> void:
health += amount
# 返回多个值(使用元组语法)
func get_bounds() -> Array:
return [Vector2.ZERO, Vector2.ONE]
# 返回节点类型
func get_player() -> CharacterBody2D:
return $Player as CharacterBody2D
# 带默认值的参数
func spawn_enemy(type: String, count: int = 1, level: int = 1) -> void:
for i in range(count):
print(f"生成 {type} Lv.{level}")
# 使用示例
func _ready() -> void:
var damage := calculate_damage(50, 1.5)
spawn_enemy("slime", 3)
spawn_enemy("dragon", 1, 10)
1.3 常见类型注解一览
| 类型 | 说明 | 示例 |
|---|---|---|
int | 整数 | var x: int = 42 |
float | 浮点数 | var f: float = 3.14 |
bool | 布尔值 | var b: bool = true |
String | 字符串 | var s: String = "hi" |
Vector2 | 2D 向量 | var v: Vector2 = Vector2.ZERO |
Vector3 | 3D 向量 | var v: Vector3 = Vector3.ZERO |
Color | 颜色 | var c: Color = Color.RED |
Array[T] | 类型化数组 | var a: Array[int] = [] |
Dictionary | 字典 | var d: Dictionary = {} |
NodePath | 节点路径 | var p: NodePath = ^"path" |
Transform2D | 2D 变换 | var t: Transform2D |
Transform3D | 3D 变换 | var t: Transform3D |
PackedByteArray | 字节数组 | var b: PackedByteArray |
PackedScene | 打包场景 | var s: PackedScene |
Texture2D | 2D 纹理 | var t: Texture2D |
Callable | 可调用对象 | var c: Callable |
Signal | 信号引用 | var s: Signal |
2. @export 导出变量
2.1 基本 @export 用法
@export 让变量在编辑器的检查器中可见并可编辑:
extends CharacterBody2D
# 基本导出(类型由默认值推断)
@export var speed := 300.0
@export var health := 100
# 显式类型导出
@export var max_health: int = 100
@export var move_speed: float = 200.0
@export var player_name: String = "Hero"
@export var is_invincible: bool = false
@export var sprite_color: Color = Color.WHITE
2.2 @export 类型化导出
# 整数范围
@export_range(1, 100) var level: int = 1
@export_range(0, 100, 1, "or_greater") var health: int = 100
@export_range(0.0, 10.0, 0.1, "suffix:秒") var cooldown: float = 1.5
# 浮点数范围
@export_range(0.0, 1.0) var opacity: float = 1.0
@export_range(-180, 180, 0.1, "radians_as_degrees") var angle: float = 0.0
# 枚举导出
@export_enum("Idle", "Running", "Jumping", "Falling") var state: String = "Idle"
# 带值的枚举
@export_enum("Warrior:1", "Mage:2", "Rogue:3", "Healer:4") var character_class: int = 1
# 标志位导出
@export_flags("Fire", "Water", "Earth", "Wind") var elements: int = 0
# 文件路径导出
@export_file("*.json") var config_path: String
@export_dir var assets_dir: String
@export_file("*.png", "*.jpg") var texture_path: String
# 多行文本
@export_multiline var description: String = "这是一个角色"
# 密码字段
@export var password: String = ""
# 颜色(带 Alpha)
@export var tint_color: Color = Color(1, 1, 1, 1)
2.3 节点和资源导出
# 节点类型导出
@export var target_node: Node2D
@export var sprite: Sprite2D
@export var collision_shape: CollisionShape2D
@export var audio_player: AudioStreamPlayer
# 资源类型导出
@export var texture: Texture2D
@export var sound_effect: AudioStream
@export var font: Font
@export var material: Material
@export var scene_file: PackedScene
# 自定义资源导出
@export var character_stats: CharacterStats # 自定义 Resource 子类
# 数组导出
@export var enemy_types: Array[PackedScene] = []
@export var waypoints: Array[Vector2] = []
@export var inventory_items: Array[Item] = []
2.4 @export 分组
extends CharacterBody2D
@export_group("Movement")
@export var move_speed: float = 300.0
@export var acceleration: float = 1500.0
@export var friction: float = 1200.0
@export_group("Combat")
@export var attack_damage: int = 25
@export var attack_range: float = 50.0
@export var attack_cooldown: float = 0.5
@export_group("Visuals")
@export_subgroup("Sprite")
@export var sprite_texture: Texture2D
@export var sprite_scale: Vector2 = Vector2.ONE
@export_subgroup("Animation")
@export var idle_animation: String = "idle"
@export var run_animation: String = "run"
@export_group("", "") # 结束分组,回到默认
@export var misc_property: int = 0
💡 提示: 使用 @export_group 和 @export_subgroup 可以在检查器中组织变量,让大量导出变量更加清晰。
3. @onready 延迟初始化
3.1 基本用法
@onready 确保变量在节点进入场景树并完成 _ready() 后才初始化:
extends CharacterBody2D
# 不使用 @onready(节点还未进入场景树,$Sprite2D 为 null)
var sprite_broken: Sprite2D = $Sprite2D # ❌ 错误!
# 使用 @onready(节点就绪后才赋值)
@onready var sprite: Sprite2D = $Sprite2D # ✅ 正确
@onready var collision: CollisionShape2D = $CollisionShape2D
@onready var animation_player: AnimationPlayer = $AnimationPlayer
@onready var camera: Camera2D = $Camera2D
@onready var health_bar: ProgressBar = $UI/HealthBar
3.2 @onready 与类型
extends Node
# 类型化 @onready
@onready var label: Label = $Label
@onready var timer: Timer = $Timer
@onready var audio: AudioStreamPlayer = $AudioStreamPlayer
# 可空类型
@onready var optional_node: Node2D = $MaybeExists # 如果节点不存在则为 null
# @onready 也可以用于非节点表达式
@onready var viewport_size: Vector2 = get_viewport_rect().size
@onready var game_time_start: float = Time.get_ticks_msec() / 1000.0
3.3 @onready 初始化顺序
extends Node
# @onready 变量初始化顺序:从上到下,先于 _ready()
@onready var a: int = _calculate_a() # 1. 先执行
@onready var b: int = _calculate_b(a) # 2. 后执行,可以使用 a
# 然后执行 _ready()
func _ready() -> void:
print(f"a={a}, b={b}") # 可以使用 a 和 b
func _calculate_a() -> int:
return 10
func _calculate_b(value: int) -> int:
return value * 2
⚠️ 注意: @onready 变量初始化时,场景树中当前节点的子节点已经就绪,但兄弟节点和父节点可能尚未完成初始化。
4. @tool 编辑器脚本
4.1 基本 @tool
@tool 让脚本在编辑器中也能运行,用于实时预览和自定义编辑器行为:
@tool
extends Sprite2D
@export var pulse_speed: float = 2.0
@export var pulse_amount: float = 0.2
var time: float = 0.0
func _process(delta: float) -> void:
# 在编辑器和游戏中都执行
time += delta
var pulse = 1.0 + sin(time * pulse_speed) * pulse_amount
scale = Vector2.ONE * pulse
4.2 区分编辑器和运行时
@tool
extends Node2D
@export var show_debug: bool = false:
set(value):
show_debug = value
queue_redraw()
func _process(delta: float) -> void:
# 使用 Engine.is_editor_hint() 判断是否在编辑器中
if Engine.is_editor_hint():
# 编辑器中执行的逻辑
_editor_update(delta)
else:
# 游戏运行时执行的逻辑
_game_update(delta)
func _editor_update(delta: float) -> void:
pass
func _game_update(delta: float) -> void:
pass
func _draw() -> void:
if not Engine.is_editor_hint():
return
if show_debug:
draw_circle(Vector2.ZERO, 50, Color.RED)
draw_arc(Vector2.ZERO, 100, 0, TAU, 64, Color.GREEN, 2.0)
4.3 @tool 实战:可视化编辑器辅助
@tool
extends Area2D
## 在编辑器中可视化碰撞区域
@export var detection_radius: float = 100.0:
set(value):
detection_radius = value
_update_shape()
queue_redraw()
@export var show_range: bool = true:
set(value):
show_range = value
queue_redraw()
@export var range_color: Color = Color(1, 0, 0, 0.3):
set(value):
range_color = value
queue_redraw()
@onready var collision: CollisionShape2D = $CollisionShape2D
func _ready() -> void:
if Engine.is_editor_hint():
return
_update_shape()
func _update_shape() -> void:
# 更新碰撞形状
if collision and collision.shape is CircleShape2D:
collision.shape.radius = detection_radius
func _draw() -> void:
if not Engine.is_editor_hint() or not show_range:
return
# 绘制检测范围
draw_circle(Vector2.ZERO, detection_radius, range_color)
draw_arc(Vector2.ZERO, detection_radius, 0, TAU, 64, Color.RED, 2.0)
# 绘制标签
draw_string(
ThemeDB.fallback_font,
Vector2(-30, -detection_radius - 10),
f"R: {detection_radius:.0f}",
HORIZONTAL_ALIGNMENT_CENTER,
-1, 14, Color.WHITE
)
⚠️ 注意: @tool 脚本会在编辑器中持续运行,必须小心避免执行破坏性操作(如删除节点、修改文件等)。
5. @icon 自定义图标
# 使用自定义图标为类注册图标
@icon("res://assets/icons/player_icon.svg")
class_name Player
extends CharacterBody2D
# @icon 可以使用项目中的 SVG 或 PNG 图标
@icon("res://addons/my_plugin/icons/custom_node.svg")
class_name CustomNode
extends Node
💡 提示: SVG 图标在不同缩放下都保持清晰,推荐使用。图标尺寸建议 16x16 或 32x32 像素。
6. 类型安全的 get_node
6.1 使用 $ 和 get_node()
extends Node
# $ 等同于 get_node(),返回 Node 类型
var sprite: Sprite2D = $Sprite2D as Sprite2D # 需要 as 转换
# @onready 自动处理类型转换
@onready var typed_sprite: Sprite2D = $Sprite2D # 直接得到正确类型
# get_node() 需要显式转换
var node: Sprite2D = get_node("Sprite2D") as Sprite2D
# 使用 NodePath
@export var target_path: NodePath
@onready var target: Node2D = get_node(target_path) as Node2D
6.2 find_node 和 get_node_or_null
extends Node
func _ready() -> void:
# 安全获取节点(不存在时返回 null)
var maybe_node: Node = get_node_or_null("MaybeExists")
if maybe_node:
print("节点存在")
# 查找节点(递归搜索)
var found: Node = find_child("TargetNode", true, false)
if found is Sprite2D:
var sprite := found as Sprite2D
print("找到精灵: %s" % sprite.name)
# 获取所有匹配的子节点
var enemies: Array[Node] = find_children("*Enemy*", "", true, false)
print("敌人数量: %d" % enemies.size())
⚠️ 注意: 尽量避免在 _process 中频繁调用 get_node 或 find_child,应在 _ready 中缓存引用。
7. 类型推断
7.1 := 类型推断
# 基本类型推断
var health := 100 # int
var speed := 300.0 # float
var name := "Player" # String
var alive := true # bool
var position := Vector2.ZERO # Vector2
var color := Color.RED # Color
# 函数返回值推断
func get_position() -> Vector2:
return Vector2(100, 200) # 返回类型注解帮助推断
# 数组推断
var numbers := [1, 2, 3] # Array[int] (如果所有元素类型相同)
var mixed := [1, "two", 3.0] # Array (混合类型)
# 字典推断
var config := {"name": "test", "value": 42} # Dictionary
7.2 推断限制
# 不能推断 null 的类型
# var x := null # 错误!无法推断类型
# 明确指定可空类型
var x: Node = null
# 不能推断空数组/字典的元素类型
# var arr := [] # 警告:类型不明确
var arr: Array[int] = [] # 正确
# 不能推断复杂表达式
# var result := complex_function() # 如果返回类型不明确则出错
var result: Variant = complex_function() # 使用 Variant
8. 可选类型与 Variant
8.1 Variant 类型
# Variant 是 GDScript 的通用类型,可以存储任何值
var anything: Variant = 100
anything = "now a string"
anything = Vector2(1, 2)
anything = null
# 推荐:尽量使用具体类型而非 Variant
# 不推荐
func process_data(data: Variant) -> Variant:
return data
# 推荐
func process_health(value: int) -> int:
return clampi(value, 0, 100)
8.2 空安全
extends Node
# 可以存储 null 的类型
var target: Node2D = null
var weapon: Weapon = null
func _process(delta: float) -> void:
# 使用前检查 null
if target != null:
look_at(target.global_position)
# 使用 is_instance_valid 检查
if is_instance_valid(target):
target.take_damage(10)
# 使用安全导航
if weapon:
weapon.attack()
9. 类型化代码最佳实践
9.1 推荐实践
# ✅ 好的实践
# 1. 始终使用类型注解
var health: int = 100
var speed: float = 300.0
# 2. 使用 @export 让变量在编辑器中配置
@export var max_health: int = 100
@export var move_speed: float = 300.0
# 3. 使用 @onready 获取节点引用
@onready var sprite: Sprite2D = $Sprite2D
@onready var camera: Camera2D = $Camera2D
# 4. 函数参数和返回值都要类型注解
func calculate_damage(base: int, mult: float) -> int:
return int(base * mult)
# 5. 使用 class_name 注册全局类
class_name HealthComponent
extends Node
# 6. 使用枚举代替魔法数字
enum State { IDLE, RUNNING, JUMPING }
var current_state: State = State.IDLE
9.2 避免的做法
# ❌ 不好的实践
# 1. 避免使用 Variant
var bad_variant: Variant = 100
# 2. 遍免在循环中创建类型
# 不推荐
for i in range(100):
var temp: Vector2 = Vector2(i, 0) # 每帧创建
# 3. 避免过度使用 as 转换
# 不推荐
var node = get_node("Sprite")
if node is Sprite2D:
var sprite = node as Sprite2D # 冗余,node 已经被推断为 Sprite2D
# 4. 避免不完整的类型注解
# 不推荐
var health = 100 # 无类型注解
# 推荐
var health: int = 100 # 清晰的类型注解
10. 游戏开发场景
场景:配置化敌人生成系统
@tool
class_name EnemySpawner
extends Node2D
## 敌人生成器 - 使用 @export 实现编辑器配置
@export_group("Enemy Settings")
## 敌人场景
@export var enemy_scene: PackedScene
## 生成半径
@export_range(50, 500) var spawn_radius: float = 200.0
## 最大同时存在数量
@export_range(1, 100, 1, "or_greater") var max_count: int = 10
## 生成间隔(秒)
@export_range(0.1, 10.0, 0.1) var spawn_interval: float = 2.0
@export_group("Wave Settings")
## 是否启用波次
@export var use_waves: bool = false
## 每波敌人数量
@export var enemies_per_wave: int = 5
## 波次间隔
@export var wave_interval: float = 30.0
@export_group("Visual")
## 显示生成范围
@export var show_range: bool = true:
set(value):
show_range = value
queue_redraw()
## 生成范围颜色
@export var range_color: Color = Color(0, 1, 0, 0.2)
# 内部状态
var _spawn_timer: float = 0.0
var _current_count: int = 0
var _wave_number: int = 0
var _active_enemies: Array[Node2D] = []
func _ready() -> void:
if Engine.is_editor_hint():
return
# 清理无效引用
_active_enemies.clear()
func _process(delta: float) -> void:
if Engine.is_editor_hint():
return
_spawn_timer += delta
# 清理已死亡的敌人引用
_active_enemies = _active_enemies.filter(func(e: Node2D) -> bool:
return is_instance_valid(e)
)
_current_count = _active_enemies.size()
if _spawn_timer >= spawn_interval and _current_count < max_count:
_spawn_timer = 0.0
_spawn_enemy()
func _spawn_enemy() -> void:
if not enemy_scene:
push_warning("EnemySpawner: 未设置敌人场景")
return
# 在圆形范围内随机位置生成
var angle := randf() * TAU
var dist := randf() * spawn_radius
var offset := Vector2(cos(angle), sin(angle)) * dist
var enemy: Node2D = enemy_scene.instantiate()
enemy.global_position = global_position + offset
get_parent().add_child(enemy)
_active_enemies.append(enemy)
_current_count += 1
func _draw() -> void:
if not Engine.is_editor_hint():
return
if show_range:
draw_circle(Vector2.ZERO, spawn_radius, range_color)
draw_arc(Vector2.ZERO, spawn_radius, 0, TAU, 64, Color.GREEN, 2.0)
# 绘制中心标记
draw_circle(Vector2.ZERO, 5, Color.RED)
11. 扩展阅读
上一章: 04 - GDScript 2.0 基础语法 下一章: 06 - 函数与 Lambda