Unity 首个游戏接入
要求 Unity 6000.3+、SDK 及 Newtonsoft JSON。Package Manager 选择 Ape Motion Backend SDK,导入 First Game。FirstGameExample.cs 是可复用的协议示例,不包含 Apple/Google 原生登录插件,不内置测试账号或绕过认证。真机平台适配器未提供时不能验收真实登录。
后台参数
从后台选定 development 游戏与 App,记录 gameSlug、gameId、gameAppId、appIdentifier。apiBaseUrl 由发布负责人提供,必须是玩家 API 的 HTTPS origin,不带 /v1。installationId 是安全持久化的随机 UUID,不是每次启动重建。
需要游戏提供的三个适配器
| 接口 | 实现要求 | ||
|---|---|---|---|
| ISecureStorage | Keychain/Keystore-backed 安全存储;SetAsync 必须原子持久化;不存在返回 null;不能用 PlayerPrefs 或明文文件 | ||
| IDeviceProofProvider | 固定 Platform=ios/android;P-256 私钥不离开安全存储;PublicJwkJson 只含公共 EC/P-256 坐标及 alg/ext;SignAsync 签 UTF-8 文本,返回 raw r | s 64 字节的 base64url(非 DER) | |
| IPlatformIdentityAdapter | Provider=game_center/play_games;CreateLoginPayloadAsync 返回下述平台字段 JSON,不负责设备挑战;重新获取原始平台证明,不合成签名或伪造玩家 ID |
iOS 返回 publicKeyUrl、signature、salt、timestamp、teamPlayerId,可选 gamePlayerId。使用 Game Center 的原始签名材料;不要替换成 Apple OIDC token。Android 返回 serverAuthCode,可选 recallSessionId;必须向后端登记的 server/Web OAuth client 请求授权码。非本例流程的 CreateFreshReauthenticationPayloadAsync 可以抛 NotSupportedException,但不能将它当成已实现。
版本比较由游戏提供 supportsMinimumVersion 回调,使用你们的正式版本规则,不能固定返回 true。初始化拒绝维护状态和过低版本,并核对 bootstrap/session gameId。服务对象从游戏 composition root 注入;所有公开方法在 Unity 主线程串行调用,UI 操作期间禁用重复按钮。
主流程
以下 publicParameters、platformIdentity、deviceProof、secureStorage 和 IsSupportedClientVersion 是游戏的真实配置与适配器实例,不是 SDK 自带变量。
var demo = new FirstGameExample(
publicParameters.ApiBaseUrl, publicParameters.GameSlug,
publicParameters.GameId, publicParameters.GameAppId,
publicParameters.AppIdentifier, persistentInstallationId,
secureStorage, deviceProof, platformIdentity, IsSupportedClientVersion);
if (!await demo.InitializeAsync(ct)) await demo.LoginAsync(ct);
var save = await demo.ReadSaveAsync(ct);
var revision = save == null ? 0 : (long)save["revision"];
var state = save == null ? new JObject { ["level"] = 1 } : FirstGameExample.DecodeDocument(save);
state["level"] = 2;
revision = await demo.SaveAsync(revision, state, ct);
await demo.LogoutAsync(ct);
await demo.LoginAsync(ct); // 同一个平台测试账号
var restored = await demo.ReadSaveAsync(ct);
// 核对账号未改变,DecodeDocument(restored)["level"] == 2。
示例使用 main 槽;document.payload 保存游戏 JSON 的 UTF-8 base64,避免跨语言数字与转义规则改变校验和。读取非本示例存档时必须先核对格式,不能直接 DecodeDocument。更大的存档、升级 schema 和格式迁移由游戏另行实现。
异常与恢复
try { await demo.SaveAsync(revision, localState, ct); }
catch (ApiException error) when (error.Problem.Code == "REVISION_CONFLICT")
{
var remote = await demo.ReadSaveAsync(ct);
var pending = await demo.ReadPendingSaveAsync(ct); // 保留本地待同步内容
// 向玩家展示本地/云端差异;由游戏合并或用户选择。
// 确认后:DiscardPendingSaveAfterResolutionAsync,
// 再用 remote.revision 和新选定文档调用 SaveAsync。
}
catch (ApiTransportException)
{
// 网络恢复后 RetryPendingSaveAsync;保留原键、文档和 baseRevision。
}
catch (ApiException error) when (error.Problem.Status == 401)
{
// 停止同步,提示重新登录。重新运行 InitializeAsync 验证并清理无效 session。
}
不要把存档原文、pending 内容、token 或平台证明写进 Debug.Log。错误日志只记录操作名、status、code、requestId。5xx、429、IDEMPOTENCY_IN_PROGRESS 使用有限退避+抖动;普通错误由用户修正,不能自动无限重试。
SaveAsync 会在发送前安全保存待提交操作。若仍有 pending,必须先 RetryPendingSaveAsync 或明确解决冲突,不能开始另一写入。pending 按 gameAccountId 隔离;重新登录换号后不能重放旧账号写入。服务器成功但本地落盘失败同样保留 pending,重复调用可幂等恢复。
LoginAsync 在响应未知时保存原平台证明、authRequestId 和幂等键,只重新申请设备挑战。EXTERNAL_IDENTITY_PROOF_REISSUE_REQUIRED 清除旧尝试,下次调用向平台取新证明。其他确定的登录配置错误需由开发者纠正并明确结束旧尝试,不自动循环。
SDK 自动在 accessToken 接近过期时刷新,或在认证请求 401 后刷新一次;刷新尝试在安全存储中持久化,响应丢失不会更换 refreshRequestId。并发旧 token 的 401 共享已经刷新的 session。登录、公开配置和设备挑战请求不会被旧 session 的自动刷新阻断。
退出请求超时会抛异常,不能显示「服务端已退出」。此示例不包含账号删除、购买、运动授权或多设备冲突自动合并。
验证边界
本地静态 SDK 检查不是 Unity 编译、真机 Keychain/Keystore 或 Game Center/Play Games 验收。发布前在本地 Unity 导入样例、编译并执行真机测试,验证登录→保存→退出→重新登录读档、token 过期、撤销、冲突、断网与应用重启。不要把 Unity 验证移入 hosted CI。