主题
CloudData系统快速指南
概述
CloudData系统是WasiCore框架中的用户数据管理核心,提供统一、类型安全、高性能的云数据操作API。本指南将帮助您快速上手CloudData系统的基本使用。
重要概念区分
在开始之前,必须理解这个核心区别:
- Player.Id (
int):当局游戏的槽位编号(0,1,2...),每次游戏会话可能不同 - User.UserId (
long):用户的全局唯一标识,持久不变
玩家云数据使用 User.UserId 或 Player 对象(会自动提取 UserId)。非玩家的全局数据可以使用项目自行约定的保留 UserId。
全局数据也可以使用云数据
云数据按 userId 区分逻辑数据表,不要求这个 ID 对应真实存在的平台账号。正式平台用户的 UserId 大于 10000,通常为 9 位数及以上;项目可约定 0 < userId < 10000 的数值保存非玩家的全局数据,例如用 1 保存天梯表、2 保存游戏统计、3 保存全服活动进度。
在服务端直接使用 CloudData.ForUser(reservedUserId) 写入,并用相同 ID 查询即可;无需注册账号或创建玩家对象,只需等待 CloudData.WaitUntilReadyAsync()。ID 的用途应集中定义并保持稳定。完整示例见使用保留 UserId 保存全局数据。
正确的参数类型
csharp
// 正确:使用 UserId 参数
public async Task SavePlayerProgress(long userId, int level)
{
await CloudData.ForUser(userId).SetData("level", level).ExecuteAsync();
}
// 正确:使用 Player 对象参数(推荐用于游戏逻辑)
public async Task SavePlayerProgress(Player player, int level)
{
await CloudData.ForPlayer(player).SetData("level", level).ExecuteAsync();
}
// 错误:容易误导的参数名
public async Task SavePlayerProgress(long playerId, int level) // 看起来像Player.Id!
{
await CloudData.ForUser(playerId).SetData("level", level).ExecuteAsync(); // 易混淆
}执行额度
云数据默认每个游戏局共享每窗口 300 次查询、300 次事务提交、48 MiB 累计提交量阈值;正常 1 倍速下窗口为 60 秒。单次完整响应默认最多 32 MiB - 100 KiB。额度由同局所有玩家和业务模块共享,应批量查询、合并事务并预留余量。窗口计时受倍速和暂停影响;批量 API 计数、配置与超限处理见默认硬限制与计数规则。
5分钟快速开始
1. 基础概念
CloudData系统支持以下数据类型:
| 类型 | 用途 | 示例 |
|---|---|---|
| BigInt | 整数数据 | 等级、经验、积分 |
| VarChar255 | 字符串数据 | 昵称、状态 |
| Currency | 货币数据 | 金币、钻石 |
| CappedData | 有上限数据(可定时重置为0) | 体力、每日活跃度、签到进度 |
| Blob | 二进制数据 | 配置、存档 |
| ListItem | 列表数据 | 背包、好友 |
2. Key 命名与大小写
远端云数据数据库保存和比较 key 时按大小写不敏感语义处理,因此不要用 Gold 和 gold 表示两份不同数据。WasiCore 包装层也按这个语义调整了按 key 取值的便利方法:UserData<T>.GetByKey / HasKey / GetByKeys 以及批量查询返回的 BlobData、VarCharData、BigIntData、Utf8BlobData、DoubleBlobData 字典都使用大小写不敏感比较。
仍然建议项目内统一使用小写 ASCII 和下划线命名,并集中放在常量类里,避免同一业务字段出现多种写法。
csharp
public static class CloudDataKeys
{
public const string Level = "level";
public const string Gold = "gold";
public const string DailyQuestAttempts = "daily_quest_attempts";
}
var result = await QueryCurrencyAsync([userId], [CloudDataKeys.Gold]);3. 引入命名空间
csharp
using GameCore.UserCloudData;
using static GameCore.UserCloudData.CloudData; // 简化调用4. 运行时前置条件
CloudData 只能在服务端逻辑中使用。客户端 UI、纯客户端调试和本地表现层代码不能直接访问 CloudData;这些代码应通过服务端快照、消息或同步属性拿到结果。
启动阶段不要在 GameStart 后立刻无条件读取云数据。云数据服务和玩家 UserId 都可能晚于游戏脚本初始化完成,首轮存档加载推荐同时满足两个条件:
csharp
#if SERVER
using GameCore.UserCloudData;
public async Task LoadPlayerArchiveWhenReady(Player player)
{
// 等待云数据服务入口可用;已经 ready 时会立即返回。
await CloudData.WaitUntilReadyAsync();
var userId = player.User?.UserId ?? 0;
if (userId <= 0)
{
Game.Logger.LogWarning("玩家 {PlayerId} 尚未获得 UserId,暂不读取云存档", player.Id);
return;
}
var result = await CloudData.QueryUserDataAsync(
userIds: [userId],
keys: ["level", "experience"]
);
if (!result)
{
Game.Logger.LogWarning("读取云存档失败: {Result}", result.Result);
}
}
#endif如果项目用事件驱动,也可以订阅 Game.OnUserCloudDataServiceInitialization,或在启动聚合逻辑中检查 CloudData.IsReady / Game.IsUserCloudDataServiceInitialized。这些 API 只表示云数据服务可用,不代表某个玩家已经连入或 UserId 已经有效。
5. 查询数据
csharp
// 方式1: 使用 UserId(推荐用于存储的用户ID)
var result = await QueryUserDataAsync(
userIds: [userId],
keys: ["level", "experience", "gold"]
);
// 方式2: 使用 Player 对象(便利方法,游戏逻辑中常用)
var playerResult = await QueryPlayersDataAsync(
players: [player],
keys: ["level", "experience", "gold"]
);
if (playerResult.IsSuccess)
{
var userData = playerResult.Data.First();
var level = userData.BigIntData["level"];
var exp = userData.BigIntData["experience"];
var gold = userData.CurrencyData["gold"];
}6. 基础事务操作
csharp
// 方式1: 使用 UserId
var result = await ForUser(userId)
.SetData("level", newLevel)
.AddToData("experience", 1000)
.AddCurrency("gold", 500)
.WithDescription("升级奖励")
.ExecuteAsync();
// 方式2: 使用 Player 对象(便利方法)
var playerResult = await ForPlayer(player)
.SetData("level", newLevel)
.AddToData("experience", 1000)
.AddCurrency("gold", 500)
.WithDescription("升级奖励")
.ExecuteAsync();7. 错误处理
csharp
if (result == UserCloudDataResult.Success)
{
Game.Logger.LogInformation("操作成功!");
}
else
{
Game.Logger.LogError("操作失败: {Result}", result);
}常见限额类结果:
| 结果 | 推荐处理 |
|---|---|
UserCloudDataResult.TooManyReadOperations | 合并查询,降低轮询频率,稍后重试 |
UserCloudDataResult.TooManyWriteOperations | 合并相关事务,避免逐字段或逐奖励提交 |
UserCloudDataResult.MessageSizeTooLarge | 缩小 Blob / ListItem payload,拆分大型存档或批量数据 |
常见使用场景
场景1: 玩家登录
csharp
public async Task<PlayerLoginInfo> HandlePlayerLogin(long userId) // 注意:这是UserId,不是Player.Id
{
// 查询玩家基础信息
var playerData = await QueryUserDataAsync(
userIds: [userId], // userId是User.UserId
keys: ["level", "experience", "last_login", "total_playtime"]
);
// 查询玩家货币
var currencyData = await QueryCurrencyAsync(
userIds: [userId],
keys: ["gold", "diamond", "energy"]
);
// 更新登录时间
var updateResult = await ForUser(userId)
.SetData("last_login", DateTime.UtcNow.ToString())
.AddToData("login_count", 1)
.WithDescription("玩家登录")
.ExecuteAsync();
return new PlayerLoginInfo
{
Level = playerData.Data.First().BigIntData["level"],
Experience = playerData.Data.First().BigIntData["experience"],
Gold = currencyData.Data.First().CurrencyData["gold"],
// ... 其他数据
};
}场景2: 完成任务
csharp
public async Task<bool> CompleteQuest(long userId, QuestInfo quest) // 注意:这是UserId
{
var builder = ForUser(userId); // userId是User.UserId,不是Player.Id
// 添加经验和金币奖励
builder.AddCurrency("experience", quest.ExpReward)
.AddCurrency("gold", quest.GoldReward);
// 添加物品奖励
if (quest.ItemRewards.Any())
{
var itemRefs = builder.PrepareListItems("inventory", quest.ItemRewards);
builder.AddListItems(itemRefs);
}
// 更新任务状态
builder.SetData($"quest_{quest.Id}_completed", true)
.SetData($"quest_{quest.Id}_complete_time", DateTime.UtcNow.ToString());
var result = await builder
.WithDescription($"完成任务: {quest.Name}")
.ExecuteAsync();
return result == UserCloudDataResult.Success;
}场景3: 商店购买
csharp
public async Task<PurchaseResult> PurchaseItem(long userId, int itemId, int cost) // UserId参数
{
// 查询玩家当前金币
var currentGold = await QueryCurrencyAsync(
userIds: [userId], // userId是User.UserId
keys: ["gold"]
);
if (!currentGold.IsSuccess)
return PurchaseResult.NetworkError;
var goldAmount = currentGold.Data.First().CurrencyData["gold"];
if (goldAmount < cost)
return PurchaseResult.InsufficientFunds;
// 执行购买
var builder = ForUser(userId); // 使用UserId
var itemRef = builder.PrepareListItem("inventory", CreateItemData(itemId));
var result = await builder
.CostCurrency("gold", cost) // 扣除金币
.AddListItem(itemRef) // 添加物品
.SetData("last_purchase", DateTime.UtcNow.ToString())
.WithDescription($"购买物品 {itemId}")
.ExecuteAsync();
if (result == UserCloudDataResult.Success)
{
return new PurchaseResult
{
Success = true,
ItemId = itemRef.Id // 返回生成的物品ID
};
}
return PurchaseResult.TransactionFailed;
}场景4: 批量用户操作(每日奖励)
csharp
public async Task DistributeDailyRewards(long[] userIds) // 明确是UserId数组
{
var tasks = userIds.Select(async userId =>
{
return await ForUser(userId) // 使用UserId
.AddCurrency("gold", 100)
.AddCurrency("energy", 20)
.SetData("last_daily_reward", DateTime.UtcNow.ToString())
.WithDescription("每日登录奖励")
.ExecuteAsync();
});
var results = await Task.WhenAll(tasks);
// 统计成功和失败
var successCount = results.Count(r => r == UserCloudDataResult.Success);
Game.Logger.LogInformation("每日奖励发放: {SuccessCount}/{TotalCount} 成功", successCount, results.Length);
}场景5: 体力系统管理
体力系统有多种实现方式:
- 定时重置:每日0点恢复满体力 → 使用CappedData重置机制
- 线性回复:每分钟回复1点体力 → 使用CappedData + LastUpdateTime
- 复杂逻辑:VIP加速、道具加速 → 使用普通数据 + 货币数据
csharp
public async Task<EnergySystemResult> ManagePlayerEnergy(long userId, EnergyAction action, int amount = 0) // UserId参数
{
switch (action)
{
case EnergyAction.Consume:
// 消耗体力:增加已消耗量,每日0点重置为0(体力恢复满值),上限100
var consumeResult = await ForUser(userId) // 使用UserId
.ModifyCappedData("energy_consumed", amount, 100, UserDataResetOption.Daily())
.WithDescription($"消耗体力 +{amount}")
.ExecuteAsync();
return EnergySystemResult.FromCloudResult(consumeResult);
case EnergyAction.UpgradeCapacity:
// 提升体力上限(VIP特权)
var upgradeResult = await ForUser(userId)
.ModifyCappedData("energy_consumed", 0, 100 + amount) // 新上限
.WithDescription($"体力上限提升至 {100 + amount}")
.ExecuteAsync();
return EnergySystemResult.FromCloudResult(upgradeResult);
case EnergyAction.Query:
// 查询当前体力状态
var queryResult = await QueryCappedDataAsync(
userIds: [userId], // 使用UserId
keys: ["energy_consumed"]
);
if (queryResult.IsSuccess && queryResult.Data.Any())
{
var energyData = queryResult.Data.First();
var energyConsumed = energyData.CappedData["energy_consumed"];
var remainingEnergy = energyConsumed.Cap - energyConsumed.Value; // 剩余体力
return new EnergySystemResult
{
Success = true,
CurrentEnergy = remainingEnergy, // 剩余体力
MaxEnergy = energyConsumed.Cap,
NextResetTime = energyConsumed.NextResetTime
};
}
return EnergySystemResult.QueryFailed;
}
}
public enum EnergyAction
{
Query, // 查询当前状态
Restore, // 恢复体力
Consume, // 消耗体力
UpgradeCapacity // 提升上限
}场景6: 在游戏逻辑中正确使用Player对象
csharp
// 推荐:在游戏事件处理中使用Player对象
public class GameEventHandler
{
// 玩家升级时保存数据
public async Task OnPlayerLevelUp(Player player, int newLevel)
{
// 使用Player便利方法,自动提取User.UserId
var result = await ForPlayer(player)
.SetData("level", newLevel)
.AddCurrency("skill_points", 1)
.WithDescription($"升级到{newLevel}级")
.ExecuteAsync();
}
// 玩家获得物品
public async Task OnPlayerGetItem(Player player, ItemData item)
{
var builder = ForPlayer(player); // 推荐:直接使用Player
var itemRef = builder.PrepareListItem("inventory", item);
await builder
.AddListItem(itemRef)
.SetData("last_item_time", DateTime.UtcNow.ToString())
.ExecuteAsync();
}
// 从外部系统传入UserId的情况
public async Task LoadUserProfile(long userId) // 明确标注这是UserId
{
var userData = await QueryUserDataAsync(
userIds: [userId], // 这里确实需要UserId
keys: ["level", "experience", "last_login"]
);
// 处理数据...
}
}
// 批量玩家操作的正确方式
public async Task DistributeEventRewards(Player[] activePlayers)
{
// 使用Player数组,自动过滤AI玩家
var result = await ForPlayers(activePlayers)
.ForAllUsers(builder => builder
.AddCurrency("event_token", 10)
.SetData("last_event_reward", DateTime.UtcNow.ToString())
)
.ExecuteAllAsync();
}高级特性
1. 事务优化
csharp
// 系统会自动合并相同键的操作
var result = await ForUser(userId) // 使用UserId
.AddCurrency("gold", 100)
.AddCurrency("gold", 50) // 自动合并为 +150
.AddCurrency("gold", -20) // 最终结果: +130
.SetData("level", 10)
.SetData("level", 11) // 自动优化为最后一个值
.WithOptimization(true) // 默认启用
.ExecuteAsync();2. 跨用户单次提交
TransactionBuilder.ForUser(...) 适合一个用户内的原子事务;ForUsers(...).ExecuteAllAsync() 会为每个用户分别提交,适合允许部分成功的批量发奖。少数情况下,例如两个玩家之间的交易,需要在一次提交中同时修改两个用户的数据,任一操作失败都回滚整笔交易。
ForUsers(...) 只声明这个批次包含哪些用户,不会自动给这些用户添加操作。使用 ForAllUsers(...) 可为所有已声明用户添加相同操作;使用 ForUser(userId, ...) 可为不同用户分别添加不同操作。传给 ForUser(...) 的 userId 必须已经包含在最初的 ForUsers(...) 中,否则会抛出 ArgumentException,避免因漏写用户而静默丢失操作。
优先使用 ForUsers(...).ForUser(...).ExecuteSingleCommitAsync(...),它仍然保留链式 API,但只发送一次 commit:
csharp
public async Task<UserCloudDataResult> CommitTradeAsync(
long buyerUserId,
long sellerUserId,
long price,
string tradeId)
{
return await CloudData.ForUsers(buyerUserId, sellerUserId)
.ForUser(buyerUserId, builder => builder
.CostCurrency("gold", price)
.SetData("last_trade_id", tradeId)
)
.ForUser(sellerUserId, builder => builder
.AddCurrency("gold", price)
.SetData("last_trade_id", tradeId)
)
.ExecuteSingleCommitAsync($"玩家交易 {tradeId}");
}如果链式封装不适合当前场景,也可以直接构造 TransactionOperation 列表并调用 CloudDataApi.ExecuteTransactionAsync(...)。每个 operation 都显式携带自己的 userId,整个列表作为一次事务提交:
csharp
using EngineInterface.BaseType;
using GameCore.UserCloudData;
public async Task<UserCloudDataResult> CommitTradeManuallyAsync(
long buyerUserId,
long sellerUserId,
long price,
string tradeId)
{
var operations = new List<TransactionOperation>
{
TransactionOperation.CostCurrency(buyerUserId, "gold", price),
TransactionOperation.AddCurrency(sellerUserId, "gold", price),
TransactionOperation.SetVarChar255(buyerUserId, "last_trade_id", tradeId),
TransactionOperation.SetVarChar255(sellerUserId, "last_trade_id", tradeId),
};
return await CloudDataApi.ExecuteTransactionAsync(operations, $"玩家交易 {tradeId}");
}手动构造 operation list 时,如果需要新增 ListItem,也先通过 builder 生成 ListItemReference,再转成 operation。这样可以复用框架的列表项 UUID 生成逻辑:
csharp
var builder = CloudData.ForUser(userId);
var itemRef = builder.PrepareListItem("inventory", itemBytes);
var operations = new List<TransactionOperation>
{
itemRef.ToListItemAddOperation(),
TransactionOperation.SetBigInt(userId, "last_item_id", itemRef.Id),
};
var result = await CloudDataApi.ExecuteTransactionAsync(operations, "发放物品");这类接口返回的是整笔提交的 UserCloudDataResult,不是逐用户结果。比如买家金币不足时,卖家的加金币操作也不会落库。
3. 列表项高级操作
csharp
var builder = ForUser(userId);
// 批量准备物品
var lootItems = GenerateLootRewards();
var itemRefs = builder.PrepareListItems("inventory", lootItems);
// 在事务中使用新物品的ID
var firstItemId = itemRefs[0].Id; // 在ExecuteAsync之前就能获取ID
var result = await builder
.AddListItems(itemRefs)
.SetData("last_loot_item_id", firstItemId) // 记录最新物品ID
.ExecuteAsync();ListItem 也支持和普通 blob 类似的 UTF-8 / Double 便利写法。适合少量需要直接保存长文本日志、小数值样本的列表项;普通背包物品仍建议继续使用你自己的 byte[] / 序列化格式。
csharp
var builder = ForUser(userId);
var logRef = builder.PrepareUtf8ListItem("trade_logs", tradeJson);
var valueRef = builder.PrepareDoubleListItem("score_samples", 42.25);
var result = await builder
.AddListItem(logRef)
.AddListItem(valueRef)
.ExecuteAsync();
var logs = await QueryUserListItemsAsync(userId, "trade_logs");
if (logs)
{
foreach (var item in logs.Data)
{
if (item.TryGetUtf8Value(out var json))
{
Game.Logger.LogInformation("交易日志: {Json}", json);
}
}
}4. 错误处理最佳实践
csharp
public async Task<GameResult> SafeExecuteTransaction(
long userId,
Func<TransactionBuilder, TransactionBuilder> buildTransaction)
{
try
{
var builder = ForUser(userId);
builder = buildTransaction(builder);
var result = await builder.ExecuteAsync();
if (result == UserCloudDataResult.Success)
{
return GameResult.Success;
}
// 根据错误类型返回不同结果
return result switch
{
UserCloudDataResult.InsufficientCurrency => GameResult.InsufficientResources,
UserCloudDataResult.FailedToSend => GameResult.NetworkError,
UserCloudDataResult.TransactionCommitEmpty => GameResult.InvalidOperation,
_ => GameResult.UnknownError
};
}
catch (ArgumentException)
{
return GameResult.InvalidArguments;
}
catch (InvalidOperationException)
{
return GameResult.InvalidOperation;
}
}
// 使用示例
var result = await SafeExecuteTransaction(userId, builder =>
builder.AddCurrency("gold", 100)
.SetData("level", newLevel)
);性能最佳实践
1. 批量查询优化
csharp
// 推荐:一次查询多个用户
var usersData = await QueryUserDataAsync(
userIds: allUserIds,
keys: ["level", "experience"]
);
// 避免:循环单个查询
foreach (var userId in allUserIds) // 注意:这里是UserId
{
var userData = await QueryUserDataAsync([userId], keys);
}2. 事务合并
csharp
// 推荐:一个事务完成所有相关操作
var result = await ForUser(userId) // 使用UserId
.CostCurrency("energy", 10) // 消耗
.AddCurrency("experience", 100) // 奖励
.SetData("last_action", DateTime.UtcNow.ToString())
.WithDescription("战斗结算")
.ExecuteAsync();
// 避免:拆分成多个事务
await ForUser(userId).CostCurrency("energy", 10).ExecuteAsync();
await ForUser(userId).AddCurrency("experience", 100).ExecuteAsync();3. 查询范围限制
csharp
// 大列表使用maxCount限制
var recentItems = await QueryUserListItemsAsync(
userId: userId,
key: "inventory",
maxCount: 50 // 只获取最新50个
);
// 小列表可以不限制
var friends = await QueryUserListItemsAsync(
userId: userId,
key: "friends" // 好友列表通常较小
);如果需要把大列表分次读完,使用 ListItem 游标扫描,而不是把 maxCount 调到很大:
csharp
long? cursor = null;
do
{
var batch = await ScanUserListItemsAsync(
userId: userId,
key: "inventory",
batchSize: 100,
beforeItemUuid: cursor
);
if (!batch)
{
Game.Logger.LogWarning("扫描列表项失败: {Result}", batch.Result);
break;
}
foreach (var item in batch.Data.Items)
{
ProcessInventoryItem(item);
}
cursor = batch.Data.NextBeforeItemUuid;
}
while (cursor is not null);ScanUserListItemsAsync 按 ItemUUID 倒序向更旧记录推进,正常情况下不会重复返回已经扫描过的列表项。它不是强一致快照;扫描过程中新增的记录通常留到下一轮完整扫描,删除或移动列表项会影响后续批次。
调试和故障排除
1. 启用详细日志
csharp
var result = await ForUser(userId)
.AddCurrency("gold", amount)
.WithDescription($"操作详情 - 玩家:{userId}, 数量:{amount}, 时间:{DateTime.UtcNow}")
.ExecuteAsync();2. 事务内容检查
csharp
var builder = ForUser(userId)
.SetData("level", 10)
.AddCurrency("gold", 100);
// 调试:检查将要执行的操作
var operations = builder.Build();
foreach (var op in operations)
{
Game.Logger.LogDebug("操作: {Type}, 键: {Key}, 值: {Value}", op.Type, op.Key, op.Value);
}
var result = await ExecuteTransactionAsync(operations, "调试事务");3. 超时处理
csharp
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
var result = await ForUser(userId)
.SetData("data", value)
.ExecuteAsync()
.WaitAsync(cts.Token);
}
catch (OperationCanceledException)
{
Game.Logger.LogWarning("操作超时,请检查网络连接");
}常见错误和解决方案
| 错误 | 原因 | 解决方案 |
|---|---|---|
ArgumentException: UserId must be positive | 用户ID无效 | 确保用户ID > 0 |
ArgumentException: Key cannot be empty | 键名为空 | 检查键名是否有效 |
UserCloudDataResult.InsufficientCurrency | 货币不足 | 先查询当前货币再执行扣除 |
UserCloudDataResult.CapExceeded | 上限数据超出设定上限 | 检查ModifyCappedData操作的增量值 |
UserCloudDataResult.TransactionCommitEmpty | 空事务 | 确保事务包含有效操作 |
UserCloudDataResult.ServiceNotInitialized | 云数据服务尚未初始化 | 启动阶段先 await CloudData.WaitUntilReadyAsync(),不要用短间隔循环撞时机 |
UserCloudDataResult.FailedToSend | 网络问题 | 检查网络连接,考虑重试 |
UserCloudDataResult.TooManyReadOperations | 查询过于频繁 | 合并查询并降低请求频率 |
UserCloudDataResult.TooManyWriteOperations | 提交过于频繁 | 合并事务,稍后重试 |
UserCloudDataResult.MessageSizeTooLarge | 请求或响应数据过大 | 缩小或拆分数据;大 ListItem 列表用游标分批扫描 |
下一步
- 阅读 CloudData系统完整文档
- 了解 异步编程最佳实践
- 查看 框架测试指南 学习如何测试CloudData操作