☰
5、场景树与节点管理
2026/9/25 11:36:29 网站建设 项目流程

第5章 场景树与节点管理

5.1 场景树结构

场景树是 Godot 的核心数据结构。所有节点按树状组织,从根节点(Viewport)开始。

Root (Window) ├── Main (Node2D) ← 当前场景根节点 │ ├── Player (CharacterBody2D) │ │ ├── Sprite2D │ │ ├── CollisionShape2D │ │ └── Camera2D │ ├── Enemy (CharacterBody2D) │ │ ├── Sprite2D │ │ └── CollisionShape2D │ └── UI (CanvasLayer) │ ├── HealthBar (ProgressBar) │ └── ScoreLabel (Label) └── GameManager (Node) ← Autoload 单例

5.2 节点生命周期

生命周期流程图

节点创建 (new / instantiate) │ ↓ ┌─────────┐ │ _init │ 构造函数,不能访问子节点 └────┬────┘ │ ↓ 加入场景树 ┌──────────────┐ │ _enter_tree │ 进入场景树 └────┬─────────┘ │ ↓ 所有子节点就绪 ┌─────────┐ │ _ready │ ✅ 安全初始化(连接信号、访问子节点) └────┬────┘ │ ↓ 每帧循环 ┌──────────────────┐ │ _process(delta) │ 渲染帧(UI、动画) │ _physics_process │ 物理帧(移动、碰撞) └────┬─────────────┘ │ ↓ 离开场景树 ┌─────────────┐ │ _exit_tree │ 清理资源 └─────────────┘
extends Node2D func _init() -> void: # 1. 构造函数:对象创建时调用 # 不能访问子节点,不能访问场景树 print("_init") func _enter_tree() -> void: # 2. 进入场景树:节点被添加到树中 print("_enter_tree") func _ready() -> void: # 3. 准备就绪:节点和所有子节点都已进入场景树 # ✅ 可以安全访问子节点 # ✅ 可以连接信号 # ✅ 大部分初始化代码放在这里 print("_ready") func _process(delta: float) -> void: # 4. 每帧更新(渲染帧) pass func _physics_process(delta: float) -> void: # 5. 物理更新(物理帧) pass func _exit_tree() -> void: # 6. 离开场景树:节点被移除 # 用于清理资源 print("_exit_tree")

_notification() 底层通知

func _notification(what: int) -> void: match what: NOTIFICATION_ENTER_TREE: print("进入场景树") NOTIFICATION_READY: print("准备就绪") NOTIFICATION_PROCESS: print("每帧通知") NOTIFICATION_PHYSICS_PROCESS: print("物理帧通知") NOTIFICATION_EXIT_TREE: print("离开场景树") NOTIFICATION_PARENTED: print("被添加为子节点") NOTIFICATION_UNPARENTED: print("从父节点移除")

5.3 场景实例化

# 预加载场景资源 var enemy_scene: PackedScene = preload("res://scenes/enemy.tscn") var bullet_scene: PackedScene = preload("res://scenes/bullet.tscn") # 实例化并添加到场景树 func spawn_enemy(pos: Vector2) -> void: var enemy: Node2D = enemy_scene.instantiate() enemy.global_position = pos add_child(enemy) # 添加为当前节点的子节点 # 批量生成 func spawn_wave() -> void: for i in 5: var pos := Vector2(randf_range(100, 1100), randf_range(100, 500)) spawn_enemy(pos) # 实例化到特定父节点 func spawn_bullet(pos: Vector2, dir: Vector2) -> void: var bullet: Node2D = bullet_scene.instantiate() bullet.global_position = pos bullet.direction = dir get_tree().current_scene.add_child(bullet) # 添加到当前场景根节点

实例化与 Autoload 的区别

# 场景实例化:创建新的独立副本 var enemy1 = enemy_scene.instantiate() # 独立副本 var enemy2 = enemy_scene.instantiate() # 另一个独立副本 # enemy1 和 enemy2 是不同的对象,互不影响 # Autoload:全局单例,只有一个 # GameManager.gd (Autoload) # 在任何地方访问都是同一个对象 GameManager.add_score(100)

5.4 Autoload 单例

Autoload 是全局可用的节点,在项目启动时自动加载。

设置

项目 → 项目设置 → 全局(Autoload) 路径:res://scripts/game_manager.gd 名称:GameManager 启用:✅

常见用法

# game_manager.gd - 游戏全局管理器 extends Node signal score_changed(new_score: int) signal game_over var score: int = 0 var lives: int = 3 var current_level: int = 1 func add_score(amount: int) -> void: score += amount score_changed.emit(score) func lose_life() -> void: lives -= 1 if lives <= 0: game_over.emit() func restart_game() -> void: score = 0 lives = 3 current_level = 1 get_tree().change_scene_to_file("res://scenes/levels/level_01.tscn")
# 任何脚本中都可以访问 extends Node2D func _ready() -> void: GameManager.score_changed.connect(_on_score_changed) func _on_enemy_died() -> void: GameManager.add_score(100) # 全局调用 func _on_score_changed(new_score: int) -> void: $ScoreLabel.text = "分数:%d" % new_score

