mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
docs(godot): 添加 Godot 架构集成和场景系统文档
- 新增 Godot 架构集成文档,介绍 AbstractArchitecture 和 ArchitectureAnchor - 添加 Godot 场景系统文档,涵盖 SceneBehavior 和场景生命周期管理 - 包含数据与存档系统文档,介绍 IDataRepository 和 ISaveRepository 接口 - 提供完整的代码示例和最佳实践指南 - 覆盖多架构支持、热重载和场景参数传递等高级功能 - 包含常见问题解答和相关文档链接
This commit is contained in:
parent
bbb91d597a
commit
b3838ce8c7
588
docs/zh-CN/game/data.md
Normal file
588
docs/zh-CN/game/data.md
Normal file
@ -0,0 +1,588 @@
|
||||
---
|
||||
title: 数据与存档系统
|
||||
description: 数据与存档系统提供了完整的数据持久化解决方案,支持多槽位存档、版本管理和数据迁移。
|
||||
---
|
||||
|
||||
# 数据与存档系统
|
||||
|
||||
## 概述
|
||||
|
||||
数据与存档系统是 GFramework.Game 中用于管理游戏数据持久化的核心组件。它提供了统一的数据加载和保存接口,支持多槽位存档管理、数据版本控制和自动迁移,让你可以轻松实现游戏存档、设置保存等功能。
|
||||
|
||||
通过数据系统,你可以将游戏数据保存到本地存储,支持多个存档槽位,并在数据结构变化时自动进行版本迁移。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 统一的数据持久化接口
|
||||
- 多槽位存档管理
|
||||
- 数据版本控制和迁移
|
||||
- 异步加载和保存
|
||||
- 批量数据操作
|
||||
- 与存储系统集成
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 数据接口
|
||||
|
||||
`IData` 标记数据类型:
|
||||
|
||||
```csharp
|
||||
public interface IData
|
||||
{
|
||||
// 标记接口,用于标识可持久化的数据
|
||||
}
|
||||
```
|
||||
|
||||
### 数据仓库
|
||||
|
||||
`IDataRepository` 提供通用的数据操作:
|
||||
|
||||
```csharp
|
||||
public interface IDataRepository : IUtility
|
||||
{
|
||||
Task<T> LoadAsync<T>(IDataLocation location) where T : class, IData, new();
|
||||
Task SaveAsync<T>(IDataLocation location, T data) where T : class, IData;
|
||||
Task<bool> ExistsAsync(IDataLocation location);
|
||||
Task DeleteAsync(IDataLocation location);
|
||||
Task SaveAllAsync(IEnumerable<(IDataLocation, IData)> dataList);
|
||||
}
|
||||
```
|
||||
|
||||
### 存档仓库
|
||||
|
||||
`ISaveRepository<T>` 专门用于管理游戏存档:
|
||||
|
||||
```csharp
|
||||
public interface ISaveRepository<TSaveData> : IUtility
|
||||
where TSaveData : class, IData, new()
|
||||
{
|
||||
Task<bool> ExistsAsync(int slot);
|
||||
Task<TSaveData> LoadAsync(int slot);
|
||||
Task SaveAsync(int slot, TSaveData data);
|
||||
Task DeleteAsync(int slot);
|
||||
Task<IReadOnlyList<int>> ListSlotsAsync();
|
||||
}
|
||||
```
|
||||
|
||||
### 版本化数据
|
||||
|
||||
`IVersionedData` 支持数据版本管理:
|
||||
|
||||
```csharp
|
||||
public interface IVersionedData : IData
|
||||
{
|
||||
int Version { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 定义数据类型
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.data;
|
||||
|
||||
// 简单数据
|
||||
public class PlayerData : IData
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; }
|
||||
}
|
||||
|
||||
// 版本化数据
|
||||
public class SaveData : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public PlayerData Player { get; set; }
|
||||
public DateTime SaveTime { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 使用存档仓库
|
||||
|
||||
```csharp
|
||||
public class SaveController : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public async Task SaveGame(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 创建存档数据
|
||||
var saveData = new SaveData
|
||||
{
|
||||
Player = new PlayerData
|
||||
{
|
||||
Name = "Player1",
|
||||
Level = 10,
|
||||
Experience = 1000
|
||||
},
|
||||
SaveTime = DateTime.Now
|
||||
};
|
||||
|
||||
// 保存到指定槽位
|
||||
await saveRepo.SaveAsync(slot, saveData);
|
||||
Console.WriteLine($"游戏已保存到槽位 {slot}");
|
||||
}
|
||||
|
||||
public async Task LoadGame(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 检查存档是否存在
|
||||
if (!await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
Console.WriteLine($"槽位 {slot} 不存在存档");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载存档
|
||||
var saveData = await saveRepo.LoadAsync(slot);
|
||||
Console.WriteLine($"加载存档: {saveData.Player.Name}, 等级 {saveData.Player.Level}");
|
||||
}
|
||||
|
||||
public async Task DeleteSave(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 删除存档
|
||||
await saveRepo.DeleteAsync(slot);
|
||||
Console.WriteLine($"已删除槽位 {slot} 的存档");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册存档仓库
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.data;
|
||||
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
// 获取存储系统
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
// 创建存档配置
|
||||
var saveConfig = new SaveConfiguration
|
||||
{
|
||||
SaveRoot = "saves",
|
||||
SaveSlotPrefix = "slot_",
|
||||
SaveFileName = "save.json"
|
||||
};
|
||||
|
||||
// 注册存档仓库
|
||||
var saveRepo = new SaveRepository<SaveData>(storage, saveConfig);
|
||||
RegisterUtility<ISaveRepository<SaveData>>(saveRepo);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 列出所有存档
|
||||
|
||||
```csharp
|
||||
public async Task ShowSaveList()
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 获取所有存档槽位
|
||||
var slots = await saveRepo.ListSlotsAsync();
|
||||
|
||||
Console.WriteLine($"找到 {slots.Count} 个存档:");
|
||||
foreach (var slot in slots)
|
||||
{
|
||||
var saveData = await saveRepo.LoadAsync(slot);
|
||||
Console.WriteLine($"槽位 {slot}: {saveData.Player.Name}, " +
|
||||
$"等级 {saveData.Player.Level}, " +
|
||||
$"保存时间 {saveData.SaveTime}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自动保存
|
||||
|
||||
```csharp
|
||||
public class AutoSaveController : IController
|
||||
{
|
||||
private CancellationTokenSource? _autoSaveCts;
|
||||
|
||||
public void StartAutoSave(int slot, TimeSpan interval)
|
||||
{
|
||||
_autoSaveCts = new CancellationTokenSource();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (!_autoSaveCts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(interval, _autoSaveCts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await SaveGame(slot);
|
||||
Console.WriteLine("自动保存完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"自动保存失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}, _autoSaveCts.Token);
|
||||
}
|
||||
|
||||
public void StopAutoSave()
|
||||
{
|
||||
_autoSaveCts?.Cancel();
|
||||
_autoSaveCts?.Dispose();
|
||||
_autoSaveCts = null;
|
||||
}
|
||||
|
||||
private async Task SaveGame(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
var saveData = CreateSaveData();
|
||||
await saveRepo.SaveAsync(slot, saveData);
|
||||
}
|
||||
|
||||
private SaveData CreateSaveData()
|
||||
{
|
||||
// 从游戏状态创建存档数据
|
||||
return new SaveData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 数据版本迁移
|
||||
|
||||
```csharp
|
||||
// 版本 1 的数据
|
||||
public class SaveDataV1 : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public string PlayerName { get; set; }
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
// 版本 2 的数据(添加了新字段)
|
||||
public class SaveDataV2 : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 2;
|
||||
public string PlayerName { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; } // 新增字段
|
||||
public DateTime LastPlayTime { get; set; } // 新增字段
|
||||
}
|
||||
|
||||
// 数据迁移器
|
||||
public class SaveDataMigrator
|
||||
{
|
||||
public SaveDataV2 Migrate(SaveDataV1 oldData)
|
||||
{
|
||||
return new SaveDataV2
|
||||
{
|
||||
Version = 2,
|
||||
PlayerName = oldData.PlayerName,
|
||||
Level = oldData.Level,
|
||||
Experience = oldData.Level * 100, // 根据等级计算经验
|
||||
LastPlayTime = DateTime.Now
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时自动迁移
|
||||
public async Task<SaveDataV2> LoadWithMigration(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveDataV2>>();
|
||||
var data = await saveRepo.LoadAsync(slot);
|
||||
|
||||
if (data.Version < 2)
|
||||
{
|
||||
// 需要迁移
|
||||
var oldData = data as SaveDataV1;
|
||||
var migrator = new SaveDataMigrator();
|
||||
var newData = migrator.Migrate(oldData);
|
||||
|
||||
// 保存迁移后的数据
|
||||
await saveRepo.SaveAsync(slot, newData);
|
||||
return newData;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
### 使用数据仓库
|
||||
|
||||
```csharp
|
||||
public class SettingsController : IController
|
||||
{
|
||||
public async Task SaveSettings()
|
||||
{
|
||||
var dataRepo = this.GetUtility<IDataRepository>();
|
||||
|
||||
var settings = new GameSettings
|
||||
{
|
||||
MasterVolume = 0.8f,
|
||||
MusicVolume = 0.6f,
|
||||
SfxVolume = 0.7f
|
||||
};
|
||||
|
||||
// 定义数据位置
|
||||
var location = new DataLocation("settings", "game_settings.json");
|
||||
|
||||
// 保存设置
|
||||
await dataRepo.SaveAsync(location, settings);
|
||||
}
|
||||
|
||||
public async Task<GameSettings> LoadSettings()
|
||||
{
|
||||
var dataRepo = this.GetUtility<IDataRepository>();
|
||||
var location = new DataLocation("settings", "game_settings.json");
|
||||
|
||||
// 检查是否存在
|
||||
if (!await dataRepo.ExistsAsync(location))
|
||||
{
|
||||
return new GameSettings(); // 返回默认设置
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
return await dataRepo.LoadAsync<GameSettings>(location);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 批量保存数据
|
||||
|
||||
```csharp
|
||||
public async Task SaveAllGameData()
|
||||
{
|
||||
var dataRepo = this.GetUtility<IDataRepository>();
|
||||
|
||||
var dataList = new List<(IDataLocation, IData)>
|
||||
{
|
||||
(new DataLocation("player", "profile.json"), playerData),
|
||||
(new DataLocation("inventory", "items.json"), inventoryData),
|
||||
(new DataLocation("quests", "progress.json"), questData)
|
||||
};
|
||||
|
||||
// 批量保存
|
||||
await dataRepo.SaveAllAsync(dataList);
|
||||
Console.WriteLine("所有数据已保存");
|
||||
}
|
||||
```
|
||||
|
||||
### 存档备份
|
||||
|
||||
```csharp
|
||||
public async Task BackupSave(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
if (!await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
Console.WriteLine("存档不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载原存档
|
||||
var saveData = await saveRepo.LoadAsync(slot);
|
||||
|
||||
// 保存到备份槽位
|
||||
int backupSlot = slot + 100;
|
||||
await saveRepo.SaveAsync(backupSlot, saveData);
|
||||
|
||||
Console.WriteLine($"存档已备份到槽位 {backupSlot}");
|
||||
}
|
||||
|
||||
public async Task RestoreBackup(int slot)
|
||||
{
|
||||
int backupSlot = slot + 100;
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
if (!await saveRepo.ExistsAsync(backupSlot))
|
||||
{
|
||||
Console.WriteLine("备份不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载备份
|
||||
var backupData = await saveRepo.LoadAsync(backupSlot);
|
||||
|
||||
// 恢复到原槽位
|
||||
await saveRepo.SaveAsync(slot, backupData);
|
||||
|
||||
Console.WriteLine($"已从备份恢复到槽位 {slot}");
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用版本化数据**:为存档数据实现 `IVersionedData`
|
||||
```csharp
|
||||
✓ public class SaveData : IVersionedData { public int Version { get; set; } = 1; }
|
||||
✗ public class SaveData : IData { } // 无法进行版本管理
|
||||
```
|
||||
|
||||
2. **定期自动保存**:避免玩家数据丢失
|
||||
```csharp
|
||||
// 每 5 分钟自动保存
|
||||
StartAutoSave(currentSlot, TimeSpan.FromMinutes(5));
|
||||
```
|
||||
|
||||
3. **保存前验证数据**:确保数据完整性
|
||||
```csharp
|
||||
public async Task SaveGame(int slot)
|
||||
{
|
||||
var saveData = CreateSaveData();
|
||||
|
||||
if (!ValidateSaveData(saveData))
|
||||
{
|
||||
throw new InvalidOperationException("存档数据无效");
|
||||
}
|
||||
|
||||
await saveRepo.SaveAsync(slot, saveData);
|
||||
}
|
||||
```
|
||||
|
||||
4. **处理保存失败**:使用 try-catch 捕获异常
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
await saveRepo.SaveAsync(slot, saveData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"保存失败: {ex.Message}");
|
||||
ShowErrorMessage("保存失败,请重试");
|
||||
}
|
||||
```
|
||||
|
||||
5. **提供多个存档槽位**:让玩家可以管理多个存档
|
||||
```csharp
|
||||
// 支持 10 个存档槽位
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
if (await saveRepo.ExistsAsync(i))
|
||||
{
|
||||
ShowSaveSlot(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **在关键时刻保存**:场景切换、关卡完成等
|
||||
```csharp
|
||||
public async Task OnLevelComplete()
|
||||
{
|
||||
// 关卡完成时自动保存
|
||||
await SaveGame(currentSlot);
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何实现多个存档槽位?
|
||||
|
||||
**解答**:
|
||||
使用 `ISaveRepository<T>` 的槽位参数:
|
||||
|
||||
```csharp
|
||||
// 保存到不同槽位
|
||||
await saveRepo.SaveAsync(1, saveData); // 槽位 1
|
||||
await saveRepo.SaveAsync(2, saveData); // 槽位 2
|
||||
await saveRepo.SaveAsync(3, saveData); // 槽位 3
|
||||
```
|
||||
|
||||
### 问题:如何处理数据版本升级?
|
||||
|
||||
**解答**:
|
||||
实现 `IVersionedData` 并在加载时检查版本:
|
||||
|
||||
```csharp
|
||||
var data = await saveRepo.LoadAsync(slot);
|
||||
if (data.Version < CurrentVersion)
|
||||
{
|
||||
data = MigrateData(data);
|
||||
await saveRepo.SaveAsync(slot, data);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:存档数据保存在哪里?
|
||||
|
||||
**解答**:
|
||||
由存储系统决定,通常在:
|
||||
|
||||
- Windows: `%AppData%/GameName/saves/`
|
||||
- Linux: `~/.local/share/GameName/saves/`
|
||||
- macOS: `~/Library/Application Support/GameName/saves/`
|
||||
|
||||
### 问题:如何实现云存档?
|
||||
|
||||
**解答**:
|
||||
实现自定义的 `IStorage`,将数据保存到云端:
|
||||
|
||||
```csharp
|
||||
public class CloudStorage : IStorage
|
||||
{
|
||||
public async Task WriteAsync(string path, byte[] data)
|
||||
{
|
||||
await UploadToCloud(path, data);
|
||||
}
|
||||
|
||||
public async Task<byte[]> ReadAsync(string path)
|
||||
{
|
||||
return await DownloadFromCloud(path);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何加密存档数据?
|
||||
|
||||
**解答**:
|
||||
在保存和加载时进行加密/解密:
|
||||
|
||||
```csharp
|
||||
public async Task SaveEncrypted(int slot, SaveData data)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
var encrypted = Encrypt(json);
|
||||
await storage.WriteAsync(path, encrypted);
|
||||
}
|
||||
|
||||
public async Task<SaveData> LoadEncrypted(int slot)
|
||||
{
|
||||
var encrypted = await storage.ReadAsync(path);
|
||||
var json = Decrypt(encrypted);
|
||||
return JsonSerializer.Deserialize<SaveData>(json);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:存档损坏怎么办?
|
||||
|
||||
**解答**:
|
||||
实现备份和恢复机制:
|
||||
|
||||
```csharp
|
||||
public async Task SaveWithBackup(int slot, SaveData data)
|
||||
{
|
||||
// 先备份旧存档
|
||||
if (await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
var oldData = await saveRepo.LoadAsync(slot);
|
||||
await saveRepo.SaveAsync(slot + 100, oldData);
|
||||
}
|
||||
|
||||
// 保存新存档
|
||||
await saveRepo.SaveAsync(slot, data);
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [存储系统](/zh-CN/game/storage) - 底层存储实现
|
||||
- [序列化系统](/zh-CN/game/serialization) - 数据序列化
|
||||
- [场景系统](/zh-CN/game/scene) - 场景切换时保存
|
||||
- [存档系统实现教程](/zh-CN/tutorials/save-system) - 完整示例
|
||||
590
docs/zh-CN/godot/architecture.md
Normal file
590
docs/zh-CN/godot/architecture.md
Normal file
@ -0,0 +1,590 @@
|
||||
---
|
||||
title: Godot 架构集成
|
||||
description: Godot 架构集成提供了 GFramework 与 Godot 引擎的无缝连接,实现生命周期同步和模块化开发。
|
||||
---
|
||||
|
||||
# Godot 架构集成
|
||||
|
||||
## 概述
|
||||
|
||||
Godot 架构集成是 GFramework.Godot 中连接框架与 Godot 引擎的核心组件。它提供了架构与 Godot 场景树的生命周期绑定、模块化扩展系统,以及与
|
||||
Godot 节点系统的深度集成。
|
||||
|
||||
通过 Godot 架构集成,你可以在 Godot 项目中使用 GFramework 的所有功能,同时保持与 Godot 引擎的完美兼容。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 架构与 Godot 生命周期自动同步
|
||||
- 模块化的 Godot 扩展系统
|
||||
- 架构锚点节点管理
|
||||
- 自动资源清理
|
||||
- 热重载支持
|
||||
- 与 Godot 场景树深度集成
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 抽象架构
|
||||
|
||||
`AbstractArchitecture` 是 Godot 项目中架构的基类:
|
||||
|
||||
```csharp
|
||||
public abstract class AbstractArchitecture : Architecture
|
||||
{
|
||||
protected Node ArchitectureRoot { get; }
|
||||
protected abstract void InstallModules();
|
||||
protected Task InstallGodotModule<TModule>(TModule module);
|
||||
}
|
||||
```
|
||||
|
||||
### 架构锚点
|
||||
|
||||
`ArchitectureAnchor` 是连接架构与 Godot 场景树的桥梁:
|
||||
|
||||
```csharp
|
||||
public partial class ArchitectureAnchor : Node
|
||||
{
|
||||
public void Bind(Action onExit);
|
||||
public override void _ExitTree();
|
||||
}
|
||||
```
|
||||
|
||||
### Godot 模块
|
||||
|
||||
`IGodotModule` 定义了 Godot 特定的模块接口:
|
||||
|
||||
```csharp
|
||||
public interface IGodotModule : IArchitectureModule
|
||||
{
|
||||
Node Node { get; }
|
||||
void OnPhase(ArchitecturePhase phase, IArchitecture architecture);
|
||||
void OnAttach(Architecture architecture);
|
||||
void OnDetach();
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建 Godot 架构
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
// 单例实例
|
||||
public static GameArchitecture Interface { get; private set; }
|
||||
|
||||
public GameArchitecture()
|
||||
{
|
||||
Interface = this;
|
||||
}
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 注册 Model
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterModel(new GameModel());
|
||||
|
||||
// 注册 System
|
||||
RegisterSystem(new GameplaySystem());
|
||||
RegisterSystem(new AudioSystem());
|
||||
|
||||
// 注册 Utility
|
||||
RegisterUtility(new StorageUtility());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在 Godot 场景中初始化架构
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.architecture;
|
||||
|
||||
public partial class GameRoot : Node
|
||||
{
|
||||
private GameArchitecture _architecture;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 创建并初始化架构
|
||||
_architecture = new GameArchitecture();
|
||||
_architecture.InitializeAsync().AsTask().Wait();
|
||||
|
||||
GD.Print("架构已初始化");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用架构锚点
|
||||
|
||||
架构锚点会自动创建并绑定到场景树:
|
||||
|
||||
```csharp
|
||||
// 架构会自动创建锚点节点
|
||||
// 节点名称格式: __GFramework__GameArchitecture__[HashCode]__ArchitectureAnchor__
|
||||
|
||||
// 当场景树销毁时,锚点会自动触发架构清理
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 创建 Godot 模块
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
using Godot;
|
||||
|
||||
public class CoroutineModule : AbstractGodotModule
|
||||
{
|
||||
private Node _coroutineNode;
|
||||
|
||||
public override Node Node => _coroutineNode;
|
||||
|
||||
public CoroutineModule()
|
||||
{
|
||||
_coroutineNode = new Node { Name = "CoroutineScheduler" };
|
||||
}
|
||||
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 注册协程调度器
|
||||
var scheduler = new CoroutineScheduler(new GodotTimeSource());
|
||||
architecture.RegisterSystem<ICoroutineScheduler>(scheduler);
|
||||
|
||||
GD.Print("协程模块已安装");
|
||||
}
|
||||
|
||||
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
|
||||
{
|
||||
if (phase == ArchitecturePhase.Ready)
|
||||
{
|
||||
GD.Print("协程模块已就绪");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDetach()
|
||||
{
|
||||
GD.Print("协程模块已分离");
|
||||
_coroutineNode?.QueueFree();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 安装 Godot 模块
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 安装核心模块
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterSystem(new GameplaySystem());
|
||||
|
||||
// 安装 Godot 模块
|
||||
InstallGodotModule(new CoroutineModule()).Wait();
|
||||
InstallGodotModule(new SceneModule()).Wait();
|
||||
InstallGodotModule(new UiModule()).Wait();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 访问架构根节点
|
||||
|
||||
```csharp
|
||||
public class SceneModule : AbstractGodotModule
|
||||
{
|
||||
private Node _sceneRoot;
|
||||
|
||||
public override Node Node => _sceneRoot;
|
||||
|
||||
public SceneModule()
|
||||
{
|
||||
_sceneRoot = new Node { Name = "SceneRoot" };
|
||||
}
|
||||
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 访问架构根节点
|
||||
if (architecture is AbstractArchitecture godotArch)
|
||||
{
|
||||
var root = godotArch.ArchitectureRoot;
|
||||
root.AddChild(_sceneRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 监听架构阶段
|
||||
|
||||
```csharp
|
||||
public class AnalyticsModule : AbstractGodotModule
|
||||
{
|
||||
private Node _analyticsNode;
|
||||
|
||||
public override Node Node => _analyticsNode;
|
||||
|
||||
public AnalyticsModule()
|
||||
{
|
||||
_analyticsNode = new Node { Name = "Analytics" };
|
||||
}
|
||||
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 安装分析系统
|
||||
}
|
||||
|
||||
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
|
||||
{
|
||||
switch (phase)
|
||||
{
|
||||
case ArchitecturePhase.Initializing:
|
||||
GD.Print("架构正在初始化");
|
||||
break;
|
||||
|
||||
case ArchitecturePhase.Ready:
|
||||
GD.Print("架构已就绪,开始追踪");
|
||||
StartTracking();
|
||||
break;
|
||||
|
||||
case ArchitecturePhase.Destroying:
|
||||
GD.Prin构正在销毁,停止追踪");
|
||||
StopTracking();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartTracking() { }
|
||||
private void StopTracking() { }
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义架构配置
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Core.Abstractions.environment;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public GameArchitecture() : base(
|
||||
configuration: CreateConfiguration(),
|
||||
environment: CreateEnvironment()
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
private static IArchitectureConfiguration CreateConfiguration()
|
||||
{
|
||||
return new ArchitectureConfiguration
|
||||
{
|
||||
EnableLogging
|
||||
LogLevel = LogLevel.Debug
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnvironment CreateEnvironment()
|
||||
{
|
||||
return new DefaultEnvironment
|
||||
{
|
||||
IsDevelopment = OS.IsDebugBuild()
|
||||
};
|
||||
}
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 根据环境配置安装模块
|
||||
if (Environment.IsDevelopment)
|
||||
{
|
||||
InstallGodotModule(new DebugModule()).Wait();
|
||||
}
|
||||
|
||||
// 安装核心模块
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterSystem(new GameplaySystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 热重载支持
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
private static bool _initialized;
|
||||
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
// 防止热重载时重复初始化
|
||||
if (_initialized)
|
||||
{
|
||||
GD.Print("架构已初始化,跳过重复初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnInitialize();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
protected override async ValueTask OnDestroyAsync()
|
||||
{
|
||||
await base.OnDestroyAsync();
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在节点中使用架构
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class Player : CharacterBody2D
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
// 通过扩展方法访问架构组件
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
var gameplaySystem = this.GetSystem<GameplaySystem>();
|
||||
|
||||
// 发送事件
|
||||
this.SendEvent(new PlayerSpawnedEvent());
|
||||
|
||||
// 执行命令
|
||||
this.SendCommand(new InitPlayerCommand());
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
// 在 Process 中使用架构组件
|
||||
var inputSystem = this.GetSystem<InputSystem>();
|
||||
var movement = inputSystem.GetMovementInput();
|
||||
|
||||
Velocity = movement * 200;
|
||||
MoveAndSlide();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 多架构支持
|
||||
|
||||
```csharp
|
||||
// 游戏架构
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static GameArchitecture Interface { get; private set; }
|
||||
|
||||
public GameArchitecture()
|
||||
{
|
||||
Interface = this;
|
||||
}
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterSystem(new GameplaySystem());
|
||||
}
|
||||
}
|
||||
|
||||
// UI 架构
|
||||
public class UiArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static UiArchitecture Interface { get; private set; }
|
||||
|
||||
public UiArchitecture()
|
||||
{
|
||||
Interface = this;
|
||||
}
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
RegisterModel(new UiModel());
|
||||
RegisterSystem(new UiSystem());
|
||||
}
|
||||
}
|
||||
|
||||
// 在不同节点中使用不同架构
|
||||
public partial class GameNode : Node, IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
}
|
||||
|
||||
public partial class UiNode : Control, IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => UiArchitecture.Interface;
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用单例模式**:为架构提供全局访问点
|
||||
```csharp
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static GameArchitecture Interface { get; private set; }
|
||||
|
||||
public GameArchitecture()
|
||||
{
|
||||
Interface = this;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **在根节点初始化架构**:确保架构在所有节点之前就绪
|
||||
```csharp
|
||||
public partial class GameRoot : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
new GameArchitecture().InitializeAsync().AsTask().Wait();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **使用 Godot 模块组织功能**:将相关功能封装为模块
|
||||
```csharp
|
||||
InstallGodotModule(new CoroutineModule()).Wait();
|
||||
InstallGodotModule(new SceneModule()).Wait();
|
||||
InstallGodotModule(new UiModule()).Wait();
|
||||
```
|
||||
|
||||
4. **利用架构阶段钩子**:在适当的时机执行逻辑
|
||||
```csharp
|
||||
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
|
||||
{
|
||||
if (phase == ArchitecturePhase.Ready)
|
||||
{
|
||||
// 架构就绪后的初始化
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **正确清理资源**:在 OnDetach 中释放 Godot 节点
|
||||
```csharp
|
||||
public override void OnDetach()
|
||||
{
|
||||
_node?.QueueFree();
|
||||
_node = null;
|
||||
}
|
||||
```
|
||||
|
||||
6. **避免在构造函数中访问架构**:使用 _Ready 或 OnPhase
|
||||
```csharp
|
||||
✗ public Player()
|
||||
{
|
||||
var model = this.GetModel<PlayerModel>(); // 架构可能未就绪
|
||||
}
|
||||
|
||||
✓ public override void _Ready()
|
||||
{
|
||||
var model = this.GetModel<PlayerModel>(); // 安全
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:架构什么时候初始化?
|
||||
|
||||
**解答**:
|
||||
在根节点的 `_Ready` 方法中初始化:
|
||||
|
||||
```csharp
|
||||
public partial class GameRoot : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
new GameArchitecture().InitializeAsync().AsTask().Wait();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何在节点中访问架构?
|
||||
|
||||
**解答**:
|
||||
实现 `IController` 接口或使用扩展方法:
|
||||
|
||||
```csharp
|
||||
// 方式 1: 实现 IController
|
||||
public partial class Player : Node, IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
var model = this.GetModel<PlayerModel>();
|
||||
}
|
||||
}
|
||||
|
||||
// 方式 2: 直接使用单例
|
||||
public partial class Enemy : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
var model = GameArchitecture.Interface.GetModel<EnemyModel>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:架构锚点节点是什么?
|
||||
|
||||
**解答**:
|
||||
架构锚点是一个隐藏的节点,用于将架构绑定到 Godot 场景树。当场景树销毁时,锚点会自动触发架构清理。
|
||||
|
||||
### 问题:如何支持热重载?
|
||||
|
||||
**解答**:
|
||||
使用静态标志防止重复初始化:
|
||||
|
||||
```csharp
|
||||
private static bool _initialized;
|
||||
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
if (_initialized) return;
|
||||
base.OnInitialize();
|
||||
_initialized = true;
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:可以有多个架构吗?
|
||||
|
||||
**解答**:
|
||||
可以,但通常一个游戏只需要一个主架构。如果需要多个架构,为每个架构提供独立的单例:
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static GameArchitecture Interface { get; private set; }
|
||||
}
|
||||
|
||||
public class UiArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static UiArchitecture Interface { get; private set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:Godot 模块和普通模块有什么区别?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **普通模块**:纯 C# 逻辑,不依赖 Godot
|
||||
- **Godot 模块**:包含 Godot 节点,与场景树集成
|
||||
|
||||
```csharp
|
||||
// 普通模块
|
||||
InstallModule(new CoreModule());
|
||||
|
||||
// Godot 模块
|
||||
InstallGodotModule(new SceneModule()).Wait();
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [架构组件](/zh-CN/core/architecture) - 核心架构系统
|
||||
- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
|
||||
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
|
||||
- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||
583
docs/zh-CN/godot/scene.md
Normal file
583
docs/zh-CN/godot/scene.md
Normal file
@ -0,0 +1,583 @@
|
||||
---
|
||||
title: Godot 场景系统
|
||||
description: Godot 场景系统提供了 GFramework 场景管理与 Godot 场景树的完整集成。
|
||||
---
|
||||
|
||||
# Godot 场景系统
|
||||
|
||||
## 概述
|
||||
|
||||
Godot 场景系统是 GFramework.Godot 中连接框架场景管理与 Godot 场景树的核心组件。它提供了场景行为封装、场景工厂、场景注册表等功能,让你可以在
|
||||
Godot 项目中使用 GFramework 的场景管理系统。
|
||||
|
||||
通过 Godot 场景系统,你可以使用 GFramework 的场景路由、生命周期管理等功能,同时保持与 Godot 场景系统的完美兼容。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 场景行为封装(SceneBehavior)
|
||||
- 场景工厂和注册表
|
||||
- 与 Godot PackedScene 集成
|
||||
- 多种场景行为类型(Node2D、Node3D、Control)
|
||||
- 场景生命周期管理
|
||||
- 场景根节点管理
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 场景行为
|
||||
|
||||
`SceneBehaviorBase<T>` 封装了 Godot 节点的场景行为:
|
||||
|
||||
```csharp
|
||||
public abstract class SceneBehaviorBase<T> : ISceneBehavior
|
||||
where T : Node
|
||||
{
|
||||
protected readonly T Owner;
|
||||
public string Key { get; }
|
||||
public IScene Scene { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### 场景工厂
|
||||
|
||||
`GodotSceneFactory` 负责创建场景实例:
|
||||
|
||||
```csharp
|
||||
public class GodotSceneFactory : ISceneFactory
|
||||
{
|
||||
public ISceneBehavior Create(string sceneKey);
|
||||
}
|
||||
```
|
||||
|
||||
### 场景注册表
|
||||
|
||||
`IGodotSceneRegistry` 管理场景资源:
|
||||
|
||||
```csharp
|
||||
public interface IGodotSceneRegistry
|
||||
{
|
||||
void Register(string key, PackedScene scene);
|
||||
PackedScene Get(string key);
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建场景脚本
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.scene;
|
||||
|
||||
public partial class MainMenuScene : Control, IScene
|
||||
{
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
GD.Print("加载主菜单资源");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnEnterAsync()
|
||||
{
|
||||
GD.Print("进入主菜单");
|
||||
Show();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnPauseAsync()
|
||||
{
|
||||
GD.Print("暂停主菜单");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnResumeAsync()
|
||||
{
|
||||
GD.Print("恢复主菜单");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnExitAsync()
|
||||
{
|
||||
GD.Print("退出主菜单");
|
||||
Hide();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnUnloadAsync()
|
||||
{
|
||||
GD.Print("卸载主菜单资源");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册场景
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.scene;
|
||||
using Godot;
|
||||
|
||||
public class GameSceneRegistry : GodotSceneRegistry
|
||||
{
|
||||
publieneRegistry()
|
||||
{
|
||||
// 注册场景资源
|
||||
Register("MainMenu", GD.Load<PackedScene>("res://scenes/MainMenu.tscn"));
|
||||
Register("Gameplay", GD.Load<PackedScene>("res://scenes/Gameplay.tscn"));
|
||||
Register("Pause", GD.Load<PackedScene>("res://scenes/Pause.tscn"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 设置场景系统
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
using GFramework.Godot.scene;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 注册场景注册表
|
||||
var sceneRegistry = new GameSceneRegistry();
|
||||
RegisterUtility<IGodotSceneRegistry>(sceneRegistry);
|
||||
|
||||
// 注册场景工厂
|
||||
var sceneFactory = new GodotSceneFactory();
|
||||
RegisterUtility<ISceneFactory>(sceneFactory);
|
||||
|
||||
// 注册场景路由
|
||||
var sceneRouter = new GodotSceneRouter();
|
||||
RegisterSystem<ISceneRouter>(sceneRouter);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用场景路由
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class GameController : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
// 切换到主菜单
|
||||
SwitchToMainMenu();
|
||||
}
|
||||
|
||||
private async void SwitchToMainMenu()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.ReplaceAsync("MainMenu");
|
||||
}
|
||||
|
||||
private async void StartGame()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.ReplaceAsync("Gameplay");
|
||||
}
|
||||
|
||||
private async void ShowPause()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.PushAsync("Pause");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 使用场景行为提供者
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.scene;
|
||||
using GFramework.Godot.scene;
|
||||
|
||||
public partial class GameplayScene : Node2D, ISceneBehaviorProvider
|
||||
{
|
||||
private GameplaySceneBehavior _behavior;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_behavior = new GameplaySceneBehavior(this, "Gameplay");
|
||||
}
|
||||
|
||||
public ISceneBehavior GetScene()
|
||||
{
|
||||
return _behavior;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义场景行为
|
||||
public class GameplaySceneBehavior : Node2DSceneBehavior
|
||||
{
|
||||
public GameplaySceneBehavior(Node2D owner, string key) : base(owner, key)
|
||||
{
|
||||
}
|
||||
|
||||
protected override async ValueTask OnLoadInternalAsync(ISceneEnterParam? param)
|
||||
{
|
||||
GD.Print("加载游戏场景");
|
||||
// 加载游戏资源
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override async ValueTask OnEnterInternalAsync()
|
||||
{
|
||||
GD.Print("进入游戏场景");
|
||||
Owner.Show();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 不同类型的场景行为
|
||||
|
||||
```csharp
|
||||
// Node2D 场景
|
||||
public class Node2DSceneBehavior : SceneBehaviorBase<Node2D>
|
||||
{
|
||||
public Node2DSceneBehavior(Node2D owner, string key) : base(owner, key)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// Node3D 场景
|
||||
public class Node3DSceneBehavior : SceneBehaviorBase<Node3D>
|
||||
{
|
||||
public Node3DSceneBehavior(Node3D owner, string key) : base(owner, key)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// Control 场景(UI)
|
||||
public class ControlSceneBehavior : SceneBehaviorBase<Control>
|
||||
{
|
||||
public ControlSceneBehavior(Control owner, string key) : base(owner, key)
|
||||
{
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景根节点管理
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.scene;
|
||||
|
||||
public partial class SceneRoot : Node, ISceneRoot
|
||||
{
|
||||
private Node _currentSceneNode;
|
||||
|
||||
public void AttachScene(Node sceneNode)
|
||||
{
|
||||
// 移除旧场景
|
||||
if (_currentSceneNode != null)
|
||||
{
|
||||
RemoveChild(_currentSceneNode);
|
||||
_currentSceneNode.QueueFree();
|
||||
}
|
||||
|
||||
// 添加新场景
|
||||
_currentSceneNode = sceneNode;
|
||||
AddChild(_currentSceneNode);
|
||||
}
|
||||
|
||||
public void DetachScene(Node sceneNode)
|
||||
{
|
||||
if (_currentSceneNode == sceneNode)
|
||||
{
|
||||
RemoveChild(_currentSceneNode);
|
||||
_currentSceneNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景参数传递
|
||||
|
||||
```csharp
|
||||
// 定义场景参数
|
||||
public class GameplayEnterParam : ISceneEnterParam
|
||||
{
|
||||
public int Level { get; set; }
|
||||
public string Difficulty { get; set; }
|
||||
}
|
||||
|
||||
// 在场景中接收参数
|
||||
public partial class GameplayScene : Node2D, IScene
|
||||
{
|
||||
private int _level;
|
||||
private string _difficulty;
|
||||
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
if (param is GameplayEnterParam gameplayParam)
|
||||
{
|
||||
_level = gameplayParam.Level;
|
||||
_difficulty = gameplayParam.Difficulty;
|
||||
GD.Print($"加载关卡 {_level},难度: {_difficulty}");
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ... 其他生命周期方法
|
||||
}
|
||||
|
||||
// 切换场景时传递参数
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam
|
||||
{
|
||||
Level = 1,
|
||||
Difficulty = "Normal"
|
||||
});
|
||||
```
|
||||
|
||||
### 场景预加载
|
||||
|
||||
```csharp
|
||||
public partial class LoadingScene : Control
|
||||
{
|
||||
public override async void _Ready()
|
||||
{
|
||||
// 预加载下一个场景
|
||||
await PreloadNextScene();
|
||||
|
||||
// 切换到预加载的场景
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.ReplaceAsync("Gameplay");
|
||||
}
|
||||
|
||||
private async Task PreloadNextScene()
|
||||
{
|
||||
var sceneFactory = this.GetUtility<ISceneFactory>();
|
||||
var sceneBehavior = sceneFactory.Create("Gameplay");
|
||||
|
||||
// 预加载场景资源
|
||||
await sceneBehavior.LoadAsync(null);
|
||||
|
||||
GD.Print("场景预加载完成");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景转换动画
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.scene;
|
||||
|
||||
public class FadeTransitionHandler : ISceneTransitionHandler
|
||||
{
|
||||
private ColorRect _fadeRect;
|
||||
|
||||
public FadeTransitionHandler(ColorRect fadeRect)
|
||||
{
|
||||
_fadeRect = fadeRect;
|
||||
}
|
||||
|
||||
public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
// 淡出动画
|
||||
var tween = _fadeRect.CreateTween();
|
||||
tween.TweenProperty(_fadeRect, "modulate:a", 1.0f, 0.3f);
|
||||
await tween.ToSignal(tween, Tween.SignalName.Finished);
|
||||
}
|
||||
|
||||
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
// 淡入动画
|
||||
var tween = _fadeRect.CreateTween();
|
||||
tween.TweenProperty(_fadeRect, "modulate:a", 0.0f, 0.3f);
|
||||
await tween.ToSignal(tween, Tween.SignalName.Finished);
|
||||
}
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
### 场景间通信
|
||||
|
||||
```csharp
|
||||
// 通过事件通信
|
||||
public partial class GameplayScene : Node2D, IScene
|
||||
{
|
||||
public async ValueTask OnEnterAsync()
|
||||
{
|
||||
// 发送场景进入事件
|
||||
this.SendEvent(new GameplaySceneEnteredEvent());
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// 在其他地方监听
|
||||
public partial class HUD : Control
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
this.RegisterEvent<GameplaySceneEnteredEvent>(OnGameplayEntered);
|
||||
}
|
||||
|
||||
private void OnGameplayEntered(GameplaySceneEnteredEvent evt)
|
||||
{
|
||||
GD.Print("游戏场景已进入,显示 HUD");
|
||||
Show();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **场景脚本实现 IScene 接口**:获得完整的生命周期管理
|
||||
```csharp
|
||||
✓ public partial class MyScene : Node2D, IScene { }
|
||||
✗ public partial class MyScene : Node2D { } // 无生命周期管理
|
||||
```
|
||||
|
||||
2. **使用场景注册表管理场景资源**:集中管理所有场景
|
||||
```csharp
|
||||
public class GameSceneRegistry : GodotSceneRegistry
|
||||
{
|
||||
public GameSceneRegistry()
|
||||
{
|
||||
Register("MainMenu", GD.Load<PackedScene>("res://scenes/MainMenu.tscn"));
|
||||
Register("Gameplay", GD.Load<PackedScene>("res://scenes/Gameplay.tscn"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **在 OnLoadAsync 中加载资源**:避免场景切换卡顿
|
||||
```csharp
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
// 异步加载资源
|
||||
await LoadTexturesAsync();
|
||||
await LoadAudioAsync();
|
||||
}
|
||||
```
|
||||
|
||||
4. **使用场景根节点管理场景树**:保持场景树结构清晰
|
||||
```csharp
|
||||
// 创建场景根节点
|
||||
var sceneRoot = new Node { Name = "SceneRoot" };
|
||||
AddChild(sceneRoot);
|
||||
|
||||
// 绑定到场景路由
|
||||
sceneRouter.BindRoot(sceneRoot);
|
||||
```
|
||||
|
||||
5. **正确清理场景资源**:在 OnUnloadAsync 中释放资源
|
||||
```csharp
|
||||
public async ValueTask OnUnloadAsync()
|
||||
{
|
||||
// 释放资源
|
||||
_texture?.Dispose();
|
||||
_audioStream?.Dispose();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
```
|
||||
|
||||
6. **使用场景参数传递数据**:避免使用全局变量
|
||||
```csharp
|
||||
✓ await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam { Level = 1 });
|
||||
✗ GlobalData.CurrentLevel = 1; // 避免全局状态
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何在 Godot 场景中使用 GFramework?
|
||||
|
||||
**解答**:
|
||||
场景脚本实现 `IScene` 接口:
|
||||
|
||||
```csharp
|
||||
public partial class MyScene : Node2D, IScene
|
||||
{
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param) { }
|
||||
public async ValueTask OnEnterAsync() { }
|
||||
// ... 实现其他方法
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:场景切换时节点如何管理?
|
||||
|
||||
**解答**:
|
||||
使用场景根节点管理:
|
||||
|
||||
```csharp
|
||||
// 场景路由会自动管理节点的添加和移除
|
||||
await sceneRouter.ReplaceAsync("NewScene");
|
||||
// 旧场景节点会被移除,新场景节点会被添加
|
||||
```
|
||||
|
||||
### 问题:如何实现场景预加载?
|
||||
|
||||
**解答**:
|
||||
使用场景工厂提前创建场景:
|
||||
|
||||
```csharp
|
||||
var sceneFactory = this.GetUtility<ISceneFactory>();
|
||||
var sceneBehavior = sceneFactory.Create("NextScene");
|
||||
await sceneBehavior.LoadAsync(null);
|
||||
```
|
||||
|
||||
### 问题:场景生命周期方法的调用顺序是什么?
|
||||
|
||||
**解答**:
|
||||
|
||||
- 进入场景:`OnLoadAsync` -> `OnEnterAsync` -> `OnShow`
|
||||
- 暂停场景:`OnPause` -> `OnHide`
|
||||
- 恢复场景:`OnShow` -> `OnResume`
|
||||
- 退出场景:`OnHide` -> `OnExitAsync` -> `OnUnloadAsync`
|
||||
|
||||
### 问题:如何在场景中访问架构组件?
|
||||
|
||||
**解答**:
|
||||
使用扩展方法:
|
||||
|
||||
```csharp
|
||||
public partial class MyScene : Node2D, IScene
|
||||
{
|
||||
public async ValueTask OnEnterAsync()
|
||||
{
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
var gameSystem = this.GetSystem<GameSystem>();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:场景切换时如何显示加载界面?
|
||||
|
||||
**解答**:
|
||||
使用场景转换处理器:
|
||||
|
||||
```csharp
|
||||
public class LoadingScreenHandler : ISceneTransitionHandler
|
||||
{
|
||||
public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
// 显示加载界面
|
||||
ShowLoadingScreen();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
// 隐藏加载界面
|
||||
HideLoadingScreen();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [场景系统](/zh-CN/game/scene) - 核心场景管理
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||
- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||
643
docs/zh-CN/godot/ui.md
Normal file
643
docs/zh-CN/godot/ui.md
Normal file
@ -0,0 +1,643 @@
|
||||
---
|
||||
title: Godot UI 系统
|
||||
description: Godot UI 系统提供了 GFramework UI 管理与 Godot Control 节点的完整集成。
|
||||
---
|
||||
|
||||
# Godot UI 系统
|
||||
|
||||
## 概述
|
||||
|
||||
Godot UI 系统是 GFramework.Godot 中连接框架 UI 管理与 Godot Control 节点的核心组件。它提供了 UI 页面行为封装、UI 工厂、UI
|
||||
注册表等功能,支持多层级 UI 显示,让你可以在 Godot 项目中使用 GFramework 的 UI 管理系统。
|
||||
|
||||
通过 Godot UI 系统,你可以使用 GFramework 的 UI 路由、生命周期管理、多层级显示等功能,同时保持与 Godot UI 系统的完美兼容。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- UI 页面行为封装
|
||||
- UI 工厂和注册表
|
||||
- 与 Godot PackedScene 集成
|
||||
- 多层级 UI 支持(Page、Overlay、Modal、Toast、Topmost)
|
||||
- UI 生命周期管理
|
||||
- UI 根节点管理
|
||||
|
||||
## 核心概念
|
||||
|
||||
### UI 页面行为
|
||||
|
||||
`CanvasItemUiPageBehaviorBase<T>` 封装了 Godot Control 节点的 UI 行为:
|
||||
|
||||
```csharp
|
||||
public abstract class CanvasItemUiPageBehaviorBase<T> : IUiPageBehavior
|
||||
where T : CanvasItem
|
||||
{
|
||||
protected readonly T Owner;
|
||||
public string Key { get; }
|
||||
public UiLayer Layer { get; }
|
||||
public bool IsReentrant { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### UI 工厂
|
||||
|
||||
`GodotUiFactory` 负责创建 UI 实例:
|
||||
|
||||
```csharp
|
||||
public class GodotUiFactory : IUiFactory
|
||||
{
|
||||
public IUiPageBehavior Create(string uiKey);
|
||||
}
|
||||
```
|
||||
|
||||
### UI 层级行为
|
||||
|
||||
不同层级的 UI 有不同的行为类:
|
||||
|
||||
```csharp
|
||||
// Page 层(栈管理)
|
||||
public class PageLayerUiPageBehavior : CanvasItemUiPageBehaviorBase<Control>
|
||||
{
|
||||
public override UiLayer Layer => UiLayer.Page;
|
||||
public override bool IsReentrant => false;
|
||||
}
|
||||
|
||||
// Modal 层(模态对话框)
|
||||
public class ModalLayerUiPageBehavior : CanvasItemUiPageBehaviorBase<Control>
|
||||
{
|
||||
public override UiLayer Layer => UiLayer.Modal;
|
||||
public override bool IsReentrant => true;
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建 UI 脚本
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.ui;
|
||||
|
||||
public partial class MainMenuPage : Control, IUiPage
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
GD.Print("进入主菜单");
|
||||
Show();
|
||||
}
|
||||
|
||||
public void OnExit()
|
||||
{
|
||||
GD.Print("退出主菜单");
|
||||
Hide();
|
||||
}
|
||||
|
||||
public void OnPause()
|
||||
{
|
||||
GD.Print("暂停主菜单");
|
||||
}
|
||||
|
||||
public void OnResume()
|
||||
{
|
||||
GD.Print("恢复主菜单");
|
||||
}
|
||||
|
||||
public void OnShow()
|
||||
{
|
||||
Show();
|
||||
}
|
||||
|
||||
public void OnHide()
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实现 UI 页面行为提供者
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.ui;
|
||||
using GFramework.Godot.ui;
|
||||
|
||||
public partial class MainMenuPage : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
private PageLayerUiPageBehavior _behavior;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_behavior = new PageLayerUiPageBehavior(this, "MainMenu");
|
||||
}
|
||||
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return _behavior;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册 UI
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.ui;
|
||||
using Godot;
|
||||
|
||||
public class GameUiRegistry : GodotUiRegistry
|
||||
{
|
||||
public GameUiRegistry()
|
||||
{
|
||||
// 注册 UI 资源
|
||||
Register("MainMenu", GD.Load<PackedScene>("res://ui/MainMenu.tscn"));
|
||||
Register("Settings", GD.Load<PackedScene>("res://ui/Settings.tscn"));
|
||||
Register("ConfirmDialog", GD.Load<PackedScene>("res://ui/ConfirmDialog.tscn"));
|
||||
Register("Toast", GD.Load<PackedScene>("res://ui/Toast.tscn"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 设置 UI 系统
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
using GFramework.Godot.ui;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 注册 UI 注册表
|
||||
var uiRegistry = new GameUiRegistry();
|
||||
RegisterUtility<IGodotUiRegistry>(uiRegistry);
|
||||
|
||||
// 注册 UI 工厂
|
||||
var uiFactory = new GodotUiFactory();
|
||||
RegisterUtility<IUiFactory>(uiFactory);
|
||||
|
||||
// 注册 UI 路由
|
||||
var uiRouter = new GodotUiRouter();
|
||||
RegisterSystem<IUiRouter>(uiRouter);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 UI 路由
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class GameController : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
ShowMainMenu();
|
||||
}
|
||||
|
||||
private async void ShowMainMenu()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
await uiRouter.PushAsync("MainMenu");
|
||||
}
|
||||
|
||||
private async void ShowSettings()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
await uiRouter.PushAsync("Settings");
|
||||
}
|
||||
|
||||
private void ShowDialog()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
uiRouter.Show("ConfirmDialog", UiLayer.Modal);
|
||||
}
|
||||
|
||||
private void ShowToast(string message)
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
uiRouter.Show("Toast", UiLayer.Toast, new ToastParam { Message = message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 不同层级的 UI 行为
|
||||
|
||||
```csharp
|
||||
// Page 层 UI(栈管理,不可重入)
|
||||
public partial class MainMenuPage : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new PageLayerUiPageBehavior(this, "MainMenu");
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay 层 UI(浮层,可重入)
|
||||
public partial class InfoPanel : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new OverlayLayerUiPageBehavior(this, "InfoPanel");
|
||||
}
|
||||
}
|
||||
|
||||
// Modal 层 UI(模态对话框,可重入)
|
||||
public partial class ConfirmDialog : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new ModalLayerUiPageBehavior(this, "ConfirmDialog");
|
||||
}
|
||||
}
|
||||
|
||||
// Toast 层 UI(提示,可重入)
|
||||
public partial class ToastMessage : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new ToastLayerUiPageBehavior(this, "Toast");
|
||||
}
|
||||
}
|
||||
|
||||
// Topmost 层 UI(顶层,不可重入)
|
||||
public partial class LoadingScreen : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new TopmostLayerUiPageBehavior(this, "Loading");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI 参数传递
|
||||
|
||||
```csharp
|
||||
// 定义 UI 参数
|
||||
public class ConfirmDialogParam : IUiPageEnterParam
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Message { get; set; }
|
||||
public Action OnConfirm { get; set; }
|
||||
public Action OnCancel { get; set; }
|
||||
}
|
||||
|
||||
// 在 UI 中接收参数
|
||||
public partial class ConfirmDialog : Control, IUiPage
|
||||
{
|
||||
private Label _titleLabel;
|
||||
private Label _messageLabel;
|
||||
private Action _onConfirm;
|
||||
private Action _onCancel;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_titleLabel = GetNode<Label>("Title");
|
||||
_messageLabel = GetNode<Label>("Message");
|
||||
|
||||
GetNode<Button>("ConfirmButton").Pressed += OnConfirmPressed;
|
||||
GetNode<Button>("CancelButton").Pressed += OnCancelPressed;
|
||||
}
|
||||
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
if (param is ConfirmDialogParam dialogParam)
|
||||
{
|
||||
_titleLabel.Text = dialogParam.Title;
|
||||
_messageLabel.Text = dialogParam.Message;
|
||||
_onConfirm = dialogParam.OnConfirm;
|
||||
_onCancel = dialogParam.OnCancel;
|
||||
}
|
||||
|
||||
Show();
|
||||
}
|
||||
|
||||
private void OnConfirmPressed()
|
||||
{
|
||||
_onConfirm?.Invoke();
|
||||
CloseDialog();
|
||||
}
|
||||
|
||||
private void OnCancelPressed()
|
||||
{
|
||||
_onCancel?.Invoke();
|
||||
CloseDialog();
|
||||
}
|
||||
|
||||
private void CloseDialog()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
if (Handle.HasValue)
|
||||
{
|
||||
uiRouter.Hide(Handle.Value, UiLayer.Modal, destroy: true);
|
||||
}
|
||||
}
|
||||
|
||||
// ... 其他生命周期方法
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
uiRouter.Show("ConfirmDialog", UiLayer.Modal, new ConfirmDialogParam
|
||||
{
|
||||
Title = "确认",
|
||||
Message = "确定要退出吗?",
|
||||
OnConfirm = () => GD.Print("确认"),
|
||||
OnCancel = () => GD.Print("取消")
|
||||
});
|
||||
```
|
||||
|
||||
### UI 根节点管理
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.ui;
|
||||
|
||||
public partial class UiRoot : CanvasLayer, IUiRoot
|
||||
{
|
||||
private Control _pageLayer;
|
||||
private Control _overlayLayer;
|
||||
private Control _modalLayer;
|
||||
private Control _toastLayer;
|
||||
private Control _topmostLayer;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 创建各层级容器
|
||||
_pageLayer = new Control { Name = "PageLayer" };
|
||||
_overlayLayer = new Control { Name = "OverlayLayer" };
|
||||
_modalLayer = new Control { Name = "ModalLayer" };
|
||||
_toastLayer = new Control { Name = "ToastLayer" };
|
||||
_topmostLayer = new Control { Name = "TopmostLayer" };
|
||||
|
||||
AddChild(_pageLayer);
|
||||
AddChild(_overlayLayer);
|
||||
AddChild(_modalLayer);
|
||||
AddChild(_toastLayer);
|
||||
AddChild(_topmostLayer);
|
||||
}
|
||||
|
||||
public void AttachPage(Control page, UiLayer layer)
|
||||
{
|
||||
var container = GetLayerContainer(layer);
|
||||
container.AddChild(page);
|
||||
}
|
||||
|
||||
public void DetachPage(Control page, UiLayer layer)
|
||||
{
|
||||
var container = GetLayerContainer(layer);
|
||||
container.RemoveChild(page);
|
||||
}
|
||||
|
||||
private Control GetLayerContainer(UiLayer layer)
|
||||
{
|
||||
return layer switch
|
||||
{
|
||||
UiLayer.Page => _pageLayer,
|
||||
UiLayer.Overlay => _overlayLayer,
|
||||
UiLayer.Modal => _modalLayer,
|
||||
UiLayer.Toast => _toastLayer,
|
||||
UiLayer.Topmost => _topmostLayer,
|
||||
_ => _pageLayer
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI 动画和过渡
|
||||
|
||||
```csharp
|
||||
public partial class AnimatedPage : Control, IUiPage
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
// 淡入动画
|
||||
Modulate = new Color(1, 1, 1, 0);
|
||||
Show();
|
||||
|
||||
var tween = CreateTween();
|
||||
tween.TweenProperty(this, "modulate:a", 1.0f, 0.3f);
|
||||
}
|
||||
|
||||
public void OnExit()
|
||||
{
|
||||
// 淡出动画
|
||||
var tween = CreateTween();
|
||||
tween.TweenProperty(this, "modulate:a", 0.0f, 0.3f);
|
||||
tween.TweenCallback(Callable.From(Hide));
|
||||
}
|
||||
|
||||
public void OnShow()
|
||||
{
|
||||
Show();
|
||||
}
|
||||
|
||||
public void OnHide()
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
### UI 句柄管理
|
||||
|
||||
```csharp
|
||||
public partial class DialogManager : Node
|
||||
{
|
||||
private UiHandle? _currentDialog;
|
||||
|
||||
public void ShowDialog(string dialogKey)
|
||||
{
|
||||
// 关闭当前对话框
|
||||
CloseCurrentDialog();
|
||||
|
||||
// 显示新对话框
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
_currentDialog = uiRouter.Show(dialogKey, UiLayer.Modal);
|
||||
}
|
||||
|
||||
public void CloseCurrentDialog()
|
||||
{
|
||||
if (_currentDialog.HasValue)
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
uiRouter.Hide(_currentDialog.Value, UiLayer.Modal, destroy: true);
|
||||
_currentDialog = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 多个 Toast 显示
|
||||
|
||||
```csharp
|
||||
public partial class ToastManager : Node
|
||||
{
|
||||
private readonly List<UiHandle> _activeToasts = new();
|
||||
|
||||
public void ShowToast(string message, float duration = 3.0f)
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// Toast 层支持重入,可以同时显示多个
|
||||
var handle = uiRouter.Show("Toast", UiLayer.Toast, new ToastParam
|
||||
{
|
||||
Message = message
|
||||
});
|
||||
|
||||
_activeToasts.Add(handle);
|
||||
|
||||
// 自动隐藏
|
||||
GetTree().CreateTimer(duration).Timeout += () =>
|
||||
{
|
||||
uiRouter.Hide(handle, UiLayer.Toast, destroy: true);
|
||||
_activeToasts.Remove(handle);
|
||||
};
|
||||
}
|
||||
|
||||
public void ClearAllToasts()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
foreach (var handle in _activeToasts)
|
||||
{
|
||||
uiRouter.Hide(handle, UiLayer.Toast, destroy: true);
|
||||
}
|
||||
|
||||
_activeToasts.Clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **UI 脚本实现 IUiPage 接口**:获得完整的生命周期管理
|
||||
```csharp
|
||||
✓ public partial class MyPage : Control, IUiPage { }
|
||||
✗ public partial class MyPage : Control { } // 无生命周期管理
|
||||
```
|
||||
|
||||
2. **使用正确的 UI 层级**:根据 UI 类型选择合适的层级
|
||||
```csharp
|
||||
✓ Page: 主要页面(主菜单、设置)
|
||||
✓ Overlay: 浮层(信息面板)
|
||||
✓ Modal: 模态对话框(确认框)
|
||||
✓ Toast: 提示消息
|
||||
✓ Topmost: 系统级(加载界面)
|
||||
```
|
||||
|
||||
3. **在 OnEnter 中显示 UI**:确保 UI 正确显示
|
||||
```csharp
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
Show(); // 显示 UI
|
||||
// 初始化 UI 状态
|
||||
}
|
||||
```
|
||||
|
||||
4. **在 OnExit 中隐藏 UI**:确保 UI 正确隐藏
|
||||
```csharp
|
||||
public void OnExit()
|
||||
{
|
||||
Hide(); // 隐藏 UI
|
||||
// 清理 UI 状态
|
||||
}
|
||||
```
|
||||
|
||||
5. **使用 UI 句柄管理非栈 UI**:对于 Modal、Toast 等层级
|
||||
```csharp
|
||||
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
|
||||
// 保存句柄以便后续关闭
|
||||
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
|
||||
```
|
||||
|
||||
6. **使用 UI 参数传递数据**:避免使用全局变量
|
||||
```csharp
|
||||
✓ uiRouter.Show("Dialog", UiLayer.Modal, new DialogParam { ... });
|
||||
✗ GlobalData.DialogMessage = "..."; // 避免全局状态
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何在 Godot UI 中使用 GFramework?
|
||||
|
||||
**解答**:
|
||||
UI 脚本实现 `IUiPage` 和 `IUiPageBehaviorProvider` 接口:
|
||||
|
||||
```csharp
|
||||
public partial class MyPage : Control, IUiPage, IUiPageBehaviorProvider
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param) { }
|
||||
public IUiPageBehavior GetPage() { return new PageLayerUiPageBehavior(this, "MyPage"); }
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:UI 层级有什么区别?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **Page**:栈管理,不可重入,用于主要页面
|
||||
- **Overlay**:可重入,用于浮层
|
||||
- **Modal**:可重入,带遮罩,用于对话框
|
||||
- **Toast**:可重入,轻量提示
|
||||
- **Topmost**:不可重入,最高优先级
|
||||
|
||||
### 问题:如何实现 UI 动画?
|
||||
|
||||
**解答**:
|
||||
在生命周期方法中使用 Godot Tween:
|
||||
|
||||
```csharp
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
var tween = CreateTween();
|
||||
tween.TweenProperty(this, "modulate:a", 1.0f, 0.3f);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何在 UI 中访问架构组件?
|
||||
|
||||
**解答**:
|
||||
使用扩展方法:
|
||||
|
||||
```csharp
|
||||
public partial class MyPage : Control, IUiPage
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
var gameSystem = this.GetSystem<GameSystem>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何关闭 Modal 或 Toast?
|
||||
|
||||
**解答**:
|
||||
使用 UI 句柄:
|
||||
|
||||
```csharp
|
||||
// 显示时保存句柄
|
||||
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
|
||||
|
||||
// 关闭时使用句柄
|
||||
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
|
||||
```
|
||||
|
||||
### 问题:UI 生命周期方法的调用顺序是什么?
|
||||
|
||||
**解答**:
|
||||
|
||||
- 进入:`OnEnter` -> `OnShow`
|
||||
- 暂停:`OnPause` -> `OnHide`
|
||||
- 恢复:`OnShow` -> `OnResume`
|
||||
- 退出:`OnHide` -> `OnExit`
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [UI 系统](/zh-CN/game/ui) - 核心 UI 管理
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||
598
docs/zh-CN/tutorials/coroutine-tutorial.md
Normal file
598
docs/zh-CN/tutorials/coroutine-tutorial.md
Normal file
@ -0,0 +1,598 @@
|
||||
---
|
||||
title: 使用协程系统
|
||||
description: 学习如何使用协程系统实现异步操作和时间控制
|
||||
---
|
||||
|
||||
# 使用协程系统
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- 理解协程的基本概念和执行机制
|
||||
- 创建和启动协程
|
||||
- 使用各种等待指令控制协程执行
|
||||
- 在架构组件中使用协程
|
||||
- 实现常见的游戏逻辑(延迟执行、循环任务、事件等待)
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已安装 GFramework.Core NuGet 包
|
||||
- 了解 C# 基础语法和迭代器(IEnumerator)
|
||||
- 阅读过[架构概览](/zh-CN/getting-started)
|
||||
- 了解[生命周期管理](/zh-CN/core/lifecycle)
|
||||
|
||||
## 步骤 1:创建第一个协程
|
||||
|
||||
首先,让我们创建一个简单的协程来理解基本概念。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.coroutine;
|
||||
using GFramework.Core.coroutine;
|
||||
using GFramework.Core.coroutine.instructions;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
public class TutorialSystem : AbstractSystem
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 启动协程
|
||||
this.StartCoroutine(MyFirstCoroutine());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 第一个协程示例
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> MyFirstCoroutine()
|
||||
{
|
||||
Console.WriteLine("协程开始执行");
|
||||
|
||||
// 等待 1 秒
|
||||
yield return CoroutineHelper.WaitForSeconds(1.0);
|
||||
|
||||
Console.WriteLine("1 秒后执行");
|
||||
|
||||
// 等待 1 帧
|
||||
yield return CoroutineHelper.WaitForOneFrame();
|
||||
|
||||
Console.WriteLine("下一帧执行");
|
||||
|
||||
// 等待 5 帧
|
||||
yield return CoroutineHelper.WaitForFrames(5);
|
||||
|
||||
Console.WriteLine("5 帧后执行");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 协程方法返回 `IEnumerator<IYieldInstruction>`
|
||||
- 使用 `yield return` 返回等待指令
|
||||
- `this.StartCoroutine()` 扩展方法启动协程
|
||||
- `WaitForSeconds` 等待指定秒数
|
||||
- `WaitForOneFrame` 等待一帧
|
||||
- `WaitForFrames` 等待多帧
|
||||
|
||||
## 步骤 2:实现生命值自动恢复
|
||||
|
||||
让我们实现一个实用的功能:玩家生命值自动恢复。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.model;
|
||||
using GFramework.Core.Abstractions.property;
|
||||
using GFramework.Core.model;
|
||||
|
||||
namespace MyGame.Models
|
||||
{
|
||||
public class PlayerModel : AbstractModel
|
||||
{
|
||||
// 当前生命值
|
||||
public BindableProperty<int> Health { get; } = new(100);
|
||||
|
||||
// 最大生命值
|
||||
public BindableProperty<int> MaxHealth { get; } = new(100);
|
||||
|
||||
// 是否启用自动恢复
|
||||
public BindableProperty<bool> AutoRegenEnabled { get; } = new(true);
|
||||
|
||||
private CoroutineHandle? _regenHandle;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 启动生命值恢复协程
|
||||
StartHealthRegeneration();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动生命值恢复
|
||||
/// </summary>
|
||||
public void StartHealthRegeneration()
|
||||
{
|
||||
// 如果已经在运行,先停止
|
||||
if (_regenHandle.HasValue)
|
||||
{
|
||||
this.StopCoroutine(_regenHandle.Value);
|
||||
}
|
||||
|
||||
// 启动新的恢复协程
|
||||
_regenHandle = this.StartCoroutine(HealthRegenerationCoroutine());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止生命值恢复
|
||||
/// </summary>
|
||||
public void StopHealthRegeneration()
|
||||
{
|
||||
if (_regenHandle.HasValue)
|
||||
{
|
||||
this.StopCoroutine(_regenHandle.Value);
|
||||
_regenHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生命值恢复协程
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> HealthRegenerationCoroutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// 等待 1 秒
|
||||
yield return CoroutineHelper.WaitForSeconds(1.0);
|
||||
|
||||
// 检查是否启用自动恢复
|
||||
if (!AutoRegenEnabled.Value)
|
||||
continue;
|
||||
|
||||
// 如果生命值未满,恢复 5 点
|
||||
if (Health.Value < MaxHealth.Value)
|
||||
{
|
||||
Health.Value = Math.Min(Health.Value + 5, MaxHealth.Value);
|
||||
Console.WriteLine($"生命值恢复: {Health.Value}/{MaxHealth.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用 `while (true)` 创建无限循环协程
|
||||
- 保存协程句柄以便后续控制
|
||||
- 使用 `StopCoroutine` 停止协程
|
||||
- 协程中可以访问类成员变量
|
||||
|
||||
## 步骤 3:实现技能冷却系统
|
||||
|
||||
接下来实现一个技能冷却系统,展示如何使用协程管理时间相关的游戏逻辑。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
public class SkillSystem : AbstractSystem
|
||||
{
|
||||
// 技能冷却状态
|
||||
private readonly Dictionary<string, bool> _skillCooldowns = new();
|
||||
|
||||
/// <summary>
|
||||
/// 使用技能
|
||||
/// </summary>
|
||||
public bool UseSkill(string skillName, double cooldownTime)
|
||||
{
|
||||
// 检查是否在冷却中
|
||||
if (_skillCooldowns.TryGetValue(skillName, out var isOnCooldown) && isOnCooldown)
|
||||
{
|
||||
Console.WriteLine($"技能 {skillName} 冷却中...");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行技能
|
||||
Console.WriteLine($"使用技能: {skillName}");
|
||||
|
||||
// 启动冷却协程
|
||||
this.StartCoroutine(SkillCooldownCoroutine(skillName, cooldownTime));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 技能冷却协程
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> SkillCooldownCoroutine(string skillName, double cooldownTime)
|
||||
{
|
||||
// 标记为冷却中
|
||||
_skillCooldowns[skillName] = true;
|
||||
|
||||
Console.WriteLine($"技能 {skillName} 开始冷却 {cooldownTime} 秒");
|
||||
|
||||
// 等待冷却时间
|
||||
yield return CoroutineHelper.WaitForSeconds(cooldownTime);
|
||||
|
||||
// 冷却结束
|
||||
_skillCooldowns[skillName] = false;
|
||||
Console.WriteLine($"技能 {skillName} 冷却完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带进度显示的技能冷却
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> SkillCooldownWithProgressCoroutine(
|
||||
string skillName,
|
||||
double cooldownTime)
|
||||
{
|
||||
_skillCooldowns[skillName] = true;
|
||||
|
||||
// 使用 WaitForProgress 显示冷却进度
|
||||
yield return CoroutineHelper.WaitForProgress(
|
||||
duration: cooldownTime,
|
||||
onProgress: progress =>
|
||||
{
|
||||
Console.WriteLine($"技能 {skillName} 冷却进度: {progress * 100:F0}%");
|
||||
}
|
||||
);
|
||||
|
||||
_skillCooldowns[skillName] = false;
|
||||
Console.WriteLine($"技能 {skillName} 冷却完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用字典管理多个技能的冷却状态
|
||||
- 每个技能使用独立的协程管理冷却
|
||||
- `WaitForProgress` 可以在等待期间执行回调
|
||||
- 协程结束后自动清理冷却状态
|
||||
|
||||
## 步骤 4:等待事件触发
|
||||
|
||||
实现一个等待玩家完成任务的系统,展示如何在协程中等待事件。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.events;
|
||||
using GFramework.Core.coroutine.instructions;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
// 任务完成事件
|
||||
public record QuestCompletedEvent(int QuestId, string QuestName) : IEvent;
|
||||
|
||||
public class QuestSystem : AbstractSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 开始任务并等待完成
|
||||
/// </summary>
|
||||
public void StartQuest(int questId, string questName)
|
||||
{
|
||||
this.StartCoroutine(QuestCoroutine(questId, questName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务协程
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> QuestCoroutine(int questId, string questName)
|
||||
{
|
||||
Console.WriteLine($"任务开始: {questName}");
|
||||
|
||||
// 获取事件总线
|
||||
var eventBus = this.GetService<IEventBus>();
|
||||
|
||||
// 等待任务完成事件
|
||||
var waitEvent = new WaitForEvent<QuestCompletedEvent>(
|
||||
eventBus,
|
||||
evt => evt.QuestId == questId // 过滤条件
|
||||
);
|
||||
|
||||
yield return waitEvent;
|
||||
|
||||
// 获取事件数据
|
||||
var completedEvent = waitEvent.EventData;
|
||||
Console.WriteLine($"任务完成: {completedEvent.QuestName}");
|
||||
|
||||
// 发放奖励
|
||||
GiveReward(questId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带超时的任务
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> TimedQuestCoroutine(
|
||||
int questId,
|
||||
string questName,
|
||||
double timeLimit)
|
||||
{
|
||||
Console.WriteLine($"限时任务开始: {questName} (时限: {timeLimit}秒)");
|
||||
|
||||
var eventBus = this.GetService<IEventBus>();
|
||||
|
||||
// 等待事件,带超时
|
||||
var waitEvent = new WaitForEventWithTimeout<QuestCompletedEvent>(
|
||||
eventBus,
|
||||
timeout: timeLimit,
|
||||
predicate: evt => evt.QuestId == questId
|
||||
);
|
||||
|
||||
yield return waitEvent;
|
||||
|
||||
if (waitEvent.IsTimeout)
|
||||
{
|
||||
Console.WriteLine($"任务超时失败: {questName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"任务完成: {questName}");
|
||||
GiveReward(questId);
|
||||
}
|
||||
}
|
||||
|
||||
private void GiveReward(int questId)
|
||||
{
|
||||
Console.WriteLine($"发放任务 {questId} 的奖励");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `WaitForEvent` 等待特定事件触发
|
||||
- 可以使用 `predicate` 参数过滤事件
|
||||
- `WaitForEventWithTimeout` 支持超时机制
|
||||
- 通过 `EventData` 属性获取事件数据
|
||||
|
||||
## 步骤 5:协程组合与嵌套
|
||||
|
||||
实现一个复杂的游戏流程,展示如何组合多个协程。
|
||||
|
||||
```csharp
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
public class GameFlowSystem : AbstractSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏开始流程
|
||||
/// </summary>
|
||||
public void StartGame()
|
||||
{
|
||||
this.StartCoroutine(GameStartSequence());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏开始序列
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> GameStartSequence()
|
||||
{
|
||||
Console.WriteLine("=== 游戏开始 ===");
|
||||
|
||||
// 1. 显示标题
|
||||
yield return ShowTitle();
|
||||
|
||||
// 2. 加载资源
|
||||
yield return LoadResources();
|
||||
|
||||
// 3. 初始化玩家
|
||||
yield return InitializePlayer();
|
||||
|
||||
// 4. 播放开场动画
|
||||
yield return PlayOpeningAnimation();
|
||||
|
||||
Console.WriteLine("=== 游戏准备完成 ===");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示标题
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> ShowTitle()
|
||||
{
|
||||
Console.WriteLine("显示游戏标题...");
|
||||
yield return CoroutineHelper.WaitForSeconds(2.0);
|
||||
Console.WriteLine("标题显示完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载资源
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> LoadResources()
|
||||
{
|
||||
Console.WriteLine("开始加载资源...");
|
||||
|
||||
// 并行加载多个资源
|
||||
var loadTextures = LoadTexturesCoroutine();
|
||||
var loadAudio = LoadAudioCoroutine();
|
||||
var loadModels = LoadModelsCoroutine();
|
||||
|
||||
// 等待所有资源加载完成
|
||||
yield return new WaitForAllCoroutines(
|
||||
this.GetCoroutineScheduler(),
|
||||
loadTextures,
|
||||
loadAudio,
|
||||
loadModels
|
||||
);
|
||||
|
||||
Console.WriteLine("所有资源加载完成");
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> LoadTexturesCoroutine()
|
||||
{
|
||||
Console.WriteLine(" 加载纹理...");
|
||||
yield return CoroutineHelper.WaitForSeconds(1.0);
|
||||
Console.WriteLine(" 纹理加载完成");
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> LoadAudioCoroutine()
|
||||
{
|
||||
Console.WriteLine(" 加载音频...");
|
||||
yield return CoroutineHelper.WaitForSeconds(1.5);
|
||||
Console.WriteLine(" 音频加载完成");
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> LoadModelsCoroutine()
|
||||
{
|
||||
Console.WriteLine(" 加载模型...");
|
||||
yield return CoroutineHelper.WaitForSeconds(0.8);
|
||||
Console.WriteLine(" 模型加载完成");
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> InitializePlayer()
|
||||
{
|
||||
Console.WriteLine("初始化玩家...");
|
||||
yield return CoroutineHelper.WaitForSeconds(0.5);
|
||||
Console.WriteLine("玩家初始化完成");
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> PlayOpeningAnimation()
|
||||
{
|
||||
Console.WriteLine("播放开场动画...");
|
||||
yield return CoroutineHelper.WaitForSeconds(3.0);
|
||||
Console.WriteLine("开场动画播放完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取协程调度器
|
||||
/// </summary>
|
||||
private CoroutineScheduler GetCoroutineScheduler()
|
||||
{
|
||||
// 从架构服务中获取
|
||||
return this.GetService<CoroutineScheduler>();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用 `yield return` 调用其他协程实现嵌套
|
||||
- `WaitForAllCoroutines` 并行执行多个协程
|
||||
- 协程可以像函数一样组合和复用
|
||||
- 清晰的流程控制,避免回调嵌套
|
||||
|
||||
## 完整代码
|
||||
|
||||
### GameArchitecture.cs
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.architecture;
|
||||
|
||||
namespace MyGame
|
||||
{
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
public static IArchitecture Interface { get; private set; }
|
||||
|
||||
protected override void Init()
|
||||
{
|
||||
Interface = this;
|
||||
|
||||
// 注册 Model
|
||||
RegisterModel(new PlayerModel());
|
||||
|
||||
// 注册 System
|
||||
RegisterSystem(new TutorialSystem());
|
||||
RegisterSystem(new SkillSystem());
|
||||
RegisterSystem(new QuestSystem());
|
||||
RegisterSystem(new GameFlowSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 测试代码
|
||||
|
||||
```csharp
|
||||
using MyGame;
|
||||
using MyGame.Systems;
|
||||
|
||||
// 初始化架构
|
||||
var architecture = new GameArchitecture();
|
||||
architecture.Initialize();
|
||||
await architecture.WaitUntilReadyAsync();
|
||||
|
||||
// 测试技能系统
|
||||
var skillSystem = architecture.GetSystem<SkillSystem>();
|
||||
skillSystem.UseSkill("火球术", 3.0);
|
||||
await Task.Delay(1000);
|
||||
skillSystem.UseSkill("火球术", 3.0); // 冷却中
|
||||
await Task.Delay(3000);
|
||||
skillSystem.UseSkill("火球术", 3.0); // 冷却完成
|
||||
|
||||
// 测试任务系统
|
||||
var questSystem = architecture.GetSystem<QuestSystem>();
|
||||
questSystem.StartQuest(1, "击败史莱姆");
|
||||
|
||||
// 模拟任务完成
|
||||
await Task.Delay(2000);
|
||||
var eventBus = architecture.GetService<IEventBus>();
|
||||
eventBus.Publish(new QuestCompletedEvent(1, "击败史莱姆"));
|
||||
|
||||
// 测试游戏流程
|
||||
var gameFlowSystem = architecture.GetSystem<GameFlowSystem>();
|
||||
gameFlowSystem.StartGame();
|
||||
```
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行程序后,你将看到类似以下的输出:
|
||||
|
||||
```
|
||||
协程开始执行
|
||||
1 秒后执行
|
||||
下一帧执行
|
||||
5 帧后执行
|
||||
|
||||
使用技能: 火球术
|
||||
技能 火球术 开始冷却 3.0 秒
|
||||
技能 火球术 冷却中...
|
||||
技能 火球术 冷却完成
|
||||
使用技能: 火球术
|
||||
|
||||
任务开始: 击败史莱姆
|
||||
任务完成: 击败史莱姆
|
||||
发放任务 1 的奖励
|
||||
|
||||
=== 游戏开始 ===
|
||||
显示游戏标题...
|
||||
标题显示完成
|
||||
开始加载资源...
|
||||
加载纹理...
|
||||
加载音频...
|
||||
加载模型...
|
||||
模型加载完成
|
||||
纹理加载完成
|
||||
音频加载完成
|
||||
所有资源加载完成
|
||||
初始化玩家...
|
||||
玩家初始化完成
|
||||
播放开场动画...
|
||||
开场动画播放完成
|
||||
=== 游戏准备完成 ===
|
||||
```
|
||||
|
||||
**验证步骤**:
|
||||
|
||||
1. 协程按预期顺序执行
|
||||
2. 技能冷却系统正常工作
|
||||
3. 事件等待功能正确
|
||||
4. 并行加载资源成功
|
||||
|
||||
## 下一步
|
||||
|
||||
恭喜!你已经掌握了协程系统的基本用法。接下来可以学习:
|
||||
|
||||
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial) - 使用协程实现状态转换
|
||||
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 在协程中加载资源
|
||||
- [使用命令系统](/zh-CN/tutorials/command-tutorial) - 协程与命令系统集成
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [协程系统](/zh-CN/core/coroutine) - 协程系统详细说明
|
||||
- [事件系统](/zh-CN/core/events) - 事件系统详解
|
||||
- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
|
||||
- [System 层](/zh-CN/core/system) - System 详细说明
|
||||
813
docs/zh-CN/tutorials/godot-complete-project.md
Normal file
813
docs/zh-CN/tutorials/godot-complete-project.md
Normal file
@ -0,0 +1,813 @@
|
||||
---
|
||||
title: Godot 完整项目搭建
|
||||
description: 从零开始使用 GFramework 构建一个完整的 Godot 游戏项目
|
||||
---
|
||||
|
||||
# Godot 完整项目搭建
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- 在 Godot 项目中集成 GFramework
|
||||
- 创建完整的游戏架构
|
||||
- 实现场景管理和 UI 系统
|
||||
- 使用协程和事件系统
|
||||
- 实现游戏存档功能
|
||||
- 构建一个可运行的完整游戏
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已安装 Godot 4.x
|
||||
- 已安装 .NET SDK 8.0+
|
||||
- 了解 C# 和 Godot 基础
|
||||
- 阅读过前面的教程:
|
||||
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial)
|
||||
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial)
|
||||
- [实现存档系统](/zh-CN/tutorials/save-system)
|
||||
|
||||
## 项目概述
|
||||
|
||||
我们将创建一个简单的 2D 射击游戏,包含以下功能:
|
||||
|
||||
- 主菜单和游戏场景
|
||||
- 玩家控制和射击
|
||||
- 敌人生成和 AI
|
||||
- 分数和生命值系统
|
||||
- 游戏存档和加载
|
||||
- 暂停菜单
|
||||
|
||||
## 步骤 1:创建 Godot 项目并配置
|
||||
|
||||
首先创建 Godot 项目并添加 GFramework 依赖。
|
||||
|
||||
### 1.1 创建项目
|
||||
|
||||
1. 打开 Godot,创建新项目 "MyShooterGame"
|
||||
2. 选择 C# 作为脚本语言
|
||||
3. 创建项目后,在项目根目录创建 `.csproj` 文件
|
||||
|
||||
### 1.2 添加 NuGet 包
|
||||
|
||||
编辑 `MyShooterGame.csproj`:
|
||||
|
||||
```xml
|
||||
<Project Sdk="Godot.NET.Sdk/4.3.0">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- GFramework 包 -->
|
||||
<PackageReference Include="GFramework.Core" Version="1.0.0" />
|
||||
<PackageReference Include="GFramework.Game" Version="1.0.0" />
|
||||
<PackageReference Include="GFramework.Godot" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
### 1.3 创建项目结构
|
||||
|
||||
```
|
||||
MyShooterGame/
|
||||
├── Scripts/
|
||||
│ ├── Architecture/
|
||||
│ │ └── GameArchitecture.cs
|
||||
│ ├── Models/
|
||||
│ │ ├── PlayerModel.cs
|
||||
│ │ └── GameModel.cs
|
||||
│ ├── Systems/
|
||||
│ │ ├── GameplaySystem.cs
|
||||
│ │ └── SpawnSystem.cs
|
||||
│ ├── Controllers/
|
||||
│ │ └── PlayerController.cs
|
||||
│ └── Data/
|
||||
│ └── GameSaveData.cs
|
||||
├── Scenes/
|
||||
│ ├── Main.tscn
|
||||
│ ├── Menu.tscn
|
||||
│ ├── Game.tscn
|
||||
│ ├── Player.tscn
|
||||
│ └── Enemy.tscn
|
||||
└── UI/
|
||||
├── MainMenu.tscn
|
||||
├── HUD.tscn
|
||||
└── PauseMenu.tscn
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用 Godot.NET.Sdk 4.3.0
|
||||
- 添加 GFramework 的三个核心包
|
||||
- 按功能组织代码结构
|
||||
|
||||
## 步骤 2:创建游戏架构
|
||||
|
||||
实现游戏的核心架构和数据模型。
|
||||
|
||||
### 2.1 定义数据模型
|
||||
|
||||
```csharp
|
||||
// Scripts/Models/PlayerModel.cs
|
||||
using GFramework.Core.model;
|
||||
using GFramework.Core.Abstractions.property;
|
||||
|
||||
namespace MyShooterGame.Models
|
||||
{
|
||||
public class PlayerModel : AbstractModel
|
||||
{
|
||||
public BindableProperty<int> Health { get; } = new(100);
|
||||
public BindableProperty<int> MaxHealth { get; } = new(100);
|
||||
public BindableProperty<int> Score { get; } = new(0);
|
||||
public BindableProperty<int> Lives { get; } = new(3);
|
||||
public BindableProperty<bool> IsAlive { get; } = new(true);
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 监听生命值变化
|
||||
Health.RegisterOnValueChanged(health =>
|
||||
{
|
||||
if (health <= 0)
|
||||
{
|
||||
IsAlive.Value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Health.Value = MaxHealth.Value;
|
||||
Score.Value = 0;
|
||||
Lives.Value = 3;
|
||||
IsAlive.Value = true;
|
||||
}
|
||||
|
||||
public void TakeDamage(int damage)
|
||||
{
|
||||
Health.Value = Math.Max(0, Health.Value - damage);
|
||||
}
|
||||
|
||||
public void AddScore(int points)
|
||||
{
|
||||
Score.Value += points;
|
||||
}
|
||||
|
||||
public void LoseLife()
|
||||
{
|
||||
Lives.Value = Math.Max(0, Lives.Value - 1);
|
||||
if (Lives.Value > 0)
|
||||
{
|
||||
Health.Value = MaxHealth.Value;
|
||||
IsAlive.Value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Scripts/Models/GameModel.cs
|
||||
using GFramework.Core.model;
|
||||
using GFramework.Core.Abstractions.property;
|
||||
|
||||
namespace MyShooterGame.Models
|
||||
{
|
||||
public class GameModel : AbstractModel
|
||||
{
|
||||
public BindableProperty<bool> IsPlaying { get; } = new(false);
|
||||
public BindableProperty<bool> IsPaused { get; } = new(false);
|
||||
public BindableProperty<int> CurrentWave { get; } = new(1);
|
||||
public BindableProperty<int> EnemiesAlive { get; } = new(0);
|
||||
public BindableProperty<float> GameTime { get; } = new(0f);
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 初始化
|
||||
}
|
||||
|
||||
public void StartGame()
|
||||
{
|
||||
IsPlaying.Value = true;
|
||||
IsPaused.Value = false;
|
||||
CurrentWave.Value = 1;
|
||||
EnemiesAlive.Value = 0;
|
||||
GameTime.Value = 0f;
|
||||
}
|
||||
|
||||
public void PauseGame()
|
||||
{
|
||||
IsPaused.Value = true;
|
||||
}
|
||||
|
||||
public void ResumeGame()
|
||||
{
|
||||
IsPaused.Value = false;
|
||||
}
|
||||
|
||||
public void EndGame()
|
||||
{
|
||||
IsPlaying.Value = false;
|
||||
IsPaused.Value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 定义存档数据
|
||||
|
||||
```csharp
|
||||
// Scripts/Data/GameSaveData.cs
|
||||
using GFramework.Game.Abstractions.data;
|
||||
using System;
|
||||
|
||||
namespace MyShooterGame.Data
|
||||
{
|
||||
public class GameSaveData : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public DateTime SaveTime { get; set; }
|
||||
|
||||
// 玩家数据
|
||||
public int HighScore { get; set; }
|
||||
public int TotalKills { get; set; }
|
||||
public float TotalPlayTime { get; set; }
|
||||
|
||||
// 设置
|
||||
public float MusicVolume { get; set; } = 0.8f;
|
||||
public float SfxVolume { get; set; } = 1.0f;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 创建游戏架构
|
||||
|
||||
```csharp
|
||||
// Scripts/Architecture/GameArchitecture.cs
|
||||
using GFramework.Godot.architecture;
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Game.Abstractions.data;
|
||||
using GFramework.Game.Abstractions.storage;
|
||||
using GFramework.Game.data;
|
||||
using GFramework.Game.storage;
|
||||
using MyShooterGame.Models;
|
||||
using MyShooterGame.Systems;
|
||||
using MyShooterGame.Data;
|
||||
using Godot;
|
||||
|
||||
namespace MyShooterGame.Architecture
|
||||
{
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static GameArchitecture Interface { get; private set; }
|
||||
|
||||
public GameArchitecture()
|
||||
{
|
||||
Interface = this;
|
||||
}
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
GD.Print("=== 初始化游戏架构 ===");
|
||||
|
||||
// 注册存储系统
|
||||
var storage = new FileStorage("user://saves");
|
||||
RegisterUtility<IFileStorage>(storage);
|
||||
|
||||
// 注册存档仓库
|
||||
var saveConfig = new SaveConfiguration
|
||||
{
|
||||
SaveRoot = "",
|
||||
SaveSlotPrefix = "save_",
|
||||
SaveFileName = "data.json"
|
||||
};
|
||||
var saveRepo = new SaveRepository<GameSaveData>(storage, saveConfig);
|
||||
RegisterUtility<ISaveRepository<GameSaveData>>(saveRepo);
|
||||
|
||||
// 注册 Model
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterModel(new GameModel());
|
||||
|
||||
// 注册 System
|
||||
RegisterSystem(new GameplaySystem());
|
||||
RegisterSystem(new SpawnSystem());
|
||||
|
||||
GD.Print("游戏架构初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `PlayerModel` 管理玩家状态
|
||||
- `GameModel` 管理游戏状态
|
||||
- `GameSaveData` 定义存档结构
|
||||
- `GameArchitecture` 注册所有组件
|
||||
|
||||
## 步骤 3:实现游戏系统
|
||||
|
||||
创建游戏逻辑系统。
|
||||
|
||||
### 3.1 游戏逻辑系统
|
||||
|
||||
```csharp
|
||||
// Scripts/Systems/GameplaySystem.cs
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.extensions;
|
||||
using MyShooterGame.Models;
|
||||
using Godot;
|
||||
|
||||
namespace MyShooterGame.Systems
|
||||
{
|
||||
public class GameplaySystem : AbstractSystem
|
||||
{
|
||||
public void StartNewGame()
|
||||
{
|
||||
GD.Print("开始新游戏");
|
||||
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
|
||||
// 重置数据
|
||||
playerModel.Reset();
|
||||
gameModel.StartGame();
|
||||
}
|
||||
|
||||
public void GameOver()
|
||||
{
|
||||
GD.Print("游戏结束");
|
||||
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
gameModel.EndGame();
|
||||
|
||||
// 保存最高分
|
||||
SaveHighScore();
|
||||
}
|
||||
|
||||
public void PauseGame()
|
||||
{
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
gameModel.PauseGame();
|
||||
GetTree().Paused = true;
|
||||
}
|
||||
|
||||
public void ResumeGame()
|
||||
{
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
gameModel.ResumeGame();
|
||||
GetTree().Paused = false;
|
||||
}
|
||||
|
||||
private void SaveHighScore()
|
||||
{
|
||||
// 实现最高分保存逻辑
|
||||
}
|
||||
|
||||
private SceneTree GetTree()
|
||||
{
|
||||
return (SceneTree)Engine.GetMainLoop();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 敌人生成系统
|
||||
|
||||
```csharp
|
||||
// Scripts/Systems/SpawnSystem.cs
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.extensions;
|
||||
using GFramework.Core.Abstractions.coroutine;
|
||||
using GFramework.Core.coroutine;
|
||||
using MyShooterGame.Models;
|
||||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MyShooterGame.Systems
|
||||
{
|
||||
public class SpawnSystem : AbstractSystem
|
||||
{
|
||||
private PackedScene _enemyScene;
|
||||
private Node2D _spawnRoot;
|
||||
private CoroutineHandle? _spawnCoroutine;
|
||||
|
||||
public void Initialize(Node2D spawnRoot, PackedScene enemyScene)
|
||||
{
|
||||
_spawnRoot = spawnRoot;
|
||||
_enemyScene = enemyScene;
|
||||
}
|
||||
|
||||
public void StartSpawning()
|
||||
{
|
||||
if (_spawnCoroutine.HasValue)
|
||||
{
|
||||
this.StopCoroutine(_spawnCoroutine.Value);
|
||||
}
|
||||
|
||||
_spawnCoroutine = this.StartCoroutine(SpawnEnemiesCoroutine());
|
||||
}
|
||||
|
||||
public void StopSpawning()
|
||||
{
|
||||
if (_spawnCoroutine.HasValue)
|
||||
{
|
||||
this.StopCoroutine(_spawnCoroutine.Value);
|
||||
_spawnCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> SpawnEnemiesCoroutine()
|
||||
{
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
|
||||
while (gameModel.IsPlaying.Value)
|
||||
{
|
||||
// 等待 2 秒
|
||||
yield return CoroutineHelper.WaitForSeconds(2.0);
|
||||
|
||||
// 生成敌人
|
||||
if (!gameModel.IsPaused.Value)
|
||||
{
|
||||
SpawnEnemy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnEnemy()
|
||||
{
|
||||
if (_enemyScene == null || _spawnRoot == null)
|
||||
return;
|
||||
|
||||
var enemy = _enemyScene.Instantiate<Node2D>();
|
||||
_spawnRoot.AddChild(enemy);
|
||||
|
||||
// 随机位置
|
||||
var random = new Random();
|
||||
enemy.Position = new Vector2(
|
||||
random.Next(100, 900),
|
||||
-50
|
||||
);
|
||||
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
gameModel.EnemiesAlive.Value++;
|
||||
|
||||
GD.Print($"生成敌人,当前数量: {gameModel.EnemiesAlive.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `GameplaySystem` 管理游戏流程
|
||||
- `SpawnSystem` 使用协程定时生成敌人
|
||||
- 系统之间通过 Model 共享数据
|
||||
|
||||
## 步骤 4:创建玩家控制器
|
||||
|
||||
实现玩家的移动和射击。
|
||||
|
||||
```csharp
|
||||
// Scripts/Controllers/PlayerController.cs
|
||||
using GFramework.Core.Abstractions.controller;
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Core.extensions;
|
||||
using MyShooterGame.Architecture;
|
||||
using MyShooterGame.Models;
|
||||
using Godot;
|
||||
|
||||
namespace MyShooterGame.Controllers
|
||||
{
|
||||
public partial class PlayerController : CharacterBody2D, IController
|
||||
{
|
||||
[Export] public float Speed = 300f;
|
||||
[Export] public PackedScene BulletScene;
|
||||
|
||||
private float _shootCooldown = 0f;
|
||||
private const float ShootInterval = 0.2f;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 监听玩家死亡
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
playerModel.IsAlive.RegisterOnValueChanged(isAlive =>
|
||||
{
|
||||
if (!isAlive)
|
||||
{
|
||||
OnPlayerDied();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
_shootCooldown -= (float)delta;
|
||||
|
||||
// 射击
|
||||
if (Input.IsActionPressed("shoot") && _shootCooldown <= 0)
|
||||
{
|
||||
Shoot();
|
||||
_shootCooldown = ShootInterval;
|
||||
}
|
||||
}
|
||||
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
// 移动
|
||||
var velocity = Vector2.Zero;
|
||||
|
||||
if (Input.IsActionPressed("move_left"))
|
||||
velocity.X -= 1;
|
||||
if (Input.IsActionPressed("move_right"))
|
||||
velocity.X += 1;
|
||||
if (Input.IsActionPressed("move_up"))
|
||||
velocity.Y -= 1;
|
||||
if (Input.IsActionPressed("move_down"))
|
||||
velocity.Y += 1;
|
||||
|
||||
Velocity = velocity.Normalized() * Speed;
|
||||
MoveAndSlide();
|
||||
|
||||
// 限制在屏幕内
|
||||
var screenSize = GetViewportRect().Size;
|
||||
Position = new Vector2(
|
||||
Mathf.Clamp(Position.X, 0, screenSize.X),
|
||||
Mathf.Clamp(Position.Y, 0, screenSize.Y)
|
||||
);
|
||||
}
|
||||
|
||||
private void Shoot()
|
||||
{
|
||||
if (BulletScene == null)
|
||||
return;
|
||||
|
||||
var bullet = BulletScene.Instantiate<Node2D>();
|
||||
GetParent().AddChild(bullet);
|
||||
bullet.GlobalPosition = GlobalPosition + new Vector2(0, -20);
|
||||
|
||||
GD.Print("发射子弹");
|
||||
}
|
||||
|
||||
public void TakeDamage(int damage)
|
||||
{
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
playerModel.TakeDamage(damage);
|
||||
|
||||
GD.Print($"玩家受伤,剩余生命: {playerModel.Health.Value}");
|
||||
}
|
||||
|
||||
private void OnPlayerDied()
|
||||
{
|
||||
GD.Print("玩家死亡");
|
||||
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
playerModel.LoseLife();
|
||||
|
||||
if (playerModel.Lives.Value > 0)
|
||||
{
|
||||
// 重生
|
||||
Position = new Vector2(400, 500);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 游戏结束
|
||||
var gameplaySystem = this.GetSystem<GameplaySystem>();
|
||||
gameplaySystem.GameOver();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 实现 `IController` 接口访问架构
|
||||
- 使用 Godot 的输入系统
|
||||
- 通过 Model 更新游戏状态
|
||||
- 监听属性变化响应事件
|
||||
|
||||
## 步骤 5:创建游戏场景
|
||||
|
||||
### 5.1 主场景 (Main.tscn)
|
||||
|
||||
创建主场景并添加架构初始化脚本:
|
||||
|
||||
```csharp
|
||||
// Scripts/Main.cs
|
||||
using Godot;
|
||||
using MyShooterGame.Architecture;
|
||||
|
||||
public partial class Main : Node
|
||||
{
|
||||
private GameArchitecture _architecture;
|
||||
|
||||
public override async void _Ready()
|
||||
{
|
||||
GD.Print("初始化游戏");
|
||||
|
||||
// 创建并初始化架构
|
||||
_architecture = new GameArchitecture();
|
||||
await _architecture.InitializeAsync();
|
||||
|
||||
GD.Print("架构初始化完成,切换到菜单");
|
||||
|
||||
// 加载菜单场景
|
||||
GetTree().ChangeSceneToFile("res://Scenes/Menu.tscn");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 菜单场景 (Menu.tscn)
|
||||
|
||||
创建菜单UI并添加控制脚本:
|
||||
|
||||
```csharp
|
||||
// Scripts/UI/MenuController.cs
|
||||
using Godot;
|
||||
using GFramework.Core.Abstractions.controller;
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Core.extensions;
|
||||
using MyShooterGame.Architecture;
|
||||
using MyShooterGame.Systems;
|
||||
|
||||
public partial class MenuController : Control, IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 连接按钮信号
|
||||
GetNode<Button>("VBoxContainer/StartButton").Pressed += OnStartPressed;
|
||||
GetNode<Button>("VBoxContainer/QuitButton").Pressed += OnQuitPressed;
|
||||
}
|
||||
|
||||
private void OnStartPressed()
|
||||
{
|
||||
GD.Print("开始游戏");
|
||||
|
||||
// 初始化游戏
|
||||
var gameplaySystem = this.GetSystem<GameplaySystem>();
|
||||
gameplaySystem.StartNewGame();
|
||||
|
||||
// 切换到游戏场景
|
||||
GetTree().ChangeSceneToFile("res://Scenes/Game.tscn");
|
||||
}
|
||||
|
||||
private void OnQuitPressed()
|
||||
{
|
||||
GetTree().Quit();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 游戏场景 (Game.tscn)
|
||||
|
||||
创建游戏场景并添加控制脚本:
|
||||
|
||||
```csharp
|
||||
// Scripts/GameScene.cs
|
||||
using Godot;
|
||||
using GFramework.Core.Abstractions.controller;
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Core.extensions;
|
||||
using MyShooterGame.Architecture;
|
||||
using MyShooterGame.Systems;
|
||||
using MyShooterGame.Models;
|
||||
|
||||
public partial class GameScene : Node2D, IController
|
||||
{
|
||||
[Export] public PackedScene EnemyScene;
|
||||
|
||||
private SpawnSystem _spawnSystem;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 初始化生成系统
|
||||
_spawnSystem = this.GetSystem<SpawnSystem>();
|
||||
_spawnSystem.Initialize(this, EnemyScene);
|
||||
_spawnSystem.StartSpawning();
|
||||
|
||||
// 监听游戏状态
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
gameModel.IsPlaying.RegisterOnValueChanged(isPlaying =>
|
||||
{
|
||||
if (!isPlaying)
|
||||
{
|
||||
OnGameOver();
|
||||
}
|
||||
});
|
||||
|
||||
GD.Print("游戏场景已加载");
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
// 更新游戏时间
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
if (gameModel.IsPlaying.Value && !gameModel.IsPaused.Value)
|
||||
{
|
||||
gameModel.GameTime.Value += (float)delta;
|
||||
}
|
||||
|
||||
// 暂停
|
||||
if (Input.IsActionJustPressed("ui_cancel"))
|
||||
{
|
||||
TogglePause();
|
||||
}
|
||||
}
|
||||
|
||||
private void TogglePause()
|
||||
{
|
||||
var gameplaySystem = this.GetSystem<GameplaySystem>();
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
|
||||
if (gameModel.IsPaused.Value)
|
||||
{
|
||||
gameplaySystem.ResumeGame();
|
||||
}
|
||||
else
|
||||
{
|
||||
gameplaySystem.PauseGame();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGameOver()
|
||||
{
|
||||
GD.Print("游戏结束,返回菜单");
|
||||
_spawnSystem.StopSpawning();
|
||||
|
||||
// 延迟返回菜单
|
||||
GetTree().CreateTimer(2.0).Timeout += () =>
|
||||
{
|
||||
GetTree().ChangeSceneToFile("res://Scenes/Menu.tscn");
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `Main` 初始化架构
|
||||
- `MenuController` 处理菜单交互
|
||||
- `GameScene` 管理游戏场景
|
||||
- 所有脚本通过架构访问系统和模型
|
||||
|
||||
## 完整代码
|
||||
|
||||
项目结构和所有代码文件已在上述步骤中提供。
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行游戏后,你将看到:
|
||||
|
||||
1. **启动**:
|
||||
- 架构初始化
|
||||
- 自动进入主菜单
|
||||
|
||||
2. **主菜单**:
|
||||
- 显示开始和退出按钮
|
||||
- 点击开始进入游戏
|
||||
|
||||
3. **游戏场景**:
|
||||
- 玩家可以移动和射击
|
||||
- 敌人定时生成
|
||||
- HUD 显示分数和生命值
|
||||
- 按 ESC 暂停游戏
|
||||
|
||||
4. **游戏结束**:
|
||||
- 玩家生命值为 0 时游戏结束
|
||||
- 显示最终分数
|
||||
- 自动返回主菜单
|
||||
|
||||
**验证步骤**:
|
||||
|
||||
1. 架构正确初始化
|
||||
2. 场景切换正常
|
||||
3. 玩家控制响应
|
||||
4. 敌人生成系统工作
|
||||
5. 数据模型正确更新
|
||||
6. 暂停功能正常
|
||||
|
||||
## 下一步
|
||||
|
||||
恭喜!你已经完成了一个基础的 Godot 游戏项目。接下来可以:
|
||||
|
||||
- 添加更多游戏功能(道具、关卡等)
|
||||
- 实现完整的 UI 系统
|
||||
- 添加音效和音乐
|
||||
- 优化性能和体验
|
||||
- 发布到不同平台
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - 架构详细说明
|
||||
- [Godot 场景系统](/zh-CN/godot/scene) - 场景管理
|
||||
- [Godot UI 系统](/zh-CN/godot/ui) - UI 管理
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - 扩展功能
|
||||
814
docs/zh-CN/tutorials/resource-management.md
Normal file
814
docs/zh-CN/tutorials/resource-management.md
Normal file
@ -0,0 +1,814 @@
|
||||
---
|
||||
title: 资源管理最佳实践
|
||||
description: 学习如何高效管理游戏资源,避免内存泄漏和性能问题
|
||||
---
|
||||
|
||||
# 资源管理最佳实践
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- 理解资源管理的核心概念和重要性
|
||||
- 实现自定义资源加载器
|
||||
- 使用资源句柄管理资源生命周期
|
||||
- 实现资源预加载和延迟加载
|
||||
- 选择合适的资源释放策略
|
||||
- 避免常见的资源管理陷阱
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已安装 GFramework.Core NuGet 包
|
||||
- 了解 C# 基础语法和 async/await
|
||||
- 阅读过[架构概览](/zh-CN/getting-started)
|
||||
- 了解[协程系统](/zh-CN/core/coroutine)
|
||||
|
||||
## 步骤 1:创建资源类型和加载器
|
||||
|
||||
首先,让我们定义游戏中常用的资源类型,并为它们实现加载器。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MyGame.Resources
|
||||
{
|
||||
// ===== 资源类型定义 =====
|
||||
|
||||
/// <summary>
|
||||
/// 纹理资源
|
||||
/// </summary>
|
||||
public class Texture : IDisposable
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public byte[]? Data { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Data = null;
|
||||
Console.WriteLine($"纹理已释放: {Path}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 音频资源
|
||||
/// </summary>
|
||||
public class AudioClip : IDisposable
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public double Duration { get; set; }
|
||||
public byte[]? Data { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Data = null;
|
||||
Console.WriteLine($"音频已释放: {Path}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置文件资源
|
||||
/// </summary>
|
||||
public class ConfigData
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> Data { get; set; } = new();
|
||||
}
|
||||
|
||||
// ===== 资源加载器实现 =====
|
||||
|
||||
/// <summary>
|
||||
/// 纹理加载器
|
||||
/// </summary>
|
||||
public class TextureLoader : IResourceLoader<Texture>
|
||||
{
|
||||
public Texture Load(string path)
|
||||
{
|
||||
Console.WriteLine($"同步加载纹理: {path}");
|
||||
|
||||
// 模拟加载纹理
|
||||
Thread.Sleep(100); // 模拟 I/O 延迟
|
||||
|
||||
return new Texture
|
||||
{
|
||||
Path = path,
|
||||
Width = 512,
|
||||
Height = 512,
|
||||
Data = new byte[512 * 512 * 4] // RGBA
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Texture> LoadAsync(string path)
|
||||
{
|
||||
Console.WriteLine($"异步加载纹理: {path}");
|
||||
|
||||
// 模拟异步加载
|
||||
await Task.Delay(100);
|
||||
|
||||
return new Texture
|
||||
{
|
||||
Path = path,
|
||||
Width = 512,
|
||||
Height = 512,
|
||||
Data = new byte[512 * 512 * 4]
|
||||
};
|
||||
}
|
||||
|
||||
public void Unload(Texture resource)
|
||||
{
|
||||
resource?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 音频加载器
|
||||
/// </summary>
|
||||
public class AudioLoader : IResourceLoader<AudioClip>
|
||||
{
|
||||
public AudioClip Load(string path)
|
||||
{
|
||||
Console.WriteLine($"同步加载音频: {path}");
|
||||
Thread.Sleep(150);
|
||||
|
||||
return new AudioClip
|
||||
{
|
||||
Path = path,
|
||||
Duration = 30.0,
|
||||
Data = new byte[1024 * 1024] // 1MB
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<AudioClip> LoadAsync(string path)
|
||||
{
|
||||
Console.WriteLine($"异步加载音频: {path}");
|
||||
await Task.Delay(150);
|
||||
|
||||
return new AudioClip
|
||||
{
|
||||
Path = path,
|
||||
Duration = 30.0,
|
||||
Data = new byte[1024 * 1024]
|
||||
};
|
||||
}
|
||||
|
||||
public void Unload(AudioClip resource)
|
||||
{
|
||||
resource?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置文件加载器
|
||||
/// </summary>
|
||||
public class ConfigLoader : IResourceLoader<ConfigData>
|
||||
{
|
||||
public ConfigData Load(string path)
|
||||
{
|
||||
Console.WriteLine($"加载配置: {path}");
|
||||
|
||||
// 模拟解析配置文件
|
||||
return new ConfigData
|
||||
{
|
||||
Path = path,
|
||||
Data = new Dictionary<string, string>
|
||||
{
|
||||
["version"] = "1.0",
|
||||
["difficulty"] = "normal"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ConfigData> LoadAsync(string path)
|
||||
{
|
||||
await Task.Delay(50);
|
||||
return Load(path);
|
||||
}
|
||||
|
||||
public void Unload(ConfigData resource)
|
||||
{
|
||||
resource.Data.Clear();
|
||||
Console.WriteLine($"配置已释放: {resource.Path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 定义了三种常见资源类型:纹理、音频、配置
|
||||
- 实现 `IResourceLoader<T>` 接口提供加载逻辑
|
||||
- 同步和异步加载方法分别处理不同场景
|
||||
- `Unload` 方法负责资源清理
|
||||
|
||||
## 步骤 2:注册资源管理器
|
||||
|
||||
在架构中注册资源管理器和所有加载器。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.architecture;
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
using GFramework.Core.resource;
|
||||
using MyGame.Resources;
|
||||
|
||||
namespace MyGame
|
||||
{
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
public static IArchitecture Interface { get; private set; }
|
||||
|
||||
protected override void Init()
|
||||
{
|
||||
Interface = this;
|
||||
|
||||
// 创建资源管理器
|
||||
var resourceManager = new ResourceManager();
|
||||
|
||||
// 注册资源加载器
|
||||
resourceManager.RegisterLoader(new TextureLoader());
|
||||
resourceManager.RegisterLoader(new AudioLoader());
|
||||
resourceManager.RegisterLoader(new ConfigLoader());
|
||||
|
||||
// 设置释放策略(默认手动释放)
|
||||
resourceManager.SetReleaseStrategy(new ManualReleaseStrategy());
|
||||
|
||||
// 注册到架构
|
||||
RegisterUtility<IResourceManager>(resourceManager);
|
||||
|
||||
Console.WriteLine("资源管理器初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 创建 `ResourceManager` 实例
|
||||
- 为每种资源类型注册对应的加载器
|
||||
- 设置资源释放策略
|
||||
- 将资源管理器注册为 Utility
|
||||
|
||||
## 步骤 3:实现资源预加载系统
|
||||
|
||||
创建一个系统来管理资源的预加载和卸载。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
using GFramework.Core.extensions;
|
||||
using MyGame.Resources;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// 资源预加载系统
|
||||
/// </summary>
|
||||
public class ResourcePreloadSystem : AbstractSystem
|
||||
{
|
||||
// 场景资源配置
|
||||
private readonly Dictionary<string, List<string>> _sceneResources = new()
|
||||
{
|
||||
["Menu"] = new List<string>
|
||||
{
|
||||
"textures/menu_bg.png",
|
||||
"textures/button.png",
|
||||
"audio/menu_bgm.mp3"
|
||||
},
|
||||
["Gameplay"] = new List<string>
|
||||
{
|
||||
"textures/player.png",
|
||||
"textures/enemy.png",
|
||||
"textures/bullet.png",
|
||||
"audio/game_bgm.mp3",
|
||||
"audio/shoot.mp3",
|
||||
"config/level_1.cfg"
|
||||
},
|
||||
["GameOver"] = new List<string>
|
||||
{
|
||||
"textures/gameover_bg.png",
|
||||
"audio/gameover.mp3"
|
||||
}
|
||||
};
|
||||
|
||||
// 当前场景的资源句柄
|
||||
private readonly List<IDisposable> _currentHandles = new();
|
||||
|
||||
/// <summary>
|
||||
/// 预加载场景资源
|
||||
/// </summary>
|
||||
public async Task PreloadSceneAsync(string sceneName)
|
||||
{
|
||||
if (!_sceneResources.TryGetValue(sceneName, out var resources))
|
||||
{
|
||||
Console.WriteLine($"场景 {sceneName} 没有配置资源");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n=== 开始预加载场景: {sceneName} ===");
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
// 并行加载所有资源
|
||||
var tasks = new List<Task>();
|
||||
|
||||
foreach (var path in resources)
|
||||
{
|
||||
if (path.EndsWith(".png"))
|
||||
{
|
||||
tasks.Add(resourceManager.PreloadAsync<Texture>(path));
|
||||
}
|
||||
else if (path.EndsWith(".mp3"))
|
||||
{
|
||||
tasks.Add(resourceManager.PreloadAsync<AudioClip>(path));
|
||||
}
|
||||
else if (path.EndsWith(".cfg"))
|
||||
{
|
||||
tasks.Add(resourceManager.PreloadAsync<ConfigData>(path));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
Console.WriteLine($"场景 {sceneName} 资源预加载完成\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预加载场景资源(带进度)
|
||||
/// </summary>
|
||||
public async Task PreloadSceneWithProgressAsync(
|
||||
string sceneName,
|
||||
Action<float> onProgress)
|
||||
{
|
||||
if (!_sceneResources.TryGetValue(sceneName, out var resources))
|
||||
return;
|
||||
|
||||
Console.WriteLine($"\n=== 开始预加载场景: {sceneName} ===");
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
int totalCount = resources.Count;
|
||||
int loadedCount = 0;
|
||||
|
||||
foreach (var path in resources)
|
||||
{
|
||||
// 根据扩展名加载不同类型的资源
|
||||
if (path.EndsWith(".png"))
|
||||
{
|
||||
await resourceManager.PreloadAsync<Texture>(path);
|
||||
}
|
||||
else if (path.EndsWith(".mp3"))
|
||||
{
|
||||
await resourceManager.PreloadAsync<AudioClip>(path);
|
||||
}
|
||||
else if (path.EndsWith(".cfg"))
|
||||
{
|
||||
await resourceManager.PreloadAsync<ConfigData>(path);
|
||||
}
|
||||
|
||||
loadedCount++;
|
||||
float progress = (float)loadedCount / totalCount;
|
||||
onProgress?.Invoke(progress);
|
||||
|
||||
Console.WriteLine($"加载进度: {progress * 100:F0}% ({loadedCount}/{totalCount})");
|
||||
}
|
||||
|
||||
Console.WriteLine($"场景 {sceneName} 资源预加载完成\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取场景资源句柄
|
||||
/// </summary>
|
||||
public void AcquireSceneResources(string sceneName)
|
||||
{
|
||||
if (!_sceneResources.TryGetValue(sceneName, out var resources))
|
||||
return;
|
||||
|
||||
Console.WriteLine($"获取场景 {sceneName} 的资源句柄");
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
// 清理旧句柄
|
||||
ReleaseCurrentResources();
|
||||
|
||||
// 获取新句柄
|
||||
foreach (var path in resources)
|
||||
{
|
||||
IDisposable? handle = null;
|
||||
|
||||
if (path.EndsWith(".png"))
|
||||
{
|
||||
handle = resourceManager.GetHandle<Texture>(path);
|
||||
}
|
||||
else if (path.EndsWith(".mp3"))
|
||||
{
|
||||
handle = resourceManager.GetHandle<AudioClip>(path);
|
||||
}
|
||||
else if (path.EndsWith(".cfg"))
|
||||
{
|
||||
handle = resourceManager.GetHandle<ConfigData>(path);
|
||||
}
|
||||
|
||||
if (handle != null)
|
||||
{
|
||||
_currentHandles.Add(handle);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"已获取 {_currentHandles.Count} 个资源句柄");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前场景资源
|
||||
/// </summary>
|
||||
public void ReleaseCurrentResources()
|
||||
{
|
||||
Console.WriteLine($"释放 {_currentHandles.Count} 个资源句柄");
|
||||
|
||||
foreach (var handle in _currentHandles)
|
||||
{
|
||||
handle.Dispose();
|
||||
}
|
||||
|
||||
_currentHandles.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 卸载场景资源
|
||||
/// </summary>
|
||||
public void UnloadSceneResources(string sceneName)
|
||||
{
|
||||
if (!_sceneResources.TryGetValue(sceneName, out var resources))
|
||||
return;
|
||||
|
||||
Console.WriteLine($"\n卸载场景 {sceneName} 的资源");
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
foreach (var path in resources)
|
||||
{
|
||||
resourceManager.Unload(path);
|
||||
}
|
||||
|
||||
Console.WriteLine($"场景 {sceneName} 资源已卸载\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示资源状态
|
||||
/// </summary>
|
||||
public void ShowResourceStatus()
|
||||
{
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
Console.WriteLine("\n=== 资源状态 ===");
|
||||
Console.WriteLine($"已加载资源数: {resourceManager.LoadedResourceCount}");
|
||||
Console.WriteLine("已加载资源列表:");
|
||||
|
||||
foreach (var path in resourceManager.GetLoadedResourcePaths())
|
||||
{
|
||||
Console.WriteLine($" - {path}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用字典配置每个场景需要的资源
|
||||
- `PreloadSceneAsync` 并行预加载所有资源
|
||||
- `PreloadSceneWithProgressAsync` 提供加载进度回调
|
||||
- `AcquireSceneResources` 获取资源句柄防止被释放
|
||||
- `ReleaseCurrentResources` 释放不再使用的资源
|
||||
|
||||
## 步骤 4:实现资源使用示例
|
||||
|
||||
创建一个游戏系统展示如何正确使用资源。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
using GFramework.Core.extensions;
|
||||
using MyGame.Resources;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏系统示例
|
||||
/// </summary>
|
||||
public class GameplaySystem : AbstractSystem
|
||||
{
|
||||
private IResourceHandle<Texture>? _playerTexture;
|
||||
private IResourceHandle<AudioClip>? _bgmClip;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化游戏
|
||||
/// </summary>
|
||||
public void InitializeGame()
|
||||
{
|
||||
Console.WriteLine("\n=== 初始化游戏 ===");
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
// 获取玩家纹理句柄
|
||||
_playerTexture = resourceManager.GetHandle<Texture>("textures/player.png");
|
||||
if (_playerTexture?.Resource != null)
|
||||
{
|
||||
Console.WriteLine($"玩家纹理已加载: {_playerTexture.Resource.Width}x{_playerTexture.Resource.Height}");
|
||||
}
|
||||
|
||||
// 获取背景音乐句柄
|
||||
_bgmClip = resourceManager.GetHandle<AudioClip>("audio/game_bgm.mp3");
|
||||
if (_bgmClip?.Resource != null)
|
||||
{
|
||||
Console.WriteLine($"背景音乐已加载: {_bgmClip.Resource.Duration}秒");
|
||||
}
|
||||
|
||||
Console.WriteLine("游戏初始化完成\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用临时资源(使用 using 语句)
|
||||
/// </summary>
|
||||
public void SpawnBullet()
|
||||
{
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
// 使用 using 语句自动管理资源生命周期
|
||||
using var bulletTexture = resourceManager.GetHandle<Texture>("textures/bullet.png");
|
||||
|
||||
if (bulletTexture?.Resource != null)
|
||||
{
|
||||
Console.WriteLine("创建子弹,使用纹理");
|
||||
// 使用纹理创建子弹...
|
||||
}
|
||||
|
||||
// 离开作用域后自动释放句柄
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 播放音效(临时资源)
|
||||
/// </summary>
|
||||
public void PlayShootSound()
|
||||
{
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
using var shootSound = resourceManager.GetHandle<AudioClip>("audio/shoot.mp3");
|
||||
|
||||
if (shootSound?.Resource != null)
|
||||
{
|
||||
Console.WriteLine("播放射击音效");
|
||||
// 播放音效...
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理游戏资源
|
||||
/// </summary>
|
||||
public void CleanupGame()
|
||||
{
|
||||
Console.WriteLine("\n=== 清理游戏资源 ===");
|
||||
|
||||
// 释放长期持有的资源句柄
|
||||
_playerTexture?.Dispose();
|
||||
_playerTexture = null;
|
||||
|
||||
_bgmClip?.Dispose();
|
||||
_bgmClip = null;
|
||||
|
||||
Console.WriteLine("游戏资源已清理\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 长期使用的资源(玩家纹理、BGM)保存句柄
|
||||
- 临时资源(子弹纹理、音效)使用 `using` 语句
|
||||
- 在清理时释放所有持有的句柄
|
||||
- 展示了正确的资源生命周期管理
|
||||
|
||||
## 步骤 5:测试资源管理
|
||||
|
||||
编写测试代码验证资源管理功能。
|
||||
|
||||
```csharp
|
||||
using MyGame;
|
||||
using MyGame.Systems;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("=== 资源管理最佳实践测试 ===\n");
|
||||
|
||||
// 1. 初始化架构
|
||||
var architecture = new GameArchitecture();
|
||||
architecture.Initialize();
|
||||
await architecture.WaitUntilReadyAsync();
|
||||
|
||||
// 2. 获取系统
|
||||
var preloadSystem = architecture.GetSystem<ResourcePreloadSystem>();
|
||||
var gameplaySystem = architecture.GetSystem<GameplaySystem>();
|
||||
|
||||
// 3. 测试场景资源预加载
|
||||
Console.WriteLine("--- 测试 1: 预加载菜单场景 ---");
|
||||
await preloadSystem.PreloadSceneWithProgressAsync("Menu", progress =>
|
||||
{
|
||||
// 进度回调
|
||||
});
|
||||
preloadSystem.ShowResourceStatus();
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 4. 切换到游戏场景
|
||||
Console.WriteLine("--- 测试 2: 切换到游戏场景 ---");
|
||||
await preloadSystem.PreloadSceneWithProgressAsync("Gameplay", progress =>
|
||||
{
|
||||
// 进度回调
|
||||
});
|
||||
preloadSystem.ShowResourceStatus();
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 5. 获取场景资源句柄
|
||||
Console.WriteLine("--- 测试 3: 获取游戏场景资源句柄 ---");
|
||||
preloadSystem.AcquireSceneResources("Gameplay");
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 6. 初始化游戏
|
||||
Console.WriteLine("--- 测试 4: 初始化游戏 ---");
|
||||
gameplaySystem.InitializeGame();
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 7. 使用临时资源
|
||||
Console.WriteLine("--- 测试 5: 使用临时资源 ---");
|
||||
gameplaySystem.SpawnBullet();
|
||||
gameplaySystem.PlayShootSound();
|
||||
preloadSystem.ShowResourceStatus();
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 8. 清理游戏
|
||||
Console.WriteLine("--- 测试 6: 清理游戏 ---");
|
||||
gameplaySystem.CleanupGame();
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 9. 释放场景资源句柄
|
||||
Console.WriteLine("--- 测试 7: 释放场景资源句柄 ---");
|
||||
preloadSystem.ReleaseCurrentResources();
|
||||
preloadSystem.ShowResourceStatus();
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 10. 卸载旧场景资源
|
||||
Console.WriteLine("--- 测试 8: 卸载菜单场景资源 ---");
|
||||
preloadSystem.UnloadSceneResources("Menu");
|
||||
preloadSystem.ShowResourceStatus();
|
||||
|
||||
Console.WriteLine("=== 测试完成 ===");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 测试资源预加载和进度回调
|
||||
- 测试场景切换时的资源管理
|
||||
- 测试资源句柄的获取和释放
|
||||
- 测试临时资源的自动管理
|
||||
- 验证资源状态和内存清理
|
||||
|
||||
## 完整代码
|
||||
|
||||
所有代码文件已在上述步骤中提供。项目结构如下:
|
||||
|
||||
```
|
||||
MyGame/
|
||||
├── Resources/
|
||||
│ ├── Texture.cs
|
||||
│ ├── AudioClip.cs
|
||||
│ ├── ConfigData.cs
|
||||
│ ├── TextureLoader.cs
|
||||
│ ├── AudioLoader.cs
|
||||
│ └── ConfigLoader.cs
|
||||
├── Systems/
|
||||
│ ├── ResourcePreloadSystem.cs
|
||||
│ └── GameplaySystem.cs
|
||||
├── GameArchitecture.cs
|
||||
└── Program.cs
|
||||
```
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行程序后,你将看到类似以下的输出:
|
||||
|
||||
```
|
||||
=== 资源管理最佳实践测试 ===
|
||||
|
||||
资源管理器初始化完成
|
||||
|
||||
--- 测试 1: 预加载菜单场景 ---
|
||||
|
||||
=== 开始预加载场景: Menu ===
|
||||
异步加载纹理: textures/menu_bg.png
|
||||
异步加载纹理: textures/button.png
|
||||
异步加载音频: audio/menu_bgm.mp3
|
||||
加载进度: 33% (1/3)
|
||||
加载进度: 67% (2/3)
|
||||
加载进度: 100% (3/3)
|
||||
场景 Menu 资源预加载完成
|
||||
|
||||
=== 资源状态 ===
|
||||
已加载资源数: 3
|
||||
已加载资源列表:
|
||||
- textures/menu_bg.png
|
||||
- textures/button.png
|
||||
- audio/menu_bgm.mp3
|
||||
|
||||
--- 测试 2: 切换到游戏场景 ---
|
||||
|
||||
=== 开始预加载场景: Gameplay ===
|
||||
异步加载纹理: textures/player.png
|
||||
异步加载纹理: textures/enemy.png
|
||||
异步加载纹理: textures/bullet.png
|
||||
异步加载音频: audio/game_bgm.mp3
|
||||
异步加载音频: audio/shoot.mp3
|
||||
加载配置: config/level_1.cfg
|
||||
加载进度: 17% (1/6)
|
||||
加载进度: 33% (2/6)
|
||||
加载进度: 50% (3/6)
|
||||
加载进度: 67% (4/6)
|
||||
加载进度: 83% (5/6)
|
||||
加载进度: 100% (6/6)
|
||||
场景 Gameplay 资源预加载完成
|
||||
|
||||
=== 资源状态 ===
|
||||
已加载资源数: 9
|
||||
|
||||
--- 测试 3: 获取游戏场景资源句柄 ---
|
||||
获取场景 Gameplay 的资源句柄
|
||||
已获取 6 个资源句柄
|
||||
|
||||
--- 测试 4: 初始化游戏 ===
|
||||
|
||||
=== 初始化游戏 ===
|
||||
玩家纹理已加载: 512x512
|
||||
背景音乐已加载: 30秒
|
||||
游戏初始化完成
|
||||
|
||||
--- 测试 5: 使用临时资源 ---
|
||||
创建子弹,使用纹理
|
||||
播放射击音效
|
||||
|
||||
--- 测试 6: 清理游戏 ---
|
||||
|
||||
=== 清理游戏资源 ===
|
||||
游戏资源已清理
|
||||
|
||||
--- 测试 7: 释放场景资源句柄 ---
|
||||
释放 6 个资源句柄
|
||||
|
||||
--- 测试 8: 卸载菜单场景资源 ---
|
||||
|
||||
卸载场景 Menu 的资源
|
||||
纹理已释放: textures/menu_bg.png
|
||||
纹理已释放: textures/button.png
|
||||
音频已释放: audio/menu_bgm.mp3
|
||||
场景 Menu 资源已卸载
|
||||
|
||||
=== 资源状态 ===
|
||||
已加载资源数: 6
|
||||
|
||||
=== 测试完成 ===
|
||||
```
|
||||
|
||||
**验证步骤**:
|
||||
|
||||
1. 资源预加载正常工作
|
||||
2. 加载进度正确显示
|
||||
3. 资源句柄管理正确
|
||||
4. 临时资源自动释放
|
||||
5. 资源卸载成功执行
|
||||
6. 内存正确清理
|
||||
|
||||
## 下一步
|
||||
|
||||
恭喜!你已经掌握了资源管理的最佳实践。接下来可以学习:
|
||||
|
||||
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 在协程中加载资源
|
||||
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial) - 在状态切换时管理资源
|
||||
- [实现存档系统](/zh-CN/tutorials/save-system) - 保存和加载游戏数据
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [资源管理系统](/zh-CN/core/resource) - 资源系统详细说明
|
||||
- [对象池系统](/zh-CN/core/pool) - 结合对象池复用资源
|
||||
- [协程系统](/zh-CN/core/coroutine) - 异步加载资源
|
||||
- [System 层](/zh-CN/core/system) - System 详细说明
|
||||
966
docs/zh-CN/tutorials/save-system.md
Normal file
966
docs/zh-CN/tutorials/save-system.md
Normal file
@ -0,0 +1,966 @@
|
||||
---
|
||||
title: 实现存档系统
|
||||
description: 学习如何实现完整的游戏存档系统,支持多槽位和自动保存
|
||||
---
|
||||
|
||||
# 实现存档系统
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- 理解游戏存档系统的设计原则
|
||||
- 定义存档数据结构
|
||||
- 实现多槽位存档管理
|
||||
- 实现自动保存功能
|
||||
- 处理存档加载和保存错误
|
||||
- 实现存档列表和删除功能
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已安装 GFramework.Game NuGet 包
|
||||
- 了解 C# 基础语法和 async/await
|
||||
- 阅读过[架构概览](/zh-CN/getting-started)
|
||||
- 了解[数据与存档系统](/zh-CN/game/data)
|
||||
|
||||
## 步骤 1:定义存档数据结构
|
||||
|
||||
首先,让我们定义游戏存档需要保存的数据结构。
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MyGame.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家数据
|
||||
/// </summary>
|
||||
public class PlayerData
|
||||
{
|
||||
public string Name { get; set; } = "Player";
|
||||
public int Level { get; set; } = 1;
|
||||
public int Experience { get; set; } = 0;
|
||||
public int Health { get; set; } = 100;
|
||||
public int MaxHealth { get; set; } = 100;
|
||||
public int Gold { get; set; } = 0;
|
||||
public Vector3 Position { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 位置数据
|
||||
/// </summary>
|
||||
public class Vector3
|
||||
{
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
public float Z { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关卡进度数据
|
||||
/// </summary>
|
||||
public class ProgressData
|
||||
{
|
||||
public int CurrentLevel { get; set; } = 1;
|
||||
public List<int> CompletedLevels { get; set; } = new();
|
||||
public Dictionary<string, bool> Achievements { get; set; } = new();
|
||||
public float PlayTime { get; set; } = 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 物品数据
|
||||
/// </summary>
|
||||
public class InventoryData
|
||||
{
|
||||
public List<ItemData> Items { get; set; } = new();
|
||||
public List<string> EquippedItems { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个物品数据
|
||||
/// </summary>
|
||||
public class ItemData
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; } = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完整的存档数据
|
||||
/// </summary>
|
||||
public class GameSaveData : IVersionedData
|
||||
{
|
||||
// 数据版本号
|
||||
public int Version { get; set; } = 1;
|
||||
|
||||
// 存档元数据
|
||||
public DateTime SaveTime { get; set; }
|
||||
public string SaveName { get; set; } = "New Save";
|
||||
public float TotalPlayTime { get; set; }
|
||||
|
||||
// 游戏数据
|
||||
public PlayerData Player { get; set; } = new();
|
||||
public ProgressData Progress { get; set; } = new();
|
||||
public InventoryData Inventory { get; set; } = new();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `GameSaveData` 实现 `IVersionedData` 支持版本管理
|
||||
- 将数据分为玩家、进度、物品等模块
|
||||
- 包含存档元数据(保存时间、名称等)
|
||||
- 使用属性初始化器设置默认值
|
||||
|
||||
## 步骤 2:创建存档管理系统
|
||||
|
||||
实现一个系统来管理存档的创建、加载和保存。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
using GFramework.Core.extensions;
|
||||
using GFramework.Game.Abstractions.data;
|
||||
using MyGame.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// 存档管理系统
|
||||
/// </summary>
|
||||
public class SaveSystem : AbstractSystem
|
||||
{
|
||||
private GameSaveData? _currentSave;
|
||||
private int _currentSlot = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 创建新存档
|
||||
/// </summary>
|
||||
public GameSaveData CreateNewSave(string saveName)
|
||||
{
|
||||
Console.WriteLine($"创建新存档: {saveName}");
|
||||
|
||||
var saveData = new GameSaveData
|
||||
{
|
||||
SaveName = saveName,
|
||||
SaveTime = DateTime.Now,
|
||||
TotalPlayTime = 0f,
|
||||
Player = new PlayerData
|
||||
{
|
||||
Name = "Player",
|
||||
Level = 1,
|
||||
Experience = 0,
|
||||
Health = 100,
|
||||
MaxHealth = 100,
|
||||
Gold = 0
|
||||
},
|
||||
Progress = new ProgressData
|
||||
{
|
||||
CurrentLevel = 1,
|
||||
CompletedLevels = new List<int>(),
|
||||
Achievements = new Dictionary<string, bool>(),
|
||||
PlayTime = 0f
|
||||
},
|
||||
Inventory = new InventoryData
|
||||
{
|
||||
Items = new List<ItemData>(),
|
||||
EquippedItems = new List<string>()
|
||||
}
|
||||
};
|
||||
|
||||
_currentSave = saveData;
|
||||
return saveData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存游戏
|
||||
/// </summary>
|
||||
public async Task<bool> SaveGameAsync(int slot)
|
||||
{
|
||||
if (_currentSave == null)
|
||||
{
|
||||
Console.WriteLine("错误: 没有当前存档数据");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"\n=== 保存游戏到槽位 {slot} ===");
|
||||
|
||||
var saveRepo = this.GetUtility<ISaveRepository<GameSaveData>>();
|
||||
|
||||
// 更新保存时间
|
||||
_currentSave.SaveTime = DateTime.Now;
|
||||
_currentSlot = slot;
|
||||
|
||||
// 保存到存储
|
||||
await saveRepo.SaveAsync(slot, _currentSave);
|
||||
|
||||
Console.WriteLine($"存档已保存: {_currentSave.SaveName}");
|
||||
Console.WriteLine($"玩家: {_currentSave.Player.Name}, 等级 {_currentSave.Player.Level}");
|
||||
Console.WriteLine($"游戏时间: {_currentSave.TotalPlayTime:F1} 小时");
|
||||
Console.WriteLine($"保存时间: {_currentSave.SaveTime}\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"保存失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载游戏
|
||||
/// </summary>
|
||||
public async Task<bool> LoadGameAsync(int slot)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"\n=== 从槽位 {slot} 加载游戏 ===");
|
||||
|
||||
var saveRepo = this.GetUtility<ISaveRepository<GameSaveData>>();
|
||||
|
||||
// 检查存档是否存在
|
||||
if (!await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
Console.WriteLine($"槽位 {slot} 不存在存档\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 加载存档
|
||||
_currentSave = await saveRepo.LoadAsync(slot);
|
||||
_currentSlot = slot;
|
||||
|
||||
Console.WriteLine($"存档已加载: {_currentSave.SaveName}");
|
||||
Console.WriteLine($"玩家: {_currentSave.Player.Name}, 等级 {_currentSave.Player.Level}");
|
||||
Console.WriteLine($"当前关卡: {_currentSave.Progress.CurrentLevel}");
|
||||
Console.WriteLine($"金币: {_currentSave.Player.Gold}");
|
||||
Console.WriteLine($"物品数量: {_currentSave.Inventory.Items.Count}");
|
||||
Console.WriteLine($"保存时间: {_currentSave.SaveTime}\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"加载失败: {ex.Message}\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除存档
|
||||
/// </summary>
|
||||
public async Task<bool> DeleteSaveAsync(int slot)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"\n删除槽位 {slot} 的存档");
|
||||
|
||||
var saveRepo = this.GetUtility<ISaveRepository<GameSaveData>>();
|
||||
|
||||
if (!await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
Console.WriteLine($"槽位 {slot} 不存在存档\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
await saveRepo.DeleteAsync(slot);
|
||||
|
||||
if (_currentSlot == slot)
|
||||
{
|
||||
_currentSave = null;
|
||||
_currentSlot = -1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"槽位 {slot} 的存档已删除\n");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"删除失败: {ex.Message}\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 列出所有存档
|
||||
/// </summary>
|
||||
public async Task<List<SaveSlotInfo>> ListSavesAsync()
|
||||
{
|
||||
var result = new List<SaveSlotInfo>();
|
||||
|
||||
try
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<GameSaveData>>();
|
||||
var slots = await saveRepo.ListSlotsAsync();
|
||||
|
||||
Console.WriteLine($"\n=== 存档列表 ({slots.Count} 个) ===");
|
||||
|
||||
foreach (var slot in slots)
|
||||
{
|
||||
var saveData = await saveRepo.LoadAsync(slot);
|
||||
|
||||
var info = new SaveSlotInfo
|
||||
{
|
||||
Slot = slot,
|
||||
SaveName = saveData.SaveName,
|
||||
PlayerName = saveData.Player.Name,
|
||||
Level = saveData.Player.Level,
|
||||
SaveTime = saveData.SaveTime,
|
||||
PlayTime = saveData.TotalPlayTime
|
||||
};
|
||||
|
||||
result.Add(info);
|
||||
|
||||
Console.WriteLine($"槽位 {slot}: {info.SaveName}");
|
||||
Console.WriteLine($" 玩家: {info.PlayerName}, 等级 {info.Level}");
|
||||
Console.WriteLine($" 保存时间: {info.SaveTime}");
|
||||
Console.WriteLine($" 游戏时间: {info.PlayTime:F1} 小时");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"列出存档失败: {ex.Message}\n");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前存档
|
||||
/// </summary>
|
||||
public GameSaveData? GetCurrentSave() => _currentSave;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前槽位
|
||||
/// </summary>
|
||||
public int GetCurrentSlot() => _currentSlot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 存档槽位信息
|
||||
/// </summary>
|
||||
public class SaveSlotInfo
|
||||
{
|
||||
public int Slot { get; set; }
|
||||
public string SaveName { get; set; } = string.Empty;
|
||||
public string PlayerName { get; set; } = string.Empty;
|
||||
public int Level { get; set; }
|
||||
public DateTime SaveTime { get; set; }
|
||||
public float PlayTime { get; set; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `CreateNewSave` 创建新的存档数据
|
||||
- `SaveGameAsync` 保存当前存档到指定槽位
|
||||
- `LoadGameAsync` 从槽位加载存档
|
||||
- `DeleteSaveAsync` 删除指定槽位的存档
|
||||
- `ListSavesAsync` 列出所有存档信息
|
||||
- 使用 try-catch 处理异常
|
||||
|
||||
## 步骤 3:实现自动保存功能
|
||||
|
||||
创建自动保存系统,定期保存游戏进度。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.extensions;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动保存系统
|
||||
/// </summary>
|
||||
public class AutoSaveSystem : AbstractSystem
|
||||
{
|
||||
private CancellationTokenSource? _autoSaveCts;
|
||||
private bool _isAutoSaveEnabled;
|
||||
private TimeSpan _autoSaveInterval = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// 启动自动保存
|
||||
/// </summary>
|
||||
public void StartAutoSave(int slot, TimeSpan? interval = null)
|
||||
{
|
||||
if (_isAutoSaveEnabled)
|
||||
{
|
||||
Console.WriteLine("自动保存已在运行");
|
||||
return;
|
||||
}
|
||||
|
||||
if (interval.HasValue)
|
||||
{
|
||||
_autoSaveInterval = interval.Value;
|
||||
}
|
||||
|
||||
_autoSaveCts = new CancellationTokenSource();
|
||||
_isAutoSaveEnabled = true;
|
||||
|
||||
Console.WriteLine($"\n启动自动保存 (间隔: {_autoSaveInterval.TotalSeconds} 秒)");
|
||||
|
||||
// 在后台线程运行自动保存
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (!_autoSaveCts.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 等待指定间隔
|
||||
await Task.Delay(_autoSaveInterval, _autoSaveCts.Token);
|
||||
|
||||
// 执行自动保存
|
||||
await PerformAutoSaveAsync(slot);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常取消
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"自动保存错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}, _autoSaveCts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止自动保存
|
||||
/// </summary>
|
||||
public void StopAutoSave()
|
||||
{
|
||||
if (!_isAutoSaveEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("停止自动保存");
|
||||
|
||||
_autoSaveCts?.Cancel();
|
||||
_autoSaveCts?.Dispose();
|
||||
_autoSaveCts = null;
|
||||
_isAutoSaveEnabled = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行自动保存
|
||||
/// </summary>
|
||||
private async Task PerformAutoSaveAsync(int slot)
|
||||
{
|
||||
Console.WriteLine($"\n[自动保存] 保存到槽位 {slot}...");
|
||||
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
var success = await saveSystem.SaveGameAsync(slot);
|
||||
|
||||
if (success)
|
||||
{
|
||||
Console.WriteLine("[自动保存] 完成");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[自动保存] 失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查自动保存是否启用
|
||||
/// </summary>
|
||||
public bool IsAutoSaveEnabled() => _isAutoSaveEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// 设置自动保存间隔
|
||||
/// </summary>
|
||||
public void SetAutoSaveInterval(TimeSpan interval)
|
||||
{
|
||||
_autoSaveInterval = interval;
|
||||
Console.WriteLine($"自动保存间隔已设置为 {interval.TotalSeconds} 秒");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用 `CancellationTokenSource` 控制后台任务
|
||||
- `StartAutoSave` 启动定时保存任务
|
||||
- `StopAutoSave` 停止自动保存
|
||||
- 使用 `Task.Delay` 实现定时触发
|
||||
- 捕获并处理异常,避免自动保存失败影响游戏
|
||||
|
||||
## 步骤 4:实现游戏数据更新
|
||||
|
||||
创建一个系统来模拟游戏数据的变化。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.extensions;
|
||||
using MyGame.Data;
|
||||
using System;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏逻辑系统(模拟)
|
||||
/// </summary>
|
||||
public class GameLogicSystem : AbstractSystem
|
||||
{
|
||||
private Random _random = new();
|
||||
|
||||
/// <summary>
|
||||
/// 模拟玩家升级
|
||||
/// </summary>
|
||||
public void LevelUp()
|
||||
{
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
var save = saveSystem.GetCurrentSave();
|
||||
|
||||
if (save == null)
|
||||
{
|
||||
Console.WriteLine("没有当前存档");
|
||||
return;
|
||||
}
|
||||
|
||||
save.Player.Level++;
|
||||
save.Player.Experience = 0;
|
||||
save.Player.MaxHealth += 10;
|
||||
save.Player.Health = save.Player.MaxHealth;
|
||||
|
||||
Console.WriteLine($"\n玩家升级! 当前等级: {save.Player.Level}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟获得金币
|
||||
/// </summary>
|
||||
public void AddGold(int amount)
|
||||
{
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
var save = saveSystem.GetCurrentSave();
|
||||
|
||||
if (save == null) return;
|
||||
|
||||
save.Player.Gold += amount;
|
||||
Console.WriteLine($"\n获得金币 +{amount}, 当前: {save.Player.Gold}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟完成关卡
|
||||
/// </summary>
|
||||
public void CompleteLevel(int level)
|
||||
{
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
var save = saveSystem.GetCurrentSave();
|
||||
|
||||
if (save == null) return;
|
||||
|
||||
if (!save.Progress.CompletedLevels.Contains(level))
|
||||
{
|
||||
save.Progress.CompletedLevels.Add(level);
|
||||
Console.WriteLine($"\n完成关卡 {level}!");
|
||||
}
|
||||
|
||||
save.Progress.CurrentLevel = level + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟获得物品
|
||||
/// </summary>
|
||||
public void AddItem(string itemId, string itemName, int quantity = 1)
|
||||
{
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
var save = saveSystem.GetCurrentSave();
|
||||
|
||||
if (save == null) return;
|
||||
|
||||
// 查找已有物品
|
||||
var existingItem = save.Inventory.Items.Find(i => i.Id == itemId);
|
||||
|
||||
if (existingItem != null)
|
||||
{
|
||||
existingItem.Quantity += quantity;
|
||||
}
|
||||
else
|
||||
{
|
||||
save.Inventory.Items.Add(new ItemData
|
||||
{
|
||||
Id = itemId,
|
||||
Name = itemName,
|
||||
Quantity = quantity
|
||||
});
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n获得物品: {itemName} x{quantity}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟游戏进行
|
||||
/// </summary>
|
||||
public void SimulateGameplay()
|
||||
{
|
||||
Console.WriteLine("\n=== 模拟游戏进行 ===");
|
||||
|
||||
// 随机事件
|
||||
int eventType = _random.Next(0, 4);
|
||||
|
||||
switch (eventType)
|
||||
{
|
||||
case 0:
|
||||
AddGold(_random.Next(10, 100));
|
||||
break;
|
||||
case 1:
|
||||
LevelUp();
|
||||
break;
|
||||
case 2:
|
||||
CompleteLevel(_random.Next(1, 10));
|
||||
break;
|
||||
case 3:
|
||||
AddItem($"item_{_random.Next(1, 5)}", $"物品 {_random.Next(1, 5)}", 1);
|
||||
break;
|
||||
}
|
||||
|
||||
// 增加游戏时间
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
var save = saveSystem.GetCurrentSave();
|
||||
if (save != null)
|
||||
{
|
||||
save.TotalPlayTime += 0.1f;
|
||||
save.Progress.PlayTime += 0.1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 提供各种游戏事件的模拟方法
|
||||
- 直接修改当前存档数据
|
||||
- 用于测试存档系统功能
|
||||
|
||||
## 步骤 5:注册系统并测试
|
||||
|
||||
在架构中注册所有系统并进行测试。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.architecture;
|
||||
using GFramework.Game.Abstractions.data;
|
||||
using GFramework.Game.Abstractions.storage;
|
||||
using GFramework.Game.data;
|
||||
using GFramework.Game.storage;
|
||||
using MyGame.Data;
|
||||
using MyGame.Systems;
|
||||
|
||||
namespace MyGame
|
||||
{
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
public static IArchitecture Interface { get; private set; }
|
||||
|
||||
protected override void Init()
|
||||
{
|
||||
Interface = this;
|
||||
|
||||
// 注册存储系统
|
||||
var storage = new FileStorage("./game_data");
|
||||
RegisterUtility<IFileStorage>(storage);
|
||||
|
||||
// 注册存档仓库
|
||||
var saveConfig = new SaveConfiguration
|
||||
{
|
||||
SaveRoot = "saves",
|
||||
SaveSlotPrefix = "slot_",
|
||||
SaveFileName = "save.json"
|
||||
};
|
||||
|
||||
var saveRepo = new SaveRepository<GameSaveData>(storage, saveConfig);
|
||||
RegisterUtility<ISaveRepository<GameSaveData>>(saveRepo);
|
||||
|
||||
// 注册系统
|
||||
RegisterSystem(new SaveSystem());
|
||||
RegisterSystem(new AutoSaveSystem());
|
||||
RegisterSystem(new GameLogicSystem());
|
||||
|
||||
Console.WriteLine("游戏架构初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 测试代码
|
||||
|
||||
```csharp
|
||||
using MyGame;
|
||||
using MyGame.Systems;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("=== 存档系统测试 ===\n");
|
||||
|
||||
// 1. 初始化架构
|
||||
var architecture = new GameArchitecture();
|
||||
architecture.Initialize();
|
||||
await architecture.WaitUntilReadyAsync();
|
||||
|
||||
// 2. 获取系统
|
||||
var saveSystem = architecture.GetSystem<SaveSystem>();
|
||||
var autoSaveSystem = architecture.GetSystem<AutoSaveSystem>();
|
||||
var gameLogic = architecture.GetSystem<GameLogicSystem>();
|
||||
|
||||
// 3. 创建新存档
|
||||
Console.WriteLine("--- 测试 1: 创建新存档 ---");
|
||||
saveSystem.CreateNewSave("我的冒险");
|
||||
await saveSystem.SaveGameAsync(1);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 4. 模拟游戏进行
|
||||
Console.WriteLine("--- 测试 2: 游戏进行 ---");
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
gameLogic.SimulateGameplay();
|
||||
await Task.Delay(500);
|
||||
}
|
||||
|
||||
// 5. 手动保存
|
||||
Console.WriteLine("\n--- 测试 3: 手动保存 ---");
|
||||
await saveSystem.SaveGameAsync(1);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 6. 创建第二个存档
|
||||
Console.WriteLine("--- 测试 4: 创建第二个存档 ---");
|
||||
saveSystem.CreateNewSave("新的旅程");
|
||||
gameLogic.AddGold(500);
|
||||
gameLogic.LevelUp();
|
||||
await saveSystem.SaveGameAsync(2);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 7. 列出所有存档
|
||||
Console.WriteLine("--- 测试 5: 列出所有存档 ---");
|
||||
await saveSystem.ListSavesAsync();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 8. 加载第一个存档
|
||||
Console.WriteLine("--- 测试 6: 加载存档 ---");
|
||||
await saveSystem.LoadGameAsync(1);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 9. 启动自动保存
|
||||
Console.WriteLine("--- 测试 7: 启动自动保存 ---");
|
||||
autoSaveSystem.StartAutoSave(1, TimeSpan.FromSeconds(3));
|
||||
|
||||
// 模拟游戏进行
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
gameLogic.SimulateGameplay();
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
// 10. 停止自动保存
|
||||
autoSaveSystem.StopAutoSave();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 11. 删除存档
|
||||
Console.WriteLine("--- 测试 8: 删除存档 ---");
|
||||
await saveSystem.DeleteSaveAsync(2);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 12. 最终存档列表
|
||||
Console.WriteLine("--- 测试 9: 最终存档列表 ---");
|
||||
await saveSystem.ListSavesAsync();
|
||||
|
||||
Console.WriteLine("=== 测试完成 ===");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 注册存储系统和存档仓库
|
||||
- 注册所有游戏系统
|
||||
- 测试存档的创建、保存、加载、删除
|
||||
- 测试自动保存功能
|
||||
- 测试多槽位管理
|
||||
|
||||
## 完整代码
|
||||
|
||||
所有代码文件已在上述步骤中提供。项目结构如下:
|
||||
|
||||
```
|
||||
MyGame/
|
||||
├── Data/
|
||||
│ ├── PlayerData.cs
|
||||
│ ├── ProgressData.cs
|
||||
│ ├── InventoryData.cs
|
||||
│ └── GameSaveData.cs
|
||||
├── Systems/
|
||||
│ ├── SaveSystem.cs
|
||||
│ ├── AutoSaveSystem.cs
|
||||
│ └── GameLogicSystem.cs
|
||||
├── GameArchitecture.cs
|
||||
└── Program.cs
|
||||
```
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行程序后,你将看到类似以下的输出:
|
||||
|
||||
```
|
||||
=== 存档系统测试 ===
|
||||
|
||||
游戏架构初始化完成
|
||||
|
||||
--- 测试 1: 创建新存档 ---
|
||||
创建新存档: 我的冒险
|
||||
|
||||
=== 保存游戏到槽位 1 ===
|
||||
存档已保存: 我的冒险
|
||||
玩家: Player, 等级 1
|
||||
游戏时间: 0.0 小时
|
||||
保存时间: 2026-03-07 10:30:00
|
||||
|
||||
--- 测试 2: 游戏进行 ---
|
||||
|
||||
=== 模拟游戏进行 ===
|
||||
|
||||
获得金币 +45, 当前: 45
|
||||
|
||||
=== 模拟游戏进行 ===
|
||||
|
||||
玩家升级! 当前等级: 2
|
||||
|
||||
=== 模拟游戏进行 ===
|
||||
|
||||
完成关卡 3!
|
||||
|
||||
=== 模拟游戏进行 ===
|
||||
|
||||
获得物品: 物品 2 x1
|
||||
|
||||
=== 模拟游戏进行 ===
|
||||
|
||||
获得金币 +78, 当前: 123
|
||||
|
||||
--- 测试 3: 手动保存 ---
|
||||
|
||||
=== 保存游戏到槽位 1 ===
|
||||
存档已保存: 我的冒险
|
||||
玩家: Player, 等级 2
|
||||
游戏时间: 0.5 小时
|
||||
保存时间: 2026-03-07 10:30:05
|
||||
|
||||
--- 测试 4: 创建第二个存档 ---
|
||||
创建新存档: 新的旅程
|
||||
|
||||
获得金币 +500, 当前: 500
|
||||
|
||||
玩家升级! 当前等级: 2
|
||||
|
||||
=== 保存游戏到槽位 2 ===
|
||||
存档已保存: 新的旅程
|
||||
玩家: Player, 等级 2
|
||||
游戏时间: 0.0 小时
|
||||
保存时间: 2026-03-07 10:30:06
|
||||
|
||||
--- 测试 5: 列出所有存档 ---
|
||||
|
||||
=== 存档列表 (2 个) ===
|
||||
槽位 1: 我的冒险
|
||||
玩家: Player, 等级 2
|
||||
保存时间: 2026-03-07 10:30:05
|
||||
游戏时间: 0.5 小时
|
||||
槽位 2: 新的旅程
|
||||
玩家: Player, 等级 2
|
||||
保存时间: 2026-03-07 10:30:06
|
||||
游戏时间: 0.0 小时
|
||||
|
||||
--- 测试 6: 加载存档 ---
|
||||
|
||||
=== 从槽位 1 加载游戏 ===
|
||||
存档已加载: 我的冒险
|
||||
玩家: Player, 等级 2
|
||||
当前关卡: 4
|
||||
金币: 123
|
||||
物品数量: 1
|
||||
保存时间: 2026-03-07 10:30:05
|
||||
|
||||
--- 测试 7: 启动自动保存 ---
|
||||
|
||||
启动自动保存 (间隔: 3 秒)
|
||||
|
||||
=== 模拟游戏进行 ===
|
||||
...
|
||||
|
||||
[自动保存] 保存到槽位 1...
|
||||
|
||||
=== 保存游戏到槽位 1 ===
|
||||
存档已保存: 我的冒险
|
||||
...
|
||||
[自动保存] 完成
|
||||
|
||||
停止自动保存
|
||||
|
||||
--- 测试 8: 删除存档 ---
|
||||
|
||||
删除槽位 2 的存档
|
||||
槽位 2 的存档已删除
|
||||
|
||||
--- 测试 9: 最终存档列表 ---
|
||||
|
||||
=== 存档列表 (1 个) ===
|
||||
槽位 1: 我的冒险
|
||||
玩家: Player, 等级 3
|
||||
保存时间: 2026-03-07 10:30:15
|
||||
游戏时间: 1.5 小时
|
||||
|
||||
=== 测试完成 ===
|
||||
```
|
||||
|
||||
**验证步骤**:
|
||||
|
||||
1. 存档创建和保存成功
|
||||
2. 游戏数据正确更新
|
||||
3. 多槽位管理正常
|
||||
4. 存档加载恢复数据
|
||||
5. 自动保存定时触发
|
||||
6. 存档删除功能正常
|
||||
7. 存档列表显示正确
|
||||
|
||||
## 下一步
|
||||
|
||||
恭喜!你已经实现了一个完整的存档系统。接下来可以学习:
|
||||
|
||||
- [Godot 完整项目搭建](/zh-CN/tutorials/godot-complete-project) - 在 Godot 中使用存档系统
|
||||
- [数据迁移实践](/zh-CN/tutorials/data-migration) - 处理存档版本升级
|
||||
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 异步加载存档
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [数据与存档系统](/zh-CN/game/data) - 数据系统详细说明
|
||||
- [存储系统](/zh-CN/game/storage) - 存储系统详解
|
||||
- [序列化系统](/zh-CN/game/serialization) - 数据序列化
|
||||
- [System 层](/zh-CN/core/system) - System 详细说明
|
||||
746
docs/zh-CN/tutorials/state-machine-tutorial.md
Normal file
746
docs/zh-CN/tutorials/state-machine-tutorial.md
Normal file
@ -0,0 +1,746 @@
|
||||
---
|
||||
title: 实现状态机
|
||||
description: 学习如何使用状态机系统管理游戏状态和场景切换
|
||||
---
|
||||
|
||||
# 实现状态机
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- 理解状态机的概念和应用场景
|
||||
- 创建自定义游戏状态
|
||||
- 实现状态之间的转换和验证
|
||||
- 使用异步状态处理加载操作
|
||||
- 在状态中访问架构组件
|
||||
- 实现完整的游戏流程控制
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已安装 GFramework.Core NuGet 包
|
||||
- 了解 C# 基础语法和 async/await
|
||||
- 阅读过[架构概览](/zh-CN/getting-started)
|
||||
- 了解[生命周期管理](/zh-CN/core/lifecycle)
|
||||
|
||||
## 步骤 1:定义游戏状态
|
||||
|
||||
首先,让我们为一个简单的游戏定义几个基本状态:主菜单、加载、游戏中、暂停和游戏结束。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.state;
|
||||
using GFramework.Core.state;
|
||||
|
||||
namespace MyGame.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 主菜单状态
|
||||
/// </summary>
|
||||
public class MenuState : ContextAwareStateBase
|
||||
{
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
Console.WriteLine("=== 进入主菜单 ===");
|
||||
|
||||
// 显示菜单 UI
|
||||
ShowMenuUI();
|
||||
|
||||
// 播放菜单音乐
|
||||
PlayMenuMusic();
|
||||
}
|
||||
|
||||
public override void OnExit(IState? to)
|
||||
{
|
||||
Console.WriteLine("退出主菜单");
|
||||
|
||||
// 隐藏菜单 UI
|
||||
HideMenuUI();
|
||||
}
|
||||
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 菜单只能切换到加载状态
|
||||
return target is LoadingState;
|
||||
}
|
||||
|
||||
private void ShowMenuUI()
|
||||
{
|
||||
Console.WriteLine("显示菜单界面");
|
||||
}
|
||||
|
||||
pd HideMenuUI()
|
||||
{
|
||||
Console.WriteLine("隐藏菜单界面");
|
||||
}
|
||||
|
||||
private void PlayMenuMusic()
|
||||
{
|
||||
Console.WriteLine("播放菜单音乐");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏中状态
|
||||
/// </summary>
|
||||
public class GameplayState : ContextAwareStateBase
|
||||
{
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
Console.WriteLine("=== 开始游戏 ===");
|
||||
|
||||
// 初始化游戏场景
|
||||
InitializeGameScene();
|
||||
|
||||
// 重置玩家数据
|
||||
ResetPlayerData();
|
||||
|
||||
// 播放游戏音乐
|
||||
PlayGameMusic();
|
||||
}
|
||||
|
||||
public override void OnExit(IState? to)
|
||||
{
|
||||
Console.WriteLine("结束游戏");
|
||||
|
||||
// 保存游戏进度(如果不是游戏结束)
|
||||
if (to is not GameOverState)
|
||||
{
|
||||
SaveGameProgress();
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 游戏中可以切换到暂停或游戏结束状态
|
||||
return target is PauseState or GameOverState;
|
||||
}
|
||||
|
||||
private void InitializeGameScene()
|
||||
{
|
||||
Console.WriteLine("初始化游戏场景");
|
||||
}
|
||||
|
||||
private void ResetPlayerData()
|
||||
{
|
||||
Console.WriteLine("重置玩家数据");
|
||||
}
|
||||
|
||||
private void PlayGameMusic()
|
||||
{
|
||||
Console.WriteLine("播放游戏音乐");
|
||||
}
|
||||
|
||||
private void SaveGameProgress()
|
||||
{
|
||||
Console.WriteLine("保存游戏进度");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停状态
|
||||
/// </summary>
|
||||
public class PauseState : ContextAwareStateBase
|
||||
{
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
Console.WriteLine("=== 游戏暂停 ===");
|
||||
|
||||
// 显示暂停菜单
|
||||
ShowPauseMenu();
|
||||
|
||||
// 暂停游戏逻辑
|
||||
PauseGameLogic();
|
||||
}
|
||||
|
||||
public override void OnExit(IState? to)
|
||||
{
|
||||
Console.WriteLine("取消暂停");
|
||||
|
||||
// 隐藏暂停菜单
|
||||
HidePauseMenu();
|
||||
|
||||
// 恢复游戏逻辑
|
||||
ResumeGameLogic();
|
||||
}
|
||||
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 暂停状态可以返回游戏或退出到菜单
|
||||
return target is GameplayState or MenuState;
|
||||
}
|
||||
|
||||
private void ShowPauseMenu()
|
||||
{
|
||||
Console.WriteLine("显示暂停菜单");
|
||||
}
|
||||
|
||||
private void HidePauseMenu()
|
||||
{
|
||||
Console.WriteLine("隐藏暂停菜单");
|
||||
}
|
||||
|
||||
private void PauseGameLogic()
|
||||
{
|
||||
Console.WriteLine("暂停游戏逻辑");
|
||||
}
|
||||
|
||||
private void ResumeGameLogic()
|
||||
{
|
||||
Console.WriteLine("恢复游戏逻辑");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏结束状态
|
||||
/// </summary>
|
||||
public class GameOverState : ContextAwareStateBase
|
||||
{
|
||||
public bool IsVictory { get; set; }
|
||||
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
Console.WriteLine(IsVictory
|
||||
? "=== 游戏胜利 ==="
|
||||
: "=== 游戏失败 ===");
|
||||
|
||||
// 显示结算界面
|
||||
ShowGameOverUI();
|
||||
|
||||
// 播放结算音乐
|
||||
PlayGameOverMusic();
|
||||
}
|
||||
|
||||
public override void OnExit(IState? to)
|
||||
{
|
||||
Console.WriteLine("退出结算界面");
|
||||
|
||||
// 隐藏结算界面
|
||||
HideGameOverUI();
|
||||
}
|
||||
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 游戏结束只能返回菜单
|
||||
return target is MenuState;
|
||||
}
|
||||
|
||||
private void ShowGameOverUI()
|
||||
{
|
||||
Console.WriteLine("显示结算界面");
|
||||
}
|
||||
|
||||
private void HideGameOverUI()
|
||||
{
|
||||
Console.WriteLine("隐藏结算界面");
|
||||
}
|
||||
|
||||
private void PlayGameOverMusic()
|
||||
{
|
||||
Console.WriteLine("播放结算音乐");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 继承 `ContextAwareStateBase` 创建状态
|
||||
- `OnEnter` 在进入状态时调用,用于初始化
|
||||
- `OnExit` 在退出状态时调用,用于清理
|
||||
- `CanTransitionTo` 定义允许的状态转换规则
|
||||
- 每个状态职责单一,逻辑清晰
|
||||
|
||||
## 步骤 2:创建异步加载状态
|
||||
|
||||
实现一个异步加载状态,用于加载游戏资源。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.state;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MyGame.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 加载状态(异步)
|
||||
/// </summary>
|
||||
public class LoadingState : AsyncContextAwareStateBase
|
||||
{
|
||||
public int TargetLevel { get; set; } = 1;
|
||||
|
||||
public override async Task OnEnterAsync(IState? from)
|
||||
{
|
||||
Console.WriteLine($"=== 开始加载关卡 {TargetLevel} ===");
|
||||
|
||||
// 显示加载界面
|
||||
ShowLoadingUI();
|
||||
|
||||
// 异步加载资源
|
||||
await LoadResourcesAsync();
|
||||
|
||||
// 加载完成后自动切换到游戏状态
|
||||
Console.WriteLine("加载完成,进入游戏");
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
await stateMachine.ChangeToAsync<GameplayState>();
|
||||
}
|
||||
|
||||
public override async Task OnExitAsync(IState? to)
|
||||
{
|
||||
Console.WriteLine("退出加载状态");
|
||||
|
||||
// 隐藏加载界面
|
||||
HideLoadingUI();
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 加载状态只能切换到游戏状态
|
||||
return target is GameplayState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步加载资源
|
||||
/// </summary>
|
||||
private async Task LoadResourcesAsync()
|
||||
{
|
||||
// 加载纹理
|
||||
Console.WriteLine("加载纹理资源...");
|
||||
await Task.Delay(500);
|
||||
Console.WriteLine("纹理加载完成 (33%)");
|
||||
|
||||
// 加载音频
|
||||
Console.WriteLine("加载音频资源...");
|
||||
await Task.Delay(500);
|
||||
Console.WriteLine("音频加载完成 (66%)");
|
||||
|
||||
// 加载关卡数据
|
||||
Console.WriteLine("加载关卡数据...");
|
||||
await Task.Delay(500);
|
||||
Console.WriteLine("关卡数据加载完成 (100%)");
|
||||
}
|
||||
|
||||
private void ShowLoadingUI()
|
||||
{
|
||||
Console.WriteLine("显示加载界面");
|
||||
}
|
||||
|
||||
private void HideLoadingUI()
|
||||
{
|
||||
Console.WriteLine("隐藏加载界面");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 继承 `AsyncContextAwareStateBase` 支持异步操作
|
||||
- `OnEnterAsync` 和 `OnExitAsync` 是异步方法
|
||||
- 可以在状态中使用 `await` 等待异步操作
|
||||
- 加载完成后自动切换到下一个状态
|
||||
|
||||
## 步骤 3:注册状态机系统
|
||||
|
||||
在架构中注册状态机系统和所有状态。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.architecture;
|
||||
using GFramework.Core.Abstractions.state;
|
||||
using GFramework.Core.state;
|
||||
using MyGame.States;
|
||||
|
||||
namespace MyGame
|
||||
{
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
public static IArchitecture Interface { get; private set; }
|
||||
|
||||
protected override void Init()
|
||||
{
|
||||
Interface = this;
|
||||
|
||||
// 创建状态机系统
|
||||
var stateMachine = new StateMachineSystem();
|
||||
|
||||
// 注册所有状态
|
||||
stateMachine
|
||||
.Register(new MenuState())
|
||||
.Register(new LoadingState())
|
||||
.Register(new GameplayState())
|
||||
.Register(new PauseState())
|
||||
.Register(new GameOverState());
|
||||
|
||||
// 注册状态机系统到架构
|
||||
RegisterSystem<IStateMachineSystem>(stateMachine);
|
||||
|
||||
Console.WriteLine("状态机系统初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 创建 `StateMachineSystem` 实例
|
||||
- 使用链式调用注册所有状态
|
||||
- 将状态机注册为 `IStateMachineSystem` 服务
|
||||
- 状态会自动获得架构上下文
|
||||
|
||||
## 步骤 4:创建游戏控制器
|
||||
|
||||
创建控制器来管理游戏流程和状态切换。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Core.Abstractions.controller;
|
||||
using GFramework.Core.Abstractions.state;
|
||||
using GFramework.Core.extensions;
|
||||
using MyGame.States;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MyGame.Controllers
|
||||
{
|
||||
public class GameFlowController : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
/// <summary>
|
||||
/// 开始游戏
|
||||
/// </summary>
|
||||
public async Task StartGame(int level = 1)
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 设置加载状态的目标关卡
|
||||
var loadingState = stateMachine.GetState<LoadingState>();
|
||||
if (loadingState != null)
|
||||
{
|
||||
loadingState.TargetLevel = level;
|
||||
}
|
||||
|
||||
// 切换到加载状态
|
||||
var success = await stateMachine.ChangeToAsync<LoadingState>();
|
||||
|
||||
if (!success)
|
||||
{
|
||||
Console.WriteLine("无法开始游戏:状态切换失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停游戏
|
||||
/// </summary>
|
||||
public async Task PauseGame()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 检查当前是否在游戏状态
|
||||
if (stateMachine.Current is not GameplayState)
|
||||
{
|
||||
Console.WriteLine("当前不在游戏中,无法暂停");
|
||||
return;
|
||||
}
|
||||
|
||||
// 切换到暂停状态
|
||||
await stateMachine.ChangeToAsync<PauseState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复游戏
|
||||
/// </summary>
|
||||
public async Task ResumeGame()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 检查当前是否在暂停状态
|
||||
if (stateMachine.Current is not PauseState)
|
||||
{
|
||||
Console.WriteLine("当前不在暂停状态");
|
||||
return;
|
||||
}
|
||||
|
||||
// 返回游戏状态
|
||||
await stateMachine.ChangeToAsync<GameplayState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏结束
|
||||
/// </summary>
|
||||
public async Task EndGame(bool isVictory)
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 设置游戏结束状态的数据
|
||||
var gameOverState = stateMachine.GetState<GameOverState>();
|
||||
if (gameOverState != null)
|
||||
{
|
||||
gameOverState.IsVictory = isVictory;
|
||||
}
|
||||
|
||||
// 切换到游戏结束状态
|
||||
await stateMachine.ChangeToAsync<GameOverState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回主菜单
|
||||
/// </summary>
|
||||
public async Task ReturnToMenu()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 切换到菜单状态
|
||||
var success = await stateMachine.ChangeToAsync<MenuState>();
|
||||
|
||||
if (!success)
|
||||
{
|
||||
Console.WriteLine("无法返回菜单:状态切换被拒绝");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示当前状态
|
||||
/// </summary>
|
||||
public void ShowCurrentState()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
var currentState = stateMachine.Current;
|
||||
|
||||
if (currentState != null)
|
||||
{
|
||||
Console.WriteLine($"当前状态: {currentState.GetType().Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("当前没有活动状态");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示状态历史
|
||||
/// </summary>
|
||||
public void ShowStateHistory()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
var history = stateMachine.GetStateHistory();
|
||||
|
||||
Console.WriteLine("状态历史:");
|
||||
foreach (var state in history)
|
||||
{
|
||||
Console.WriteLine($" - {state.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 实现 `IController` 接口
|
||||
- 提供各种游戏流程控制方法
|
||||
- 使用 `GetState<T>()` 获取状态实例并设置数据
|
||||
- 使用 `ChangeToAsync<T>()` 切换状态
|
||||
- 检查切换结果并处理失败情况
|
||||
|
||||
## 步骤 5:测试状态机
|
||||
|
||||
编写测试代码验证状态机功能。
|
||||
|
||||
```csharp
|
||||
using MyGame;
|
||||
using MyGame.Controllers;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("=== 状态机系统测试 ===\n");
|
||||
|
||||
// 1. 初始化架构
|
||||
var architecture = new GameArchitecture();
|
||||
architecture.Initialize();
|
||||
await architecture.WaitUntilReadyAsync();
|
||||
|
||||
// 2. 创建游戏流程控制器
|
||||
var gameFlow = new GameFlowController();
|
||||
|
||||
// 3. 切换到主菜单
|
||||
Console.WriteLine("\n--- 测试 1: 进入主菜单 ---");
|
||||
await gameFlow.ReturnToMenu();
|
||||
gameFlow.ShowCurrentState();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 4. 开始游戏(会经过加载状态)
|
||||
Console.WriteLine("\n--- 测试 2: 开始游戏 ---");
|
||||
await gameFlow.StartGame(level: 1);
|
||||
await Task.Delay(2000); // 等待加载完成
|
||||
gameFlow.ShowCurrentState();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 5. 暂停游戏
|
||||
Console.WriteLine("\n--- 测试 3: 暂停游戏 ---");
|
||||
await gameFlow.PauseGame();
|
||||
gameFlow.ShowCurrentState();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 6. 恢复游戏
|
||||
Console.WriteLine("\n--- 测试 4: 恢复游戏 ---");
|
||||
await gameFlow.ResumeGame();
|
||||
gameFlow.ShowCurrentState();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 7. 游戏胜利
|
||||
Console.WriteLine("\n--- 测试 5: 游戏胜利 ---");
|
||||
await gameFlow.EndGame(isVictory: true);
|
||||
gameFlow.ShowCurrentState();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 8. 返回菜单
|
||||
Console.WriteLine("\n--- 测试 6: 返回菜单 ---");
|
||||
await gameFlow.ReturnToMenu();
|
||||
gameFlow.ShowCurrentState();
|
||||
|
||||
// 9. 显示状态历史
|
||||
Console.WriteLine("\n--- 状态历史 ---");
|
||||
gameFlow.ShowStateHistory();
|
||||
|
||||
Console.WriteLine("\n=== 测试完成 ===");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 初始化架构并等待就绪
|
||||
- 按顺序测试各种状态切换
|
||||
- 使用 `Task.Delay` 模拟游戏运行
|
||||
- 验证状态转换规则和历史记录
|
||||
|
||||
## 完整代码
|
||||
|
||||
所有代码文件已在上述步骤中提供。项目结构如下:
|
||||
|
||||
```
|
||||
MyGame/
|
||||
├── States/
|
||||
│ ├── MenuState.cs
|
||||
│ ├── LoadingState.cs
|
||||
│ ├── GameplayState.cs
|
||||
│ ├── PauseState.cs
|
||||
│ └── GameOverState.cs
|
||||
├── Controllers/
|
||||
│ └── GameFlowController.cs
|
||||
├── GameArchitecture.cs
|
||||
└── Program.cs
|
||||
```
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行程序后,你将看到类似以下的输出:
|
||||
|
||||
```
|
||||
=== 状态机系统测试 ===
|
||||
|
||||
状态机系统初始化完成
|
||||
|
||||
--- 测试 1: 进入主菜单 ---
|
||||
=== 进入主菜单 ===
|
||||
显示菜单界面
|
||||
播放菜单音乐
|
||||
当前状态: MenuState
|
||||
|
||||
--- 测试 2: 开始游戏 ---
|
||||
退出主菜单
|
||||
隐藏菜单界面
|
||||
=== 开始加载关卡 1 ===
|
||||
显示加载界面
|
||||
加载纹理资源...
|
||||
纹理加载完成 (33%)
|
||||
加载音频资源...
|
||||
音频加载完成 (66%)
|
||||
加载关卡数据...
|
||||
关卡数据加载完成 (100%)
|
||||
加载完成,进入游戏
|
||||
退出加载状态
|
||||
隐藏加载界面
|
||||
=== 开始游戏 ===
|
||||
初始化游戏场景
|
||||
重置玩家数据
|
||||
播放游戏音乐
|
||||
当前状态: GameplayState
|
||||
|
||||
--- 测试 3: 暂停游戏 ---
|
||||
结束游戏
|
||||
保存游戏进度
|
||||
=== 游戏暂停 ===
|
||||
显示暂停菜单
|
||||
暂停游戏逻辑
|
||||
当前状态: PauseState
|
||||
|
||||
--- 测试 4: 恢复游戏 ---
|
||||
取消暂停
|
||||
隐藏暂停菜单
|
||||
恢复游戏逻辑
|
||||
=== 开始游戏 ===
|
||||
初始化游戏场景
|
||||
重置玩家数据
|
||||
播放游戏音乐
|
||||
当前状态: GameplayState
|
||||
|
||||
--- 测试 5: 游戏胜利 ---
|
||||
结束游戏
|
||||
=== 游戏胜利 ===
|
||||
显示结算界面
|
||||
播放结算音乐
|
||||
当前状态: GameOverState
|
||||
|
||||
--- 测试 6: 返回菜单 ---
|
||||
退出结算界面
|
||||
隐藏结算界面
|
||||
=== 进入主菜单 ===
|
||||
显示菜单界面
|
||||
播放菜单音乐
|
||||
当前状态: MenuState
|
||||
|
||||
--- 状态历史 ---
|
||||
状态历史:
|
||||
- MenuState
|
||||
- LoadingState
|
||||
- GameplayState
|
||||
- PauseState
|
||||
- GameplayState
|
||||
- GameOverState
|
||||
- MenuState
|
||||
|
||||
=== 测试完成 ===
|
||||
```
|
||||
|
||||
**验证步骤**:
|
||||
|
||||
1. 所有状态切换成功执行
|
||||
2. 状态转换规则正确生效
|
||||
3. 异步加载状态正常工作
|
||||
4. 状态历史记录完整
|
||||
5. 状态数据传递正确
|
||||
|
||||
## 下一步
|
||||
|
||||
恭喜!你已经掌握了状态机系统的使用。接下来可以学习:
|
||||
|
||||
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 在状态中使用协程
|
||||
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 在加载状态中管理资源
|
||||
- [实现存档系统](/zh-CN/tutorials/save-system) - 保存和恢复游戏状态
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [状态机系统](/zh-CN/core/state-machine) - 状态机详细说明
|
||||
- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
|
||||
- [System 层](/zh-CN/core/system) - System 详细说明
|
||||
- [架构组件](/zh-CN/core/architecture) - 架构基础
|
||||
Loading…
x
Reference in New Issue
Block a user