API更新解析:存档、排行榜、昵称相关-新项目无需关注
我们已于2月10日更新了最新的存档、排行榜、昵称相关API,新建的项目均会更新到新版本文档,但是对于旧项目,则可以参考文档内容
(所谓参考,其实就是复制给嗒啦啦)


概念
clientScore
是全局对象,无需 require。提供客户端直接读写云端变量的能力。
数据类型: values (任意类型) 和 iscores (整数)
无需服务端: 客户端直接调用,异步回调返回结果
核心用途: 存档、积分、排行榜
仅限客户端: Standalone / Client 模式可用,Server 模式暂不支持
属性
lua
clientScore.userId -- number: 当前用户 ID
clientScore.mapName -- string: 当前地图名称
数据类型说明
| 存储表 | 写入方法 | 读取位置 | 用途 |
|--------|----------|----------|------|
| values | Set()
/ BatchSet():Set()
| 回调第 1 参数 values.key
| 任意类型(字符串、表、数值等) |
| iscores | SetInt()
/ Add()
/ BatchSet():SetInt()
/ BatchSet():Add()
| 回调第 2 参数 iscores.key
| 整数,可参与排行榜排序 |
~~sscores~~ 已废弃,不再使用。所有非整数数据统一使用 values。
API 速查
1. 单值写入
```lua
-- 写入任意类型 → values (支持 string/table/number/boolean)
clientScore:Set("game_config", { difficulty = "hard", music = true }, {
ok = function() print("保存成功") end,
error = function(code, reason) print("失败:", reason) end,
})
-- 写入复杂嵌套结构 → values (引擎自动 JSON 序列化/反序列化)
clientScore:Set("inventory", {
{ id = "sword01", level = 5, enchant = "fire" },
{ id = "plate02", level = 3 },
{ id = "plate02", level = 3 },
}, { ok = function() end })
-- 写入整数 → iscores(可排行榜)
clientScore:SetInt("high_score", 9999, {
ok = function() end,
})
-- 整数增量 → iscores(delta 可为负数)
clientScore:Add("gold", 100, { ok = function() end })
clientScore:Add("gold", -50, { ok = function() end })
```
2. 单值读取
lua
clientScore:Get("gold", {
ok = function(values, iscores)
local gold = iscores.gold or 0 -- 整数从 iscores 读
local config = values.game_config -- table 从 values 读 (已自动反序列化)
end,
error = function(code, reason) end,
})
3. 批量读写
```lua
-- 批量读取 (推荐: 比多次 Get 更高效)
clientScore:BatchGet()
:Key("gold"):Key("exp"):Key("level")
:Key("inventory")
:Fetch({
ok = function(values, iscores)
local gold = iscores.gold or 0
local inv = values.inventory or {} -- table 自动反序列化
end,
error = function(code, reason) end,
})
-- 批量写入
clientScore:BatchSet()
:Set("inventory", { "铁剑", "木盾" }) -- 任意类型 → values
:SetInt("gold", 100) -- 整数 → iscores
:Add("playcount", 1) -- 增量 → iscores
:Delete("tempdata") -- 删除云变量
:Delete("tempdata") -- 删除云变量
:Save("操作描述", {
ok = function() end,
error = function(code, reason) end,
})
```
BatchSet 方法:
| 方法 | 目标表 | 说明 |
|------|--------|------|
| :Set(key, value)
| values | 设置任意类型 |
| :SetInt(key, value)
| iscores | 设置整数(可排行榜) |
| :Add(key, delta)
| iscores | 增量(delta 可为负数) |
| :Delete(key)
| 两者 | 删除云变量 |
| :Save(desc, events)
| - | 提交保存 |
4. 排行榜
```lua
-- 获取排行榜(前 10 名,按 iscores.key 值降序)
-- events 之后可传附加 key 名,同时获取额外字段
clientScore:GetRankList("gold", 0, 10, {
ok = function(rankList)
for i, item in ipairs(rankList) do
local uid = item.userId -- 用户 ID
local gold = item.iscore.gold or 0 -- iscores 数据
local playCount = item.iscore.playcount or 0 -- 附加字段数据
local isMe = uid == clientScore.userId
end
end,
}, "playcount") -- ← 附加字段: 同时获取 play_count
local isMe = uid == clientScore.userId
end
end,
}, "playcount") -- ← 附加字段: 同时获取 play_count
-- 获取自己的排名(未上榜返回 nil)
clientScore:GetUserRank(clientScore.userId, "gold", {
ok = function(rank, scoreValue)
if rank then print("排名: #" .. rank) end
end,
})
-- 获取排行榜总人数
clientScore:GetRankTotal("gold", {
ok = function(total) print("总人数: " .. total) end,
})
```
5. 获取用户昵称
```lua
-- ⚠️ 昵称由 TapTap 账号系统管理,不存储在云变量中
-- 不要用 clientScore:Set("nickname", ...) 存昵称!
-- 获取当前用户 ID
---@diagnostic disable-next-line: undefined-global
local myUserId = lobby:GetMyUserId()
-- 全局函数,客户端/服务端通用(客户端异步回调,服务端同步返回)
GetUserNickname({
userIds = { myUserId },
onSuccess = function(nicknames)
for _, info in ipairs(nicknames) do
print(info.userId, info.nickname)
end
end,
onError = function(errorCode)
print("查询失败, errorCode=" .. tostring(errorCode))
end,
})
```
6. 排行榜 + 昵称 (完整示例)
```lua
function ShowLeaderboard(topN, callback)
clientScore:GetRankList("highscore", 0, topN or 10, {
ok = function(rankList)
local leaderboard = {}
local userIds = {}
for i, item in ipairs(rankList) do
table.insert(leaderboard, {
rank = i,
userId = item.userId,
score = item.iscore.highscore or 0,
ok = function(rankList)
local leaderboard = {}
local userIds = {}
for i, item in ipairs(rankList) do
table.insert(leaderboard, {
rank = i,
userId = item.userId,
score = item.iscore.highscore or 0,
playCount = item.iscore.play_count or 0,
isMe = item.userId == clientScore.userId,
})
table.insert(userIds, item.userId)
end
if #userIds == 0 then
if callback then callback(leaderboard) end
return
end
-- 昵称通过 GetUserNickname() 查询
GetUserNickname({
userIds = userIds,
onSuccess = function(nicknames)
local map = {}
for _, info in ipairs(nicknames) do
map[info.userId] = info.nickname or ""
end
for _, entry in ipairs(leaderboard) do
entry.nickname = map[entry.userId] or "未知"
end
if callback then callback(leaderboard) end
end,
onError = function(errorCode)
if callback then callback(leaderboard) end
end,
})
end,
}, "play_count") -- ← 附加字段
end
```
回调格式
所有异步操作使用统一回调表:
lua
{
ok = function(...) -- 成功(参数因方法不同)
error = function(code, reason) -- 失败(code: integer, reason: string)
timeout = function() -- 超时(可选)
}
各方法回调参数
| 方法 | ok 回调参数 |
|------|-----------|
| Get(key)
| (values, iscores)
|
| Set(key, value)
| (无参数) |
| SetInt(key, value)
| (无参数) |
| Add(key, delta)
| (无参数) |
| BatchGet():Fetch()
| (values, iscores)
|
| BatchSet():Save()
| (无参数) |
| GetRankList()
| (rankList)
— 每个 item: { userId, player, iscore, score }
|
| GetUserRank()
| (rank, scoreValue)
— rank 为 nil 表示未上榜 |
| GetRankTotal()
| (total)
|
排行榜 item 字段详解
| 字段 | 类型 | 说明 |
|------|------|------|
| item.player
| number | 用户 ID |
| item.userId
| number | 用户 ID(与 item.player
等价) |
| item.iscore
| table | { key = intValue, ... }
— 整数云变量 (SetInt/Add 写入) |
| item.score
| table | { key = value, ... }
— 任意类型云变量 (Set 写入)。table 自动 JSON 编解码 |
| item.sscore
| table | 已废弃,通常为空 {}
,不要使用 |
陷阱
1. userId/player 字段是 number
lua
-- 排行榜 item.userId 是 number,拼接字符串时必须 tostring()
local name = "玩家" .. tostring(item.userId)
2. 读取不存在的 key 返回 nil
lua
-- 始终提供默认值
local gold = iscores.gold or 0
local inv = values.inventory or {}
3. Add 是增量,不是赋值
lua
:Add("gold", 10) -- 云端 gold += 10
:SetInt("gold", 100) -- 云端 gold = 100(赋绝对值用 SetInt)
4. 异步操作不阻塞
```lua
-- 错误: 回调还没执行,myGold 是 nil
clientScore:Get("gold", { ok = function(v, i) myGold = i.gold end })
print(myGold) -- nil!
-- 正确: 在回调中处理后续逻辑
clientScore:Get("gold", {
ok = function(values, iscores)
myGold = iscores.gold or 0
onGoldLoaded(myGold)
end,
})
```
5. Delete 后读取返回 nil,不是 0
Delete 某个 key 后再 Get 回来是 nil,本地状态需要手动归零。
6. 先读后写无事务保证
更新最高分等 Get → Set 场景,多端同时操作可能互相覆盖。竞争场景建议用 Add
增量操作。
7. Set() 支持任意类型,table 自动序列化
```lua
-- 直接存 table,引擎自动 JSON 序列化/反序列化
clientScore:Set("inventory", { "铁剑", "木盾", "药水" })
-- 读回来直接是 table,无需手动 cjson.decode
clientScore:Get("inventory", {
ok = function(values, iscores)
local inv = values.inventory -- 直接是 table
for _, item in ipairs(inv) do print(item) end
end,
})
```
8. 不要使用 sscores (已废弃)
```lua
-- 错误: sscores 参数不存在
ok = function(scores, iscores, sscores) -- 第三个参数不存在
local nick = sscores.nickname -- 报错!
end
-- 正确: 只有两个参数
ok = function(values, iscores)
local config = values.game_config or {}
end
```
9. 昵称不要存云变量
```lua
-- ❌ 错误: 不要用云变量存昵称
clientScore:Set("player_name", "张三")
-- ✅ 正确: 用 GetUserNickname() 查询
GetUserNickname({
userIds = { clientScore.userId },
onSuccess = function(nicknames)
local nick = nicknames[1].nickname
end,
})
```
10. cjson 是全局对象,不能 require
```lua
-- 错误: require 会报 Module not found
local cjson = require("cjson")
-- 正确: cjson 是全局对象,直接使用 (但 Set() 已自动处理序列化,大多数场景不需要手动用 cjson)
local encoded = cjson.encode({ key = "value" })
local decoded = cjson.decode('{"key":"value"}')
-- pcall 包裹 decode 防止异常
local ok, result = pcall(cjson.decode, jsonStr)
```
11. 排行榜 item 结构
```lua
-- ❌ item.sscore 已废弃
local nick = item.sscore.nickname -- 不要使用! sscore 通常为空 {}
-- ✅ item.player 和 item.userId 均可用(等价)
local uid = item.player
local uid = item.userId
-- ✅ 读取数据
local gold = item.iscore.gold or 0 -- iscores
local config = (item.score and item.score.game_config) -- values
```
12. GetRankList 附加字段
lua
-- events 参数之后可传入额外 key 名,减少额外请求
clientScore:GetRankList("gold", 0, 10, {
ok = function(rankList)
for _, item in ipairs(rankList) do
-- 附加字段在 item.iscore 中
local playCount = item.iscore.play_count or 0
end
end,
}, "play_count", "level") -- ← 可传多个附加字段
存档结构管理
存档结构定义文件
首次为项目实现存档功能时,必须创建 docs/save-schema.md
,记录所有云变量字段。后续每次变更存档结构时同步更新此文件。
,记录所有云变量字段。后续每次变更存档结构时同步更新此文件。
变更检查清单
当存档字段新增、删除或改名时,必须逐项检查:
[ ] docs/save-schema.md
— 更新字段定义表
[ ] BatchGet
的 :Key()
链 — 是否加了新 key / 去掉了旧 key
[ ] 读取回调 — 是否从 values/iscores 正确读取并赋默认值
[ ] BatchSet
/ Save
— 写入逻辑是否覆盖新字段
[ ] 本地状态表 — 是否包含新字段和默认值
[ ] 重置/清档逻辑 — 是否 Delete 了新字段、清空了本地状态
[ ] UI 展示 — 新字段是否需要显示
[ ] 排行榜 — 如果新字段参与排行,读取和展示是否更新
推荐架构
本地状态先行
```lua
-- 本地状态表(内存缓存)
local player = { gold = 0, exp = 0, level = 1, inventory = {} }
-- 启动时加载
function Start()
clientScore:BatchGet()
:Key("gold"):Key("exp"):Key("level"):Key("inventory")
:Fetch({
ok = function(values, iscores)
player.gold = iscores.gold or 0
player.exp = iscores.exp or 0
player.level = iscores.level or 1
player.inventory = values.inventory or {} -- table 自动反序列化
end,
})
end
-- 游戏中修改:本地先行,云端异步追赶
function OnGoldEarned(amount)
player.gold = player.gold + amount -- 立即更新本地
clientScore:BatchSet()
:Add("gold", amount)
:Save("获得金币", { ok = function() end })
end
```
saveGeneration 防过期回调
重置存档时递增 generation,异步回调检查是否匹配:
```lua
local saveGeneration = 0
function resetAll()
saveGeneration = saveGeneration + 1
local gen = saveGeneration
-- 重置本地...
clientScore:BatchSet()
:Delete("gold"):Delete("exp")
:Save("重置", {
ok = function()
if gen ~= saveGeneration then return end -- 过期则丢弃
-- 二次覆盖归零...
clientScore:BatchSet()
:SetInt("gold", 0):SetInt("exp", 0)
:Save("重置-归零")
end,
})
end
```
两种存档策略
| 模式 | 存储方式 | 优点 | 缺点 |
|------|---------|------|------|
| 逐 key | 每个字段一个云变量 (iscores) | 可排行、可增量、原子操作 | 字段多时 Key 链长 |
| 结构化 table | 单个 values 字段存整个 table | 灵活嵌套(背包等),自动序列化 | 不可排行、不可增量、覆盖式写入 |
建议: 核心数值用逐 key (iscores),复杂嵌套结构用 table (values)。

