AIゲーム制作ラボ
公開・収益化7分で読めます

Unity WebGLゲームにランキングを付ける(不正対策と送る条件)

ブラウザで動くゲームから送られる記録は、いくらでも書き換えられます。弾くのではなく「ありえない値は保存して無効にする」設計と、送るのは自己ベスト更新時だけという条件、EventSystem が無いシーンで名前を入力させる方法を、実装と実測つきで残します。

Libra

公開 2026年9月18日

この記事の要点4 points

  1. ブラウザから送られる値は信用できない。 弾くのではなく、保存したうえで無効印を付ける。無効の理由も残す。
  2. 「ありえない値」の線引きは、ゲームの数値から計算する。 このゲームなら登る速さの上限から高さの上限が出る。
  3. 送るのは自己ベストを更新して50点以上のときだけ 毎回送ると、短い記録で表が埋まる。
  4. 名前は3文字の選択式にした。 WebGL のスマホでは文字入力が出せず、画面に日本語フォントも積んでいないため。
目次7
検証した環境最終確認 2026年9月18日
  • Unity 6.6(6000.6.0f1)・WebGL ビルド
  • Next.js 16 / PostgreSQL 18(同じドメインで配信)
  • Claude Code(Opus 5 / 思考レベル 高)

書いてあるのは、この環境で実際に試した結果です。ツールの仕様や料金は変わるので、重要な判断の前には公式の情報も確認してください。

自作のゲームにランキングを付けました。ブラウザで動くゲームから送られてくる値は、いくらでも書き換えられます。 それを前提に、どこまでやるかを決めた記録です。

5件/分

同じ相手からの上限

1時間なら60件

12字

名前の長さ

ASCII のみ・ゲーム内は3文字

50点

送る下限

自己ベスト更新時のみ

71点

通しの確認で送った記録

自動操作で実際に遊んだ

弾かずに「保存して無効にする」

最初に決めたのはここです。ありえない値が来たとき、エラーを返して捨てるのではなく、保存したうえで無効の印を付けます。

弾いて捨てる

実装は簡単
どんな値が来たのか分からない
上限の式が厳しすぎたとき、正当な記録を落としたことに気づけない

保存して無効にする(採用)

無効の理由も一緒に残す
あとで「どこで切ったか」を見直せる
表示するのは有効な記録だけなので、見た目は同じ

返すのは 200 のままで、accepted: false と理由を返します。ゲーム側は「ランキングに載らなかった」とだけ出せば済みます。

ありえない値を送ったときERROR
$ curl -X POST … -d '{"name":"CHEAT","score":999999,"playMs":5000,
  "detail":{"heightM":9000,"coins":500}}'
{"ok":true,"accepted":false,"reason":"高さ 9000m は 5秒では届かない"}

「ありえない値」は、ゲームの数値から計算する

線引きは勘ではなく、そのゲームの数値から出します。 このゲームは壁を跳んで登るので、登る速さの上限が決まっています。跳び続けても毎秒 5.5m を超えられません。

const seconds = input.playMs / 1000
if (height > seconds * 5.5 + 30) return `高さ ${height}m は ${Math.round(seconds)}秒では届かない`
if (coins > seconds * 2 + 10) return `コイン ${coins}枚は ${Math.round(seconds)}秒では取れない`
if (input.score > height + coins * 150 + 50) return `スコア ${input.score} は 高さ${height}m・コイン${coins}枚では出ない`

余裕(+30+10)を足しているのは、正当な記録を落とさないためです。ここを攻めても不正は止まりません(上限ぎりぎりの値を送られたら通ります)。目的は「桁違いの値を表から外す」ことだけです。

⚠️ ゲームの数値を変えたら、この式も見直す

遊びの数値(跳ぶ速さ・コインの点数)を変えると、上限も変わります。式はゲームごとに1か所へ置き、ゲーム側の数値と対応していることをコメントに書いておくのが現実的でした。

残りの3つ:連投・送信元・名前

  1. 1

    連投を止める

    同じ相手から 1分5件・1時間60件 まで。超えたら 429 を返す
  2. 2

    送信元を確かめる

    他サイトからの POST は Origin で拒否する。ゲームは同じドメインから配信しているので、これで困らない
  3. 3

    名前を絞る

    ASCII の英数字と _ . - と空白だけ・12字まで。HTML も URL も入らない。NG ワードは既定の名前に置き換える

IP アドレスは保存していません。連投の判定に使うのは HMAC でハッシュにした値だけです。

送るのは「自己ベストを更新したときだけ」

サーバー側の対策より、こちらのほうが表の見た目に効きました。

