Unity / Godot SDK ガイド
GameRush のゲームから、プレイヤーの疑似ID・ランキング・公開プレイヤー状態・リアルタイムマルチプレイを扱うための SDK です。Unity には UPM パッケージ、Godot 4 にはアドオンを配布しています。
ここで扱うのは、書き出したゲームから GameRush の API を呼ぶ方法です。どのエンジンからどうやって GameRush に載る形を書き出すかは https://gamerush.cc/engines にあります。
1. 共通の約束
入力の検証・再送・通信方式の選択・プロトコル差分の吸収は GameRush 側のランタイムが持っていて、SDK が持つのは型のついた呼び出しと待ち合わせだけです。そのため、作者が SDK を更新しなくても GameRush 側の改善はそのまま既存のビルドへ届きます。
SDK の更新が要るのは、新しい API を使いたいときだけです。
- 例外を投げません。すべての呼び出しが結果オブジェクトを返します。GameRush の外(作者の自サイトなど)で動かした場合も unsupported を返して静かに縮退します。
- 起動時に protocolVersion を読みます。SDK が要求する版数を満たさない環境では、その機能を無効にします。
- 知らないイベントは黙って捨てます。ランタイムが SDK より新しいことは常に起こり、close の reason のように既知のイベントへ新しい値が増えることもあります。
- メッセージは 8KB 上限です。WebRTC のデータチャネルの実用上限に合わせてあり、緩める予定はありません。
- channel と宛先は必須の概念です。いまの WebSocket 中継では unreliable も落ちずに順序どおり届きますが、将来 WebRTC へ切り替わると本当に落ちて並び替わります。落ちる前提で書いてください。
- join は置き換えです。部屋にいる状態で join を呼ぶと、先に現在の部屋が閉じて close(reason は replaced)が届き、それから新しい部屋を取りに行きます。join が失敗しても元の部屋には戻らないので、続けたいときは close を受けてから join し直してください。
2. Unity
導入
Package Manager の Add package from git URL... に https://github.com/gamerush-cc/grush-sdk-unity.git を入れてください。Unity 2021.3 以降、ビルドターゲットは WebGL です。パッケージ識別子は cc.gamerush.sdk です。
入れると GameRush メニューが増え、Unity から出ずにログインしてビルドを上げられます(「7. Unity Editor からアップロードする」)。
API
using GRushSdk;
var self = await GRush.Player.GetSelfAsync();
if (self.Ok) Debug.Log(self.Value.PseudoId);
var joined = await GRush.Net.JoinAsync("duel");
if (!joined.Ok) return;
var room = joined.Value;
room.Message += message =>
{
if (message.IsStale) return;
Apply(message.From, message.Payload);
};
room.PeerJoined += peer => Debug.Log(peer.Index);
room.TransportChanged += transport => Debug.Log(transport);
room.Closed += reason => Debug.Log(reason);
room.Send(bytes, GRushChannel.Unreliable, GRushRoom.Everyone);
var now = room.ServerTimeMs();
await room.LeaveAsync();| 型 | 内容 |
|---|---|
GRushResult<T> | Ok / Value / Code(GRushErrorCode)/ Message |
GRushPlayer | PseudoId / IsGuest / ProfileConsent / DisplayName / AvatarUrl |
GRushPeer | Index / PseudoId / DisplayName / AvatarUrl(表示名とアイコンは、その相手が同意済みのときだけ入る。未同意とゲストでは null) |
GRushMessage | From / Channel / Seq / Payload / IsStale |
GRushRoom | LocalPeerId / HostPeerId / IsHost / Peers / Stats.Transport / RoomCode / RoomEpoch |
JS からのコールバックは次の Update で発火します。イベントハンドラの中は Unity のメインスレッドなので、そのまま Unity の API を触って構いません。
3. Godot 4
導入
https://github.com/gamerush-cc/grush-sdk-godot の addons/grush_sdk/ をプロジェクトの addons/ へコピーし、Project Settings > Plugins で有効にしてください。autoload シングルトン GRush が自動登録されます。書き出しは Web です。
API
var self_result: Dictionary = await GRush.player.get_self()
if self_result["ok"]:
print(self_result["value"]["pseudo_id"])
var joined: Dictionary = await GRush.net.join("duel")
if not joined["ok"]:
return
var room: GRushRoom = joined["value"]
room.message_received.connect(func(message: Dictionary) -> void:
if message["is_stale"]:
return
apply(message["from"], message["payload"]))
room.peer_joined.connect(func(peer: Dictionary) -> void: print(peer["index"]))
room.transport_changed.connect(func(transport: String) -> void: print(transport))
room.closed.connect(func(reason: String) -> void: print(reason))
room.send(bytes, GRush.CHANNEL_UNRELIABLE, GRush.EVERYONE)
var now := room.server_time_ms()
await room.leave()結果はすべて {"ok": bool, "value": Variant, "code": String, "message": String} の形で返ります。Unity 版と同じ形を GDScript の慣習(snake_case・Dictionary・signal)に置いたものです。
4. ランキング
ランキングの定義は作者が宣言するもので、ゲームからは作れません。
ゲームからできるのは、宣言済みの枠へ投稿することと読むことだけです。宣言は Studio の画面からでも API からでもできます(「6. API トークン」)。
スコアはゲームからの自己申告値で、GameRush は値を検証していません。
応答の Verified / verified は常に false です。これをゲーム内で表示する義務はありません — 使いたいときのために値を渡しているだけで、GameRush 側の画面では GameRush が自分で明示します。
entry の DisplayName / AvatarUrl は、その相手が同意済みのときだけ入ります。
// Unity
var boards = await GRush.Leaderboards.ListAsync();
var posted = await GRush.Leaderboards.SubmitAsync("high_score", 12000);
var page = await GRush.Leaderboards.TopAsync("high_score", 20);
var near = await GRush.Leaderboards.AroundMeAsync("high_score", 5);
foreach (var entry in page.Value.Entries)
{
var label = entry.IsGuest ? "ゲスト" : (entry.DisplayName ?? "匿名");
}# Godot
var boards: Dictionary = await GRush.leaderboards.list()
var posted: Dictionary = await GRush.leaderboards.submit("high_score", 12000)
var page: Dictionary = await GRush.leaderboards.top("high_score", {"limit": 20})
var near: Dictionary = await GRush.leaderboards.around_me("high_score", {"range": 5})| 投稿が弾かれる条件 | HTTP | 返る GRushErrorCode |
|---|---|---|
| 宣言されていない key / 停止中の枠 | 404 | InvalidParams |
| 作者が宣言した値域の外 | 400 | InvalidParams |
| 値域を宣言していない枠 | 400 | InvalidParams |
| 有効プレイ 10 秒未満のセッション | 400 | InvalidParams |
| session が見つからない / 別ゲーム・別プレイヤーの session | 403 | ConsentDeclined |
| session あたり・毎分あたりの投稿上限 | 429 | RateLimited |
| ホストに再生中のセッションが無い(投稿がサーバへ届かない) | — | Unavailable |
ConsentDeclined は session が拒否されたときと FriendsAsync / friends の同意拒否のときの両方で返り、コードもメッセージも同じなので結果からは区別できません。投稿の失敗を Code だけで一括に捌くと、切れたセッションを同意拒否と誤診します。
session はゲームが指定できません。信頼済みの親 UI が live なセッションを付け、そのセッションがこのゲーム・この相手のものかをサーバが検証します。
AroundMeAsync / around_me は、自分がまだ載っていないときに空の一覧を返します。先頭を返すとゲーム側が「自分は1位」と誤読するためです。
FriendsAsync / friends は相互フォローに絞った一覧ですが、ゲームごとのフレンド同意がまだ無いため、現在は常に ConsentDeclined を返します。
エントリの metadata は、Godot はそのまま辞書で返しますが Unity は返しません。metadata が要る Unity のゲームは、応答の生 JSON である GRushLeaderboardPage.RawJson を自前のパーサへ渡してください。
集約が sum のランキングを再送するときは識別子を渡す
SubmitAsync(key, value, metadataJson, operationId) / submit(key, value, metadata, operation_id) の第4引数は、同じ投稿の再送を弾くための識別子です。
加算は冪等ではありません。
投稿がサーバに届いて保存された後で応答だけが失われた場合、そのまま送り直すと二重に足され、集約後の値からは元へ戻せません。再送するなら同じ operationId を渡してください。best と last は同じ値を再適用しても結果が変わらないので、省略して構いません。
5. 公開プレイヤー状態
他プレイヤーへ見せる進捗(称号・ひとことなど)の入れ物です。クラウドセーブとは別で、本人だけが読む非公開データはこの経路から一切読めません。
ここに置いたものは他プレイヤーへ見えるユーザー生成コンテンツで、通報とモデレーションの対象になります。そのため payload に制約があります。
| 制約 | 理由 |
|---|---|
| JSON オブジェクトに限る(裸の文字列・配列は不可) | 実質バイナリの詰め込み場所になる |
| 4KB 以下 | — |
| base64 のような読めない長い文字列を含まない | 通報されたとき人間が読めないとモデレーションが成立しない |
Unity の payload は生の JSON 文字列、Godot は辞書です。
// Unity
await GRush.PlayerState.SetMineAsync("{\"title\":\"初心者\",\"level\":3}");
var mine = await GRush.PlayerState.GetMineAsync();
var others = await GRush.PlayerState.GetAsync(new[] { pseudoIdA, pseudoIdB });# Godot
await GRush.player_state.set_mine({"title": "初心者", "level": 3})
var mine: Dictionary = await GRush.player_state.get_mine()
var others: Dictionary = await GRush.player_state.get_many([pseudo_id_a, pseudo_id_b])Godot 側だけメソッド名が get_many なのは、get が Godot 側の Object.get と衝突してパースエラーになるためです。通信上のメソッド名は Unity と同じ playerState.get です。
運営が伏せた状態は返りません。要求した疑似IDの数だけ返ってくる前提で書かないでください。エディタのモックでも GRushMockPlayerStates.Define(id, json, hidden: true) / define(id, payload, true) で同じ挙動を試せます。
SetMine に baseRevision を渡すと楽観ロックになり、他所で更新されていれば InvalidParams で戻ります。省略すれば常に上書きします。
6. API トークン
ランキングを画面で1つずつ足すのが面倒なときは、Studio の「API トークン」から資格情報を発行して HTTP で宣言できます。CI から流しても構いません。
トークンをゲームのビルドへ入れないでください。ビルドに入れたものは公開したのと同じで、出回った後は取り消せません。
置き場所はエンジンプロジェクトの外側 — 手元の環境変数か CI のシークレットです。
- GameRush Studio の「API トークン」から発行する
- 権限は要るものだけ選ぶ(宣言だけなら leaderboards:read と leaderboards:write。ゲーム ID を調べるなら games:read)
- 対象のゲームを絞れる。絞ったトークンを他のゲームへ向けると 404 になる
- 平文が出るのは発行直後の一度だけ。失くしたら失効させて出し直す
curl -X PUT "https://gamerush.cc/api/games/$GAME_ID/leaderboards/high_score" \
-H "Authorization: Bearer $GRUSH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"ハイスコア","sort":"desc","valueType":"int","aggregation":"best","period":"all_time","minValue":0,"maxValue":1000000}'PUT は key での冪等な適用です。無ければ作り、あって内容が違えば更新し、同じ内容なら何もしません。同じコマンドを何度流しても結果は変わらないので、宣言をリポジトリのスクリプトに置いて毎回流して構いません。
- 一覧は GET /api/games/$GAME_ID/leaderboards、ゲーム ID は GET /api/games で取れる
- 投稿が1件でもある枠では sort / valueType / aggregation / period を変えられない(409)。保存済みの値の意味が後から変わるため。変えたいときは別の key を作る。同じ値を流し直す分は変更ではないので通る
- 値域(minValue / maxValue)は両方揃って初めて意味を持つ。宣言が無い枠は投稿を受け付けない
自分のツールから取る(loopback 承認)
Unity を使うなら自分で実装する必要はありません — 「7. Unity Editor からアップロードする」の Editor 拡張がこの手順を持っています。ここは自前の CLI やエディタ拡張を書く人向けです。
手元でループバックのサーバを立てて /studio/authorize を開き、承認後に戻ってきたコードを交換します(RFC 8252 + PKCE)。
- redirect_uri は http://127.0.0.1:<port> / http://localhost:<port> / http://[::1]:<port> だけ。ポートは任意で、クエリもフラグメントも付けられない
- code_challenge_method は S256 のみ。code_verifier は 43〜128 文字
- コードはワンタイムで 5 分。code_verifier を間違えた時点でそのコードは死ぬので、失敗したら承認からやり直す
- こうして取ったトークンは Studio の一覧に発行元のクライアント名つきで並ぶので、手発行と見分けられる
- games:create はゲームの許可リストと併用できない(発行時に 400、使用時に 403)。まだ無いゲームは許可リストに載せられないため
7. Unity Editor からアップロードする
Unity から出ずに、ログインからビルドのアップロード、ランキングの宣言まで通せます。
「6. API トークン」のトークンを手で貼る必要はありません。Editor がブラウザ承認で自分で取ります。
| GameRush メニュー | 内容 |
|---|---|
| GameRush ウィンドウ | アップロードとランキング宣言の画面 |
| ログイン | 既定のブラウザで承認画面を開き、戻ってきたコードをトークンへ交換する |
| ログアウト | 保存したトークンを消す |
トークンはプロジェクトの外(%APPDATA%\GameRush\ / ~/Library/Application Support/GameRush/ / $XDG_CONFIG_HOME/GameRush/)に置きます。Assets/ にも EditorPrefs にも書かないので、リポジトリにもプロジェクト設定にも漏れません。Editor 側のコードは Editor 専用のアセンブリに入っており、書き出したビルドにはコードもトークンも含まれません。
アップロードの手順
- Unity で WebGL に書き出す(index.html が直下にあること)
- GameRush ウィンドウでゲームを選ぶ。新規作成もできる(games:create を要求したトークンが要る)
- 書き出しディレクトリを選ぶと、上げる前に手元で検査する — 拡張子・パス・ファイル数・総量がサーバと同じ規則で見られるので、2000 ファイル上げ切ってから弾かれることがない
- アップロードする(進捗が出る。途中で止められる。失敗したファイルだけ再送する)
新規作成のときだけ可視性を選べます(private か unlisted)。既存ゲームの公開設定は Studio で変えてください。
unlisted も審査を通ります。アップロードが終わった時点では審査待ちで、すぐ遊べる状態にはなりません。
覚えておくこと
- Brotli / gzip で書き出したファイルはそのまま上げる。Build/x.wasm.br は .br が付いた名前のまま登録され、配信側がその名前で Content-Encoding を決める
- 作成の失敗を機械的に再試行しない。ゲームとビルドの作成には冪等キーが無く、再試行が2つ目を作る(個別ファイルの送り直しは安全)
- Editor 拡張が古いとサーバが操作を止める。止まったらパッケージを更新する
8. エディタでの動作確認
WebGL / Web 書き出し以外では、SDK は自動的にモックへ落ちます。これが無いとエディタで一切テストできないため、モックは付属品ではなく SDK 本体の一部として扱っています。
| 切り替え | Unity | Godot |
|---|---|---|
| サインイン状態 | GRushMock.SignedIn | GRushMock.signed_in |
| 表示名 | GRushMock.DisplayName | GRushMock.display_name |
| 同意ダイアログの結果 | GRushMock.GrantProfileConsent | GRushMock.grant_profile_consent |
| unreliable の欠落率 | GRushMock.UnreliableDropRate | GRushMock.unreliable_drop_rate |
| 2人目の peer | GRushMock.AddPeer(name) | GRush.mock_add_peer(name) |
| ランキングの枠 | GRushMockLeaderboards.Define(key, title, …) | GRushMockLeaderboards.define(key, title, …) |
| ランキングの他プレイヤー | GRushMockLeaderboards.AddRival(key, name, value) | GRushMockLeaderboards.add_rival(key, name, value) |
| 他プレイヤーの公開状態 | GRushMockPlayerStates.Define(id, json, hidden) | GRushMockPlayerStates.define(id, payload, hidden) |
AddPeer / mock_add_peer が返す相手は、同じプロセス内の2人目の peer として実際に部屋へ入り、送受信が往復します。実機やサーバへ繋がずに2人分の挙動を確認できます。
ランキングのモックが再現するのは、実サーバの縛りのうち「値域」と「集約」だけです。
session 束縛と有効プレイ 10 秒はエディタでは意味を持たないので省いてあります。つまりエディタで通った投稿が実環境で 400 / 403 になることはあるので、投稿の失敗経路はゲーム側で必ず扱ってください。
UnreliableDropRate は既定 0 ですが、出荷前に必ず 0 より大きくして試してください。
本番の WebSocket 中継は unreliable を落とさないため、パケットが落ちる前提で書けているかを確かめられる場所はここしかありません。確認を怠ったゲームは、WebRTC へ切り替わった時点で初めて壊れます。
9. サンプル
各エンジン2本です。どちらもシーンアセットを持たないので、空のシーンにノード(Unity は GameObject、Godot は Control)を1つ作ってスクリプトを付けてください。
| サンプル | 内容 |
|---|---|
| Score Attack | 疑似IDの取得と表示名の同意要求。ベストスコアはローカル保存 |
| Duel | 2人対戦。paddle は unreliable、ボールはホストが送る。エディタではモックの相手が動く |
ランキングと公開プレイヤー状態はサンプルではまだ使っていないので、「4. ランキング」「5. 公開プレイヤー状態」の API を直接呼んでください。