第一部分 11:Autoload 单例系统
这一章讲 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 专注自己的职责
比如 Game 或 GameFlow 负责新游戏流程:
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 只放真正全局的游戏状态
节点之间关系由 Main、Signal、Group 管理
错误 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 适合跨场景长期存在的系统。
Game、SceneManager、AudioManager、SaveManager 很适合 Autoload。
Player、Enemy、Chest、NPC 通常不适合直接做 Autoload。
Autoload 不会切场景自动消失。
Autoload 里的数据不会新游戏自动重置,需要自己 reset。
Autoload 可以配合 Signal、Group、Resource 使用。
不要把 Autoload 当全局垃圾桶。
50. 最重要的一句话
Autoload 适合放“全局服务和跨场景状态”,不适合放“所有游戏对象”。
再压缩一下:
跨场景存在:Autoload
当前场景对象:普通 Node
事件通知:Signal
批量查找:Group
配置数据:Resource如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录