プレイのたびに送ると、数秒で終わった記録まで表に並びます。 このゲームは1プレイが30〜90秒と短く、失敗も多いので、そのままだと大半が低い記録になります。

そこで、送信は「自己ベストを更新して、かつ50点以上」のときだけにしました。結果として、送信の回数も1プレイごとではなく、上達したときだけになります。

// 🔴 送るのは自己ベストを更新したときだけ(短い記録で表を埋めない)
if (_isNewBest && Session.Score >= 50) SendScore();

エディターからは送らない

作っている最中に何度も遊ぶので、エディターから本番の表に試し撃ちが残ると面倒です。送信のコードは #if UNITY_EDITOR で切り、エディターではログに出すだけにしました。

EventSystem の無いシーンで、名前を入力させる

このプロジェクトのシーンには EventSystem がありません(入力は自分で読んでいるため、外してあります)。つまり uGUI のボタンは押せません。

名前の入力をどうするかで、2つ決めました。

  1. 3文字を + / - で選ぶ形にする。 WebGL のスマホでは文字入力が期待どおりに出ず、画面に日本語フォントも積んでいないためです
  2. 押した場所が、その文字の枠に入っているかを自分で判定する
bool Hit(Text text, Vector2 screenPoint)
{
    if (text == null || !text.gameObject.activeInHierarchy) return false;
    return RectTransformUtility.RectangleContainsScreenPoint(text.rectTransform, screenPoint, _camera);
}

押した座標は、新しい Input System から取ります(Input.mousePosition は使えません)。タイトルでのタップは、先に名前のボタンを見て、当たっていなければゲームを始めるという順番にしました。

うまくいかない

  • uGUI の Button を置く(EventSystem が無いので押せない)
  • タップでいきなりゲームが始まり、名前を変えられない
  • 文字入力を出す(スマホの WebGL で出ない)

この形にした

  • 文字の枠に当たっているかを自分で判定する
  • 名前のボタンに当たったときはゲームを始めない
  • 3文字を選ぶだけ。英数字なので追加のフォントも要らない

通しの確認は、自動で遊ばせて行った

人が遊んで確かめると、失敗したときに何が起きたか分かりません。自動操作のブラウザで実際に遊ばせて、送信まで通ることを確かめました。

自動操作で遊んだときの通信
{ "method": "GET",  "status": 200, "body": "{\"scores\":[]}" }
{ "method": "POST", "status": 200, "body": "{\"ok\":true,\"accepted\":true,\"rank\":1}" }
{ "method": "GET",  "status": 200, "body": "…\"name\":\"AAA\",\"score\":71…" }

画面には RANKED #1 が出て、紹介ページの一覧にも並びました。確認に使った記録は、そのあと消しています。

ゲームオーバーの画面。スコア71・31m・コイン2・NEW BEST! の下に RANKED #1 と表示されている
自動操作で遊んだときのゲームオーバー画面。送信が通ると、その場で順位が出る

まとめ

  • ブラウザゲームのランキングで不正は防げない。ありえない値を保存して無効にする、連投を止める、ページに「参考記録」と書く
  • 「ありえない」の線引きはゲームの数値から計算する。余裕を持たせて、正当な記録を落とさない
  • 送るのは自己ベスト更新時だけ。表の中身と送信の回数が、これだけで変わる
  • 作っている最中の試し撃ちが本番に残らないよう、エディターからは送らない
  • EventSystem が無いなら、押した座標が枠に入っているかを自分で判定すればボタンは作れる

FAQ

よくある質問

ブラウザゲームのランキングで不正は防げますか?

防げません。ゲームはブラウザの中で動いているので、送信の中身は書き換えられます。できるのは、ありえない値を無効にすること、連投を止めること、そしてページに「参考記録」と書いて期待値を下げることです。

不正な記録は削除したほうがいいですか?

消さずに、無効印(is_valid = false)と理由を付けて残しています。どんな値が来たのかが分かると、上限の式を直すときの材料になります。表示するのは有効な記録だけです。

スコアはいつ送るべきですか?

このゲームでは、自己ベストを更新して50点以上のときだけにしました。プレイのたびに送ると、数秒で終わった記録まで表に並び、サーバーへの送信も無駄に増えます。

Unity の WebGL で名前を入力させるには?

スマホのブラウザでは文字入力(TouchScreenKeyboard)が期待どおりに出ません。3文字を + / - で選ぶ昔ながらの形にすると、キーボードが無くても入力でき、日本語フォントも要りません。

この記事は役に立ちましたか?

NEXT