mcpField
G
gamekit

gamekit

github.com/dpearson2699開発1.2k3.2k 回インストール13 週間前に更新
リポジトリ

gamekit について

GameKit は、Game Center の認証、リーダーボード、実績、マルチプレイヤーを扱う、Apple ゲーム開発者向けの Swift スキルガイドです。API の適切な呼び出し順や、よくある誤り、レビュー項目も説明します。

主な機能

  1. 01Game Center のプレイヤー認証
  2. 02リーダーボードと実績の管理
  3. 03リアルタイム・ターン制マルチプレイヤー
  4. 04ゲーム状態データの交換
  5. 05アクセス ポイントとダッシュボードの設定

活用シーン

  1. 01ゲームへの Game Center サインイン追加
  2. 02リーダーボードスコアの送信と取得
  3. 03実績進捗の報告と読み込み
  4. 04リアルタイムまたは非同期対戦の実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill gamekit

インストール後、Claude Code で/gamekitを入力して読み込まれたか確認します。

同梱ファイルevals/evals.jsonreferences/gamekit-patterns.md

詳細

GameKit

Game Center 認証、対戦、マッチメイキング、ソーシャル機能、セーブデータの引き継ぎには GameKit を使用します。レンダリング、ボードロジック、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 を確認してください。サーバー側での ID 検証については、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 でリーダーボードを設定してください。クラシック(永続型)と定期(期間限定で自動リセット)のタイプに対応しています。

スコアの送信

クラスメソッドを使用して、1 つ以上のリーダーボードに送信します。

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)
}
ターンイベントを受信する

リスナーとして登録します。1つのオブジェクトで複数の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を1回設定する
  • 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呼び出しでエラーを処理する

参考資料

コメント