关于 gamekit
GameKit 是面向 Apple 游戏开发者的 Swift 技能指南,涵盖 Game Center 身份验证、排行榜、成就和多人游戏。它强调按正确顺序调用 API,并提供常见错误与发布检查清单。
主要功能
- 01Game Center 玩家身份验证
- 02排行榜与成就管理
- 03实时及回合制多人游戏
- 04游戏状态数据交换
- 05访问点与仪表盘配置
适用场景
- 01为玩家接入 Game Center
- 02提交和读取排行榜分数
- 03报告并读取成就进度
- 04实现实时或异步多人对局
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill gamekit安装后在 Claude Code 中输入/gamekit确认已加载。
附带文件evals/evals.jsonreferences/gamekit-patterns.md
详细介绍
GameKit
使用 GameKit 进行 Game Center 身份验证、竞赛、匹配、社交界面和已保存游戏的交接;渲染、棋盘逻辑和完整的 SharePlay 群组活动设计应放在各自所属的框架技能中。
内容
身份验证
所有 GameKit 功能都要求本地玩家先完成身份验证。在应用生命周期早期为 GKLocalPlayer.local 设置 authenticateHandler。初始化期间,GameKit 会多次调用该处理程序。
import GameKit
func authenticatePlayer() {
GKLocalPlayer.local.authenticateHandler = { viewController, error in
if let viewController {
// Present so the player can sign in or create an account.
present(viewController, animated: true)
return
}
if let error {
// Player could not sign in. Disable Game Center features.
disableGameCenter()
return
}
// Player authenticated. Check restrictions before starting.
let player = GKLocalPlayer.local
if player.isUnderage {
hideExplicitContent()
}
if player.isMultiplayerGamingRestricted {
disableMultiplayer()
}
if player.isPersonalizedCommunicationRestricted {
disableInGameChat()
}
configureAccessPoint()
}
}
调用任何 GameKit API 前,先检查 GKLocalPlayer.local.isAuthenticated。如需在服务器端验证身份,请参阅 references/gamekit-patterns.md。
访问点
GKAccessPoint 会在屏幕角落显示一个 Game Center 控件。轻点该控件即可打开 Game Center 仪表板。请在身份验证后进行配置。
func configureAccessPoint() {
GKAccessPoint.shared.location = .topLeading
GKAccessPoint.shared.showHighlights = true
GKAccessPoint.shared.isActive = true
}
游戏过程中隐藏访问点,并在菜单屏幕上显示:
GKAccessPoint.shared.isActive = false // Hide during active gameplay
GKAccessPoint.shared.isActive = true // Show on pause or menu
以编程方式打开特定状态的仪表板。特定排行榜的访问点触发器需要 iOS 18+。
// Open directly to a leaderboard
GKAccessPoint.shared.trigger(
leaderboardID: "com.mygame.highscores",
playerScope: .global,
timeScope: .allTime
) { }
// Open directly to achievements
GKAccessPoint.shared.trigger(state: .achievements) { }
仪表板
使用 GKGameCenterViewController 展示 Game Center 仪表板。呈现对象必须遵循 GKGameCenterControllerDelegate。
final class GameViewController: UIViewController, GKGameCenterControllerDelegate {
func showDashboard() {
let vc = GKGameCenterViewController(state: .dashboard)
vc.gameCenterDelegate = self
present(vc, animated: true)
}
func showLeaderboard(_ leaderboardID: String) {
let vc = GKGameCenterViewController(
leaderboardID: leaderboardID,
playerScope: .global,
timeScope: .allTime
)
vc.gameCenterDelegate = self
present(vc, animated: true)
}
func gameCenterViewControllerDidFinish(
_ gameCenterViewController: GKGameCenterViewController
) {
gameCenterViewController.dismiss(animated: true)
}
}
仪表板状态包括 .dashboard、.leaderboards、.achievements、.challenges、.localPlayerProfile 和 .localPlayerFriendsList。
排行榜
提交分数前,请先在 App Store Connect 中配置排行榜。支持经典型(持久)和循环型(限时、自动重置)排行榜。
提交分数
使用类方法提交到一个或多个排行榜:
func submitScore(_ score: Int, leaderboardIDs: [String]) async throws {
try await GKLeaderboard.submitScore(
score,
context: 0,
player: GKLocalPlayer.local,
leaderboardIDs: leaderboardIDs
)
}
加载条目
func loadTopScores(
leaderboardID: String,
count: Int = 10
) async throws -> (GKLeaderboard.Entry?, [GKLeaderboard.Entry]) {
let leaderboards = try await GKLeaderboard.loadLeaderboards(
IDs: [leaderboardID]
)
guard let leaderboard = leaderboards.first else { return (nil, []) }
let (localEntry, entries, _) = try await leaderboard.loadEntries(
for: .global,
timeScope: .allTime,
range: 1...count
)
return (localEntry, entries)
}
GKLeaderboard.Entry 提供 player、rank、score、formattedScore、context 和 date。有关循环排行榜的计时、排行榜图像和排行榜集,请参阅 references/gamekit-patterns.md。
成就
在 App Store Connect 中配置成就。每项成就都有唯一标识符、点数以及本地化的标题和描述。
报告进度
将 percentComplete 设为 0...100。该属性的类型是 Double,但 Apple 要求其值为整数。GameKit 只接受进度增加。
func reportAchievement(identifier: String, percentComplete: Int) async throws {
let achievement = GKAchievement(identifier: identifier)
achievement.percentComplete = Double(min(max(percentComplete, 0), 100))
achievement.showsCompletionBanner = true
try await GKAchievement.report([achievement])
}
// Unlock an achievement completely
func unlockAchievement(_ identifier: String) async throws {
try await reportAchievement(identifier: identifier, percentComplete: 100)
}
加载玩家成就
func loadPlayerAchievements() async throws -> [GKAchievement] {
try await GKAchievement.loadAchievements()
}
如果未返回某项成就,则表示玩家尚未取得该成就的任何进度。创建新的 GKAchievement(identifier:) 以开始报告进度。测试期间,可使用 GKAchievement.resetAchievements() 重置所有进度。
实时多人游戏
实时多人游戏通过点对点网络连接玩家,以便同时进行游戏。玩家通过 GKMatch 直接交换数据。
使用 GameKit UI 进行匹配
使用 GKMatchmakerViewController 实现标准匹配界面:
func presentMatchmaker() {
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 4
request.inviteMessage = "Join my game!"
guard let matchmakerVC = GKMatchmakerViewController(matchRequest: request) else {
return
}
matchmakerVC.matchmakerDelegate = self
present(matchmakerVC, animated: true)
}
实现 GKMatchmakerViewControllerDelegate:
extension GameViewController: GKMatchmakerViewControllerDelegate {
func matchmakerViewController(
_ viewController: GKMatchmakerViewController,
didFind match: GKMatch
) {
match.delegate = self
viewController.dismiss(animated: true)
startGame(with: match)
}
func matchmakerViewControllerWasCancelled(
_ viewController: GKMatchmakerViewController
) {
viewController.dismiss(animated: true)
}
func matchmakerViewController(
_ viewController: GKMatchmakerViewController,
didFailWithError error: Error
) {
viewController.dismiss(animated: true)
}
}
交换数据
通过 GKMatch 和 GKMatchDelegate 发送和接收游戏状态:
extension GameViewController: GKMatchDelegate {
func sendAction(_ action: GameAction, to match: GKMatch) throws {
let data = try JSONEncoder().encode(action)
try match.sendData(toAllPlayers: data, with: .reliable)
}
func match(_ match: GKMatch, didReceive data: Data, fromRemotePlayer player: GKPlayer) {
guard let action = try? JSONDecoder().decode(GameAction.self, from: data) else {
return
}
handleRemoteAction(action, from: player)
}
func match(_ match: GKMatch, player: GKPlayer, didChange state: GKPlayerConnectionState) {
switch state {
case .connected:
checkIfReadyToStart(match)
case .disconnected:
handlePlayerDisconnected(player)
default:
break
}
}
}
数据模式:.reliable 会持续发送,直到成功送达或连接超时;.unreliable 只发送一次,且可能乱序到达。关键状态使用 .reliable,小型且对时效性要求较高的更新使用 .unreliable。将收到的比赛数据视为不可信输入。将本地玩家注册为监听器(GKLocalPlayer.local.register(self))以接收邀请。有关以编程方式进行匹配和自定义匹配 UI,请参阅 references/gamekit-patterns.md。
回合制多人游戏
回合制游戏会将比赛状态存储在 Game Center 服务器上。玩家异步轮流操作,无需同时在线。
开始一场比赛
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 4
let matchmakerVC = GKTurnBasedMatchmakerViewController(matchRequest: request)
matchmakerVC.turnBasedMatchmakerDelegate = self
present(matchmakerVC, animated: true)
轮流进行
将游戏状态编码到 Data 中,结束本回合,并指定下一位参与者:
func endTurn(match: GKTurnBasedMatch, gameState: GameState) async throws {
let data = try JSONEncoder().encode(gameState)
// Build next participants list: remaining active players
let nextParticipants = match.participants.filter {
$0.status != .done && $0 != match.currentParticipant
}
try await match.endTurn(
withNextParticipants: nextParticipants,
turnTimeout: GKTurnTimeoutDefault,
match: data
)
}
结束比赛
为所有参与者设置结果,然后结束比赛:
func endMatch(_ match: GKTurnBasedMatch, winnerIndex: Int, data: Data) async throws {
for (index, participant) in match.participants.enumerated() {
participant.matchOutcome = (index == winnerIndex) ? .won : .lost
}
try await match.endMatchInTurn(withMatch: data)
}
监听回合事件
注册为监听器。如果一个对象要处理多个 Game Center 事件类别,优先使用 GKLocalPlayerListener。
GKLocalPlayer.local.register(self)
extension GameViewController: GKLocalPlayerListener {
func player(_ player: GKPlayer, receivedTurnEventFor match: GKTurnBasedMatch,
didBecomeActive: Bool) {
// Load match data and update UI
loadAndDisplayMatch(match)
}
func player(_ player: GKPlayer, matchEnded match: GKTurnBasedMatch) {
showMatchResults(match)
}
}
比赛数据大小
结束回合前,检查比赛对象的 matchDataMaximumSize。将较大的状态存储在外部,并在比赛数据中仅保留精简的引用。
常见错误
使用 GameKit API 前未进行身份验证
// DON'T
func submitScore() {
GKLeaderboard.submitScore(100, context: 0, player: GKLocalPlayer.local,
leaderboardIDs: ["scores"]) { _ in }
}
// DO
func submitScore() async throws {
guard GKLocalPlayer.local.isAuthenticated else { return }
try await GKLeaderboard.submitScore(
100, context: 0, player: GKLocalPlayer.local, leaderboardIDs: ["scores"]
)
}
多次设置 authenticateHandler
// DON'T: Set handler on every scene transition
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
GKLocalPlayer.local.authenticateHandler = { vc, error in /* ... */ }
}
// DO: Set the handler once, early in the app lifecycle
忽略多人游戏限制
// DON'T
func showMultiplayerMenu() { presentMatchmaker() }
// DO
func showMultiplayerMenu() {
guard !GKLocalPlayer.local.isMultiplayerGamingRestricted else { return }
presentMatchmaker()
}
未立即设置比赛代理
// DON'T: Set delegate in dismiss completion -- misses early messages
func matchmakerViewController(_ vc: GKMatchmakerViewController, didFind match: GKMatch) {
vc.dismiss(animated: true) { match.delegate = self }
}
// DO: Set delegate before dismissing
func matchmakerViewController(_ vc: GKMatchmakerViewController, didFind match: GKMatch) {
match.delegate = self
vc.dismiss(animated: true)
}
未为以编程方式创建的比赛调用 finishMatchmaking
// DON'T
let match = try await GKMatchmaker.shared().findMatch(for: request)
startGame(with: match)
// DO
let match = try await GKMatchmaker.shared().findMatch(for: request)
GKMatchmaker.shared().finishMatchmaking(for: match)
startGame(with: match)
未断开与比赛的连接
// DON'T
func returnToMenu() { showMainMenu() }
// DO
func returnToMenu() {
currentMatch?.disconnect()
currentMatch?.delegate = nil
currentMatch = nil
showMainMenu()
}
检查清单
- 在应用启动时设置一次
GKLocalPlayer.local.authenticateHandler - 调用任何 GameKit API 前检查
isAuthenticated - 检查玩家限制(
isUnderage、isMultiplayerGamingRestricted、isPersonalizedCommunicationRestricted) - 在 Xcode 签名设置中添加 Game Center 功能
- 在 App Store Connect 中配置排行榜和成就
- 配置访问点,并在游戏过程中适时启用或停用
-
GKGameCenterControllerDelegate在gameCenterViewControllerDidFinish中关闭控制面板 - 找到比赛后立即设置比赛代理
- 为以编程方式创建的比赛调用
finishMatchmaking(for:);退出时调用disconnect()并将代理设为 nil - 回合制比赛数据不超过
match.matchDataMaximumSize - 调用
endMatchInTurn前为回合制比赛的参与者设置结果 - 使用
GKLocalPlayer.local.register(_:)注册邀请或回合监听器 - 根据需要选择数据模式:状态数据使用
.reliable,频繁更新使用.unreliable - 为所有异步 GameKit 调用添加错误处理
参考资料
- 有关身份验证、旧版语音聊天、存档、定制比赛 UI、排行榜图片、挑战处理和基于规则的匹配,请参阅 references/gamekit-patterns.md。
- GameKit 文档
- GKLocalPlayer
- GKAccessPoint
- GKLeaderboard
- GKAchievement
- GKMatch
- GKTurnBasedMatch