第十三章:2D场景系统

第十三章: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:新建场景

  1. 菜单:场景 → 新建场景
  2. 选择根节点类型
  3. 添加子节点
  4. 保存场景(Ctrl+S)

方法2:从选中节点创建

  1. 选中一个节点及其子节点
  2. 右键 → 保存分支为场景
  3. 选择保存位置

根节点选择建议:

场景类型推荐根节点
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场景系统:

  1. 场景概念:场景是节点树,可保存、加载、复用
  2. 创建场景:编辑器创建、代码创建
  3. 场景实例化:preload、load、异步加载
  4. 场景切换:直接切换、带过渡、场景栈
  5. 场景组织:继承、组合、文件结构
  6. 场景通信:父子调用、信号、分组
  7. 生命周期:回调顺序、延迟初始化
  8. 实际案例:关卡加载、对象池

场景系统是Godot的核心,掌握它对于开发任何游戏都至关重要。下一章我们将深入学习节点树架构。


上一章:错误处理与调试

下一章:节点树架构

← 返回目录