gamekit

gamekit

热门

使用 GameKit 集成 Game Center 功能。适用于处理 GKLocalPlayer 身份验证、检查玩家功能限制、提交排行榜分数、上报成就进度、实现实时或回合制匹配、处理 GKMatch 数据、展示 Game Center 仪表盘或入口点(Access Point)、添加挑战与好友邀请、保存游戏数据或在服务端验证玩家身份等场景。

967Star
49Fork
更新于 2026/7/31
SKILL.md
只读
名称
gamekit
描述

使用 GameKit 集成 Game Center 功能。适用于处理 GKLocalPlayer 身份验证、检查玩家功能限制、提交排行榜分数、上报成就进度、实现实时或回合制匹配、处理 GKMatch 数据、展示 Game Center 仪表盘或入口点(Access Point)、添加挑战与好友邀请、保存游戏数据或在服务端验证玩家身份等场景。

GameKit

使用 GameKit 实现 Game Center 身份验证、竞技比赛、玩家匹配、社交界面以及存档同步;渲染、棋盘逻辑和完整的 SharePlay Group Activities 架构请交由对应的框架 Skill 处理。

目录

身份验证

所有 GameKit 功能都需要先对本地玩家进行身份验证。请在应用生命周期的早期设置 GKLocalPlayer.localauthenticateHandler。GameKit 在初始化期间会多次调用该处理函数。

import GameKit

func authenticatePlayer() {
    GKLocalPlayer.local.authenticateHandler = { viewController, error in
        if let viewController {
            // 弹出视图控制器,供玩家登录或创建账号
            present(viewController, animated: true)
            return
        }
        if let error {
            // 玩家无法登录。禁用 Game Center 功能
            disableGameCenter()
            return
        }

        // 玩家已完成验证。开始前检查权限限制
        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

入口点(Access Point)

GKAccessPoint 用于在屏幕角落显示 Game Center 控件。点击后可打开 Game Center 仪表盘。请在身份验证完成后进行配置。

func configureAccessPoint() {
    GKAccessPoint.shared.location = .topLeading
    GKAccessPoint.shared.showHighlights = true
    GKAccessPoint.shared.isActive = true
}

在游戏对局过程中隐藏入口点,在菜单界面显示:

GKAccessPoint.shared.isActive = false  // 游戏进行中时隐藏
GKAccessPoint.shared.isActive = true   // 暂停或菜单界面时显示

也可以通过代码直接打开仪表盘的指定状态。直接打开特定排行榜的入口点触发功能需要 iOS 18+ 支持。

// 直接打开指定排行榜
GKAccessPoint.shared.trigger(
    leaderboardID: "com.mygame.highscores",
    playerScope: .global,
    timeScope: .allTime
) { }

// 直接打开成就界面
GKAccessPoint.shared.trigger(state: .achievements) { }

仪表盘(Dashboard)

使用 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 提供了 playerrankscoreformattedScorecontextdate 属性。有关周期性排行榜时间安排、排行榜图标和排行榜组(Leaderboard Sets)的详情,请参阅 references/gamekit-patterns.md

成就系统

在 App Store Connect 中配置成就。每个成就都有唯一的标识符(identifier)、分值以及本地化的标题/描述。

上报进度

设置 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])
}

// 完全解锁成就
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() 重置所有成就进度。

实时多人对战

实时多人对战通过点对点(P2P)网络连接玩家以进行同步游戏。玩家直接通过 GKMatch 交换数据。

使用 GameKit UI 进行匹配

使用 GKMatchmakerViewController 呈现标准匹配界面:

func presentMatchmaker() {
    let request = GKMatchRequest()
    request.minPlayers = 2
    request.maxPlayers = 4
    request.inviteMessage = "快来加入我的游戏!"

    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)
    }
}

交换数据

通过 GKMatchGKMatchDelegate 发送和接收游戏状态:

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)

    // 构建下一轮参与者列表:剩余的活跃玩家
    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) {
        // 加载对战数据并更新 UI
        loadAndDisplayMatch(match)
    }

    func player(_ player: GKPlayer, matchEnded match: GKTurnBasedMatch) {
        showMatchResults(match)
    }
}

对战数据大小限制

在结束回合前检查对战对象的 matchDataMaximumSize。较大的状态请存储在外部,对战数据中仅保留精简引用。

常见错误

在使用 GameKit API 前未进行身份验证

// 错误示范
func submitScore() {
    GKLeaderboard.submitScore(100, context: 0, player: GKLocalPlayer.local,
                              leaderboardIDs: ["scores"]) { _ in }