第一部分 11:Autoload 单例系统

2026-05-19
487617 分钟
...

这一章讲 Autoload,也就是 Godot 里常说的“单例”。

先用一句话理解:

Autoload = 游戏启动时自动加载全局可访问切换场景也不会消失的节点

它适合放一些“整个游戏都需要”的系统,比如:

GameManager
SaveManager
AudioManager
SceneManager
SettingsManager
EventBus
ItemDatabase
QuestManager

Godot 官方文档里说明,Autoload 可以加载一个场景或一个继承自 Node 的脚本;如果 Autoload 的是脚本,Godot 会创建一个 Node,把脚本挂到这个 Node 上,并且这个节点会在其他场景加载前加入 root viewport。(Godot Engine documentation)

1. Autoload 是什么

正常节点需要放在某个场景里,比如:

Main Node2D
├── Player
├── World
└── UI

如果你切换场景,Main 可能会被卸载。

但是有些系统你不希望它随着场景切换消失。

比如:

玩家金币
游戏设置
音量设置
存档系统
场景切换系统
全局事件系统
物品数据库
任务状态

这些就适合做成 Autoload。

Autoload 节点会挂在:

/root

下面。

比如你注册了一个叫 Game 的 Autoload,运行时大概是:

/root
├── Game
├── Audio
├── Save
└── 当前场景

这样你在任何地方都可以访问:

Game.gold
Audio.play_bgm("town")
Save.save_game()

2. 前端类比

如果你写前端,Autoload 很像:

Pinia / Vuex store
Redux store
全局 service
全局 router
全局 event bus
全局 config

比如:

userStore.userInfo
router.push("/home")
audioService.play("click")
settingsStore.theme

Godot 里可能是:

Game.player_gold
SceneManager.change_scene("town")
AudioManager.play_sfx("click")
SettingsManager.volume

但注意,Autoload 不是让你什么都往里面塞。

它更像“全局服务”,不是“全局垃圾桶”。

3. 为什么需要 Autoload

Godot 的 Scene 很适合拆分和切换,但这也带来一个问题:

场景 A 切换到场景 B场景 A 里的节点会被释放

如果你的金币、背包、任务状态都放在场景 A 的节点里,切场景后就麻烦了。

Autoload 适合解决这些问题:

跨场景保存数据
跨场景调用服务
统一管理音频
统一管理存档
统一管理场景切换
统一保存游戏设置
提供全局数据库
提供全局事件总线

Godot 文档也提到,Autoload 常用于在场景之间保存信息,比如玩家信息,也可以处理场景切换和场景间过渡。(Godot Engine documentation)

4. 怎么创建 Autoload

操作路径:

Project
Project Settings
Globals
Autoload

然后:

1. 选择一个脚本或场景
2. 填写 Autoload 名称
3. 点击 Add
4. 勾选启用

比如你有:

res://scripts/autoload/game.gd

添加名称:

Game

之后代码里就能直接访问:

Game

比如:

Game.gold += 10

Autoload 文档里也说明,配置路径在 Project > Project Settings > Globals > Autoload。(Godot Engine documentation)

5. Autoload 脚本基本写法

比如 game.gd

# res://scripts/autoload/game.gd
extends Node

var gold: int = 0
var current_day: int = 1

func add_gold(amount: int) -> void:
    gold += amount
    print("当前金币:", gold)

注册为 Autoload,名字叫:

Game

之后任何脚本里都可以:

Game.add_gold(100)

或者:

print(Game.gold)

6. Autoload 场景和 Autoload 脚本

Autoload 可以加载:

脚本
场景

Autoload 脚本

比如:

game.gd
save_manager.gd
settings_manager.gd

Godot 会自动创建一个 Node,并把脚本挂上去。

适合纯逻辑系统:

Game
Save
Settings
Events

Autoload 场景

比如:

audio_manager.tscn
scene_manager.tscn

场景里可以有子节点。

例如 AudioManager 场景:

AudioManager Node
├── BGMPlayer AudioStreamPlayer
├── SFXPlayer AudioStreamPlayer
└── AmbiencePlayer AudioStreamPlayer

这种就适合用场景做 Autoload,因为你需要多个子节点协作。

7. Autoload 不是真正“不可复制”的单例

虽然大家常叫它“单例”,但 Godot 文档提醒:Autoload 并不是严格意义上设计模式里的 true singleton;你仍然可以手动实例化它的副本。(Godot Engine documentation)

所以更准确地说:

Autoload 是自动加载到 /root 的全局节点

不是魔法。

不是不可复制对象。

只是 Godot 帮你在游戏启动时自动创建并挂到根节点。

8. Autoload 在场景树里的位置

假设你注册了:

Game
Audio
Save
SceneManager

运行时大概是:

/root
├── Game
├── Audio
├── Save
├── SceneManager
└── Main
    ├── World
    ├── Player
    └── UI

