第十三章:2D场景系统
"场景是Godot构建游戏世界的基本单元,理解场景系统是掌握引擎的关键。"
从本章开始,我们进入2D游戏开发的实战阶段。首先深入了解Godot的场景系统——这是整个引擎架构的核心。
13.1 场景概念
13.1.1 什么是场景
在Godot中,场景(Scene)是节点的树形集合,可以被保存、加载、实例化和嵌套。
场景的本质:
├── 一个根节点
├── 零个或多个子节点
├── 保存为.tscn或.scn文件
└── 可以被实例化多次
场景的用途:
- 关卡(Level)
- 角色(Character)
- UI界面(UI Screen)
- 可复用组件(Component)
- 整个游戏(Main Scene)
13.1.2 场景文件格式
# .tscn - 文本格式(推荐,便于版本控制)
# .scn - 二进制格式(较小,加载更快)
# tscn文件结构示例
[gd_scene load_steps=3 format=3]
[ext_resource type="Texture2D" path="res://icon.png" id="1"]
[ext_resource type="Script" path="res://player.gd" id="2"]
[node name="Player" type="CharacterBody2D"]
script = ExtResource("2")
speed = 200.0
[node name="Sprite2D" type="Sprite2D" parent="."]
texture = ExtResource("1")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
13.1.3 场景树
场景树是当前运行的所有场景组成的层次结构:
场景树
├── /root (Viewport)
│ ├── Main (主场景)
│ │ ├── World
│ │ │ ├── Player
│ │ │ ├── Enemy
│ │ │ └── TileMap
│ │ └── UI
│ │ ├── HUD
│ │ └── PauseMenu
│ └── Autoloads
│ ├── GameManager
│ └── AudioManager
13.2 创建场景
13.2.1 在编辑器中创建
方法1:新建场景
- 菜单:场景 → 新建场景
- 选择根节点类型
- 添加子节点
- 保存场景(Ctrl+S)
方法2:从选中节点创建
- 选中一个节点及其子节点
- 右键 → 保存分支为场景
- 选择保存位置
根节点选择建议:
| 场景类型 | 推荐根节点 |
|---|---|
| 2D关卡 | Node2D |
| 2D角色 | CharacterBody2D |
| UI界面 | Control/CanvasLayer |
| 纯逻辑 | Node |
13.2.2 通过代码创建
# 创建简单场景
func create_simple_scene() -> Node2D:
var root = Node2D.new()
root.name = "GeneratedScene"
var sprite = Sprite2D.new()
sprite.name = "Sprite"
sprite.texture = preload("res://icon.png")
root.add_child(sprite)
var collision = CollisionShape2D.new()
collision.name = "Collision"
var shape = CircleShape2D.new()
shape.radius = 32
collision.shape = shape
root.add_child(collision)
return root
# 保存场景到文件
func save_scene(scene: Node, path: String) -> void:
var packed = PackedScene.new()
packed.pack(scene)
ResourceSaver.save(packed, path)
13.3 场景实例化
13.3.1 预加载(preload)
# 在编译时加载,推荐用于常用场景
const PlayerScene = preload("res://scenes/player.tscn")
const BulletScene = preload("res://scenes/bullet.tscn")
func spawn_player() -> Player:
var player = PlayerScene.instantiate()
add_child(player)
return player
func fire_bullet(pos: Vector2, dir: Vector2) -> void:
var bullet = BulletScene.instantiate()
bullet.position = pos
bullet.direction = dir
get_parent().add_child(bullet)
13.3.2 运行时加载(load)
# 在运行时加载,用于动态内容
func load_level(level_name: String) -> void:
var path = "res://levels/%s.tscn" % level_name
var scene = load(path)
if scene:
var level = scene.instantiate()
add_child(level)
# 异步加载(避免卡顿)
func load_level_async(level_name: String) -> void:
var path = "res://levels/%s.tscn" % level_name
ResourceLoader.load_threaded_request(path)
func _process(delta):
var path = "res://levels/level1.tscn"
var status = ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
var scene = ResourceLoader.load_threaded_get(path)
var level = scene.instantiate()
add_child(level)
13.3.3 实例化选项
# 基本实例化
var instance = scene.instantiate()
# 指定编辑状态
var instance = scene.instantiate(PackedScene.GEN_EDIT_STATE_DISABLED)
# GEN_EDIT_STATE_DISABLED - 禁用编辑状态(默认)
# GEN_EDIT_STATE_INSTANCE - 保留实例化信息
# GEN_EDIT_STATE_MAIN - 作为主场景
# 实例化后设置属性
var enemy = EnemyScene.instantiate()
enemy.name = "Enemy_%d" % enemy_count
enemy.position = spawn_point
enemy.health = 100
add_child(enemy)
13.4 场景切换
13.4.1 直接切换
# 切换到场景文件
func goto_level(level_path: String) -> void:
get_tree().change_scene_to_file(level_path)
# 切换到PackedScene
func goto_scene(scene: PackedScene) -> void:
get_tree().change_scene_to_packed(scene)
# 示例
func _on_start_button_pressed():
get_tree().change_scene_to_file("res://scenes/game.tscn")
func _on_quit_button_pressed():
get_tree().change_scene_to_file("res://scenes/main_menu.tscn")
13.4.2 带过渡效果的切换
# scene_manager.gd (Autoload)
extends CanvasLayer
@onready var animation_player: AnimationPlayer = $AnimationPlayer
@onready var color_rect: ColorRect = $ColorRect
var next_scene_path: String = ""
func change_scene(path: String) -> void:
next_scene_path = path
animation_player.play("fade_out")
func _on_animation_finished(anim_name: String) -> void:
if anim_name == "fade_out":
get_tree().change_scene_to_file(next_scene_path)
animation_player.play("fade_in")
# 使用
func _ready():
SceneManager.change_scene("res://scenes/level2.tscn")
13.4.3 场景栈管理
# scene_stack.gd (Autoload)
extends Node
var scene_stack: Array[String] = []
var current_scene: Node = null
func push_scene(path: String) -> void:
if current_scene:
scene_stack.append(current_scene.scene_file_path)
get_tree().change_scene_to_file(path)
func pop_scene() -> void:
if scene_stack.size() > 0:
var prev_path = scene_stack.pop_back()
get_tree().change_scene_to_file(prev_path)
# 使用
func open_settings():
SceneStack.push_scene("res://scenes/settings.tscn")
func close_settings():
SceneStack.pop_scene()
13.5 场景组织
13.5.1 场景继承
# 基础场景: base_enemy.tscn
# - Enemy (CharacterBody2D)
# - Sprite2D
# - CollisionShape2D
# - HealthComponent
# 派生场景: goblin.tscn(继承base_enemy.tscn)
# 在编辑器中:场景 → 新建继承的场景
# 选择base_enemy.tscn作为基础
# 脚本继承
# goblin.gd
extends "res://scripts/base_enemy.gd"
func _ready():
super._ready()
health = 50 # 覆盖基础值
13.5.2 场景组合
# 组合优于继承
# player.tscn
# - Player (CharacterBody2D)
# - Sprite2D
# - CollisionShape2D
# - HealthComponent.tscn (实例化)
# - WeaponSlot.tscn (实例化)
# - InventoryComponent.tscn (实例化)
# 组件场景可以被多个角色复用
13.5.3 推荐的文件结构
project/
├── scenes/
│ ├── main.tscn # 主场景
│ ├── levels/ # 关卡场景
│ │ ├── level_01.tscn
│ │ └── level_02.tscn
│ ├── characters/ # 角色场景
│ │ ├── player.tscn
│ │ └── enemies/
│ │ ├── goblin.tscn
│ │ └── orc.tscn
│ ├── objects/ # 游戏对象
│ │ ├── items/
│ │ └── projectiles/
│ └── ui/ # UI场景
│ ├── hud.tscn
│ └── menus/
├── scripts/
├── assets/
└── project.godot
13.6 场景通信
13.6.1 父子通信
# 父节点调用子节点
func _ready():
var player = $Player
player.set_health(100)
player.move_to(Vector2(100, 200))
# 子节点调用父节点
func _ready():
var parent = get_parent()
if parent.has_method("on_child_ready"):
parent.on_child_ready(self)
13.6.2 信号通信
# 子节点发送信号
# player.gd
signal died
signal health_changed(new_health: int)
func take_damage(amount: int):
health -= amount
health_changed.emit(health)
if health <= 0:
died.emit()
# 父节点接收信号
# level.gd
func _ready():
$Player.died.connect(_on_player_died)
$Player.health_changed.connect(_on_player_health_changed)
func _on_player_died():
show_game_over()
func _on_player_health_changed(new_health: int):
$UI/HealthBar.value = new_health
13.6.3 分组通信
# 添加到分组
func _ready():
add_to_group("enemies")
add_to_group("damageable")
# 向分组广播
func kill_all_enemies():
get_tree().call_group("enemies", "die")
func damage_all(amount: int):
for node in get_tree().get_nodes_in_group("damageable"):
if node.has_method("take_damage"):
node.take_damage(amount)
# 分组信号
func notify_enemies():
get_tree().call_group("enemies", "on_player_spotted", player_position)
13.7 场景生命周期
13.7.1 节点回调顺序
extends Node2D
func _init():
print("1. _init - 对象创建")
func _enter_tree():
print("2. _enter_tree - 进入场景树")
func _ready():
print("3. _ready - 节点就绪")
# 所有子节点都已_ready
func _process(delta):
print("4. _process - 每帧调用")
func _physics_process(delta):
print("5. _physics_process - 物理帧调用")
func _exit_tree():
print("6. _exit_tree - 退出场景树")
13.7.2 子节点初始化顺序
# 子节点的_ready在父节点之前调用
# 但_enter_tree是父节点先调用
# 场景树:
# Parent
# ├── Child1
# └── Child2
# _enter_tree顺序: Parent → Child1 → Child2
# _ready顺序: Child1 → Child2 → Parent
13.7.3 延迟初始化
# 等待一帧
func _ready():
# 某些操作需要等待场景完全加载
await get_tree().process_frame
initialize_game()
# 使用call_deferred
func _ready():
# 延迟到当前帧结束时调用
setup.call_deferred()
func setup():
# 安全的初始化操作
pass
13.8 实际案例
13.8.1 关卡加载系统
# level_manager.gd (Autoload)
extends Node
signal level_loading_started
signal level_loading_progress(progress: float)
signal level_loaded
signal level_unloaded
var current_level: Node = null
var level_container: Node = null
func _ready():
level_container = Node.new()
level_container.name = "LevelContainer"
add_child(level_container)
func load_level(path: String) -> void:
level_loading_started.emit()
# 卸载当前关卡
if current_level:
await unload_current_level()
# 异步加载新关卡
ResourceLoader.load_threaded_request(path)
while true:
var progress = []
var status = ResourceLoader.load_threaded_get_status(path, progress)
level_loading_progress.emit(progress[0])
if status == ResourceLoader.THREAD_LOAD_LOADED:
break
elif status == ResourceLoader.THREAD_LOAD_FAILED:
push_error("关卡加载失败:" + path)
return
await get_tree().process_frame
var scene = ResourceLoader.load_threaded_get(path)
current_level = scene.instantiate()
level_container.add_child(current_level)
level_loaded.emit()
func unload_current_level() -> void:
if current_level:
current_level.queue_free()
current_level = null
level_unloaded.emit()
await get_tree().process_frame
13.8.2 对象池系统
# object_pool.gd
class_name ObjectPool
extends Node
var _scene: PackedScene
var _pool: Array[Node] = []
var _active: Array[Node] = []
func _init(scene: PackedScene, initial_size: int = 10):
_scene = scene
for i in range(initial_size):
var obj = _scene.instantiate()
obj.set_process(false)
obj.hide()
_pool.append(obj)
func get_object() -> Node:
var obj: Node
if _pool.size() > 0:
obj = _pool.pop_back()
else:
obj = _scene.instantiate()
obj.set_process(true)
obj.show()
_active.append(obj)
if not obj.is_inside_tree():
add_child(obj)
return obj
func return_object(obj: Node) -> void:
if obj in _active:
_active.erase(obj)
obj.set_process(false)
obj.hide()
_pool.append(obj)
func return_all() -> void:
for obj in _active.duplicate():
return_object(obj)
# 使用示例
var bullet_pool: ObjectPool
func _ready():
var bullet_scene = preload("res://scenes/bullet.tscn")
bullet_pool = ObjectPool.new(bullet_scene, 50)
add_child(bullet_pool)
func fire():
var bullet = bullet_pool.get_object()
bullet.position = muzzle.global_position
bullet.direction = aim_direction
bullet.pool = bullet_pool # 让子弹能够归还自己
本章小结
本章我们深入学习了Godot的2D场景系统:
- 场景概念:场景是节点树,可保存、加载、复用
- 创建场景:编辑器创建、代码创建
- 场景实例化:preload、load、异步加载
- 场景切换:直接切换、带过渡、场景栈
- 场景组织:继承、组合、文件结构
- 场景通信:父子调用、信号、分组
- 生命周期:回调顺序、延迟初始化
- 实际案例:关卡加载、对象池
场景系统是Godot的核心,掌握它对于开发任何游戏都至关重要。下一章我们将深入学习节点树架构。
上一章:错误处理与调试
下一章:节点树架构