Godot 3 GDScript 教程 / 节点与场景树
节点与场景树
节点类型概览
Godot 中一切皆节点(Node)。不同类型的节点负责不同的功能。
2D 节点
| 节点类型 | 功能 |
|---|---|
Node2D | 2D 基础节点(位置、旋转、缩放) |
Sprite | 显示 2D 纹理 |
AnimatedSprite | 帧动画精灵 |
Camera2D | 2D 相机 |
TileMap | 瓦片地图 |
YSort | Y 轴排序容器 |
ParallaxBackground | 视差背景 |
Particles2D | 2D 粒子 |
Light2D | 2D 光源 |
物理节点
| 节点类型 | 功能 |
|---|---|
RigidBody2D | 2D 刚体(受物理模拟控制) |
StaticBody2D | 2D 静态体(不移动的碰撞体) |
KinematicBody2D | 2D 运动学体(代码控制移动) |
Area2D | 2D 区域(检测进入/离开) |
CollisionShape2D | 碰撞形状 |
CollisionPolygon2D | 碰撞多边形 |
UI 节点(Control)
| 节点类型 | 功能 |
|---|---|
Control | UI 基础节点 |
Label | 文本标签 |
Button | 按钮 |
LineEdit | 单行文本输入 |
TextEdit | 多行文本编辑 |
TextureRect | 纹理显示 |
ProgressBar | 进度条 |
HSlider / VSlider | 滑块 |
CheckBox / CheckButton | 复选框 |
OptionButton | 下拉选择 |
Container | 布局容器 |
通用节点
| 节点类型 | 功能 |
|---|---|
Node | 基础节点(用于逻辑组织) |
Timer | 计时器 |
Tween | 补间动画 |
AnimationPlayer | 动画播放器 |
AnimationTree | 动画状态机 |
AudioStreamPlayer | 2D/3D 音频播放 |
HTTPRequest | HTTP 请求 |
3D 节点
| 节点类型 | 功能 |
|---|---|
Spatial | 3D 基础节点 |
MeshInstance | 3D 网格显示 |
Camera | 3D 相机 |
DirectionalLight | 方向光 |
OmniLight | 点光源 |
SpotLight | 聚光灯 |
RigidBody | 3D 刚体 |
KinematicBody | 3D 运动学体 |
CSGBox / CSGSphere | CSG 几何体 |
场景树结构
场景树是 Godot 的核心概念,每个场景都是节点的树状层次结构。
Main (Node2D)
├── Player (KinematicBody2D)
│ ├── Sprite (Sprite)
│ ├── CollisionShape2D
│ ├── Camera2D
│ └── Area2D
│ └── CollisionShape2D
├── Enemies (Node2D)
│ ├── Slime (KinematicBody2D)
│ │ ├── Sprite
│ │ └── CollisionShape2D
│ └── Goblin (KinematicBody2D)
│ ├── Sprite
│ └── CollisionShape2D
├── TileMap
├── CanvasLayer
│ └── UI (Control)
│ ├── HealthBar (ProgressBar)
│ └── ScoreLabel (Label)
└── AudioStreamPlayer
场景树规则
- 根节点:每个场景文件(
.tscn)有一个根节点 - 父子关系:子节点的位置相对于父节点
- 场景实例化:一个场景可以作为节点嵌入另一个场景
- 唯一根:整个运行时有一个全局场景树根(
SceneTree)
节点生命周期
节点创建 (new / instance)
│
├── _init() # 构造函数
│
├── add_child() 调用
│ ├── _enter_tree() # 进入场景树
│ │
│ ├── 子节点初始化...
│ │
│ └── _ready() # 子节点全部就绪(自下而上)
│
├── 运行时
│ ├── _process(delta) # 每帧
│ ├── _physics_process(delta) # 每物理帧
│ └── _input(event) # 输入事件
│
├── remove_child() / queue_free()
│ ├── _exit_tree() # 离开场景树
│ └── 对象被销毁
└── _notification() # 各种通知
生命周期代码示例
extends Node2D
func _init() -> void:
print("[_init] 构造函数 - 节点对象已创建")
func _enter_tree() -> void:
print("[_enter_tree] 进入场景树 - path: ", get_path())
func _ready() -> void:
print("[_ready] 就绪 - 所有子节点已初始化")
print(" 子节点数量: ", get_child_count())
print(" 树中的路径: ", get_path())
func _process(delta: float) -> void:
pass # 每帧调用(不建议在生命周期学习时开启)
func _exit_tree() -> void:
print("[_exit_tree] 离开场景树")
func _notification(what: int) -> void:
match what:
NOTIFICATION_ENTER_TREE:
pass # 与 _enter_tree 相同时机
NOTIFICATION_READY:
pass # 与 _ready 相同时机
NOTIFICATION_EXIT_TREE:
pass # 与 _exit_tree 相同时机
NOTIFICATION_PAUSED:
print("游戏暂停")
NOTIFICATION_UNPAUSED:
print("游戏恢复")
_ready 的执行顺序
# 假设场景树:
# A
# ├── B
# │ └── D
# └── C
# _ready 执行顺序: D → B → C → A
# 从最深的子节点开始,由下往上执行
# 这保证了 _ready 中访问子节点时,子节点已就绪
💡 提示:在 _ready() 中可以安全地访问子节点,因为子节点的 _ready() 已经先执行完毕。
add_child / remove_child
动态添加子节点
extends Node2D
var enemy_scene = preload("res://scenes/Enemy.tscn")
func spawn_enemy(pos: Vector2) -> void:
var enemy = enemy_scene.instance()
enemy.position = pos
add_child(enemy) # 将 enemy 添加为当前节点的子节点
func spawn_bullet(pos: Vector2, direction: Vector2) -> void:
var bullet = preload("res://scenes/Bullet.tscn").instance()
bullet.position = pos
bullet.direction = direction
$Bullets.add_child(bullet) # 添加到 Bullets 子节点下
动态移除子节点
# 方式一:直接释放
func remove_all_enemies() -> void:
for enemy in $Enemies.get_children():
$Enemies.remove_child(enemy) # 从树中移除
enemy.queue_free() # 释放内存
# 方式二:queue_free(推荐)
func remove_enemy(enemy: Node) -> void:
enemy.queue_free() # 在当前帧结束后自动移除并释放
# 方式三:立即释放(不推荐,可能导致问题)
func immediate_remove() -> void:
$Enemy.free() # 立即释放,可能在回调中出问题
queue_free vs free
| 特性 | queue_free() | free() |
|---|---|---|
| 释放时机 | 当前帧结束后 | 立即 |
| 安全性 | 🟢 安全 | 🔴 可能崩溃 |
| 推荐度 | ✅ 推荐 | ❌ 谨慎使用 |
⚠️ 注意:不要在 _process 或信号回调中直接 free(),使用 queue_free() 更安全。
场景实例化
PackedScene 与 instance
# 预加载场景资源
var enemy_scene = preload("res://scenes/Enemy.tscn")
# 实例化场景
func spawn(pos: Vector2) -> void:
var instance = enemy_scene.instance() # 创建实例
instance.position = pos
add_child(instance)
# 动态加载场景
func spawn_from_path(path: String, pos: Vector2) -> void:
var scene = load(path)
if scene:
var instance = scene.instance()
instance.position = pos
add_child(instance)
传递初始化参数
# Enemy.gd
extends KinematicBody2D
var enemy_type: String = ""
var level: int = 1
func init(type: String, lvl: int) -> void:
enemy_type = type
level = lvl
_apply_type_settings()
func _apply_type_settings() -> void:
match enemy_type:
"slime":
health = 50
speed = 100
"goblin":
health = 80
speed = 150
"dragon":
health = 500
speed = 80
# 生成时
var enemy = enemy_scene.instance()
enemy.init("dragon", 5)
add_child(enemy)
⚠️ 注意:init() 方法需要在 add_child() 之前调用,否则 _ready() 中访问的变量可能未设置。
组(Groups)
组是将节点按功能分类的机制,类似于"标签"。
使用组
# 添加节点到组
$Player.add_to_group("players")
$Enemy.add_to_group("enemies")
$Enemy.add_to_group("damageable")
# 移除节点从组
$Enemy.remove_from_group("enemies")
# 检查节点是否在组中
if $Enemy.is_in_group("enemies"):
print("是敌人")
通过组获取节点列表
# 获取组内所有节点
var enemies = get_tree().get_nodes_in_group("enemies")
print("敌人数量: ", enemies.size())
# 遍历敌人
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.take_damage(10)
组调用(call_group)
# 对组内所有节点调用方法
get_tree().call_group("enemies", "take_damage", 50)
get_tree().call_group("players", "heal", 20)
# 延迟调用(当前帧结束后)
get_tree().call_group_flags(
SceneTree.GROUP_CALL_DEFERRED,
"enemies",
"set_active",
false
)
组的实际应用
# 敌人生成器
extends Node
func _ready() -> void:
# 将所有敌人加入组
for child in get_children():
child.add_to_group("enemies")
# 伤害区域
extends Area2D
func _on_body_entered(body: Node) -> void:
if body.is_in_group("damageable"):
body.take_damage(25)
if body.is_in_group("players"):
body.collect_item()
# 存档系统
func save_all_enemies() -> Array:
var data = []
for enemy in get_tree().get_nodes_in_group("enemies"):
data.append({
"type": enemy.enemy_type,
"position": enemy.global_position,
"health": enemy.health,
})
return data
节点通信策略
策略一:信号(推荐)
# 适合:松耦合、跨场景通信
# Player.gd
signal health_changed(value)
func take_damage(amount: int) -> void:
health -= amount
emit_signal("health_changed", health)
# UI.gd(连接 Player 的信号)
$Player.connect("health_changed", self, "_update_health_bar")
策略二:直接调用
# 适合:紧耦合、父子关系
# Player.gd
func attack() -> void:
$Weapon.fire() # 直接调用子节点方法
# 跨节点调用
func _on_hit(damage: int) -> void:
var game = get_node("/root/Main")
game.on_player_hit(damage)
策略三:组调用
# 适合:广播消息
# 通知所有敌人玩家已死亡
get_tree().call_group("enemies", "on_player_died")
# 暂停所有敌人
get_tree().call_group("enemies", "set_active", false)
通信策略对比
| 策略 | 耦合度 | 适用场景 | 方向 |
|---|---|---|---|
| 信号 | 🟢 低 | 跨节点/场景 | 多对多 |
| 直接调用 | 🔴 高 | 父子/已知引用 | 一对一 |
| 组调用 | 🟢 低 | 广播/分类操作 | 一对多 |
场景管理器(Autoload)
Autoload 是全局单例,在项目启动时自动加载,用于管理全局状态和跨场景数据。
创建 Autoload
- 创建脚本
res://scripts/SceneManager.gd - Project → Project Settings → Autoload
- 添加路径和名称
场景管理器实现
# res://scripts/SceneManager.gd
extends Node
var current_scene: Node = null
var scene_history: Array = []
func _ready() -> void:
var root = get_tree().get_root()
current_scene = root.get_child(root.get_child_count() - 1)
func goto_scene(path: String) -> void:
# 记录历史
scene_history.append(current_scene.filename)
# 延迟切换(确保安全)
call_deferred("_deferred_goto_scene", path)
func _deferred_goto_scene(path: String) -> void:
# 释放当前场景
current_scene.free()
# 加载新场景
var new_scene = load(path).instance()
get_tree().get_root().add_child(new_scene)
get_tree().set_current_scene(new_scene)
current_scene = new_scene
func go_back() -> void:
if scene_history.size() > 0:
var previous = scene_history.pop_back()
goto_scene(previous)
func reload_scene() -> void:
goto_scene(current_scene.filename)
GameManager 示例
# res://scripts/GameManager.gd
extends Node
enum GameState { MENU, PLAYING, PAUSED, GAME_OVER }
var state: int = GameState.MENU
var score: int = 0
var high_score: int = 0
var lives: int = 3
var current_level: int = 1
signal state_changed(new_state: int)
signal score_changed(new_score: int)
func change_state(new_state: int) -> void:
state = new_state
emit_signal("state_changed", new_state)
match new_state:
GameState.PLAYING:
get_tree().paused = false
GameState.PAUSED:
get_tree().paused = true
GameState.GAME_OVER:
_check_high_score()
func add_score(points: int) -> void:
score += points
emit_signal("score_changed", score)
func lose_life() -> void:
lives -= 1
if lives <= 0:
change_state(GameState.GAME_OVER)
func reset_game() -> void:
score = 0
lives = 3
current_level = 1
change_state(GameState.PLAYING)
func _check_high_score() -> void:
if score > high_score:
high_score = score
游戏开发场景
场景:动态生成无尽地图
extends Node2D
var chunk_scene = preload("res://scenes/MapChunk.tscn")
var chunk_size: float = 512.0
var active_chunks: Dictionary = {}
var render_distance: int = 3
func _process(delta: float) -> void:
var player_chunk = _get_chunk_coord($Player.position)
_update_chunks(player_chunk)
func _get_chunk_coord(pos: Vector2) -> Vector2:
return Vector2(
floor(pos.x / chunk_size),
floor(pos.y / chunk_size)
)
func _update_chunks(center: Vector2) -> void:
# 需要加载的区块
var needed_chunks = {}
for x in range(-render_distance, render_distance + 1):
for y in range(-render_distance, render_distance + 1):
var coord = center + Vector2(x, y)
needed_chunks[coord] = true
# 加载新区块
for coord in needed_chunks:
if not active_chunks.has(coord):
_load_chunk(coord)
# 卸载远处区块
for coord in active_chunks.keys():
if not needed_chunks.has(coord):
_unload_chunk(coord)
func _load_chunk(coord: Vector2) -> void:
var chunk = chunk_scene.instance()
chunk.position = coord * chunk_size
chunk.init(coord)
add_child(chunk)
active_chunks[coord] = chunk
func _unload_chunk(coord: Vector2) -> void:
active_chunks[coord].queue_free()
active_chunks.erase(coord)