WWDC Quick Look 💓 By SwiftGGTeam
Bring Recurring Leaderboards to your game

Bring Recurring Leaderboards to your game

元の動画を見る

ハイライト

iOS 15 では、開発者が定義したサイクル (時間ごと、毎日、毎週) に従って自動的にリセットされる Recurring Leaderboards が導入されています。プレイヤーは現在のサイクルと過去のサイクルのランキングを表示でき、システムは発生ライフサイクルを自動的に管理します。

主な内容

従来のランキングは永続的なものであり、新規プレイヤーが初期プレイヤーに追いつくことは決してありません。ゲームは独自の一時的なアクティビティ ランキングのみを作成できますが、これには複雑なロジックがあり、サーバーのサポートが必要です。

iOS 15 の定期的なリーダーボードでは、システムがこれを実行します。App Store Connect で一度設定すると、システムによって定期的に新しいオカレンスが作成されます。たとえば、毎日のランキング リスト: 新しいラウンドは毎日 0:00 に自動的に開始され、前日のデータはアーカイブされ、プレイヤーは 30 日以内の過去のランキングを引き続き確認できます。

これにより、いくつかの実際的な問題が解決されます。

  • 新しいプレイヤーには、すべてのサイクルで競争する公平なチャンスがあります
  • 定期的なアクティビティを行うために独自のランキングを作成する必要はありません
  • プレイヤーはサイクル全体での進歩または後退を確認できます

App Store Connect で設定する

06:59

App Store Connect → アプリ → 機能 → Game Center → Leaderboards → 「+」をクリック → Recurring Leaderboard を選択します。

基本的な設定フィールド:

フィールド説明
参照名App Store Connect によって内部的に使用される名前毎日のハイスコア
リーダーボード IDコード内で参照される ID毎日のハイスコア
スコア形式のタイプスコア表示形式整数、時間、お金
スコア提出タイプ複数回提出する場合はどのスコアを取るべきか最高 (最高スコア) または最新
並べ替え順序並べ替え高から低 (スコアが高い順)
スコア範囲有効なスコア範囲最小値 0、最大値 200

** サイクルランキング専用フィールド: **

フィールド説明
開始日時最初の発生の開始時刻 (UTC)
期間1 回の発生の期間
ごとに再起動します新しいオカレンスが作成される頻度

キーポイント:

  • Restarts Every必須 ≥Duration、オカレンスが重複しないようにする
  • 間隔を空けずに連続してループさせたい場合は、Restarts Every = Duration
  • 過去の時刻を開始時刻として選択することはできません

ローカリゼーション設定:

少なくとも 1 つの言語の情報を構成します。

  • 表示名: Game Center UI に表示される名前
  • スコア形式: 千の位区切り文字 (カンマまたはピリオド)
  • スコア形式のサフィックス:スコア単位(「ポイント」など)
  • リーダーボード画像: オプションですが、ブランド認知度を高めるためにアップロードすることをお勧めします

ローカルプレイヤーを認証する

10:35

Game Center の機能を使用する前に、ローカル プレーヤーは認証される必要があります。

func authenticateLocalPlayer() {
    GKLocalPlayer.local.authenticateHandler = { viewController, error in
        if let viewController = viewController {
            // 需要显示登录界面
            self.present(viewController, animated: true)
        } else if GKLocalPlayer.local.isAuthenticated {
            // 认证成功
            print("Authenticated successfully")
        } else {
            // 认证失败
            print("Authentication failed: \(error?.localizedDescription ?? "unknown")")
        }
    }
}

キーポイント:

  • 存在するapplicationDidFinishLaunchingWithOptions電話する
  • authenticateHandlerこれはグローバルであり、一度だけ設定されます
  • 認証は非同期であり、システム ログイン インターフェイスがポップアップ表示される場合があります。

定期的なリーダーボードにスコアを送信する

11:16

送信メソッドには、クラス メソッドとインスタンス メソッドの 2 つがあります。

クラス メソッド (単純なシナリオに推奨):

func submitScore(_ score: Int) {
    GKLeaderboard.submitScore(score,
                              leaderboard: "DailyHighScore",
                              playerScope: .global,
                              timeScope: .allTime)
}

キーポイント:

  • スコアは現在アクティブなオカレンスに送信されます
  • アクティブなオカレンスがない場合、送信は失敗します。
  • timeScopeループするリーダーボードの場合は、.allTime

