mcpField
G
gamekit

gamekit

github.com/dpearson2699开发1.2k3.2k 次安装13 周前更新
代码仓库

关于 gamekit

GameKit 是面向 Apple 游戏开发者的 Swift 技能指南,涵盖 Game Center 身份验证、排行榜、成就和多人游戏。它强调按正确顺序调用 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

使用 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 调用添加错误处理

参考资料

评论

支持 Markdown