你可以用路径访问:

get_node("/root/Game")

不过更常用的是直接:

Game

Godot 的最佳实践文档也说,Autoload 本质上就是自动作为 scene tree root 的子节点加载,所以也可以通过 /root/AutoloadName 获取。(Godot Engine documentation)

9. Autoload 适合放什么

适合放:

跨场景都存在的数据
跨场景都要用的服务
全局配置
全局事件
数据库式资源索引
存档读写
音频播放
场景切换
任务系统
成就系统

常见:

GameManager
管理游戏状态比如是否暂停当前模式当前金币等

SaveManager
保存和读取存档

AudioManager
播放 BGM音效环境音

SceneManager
切换场景淡入淡出

SettingsManager
音量分辨率语言等设置

EventBus
全局事件信号中心

ItemDatabase
根据 item_id ItemData

QuestManager
任务状态管理

10. Autoload 不适合放什么

不适合放:

玩家移动逻辑
敌人 AI
宝箱打开逻辑
NPC 对话具体行为
某个地图里的局部对象
某个 UI 面板内部状态
一堆临时变量
所有节点引用

比如不要写成:

Game.player_position
Game.player_velocity
Game.current_enemy
Game.current_chest
Game.everything

这会把架构搞成一锅汤。

更好的做法:

Player 自己管移动和当前血量
Enemy 自己管 AI 和当前血量
Chest 自己管是否打开
HUD 自己管显示
Autoload 管跨场景和全局服务

11. 常见 Autoload 设计总览

你的 RPG 项目可以先考虑这些:

Game
游戏全局状态

SceneManager
场景切换和过渡

AudioManager
BGM音效环境音

SaveManager
存档读写

SettingsManager
设置数据

Events
全局事件总线

ItemDatabase
物品数据库

QuestManager
任务状态

但不要一开始全建。

建议初期先建:

Game
AudioManager
SceneManager

等做到存档、物品、任务时,再加:

SaveManager
ItemDatabase
QuestManager

12. Game:全局游戏状态

Game 可以放一些全局状态。

比如:

# res://scripts/autoload/game.gd
extends Node

signal gold_changed(value: int)
signal game_mode_changed(mode: GameMode)

enum GameMode {
    PLAYING,
    DIALOG,
    INVENTORY,
    PAUSED
}

var gold: int = 0
var game_mode: GameMode = GameMode.PLAYING

func add_gold(amount: int) -> void:
    gold += amount
    gold_changed.emit(gold)

func spend_gold(amount: int) -> bool:
    if gold < amount:
        return false

    gold -= amount
    gold_changed.emit(gold)
    return true

func set_game_mode(mode: GameMode) -> void:
    if game_mode == mode:
        return

    game_mode = mode
    game_mode_changed.emit(game_mode)

func is_playing() -> bool:
    return game_mode == GameMode.PLAYING

使用:

Game.add_gold(100)

HUD 监听:

func _ready() -> void:
    Game.gold_changed.connect(_on_gold_changed)

func _on_gold_changed(value: int) -> void:
    $GoldLabel.text = "金币:" + str(value)

13. Game 里适合放什么

适合:

金币
当前游戏模式
是否暂停
当前章节
全局解锁状态
当前难度
玩家跨场景状态摘要

不适合:

玩家每帧移动方向
敌人当前位置
当前攻击动画
某个宝箱节点引用
某个按钮的 visible

简单判断:

切换场景后还要保留吗
多个场景都要访问吗
它是全局游戏状态吗

如果是,可以考虑放 Game。

否则,先放在对应节点里。

14. SceneManager:场景切换管理

场景切换很适合做成 Autoload。

比如:

# res://scripts/autoload/scene_manager.gd
extends Node

var current_scene_path: String = ""

func change_scene(path: String) -> void:
    current_scene_path = path
    get_tree().change_scene_to_file(path)

使用:

SceneManager.change_scene("res://scenes/world/house_inside.tscn")

门:

class_name Door
extends Area2D

@export var target_scene_path: String

func interact() -> void:
    SceneManager.change_scene(target_scene_path)

这样 Door 不需要自己直接处理复杂切场景逻辑。

15. 带淡入淡出的 SceneManager

后面你做 RPG,切场景通常不想硬切。

可以做一个过渡 UI。

如果 SceneManager 是场景 Autoload,可以这样:

SceneManager CanvasLayer
├── FadeRect ColorRect
└── AnimationPlayer

SceneManager.gd:

extends CanvasLayer

@onready var fade_rect: ColorRect = $FadeRect
@onready var animation_player: AnimationPlayer = $AnimationPlayer

var changing_scene: bool = false

func change_scene(path: String) -> void:
    if changing_scene:
        return

    changing_scene = true

    animation_player.play("fade_out")
    await animation_player.animation_finished

    get_tree().change_scene_to_file(path)

    animation_player.play("fade_in")
    await animation_player.animation_finished

    changing_scene = false

