個人開発で小さなパズルゲームを出したとき、最初はランキングを自前の API で作りました。スコアの受け口を書き、重複投稿を弾き、表示用のエンドポイントを整え——気づけばゲーム本体より、周辺の保守に時間を取られていました。
その反省から、次のゲームではランキングを Game Center に寄せました。サーバーは持たず、認証もランキング UI も Apple 側に任せる構成です。Rork Max はネイティブ Swift を出力するので、GameKit のような「React Native からは届きにくい」フレームワークにもそのまま手が届きます。
以下、Rork Max で生成したゲームに Game Center を組み込む流れを、App Store Connect の設定から認証・スコア送信・実績、そして避けて通れないスコア改ざんの話まで、私が実装した順序のまま追っていきます。
ランキングを自前で持つか、Game Center に任せるか
まず判断の分かれ目から。私自身は次の基準で使い分けています。
観点 自前 API Game Center 実装コスト 受け口・認証・UI を全て自作 クライアント実装のみ。UI は標準提供 運用コスト サーバー費・監視・スパム対策が継続的に必要 ほぼゼロ。Apple 側が保持 不正対策 サーバー側で検証ロジックを組める クライアント申告制。検証の仕組みはない UI の自由度 完全に自由 標準 UI が基本。カスタム表示は API 経由で可能 対応範囲 任意のプラットフォーム Apple プラットフォームのみ
競技性が高く賞金や報酬が絡むゲームなら、サーバー検証を組める自前 API に分があります。一方、個人開発の規模で「友達と競える」「昨日の自分を超える」体験を届けたいだけなら、Game Center の運用コストゼロは強力です。
Android 版を同じコードベースで出す予定があるかどうかも効きます。Rork Max はネイティブ Swift 出力なので、この記事の構成は iOS 専用が前提です。
App Store Connect 側の準備
コードより先に、App Store Connect の設定を済ませます。ここを後回しにすると、実装が終わっているのにテストできない時間が生まれます。
App Store Connect で対象アプリを開き、「サービス」から Game Center を有効にします
リーダーボードを作成します。参照名・ID(例: com.example.puzzle.highscore)・スコア形式(整数か小数か)・並び順(高い方が上か低い方が上か)・スコアの提出範囲を決めます
実績を作成します。ID・ポイント(合計 1,000 まで)・達成前に隠すかどうかを設定します
少なくとも1言語分のローカライズ(表示名・画像)を埋めます。ここが空だと保存できません
Xcode 側でターゲットの Signing & Capabilities に Game Center capability を追加します
ひとつ注意点があります。リーダーボードの ID は作成後に変更できません。アプリの bundle ID と同じ要領で、逆ドメイン形式で命名しておくと、ゲームが増えても衝突しません。
認証 — authenticateHandler の作法
GameKit の入口は GKLocalPlayer の認証です。ここには作法があり、知らないと審査や UX で足を取られます。
このコードは、起動時に一度だけ認証ハンドラを登録し、以後の状態変化を受け取る最小構成です。
import GameKit
final class GameCenterManager {
static let shared = GameCenterManager ()
private ( set ) var isAuthenticated = false
func authenticate ( presenting rootVC: UIViewController) {
GKLocalPlayer.local.authenticateHandler = { [ weak self ] viewController, error in
if let viewController {
// サインインが必要な場合のみ、Apple 標準のサインイン画面を提示する
rootVC. present (viewController, animated : true )
return
}
if let error {
// 認証失敗。ゲーム自体は遊べる状態を維持する
print ( "Game Center 認証に失敗しました: \( error. localizedDescription ) " )
self ? .isAuthenticated = false
return
}
self ? .isAuthenticated = GKLocalPlayer.local.isAuthenticated
}
}
}
実装して分かった注意点が3つあります。
まず、authenticateHandler は起動のたびに設定します。一度認証済みでも、バックグラウンドから戻った際などに再評価が走るため、ハンドラは「登録しっぱなし」が正しい形です。
次に、サインイン用の ViewController が渡されるのは未サインイン時だけです。ユーザーが一度キャンセルすると、以後は自動で再提示されません。設定アプリの Game Center からサインインし直してもらう導線を、ヘルプ等にひっそり用意しておくと問い合わせが減ります。
そして最も大切なのは、認証に失敗してもゲームを遊べる状態に保つことです。Game Center へのサインインを強制してゲーム本体をロックする作りは、体験としても審査の観点でも避けるべきです。ランキング機能だけを静かに無効化し、本体は普通に遊べるようにしておきます。
スコア送信とランキング表示
スコアの送信は一行に近い簡潔さです。次のコードは、ゲームオーバー時に確定スコアを送る例です。
func submit ( score : Int , to leaderboardID: String ) {
guard isAuthenticated else { return }
GKLeaderboard. submitScore (
score,
context : 0 ,
player : GKLocalPlayer.local,
leaderboardIDs : [leaderboardID]
) { error in
if let error {
print ( "スコア送信に失敗しました: \( error. localizedDescription ) " )
}
}
}
送信が失敗するのは主にオフライン時です。submitScore は自動リトライを持たないため、失敗したスコアは UserDefaults 等に保留し、次回起動時や再接続時に送り直す小さな仕組みを足しておくと取りこぼしが消えます。ベストスコアだけ保留すれば十分なので、実装は数十行で収まります。
ランキングの入口には GKAccessPoint が便利です。画面隅に Game Center 公式のフローティングボタンを出せます。
GKAccessPoint.shared.location = .topLeading
GKAccessPoint.shared.showHighlights = true
GKAccessPoint.shared.isActive = true
ただし、ゲームプレイ中は非表示にすることをおすすめします。プレイ領域の隅にあると誤タップの原因になるため、私はこの構成をタイトル画面とリザルト画面だけで有効にしています。
任意のタイミングでランキング画面を開くには GKGameCenterViewController を使います。
func showLeaderboard ( from rootVC: UIViewController, leaderboardID : String ) {
let vc = GKGameCenterViewController (
leaderboardID : leaderboardID,
playerScope : .global,
timeScope : .allTime
)
vc.gameCenterDelegate = rootVC as? GKGameCenterControllerDelegate
rootVC. present (vc, animated : true )
}
delegate の gameCenterViewControllerDidFinish で dismiss を呼ぶのを忘れると、閉じるボタンが効かない画面が出来上がります。最初のテストで必ず踏む落とし穴なので、先に書いておきます。
実績は「累積」に使うと長持ちする
実績は percentComplete を 0〜100 で報告する仕組みです。単発の解除だけでなく、累積の進捗表示に使えるのが良いところです。
func reportAchievement ( id : String , percent : Double ) {
guard isAuthenticated else { return }
let achievement = GKAchievement ( identifier : id)
achievement.percentComplete = min (percent, 100 )
achievement.showsCompletionBanner = true
GKAchievement. report ([achievement]) { error in
if let error {
print ( "実績の報告に失敗しました: \( error. localizedDescription ) " )
}
}
}
「通算 100 ステージクリア」のような実績なら、クリアのたびに現在値を percent に換算して送るだけで、進捗バー付きの実績になります。同じ値を重ねて報告しても害はありませんが、毎フレーム送るような作りは避け、値が変わった時だけ報告します。
スコア改ざんとどう向き合うか
ここが本記事でいちばん書きたかった部分です。Game Center のスコアはクライアント申告制で、サーバー側の検証機構は用意されていません。つまり、改ざんを完全に防ぐ手段はないという前提から設計を始める必要があります。
それでも、個人開発の規模で現実的に効く緩和策はあります。
理論上の最大値でクリップする : そのゲームで物理的に到達し得るスコアの上限を計算し、超過分は送信前に弾きます。これだけで「9,999,999,999 点」のような明らかな異常値はリーダーボードから消えます
送信箇所をゲームロジックの内側に置く : スコア送信をリザルト処理の深部に置き、UI 層から直接呼べる形にしないだけでも、雑な改造への耐性が上がります
context にメタデータを載せる : submitScore の context にプレイ時間やステージ番号を畳み込んでおくと、後から異常値を分析する手がかりになります
上位帯を定期的に眺める : 週に一度、リーダーボード上位の分布を見るだけで、異常の混入にはすぐ気づけます
それ以上の強度が必要なら、Game Center と並行して自前 API に App Attest を組み合わせる構成になります。端末とアプリの正当性をサーバー側で検証する流れは App Attest と Play Integrity のサーバー検証 で詳しく書きました。
大切なのは目的を見失わないことです。守りたいのは「正直に遊んでいるプレイヤーの体験」であって、改ざん者との軍拡競争に勝つことではありません。上限クリップと定期監視だけでも、体感的なリーダーボードの健全さはかなり保てます。
導入して分かったこと
小さなゲームでの観測ですが、リーダーボードを開放してから翌日継続率が約 31% から 36% へ、5 ポイントほど改善しました。GKAccessPoint 経由でランキングを開くセッションは全体の約 12% でした。リザルト画面に「あと 120 点で 10 位以内」という一行を出すようにしてからは、連続プレイの回数も目に見えて伸びています。順位という比較軸は、それだけで再訪の理由になります。
ホーム画面からの再訪導線を増やしたい場合は、ControlWidget で起動導線を増やす設計 との組み合わせも効きます。ゲーム側の題材としては Metal による 3D ゲーム生成 も参考になるはずです。
まずはリーダーボード1本だけの構成で出してみてください。実績やカスタム UI は、プレイヤーがランキングを見てくれていると分かってからで十分です。最小構成なら、この記事のコードで今日中に動くところまで行けます。
お読みいただきありがとうございました。みなさまのゲームのリーダーボードが、正直なスコアで賑わうことを願っています。