第3章 全局系统与自动加载
全局系统与自动加载是游戏框架的核心基础设施,它们负责管理跨场景共享的数据和服务,协调系统初始化顺序,并提供统一的访问接口。本章将详细介绍如何在 Godot C# 中设计和实现这些关键系统。
3.1.1 AutoLoad设计原理
Godot的AutoLoad机制是游戏开发中最基础也是最重要的架构模式之一。理解其设计原理有助于正确构建全局系统。
为什么需要全局系统
游戏开发中常见的全局需求:
1. 跨场景状态保持
- 玩家数据需要在不同关卡间传递
- 游戏设置需要在整个游戏生命周期内有效
- 成就进度需要持久化
2. 资源管理
- 音频管理器需要全局访问
- 输入系统需要持续监听
- 网络连接需要保持
3. 事件协调
- 全局事件总线需要跨场景工作
- 成就系统需要监听各种游戏事件
- 分析系统需要追踪玩家行为
AutoLoad vs 静态单例
两种实现全局系统的方式对比:
| 特性 | AutoLoad | 静态单例 |
|---|---|---|
| 生命周期 | 随场景树管理 | 随应用程序生命周期 |
| Godot功能 | 完整使用Godot API | 受限(无节点树访问) |
| 初始化时机 | _EnterTree/_Ready | 首次访问时 |
| 调试友好 | 可在编辑器查看 | 需要日志调试 |
| 测试难度 | 需要场景 | 可直接实例化 |
推荐:优先使用AutoLoad,除非是纯数据类(如配置常量)。
AutoLoad的初始化顺序
AutoLoad节点的加载顺序至关重要:
初始化顺序:
1. 服务容器(ServiceProvider)
2. 事件总线(EventBus)
3. 资源管理器(ResourceManager)
4. 音频管理器(AudioManager)
5. 输入管理器(InputManager)
6. 游戏状态管理器(GameStateManager)
设计原则:被依赖的节点先初始化。
AutoLoad的依赖管理
复杂的依赖关系需要仔细设计:
// 不推荐:循环依赖
// AudioManager 依赖 Settings
// Settings 依赖 AudioManager(获取默认音量)
// 推荐:分层依赖
// Core(无依赖)
// ├── Settings
// ├── EventBus
// └── Logger
// Services(依赖Core)
// ├── AudioManager(依赖Settings, EventBus)
// ├── InputManager
// └── ResourceManager
延迟初始化策略
某些服务可能不需要立即初始化:
public partial class AchievementManager : Node
{
private bool _isInitialized = false;
public override void _Ready()
{
// 延迟到实际需要时初始化
// 可以监听游戏开始事件
EventBus.Instance.GameStarted += OnGameStarted;
}
private void OnGameStarted()
{
if (!_isInitialized)
{
InitializeAchievements();
_isInitialized = true;
}
}
}
3.1.2 服务容器设计思路
服务容器(Service Container)是实现依赖注入的核心组件,它将对象的创建和使用分离。
服务定位器 vs 依赖注入
两种依赖管理模式的对比:
服务定位器模式:
// 主动获取依赖
public class Player
{
private IAudioService _audio = ServiceLocator.GetService<IAudioService>();
}
优点:
- 使用简单,不需要配置
- 向后兼容现有代码
缺点:
- 隐藏了类的依赖关系
- 难以进行单元测试
- 运行时错误风险
依赖注入模式:
// 被动接收依赖
public class Player
{
private IAudioService _audio;
public Player(IAudioService audio)
{
_audio = audio;
}
}
优点:
- 依赖关系显式可见
- 易于Mock测试
- 编译期类型检查
缺点:
- 需要配置容器
- 增加代码复杂度
Godot中的服务容器设计
结合Godot特性的服务容器设计:
public interface IGameService
{
void Initialize();
void Shutdown();
}
public partial class ServiceProvider : Node
{
private Dictionary<Type, IGameService> _services = new();
// 注册服务
public void Register<T>(T service) where T : class, IGameService
{
_services[typeof(T)] = service;
service.Initialize();
}
// 获取服务
public T GetService<T>() where T : class, IGameService
{
if (_services.TryGetValue(typeof(T), out var service))
return (T)service;
throw new Exception($"Service {typeof(T)} not found");
}
// 尝试获取服务(安全)
public bool TryGetService<T>(out T service) where T : class, IGameService
{
if (_services.TryGetValue(typeof(T), out var s))
{
service = (T)s;
return true;
}
service = null;
return false;
}
}
服务生命周期管理
不同服务需要不同的生命周期策略:
单例服务(游戏期间一直存在):
public class AudioManager : IGameService, IDisposable
{
public void Initialize() { /* 初始化音频系统 */ }
public void Shutdown() { /* 释放资源 */ }
public void Dispose() { Shutdown(); }
}
场景服务(随场景变化):
public class LevelManager : IGameService
{
public void Initialize()
{
SceneManager.Instance.SceneChanged += OnSceneChanged;
}
private void OnSceneChanged(string sceneName)
{
// 新场景加载时重新初始化
LoadLevelData(sceneName);
}
}
按需服务(延迟加载):
public class CloudSaveManager : IGameService
{
private bool _isInitialized = false;
public void EnsureInitialized()
{
if (!_isInitialized)
{
Initialize();
_isInitialized = true;
}
}
}
3.1.3 跨场景数据持久化策略
跨场景数据持久化是全局系统的核心需求之一。
持久化需求分类
| 数据类型 | 持久化需求 | 存储位置 |
|---|---|---|
| 玩家进度 | 长期(跨游戏会话) | 本地文件/云端 |
| 游戏设置 | 长期 | 本地配置 |
| 当前关卡 | 短期(场景间) | 内存/静态变量 |
| 临时状态 | 即时(当前会话) | AutoLoad节点 |
内存级持久化
最简单的跨场景数据传递:
// 不推荐:静态变量
public static class GameData
{
public static int PlayerHealth;
public static int CurrentLevel;
}
// 推荐:AutoLoad + 数据对象
public partial class GameState : Node
{
public PlayerData CurrentPlayer { get; set; }
public LevelData CurrentLevel { get; set; }
public void SaveCheckpoint()
{
_lastCheckpoint = new GameCheckpoint
{
PlayerData = CurrentPlayer.Clone(),
LevelData = CurrentLevel.Clone()
};
}
}
文件级持久化
需要长期保存的数据使用文件存储:
public class SaveSystem : IGameService
{
private string SaveDirectory =>
Path.Combine(OS.GetUserDataDir(), "Saves");
public void SaveGame(string slotName)
{
var saveData = new SaveData
{
Timestamp = DateTime.Now,
PlayerState = ServiceProvider.Instance.GetService<GameState>().CurrentPlayer,
Inventory = ServiceProvider.Instance.GetService<InventoryManager>().GetAllItems(),
UnlockedLevels = ProgressManager.Instance.UnlockedLevels
};
string json = JsonSerializer.Serialize(saveData, new JsonSerializerOptions
{
WriteIndented = true
});
string path = Path.Combine(SaveDirectory, $"{slotName}.json");
File.WriteAllText(path, json);
}
}
自动保存策略
减少玩家进度丢失的自动保存机制:
- 检查点自动保存:到达检查点时自动保存
- 定时自动保存:每5-10分钟自动保存
- 事件触发保存:重要事件后保存(如获得稀有物品)
- 后台保存:不阻塞游戏线程的异步保存
public class AutoSaveManager : Node
{
[Export] public float AutoSaveInterval { get; set; } = 300f; // 5分钟
private float _timer = 0f;
public override void _Process(double delta)
{
_timer += (float)delta;
if (_timer >= AutoSaveInterval)
{
PerformAutoSave();
_timer = 0f;
}
}
private async void PerformAutoSave()
{
// 异步保存避免卡顿
await Task.Run(() => SaveSystem.Instance.SaveGame("autosave"));
GD.Print("[AutoSave] 自动保存完成");
}
}
3.1 AutoLoad 服务容器设计
3.1.1 AutoLoad 概念和配置
Godot 的 AutoLoad(自动加载)功能允许我们将节点设置为在场景树中自动实例化,这些节点会在游戏启动时创建,并在整个游戏生命周期中保持存在。这使得 AutoLoad 节点成为实现单例服务容器的理想选择。
项目配置步骤:
- 在 Godot 编辑器中,点击
项目->项目设置->自动加载标签 - 点击文件夹图标选择要自动加载的脚本或场景
- 设置节点名称(通常以 “Global” 或 “Manager” 为后缀)
- 勾选
启用复选框 - 点击
添加按钮
配置注意事项:
- AutoLoad 节点的添加顺序决定了它们在场景树中的初始化顺序
- 排在后面的 AutoLoad 可以依赖前面已初始化的 AutoLoad
- 建议将服务容器类(如 ServiceProvider)放在第一个位置
3.1.2 单例模式实现
在 Godot C# 中,我们可以通过静态属性和 _Ready 方法来实现单例模式。
// 文件路径: Scripts/Core/ServiceProvider.cs
using Godot;
using System;
using System.Collections.Generic;
namespace GameFramework.Core
{
/// <summary>
/// 服务提供者 - 全局服务容器
/// 作为所有管理器的中央注册表,提供依赖注入和服务定位功能
/// </summary>
public partial class ServiceProvider : Node
{
// 步骤1: 定义静态实例字段
private static ServiceProvider _instance;
// 步骤2: 定义公共静态属性,提供全局访问点
public static ServiceProvider Instance
{
get
{
// 注意: 如果在游戏启动前访问,会抛出异常
if (_instance == null)
{
GD.PushError("ServiceProvider 尚未初始化!确保它已配置为 AutoLoad");
}
return _instance;
}
}
// 步骤3: 创建服务注册表,用于存储已注册的服务
private Dictionary<Type, object> _services = new Dictionary<Type, object>();
// 步骤4: 创建服务初始化顺序列表
private List<IService> _initializedServices = new List<IService>();
// 步骤5: 在 _Ready 中设置单例实例
public override void _Ready()
{
base._Ready();
// 注意: 确保只有一个实例存在
if (_instance != null)
{
GD.PushWarning("ServiceProvider 存在多个实例!这是设计错误。");
QueueFree(); // 销毁重复的实例
return;
}
_instance = this;
GD.Print("[ServiceProvider] 服务容器已初始化");
}
// 步骤6: 注册服务方法
/// <summary>
/// 注册服务实例
/// </summary>
/// <typeparam name="T">服务接口类型</typeparam>
/// <param name="implementation">服务实现实例</param>
public void RegisterService<T>(T implementation) where T : class
{
Type serviceType = typeof(T);
// 注意: 检查是否已存在相同类型的服务
if (_services.ContainsKey(serviceType))
{
GD.PushWarning($"[ServiceProvider] 服务 {serviceType.Name} 已被注册,将被覆盖");
_services.Remove(serviceType);
}
_services[serviceType] = implementation;
// 如果服务实现了 IService 接口,调用初始化
if (implementation is IService service)
{
service.Initialize();
_initializedServices.Add(service);
}
GD.Print($"[ServiceProvider] 服务已注册: {serviceType.Name}");
}
// 步骤7: 获取服务方法
/// <summary>
/// 获取已注册的服务
/// </summary>
/// <typeparam name="T">服务接口类型</typeparam>
/// <returns>服务实例,如果不存在则返回 null</returns>
public T GetService<T>() where T : class
{
Type serviceType = typeof(T);
if (_services.TryGetValue(serviceType, out object service))
{
return service as T;
}
GD.PushError($"[ServiceProvider] 未找到服务: {serviceType.Name}");
return null;
}
// 步骤8: 检查服务是否已注册
public bool HasService<T>() where T : class
{
return _services.ContainsKey(typeof(T));
}
// 步骤9: 游戏退出时清理
public override void _Notification(int what)
{
// 注意: NOTIFICATION_WM_CLOSE_REQUEST 在窗口关闭时触发
if (what == NotificationWMCloseRequest)
{
Shutdown();
}
}
/// <summary>
/// 关闭所有服务并清理资源
/// </summary>
private void Shutdown()
{
GD.Print("[ServiceProvider] 正在关闭服务...");
// 按相反顺序关闭服务
for (int i = _initializedServices.Count - 1; i >= 0; i--)
{
_initializedServices[i].Shutdown();
}
_initializedServices.Clear();
_services.Clear();
_instance = null;
}
}
/// <summary>
/// 服务接口 - 所有需要初始化的服务都应实现此接口
/// </summary>
public interface IService
{
/// <summary>
/// 初始化服务
/// </summary>
void Initialize();
/// <summary>
/// 关闭服务,释放资源
/// </summary>
void Shutdown();
}
}
3.1.3 服务容器设计
服务容器是整个框架的核心,它负责管理所有全局服务的生命周期。下面展示如何基于 ServiceProvider 构建具体的管理器。
// 文件路径: Scripts/Core/IManager.cs
namespace GameFramework.Core
{
/// <summary>
/// 管理器接口 - 定义所有管理器的通用契约
/// </summary>
public interface IManager : IService
{
/// <summary>
/// 管理器优先级,数值越小越早初始化
/// </summary>
int Priority { get; }
/// <summary>
/// 管理器是否已就绪
/// </summary>
bool IsReady { get; }
}
}
// 文件路径: Scripts/Core/ServiceInitializer.cs
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
namespace GameFramework.Core
{
/// <summary>
/// 服务初始化器 - 自动注册和初始化所有管理器
/// 必须配置为 AutoLoad,且在 ServiceProvider 之后加载
/// </summary>
public partial class ServiceInitializer : Node
{
// 步骤1: 定义需要自动注册的管理器列表
[Export]
public Godot.Collections.Array<NodePath> AutoLoadManagers { get; set; } =
new Godot.Collections.Array<NodePath>();
public override void _Ready()
{
base._Ready();
// 步骤2: 确保 ServiceProvider 已就绪
if (ServiceProvider.Instance == null)
{
GD.PushError("[ServiceInitializer] ServiceProvider 未初始化!");
return;
}
// 步骤3: 从配置中查找管理器节点并注册
InitializeManagers();
GD.Print("[ServiceInitializer] 所有管理器初始化完成");
}
private void InitializeManagers()
{
// 步骤4: 收集所有实现了 IManager 接口的节点
List<IManager> managers = new List<IManager>();
// 从 AutoLoad 路径中查找
foreach (var path in AutoLoadManagers)
{
if (path == null) continue;
Node node = GetNodeOrNull<Node>(path);
if (node is IManager manager)
{
managers.Add(manager);
}
}
// 步骤5: 从当前场景树中查找其他管理器
foreach (Node child in GetTree().Root.GetChildren())
{
if (child is IManager manager && !managers.Contains(manager))
{
managers.Add(manager);
}
}
// 步骤6: 按优先级排序(数值小的先初始化)
managers = managers.OrderBy(m => m.Priority).ToList();
// 步骤7: 注册所有管理器
foreach (var manager in managers)
{
RegisterManager(manager);
}
}
private void RegisterManager(IManager manager)
{
// 注意: 使用管理器的具体类型进行注册
Type managerType = manager.GetType();
// 获取或创建 RegisterService 的泛型方法
var method = typeof(ServiceProvider).GetMethod("RegisterService");
var genericMethod = method.MakeGenericMethod(managerType);
genericMethod.Invoke(ServiceProvider.Instance, new object[] { manager });
GD.Print($"[ServiceInitializer] 已注册管理器: {managerType.Name} (优先级: {manager.Priority})");
}
}
}
3.2 跨场景数据持久化方案
3.2.1 场景切换时的数据保持
在 Godot 中,场景切换默认会销毁当前场景中的所有节点。为了保持跨场景的数据,我们需要设计专门的数据持久化方案。
// 文件路径: Scripts/Data/IDataPersist.cs
namespace GameFramework.Data
{
/// <summary>
/// 数据持久化接口 - 定义可保存/加载的数据契约
/// </summary>
public interface IDataPersist
{
/// <summary>
/// 获取持久化数据对象
/// </summary>
object GetPersistentData();
/// <summary>
/// 从持久化数据对象恢复
/// </summary>
/// <param name="data">持久化数据</param>
void LoadPersistentData(object data);
}
}
// 文件路径: Scripts/Managers/DataManager.cs
using Godot;
using System;
using System.Collections.Generic;
using GameFramework.Core;
namespace GameFramework.Managers
{
/// <summary>
/// 数据管理器 - 管理游戏中所有需要持久化的数据
/// 作为 AutoLoad 节点存在,跨场景保持数据
/// </summary>
public partial class DataManager : Node, IManager
{
// 步骤1: 实现 IManager 接口
public int Priority => 0; // 最高优先级,最早初始化
public bool IsReady { get; private set; }
// 步骤2: 定义数据存储字典
// Key: 数据类型全名, Value: 序列化后的数据
private Dictionary<string, string> _persistentData = new Dictionary<string, string>();
// 步骤3: 定义场景过渡数据存储(用于场景间临时传递数据)
private Dictionary<string, object> _transitionData = new Dictionary<string, object>();
// 步骤4: 定义自动保存间隔(秒)
[Export]
public float AutoSaveInterval { get; set; } = 300f; // 默认5分钟
private double _timeSinceLastSave = 0;
public override void _Ready()
{
base._Ready();
// 注意: 防止场景切换时重复创建
if (GetTree().Root.GetChildCount() > 0 &&
GetTree().Root.GetChild(0) != this &&
GetTree().Root.GetChild(0) is DataManager)
{
QueueFree();
return;
}
}
// 步骤5: 初始化接口实现
public void Initialize()
{
LoadGameData();
IsReady = true;
GD.Print("[DataManager] 数据管理器初始化完成");
}
// 步骤6: 每帧检查自动保存
public override void _Process(double delta)
{
if (AutoSaveInterval > 0)
{
_timeSinceLastSave += delta;
if (_timeSinceLastSave >= AutoSaveInterval)
{
AutoSave();
_timeSinceLastSave = 0;
}
}
}
/// <summary>
/// 保存实现了 IDataPersist 接口的对象数据
/// </summary>
/// <param name="persistObject">需要保存数据的对象</param>
public void SaveData(IDataPersist persistObject)
{
// 步骤7: 获取对象类型作为键
string key = persistObject.GetType().FullName;
// 步骤8: 序列化数据为 JSON
object data = persistObject.GetPersistentData();
string json = JSON.Stringify(data);
// 步骤9: 存储到字典
_persistentData[key] = json;
GD.Print($"[DataManager] 数据已保存: {key}");
}
/// <summary>
/// 加载数据到实现了 IDataPersist 接口的对象
/// </summary>
/// <param name="persistObject">需要加载数据的对象</param>
/// <returns>是否成功加载</returns>
public bool LoadData(IDataPersist persistObject)
{
string key = persistObject.GetType().FullName;
// 步骤10: 检查是否存在保存的数据
if (!_persistentData.ContainsKey(key))
{
GD.Print($"[DataManager] 未找到保存的数据: {key}");
return false;
}
// 步骤11: 解析 JSON 并恢复数据
string json = _persistentData[key];
var data = JSON.Parse(json);
if (data.Error != Error.Ok)
{
GD.PushError($"[DataManager] 解析数据失败: {key}, 错误: {data.Error}");
return false;
}
// 步骤12: 将数据加载到对象
persistObject.LoadPersistentData(data.Result);
GD.Print($"[DataManager] 数据已加载: {key}");
return true;
}
/// <summary>
/// 设置场景过渡数据(用于场景切换时临时传递数据)
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="key">数据键</param>
/// <param name="data">数据值</param>
public void SetTransitionData<T>(string key, T data)
{
_transitionData[key] = data;
}
/// <summary>
/// 获取场景过渡数据
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="key">数据键</param>
/// <returns>数据值,如果不存在返回默认值</returns>
public T GetTransitionData<T>(string key)
{
if (_transitionData.TryGetValue(key, out object value) && value is T typedValue)
{
// 注意: 获取后可以选择删除,使数据只能被消费一次
// _transitionData.Remove(key);
return typedValue;
}
return default(T);
}
/// <summary>
/// 消费场景过渡数据(获取后自动删除)
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="key">数据键</param>
/// <returns>数据值</returns>
public T ConsumeTransitionData<T>(string key)
{
T value = GetTransitionData<T>(key);
_transitionData.Remove(key);
return value;
}
/// <summary>
/// 清除所有场景过渡数据
/// </summary>
public void ClearTransitionData()
{
_transitionData.Clear();
GD.Print("[DataManager] 场景过渡数据已清除");
}
// 步骤13: 自动保存实现
private void AutoSave()
{
SaveGameData();
GD.Print("[DataManager] 自动保存完成");
}
/// <summary>
/// 保存游戏数据到文件
/// </summary>
public void SaveGameData()
{
// 注意: 使用用户数据目录存储存档
string savePath = GetSaveFilePath();
// 序列化所有数据
var saveData = new Godot.Collections.Dictionary<string, string>(_persistentData);
string json = saveData.ToString();
// 使用 FileAccess 写入文件
using var file = FileAccess.Open(savePath, FileAccess.ModeFlags.Write);
if (file != null)
{
file.StoreString(json);
GD.Print($"[DataManager] 游戏数据已保存到: {savePath}");
}
else
{
GD.PushError($"[DataManager] 保存游戏数据失败: {FileAccess.GetOpenError()}");
}
}
/// <summary>
/// 从文件加载游戏数据
/// </summary>
public void LoadGameData()
{
string savePath = GetSaveFilePath();
// 检查文件是否存在
if (!FileAccess.FileExists(savePath))
{
GD.Print("[DataManager] 未找到存档文件,将创建新存档");
return;
}
// 读取文件内容
using var file = FileAccess.Open(savePath, FileAccess.ModeFlags.Read);
if (file == null)
{
GD.PushError($"[DataManager] 读取存档失败: {FileAccess.GetOpenError()}");
return;
}
string json = file.GetAsText();
var data = JSON.Parse(json);
if (data.Error == Error.Ok && data.Result is Godot.Collections.Dictionary dict)
{
_persistentData.Clear();
foreach (var key in dict.Keys)
{
_persistentData[key.ToString()] = dict[key].ToString();
}
GD.Print("[DataManager] 游戏数据加载完成");
}
else
{
GD.PushError("[DataManager] 解析存档数据失败");
}
}
/// <summary>
/// 获取存档文件路径
/// </summary>
/// <returns>存档文件完整路径</returns>
private string GetSaveFilePath()
{
// 注意: 使用 GetUserDir() 获取用户数据目录,确保跨平台兼容性
string userDir = OS.GetUserDataDir();
return System.IO.Path.Combine(userDir, "savegame.json");
}
// 步骤14: 关闭接口实现
public void Shutdown()
{
SaveGameData();
_persistentData.Clear();
_transitionData.Clear();
IsReady = false;
GD.Print("[DataManager] 数据管理器已关闭");
}
}
}
3.2.2 数据传递模式
// 文件路径: Scripts/Data/SceneDataPackage.cs
using Godot;
using System.Collections.Generic;
namespace GameFramework.Data
{
/// <summary>
/// 场景数据包 - 用于场景间传递复杂数据
/// </summary>
public class SceneDataPackage
{
// 步骤1: 定义数据存储字典
private Dictionary<string, object> _data = new Dictionary<string, object>();
// 步骤2: 定义场景进入参数
public string TargetScenePath { get; set; }
public string EntryPointId { get; set; }
public Vector2? PlayerPosition { get; set; }
/// <summary>
/// 添加数据到包
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="key">数据键</param>
/// <param name="value">数据值</param>
public void Set<T>(string key, T value)
{
_data[key] = value;
}
/// <summary>
/// 从包中获取数据
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="key">数据键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>数据值</returns>
public T Get<T>(string key, T defaultValue = default)
{
if (_data.TryGetValue(key, out object value) && value is T typedValue)
{
return typedValue;
}
return defaultValue;
}
/// <summary>
/// 检查是否包含指定键
/// </summary>
/// <param name="key">数据键</param>
/// <returns>是否包含</returns>
public bool Contains(string key)
{
return _data.ContainsKey(key);
}
/// <summary>
/// 清除所有数据
/// </summary>
public void Clear()
{
_data.Clear();
TargetScenePath = null;
EntryPointId = null;
PlayerPosition = null;
}
}
}
3.2.3 存档与加载
// 文件路径: Scripts/Managers/SaveManager.cs
using Godot;
using System;
using System.Collections.Generic;
using System.IO;
using GameFramework.Core;
namespace GameFramework.Managers
{
/// <summary>
/// 存档管理器 - 提供完整的存档/读档功能
/// 支持多个存档槽位和自动备份
/// </summary>
public partial class SaveManager : Node, IManager
{
// 步骤1: 实现 IManager 接口
public int Priority => 1;
public bool IsReady { get; private set; }
// 步骤2: 定义存档配置
[Export]
public int MaxSaveSlots { get; set; } = 5; // 最大存档槽位数
[Export]
public bool EnableAutoBackup { get; set; } = true; // 是否启用自动备份
[Export]
public int MaxBackupCount { get; set; } = 3; // 每个存档的最大备份数
// 步骤3: 定义存档数据结构
[System.Serializable]
public class SaveData
{
public string SaveId { get; set; }
public string SaveName { get; set; }
public DateTime SaveTime { get; set; }
public string GameVersion { get; set; }
public int PlayTimeSeconds { get; set; }
public Godot.Collections.Dictionary<string, string> GameData { get; set; }
// 元数据用于显示
public string SceneName { get; set; }
public string PlayerLevel { get; set; }
}
// 步骤4: 定义存档事件
[Signal]
public delegate void SaveCompletedEventHandler(string savePath);
[Signal]
public delegate void LoadCompletedEventHandler(string savePath);
[Signal]
public delegate void SaveErrorEventHandler(string errorMessage);
private string _saveDirectory;
public void Initialize()
{
// 步骤5: 初始化存档目录
_saveDirectory = Path.Combine(OS.GetUserDataDir(), "Saves");
// 注意: 确保存档目录存在
if (!Directory.Exists(_saveDirectory))
{
Directory.CreateDirectory(_saveDirectory);
}
IsReady = true;
GD.Print($"[SaveManager] 存档管理器初始化完成,存档目录: {_saveDirectory}");
}
/// <summary>
/// 创建新存档
/// </summary>
/// <param name="slotIndex">存档槽位(0-MaxSaveSlots-1)</param>
/// <param name="saveName">存档名称</param>
/// <returns>是否成功</returns>
public bool CreateSave(int slotIndex, string saveName = null)
{
// 步骤6: 验证槽位有效性
if (slotIndex < 0 || slotIndex >= MaxSaveSlots)
{
EmitSignal(SignalName.SaveError, $"无效的存档槽位: {slotIndex}");
return false;
}
// 步骤7: 如果启用备份,先备份现有存档
if (EnableAutoBackup && SaveExists(slotIndex))
{
BackupSave(slotIndex);
}
// 步骤8: 构建存档数据
var saveData = new SaveData
{
SaveId = Guid.NewGuid().ToString(),
SaveName = saveName ?? $"存档 {slotIndex + 1}",
SaveTime = DateTime.Now,
GameVersion = GetGameVersion(),
PlayTimeSeconds = GetPlayTime(),
GameData = CollectGameData(),
SceneName = GetCurrentSceneName(),
PlayerLevel = GetPlayerLevel()
};
// 步骤9: 序列化并保存
string savePath = GetSaveFilePath(slotIndex);
if (WriteSaveFile(savePath, saveData))
{
EmitSignal(SignalName.SaveCompleted, savePath);
GD.Print($"[SaveManager] 存档已创建: {savePath}");
return true;
}
return false;
}
/// <summary>
/// 加载存档
/// </summary>
/// <param name="slotIndex">存档槽位</param>
/// <returns>是否成功</returns>
public bool LoadSave(int slotIndex)
{
// 步骤10: 验证存档存在
if (!SaveExists(slotIndex))
{
EmitSignal(SignalName.SaveError, $"存档不存在: 槽位 {slotIndex}");
return false;
}
// 步骤11: 读取存档文件
string savePath = GetSaveFilePath(slotIndex);
SaveData saveData = ReadSaveFile(savePath);
if (saveData == null)
{
EmitSignal(SignalName.SaveError, $"读取存档失败: {savePath}");
return false;
}
// 步骤12: 验证版本兼容性
if (!IsVersionCompatible(saveData.GameVersion))
{
GD.PushWarning($"[SaveManager] 存档版本不匹配: {saveData.GameVersion} vs {GetGameVersion()}");
}
// 步骤13: 恢复游戏数据
RestoreGameData(saveData.GameData);
EmitSignal(SignalName.LoadCompleted, savePath);
GD.Print($"[SaveManager] 存档已加载: {savePath}");
return true;
}
/// <summary>
/// 删除存档
/// </summary>
/// <param name="slotIndex">存档槽位</param>
/// <returns>是否成功</returns>
public bool DeleteSave(int slotIndex)
{
string savePath = GetSaveFilePath(slotIndex);
if (File.Exists(savePath))
{
File.Delete(savePath);
// 同时删除备份
string backupDir = GetBackupDirectory(slotIndex);
if (Directory.Exists(backupDir))
{
Directory.Delete(backupDir, true);
}
GD.Print($"[SaveManager] 存档已删除: {savePath}");
return true;
}
return false;
}
/// <summary>
/// 检查存档是否存在
/// </summary>
/// <param name="slotIndex">存档槽位</param>
/// <returns>是否存在</returns>
public bool SaveExists(int slotIndex)
{
return File.Exists(GetSaveFilePath(slotIndex));
}
/// <summary>
/// 获取存档信息
/// </summary>
/// <param name="slotIndex">存档槽位</param>
/// <returns>存档数据,如果不存在返回 null</returns>
public SaveData GetSaveInfo(int slotIndex)
{
if (!SaveExists(slotIndex))
return null;
return ReadSaveFile(GetSaveFilePath(slotIndex));
}
/// <summary>
/// 获取所有存档信息
/// </summary>
/// <returns>存档信息列表</returns>
public List<SaveData> GetAllSaveInfo()
{
var saves = new List<SaveData>();
for (int i = 0; i < MaxSaveSlots; i++)
{
saves.Add(GetSaveInfo(i));
}
return saves;
}
/// <summary>
/// 快速存档(使用自动存档槽位)
/// </summary>
/// <returns>是否成功</returns>
public bool QuickSave()
{
return CreateSave(0, "快速存档");
}
/// <summary>
/// 快速读档(从自动存档槽位)
/// </summary>
/// <returns>是否成功</returns>
public bool QuickLoad()
{
return LoadSave(0);
}
// 步骤14: 备份相关方法
private void BackupSave(int slotIndex)
{
string sourcePath = GetSaveFilePath(slotIndex);
string backupDir = GetBackupDirectory(slotIndex);
if (!Directory.Exists(backupDir))
{
Directory.CreateDirectory(backupDir);
}
// 生成备份文件名(带时间戳)
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string backupPath = Path.Combine(backupDir, $"save_{timestamp}.sav");
File.Copy(sourcePath, backupPath, true);
// 清理旧备份
CleanupOldBackups(backupDir);
GD.Print($"[SaveManager] 存档已备份: {backupPath}");
}
private void CleanupOldBackups(string backupDir)
{
var backupFiles = new List<FileInfo>(new DirectoryInfo(backupDir).GetFiles("*.sav"));
backupFiles.Sort((a, b) => b.CreationTime.CompareTo(a.CreationTime)); // 按时间倒序
// 删除超出保留数量的旧备份
for (int i = MaxBackupCount; i < backupFiles.Count; i++)
{
backupFiles[i].Delete();
}
}
// 步骤15: 文件操作方法
private string GetSaveFilePath(int slotIndex)
{
return Path.Combine(_saveDirectory, $"save_{slotIndex}.sav");
}
private string GetBackupDirectory(int slotIndex)
{
return Path.Combine(_saveDirectory, $"backup_{slotIndex}");
}
private bool WriteSaveFile(string path, SaveData data)
{
try
{
// 使用 JSON 序列化
var dict = new Godot.Collections.Dictionary
{
["saveId"] = data.SaveId,
["saveName"] = data.SaveName,
["saveTime"] = data.SaveTime.ToString("O"),
["gameVersion"] = data.GameVersion,
["playTimeSeconds"] = data.PlayTimeSeconds,
["gameData"] = data.GameData,
["sceneName"] = data.SceneName,
["playerLevel"] = data.PlayerLevel
};
string json = dict.ToString();
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
if (file != null)
{
file.StoreString(json);
return true;
}
}
catch (Exception ex)
{
GD.PushError($"[SaveManager] 写入存档失败: {ex.Message}");
}
return false;
}
private SaveData ReadSaveFile(string path)
{
try
{
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
if (file == null) return null;
string json = file.GetAsText();
var result = JSON.Parse(json);
if (result.Error != Error.Ok || !(result.Result is Godot.Collections.Dictionary dict))
return null;
return new SaveData
{
SaveId = dict["saveId"].AsString(),
SaveName = dict["saveName"].AsString(),
SaveTime = DateTime.Parse(dict["saveTime"].AsString()),
GameVersion = dict["gameVersion"].AsString(),
PlayTimeSeconds = dict["playTimeSeconds"].AsInt32(),
SceneName = dict.ContainsKey("sceneName") ? dict["sceneName"].AsString() : "",
PlayerLevel = dict.ContainsKey("playerLevel") ? dict["playerLevel"].AsString() : ""
};
}
catch (Exception ex)
{
GD.PushError($"[SaveManager] 读取存档失败: {ex.Message}");
}
return null;
}
// 步骤16: 辅助方法
private string GetGameVersion()
{
// 从项目设置或其他配置获取版本号
return (string)ProjectSettings.GetSetting("application/config/version") ?? "1.0.0";
}
private int GetPlayTime()
{
// 从 TimeManager 或其他系统获取游戏时间
return 0;
}
private string GetCurrentSceneName()
{
var sceneManager = ServiceProvider.Instance?.GetService<ISceneManager>();
return sceneManager?.CurrentScene?.Name ?? "Unknown";
}
private string GetPlayerLevel()
{
// 从 PlayerManager 或其他系统获取玩家等级
return "1";
}
private Godot.Collections.Dictionary<string, string> CollectGameData()
{
// 收集所有需要保存的游戏数据
var data = new Godot.Collections.Dictionary<string, string>();
// 触发数据收集事件,让各个系统添加自己的数据
// EmitSignal(SignalName.CollectingSaveData, data);
return data;
}
private void RestoreGameData(Godot.Collections.Dictionary<string, string> data)
{
// 恢复游戏数据到各个系统
// EmitSignal(SignalName.RestoringSaveData, data);
}
private bool IsVersionCompatible(string saveVersion)
{
// 实现版本兼容性检查逻辑
return saveVersion == GetGameVersion();
}
public void Shutdown()
{
IsReady = false;
GD.Print("[SaveManager] 存档管理器已关闭");
}
}
}
3.3 启动流程控制与初始化顺序
3.3.1 游戏启动流程设计
// 文件路径: Scripts/Core/GameBoot.cs
using Godot;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using GameFramework.Managers;
namespace GameFramework.Core
{
/// <summary>
/// 游戏启动器 - 控制整个游戏的启动流程
/// 这是第一个被加载的 AutoLoad 节点
/// </summary>
public partial class GameBoot : Node
{
// 步骤1: 定义启动阶段枚举
public enum BootPhase
{
None = 0,
PreInitialize = 1, // 预初始化
CoreServices = 2, // 核心服务
DataLoading = 3, // 数据加载
ContentLoading = 4, // 内容加载
PostInitialize = 5, // 后初始化
Complete = 6 // 完成
}
// 步骤2: 定义启动配置
[ExportGroup("启动配置")]
[Export]
public string InitialScene { get; set; } = "res://Scenes/MainMenu.tscn";
[Export]
public bool SkipIntro { get; set; } = false;
[Export]
public bool LoadLastSave { get; set; } = false;
// 步骤3: 定义启动事件
[Signal]
public delegate void BootPhaseChangedEventHandler(BootPhase phase, float progress);
[Signal]
public delegate void BootCompletedEventHandler();
[Signal]
public delegate void BootErrorEventHandler(string error);
// 步骤4: 启动状态
public BootPhase CurrentPhase { get; private set; } = BootPhase.None;
public float BootProgress { get; private set; } = 0f;
public bool IsBooting { get; private set; } = false;
public bool IsComplete { get; private set; } = false;
// 步骤5: 启动任务队列
private Queue<BootTask> _bootTasks = new Queue<BootTask>();
private List<BootTask> _completedTasks = new List<BootTask>();
public override void _Ready()
{
base._Ready();
// 步骤6: 设置进程模式为始终运行(即使在暂停时)
ProcessMode = ProcessModeEnum.Always;
// 步骤7: 开始启动流程
StartBootSequence();
}
/// <summary>
/// 开始启动序列
/// </summary>
private async void StartBootSequence()
{
if (IsBooting) return;
IsBooting = true;
GD.Print("[GameBoot] 游戏启动序列开始...");
try
{
// 步骤8: 按顺序执行各个启动阶段
await ExecutePhase(BootPhase.PreInitialize, InitializePreServices);
await ExecutePhase(BootPhase.CoreServices, InitializeCoreServices);
await ExecutePhase(BootPhase.DataLoading, InitializeDataLoading);
await ExecutePhase(BootPhase.ContentLoading, InitializeContentLoading);
await ExecutePhase(BootPhase.PostInitialize, InitializePostServices);
// 步骤9: 启动完成
await CompleteBoot();
}
catch (Exception ex)
{
GD.PushError($"[GameBoot] 启动失败: {ex.Message}");
EmitSignal(SignalName.BootError, ex.Message);
}
finally
{
IsBooting = false;
}
}
/// <summary>
/// 执行启动阶段
/// </summary>
private async Task ExecutePhase(BootPhase phase, Func<Task> phaseAction)
{
CurrentPhase = phase;
EmitSignal(SignalName.BootPhaseChanged, (int)phase, BootProgress);
GD.Print($"[GameBoot] 进入阶段: {phase}");
if (phaseAction != null)
{
await phaseAction();
}
// 更新进度
BootProgress = (int)phase / (float)BootPhase.Complete;
}
// 步骤10: 各阶段初始化实现
private async Task InitializePreServices()
{
// 初始化日志系统
InitializeLogging();
// 初始化配置系统
InitializeConfiguration();
// 初始化性能监控
InitializePerformanceMonitor();
await Task.CompletedTask;
}
private async Task InitializeCoreServices()
{
// 确保 ServiceProvider 已就绪
if (ServiceProvider.Instance == null)
{
throw new Exception("ServiceProvider 未初始化");
}
// 等待一帧确保其他 AutoLoad 节点已 _Ready
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
}
private async Task InitializeDataLoading()
{
// 加载存档数据
var dataManager = ServiceProvider.Instance.GetService<DataManager>();
if (dataManager != null)
{
// 等待 DataManager 就绪
while (!dataManager.IsReady)
{
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
}
}
// 加载配置数据
await LoadConfigurationData();
// 加载本地化数据
await LoadLocalizationData();
}
private async Task InitializeContentLoading()
{
// 预加载必要资源
await PreloadEssentialResources();
// 初始化对象池
InitializeObjectPools();
}
private async Task InitializePostServices()
{
// 应用用户设置
ApplyUserSettings();
// 初始化输入系统
InitializeInputSystem();
// 初始化音频系统
InitializeAudioSystem();
}
private async Task CompleteBoot()
{
CurrentPhase = BootPhase.Complete;
BootProgress = 1f;
IsComplete = true;
EmitSignal(SignalName.BootPhaseChanged, (int)BootPhase.Complete, 1f);
EmitSignal(SignalName.BootCompleted);
GD.Print("[GameBoot] 游戏启动序列完成");
// 切换到初始场景
await SwitchToInitialScene();
}
/// <summary>
/// 切换到初始场景
/// </summary>
private async Task SwitchToInitialScene()
{
var sceneManager = ServiceProvider.Instance.GetService<ISceneManager>();
if (sceneManager != null)
{
await sceneManager.LoadSceneAsync(InitialScene);
}
else
{
// 如果 SceneManager 不可用,使用默认方式切换场景
GetTree().ChangeSceneToFile(InitialScene);
}
}
// 步骤11: 辅助初始化方法
private void InitializeLogging()
{
// 配置日志系统
GD.Print("[GameBoot] 日志系统初始化");
}
private void InitializeConfiguration()
{
// 加载配置文件
GD.Print("[GameBoot] 配置系统初始化");
}
private void InitializePerformanceMonitor()
{
// 设置性能监控
GD.Print("[GameBoot] 性能监控初始化");
}
private async Task LoadConfigurationData()
{
GD.Print("[GameBoot] 加载配置数据...");
await Task.Delay(100); // 模拟加载延迟
}
private async Task LoadLocalizationData()
{
GD.Print("[GameBoot] 加载本地化数据...");
await Task.Delay(100);
}
private async Task PreloadEssentialResources()
{
GD.Print("[GameBoot] 预加载必要资源...");
await Task.Delay(200);
}
private void InitializeObjectPools()
{
GD.Print("[GameBoot] 对象池初始化");
}
private void ApplyUserSettings()
{
GD.Print("[GameBoot] 应用用户设置");
}
private void InitializeInputSystem()
{
GD.Print("[GameBoot] 输入系统初始化");
}
private void InitializeAudioSystem()
{
GD.Print("[GameBoot] 音频系统初始化");
}
// 步骤12: 启动任务类定义
private class BootTask
{
public string Name { get; set; }
public BootPhase Phase { get; set; }
public Func<Task> Action { get; set; }
public bool IsCompleted { get; set; }
public bool IsOptional { get; set; }
}
}
}
3.3.2 系统初始化顺序管理
// 文件路径: Scripts/Core/InitializationOrder.cs
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
namespace GameFramework.Core
{
/// <summary>
/// 初始化顺序管理器 - 确保系统按正确顺序初始化
/// </summary>
public class InitializationOrder
{
// 步骤1: 定义系统依赖关系
private Dictionary<Type, List<Type>> _dependencies = new Dictionary<Type, List<Type>>();
// 步骤2: 定义系统优先级
private Dictionary<Type, int> _priorities = new Dictionary<Type, int>();
// 步骤3: 已排序的系统列表
private List<Type> _sortedSystems;
/// <summary>
/// 注册系统及其依赖
/// </summary>
/// <typeparam name="T">系统类型</typeparam>
/// <param name="priority">优先级(数值越小越早初始化)</param>
/// <param name="dependencies">依赖的系统类型</param>
public void RegisterSystem<T>(int priority, params Type[] dependencies) where T : IManager
{
Type systemType = typeof(T);
_priorities[systemType] = priority;
_dependencies[systemType] = new List<Type>(dependencies);
// 注意: 注册新系统后需要重新排序
_sortedSystems = null;
}
/// <summary>
/// 获取按正确顺序排列的系统类型列表
/// </summary>
/// <returns>排序后的系统类型列表</returns>
public List<Type> GetOrderedSystems()
{
if (_sortedSystems != null)
return _sortedSystems;
// 步骤4: 使用拓扑排序确保依赖关系正确
_sortedSystems = TopologicalSort();
return _sortedSystems;
}
/// <summary>
/// 拓扑排序算法
/// </summary>
private List<Type> TopologicalSort()
{
var result = new List<Type>();
var visited = new HashSet<Type>();
var visiting = new HashSet<Type>();
// 步骤5: 对所有系统进行拓扑排序
var allSystems = new HashSet<Type>(_priorities.Keys);
foreach (var depList in _dependencies.Values)
{
foreach (var dep in depList)
{
allSystems.Add(dep);
}
}
foreach (var system in allSystems)
{
if (!visited.Contains(system))
{
if (!Visit(system, visited, visiting, result))
{
throw new InvalidOperationException("检测到循环依赖!");
}
}
}
return result;
}
private bool Visit(Type system, HashSet<Type> visited, HashSet<Type> visiting, List<Type> result)
{
// 步骤6: 检测循环依赖
if (visiting.Contains(system))
return false; // 发现循环依赖
if (visited.Contains(system))
return true;
visiting.Add(system);
// 步骤7: 先访问依赖项
if (_dependencies.ContainsKey(system))
{
foreach (var dependency in _dependencies[system])
{
if (!Visit(dependency, visited, visiting, result))
return false;
}
}
visiting.Remove(system);
visited.Add(system);
// 步骤8: 添加当前系统到结果
// 注意: 按优先级插入到正确位置
int insertIndex = result.Count;
int currentPriority = GetPriority(system);
for (int i = 0; i < result.Count; i++)
{
if (GetPriority(result[i]) > currentPriority)
{
insertIndex = i;
break;
}
}
result.Insert(insertIndex, system);
return true;
}
private int GetPriority(Type system)
{
return _priorities.TryGetValue(system, out int priority) ? priority : int.MaxValue;
}
}
/// <summary>
/// 场景管理器接口
/// </summary>
public interface ISceneManager : IManager
{
Node CurrentScene { get; }
System.Threading.Tasks.Task LoadSceneAsync(string path);
void ChangeScene(string path);
}
}
// 文件路径: Scripts/Managers/SceneManager.cs
using Godot;
using System;
using System.Threading.Tasks;
using GameFramework.Core;
using GameFramework.Data;
namespace GameFramework.Managers
{
/// <summary>
/// 场景管理器 - 处理场景切换和过渡效果
/// </summary>
public partial class SceneManager : Node, ISceneManager
{
// 步骤1: 实现 IManager 接口
public int Priority => 5; // 中等优先级
public bool IsReady { get; private set; }
// 步骤2: 定义场景切换配置
[Export]
public PackedScene LoadingScreenScene { get; set; }
[Export]
public float TransitionDuration { get; set; } = 0.5f;
[Export]
public bool UseLoadingScreen { get; set; } = true;
// 步骤3: 场景状态
public Node CurrentScene { get; private set; }
public bool IsTransitioning { get; private set; }
public string CurrentScenePath { get; private set; }
// 步骤4: 事件
[Signal]
public delegate void SceneLoadedEventHandler(string scenePath);
[Signal]
public delegate void SceneUnloadedEventHandler(string scenePath);
[Signal]
public delegate void TransitionStartedEventHandler();
[Signal]
public delegate void TransitionCompletedEventHandler();
// 步骤5: 过渡动画节点
private CanvasLayer _transitionLayer;
private ColorRect _fadeRect;
private AnimationPlayer _animationPlayer;
public void Initialize()
{
// 步骤6: 创建过渡层
CreateTransitionLayer();
// 步骤7: 获取当前场景
UpdateCurrentScene();
IsReady = true;
GD.Print("[SceneManager] 场景管理器初始化完成");
}
/// <summary>
/// 异步加载场景
/// </summary>
/// <param name="path">场景资源路径</param>
/// <param name="transitionData">传递给新场景的数据</param>
public async Task LoadSceneAsync(string path, SceneDataPackage transitionData = null)
{
if (IsTransitioning)
{
GD.PushWarning("[SceneManager] 正在切换场景中,请求被忽略");
return;
}
if (CurrentScenePath == path)
{
GD.PushWarning($"[SceneManager] 已经在场景 {path} 中");
return;
}
IsTransitioning = true;
EmitSignal(SignalName.TransitionStarted);
// 步骤8: 设置场景过渡数据
if (transitionData != null)
{
var dataManager = ServiceProvider.Instance?.GetService<DataManager>();
if (dataManager != null)
{
foreach (var key in transitionData.GetDataKeys())
{
dataManager.SetTransitionData(key, transitionData.Get<object>(key));
}
}
}
// 步骤9: 开始淡出过渡
await FadeOut();
// 步骤10: 显示加载画面(如果需要)
if (UseLoadingScreen && LoadingScreenScene != null)
{
ShowLoadingScreen();
}
// 步骤11: 卸载当前场景
if (CurrentScene != null)
{
EmitSignal(SignalName.SceneUnloaded, CurrentScenePath);
CurrentScene.QueueFree();
CurrentScene = null;
}
// 步骤12: 异步加载新场景
var loadTask = ResourceLoader.LoadThreadedRequest(path);
// 等待加载完成
while (ResourceLoader.LoadThreadedGetStatus(path) == ResourceLoader.ThreadLoadStatus.InProgress)
{
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
}
var status = ResourceLoader.LoadThreadedGetStatus(path);
if (status != ResourceLoader.ThreadLoadStatus.Loaded)
{
GD.PushError($"[SceneManager] 场景加载失败: {path}");
IsTransitioning = false;
return;
}
// 步骤13: 实例化并切换到新场景
var packedScene = ResourceLoader.LoadThreadedGet(path) as PackedScene;
if (packedScene != null)
{
CurrentScene = packedScene.Instantiate();
GetTree().Root.AddChild(CurrentScene);
CurrentScenePath = path;
// 步骤14: 调用新场景的 OnSceneEntered(如果存在)
CallDeferred(nameof(NotifySceneEntered));
}
// 步骤15: 隐藏加载画面
HideLoadingScreen();
// 步骤16: 淡入新场景
await FadeIn();
EmitSignal(SignalName.SceneLoaded, path);
EmitSignal(SignalName.TransitionCompleted);
IsTransitioning = false;
GD.Print($"[SceneManager] 场景切换完成: {path}");
}
/// <summary>
/// 同步切换场景(简单场景切换)
/// </summary>
/// <param name="path">场景资源路径</param>
public void ChangeScene(string path)
{
if (IsTransitioning) return;
Error error = GetTree().ChangeSceneToFile(path);
if (error == Error.Ok)
{
CurrentScenePath = path;
UpdateCurrentScene();
EmitSignal(SignalName.SceneLoaded, path);
}
else
{
GD.PushError($"[SceneManager] 切换场景失败: {error}");
}
}
/// <summary>
/// 重新加载当前场景
/// </summary>
public async Task ReloadCurrentScene()
{
if (!string.IsNullOrEmpty(CurrentScenePath))
{
await LoadSceneAsync(CurrentScenePath);
}
}
// 步骤17: 过渡动画方法
private async Task FadeOut()
{
if (_animationPlayer == null) return;
_transitionLayer.Visible = true;
_animationPlayer.Play("fade_out");
await ToSignal(_animationPlayer, AnimationPlayer.SignalName.AnimationFinished);
}
private async Task FadeIn()
{
if (_animationPlayer == null)
{
_transitionLayer.Visible = false;
return;
}
_animationPlayer.Play("fade_in");
await ToSignal(_animationPlayer, AnimationPlayer.SignalName.AnimationFinished);
_transitionLayer.Visible = false;
}
private void CreateTransitionLayer()
{
// 步骤18: 创建过渡层节点
_transitionLayer = new CanvasLayer
{
Name = "SceneTransitionLayer",
Layer = 100, // 确保在最上层
Visible = false
};
// 创建淡出矩形
_fadeRect = new ColorRect
{
Name = "FadeRect",
Color = Colors.Black,
Size = new Vector2(1920, 1080), // 根据需要调整
MouseFilter = Control.MouseFilterEnum.Ignore
};
_fadeRect.SetAnchorsPreset(Control.LayoutPreset.FullRect);
_transitionLayer.AddChild(_fadeRect);
// 创建动画播放器
_animationPlayer = new AnimationPlayer();
_transitionLayer.AddChild(_animationPlayer);
// 步骤19: 创建默认过渡动画
CreateDefaultAnimations();
AddChild(_transitionLayer);
}
private void CreateDefaultAnimations()
{
// 创建淡出动画
var fadeOutAnim = new Animation();
fadeOutAnim.ResourceName = "fade_out";
fadeOutAnim.Length = TransitionDuration;
var trackIdx = fadeOutAnim.AddTrack(Animation.TrackType.Value);
fadeOutAnim.TrackSetPath(trackIdx, "FadeRect:modulate:a");
fadeOutAnim.TrackInsertKey(trackIdx, 0.0f, 0.0f);
fadeOutAnim.TrackInsertKey(trackIdx, TransitionDuration, 1.0f);
_animationPlayer.AddAnimationLibrary("", new AnimationLibrary());
_animationPlayer.GetAnimationLibrary("").AddAnimation("fade_out", fadeOutAnim);
// 创建淡入动画
var fadeInAnim = new Animation();
fadeInAnim.ResourceName = "fade_in";
fadeInAnim.Length = TransitionDuration;
trackIdx = fadeInAnim.AddTrack(Animation.TrackType.Value);
fadeInAnim.TrackSetPath(trackIdx, "FadeRect:modulate:a");
fadeInAnim.TrackInsertKey(trackIdx, 0.0f, 1.0f);
fadeInAnim.TrackInsertKey(trackIdx, TransitionDuration, 0.0f);
_animationPlayer.GetAnimationLibrary("").AddAnimation("fade_in", fadeInAnim);
}
private void ShowLoadingScreen()
{
// 显示加载画面的实现
}
private void HideLoadingScreen()
{
// 隐藏加载画面的实现
}
private void UpdateCurrentScene()
{
// 获取当前场景
CurrentScene = GetTree().CurrentScene;
}
private void NotifySceneEntered()
{
// 通知新场景已进入
if (CurrentScene is ISceneEntry entry)
{
entry.OnSceneEntered();
}
}
public void Shutdown()
{
IsReady = false;
if (_transitionLayer != null)
{
_transitionLayer.QueueFree();
}
GD.Print("[SceneManager] 场景管理器已关闭");
}
}
/// <summary>
/// 场景进入接口 - 场景可以实现此接口以接收进入通知
/// </summary>
public interface ISceneEntry
{
/// <summary>
/// 当场景被加载并显示时调用
/// </summary>
void OnSceneEntered();
}
}
3.3.3 依赖注入基础
// 文件路径: Scripts/Core/DependencyInjection.cs
using Godot;
using System;
using System.Collections.Generic;
using System.Reflection;
namespace GameFramework.Core
{
/// <summary>
/// 依赖注入容器 - 简化服务获取和依赖管理
/// </summary>
public static class DI
{
// 步骤1: 创建服务解析器委托缓存
private static Dictionary<Type, Func<object>> _resolvers = new Dictionary<Type, Func<object>>();
// 步骤2: 已解析的单例缓存
private static Dictionary<Type, object> _singletons = new Dictionary<Type, object>();
/// <summary>
/// 注册服务类型
/// </summary>
/// <typeparam name="TInterface">服务接口</typeparam>
/// <typeparam name="TImplementation">实现类型</typeparam>
public static void Register<TInterface, TImplementation>() where TImplementation : TInterface, new()
{
Type interfaceType = typeof(TInterface);
_resolvers[interfaceType] = () => new TImplementation();
}
/// <summary>
/// 注册单例服务
/// </summary>
/// <typeparam name="TInterface">服务接口</typeparam>
/// <param name="instance">单例实例</param>
public static void RegisterSingleton<TInterface>(TInterface instance)
{
Type interfaceType = typeof(TInterface);
_singletons[interfaceType] = instance;
_resolvers[interfaceType] = () => instance;
}
/// <summary>
/// 解析服务实例
/// </summary>
/// <typeparam name="T">服务类型</typeparam>
/// <returns>服务实例</returns>
public static T Resolve<T>()
{
Type type = typeof(T);
// 步骤3: 检查是否是单例
if (_singletons.TryGetValue(type, out object singleton))
{
return (T)singleton;
}
// 步骤4: 使用解析器创建实例
if (_resolvers.TryGetValue(type, out Func<object> resolver))
{
return (T)resolver();
}
// 步骤5: 尝试从 ServiceProvider 获取
if (ServiceProvider.Instance != null)
{
var service = ServiceProvider.Instance.GetService<T>();
if (service != null)
return service;
}
throw new InvalidOperationException($"未注册的服务类型: {type.Name}");
}
/// <summary>
/// 尝试解析服务,如果未注册返回默认值
/// </summary>
public static bool TryResolve<T>(out T instance)
{
try
{
instance = Resolve<T>();
return true;
}
catch
{
instance = default;
return false;
}
}
/// <summary>
/// 清除所有注册
/// </summary>
public static void Clear()
{
_resolvers.Clear();
_singletons.Clear();
}
}
/// <summary>
/// 自动注入特性 - 标记需要自动注入的字段
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class InjectAttribute : Attribute
{
public string ServiceName { get; set; }
public InjectAttribute() { }
public InjectAttribute(string serviceName)
{
ServiceName = serviceName;
}
}
/// <summary>
/// 自动注入扩展方法
/// </summary>
public static class InjectionExtensions
{
/// <summary>
/// 自动注入标记了 [Inject] 特性的字段
/// </summary>
/// <param name="target">目标对象</param>
public static void InjectDependencies(this object target)
{
Type type = target.GetType();
// 步骤6: 注入字段
foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
var attr = field.GetCustomAttribute<InjectAttribute>();
if (attr != null)
{
object service = ResolveService(field.FieldType, attr.ServiceName);
if (service != null)
{
field.SetValue(target, service);
}
else
{
GD.PushWarning($"无法注入字段 {field.Name},服务 {field.FieldType.Name} 未注册");
}
}
}
// 步骤7: 注入属性
foreach (PropertyInfo prop in type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
if (!prop.CanWrite) continue;
var attr = prop.GetCustomAttribute<InjectAttribute>();
if (attr != null)
{
object service = ResolveService(prop.PropertyType, attr.ServiceName);
if (service != null)
{
prop.SetValue(target, service);
}
}
}
}
private static object ResolveService(Type serviceType, string serviceName)
{
// 步骤8: 尝试从 ServiceProvider 获取
if (ServiceProvider.Instance != null)
{
// 使用反射调用泛型方法 GetService<T>()
var method = typeof(ServiceProvider).GetMethod("GetService");
var genericMethod = method.MakeGenericMethod(serviceType);
return genericMethod.Invoke(ServiceProvider.Instance, null);
}
return null;
}
}
}
// 文件路径: Scripts/Examples/DependencyInjectionExample.cs
using Godot;
using GameFramework.Core;
using GameFramework.Managers;
namespace GameFramework.Examples
{
/// <summary>
/// 依赖注入使用示例
/// </summary>
public partial class DependencyInjectionExample : Node
{
// 步骤1: 使用 [Inject] 特性自动注入服务
[Inject]
private DataManager _dataManager;
[Inject]
private ISceneManager _sceneManager;
public override void _Ready()
{
base._Ready();
// 步骤2: 调用注入方法
this.InjectDependencies();
// 步骤3: 现在可以直接使用注入的服务
if (_dataManager != null)
{
GD.Print("[DIExample] DataManager 注入成功");
}
if (_sceneManager != null)
{
GD.Print("[DIExample] SceneManager 注入成功");
}
}
/// <summary>
/// 使用 DI 容器直接解析服务
/// </summary>
private void ResolveServicesExample()
{
// 步骤4: 使用 DI.Resolve 获取服务
var dataManager = DI.Resolve<DataManager>();
var sceneManager = DI.Resolve<ISceneManager>();
// 步骤5: 使用 TryResolve 安全获取服务
if (DI.TryResolve<SaveManager>(out var saveManager))
{
GD.Print("SaveManager 可用");
}
}
/// <summary>
/// 构造函数注入示例(在 Godot 中较少使用)
/// </summary>
public void ConstructorInjectionExample()
{
// 步骤6: 手动注入构造函数参数
var playerManager = new PlayerManager(
DI.Resolve<DataManager>(),
DI.Resolve<ISceneManager>()
);
}
}
/// <summary>
/// 玩家管理器示例
/// </summary>
public class PlayerManager
{
private readonly DataManager _dataManager;
private readonly ISceneManager _sceneManager;
// 步骤7: 通过构造函数注入依赖
public PlayerManager(DataManager dataManager, ISceneManager sceneManager)
{
_dataManager = dataManager;
_sceneManager = sceneManager;
}
}
}
3.4 完整 GameManager 示例
// 文件路径: Scripts/Managers/GameManager.cs
using Godot;
using System;
using GameFramework.Core;
using GameFramework.Data;
namespace GameFramework.Managers
{
/// <summary>
/// 游戏管理器 - 主控制器,协调所有子系统
/// 这是游戏中最高层的管理器,负责整体游戏流程控制
/// </summary>
public partial class GameManager : Node, IManager
{
// 步骤1: 单例访问
public static GameManager Instance { get; private set; }
// 步骤2: 实现 IManager 接口
public int Priority => int.MinValue; // 最后初始化,最先关闭
public bool IsReady { get; private set; }
// 步骤3: 游戏状态
public enum GameState
{
None,
Booting,
MainMenu,
Playing,
Paused,
Loading,
GameOver
}
public GameState CurrentState { get; private set; } = GameState.None;
public GameState PreviousState { get; private set; } = GameState.None;
// 步骤4: 运行数据
public float GameTime { get; private set; }
public bool IsPaused => CurrentState == GameState.Paused;
// 步骤5: 依赖服务(通过 ServiceProvider 获取)
private DataManager DataManager => ServiceProvider.Instance?.GetService<DataManager>();
private ISceneManager SceneManager => ServiceProvider.Instance?.GetService<ISceneManager>();
private SaveManager SaveManager => ServiceProvider.Instance?.GetService<SaveManager>();
// 步骤6: 事件
[Signal]
public delegate void GameStateChangedEventHandler(GameState newState, GameState oldState);
[Signal]
public delegate void GamePausedEventHandler();
[Signal]
public delegate void GameResumedEventHandler();
public override void _Ready()
{
base._Ready();
// 步骤7: 设置单例
if (Instance != null)
{
QueueFree();
return;
}
Instance = this;
// 步骤8: 注册到服务容器
ServiceProvider.Instance?.RegisterService<IGameManager>(this);
}
public void Initialize()
{
// 步骤9: 初始化游戏
ChangeState(GameState.Booting);
IsReady = true;
GD.Print("[GameManager] 游戏管理器初始化完成");
}
public override void _Process(double delta)
{
// 步骤10: 更新游戏时间
if (!IsPaused && CurrentState == GameState.Playing)
{
GameTime += (float)delta;
}
}
/// <summary>
/// 开始新游戏
/// </summary>
public void StartNewGame()
{
GD.Print("[GameManager] 开始新游戏");
// 重置游戏数据
DataManager?.ClearTransitionData();
GameTime = 0f;
// 切换到游戏场景
ChangeState(GameState.Loading);
SceneManager?.LoadSceneAsync("res://Scenes/GameScene.tscn");
}
/// <summary>
/// 继续游戏
/// </summary>
public void ContinueGame()
{
if (SaveManager?.SaveExists(0) == true)
{
GD.Print("[GameManager] 继续游戏");
SaveManager.LoadSave(0);
ChangeState(GameState.Playing);
}
else
{
GD.Print("[GameManager] 没有找到存档,开始新游戏");
StartNewGame();
}
}
/// <summary>
/// 返回主菜单
/// </summary>
public void ReturnToMainMenu()
{
GD.Print("[GameManager] 返回主菜单");
ChangeState(GameState.MainMenu);
SceneManager?.LoadSceneAsync("res://Scenes/MainMenu.tscn");
}
/// <summary>
/// 暂停游戏
/// </summary>
public void PauseGame()
{
if (CurrentState == GameState.Playing)
{
ChangeState(GameState.Paused);
GetTree().Paused = true;
EmitSignal(SignalName.GamePaused);
}
}
/// <summary>
/// 恢复游戏
/// </summary>
public void ResumeGame()
{
if (CurrentState == GameState.Paused)
{
GetTree().Paused = false;
ChangeState(GameState.Playing);
EmitSignal(SignalName.GameResumed);
}
}
/// <summary>
/// 退出游戏
/// </summary>
public void QuitGame()
{
GD.Print("[GameManager] 退出游戏");
// 自动保存
SaveManager?.QuickSave();
// 关闭服务
ServiceProvider.Instance?.Shutdown();
// 退出应用
GetTree().Quit();
}
/// <summary>
/// 切换游戏状态
/// </summary>
private void ChangeState(GameState newState)
{
if (CurrentState == newState) return;
PreviousState = CurrentState;
CurrentState = newState;
EmitSignal(SignalName.GameStateChanged, (int)newState, (int)PreviousState);
GD.Print($"[GameManager] 游戏状态变更: {PreviousState} -> {newState}");
}
/// <summary>
/// 保存游戏
/// </summary>
public void SaveGame(int slotIndex, string saveName = null)
{
if (SaveManager != null)
{
bool success = SaveManager.CreateSave(slotIndex, saveName);
if (success)
{
GD.Print($"[GameManager] 游戏已保存到槽位 {slotIndex}");
}
}
}
/// <summary>
/// 加载游戏
/// </summary>
public void LoadGame(int slotIndex)
{
if (SaveManager != null)
{
bool success = SaveManager.LoadSave(slotIndex);
if (success)
{
ChangeState(GameState.Playing);
GD.Print($"[GameManager] 游戏已从槽位 {slotIndex} 加载");
}
}
}
public void Shutdown()
{
// 清理
IsReady = false;
Instance = null;
GD.Print("[GameManager] 游戏管理器已关闭");
}
}
/// <summary>
/// 游戏管理器接口
/// </summary>
public interface IGameManager : IManager
{
GameManager.GameState CurrentState { get; }
float GameTime { get; }
bool IsPaused { get; }
void StartNewGame();
void ContinueGame();
void ReturnToMainMenu();
void PauseGame();
void ResumeGame();
void QuitGame();
void SaveGame(int slotIndex, string saveName = null);
void LoadGame(int slotIndex);
}
}
3.5 最佳实践与注意事项
3.5.1 AutoLoad 配置建议
建议在项目设置中按以下顺序配置 AutoLoad 节点:
- ServiceProvider - 服务容器(最先加载)
- GameBoot - 游戏启动器
- DataManager - 数据管理器
- SaveManager - 存档管理器
- SceneManager - 场景管理器
- GameManager - 游戏管理器(最后加载,依赖其他管理器)
3.5.2 循环依赖避免
// 不好的做法 - 直接引用其他管理器
public class BadManager : IManager
{
private SceneManager _sceneManager; // 直接引用具体类
public void Initialize()
{
_sceneManager = ServiceProvider.Instance.GetService<SceneManager>(); // 可能导致循环
}
}
// 好的做法 - 使用接口和延迟获取
public class GoodManager : IManager
{
private ISceneManager SceneManager =>
ServiceProvider.Instance?.GetService<ISceneManager>(); // 延迟获取
public void Initialize()
{
// 初始化时不立即获取依赖
}
public void DoSomething()
{
// 使用时才获取
SceneManager?.ChangeScene("path");
}
}
3.5.3 测试与调试
// 文件路径: Scripts/Tests/ManagerTests.cs
using Godot;
using GameFramework.Core;
using GameFramework.Managers;
namespace GameFramework.Tests
{
/// <summary>
/// 管理器测试工具
/// </summary>
public static class ManagerTests
{
/// <summary>
/// 验证所有管理器已正确注册
/// </summary>
public static void VerifyManagerRegistrations()
{
GD.Print("[ManagerTests] 验证管理器注册...");
// 检查 ServiceProvider
if (ServiceProvider.Instance == null)
{
GD.PushError("[ManagerTests] ServiceProvider 未注册");
return;
}
// 检查各管理器
CheckService<DataManager>("DataManager");
CheckService<ISceneManager>("SceneManager");
CheckService<SaveManager>("SaveManager");
CheckService<IGameManager>("GameManager");
GD.Print("[ManagerTests] 验证完成");
}
private static void CheckService<T>(string name) where T : class
{
var service = ServiceProvider.Instance.GetService<T>();
if (service == null)
{
GD.PushError($"[ManagerTests] {name} 未注册");
}
else
{
GD.Print($"[ManagerTests] {name} 已注册");
}
}
}
}
3.5.4 性能考虑
- 延迟初始化:管理器应该延迟加载资源,避免启动时加载所有内容
- 对象池:频繁创建销毁的对象应该使用对象池
- 异步加载:场景切换时使用异步加载避免卡顿
- 事件解耦:使用事件而非直接调用来降低耦合度
3.5.5 常见陷阱
- 在构造函数中访问 AutoLoad:此时场景树尚未就绪,应使用
_Ready - 循环依赖:A 依赖 B,B 又依赖 A,使用接口和事件解耦
- 场景切换时数据丢失:使用 DataManager 保存跨场景数据
- 忘记处理游戏退出:实现
NOTIFICATION_WM_CLOSE_REQUEST处理
本章详细介绍了 Godot C# 游戏开发框架中的全局系统与自动加载机制,包括服务容器设计、数据持久化方案、启动流程控制和依赖注入基础。这些系统是构建稳定、可扩展游戏架构的基石。