这样任何地方都可以:

SceneManager.change_scene("res://scenes/world/town.tscn")

这个比每个门都写一遍淡出动画好太多。

16. AudioManager:全局音频管理

音频管理很适合 Autoload,尤其是 BGM 不应该每次切场景都重置得乱七八糟。

推荐用场景 Autoload:

AudioManager Node
├── BGMPlayer AudioStreamPlayer
├── SFXPlayer AudioStreamPlayer
└── AmbiencePlayer AudioStreamPlayer

AudioManager.gd:

extends Node

@onready var bgm_player: AudioStreamPlayer = $BGMPlayer
@onready var sfx_player: AudioStreamPlayer = $SFXPlayer
@onready var ambience_player: AudioStreamPlayer = $AmbiencePlayer

func play_bgm(stream: AudioStream) -> void:
    if bgm_player.stream == stream and bgm_player.playing:
        return

    bgm_player.stream = stream
    bgm_player.play()

func stop_bgm() -> void:
    bgm_player.stop()

func play_sfx(stream: AudioStream) -> void:
    sfx_player.stream = stream
    sfx_player.play()

func play_ambience(stream: AudioStream) -> void:
    ambience_player.stream = stream
    ambience_player.play()

func stop_ambience() -> void:
    ambience_player.stop()

使用:

AudioManager.play_sfx(preload("res://assets/audio/click.ogg"))

17. 更舒服的 AudioManager:用字典管理音效

可以先预加载常用音效:

extends Node

@onready var sfx_player: AudioStreamPlayer = $SFXPlayer

var sfx_map: Dictionary = {
    "click": preload("res://assets/audio/click.ogg"),
    "hit": preload("res://assets/audio/hit.ogg"),
    "open_chest": preload("res://assets/audio/open_chest.ogg")
}

func play_sfx(id: String) -> void:
    if not sfx_map.has(id):
        push_warning("找不到音效:" + id)
        return

    sfx_player.stream = sfx_map[id]
    sfx_player.play()

使用:

AudioManager.play_sfx("click")
AudioManager.play_sfx("open_chest")

后面音效多了,可以改成 Resource 数据驱动。

18. SaveManager:存档管理

存档也适合 Autoload。

因为存档需要跨场景访问,也需要找所有 saveable 对象。

SaveManager.gd:

extends Node

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

func save_game() -> void:
    var data := {
        "gold": Game.gold,
        "current_scene": SceneManager.current_scene_path,
        "objects": collect_saveable_data()
    }

    var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    file.store_string(JSON.stringify(data))
    file.close()

func load_game() -> void:
    if not FileAccess.file_exists(SAVE_PATH):
        return

    var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
    var text := file.get_as_text()
    file.close()

    var data = JSON.parse_string(text)

    if typeof(data) != TYPE_DICTIONARY:
        return

    Game.gold = data.get("gold", 0)

func collect_saveable_data() -> Array:
    var result := []

    for node in get_tree().get_nodes_in_group("saveable"):
        if node.has_method("get_save_data"):
            result.append(node.get_save_data())

    return result

这里用了 user://

简单记:

res:// 是项目资源
user:// 是用户本地数据,比如存档和设置

19. SettingsManager:设置管理

设置包括:

主音量
音乐音量
音效音量
语言
分辨率
全屏
按键配置

简单版本:

extends Node

signal settings_changed

var master_volume: float = 1.0
var music_volume: float = 1.0
var sfx_volume: float = 1.0
var language: String = "zh_CN"

func set_master_volume(value: float) -> void:
    master_volume = clamp(value, 0.0, 1.0)
    settings_changed.emit()

func set_music_volume(value: float) -> void:
    music_volume = clamp(value, 0.0, 1.0)
    settings_changed.emit()

func set_sfx_volume(value: float) -> void:
    sfx_volume = clamp(value, 0.0, 1.0)
    settings_changed.emit()

设置保存:

const SETTINGS_PATH := "user://settings.json"

func save_settings() -> void:
    var data := {
        "master_volume": master_volume,
        "music_volume": music_volume,
        "sfx_volume": sfx_volume,
        "language": language
    }

    var file := FileAccess.open(SETTINGS_PATH, FileAccess.WRITE)
    file.store_string(JSON.stringify(data))
    file.close()

20. EventBus:全局事件总线

有时候你需要一个全局信号中心。

可以建:

Events

Events.gd:

extends Node

signal player_died
signal quest_completed(quest_id: String)
signal item_collected(item_id: String, amount: int)
signal weather_changed(weather: String)
signal scene_changed(scene_path: String)

发事件:

Events.quest_completed.emit("first_quest")

监听:

func _ready() -> void:
    Events.quest_completed.connect(_on_quest_completed)

