强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Godot 3 GDScript 教程 / Godot 3 GDScript 教程(十三):场景切换与数据持久化

场景切换与数据持久化

场景切换决定了玩家在不同关卡、菜单之间的体验流畅度,而数据持久化确保了玩家的进度和设置在退出后得以保留。


场景切换 change_scene

# 基本切换
get_tree().change_scene("res://scenes/level_2.tscn")

# 带淡入淡出的切换
extends Node

var is_transitioning: bool = false

func change_scene_with_fade(scene_path: String, fade_time: float = 0.5):
    if is_transitioning:
        return
    is_transitioning = true

    var fade = ColorRect.new()
    fade.color = Color(0, 0, 0, 0)
    fade.set_anchors_and_margins_preset(Control.PRESET_WIDE)
    get_tree().root.add_child(fade)

    var tween = Tween.new()
    add_child(tween)

    # 淡出
    tween.interpolate_property(fade, "color:a", 0.0, 1.0, fade_time)
    tween.start()
    yield(tween, "tween_all_completed")

    get_tree().change_scene(scene_path)
    yield(get_tree(), "idle_frame")

    # 淡入
    tween.interpolate_property(fade, "color:a", 1.0, 0.0, fade_time)
    tween.start()
    yield(tween, "tween_all_completed")

    fade.queue_free()
    tween.queue_free()
    is_transitioning = false

异步加载大场景

var loader: ResourceInteractiveLoader = null

func start_async_load(scene_path: String):
    loader = ResourceLoader.load_interactive(scene_path)
    set_process(true)

func _process(delta):
    if loader == null:
        return
    var err = loader.poll()
    if err == ERR_FILE_EOF:
        var resource = loader.get_resource()
        loader = null
        set_process(false)
        get_tree().change_scene_to(resource)
    elif err == OK:
        var progress = float(loader.get_stage()) / float(loader.get_stage_count())
        $LoadingBar.value = progress * 100

⚠️ 注意change_scene 会销毁当前整个场景树。需要保留的数据请保存到 Autoload 中。


Autoload 全局数据

配置:Project → Project Settings → Autoload
路径: res://scripts/game_manager.gd
名称: GameManager
# game_manager.gd(Autoload)
extends Node

var player_name: String = ""
var player_hp: int = 100
var player_max_hp: int = 100
var player_gold: int = 0
var current_level: int = 1
var unlocked_levels: Array = [1]

# 游戏设置
var master_volume: float = 1.0
var music_volume: float = 0.8
var fullscreen: bool = false

signal player_data_changed()

func add_gold(amount: int):
    player_gold += amount
    emit_signal("player_data_changed")

func take_damage(amount: int):
    player_hp = max(0, player_hp - amount)
    emit_signal("player_data_changed")

# 任意场景中访问:GameManager.player_hp

存档系统(File/JSON)

# save_manager.gd(Autoload)
extends Node

const SAVE_PATH = "user://save_game.json"

func save_game():
    var save_data = {
        "version": 1,
        "timestamp": OS.get_datetime(),
        "player": {
            "hp": GameManager.player_hp,
            "gold": GameManager.player_gold,
            "level": GameManager.current_level
        },
        "settings": {
            "master_volume": GameManager.master_volume,
            "fullscreen": GameManager.fullscreen
        }
    }

    var file = File.new()
    var err = file.open(SAVE_PATH, File.WRITE)
    if err != OK:
        push_error("保存失败: " + str(err))
        return
    file.store_string(JSON.print(save_data, "\t"))
    file.close()

func load_game() -> bool:
    var file = File.new()
    if not file.file_exists(SAVE_PATH):
        return false
    file.open(SAVE_PATH, File.READ)
    var json = JSON.new()
    var result = json.parse(file.get_as_text())
    file.close()

    if result.error != OK:
        return false

    var data = result.result
    GameManager.player_hp = data["player"]["hp"]
    GameManager.player_gold = data["player"]["gold"]
    GameManager.current_level = data["player"]["level"]
    return true

func has_save() -> bool:
    return File.new().file_exists(SAVE_PATH)

func delete_save():
    var dir = Directory.new()
    if dir.file_exists(SAVE_PATH):
        dir.remove(SAVE_PATH)