5.5 组(Group)

组用于将分散在场景树中的节点归类管理。

# 添加到组 enemy.add_to_group("enemies") pickup.add_to_group("pickups") boss.add_to_group("enemies") # 可以属于多个组 boss.add_to_group("bosses") # 在编辑器中设置 # 选中节点 → 检查器 → Node → Groups # 获取组内所有节点 for enemy in get_tree().get_nodes_in_group("enemies"): enemy.take_damage(10) # 检查节点是否在组中 if node.is_in_group("enemies"): print("这是敌人") # 从组中移除 node.remove_from_group("enemies") # 组内所有节点调用方法 get_tree().call_group("enemies", "take_damage", 10) get_tree().call_group("pickups", "collect")

5.6 节点查找

# 使用路径查找(需要知道路径) var sprite: Sprite2D = $Sprite2D # 直接子节点 var nested: Node = $UI/HealthBar/Value # 嵌套路径 var parent: Node = get_parent() # 父节点 # 使用 get_node(等价于 $) var sprite: Sprite2D = get_node("Sprite2D") # 使用 find_child(递归查找子节点) var sprite: Sprite2D = find_child("Sprite2D", true, false) # 使用 has_node 检查是否存在 if has_node("Sprite2D"): var sprite: Sprite2D = $Sprite2D # 使用 SceneUniqueName(% 语法) # 在编辑器中右键节点 → "作为场景唯一名称访问" # 然后在代码中使用 % 访问 @onready var health_bar: ProgressBar = %HealthBar @onready var sprite: Sprite2D = %Sprite

5.7 CallDeferred 延迟调用

# 问题:在遍历节点时删除节点会导致崩溃 # 解决:使用 call_deferred 延迟到当前帧结束后执行 # ❌ 危险:在遍历中直接删除 for enemy in get_tree().get_nodes_in_group("enemies"): if enemy.health <= 0: enemy.queue_free() # queue_free 本身是安全的 # ✅ 安全:延迟调用 func remove_all_enemies() -> void: for enemy in get_tree().get_nodes_in_group("enemies"): enemy.call_deferred("queue_free") # 延迟添加子节点(在 _ready 中添加到父节点) func _ready() -> void: # ❌ 可能有问题:父节点可能还没准备好 # get_parent().add_child(some_node) # ✅ 安全:延迟添加 call_deferred("_add_child_deferred") func _add_child_deferred() -> void: get_parent().add_child(some_node)

5.8 场景组织最佳实践

推荐的目录结构

res:// ├── scenes/ │ ├── characters/ │ │ ├── player.tscn │ │ ├── enemies/ │ │ │ ├── slime.tscn │ │ │ └── bat.tscn │ │ └── npc.tscn │ ├── levels/ │ │ ├── level_01.tscn │ │ └── level_02.tscn │ ├── ui/ │ │ ├── main_menu.tscn │ │ ├── hud.tscn │ │ └── pause_menu.tscn │ └── effects/ │ ├── explosion.tscn │ └── hit_spark.tscn ├── scripts/ │ ├── characters/ │ ├── systems/ │ └── ui/ ├── assets/ │ ├── sprites/ │ ├── audio/ │ ├── fonts/ │ └── shaders/ └── resources/ ├── player_stats.tres └── enemy_data.tres

组件化设计

# HealthComponent.gd - 可复用的生命值组件 extends Node signal health_changed(new_health: int, max_health: int) signal died @export var max_health: int = 100 var health: int func _ready() -> void: health = max_health func take_damage(amount: int) -> void: health = clampi(health - amount, 0, max_health) health_changed.emit(health, max_health) if health <= 0: died.emit() func heal(amount: int) -> void: health = clampi(health + amount, 0, max_health) health_changed.emit(health, max_health)
# 使用组件 # Player (CharacterBody2D) # ├── Sprite2D # ├── CollisionShape2D # └── HealthComponent (Node) ← 挂载 HealthComponent.gd # Player.gd extends CharacterBody2D func _ready() -> void: $HealthComponent.died.connect(_on_died) $HealthComponent.health_changed.connect(_on_health_changed) func _on_died() -> void: play_death_animation() func _on_health_changed(new_health: int, max_health: int) -> void: update_health_ui(new_health, max_health)

实操练习

练习:敌人生成器

extends Node2D @export var enemy_scene: PackedScene @export var spawn_interval: float = 2.0 @export var max_enemies: int = 10 var spawn_timer: float = 0.0 func _process(delta: float) -> void: # 检查当前敌人数量 var enemy_count := get_tree().get_nodes_in_group("enemies").size() if enemy_count >= max_enemies: return # 定时生成 spawn_timer += delta if spawn_timer >= spawn_interval: spawn_timer = 0.0 spawn_enemy() func spawn_enemy() -> void: var enemy: Node2D = enemy_scene.instantiate() enemy.global_position = global_position + Vector2(randf_range(-100, 100), 0) add_child(enemy) enemy.add_to_group("enemies")

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询