func _on_quest_completed(quest_id: String) -> void:
    print("任务完成:", quest_id)

21. EventBus 什么时候适合用

适合:

很多系统都可能关心的事件
跨场景事件
发出者和接收者不方便直接引用
全局状态变化

比如:

任务完成
成就解锁
天气变化
玩家死亡
场景切换完成
全局设置变化

22. EventBus 什么时候不适合用

不适合:

Player 内部动画
宝箱内部打开逻辑
按钮点击自己的 UI 回调
一个父节点和一个子节点之间的简单通信
局部场景内只有两三个对象的通信

不要所有东西都:

Events.something.emit()

那会让关系变得很隐形。

如果你在前端里见过全项目到处乱发 event bus,大概懂那个味儿:能跑,但找 bug 像考古。

23. ItemDatabase:物品数据库

前面 Resource 章讲过 ItemData

如果有很多物品,可以做一个全局数据库。

ItemDatabase.gd:

extends Node

@export var items: Array[ItemData] = []

var item_map: Dictionary = {}

func _ready() -> void:
    build_map()

func build_map() -> void:
    item_map.clear()

    for item in items:
        if item == null:
            continue

        if item.id == "":
            push_warning("发现没有 id 的 ItemData")
            continue

        item_map[item.id] = item

func get_item(id: String) -> ItemData:
    return item_map.get(id)

func has_item(id: String) -> bool:
    return item_map.has(id)

注册 Autoload 名字:

ItemDatabase

使用:

var item_data = ItemDatabase.get_item("potion")

背包存档只保存:

item_id
amount

显示 UI 时再通过 ItemDatabase 找回资源。

24. QuestManager:任务管理

任务状态适合放全局管理器。

简单版本:

extends Node

signal quest_started(quest_id: String)
signal quest_completed(quest_id: String)

var active_quests: Array[String] = []
var completed_quests: Array[String] = []

func start_quest(quest_id: String) -> void:
    if active_quests.has(quest_id):
        return

    if completed_quests.has(quest_id):
        return

    active_quests.append(quest_id)
    quest_started.emit(quest_id)

func complete_quest(quest_id: String) -> void:
    if not active_quests.has(quest_id):
        return

    active_quests.erase(quest_id)
    completed_quests.append(quest_id)

    quest_completed.emit(quest_id)

使用:

QuestManager.start_quest("find_sword")
QuestManager.complete_quest("find_sword")

HUD 可以监听:

func _ready() -> void:
    QuestManager.quest_completed.connect(_on_quest_completed)

func _on_quest_completed(quest_id: String) -> void:
    print("显示任务完成提示:", quest_id)

25. Autoload 和普通节点怎么选

用普通节点的情况

这个对象只属于当前场景。

比如:

Player
NPC
Enemy
Chest
Door
TileMapLayer
HUD
DialogBox
WeatherEffect

这些通常是普通节点。

用 Autoload 的情况

这个对象跨场景都需要,并且应该长期存在。

比如:

SaveManager
AudioManager
SceneManager
SettingsManager
QuestManager
ItemDatabase
Events

简单判断:

切换场景后它还应该存在吗
很多场景都需要访问它吗
它是全局服务或全局状态吗

如果是,考虑 Autoload。

26. 不要把 Player 做成 Autoload

初学者很容易想:

玩家到处都需要 Player Autoload

不太推荐。

Player 通常是场景里的实体对象:

它有位置
它有碰撞
它在不同地图里出生点不同
它和当前地图强相关

更推荐:

Player 节点仍然放在当前场景
Game PlayerState 保存跨场景状态
SceneManager 负责切场景后把玩家放到出生点

比如:

# Game.gd
var player_health: int = 100
var player_gold: int = 0
var spawn_point_id: String = "default"

Player 进入新场景后读取:

func _ready() -> void:
    health = Game.player_health

这样 Player 仍然是场景实体,Game 保存跨场景数据。

27. 不要把所有节点引用都存在 Autoload

不推荐:

# Game.gd
var player
var hud
var world
var chest
var npc
var enemy
var camera

这会让 Autoload 变成全局指针仓库。

问题:

节点可能已经被释放
切场景后引用失效
依赖关系混乱
调试困难

更好的方式:

局部场景里的引用交给 Main
跨场景状态交给 Game
全局服务交给 Manager
临时查找可以用 Group

28. Autoload 初始化顺序

Autoload 会在普通场景之前加载。

所以你可以在主场景 _ready() 里访问:

Game
AudioManager
SaveManager

但是要注意:

Autoload _ready 可能早于当前场景的节点

所以在 Autoload 的 _ready() 里不要假设当前场景里的 Player、HUD、World 已经存在。

不推荐:

# Game.gd
func _ready() -> void:
    var player = get_tree().current_scene.get_node("Player")

因为这时候 current_scene 可能还不是你以为的场景。

更推荐:

Main 场景 ready把需要的关系连接起来