多存档槽位

func get_save_path(slot: int) -> String:
    return "user://save_slot_%d.json" % slot

func get_all_slots_info() -> Array:
    var slots = []
    for i in range(1, 4):
        var exists = File.new().file_exists(get_save_path(i))
        slots.append({"slot": i, "exists": exists})
    return slots

⚠️ 注意user:// 路径在不同平台上位置不同(Windows 在 %APPDATA%,Linux 在 ~/.local/share/godot/)。不要使用 res://,打包后它是只读的。


ConfigFile

ConfigFile 适用于保存设置和简单的键值对数据。

func save_settings():
    var config = ConfigFile.new()
    config.set_value("audio", "master", GameManager.master_volume)
    config.set_value("audio", "music", GameManager.music_volume)
    config.set_value("video", "fullscreen", GameManager.fullscreen)
    config.save("user://settings.cfg")

func load_settings():
    var config = ConfigFile.new()
    if config.load("user://settings.cfg") != OK:
        return
    GameManager.master_volume = config.get_value("audio", "master", 1.0)
    GameManager.fullscreen = config.get_value("video", "fullscreen", false)
    OS.window_fullscreen = GameManager.fullscreen

ConfigFile vs JSON 对比

特性ConfigFileJSON
格式INI 样式JSON 格式
嵌套支持有限(Section-Key)完整支持
适用场景游戏设置、简单数据复杂存档、结构化数据

存档加密

const ENCRYPTION_KEY = "MyGameSecretKey2026"

func save_encrypted(data: Dictionary):
    var json_str = JSON.print(data)
    var encrypted = _xor_encrypt(json_str.to_utf8(), ENCRYPTION_KEY.to_utf8())

    var file = File.new()
    file.open("user://save_encrypted.dat", File.WRITE)
    file.store_var(json_str.hash())  # 校验和
    file.store_buffer(encrypted)
    file.close()

func _xor_encrypt(input: PoolByteArray, key: PoolByteArray) -> PoolByteArray:
    var output = PoolByteArray()
    for i in range(input.size()):
        output.append(input[i] ^ key[i % key.size()])
    return output

数据迁移策略

# 存档版本迁移(老存档升级)
const CURRENT_VERSION = 3

func migrate_save(data: Dictionary) -> Dictionary:
    var version = data.get("version", 1)
    if version == 1:
        data["player"]["exp"] = 0  # v2: 添加经验值
        data["version"] = 2
        version = 2
    if version == 2:
        data["inventory"] = []  # v3: inventory 格式变更
        data["version"] = 3
    return data

💡 提示:始终在存档中保留版本号字段。每次修改存档格式时,编写一个迁移函数。


游戏进度管理

extends Node

var completed_levels := {}

func complete_level(level_id: int):
    completed_levels[level_id] = true

func is_level_unlocked(level_id: int) -> bool:
    if level_id == 1:
        return true
    return completed_levels.get(level_id - 1, false)

func get_level_scene(level_id: int) -> String:
    return "res://scenes/levels/level_%02d.tscn" % level_id

func go_to_next_level():
    var next = GameManager.current_level + 1
    var scene = get_level_scene(next)
    var file = File.new()
    if file.file_exists(scene):
        GameManager.current_level = next
        get_tree().change_scene(scene)
    else:
        get_tree().change_scene("res://scenes/main_menu.tscn")

游戏开发场景

自动保存

func _notification(what):
    if what == MainLoop.NOTIFICATION_WM_FOCUS_OUT:
        SaveManager.save_game()
    if what == MainLoop.NOTIFICATION_WM_QUIT_REQUEST:
        SaveManager.save_game()
        get_tree().quit()

场景间数据传递

# Autoload 中存储过渡数据
var scene_data := {}

func transition_to(scene_path: String, data: Dictionary = {}):
    scene_data = data
    get_tree().change_scene(scene_path)

# 目标场景中读取
func _ready():
    var data = GameManager.scene_data
    if "spawn_point" in data:
        $Player.position = data["spawn_point"]
    GameManager.scene_data = {}

扩展阅读

💡 总结:Autoload + JSON 是最常用的数据持久化组合。始终为存档添加版本号,方便后续数据迁移。user:// 是跨平台存档的标准路径。