私はZEN大学生のsamoyed130-zenです。主に情報系分野を履修しています。
Hanabee が「ZEN Study 動くWebページコンテスト2025 夏」で優秀賞をいただいたので、制作過程と工夫点を3本に分けてまとめています(Day3)。
-
Day3:バックエンド(GASでランキング)(この記事)
Day3では、Hanabeeの「オンラインランキング」部分を解説します。
サーバを立てずに、Google Apps Script(GAS)+スプレッドシートだけで完結させたのがポイントです。
「WebアプリのAPIって何?」「POSTとGETって何?」も簡単に補足します。
0. 結論:ランキングに必要なのは“保存”と“取得”の2つだけ
ランキング機能は要するに次の2つです。
- スコアを保存する(送信)
- 上位スコアを取得する(表示)
これをWeb上の「API」として提供すれば、
フロント(GitHub Pages)からアクセスできます。
Hanabeeでは GAS を Webアプリとしてデプロイして、APIの代わりにしました。
1. データ設計:スプレッドシートに4列だけ
スプレッドシートの1行を「1プレイ」として保存します。
-
t:時刻(同点時の並びや表示用) -
name:プレイヤー名 -
score:スコア -
maxCombo:最大コンボ(同点比較の材料)
GAS側でヘッダを固定しています。
const HEADERS = ['t','name','score','maxCombo'];
実際に保存される例(シート1)
シート1 には、実際には次のような形で行が追加されていきます。1行目はカラム名自体になります。
| t | name | score | maxCombo |
|---|---|---|---|
| 1756567042159 | samoyed130 | 27450 | 16 |
| 1756568215099 | samoyed130 | 34450 | 16 |
| 1756568577121 | samoyed130 | 10950 | 16 |
| 1756569348805 | samoyed130 | 25440 | 16 |
| 1756571221206 | samoyed130 | 16350 | 16 |
ポイント:
最初は“正規化”とか難しいことを考えず、必要最小限の列でスタートすると完成しやすいです。
2. GASの基本:doGet / doPost が入口になる
GASをWebアプリとして公開すると、
-
doGet(e):ブラウザからGETで来たときに呼ばれる -
doPost(e):フォームなどからPOSTで来たときに呼ばれる
というルールで動きます。
Hanabeeでは、
-
GET:ランキング取得(
?limit=10) - POST:スコア追加(フォームPOST)
にしています。
3. 実装(保存側):スコアをappendRowで追加
gas/leaderboard.gs の doPost が保存担当です。
フォームから送られてきた name/score/maxCombo を受け取り、1行追加します。
function doPost(e) {
const p = e && e.parameter ? e.parameter : {};
const t = Number(p.t || Date.now());
const name = String(p.name || 'YOU').slice(0,16);
const score = Number(p.score || 0) | 0;
const maxCombo = Number(p.maxCombo || 0) | 0;
const sh = getSheet_();
// 同時書き込み対策(最低限)
const lock = LockService.getScriptLock();
lock.tryLock(3000);
sh.appendRow([t, name, score, maxCombo]);
lock.releaseLock();
return ContentService
.createTextOutput(JSON.stringify({ ok:true }))
.setMimeType(ContentService.MimeType.JSON);
}
なぜLockServiceを使う?
複数人が同時に送信すると、
まれに書き込み競合が起きることがあります。
LockService は「順番に書き込む」ための簡易ロックです。
補足:
“同時に来ても壊れない”ように、少しだけ安全対策を入れたイメージです。
4. 実装(取得側):上位だけ返す(limit付き)
doGet はランキング取得です。
スプレッドシート全件を読み、並び替えて上位だけ返します。
function doGet(e) {
const limit = Math.max(1, Math.min(50, Number(e && e.parameter && e.parameter.limit || 10)));
const sh = getSheet_();
const last = sh.getLastRow();
let items = [];
if (last >= 2) {
const values = sh.getRange(2,1,last-1,HEADERS.length).getValues();
items = values.map(r => ({
t:Number(r[0]),
name:String(r[1]),
score:Number(r[2])|0,
maxCombo:Number(r[3])|0
}));
// score降順 → maxCombo降順 → 古い順
items.sort((a,b)=> (b.score-a.score) || (b.maxCombo-a.maxCombo) || (a.t-b.t));
items = items.slice(0, limit);
}
return ContentService
.createTextOutput(JSON.stringify({ ok:true, items }))
.setMimeType(ContentService.MimeType.JSON);
}
limitを制限している理由
用途的には「上位10件だけ」で十分です。
無制限にすると処理が重くなりやすいので、最大50に制限しています。
5. ちょい工夫:ヘッダ行を自動で整える
スプレッドシートは、人が手で編集すると壊れやすいです。
そこで getSheet_() で、
- シートが無ければ作る
- ヘッダ行がズレてたら直す
を自動化しました。
function getSheet_() {
const ss = SpreadsheetApp.openById(SHEET_ID);
const sh = ss.getSheetByName(SHEET_NAME) || ss.insertSheet(SHEET_NAME);
const rng = sh.getRange(1,1,1,HEADERS.length);
const values = rng.getValues()[0];
let changed = false;
for (let i=0;i<HEADERS.length;i++){
if (values[i] !== HEADERS[i]) { values[i] = HEADERS[i]; changed = true; }
}
if (changed) rng.setValues([values]);
return sh;
}
意図:
「動かない原因が“シートが無い/ヘッダが違う”」みたいな初歩で詰まるのを防ぎます。
6. フロントからの呼び出し:フォームPOSTで送る(CORS対策)
CORSって何?(超ざっくり)
ブラウザには「同一オリジン制約」という安全のためのルールがあり、
Webページ(例:GitHub Pages)から別ドメインのAPI(例:GAS)を直接叩くときは、相手サーバが「この呼び出しを許可します」と明示しないとブロックされます。
この“別ドメイン呼び出しを許可するための仕組み”が CORS(Cross-Origin Resource Sharing) です。
また、送信ヘッダや Content-Type の種類によっては、ブラウザが本リクエストの前に「このリクエスト送っていい?」という確認(プリフライトリクエスト)を自動で送ります。
ここで許可が出ないと、フロント側はエラーになります。
ブラウザから別ドメイン(GAS)に fetch する場合、
Content-Type: application/json などにすると CORSのプリフライトが発生して詰まりがちです。
そこで Hanabee は、
URLSearchParams を使った フォームPOST で送っています。
// scripts/main.js より(送信)
const form = new URLSearchParams();
form.append("t", Date.now());
form.append("name", name);
form.append("score", score);
form.append("maxCombo", maxCombo);
await fetch(LB_URL, { method: "POST", body: form });
取得はシンプルにGET。
// scripts/main.js より(取得)
const url = `${LB_URL}?limit=10&t=${Date.now()}`; // cache-buster
const data = await getJSON(url);
7. 失敗しても遊べる:localStorageにフォールバック
オンラインランキングは、ネットワークやGAS側の不調で失敗する可能性があります。
でもゲーム自体は止めたくない。
そこで、
- 送信失敗 → ローカルランキングへ保存
- 取得失敗 → ローカルランキングを表示
というフォールバックを入れています。
また、ローカル環境で動作確認するとき(例:index.html を直接開く、簡易サーバで配信するなど)に通信がうまくいかなくても、ランキング表示まわりの挙動を localStorage で確認できるようにしています。LB_URL を空にしておけば、最初からローカルランキングだけで動くので検証が楽です。
async function submitScoreRemote(name, score, maxCombo){
if(!LB_URL){
addToLeaderboard(name, score, maxCombo);
return { ok:true, remote:false };
}
try{
await fetch(LB_URL, { method: "POST", body: form });
return { ok:true, remote:true };
}catch(e){
addToLeaderboard(name, score, maxCombo);
return { ok:false, remote:false, error:String(e) };
}
}
考え方:
「ネット機能は壊れても、ゲームが壊れない」を優先すると完成度が上がります。
8. デプロイ手順(GAS側)
ざっくり流れは以下です。
- スプレッドシートを用意する
- Googleドライブで新規作成します(名前は何でもOKです)
- スプレッドシートのURLが
https://docs.google.com/spreadsheets/d/<ここがID>/edit...になっているので、/d/と/editの間をSHEET_IDに入れます - ランキング用のタブ名(例:
シート1)をSHEET_NAMEに入れます
-
拡張機能 → Apps Scriptを開く -
gas/leaderboard.gsを貼り付ける -
SHEET_IDとSHEET_NAMEを設定する - (初回のみ)実行して権限を承認する
- GASは最初の実行時に「スプレッドシートへのアクセス」などの承認が必要です
-
doGet/doPostを一度実行して、指示に従って許可します
-
デプロイ → 新しいデプロイ → ウェブアプリ- 実行ユーザー:自分(スプレッドシートへの書き込み権限を確実にするため)
- アクセス:全員(匿名アクセスを許可したい場合)/ もしくは「Googleアカウントを持つ全員」など、目的に合うもの
- 発行された
/execのURLをscripts/main.jsのLB_URLに貼る
const LB_URL = 'https://script.google.com/macros/s/......../exec';
スプレッドシート側の公開設定はどうする?
基本的には、スプレッドシート自体は非公開のままで大丈夫です。
Hanabeeの構成だと、スプレッドシートを直接公開するのではなく、GASを「実行ユーザー:自分」で動かしてスプレッドシートを読み書きし、その結果だけをJSONで返します。
一方で、共同作業したい場合はスプレッドシートを編集者として共有してもOKです(ただし、Webアプリとして公開するのはGASなので、公開範囲の本体はデプロイ設定になります)。
注意:Webアプリの「アクセス:全員」にすると、URLを知っている人なら誰でも投稿できる状態になります。コンテスト用途として割り切るならシンプルで運用が楽ですが、荒らし対策までやるなら別の認証設計が必要です。
9. セキュリティ/改ざんについて(現実ライン)
正直、ブラウザゲームのスコアは改ざんできます。
(DevToolsで score=99999999 にして送れてしまう)
Hanabeeでは「授業/コンテスト想定の簡易ランキング」として、
- 送信形式をフォームPOSTにして運用を安定
- 同時書き込みをLockでガード
- 失敗時フォールバックでUXを守る
を優先しました。
もし本格的に防ぐなら、
サーバ側でリプレイ検証(操作ログを送る)などが必要になります。
ただし難易度が跳ね上がるので、まず“作り切る”のが勝ちです。
まとめ
- GASは「小さなAPI」を作るのに最強(サーバ不要)
-
doGet/doPostでランキングの取得・保存ができる - スプレッドシートは最小4列で十分
- 失敗しても遊べるフォールバックが完成度を上げる
おつかれさまでした!
Day1〜Day3を通して、再現しやすい作り方を意識して書きました。