比如:

# Main.gd
func _ready() -> void:
    Game.gold_changed.connect($UI/HUD.update_gold)

29. Autoload 的生命周期

Autoload 通常在游戏运行期间一直存在。

它不会因为你切换当前场景而消失。

所以适合:

音频播放持续存在
设置持续存在
任务状态持续存在
存档系统持续存在
全局事件系统持续存在

但也意味着:

Autoload 里的变量不会自动重置

如果你开始新游戏,需要自己重置。

比如:

# Game.gd
func reset_for_new_game() -> void:
    gold = 0
    current_day = 1
    game_mode = GameMode.PLAYING

新游戏按钮:

func _on_new_game_pressed() -> void:
    Game.reset_for_new_game()
    SceneManager.change_scene("res://scenes/world/start_map.tscn")

30. 开始新游戏时要重置 Autoload

这是常见坑。

假设你上一局金币是 999。

回主菜单,点新游戏。

如果没有重置:

新游戏还是 999 金币

所以全局状态管理器要有 reset。

比如:

# Game.gd
func reset() -> void:
    gold = 0
    current_day = 1
    game_mode = GameMode.PLAYING
    player_health = 100

QuestManager:

func reset() -> void:
    active_quests.clear()
    completed_quests.clear()

Inventory:

func reset() -> void:
    items.clear()

GameManager 可以统一调用:

func start_new_game() -> void:
    Game.reset()
    QuestManager.reset()
    Inventory.reset()
    SceneManager.change_scene("res://scenes/world/start_map.tscn")

31. Autoload 和暂停

如果你用:

get_tree().paused = true

节点是否继续处理,取决于它的 process mode。

Godot 暂停文档里说明,暂停菜单根节点可以设置 Process Mode 为 When Paused,这样暂停时菜单以及子节点仍然会处理输入和逻辑。(Godot Engine documentation)

所以暂停系统常见设计:

游戏世界节点暂停时停止
暂停菜单暂停时继续
AudioManager看需求
SceneManager通常继续
SettingsManager继续

比如 PauseMenu:

Process Mode: When Paused

否则你暂停后,菜单按钮可能无法响应。

32. Autoload 访问当前场景

可以:

get_tree().current_scene

比如 SceneManager:

func reload_current_scene() -> void:
    get_tree().reload_current_scene()

或者:

func get_current_scene_name() -> String:
    if get_tree().current_scene == null:
        return ""

    return get_tree().current_scene.name

但不要让 Autoload 频繁深入操作当前场景内部节点:

get_tree().current_scene.get_node("World/Enemies/Slime")

这种很脆。

如果必须通信,更推荐:

Signal
Group
Main 负责连接
明确传参

33. Autoload 和 Signal 配合

Autoload 很适合发全局 signal。

比如 Game.gd:

extends Node

signal gold_changed(value: int)

var gold: int = 0

func add_gold(amount: int) -> void:
    gold += amount
    gold_changed.emit(gold)

HUD.gd:

func _ready() -> void:
    Game.gold_changed.connect(_on_gold_changed)

func _on_gold_changed(value: int) -> void:
    $GoldLabel.text = "金币:" + str(value)

这样 HUD 不需要被 Game 直接调用。

Game 只负责通知金币变化。

34. Autoload 和 Group 配合

Autoload 也常通过 Group 找节点。

比如 WeatherManager:

func set_weather(weather: String) -> void:
    get_tree().call_group("weather_affected", "apply_weather", weather)

SaveManager:

func collect_save_data() -> Array:
    var result := []

    for node in get_tree().get_nodes_in_group("saveable"):
        if node.has_method("get_save_data"):
            result.append(node.get_save_data())

    return result

GameManager:

func pause_gameplay() -> void:
    get_tree().call_group("pauseable", "pause_gameplay")

这套组合很好用:

Autoload 负责全局管理
Group 负责找到一批对象
Signal 负责事件通知

35. Autoload 和 Resource 配合

Autoload 常用来管理 Resource 数据库。

比如 ItemDatabase:

extends Node

@export var items: Array[ItemData] = []
var item_map: Dictionary = {}

func _ready() -> void:
    for item in items:
        item_map[item.id] = item

func get_item(id: String) -> ItemData:
    return item_map.get(id)

QuestDatabase:

extends Node

@export var quests: Array[QuestData] = []
var quest_map: Dictionary = {}

func _ready() -> void:
    for quest in quests:
        quest_map[quest.id] = quest

func get_quest(id: String) -> QuestData:
    return quest_map.get(id)

这样 Resource 负责配置,Autoload 负责索引和查询。

36. 常见架构:Main 负责局部,Autoload 负责全局

推荐你记住这句话:

当前场景内部关系 Main
跨场景长期存在的系统 Autoload

比如 Main.gd:

extends Node2D