インスタンス メソッド (オカレンスをまたがないように注意してください):

func submitScoreSafely(_ score: Int) {
    // 先加载当前 occurrence
    GKLeaderboard.loadLeaderboards(IDs: ["DailyHighScore"]) { leaderboards, error in
        guard let leaderboard = leaderboards?.first else { return }

        // 在 completion handler 中提交
        leaderboard.submitScore(score,
                                playerScope: .global,
                                timeScope: .allTime)
    }
}

キーポイント:

  • 最初にロードしますGKLeaderboard例を示して再度送信する
  • スコアが同じオカレンスに送信されるようにする
  • 非常に高い適時性要件があるシナリオ (時間ごとのチャレンジなど) に適しています

Game Center システムのランキングを表示します

12:57

@IBAction func showLeaderboardVC(_ sender: UIButton) {
    let vc = GKGameCenterViewController(leaderboardID: "DailyHighScore",
                                       playerScope: .global,
                                       timeScope: .allTime)
    vc.gameCenterDelegate = self
    present(vc, animated: true)
}

extension TitleScreenViewController: GKGameCenterViewControllerDelegate {
    func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
        gameCenterViewController.dismiss(animated: true)
    }
}

キーポイント:

  • playerScope持っている.global(グローバル) と.friendsOnly(友達限定) 2種類
  • 自転車ランキング一覧timeScopeに固定.allTime
  • 成し遂げるgameCenterViewControllerDidFinish処理が終了しました

リアルタイムランキングを表示

16:19

ゲーム中のトップ 5 の現在のリアルタイム ランキングを表示します。

func updateLeaderboardNode() {
    // 加载排行榜
    GKLeaderboard.loadLeaderboards(IDs: ["DailyHighScore"]) { leaderboards, error in
        guard let leaderboard = leaderboards?.first else { return }

        // 加载前 5 名
        leaderboard.loadEntries(for: .global,
                              timeScope: .allTime,
                              range: NSRange(location: 1, length: 5)) { entries, error in
            guard let entries = entries else { return }

            // 转换为自定义 Entry 结构
            let leaderboardEntries = entries.1.map { entry in
                LeaderboardEntry(name: entry.player.displayName,
                               score: entry.score)
            }

            // 更新 UI
            self.leaderboardNode?.updateEntries(leaderboardEntries)
        }
    }
}

// 在每次得分更新时调用
func submitScore(_ score: Int) {
    GKLeaderboard.submitScore(score,
                              leaderboard: "DailyHighScore",
                              playerScope: .global,
                              timeScope: .allTime)

    // 更新实时排行榜
    updateLeaderboardNode()
}

キーポイント:

  • loadEntries(for:timeScope:range:)rangeパラメータは取得されるランキング範囲を制御します
  • entriesタプルです(totalPlayerCount, entryArray)
  • 存在するsubmitScoreランキングをリアルタイムで更新し続けるために完了時に呼び出されます

現在のランキングと過去のランキングを比較する

21:01

Swift async/await を使用して、サイクル間のランキング比較を簡素化します。

func addRankToGameMenu() async {
    do {
        // 加载当前 occurrence
        let leaderboard = try await GKLeaderboard.loadLeaderboards(IDs: ["DailyHighScore"]).first

        // 并发加载当前排名和上期排名
        async let currentEntries = leaderboard?.loadEntries(for: .global,
                                                           timeScope: .allTime)
        async let previousLeaderboard = leaderboard?.loadPreviousOccurrence()
        async let previousEntries = previousLeaderboard?.loadEntries(for: .global,
                                                                     timeScope: .allTime)

        // 等待结果
        let (_, currentPlayerEntries) = try await currentEntries ?? ([], [])
        let (_, previousPlayerEntries) = try await previousEntries ?? ([], [])

        // 提取本地玩家排名
        let currentRank = currentPlayerEntries.first?.rank
        let previousRank = previousPlayerEntries.first?.rank

        // 显示排名变化
        gameMenuNode.addRankNode(currentRank: currentRank,
                                 previousRank: previousRank)

    } catch {
        print("Error loading ranks: \(error.localizedDescription)")
    }
}

// 在游戏结束时调用
func timeIsUp() {
    // ...
    Task {
        await addRankToGameMenu()
    }
}

