【OpenHarmony/HarmonyOS】从本地排行到 AGC 云数据:玩家、匹配、房间与战绩模型设计
【OpenHarmony/HarmonyOS】从本地排行到 AGC 云数据:玩家、匹配、房间与战绩模型设计
当前项目的正式数据链路以 Preferences 本地存储为主,同时已经准备了 PlayerStats、MatchRequest、GameRoom 和 BattleRecord 模型。本文给出一条从离线优先走向云同步与匹配服务的演进方案。☁️
⚠️ 说明:本文是基于工程预留模型的架构设计,不代表项目当前已经完成 AGC 云数据库和云排行榜接入。
一、为什么先做本地优先?
游戏的基本体验不应被网络阻断。本地优先具有明显优势:
- 首次进入不等待远端请求;
- 无网也能玩 PvE、解谜和限时模式;
- 结算结果立即可见;
- 云服务故障不会导致当局数据全部丢失;
- 在账号和服务端未完成前,也能独立迭代核心玩法。
但本地存储无法提供跨设备同步、全服排行榜、在线匹配和可信战绩,因此需要在稳定本地模型之上逐步加云层。
二、四个核心云对象
1. PlayerStats
class PlayerStats {
uid: string;
level: number;
exp: number;
winRate: number;
bestScore: number;
updatedAt: number;
userName: string;
avatar: string;
}
它代表玩家聚合数据。uid 应来自正式认证服务,不应直接使用本地时间戳 ID。
2. MatchRequest
class MatchRequest {
requestId: string;
uid: string;
status: string;
roomId: string;
createdAt: number;
}
玩家点击匹配时创建。状态可定义为 matching、matched、cancelled、expired,并使用服务端时间判断超时。
3. GameRoom
class GameRoom {
roomId: string;
playerA: string;
playerB: string;
state: string;
lastFrameData: string;
}
房间保存参与者与当前状态。把完整实时帧持续写云数据库并不适合高频动作游戏,lastFrameData 更适合作为断线恢复摘要、准备状态或低频快照。
4. BattleRecord
class BattleRecord {
recordId: string;
winnerId: string;
loserId: string;
duration: string;
timestamp: string;
}
战绩用于历史、排行审计和统计。工程当前字段把时长与时间戳定义为字符串,后续建议改为明确数值/日期类型,避免排序和区间查询困难。
三、本地和云端不应互相直接覆盖
推荐在本地数据外加同步元数据:
interface SyncEnvelope<T> {
schemaVersion: number;
revision: number;
updatedAt: number;
dirty: boolean;
deviceId: string;
data: T;
}
本地每次结算先写入 Preferences,并标记 dirty。网络可用且已登录时,后台同步队列再提交云端;成功后清除 dirty 标记。
flowchart TD
A[游戏结算] --> B[事务性写本地]
B --> C[生成 Sync Job]
C --> D{登录且联网}
D -- 否 --> E[保留队列]
D -- 是 --> F[提交云端]
F --> G{成功}
G -- 是 --> H[清除 dirty]
G -- 否 --> I[指数退避重试]
四、冲突解决不能只比较分数
不同字段需要不同策略:
bestScore:取最大值;totalGames、wins:不能简单取最大,最好上传不可重复事件或增量;- 昵称、头像:最后修改优先或让用户选择;
- 晶石余额:必须有服务端账本,不能客户端随意覆盖;
- 升级等级:服务端校验购买交易后更新;
- 设置项:通常设备本地,不一定需要上云。
一个统一“云端新就全覆盖本地”的策略会丢失离线期间的增量。
五、排行榜数据应该由谁可信? 🏆
本地 ScoreManager 接收客户端给出的任意分数。用于个人历史没有问题,用于全服排行榜就容易被修改。
云排行榜至少需要:
- 已认证 UID;
- 服务端或权威主机确认的比赛结果;
- 合理的分数范围和时长校验;
- 幂等的
recordId,防止重复提交; - 可追踪的模式、难度、客户端版本;
- 异常分数审查或风控。
提交接口不应只有 uid + score,而应围绕一场不可重复的比赛记录设计。
六、匹配不能由两个客户端抢同一条记录
最简单的客户端匹配逻辑是查询另一个 matching 请求,然后双方更新为 matched。如果三个客户端同时读取,同一个玩家可能被重复匹配。
更可靠的方式是由云函数或服务端事务完成:
- 按
createdAt获取等待队列; - 在事务/锁中选择两个仍为 matching 的请求;
- 创建唯一
roomId; - 原子更新两条请求;
- 客户端监听自己的请求变化;
- 超时或取消时使用条件更新。
客户端只提交和订阅,不参与权威配对。
七、云数据库不适合直接做 120Hz 帧同步
实时动作游戏每秒可能产生几十到上百次输入/状态。把每帧 JSON 写数据库会带来:
- 写入延迟与抖动;
- 成本和限流;
- 数据库监听不是实时游戏协议;
- 状态覆盖、乱序和冲突难以处理;
- 大量无意义历史帧。
更合理的分工:
云数据库:账号、匹配、房间元数据、战绩、排行榜
云函数:匹配、结算校验、异步任务
SoftBus/UDP/专用实时通道:当局输入和快照
本地 Preferences:离线进度、缓存、待同步队列
八、模型字段需要加强
建议为所有云对象增加:
interface CloudMeta {
schemaVersion: number;
createdAt: number;
updatedAt: number;
revision: number;
}
房间还应包含:
hostUid、玩家列表与队伍;- 地图 seed 与规则版本;
- 模式、难度、目标比分;
state枚举而非任意字符串;- 最后心跳和过期时间;
- 结算 ID 与签名。
地图 seed 很重要:双方用相同算法和 seed 生成地图,避免传输完整二维数组,但必须同时锁定生成器版本。
九、认证与本地游客迁移 🔐
玩家可能先以本地游客玩了很多局,之后才登录账号。此时要决定:
- 把本地进度合并到新账号;
- 保留云端已有进度;
- 对晶石等敏感经济只允许有限迁移;
- 一个本地档案是否只能绑定一次;
- 退出账号后保留哪些缓存。
推荐把本地 userId 视为设备档案 ID,登录后另存认证 UID,并记录已完成迁移的标志,保证过程幂等。
十、同步队列设计
interface SyncJob {
jobId: string;
type: 'SAVE_STATS' | 'SUBMIT_RECORD' | 'UPDATE_PROFILE';
payload: string;
attempts: number;
nextRetryAt: number;
createdAt: number;
}
失败后采用指数退避,网络恢复时唤醒;达到最大次数后保留并记录可诊断错误。不要在游戏结束弹窗中阻塞等待上传。
十一、迁移路径建议 🚀
阶段 1:整理本地模型
修复字段类型,加入 schemaVersion,统一 Manager 接口和写入串行化。
阶段 2:只读云排行
保留本地个人记录,登录用户可以查看云榜,失败回退空态。
阶段 3:异步提交战绩
本地先成功,后台队列提交,服务端做幂等和范围校验。
阶段 4:用户资料同步
处理游客绑定、冲突和多设备。
阶段 5:云端匹配
云函数负责原子配对,房间只保存元数据,实时战斗走独立通道。
十二、测试重点 ✅
- 断网结算后重启应用,任务是否仍在;
- 同一 recordId 重试是否只记一次;
- 两台设备同时修改资料如何解决;
- 匹配取消与成功同时发生时的最终状态;
- 房间过期是否清理;
- 云数据 schema 升级;
- 服务端时间与客户端时间不一致;
- 登录过期、权限拒绝和账号切换;
- 排行榜异常分数拦截;
- 网络恢复时是否集中重试造成请求风暴。
十三、总结 ✨
从本地游戏演进到云端,不是把 Preferences.put() 换成一个远程 API。需要重新定义:
- 哪些数据本地优先,哪些必须服务端权威;
- 每个字段如何合并;
- 提交如何幂等;
- 匹配如何原子化;
- 实时战斗与数据库如何分工;
- 游客和正式账号如何迁移;
- 失败任务如何持久重试。
先做稳健的离线层,再逐步叠加云能力,通常比一开始让每个页面直接访问云数据库更可靠。☁️
推荐标签: HarmonyOS OpenHarmony AGC 云数据库 离线优先 游戏匹配

更多推荐



所有评论(0)