@onready var player: Player = $Player
@onready var hud: HUD = $UI/HUD
@onready var weather_manager: WeatherManager = $Systems/WeatherManager

func _ready() -> void:
    player.health_changed.connect(hud.update_health)
    weather_manager.weather_changed.connect(hud.update_weather)

Autoload:

Game
SceneManager
AudioManager
SaveManager
SettingsManager

这样不会所有东西都涌向全局。

37. 推荐的 RPG Autoload 目录

可以这样组织:

res://scripts/autoload/
├── game.gd
├── scene_manager.gd
├── audio_manager.gd
├── save_manager.gd
├── settings_manager.gd
├── events.gd
├── item_database.gd
└── quest_manager.gd

如果某些 Autoload 是场景:

res://scenes/autoload/
├── audio_manager.tscn
└── scene_manager.tscn

注册名称:

Game
SceneManager
AudioManager
SaveManager
SettingsManager
Events
ItemDatabase
QuestManager

38. Autoload 命名建议

注册名建议用 PascalCase:

Game
SceneManager
AudioManager
SaveManager
SettingsManager
Events
ItemDatabase
QuestManager

脚本文件用 snake_case:

game.gd
scene_manager.gd
audio_manager.gd
save_manager.gd
settings_manager.gd
events.gd
item_database.gd
quest_manager.gd

这样代码里读起来舒服:

Game.add_gold(100)
AudioManager.play_sfx("click")
SceneManager.change_scene("res://scenes/world/town.tscn")

39. Autoload 之间可以互相调用吗

可以,但要谨慎。

比如:

func start_new_game() -> void:
    Game.reset()
    QuestManager.reset()
    Inventory.reset()
    SceneManager.change_scene("res://scenes/world/start_map.tscn")

这是可以的。

但不要让依赖变成环:

Game 调用 SaveManager
SaveManager 调用 Game
Game 调用 QuestManager
QuestManager 又调用 Game

不是绝对不行,但容易混乱。

更好的设计:

高层协调者调用多个 Manager
单个 Manager 专注自己的职责

比如 GameGameFlow 负责新游戏流程:

func start_new_game() -> void:
    GameState.reset()
    QuestManager.reset()
    Inventory.reset()
    SceneManager.change_scene("res://scenes/world/start_map.tscn")

40. Autoload 和 current_scene 切换坑

如果你写:

get_tree().change_scene_to_file(path)

切场景不是“你当前函数后面的所有节点都还安全”。

比如当前节点可能马上被释放。

所以切场景后,不要继续操作当前场景里的旧节点。

不推荐:

func go_to_house() -> void:
    get_tree().change_scene_to_file("res://house.tscn")
    $Player.global_position = Vector2(100, 100)

更推荐:

SceneManager 记录目标出生点
新场景 ready 后根据出生点放置玩家

比如:

# SceneManager.gd
var next_spawn_id: String = "default"

func change_scene(path: String, spawn_id: String = "default") -> void:
    next_spawn_id = spawn_id
    get_tree().change_scene_to_file(path)

新场景 Main.gd:

func _ready() -> void:
    place_player_at_spawn(SceneManager.next_spawn_id)

41. 出生点系统例子

地图里放多个出生点:

Town Node2D
├── Player
└── SpawnPoints Node2D
    ├── FromHouse Marker2D
    ├── FromForest Marker2D
    └── Default Marker2D

SceneManager:

extends Node

var next_spawn_id: String = "Default"

func change_scene(path: String, spawn_id: String = "Default") -> void:
    next_spawn_id = spawn_id
    get_tree().change_scene_to_file(path)

Door:

class_name Door
extends Area2D

@export var target_scene_path: String
@export var target_spawn_id: String = "Default"

func interact() -> void:
    SceneManager.change_scene(target_scene_path, target_spawn_id)

Main.gd:

extends Node2D

@onready var player: Player = $Player
@onready var spawn_points: Node2D = $SpawnPoints

func _ready() -> void:
    place_player_at_spawn(SceneManager.next_spawn_id)

func place_player_at_spawn(spawn_id: String) -> void:
    var spawn_point := spawn_points.get_node_or_null(spawn_id) as Marker2D

    if spawn_point == null:
        push_warning("找不到出生点:" + spawn_id)
        return

    player.global_position = spawn_point.global_position

这样从房子出来、从森林回来,都可以落在不同位置。

42. Autoload 和存档读档流程

一个简化流程:

点击继续游戏
SaveManager 读取 user://save.json
恢复 Game 状态
SceneManager 切到存档里的场景
新场景 ready
SaveManager 把对象状态应用到 saveable 节点

伪代码:

# SaveManager.gd
var pending_object_data: Dictionary = {}

func load_game() -> void:
    var data := read_save_file()

    Game.gold = data.get("gold", 0)
    pending_object_data = data.get("objects", {})

    var scene_path: String = data.get("current_scene", "")
    SceneManager.change_scene(scene_path)

