Highlight
iOS 15 引入循环排行榜(Recurring Leaderboards),按开发者定义的周期(每小时、每天、每周)自动重置,玩家可查看当前周期和历史周期的排名,系统自动管理 occurrence 生命周期。
核心内容
传统排行榜是永久的,新玩家永远追不上早期玩家。游戏只能自己做临时活动排行榜,逻辑复杂且需要服务端支持。
iOS 15 的循环排行榜让系统帮你做这件事。在 App Store Connect 中配置一次,系统按固定周期创建新的 occurrence。比如每日排行榜:每天零点自动开启新一轮,前一天的数据归档,玩家仍可查看 30 天内的历史排名。
这解决了几个实际问题:
- 新玩家每个周期都有公平竞争机会
- 做定期活动不需要自己开发排行榜
- 玩家可以看到自己跨周期的进步或退步
在 App Store Connect 中配置
(06:59)
进入 App Store Connect → 你的 App → Features → Game Center → Leaderboards → 点击「+」 → 选择 Recurring Leaderboard。
基础配置字段:
| 字段 | 说明 | 示例 |
|---|---|---|
| Reference Name | App Store Connect 内部使用的名称 | DailyHighScore |
| Leaderboard ID | 代码中引用的 ID | daily_high_score |
| Score Format Type | 分数显示格式 | Integer、Time、Money |
| Score Submission Type | 多次提交时取哪个分数 | Best(最高分)或 Most Recent |
| Sort Order | 排序方式 | High to Low(高分在前) |
| Score Range | 有效分数范围 | 最小值 0,最大值 200 |
循环排行榜专属字段:
| 字段 | 说明 |
|---|---|
| Start Date and Time | 第一个 occurrence 的开始时间(UTC) |
| Duration | 单个 occurrence 的持续时间 |
| Restarts Every | 新 occurrence 的创建频率 |
关键点:
Restarts Every必须 ≥Duration,确保 occurrence 不重叠- 如果希望无间隔连续循环,设置
Restarts Every = Duration - 不能选择过去的时间作为开始时间
本地化配置:
至少配置一种语言的信息:
- Display Name:在 Game Center UI 中显示的名称
- Score Format:千分位分隔符(逗号或句点)
- Score Format Suffix:分数单位(如 “points”)
- Leaderboard Image:可选但推荐上传,提升品牌识别度
认证本地玩家
(10:35)
在使用 Game Center 任何功能之前,需要认证本地玩家:
func authenticateLocalPlayer() {
GKLocalPlayer.local.authenticateHandler = { viewController, error in
if let viewController = viewController {
// 需要显示登录界面
self.present(viewController, animated: true)
} else if GKLocalPlayer.local.isAuthenticated {
// 认证成功
print("Authenticated successfully")
} else {
// 认证失败
print("Authentication failed: \(error?.localizedDescription ?? "unknown")")
}
}
}
关键点:
- 在
applicationDidFinishLaunchingWithOptions中调用 authenticateHandler是全局的,只设置一次- 认证是异步的,可能弹出系统登录界面
提交分数到循环排行榜
(11:16)
有两种提交方式:类方法和实例方法。
类方法(推荐用于简单场景):
func submitScore(_ score: Int) {
GKLeaderboard.submitScore(score,
leaderboard: "DailyHighScore",
playerScope: .global,
timeScope: .allTime)
}
关键点:
- 分数会提交到当前 active 的 occurrence
- 如果没有 active occurrence,提交会失败
timeScope对于循环排行榜必须是.allTime
实例方法(确保不跨 occurrence):
func submitScoreSafely(_ score: Int) {
// 先加载当前 occurrence
GKLeaderboard.loadLeaderboards(IDs: ["DailyHighScore"]) { leaderboards, error in
guard let leaderboard = leaderboards?.first else { return }
// 在 completion handler 中提交
leaderboard.submitScore(score,
playerScope: .global,
timeScope: .allTime)
}
}
关键点:
- 先加载
GKLeaderboard实例,再提交 - 确保分数提交到同一个 occurrence
- 适用于对时效性要求极高的场景(如每小时挑战)
显示 Game Center 系统排行榜
(12:57)
@IBAction func showLeaderboardVC(_ sender: UIButton) {
let vc = GKGameCenterViewController(leaderboardID: "DailyHighScore",
playerScope: .global,
timeScope: .allTime)
vc.gameCenterDelegate = self
present(vc, animated: true)
}
extension TitleScreenViewController: GKGameCenterViewControllerDelegate {
func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
gameCenterViewController.dismiss(animated: true)
}
}
关键点:
playerScope有.global(全球)和.friendsOnly(仅好友)两种- 循环排行榜的
timeScope固定为.allTime - 实现
gameCenterViewControllerDidFinish处理关闭
显示实时排行榜
(16:19)
在游戏过程中显示当前前 5 名的实时排名:
func updateLeaderboardNode() {
// 加载排行榜
GKLeaderboard.loadLeaderboards(IDs: ["DailyHighScore"]) { leaderboards, error in
guard let leaderboard = leaderboards?.first else { return }
// 加载前 5 名
leaderboard.loadEntries(for: .global,
timeScope: .allTime,
range: NSRange(location: 1, length: 5)) { entries, error in
guard let entries = entries else { return }
// 转换为自定义 Entry 结构
let leaderboardEntries = entries.1.map { entry in
LeaderboardEntry(name: entry.player.displayName,
score: entry.score)
}
// 更新 UI
self.leaderboardNode?.updateEntries(leaderboardEntries)
}
}
}
// 在每次得分更新时调用
func submitScore(_ score: Int) {
GKLeaderboard.submitScore(score,
leaderboard: "DailyHighScore",
playerScope: .global,
timeScope: .allTime)
// 更新实时排行榜
updateLeaderboardNode()
}
关键点:
loadEntries(for:timeScope:range:)的range参数控制获取的排名范围entries是一个元组(totalPlayerCount, entryArray)- 在
submitScore的 completion 中调用,保持排行榜实时更新
对比当前和历史排名
(21:01)
用 Swift async/await 简化跨周期排名对比:
func addRankToGameMenu() async {
do {
// 加载当前 occurrence
let leaderboard = try await GKLeaderboard.loadLeaderboards(IDs: ["DailyHighScore"]).first
// 并发加载当前排名和上期排名
async let currentEntries = leaderboard?.loadEntries(for: .global,
timeScope: .allTime)
async let previousLeaderboard = leaderboard?.loadPreviousOccurrence()
async let previousEntries = previousLeaderboard?.loadEntries(for: .global,
timeScope: .allTime)
// 等待结果
let (_, currentPlayerEntries) = try await currentEntries ?? ([], [])
let (_, previousPlayerEntries) = try await previousEntries ?? ([], [])
// 提取本地玩家排名
let currentRank = currentPlayerEntries.first?.rank
let previousRank = previousPlayerEntries.first?.rank
// 显示排名变化
gameMenuNode.addRankNode(currentRank: currentRank,
previousRank: previousRank)
} catch {
print("Error loading ranks: \(error.localizedDescription)")
}
}
// 在游戏结束时调用
func timeIsUp() {
// ...
Task {
await addRankToGameMenu()
}
}
关键点:
loadPreviousOccurrence()加载最近结束的 occurrenceasync let实现并发加载,提升性能- 玩家可以在 30 天内查看历史 occurrence 的数据
详细内容
循环排行榜的工作原理
系统按配置的周期自动创建 occurrence:
Start Date: June 11, 9:00 AM
Duration: 1 day
Restarts Every: 1 day
Occurrence 1: June 11, 9:00 AM → June 12, 9:00 AM
Occurrence 2: June 12, 9:00 AM → June 13, 9:00 AM
Occurrence 3: June 13, 9:00 AM → June 14, 9:00 AM
...
关键点:
- 每个 occurrence 是独立的排行榜实例
- occurrence 过期后不能再提交分数
- 过期的 occurrence 数据保留 30 天
Context 参数的使用
submitScore 方法支持 context 参数,用于存储游戏特定信息:
let context = 0 // 可以是任意整数
GKLeaderboard.submitScore(score,
leaderboard: "DailyHighScore",
playerScope: .global,
timeScope: .allTime,
context: context)
context 的常见用途:
- 记录提交分数时使用的角色/难度
- 标记特殊事件(如双倍得分期间)
- 验证分数合法性
错误处理
常见错误及其原因:
GKErrorInvalidPlayer:玩家未认证GKErrorInvalidScope:timeScope设置错误GKErrorInvalidArgument:分数超出配置的有效范围GKErrorAuthenticationFailed:网络问题或认证失败
核心启发
-
做每日挑战活动
- 做什么:每天零点开启一个新排行榜,持续 24 小时,主题可以是「最高连击」、「最快通关」等
- 为什么值得做:固定周期的活动能培养玩家习惯,日活会形成自然波峰。循环排行榜零服务端成本
- 怎么开始:App Store Connect 配置 24 小时循环排行榜,游戏内展示倒计时和当前排名
-
做赛季制竞技场
- 做什么:每月开启一个新赛季排行榜,赛季结束时根据排名发放奖励
- 为什么值得做:赛季制让玩家有明确的参与目标和时间压力,比永久排行榜更能激发竞争
- 怎么开始:
Duration = 30 days,赛季结束时用loadPreviousOccurrence读取冠军数据并分发奖励
-
做跨周期进度可视化
- 做什么:在游戏结束时显示玩家与上期排名的对比:「上升 15 名」或「下降 3 名」
- 为什么值得做:让玩家看到自己的成长轨迹,比单纯显示当前排名更有成就感
- 怎么开始:对比
loadEntries的当前 occurrence 和loadPreviousOccurrence的排名
-
做好友接力排行榜
- 做什么:每周排行榜的主题是「和好友一起刷分」,计算好友总分或最高分
- 为什么值得做:社交激励比单纯竞争更有效,玩家会主动拉好友参与
- 怎么开始:用 Friends API 获取好友列表,然后在
playerScope: .friendsOnly的排行榜中显示
-
做限时挑战模式
- 做什么:每小时开启一次 15 分钟的极速挑战,看谁在限定时间内得分最高
- 为什么值得做:高频短周期的活动能显著提升用户粘性,玩家会准时回来参加
- 怎么开始:
Duration = 15 minutes,Restarts Every = 1 hour,游戏内显示下次挑战倒计时
关联 Session
- What’s new in Game Center: Widgets, friends, and multiplayer improvements — Game Center 年度新特性总览
- Support real-time and turn-based multiplayer with Game Center — 多人游戏技术详解
- Tap into Game Center: Leaderboards, achievements, and multiplayer — 排行榜和成就完整指南(2020)
评论
GitHub Issues · utterances