GFramework/docs/zh-CN/tutorials/data-migration.md
GeWuYou fb14d7122c docs(style): 更新文档中的命名空间导入格式
- 将所有小写的命名空间导入更正为首字母大写格式
- 统一 GFramework 框架的命名空间引用规范
- 修复 core、ecs、godot 等模块的命名空间导入错误
- 标准化文档示例代码中的 using 语句格式
- 确保所有文档中的命名空间引用保持一致性
- 更新 global using 语句以匹配正确的命名空间格式
2026-03-10 07:18:49 +08:00

786 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 实现数据版本迁移
description: 学习如何实现数据版本迁移系统,处理不同版本间的数据升级
---
# 实现数据版本迁移
## 学习目标
完成本教程后,你将能够:
- 理解数据版本迁移的重要性和应用场景
- 定义版本化数据结构
- 实现数据迁移接口
- 注册和管理迁移策略
- 处理多版本连续升级
- 测试迁移流程的正确性
## 前置条件
- 已安装 GFramework.Game NuGet 包
- 了解 C# 基础语法和接口实现
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
- 了解[数据与存档系统](/zh-CN/game/data)
- 建议先完成[实现存档系统](/zh-CN/tutorials/save-system)教程
## 为什么需要数据迁移
在游戏开发过程中,数据结构经常会发生变化:
- **新增功能**:添加新的游戏系统需要新的数据字段
- **重构优化**:改进数据结构以提升性能或可维护性
- **修复问题**:修正早期设计的缺陷
- **平衡调整**:调整游戏数值和配置
数据迁移系统能够:
- 自动将旧版本数据升级到新版本
- 保证玩家存档的兼容性
- 避免数据丢失和游戏崩溃
- 提供平滑的版本过渡体验
## 步骤 1定义版本化数据结构
首先,让我们定义一个支持版本控制的游戏数据结构。
```csharp
using GFramework.Game.Abstractions.Data;
using System;
using System.Collections.Generic;
namespace MyGame.Data
{
/// <summary>
/// 玩家数据 - 版本 1初始版本
/// </summary>
public class PlayerSaveData : IVersionedData
{
// IVersionedData 接口要求的属性
public int Version { get; set; } = 1;
public DateTime LastModified { get; set; } = DateTime.Now;
// 基础数据
public string PlayerName { get; set; } = "Player";
public int Level { get; set; } = 1;
public int Gold { get; set; } = 0;
// 版本 1 的简单位置数据
public float PositionX { get; set; }
public float PositionY { get; set; }
}
}
```
**代码说明**
- 实现 `IVersionedData` 接口以支持版本管理
- `Version` 属性标识当前数据版本(从 1 开始)
- `LastModified` 记录最后修改时间
- 初始版本使用简单的 X、Y 坐标表示位置
## 步骤 2定义新版本数据结构
随着游戏开发,我们需要添加新功能,数据结构也需要升级。
```csharp
namespace MyGame.Data
{
/// <summary>
/// 玩家数据 - 版本 2添加 Z 轴和经验值)
/// </summary>
public class PlayerSaveDataV2 : IVersionedData
{
public int Version { get; set; } = 2;
public DateTime LastModified { get; set; } = DateTime.Now;
// 基础数据
public string PlayerName { get; set; } = "Player";
public int Level { get; set; } = 1;
public int Gold { get; set; } = 0;
// 版本 2添加 Z 轴支持 3D 游戏
public float PositionX { get; set; }
public float PositionY { get; set; }
public float PositionZ { get; set; } // 新增
// 版本 2添加经验值系统
public int Experience { get; set; } // 新增
public int ExperienceToNextLevel { get; set; } = 100; // 新增
}
/// <summary>
/// 玩家数据 - 版本 3重构为结构化数据
/// </summary>
public class PlayerSaveDataV3 : IVersionedData
{
public int Version { get; set; } = 3;
public DateTime LastModified { get; set; } = DateTime.Now;
// 基础数据
public string PlayerName { get; set; } = "Player";
public int Level { get; set; } = 1;
public int Gold { get; set; } = 0;
// 版本 3使用结构化的位置数据
public Vector3Data Position { get; set; } = new();
// 版本 3使用结构化的经验值数据
public ExperienceData Experience { get; set; } = new();
// 版本 3新增技能系统
public List<string> UnlockedSkills { get; set; } = new();
}
/// <summary>
/// 3D 位置数据
/// </summary>
public class Vector3Data
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
}
/// <summary>
/// 经验值数据
/// </summary>
public class ExperienceData
{
public int Current { get; set; }
public int ToNextLevel { get; set; } = 100;
}
}
```
**代码说明**
- **版本 2**:添加 Z 轴坐标和经验值系统
- **版本 3**:重构为更清晰的结构化数据
- 每个版本的 `Version` 属性递增
- 保持向后兼容,新字段提供默认值
## 步骤 3实现数据迁移器
创建迁移器来处理版本间的数据转换。
```csharp
using GFramework.Game.Abstractions.Setting;
using System;
namespace MyGame.Data.Migrations
{
/// <summary>
/// 从版本 1 迁移到版本 2
/// </summary>
public class PlayerDataMigration_V1_to_V2 : ISettingsMigration
{
public Type SettingsType => typeof(PlayerSaveData);
public int FromVersion => 1;
public int ToVersion => 2;
public ISettingsSection Migrate(ISettingsSection oldData)
{
if (oldData is not PlayerSaveData v1)
{
throw new ArgumentException($"Expected PlayerSaveData, got {oldData.GetType().Name}");
}
Console.WriteLine($"[迁移] 版本 1 -> 2: {v1.PlayerName}");
// 创建版本 2 数据
var v2 = new PlayerSaveDataV2
{
Version = 2,
LastModified = DateTime.Now,
// 复制现有数据
PlayerName = v1.PlayerName,
Level = v1.Level,
Gold = v1.Gold,
PositionX = v1.PositionX,
PositionY = v1.PositionY,
// 新字段Z 轴默认为 0
PositionZ = 0f,
// 新字段:根据等级计算经验值
Experience = 0,
ExperienceToNextLevel = 100 * v1.Level
};
Console.WriteLine($" - 添加 Z 轴坐标: {v2.PositionZ}");
Console.WriteLine($" - 初始化经验值系统: {v2.Experience}/{v2.ExperienceToNextLevel}");
return v2;
}
}
/// <summary>
/// 从版本 2 迁移到版本 3
/// </summary>
public class PlayerDataMigration_V2_to_V3 : ISettingsMigration
{
public Type SettingsType => typeof(PlayerSaveDataV2);
public int FromVersion => 2;
public int ToVersion => 3;
public ISettingsSection Migrate(ISettingsSection oldData)
{
if (oldData is not PlayerSaveDataV2 v2)
{
throw new ArgumentException($"Expected PlayerSaveDataV2, got {oldData.GetType().Name}");
}
Console.WriteLine($"[迁移] 版本 2 -> 3: {v2.PlayerName}");
// 创建版本 3 数据
var v3 = new PlayerSaveDataV3
{
Version = 3,
LastModified = DateTime.Now,
// 复制基础数据
PlayerName = v2.PlayerName,
Level = v2.Level,
Gold = v2.Gold,
// 迁移位置数据到结构化格式
Position = new Vector3Data
{
X = v2.PositionX,
Y = v2.PositionY,
Z = v2.PositionZ
},
// 迁移经验值数据到结构化格式
Experience = new ExperienceData
{
Current = v2.Experience,
ToNextLevel = v2.ExperienceToNextLevel
},
// 新字段:根据等级解锁基础技能
UnlockedSkills = GenerateDefaultSkills(v2.Level)
};
Console.WriteLine($" - 重构位置数据: ({v3.Position.X}, {v3.Position.Y}, {v3.Position.Z})");
Console.WriteLine($" - 重构经验值数据: {v3.Experience.Current}/{v3.Experience.ToNextLevel}");
Console.WriteLine($" - 初始化技能系统: {v3.UnlockedSkills.Count} 个技能");
return v3;
}
/// <summary>
/// 根据等级生成默认技能
/// </summary>
private List<string> GenerateDefaultSkills(int level)
{
var skills = new List<string> { "basic_attack" };
if (level >= 5)
skills.Add("power_strike");
if (level >= 10)
skills.Add("shield_block");
if (level >= 15)
skills.Add("ultimate_skill");
return skills;
}
}
}
```
**代码说明**
- 实现 `ISettingsMigration` 接口
- `SettingsType` 指定要迁移的数据类型
- `FromVersion``ToVersion` 定义迁移的版本范围
- `Migrate` 方法执行实际的数据转换
- 为新字段提供合理的默认值或计算值
- 添加日志输出便于调试
## 步骤 4注册迁移策略
创建迁移管理器来注册和执行迁移。
```csharp
using GFramework.Game.Abstractions.Setting;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MyGame.Data.Migrations
{
/// <summary>
/// 数据迁移管理器
/// </summary>
public class DataMigrationManager
{
private readonly Dictionary<(Type type, int from), ISettingsMigration> _migrations = new();
/// <summary>
/// 注册迁移器
/// </summary>
public void RegisterMigration(ISettingsMigration migration)
{
var key = (migration.SettingsType, migration.FromVersion);
if (_migrations.ContainsKey(key))
{
Console.WriteLine($"警告: 迁移器已存在 {migration.SettingsType.Name} " +
$"v{migration.FromVersion}->v{migration.ToVersion}");
return;
}
_migrations[key] = migration;
Console.WriteLine($"注册迁移器: {migration.SettingsType.Name} " +
$"v{migration.FromVersion} -> v{migration.ToVersion}");
}
/// <summary>
/// 执行迁移(支持跨多个版本)
/// </summary>
public ISettingsSection MigrateToLatest(ISettingsSection data, int targetVersion)
{
if (data is not IVersionedData versioned)
{
Console.WriteLine("数据不支持版本控制,跳过迁移");
return data;
}
var currentVersion = versioned.Version;
if (currentVersion == targetVersion)
{
Console.WriteLine($"数据已是最新版本 v{targetVersion}");
return data;
}
if (currentVersion > targetVersion)
{
Console.WriteLine($"警告: 数据版本 v{currentVersion} 高于目标版本 v{targetVersion}");
return data;
}
Console.WriteLine($"\n开始迁移: v{currentVersion} -> v{targetVersion}");
var current = data;
var currentVer = currentVersion;
// 逐步迁移到目标版本
while (currentVer < targetVersion)
{
var key = (current.GetType(), currentVer);
if (!_migrations.TryGetValue(key, out var migration))
{
Console.WriteLine($"错误: 找不到迁移器 {current.GetType().Name} v{currentVer}");
break;
}
current = migration.Migrate(current);
currentVer = migration.ToVersion;
Console.WriteLine($"迁移完成: v{currentVersion} -> v{currentVer}\n");
return current;
}
/// <summary>
/// 获取迁移路径
/// </summary>
public List<string> GetMigrationPath(Type dataType, int fromVersion, int toVersion)
{
var path = new List<string>();
var currentVer = fromVersion;
var currentType = dataType;
while (currentVer < toVersion)
{
var key = (currentType, currentVer);
if (!_migrations.TryGetValue(key, out var migration))
{
path.Add($"v{currentVer} -> ? (缺失迁移器)");
break;
}
path.Add($"v{migration.FromVersion} -> v{migration.ToVersion}");
currentVer = migration.ToVersion;
currentType = migration.SettingsType;
}
return path;
}
}
}
```
**代码说明**
- 使用字典存储迁移器,键为 (类型, 源版本)
- `RegisterMigration` 注册单个迁移器
- `MigrateToLatest` 自动执行多步迁移
- `GetMigrationPath` 显示迁移路径,便于调试
- 支持跨多个版本的连续迁移
## 步骤 5测试迁移流程
创建完整的测试程序验证迁移功能。
```csharp
using MyGame.Data;
using MyGame.Data.Migrations;
using System;
namespace MyGame
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("=== 数据迁移系统测试 ===\n");
// 1. 创建迁移管理器
var migrationManager = new DataMigrationManager();
// 2. 注册所有迁移器
Console.WriteLine("--- 注册迁移器 ---");
migrationManager.RegisterMigration(new PlayerDataMigration_V1_to_V2());
migrationManager.RegisterMigration(new PlayerDataMigration_V2_to_V3());
Console.WriteLine();
// 3. 测试场景 1从版本 1 迁移到版本 3
Console.WriteLine("--- 测试 1: V1 -> V3 迁移 ---");
var v1Data = new PlayerSaveData
{
Version = 1,
PlayerName = "老玩家",
Level = 12,
Gold = 5000,
PositionX = 100.5f,
PositionY = 200.3f
};
Console.WriteLine("原始数据 (V1):");
Console.WriteLine($" 玩家: {v1Data.PlayerName}");
Console.WriteLine($" 等级: {v1Data.Level}");
Console.WriteLine($" 金币: {v1Data.Gold}");
Console.WriteLine($" 位置: ({v1Data.PositionX}, {v1Data.PositionY})");
Console.WriteLine();
// 显示迁移路径
var path = migrationManager.GetMigrationPath(typeof(PlayerSaveData), 1, 3);
Console.WriteLine("迁移路径:");
foreach (var step in path)
{
Console.WriteLine($" {step}");
}
Console.WriteLine();
// 执行迁移
var v3Data = (PlayerSaveDataV3)migrationManager.MigrateToLatest(v1Data, 3);
Console.WriteLine("迁移后数据 (V3):");
Console.WriteLine($" 玩家: {v3Data.PlayerName}");
Console.WriteLine($" 等级: {v3Data.Level}");
Console.WriteLine($" 金币: {v3Data.Gold}");
Console.WriteLine($" 位置: ({v3Data.Position.X}, {v3Data.Position.Y}, {v3Data.Position.Z})");
Console.WriteLine($" 经验值: {v3Data.Experience.Current}/{v3Data.Experience.ToNextLevel}");
Console.WriteLine($" 技能: {string.Join(", ", v3Data.UnlockedSkills)}");
Console.WriteLine();
// 4. 测试场景 2从版本 2 迁移到版本 3
Console.WriteLine("--- 测试 2: V2 -> V3 迁移 ---");
var v2Data = new PlayerSaveDataV2
{
Version = 2,
PlayerName = "中期玩家",
Level = 8,
Gold = 2000,
PositionX = 50.0f,
PositionY = 75.0f,
PositionZ = 10.0f,
Experience = 350,
ExperienceToNextLevel = 800
};
Console.WriteLine("原始数据 (V2):");
Console.WriteLine($" 玩家: {v2Data.PlayerName}");
Console.WriteLine($" 等级: {v2Data.Level}");
Console.WriteLine($" 位置: ({v2Data.PositionX}, {v2Data.PositionY}, {v2Data.PositionZ})");
Console.WriteLine($" 经验值: {v2Data.Experience}/{v2Data.ExperienceToNextLevel}");
Console.WriteLine();
var v3Data2 = (PlayerSaveDataV3)migrationManager.MigrateToLatest(v2Data, 3);
Console.WriteLine("迁移后数据 (V3):");
Console.WriteLine($" 玩家: {v3Data2.PlayerName}");
Console.WriteLine($" 等级: {v3Data2.Level}");
Console.WriteLine($" 位置: ({v3Data2.Position.X}, {v3Data2.Position.Y}, {v3Data2.Position.Z})");
Console.WriteLine($" 经验值: {v3Data2.Experience.Current}/{v3Data2.Experience.ToNextLevel}");
Console.WriteLine($" 技能: {string.Join(", ", v3Data2.UnlockedSkills)}");
Console.WriteLine();
// 5. 测试场景 3已是最新版本
Console.WriteLine("--- 测试 3: 已是最新版本 ---");
var v3DataLatest = new PlayerSaveDataV3
{
Version = 3,
PlayerName = "新玩家",
Level = 1
};
migrationManager.MigrateToLatest(v3DataLatest, 3);
Console.WriteLine();
Console.WriteLine("=== 测试完成 ===");
}
}
}
```
**代码说明**
- 创建不同版本的测试数据
- 测试单步迁移V2 -> V3
- 测试多步迁移V1 -> V3
- 测试已是最新版本的情况
- 显示迁移前后的数据对比
## 完整代码
所有代码文件已在上述步骤中提供。项目结构如下:
```
MyGame/
├── Data/
│ ├── PlayerSaveData.cs # 版本 1 数据结构
│ ├── PlayerSaveDataV2.cs # 版本 2 数据结构
│ ├── PlayerSaveDataV3.cs # 版本 3 数据结构
│ └── Migrations/
│ ├── PlayerDataMigration_V1_to_V2.cs
│ ├── PlayerDataMigration_V2_to_V3.cs
│ └── DataMigrationManager.cs
└── Program.cs
```
## 运行结果
运行程序后,你将看到类似以下的输出:
```
=== 数据迁移系统测试 ===
--- 注册迁移器 ---
注册迁移器: PlayerSaveData v1 -> v2
注册迁移器: PlayerSaveDataV2 v2 -> v3
--- 测试 1: V1 -> V3 迁移 ---
原始数据 (V1):
玩家: 老玩家
等级: 12
金币: 5000
位置: (100.5, 200.3)
迁移路径:
v1 -> v2
v2 -> v3
开始迁移: v1 -> v3
[迁移] 版本 1 -> 2: 老玩家
- 添加 Z 轴坐标: 0
- 初始化经验值系统: 0/1200
[迁移] 版本 2 -> 3: 老玩家
- 重构位置数据: (100.5, 200.3, 0)
- 重构经验值数据: 0/1200
- 初始化技能系统: 3 个技能
迁移完成: v1 -> v3
迁移后数据 (V3):
玩家: 老玩家
等级: 12
金币: 5000
位置: (100.5, 200.3, 0)
经验值: 0/1200
技能: basic_attack, power_strike, shield_block
--- 测试 2: V2 -> V3 迁移 ---
原始数据 (V2):
玩家: 中期玩家
等级: 8
金币: 2000
位置: (50, 75, 10)
经验值: 350/800
开始迁移: v2 -> v3
[迁移] 版本 2 -> 3: 中期玩家
- 重构位置数据: (50, 75, 10)
- 重构经验值数据: 350/800
- 初始化技能系统: 2 个技能
迁移完成: v2 -> v3
迁移后数据 (V3):
玩家: 中期玩家
等级: 8
位置: (50, 75, 10)
经验值: 350/800
技能: basic_attack, power_strike
--- 测试 3: 已是最新版本 ---
数据已是最新版本 v3
=== 测试完成 ===
```
**验证步骤**
1. 迁移器成功注册
2. V1 数据正确迁移到 V3
3. V2 数据正确迁移到 V3
4. 新字段获得合理的默认值
5. 已是最新版本的数据不会重复迁移
6. 迁移路径清晰可追踪
## 下一步
恭喜!你已经实现了一个完整的数据版本迁移系统。接下来可以学习:
- [实现存档系统](/zh-CN/tutorials/save-system) - 结合存档系统使用迁移
- [Godot 完整项目搭建](/zh-CN/tutorials/godot-complete-project) - 在实际项目中应用
- [数据与存档系统](/zh-CN/game/data) - 深入了解数据系统
## 最佳实践
### 1. 版本号管理
```csharp
// 使用常量管理版本号
public static class DataVersions
{
public const int PlayerData_V1 = 1;
public const int PlayerData_V2 = 2;
public const int PlayerData_V3 = 3;
public const int PlayerData_Latest = PlayerData_V3;
}
```
### 2. 迁移测试
```csharp
// 为每个迁移器编写单元测试
[Test]
public void TestMigration_V1_to_V2()
{
var v1 = new PlayerSaveData { Level = 10 };
var migration = new PlayerDataMigration_V1_to_V2();
var v2 = (PlayerSaveDataV2)migration.Migrate(v1);
Assert.AreEqual(10, v2.Level);
Assert.AreEqual(0, v2.PositionZ);
Assert.AreEqual(1000, v2.ExperienceToNextLevel);
}
```
### 3. 数据备份
```csharp
// 迁移前自动备份
public ISettingsSection MigrateWithBackup(ISettingsSection data)
{
// 备份原始数据
var backup = SerializeData(data);
SaveBackup(backup);
try
{
// 执行迁移
var migrated = MigrateToLatest(data, targetVersion);
return migrated;
}
catch (Exception ex)
{
// 迁移失败,恢复备份
RestoreBackup(backup);
throw;
}
}
```
### 4. 迁移日志
```csharp
// 记录详细的迁移日志
public class MigrationLogger
{
public void LogMigration(string playerName, int from, int to)
{
var log = $"[{DateTime.Now}] {playerName}: v{from} -> v{to}";
File.AppendAllText("migration.log", log + "\n");
}
}
```
### 5. 向后兼容
- 新版本保留所有旧字段
- 为新字段提供合理的默认值
- 避免删除或重命名字段
- 使用 `[Obsolete]` 标记废弃字段
### 6. 性能优化
```csharp
// 批量迁移优化
public async Task<List<ISettingsSection>> MigrateBatchAsync(
List<ISettingsSection> dataList,
int targetVersion)
{
var tasks = dataList.Select(data =>
Task.Run(() => MigrateToLatest(data, targetVersion)));
return (await Task.WhenAll(tasks)).ToList();
}
```
## 常见问题
### 1. 如何处理跨多个版本的迁移?
迁移管理器会自动按顺序应用所有必要的迁移。例如从 V1 到 V3会先执行 V1->V2再执行 V2->V3。
### 2. 迁移失败如何处理?
建议在迁移前备份原始数据,迁移失败时可以恢复。同时在迁移过程中添加详细的日志记录。
### 3. 如何处理不兼容的数据变更?
对于破坏性变更,建议:
- 提供数据转换工具
- 在迁移中添加数据验证
- 通知用户可能的数据丢失
- 提供回滚机制
### 4. 是否需要保留所有历史版本的数据结构?
建议保留,这样可以:
- 支持从任意旧版本迁移
- 便于调试和测试
- 作为文档记录数据演变
### 5. 如何测试迁移功能?
- 创建各个版本的测试数据
- 验证迁移后的数据完整性
- 测试迁移链的正确性
- 使用真实的历史数据进行测试
## 相关文档
- [数据与存档系统](/zh-CN/game/data) - 数据系统详细说明
- [实现存档系统](/zh-CN/tutorials/save-system) - 存档系统教程
- [架构系统](/zh-CN/core/architecture) - 架构设计原则