func apply_pending_object_data() -> void:
    for node in get_tree().get_nodes_in_group("saveable"):
        if not node.has_method("load_save_data"):
            continue

        var save_id: String = node.save_id

        if pending_object_data.has(save_id):
            node.load_save_data(pending_object_data[save_id])

新场景 Main.gd:

func _ready() -> void:
    SaveManager.apply_pending_object_data()

这就是 Autoload + Group + 存档 ID 的组合。

43. Autoload 常见错误

错误 1:什么都塞进 Game

不推荐:

Game.player
Game.enemy
Game.chest
Game.current_button
Game.dialog_label
Game.weather_particle
Game.camera

这会让 Game 变成“全局杂物间”。

推荐:

Game 只放真正全局的游戏状态
节点之间关系由 MainSignalGroup 管理

错误 2:Autoload 里保存已释放节点引用

切场景后旧节点可能被释放。

如果你保存:

Game.player = $Player

切场景后这个引用可能失效。

更推荐:

需要时通过 Group 找当前 player
或者当前 Main 负责传递引用

比如:

var player = get_tree().get_first_node_in_group("player")

错误 3:新游戏不 reset

Autoload 不会自动重置。

所以新游戏要:

Game.reset()
QuestManager.reset()
Inventory.reset()

错误 4:在 Autoload _ready 里找当前场景节点

不推荐:

func _ready() -> void:
    var player = get_tree().current_scene.get_node("Player")

Autoload 可能比当前场景更早 ready。

错误 5:EventBus 滥用

不推荐所有东西都:

Events.anything.emit()

局部通信优先:

直接调用
Signal
Main 连接

跨场景、跨系统再用 Events。

44. Autoload 使用速查

# 访问 Autoload
Game.add_gold(100)

# 访问 /root 下的 Autoload
var game = get_node("/root/Game")

# Game.gd 示例
extends Node

signal gold_changed(value: int)

var gold: int = 0

func add_gold(amount: int) -> void:
    gold += amount
    gold_changed.emit(gold)

# SceneManager 切场景
SceneManager.change_scene("res://scenes/world/town.tscn")

# AudioManager 播放音效
AudioManager.play_sfx("click")

# SaveManager 保存
SaveManager.save_game()

# ItemDatabase 获取物品
var item = ItemDatabase.get_item("potion")

# Events 全局事件
Events.quest_completed.emit("first_quest")

45. 推荐你当前项目的最小 Autoload 方案

你现在做 2D RPG 初期,不要一下建太多。

先这样就够:

Game
保存金币当前游戏模式玩家跨场景状态

SceneManager
切换场景出生点淡入淡出

AudioManager
播放 BGM音效环境音

等做到物品和存档,再加:

ItemDatabase
SaveManager

等做到任务和复杂系统,再加:

QuestManager
Events
SettingsManager

46. 一个初期可用的 Game.gd

extends Node

signal gold_changed(value: int)
signal game_mode_changed(mode: GameMode)

enum GameMode {
    PLAYING,
    DIALOG,
    INVENTORY,
    PAUSED
}

var gold: int = 0
var game_mode: GameMode = GameMode.PLAYING
var player_health: int = 100
var current_day: int = 1

func reset_for_new_game() -> void:
    gold = 0
    game_mode = GameMode.PLAYING
    player_health = 100
    current_day = 1

    gold_changed.emit(gold)
    game_mode_changed.emit(game_mode)

func add_gold(amount: int) -> void:
    gold += amount
    gold_changed.emit(gold)

func spend_gold(amount: int) -> bool:
    if gold < amount:
        return false

    gold -= amount
    gold_changed.emit(gold)
    return true

func set_game_mode(mode: GameMode) -> void:
    if game_mode == mode:
        return

    game_mode = mode
    game_mode_changed.emit(game_mode)

func is_playing() -> bool:
    return game_mode == GameMode.PLAYING

47. 一个初期可用的 SceneManager.gd

extends Node

var current_scene_path: String = ""
var next_spawn_id: String = "Default"
var changing_scene: bool = false

func change_scene(path: String, spawn_id: String = "Default") -> void:
    if changing_scene:
        return

    changing_scene = true
    current_scene_path = path
    next_spawn_id = spawn_id

    get_tree().change_scene_to_file(path)

    changing_scene = false

后面加淡入淡出时,再升级成场景 Autoload。

48. 一个初期可用的 AudioManager.gd

extends Node

var sfx_map: Dictionary = {
    "click": preload("res://assets/audio/click.ogg"),
    "open_chest": preload("res://assets/audio/open_chest.ogg")
}

var sfx_player: AudioStreamPlayer

func _ready() -> void:
    sfx_player = AudioStreamPlayer.new()
    add_child(sfx_player)