キーポイント:

  • loadPreviousOccurrence()最後に終了したオカレンスをロードします
  • async let同時読み込みを実装してパフォーマンスを向上させる
  • プレイヤーは 30 日以内の過去の発生データを表示できます

##詳細

ループリーダーボードの仕組み

システムは、設定された期間に従ってオカレンスを自動的に作成します。

Start Date: June 11, 9:00 AM
Duration: 1 day
Restarts Every: 1 day

Occurrence 1: June 11, 9:00 AM → June 12, 9:00 AM
Occurrence 2: June 12, 9:00 AM → June 13, 9:00 AM
Occurrence 3: June 13, 9:00 AM → June 14, 9:00 AM
...

キーポイント:

  • 各オカレンスは独立したリーダーボード インスタンスです
  • オカレンスの有効期限が切れた後はスコアを送信できません
  • 期限切れの発生データは 30 日間保持されます

コンテキストパラメータの使用

submitScoreメソッドのサポートcontextゲーム固有の情報を保存するために使用されるパラメータ:

let context = 0  // 可以是任意整数
GKLeaderboard.submitScore(score,
                          leaderboard: "DailyHighScore",
                          playerScope: .global,
                          timeScope: .allTime,
                          context: context)

context一般的な用途:

  • スコア提出時に使用したキャラクター/難易度を記録します
  • 特別なイベントをマークします (ダブルスコア期間など)
  • スコアの正当性を検証する

エラー処理

一般的なエラーとその原因:

  • GKErrorInvalidPlayer: プレーヤーが認証されていません
  • GKErrorInvalidScopetimeScope設定ミス
  • GKErrorInvalidArgument: スコアが設定された有効範囲を超えています
  • GKErrorAuthenticationFailed:ネットワークの問題または認証失敗

重要ポイント

  1. 毎日チャレンジ活動を行う
  • やるべきこと: 新しいランキングリストは毎日0:00に24時間オープンされます。テーマは「最高コンボ」「最速クリア」など。
  • やる価値がある理由: 一定期間のアクティビティはプレイヤーの習慣を養うことができ、毎日のアクティビティは自然なピークを形成します。サーバーコストゼロでランキングリストをループ
  • 開始方法: App Store Connectは24時間周期のランキングリストを構成し、ゲーム内にカウントダウンと現在のランキングを表示します
  1. 季節ベースのアリーナを作成
  • やるべきこと: 毎月新しいシーズンランキングが開設され、シーズン終了時にランキングに基づいて報酬が発行されます。
  • 実行する価値がある理由: シーズン システムにより、プレイヤーは明確な参加目標と時間的プレッシャーを持つことができるため、永続的なランキングよりも競争を刺激することができます。
  • 開始方法:Duration = 30 days、シーズンの終わりに使用されますloadPreviousOccurrenceチャンピオンデータを読み取り、報酬を配布します
  1. クロスサイクルの進行状況を視覚化
  • やるべきこと: ゲームの終了時に、前の期間と比較したプレイヤーのランキングを表示します:「15 上がった」または「3 下がった」
  • 実行する価値がある理由: プレイヤーが自分の成長の軌跡を確認できるようにすると、現在のランキングを単に表示するよりも大きな達成感が得られます。
  • 開始方法: 比較loadEntries現在の発生状況とloadPreviousOccurrenceランキング
  1. フレンドリレーランキング
  • やるべきこと: 週間ランキングのテーマは「友達とポイントを磨く」で、合計スコアまたは友達の最高スコアを計算します
  • 実行する価値がある理由: 純粋な競争よりも社会的なインセンティブの方が効果的です。プレイヤーは友達を積極的に招待して参加します。
  • 開始方法: Friends API を使用してフレンドリストを取得し、playerScope: .friendsOnlyランキングに表示されます
  1. 期間限定チャレンジモードを実行
  • やるべきこと: 1時間ごとに15分間のスピードチャレンジを開始して、誰が制限時間内に最も多くのスコアを獲得できるかを確認してください
  • 実行する価値がある理由: 高頻度で短いサイクルのアクティビティにより、ユーザーの定着率が大幅に向上し、プレイヤーは時間通りに戻ってきて参加するようになります。
  • 開始方法:Duration = 15 minutesRestarts Every = 1 hour、ゲームには次のチャレンジまでのカウントダウンが表示されます

関連セッション

コメント

GitHub Issues · utterances