gamekit

gamekit

熱門

使用 GameKit 整合 Game Center 功能。適用於驗證 GKLocalPlayer、檢查玩家限制、提交排行榜分數、回報成就、實作即時或回合制對戰配對、處理 GKMatch 資料、顯示 Game Center 儀表板或存取點(Access Point)、新增挑戰與好友邀請、儲存遊戲資料,或是於伺服器端驗證玩家身分。

967星標
49分支
更新於 2026/7/31
SKILL.md
唯讀
名稱
gamekit
描述

使用 GameKit 整合 Game Center 功能。適用於驗證 GKLocalPlayer、檢查玩家限制、提交排行榜分數、回報成就、實作即時或回合制對戰配對、處理 GKMatch 資料、顯示 Game Center 儀表板或存取點(Access Point)、新增挑戰與好友邀請、儲存遊戲資料,或是於伺服器端驗證玩家身分。

GameKit

使用 GameKit 處理 Game Center 身份驗證、競賽、配對、社交介面與遊戲存檔轉移;請將渲染(rendering)、棋牌與盤面邏輯(board logic)以及完整的 SharePlay 群組活動設計留給各自對應的框架 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 為 true。關於伺服器端的身份驗證,請參閱 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 等屬性。關於循環排行榜時間、排行榜圖片及排行榜集錦(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() 重置所有進度。

即時多人對戰

即時多人對戰透過點對點(peer-to-peer)網路連線玩家以進行同步遊戲。玩家之間直接透過 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 前未先進行身份驗證

// 請勿這樣做 (DON'T)
func submitScore() {
    GKLeaderboard.submitScore(100, context: 0, player: GKLocalPlayer.local,
                              leaderboardIDs: ["scores"]) { _ in }

<!-- truncated for translation batch; full body continues in source -->