func play_sfx(id: String) -> void:
    if not sfx_map.has(id):
        push_warning("找不到音效:" + id)
        return

    sfx_player.stream = sfx_map[id]
    sfx_player.play()

如果你想更正式一点,可以做成场景:

AudioManager Node
└── SFXPlayer AudioStreamPlayer

这样比代码里 new 更直观。

49. 这一章你先记住这些

Autoload 是启动时自动加载到 /root 的全局节点
Autoload 可以是脚本也可以是场景
Autoload 适合跨场景长期存在的系统
GameSceneManagerAudioManagerSaveManager 很适合 Autoload
PlayerEnemyChestNPC 通常不适合直接做 Autoload
Autoload 不会切场景自动消失
Autoload 里的数据不会新游戏自动重置需要自己 reset
Autoload 可以配合 SignalGroupResource 使用
不要把 Autoload 当全局垃圾桶

50. 最重要的一句话

Autoload 适合放全局服务和跨场景状态”,不适合放所有游戏对象”。

再压缩一下:

跨场景存在Autoload
当前场景对象普通 Node
事件通知Signal
批量查找Group
配置数据Resource

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
godot2026-07-27
Godot 4 常用 UI 节点详解
Godot 4 常用 UI 节点详解 在 Godot 4 中,UI 系统基于 Control(控件) 节点构建。所有 UI 节点都继承自 Control,形成一棵完整的 UI 树。与游戏引擎中常见的"Canvas + DOM"模式不同,Godot 的 UI 系统是声明式的——你在场景中搭好节点树,引擎自动完成布局计算。 一、布局系统:Container 家族 Container 是 Godot UI 的 骨架 。它决定了子节点的大小和位...
学习
godot2026-07-09
Godot 4 自动地形系统(AutoTileSet / Terrains)完全指南
前言 在 2D 游戏开发中,地形瓦片(tile)的拼接是一个绕不开的问题。想象一下:你有一片草地、一条河流、一段平台——如果每一块边缘、角落、过渡区域都要手动选择对应的瓦片图,工作量将是巨大的。 自动地形系统 就是为了解决这个问题而生的。 一、什么是自动地形(Autotiling)? 自动地形的核心思想很简单: 你只管画,引擎帮你选对瓦片 。 当你在 TileMap 上绘制地形时,引擎会自动检测每个瓦片的上下左右邻居,然后根据预设的规则...
学习
godot2026-06-25
Godot 中 zindex 和 ysort 的区别总结
在 Godot 2D 游戏开发中,角色、树木、怪物、地面、技能特效、UI 都需要正确的显示顺序。比如角色走到树前面时,角色应该挡住树;角色走到树后面时,树又应该挡住角色。 这种显示顺序主要和两个概念有关: 和 。 其中 用来手动控制图层顺序, 用来根据物体的 Y 坐标自动排序。 一句话理解 是手动分层。 是根据 Y 坐标自动排序。 简单来说: | 属性 | 作用 | 适合场景 | | | | | | | 数值越大,显示越靠前 | 地面、...
学习
godot2026-06-11
Tileset 资源图的标准和规范
一、什么是 Tileset 资源图 Tileset,中文通常叫“图块资源图”或“瓦片图”,是 2D 游戏中非常常见的一种地图资源组织方式。 简单来说,Tileset 就是把很多小图块按照固定尺寸排列在一张图片里。游戏引擎会按照固定的格子大小去切割这张图片,然后把每个小格子当成一个独立的地图块使用。 比如一个 32×32 像素的 Tileset 中,每一个 tile 都是 32×32 像素。地图编辑器或游戏引擎会按照 32×32 的网格,...
学习
godot2026-05-29
用户角色精灵图制作角色的完整流程
在 2D RPG 游戏里,角色通常不是用一张单独图片完成的,而是用一张“角色精灵图”来做。 所谓角色精灵图,通常是一张包含多个动作帧的大图。比如角色向下走有 4 帧,向左走有 4 帧,向右走有 4 帧,向上走有 4 帧。Godot 会根据这些帧不断切换图片,看起来角色就动起来了。 这篇文章主要介绍:拿到一张角色精灵图之后,如何在 Godot 中把它做成一个可以正常移动、播放动画、和地图产生遮挡关系的角色。 一、先理解角色精灵图是什么 角...
学习
godot2026-05-26
Godot 节点系统详细介绍
Godot 里最核心的东西不是“类”,也不是“组件”,而是 节点 Node 。 你可以把 Godot 的节点理解成: 在前端里,一个页面是由很多 DOM 元素组成的; 在 Godot 里,一个游戏场景是由很多 Node 节点组成的。 比如一个玩家角色,可能不是一个单独对象,而是这样的结构: 这里的 是根节点,下面挂着显示图片、播放动画、碰撞检测、摄像机、音效等子节点。 Godot 官方文档也把节点和场景放在一起讲:多个节点组成树状结构后...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录