1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hanabee 制作記(Day3)バックエンド:GASでオンラインランキングを作る

Last updated at Posted at 2025-12-17

私はZEN大学生のsamoyed130-zenです。主に情報系分野を履修しています。

Hanabee が「ZEN Study 動くWebページコンテスト2025 夏」で優秀賞をいただいたので、制作過程と工夫点を3本に分けてまとめています(Day3)。

Day3では、Hanabeeの「オンラインランキング」部分を解説します。
サーバを立てずに、Google Apps Script(GAS)+スプレッドシートだけで完結させたのがポイントです。

「WebアプリのAPIって何?」「POSTとGETって何?」も簡単に補足します。


0. 結論:ランキングに必要なのは“保存”と“取得”の2つだけ

ランキング機能は要するに次の2つです。

  1. スコアを保存する(送信)
  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.gsdoPost が保存担当です。
フォームから送られてきた 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側)

ざっくり流れは以下です。

  1. スプレッドシートを用意する
    • Googleドライブで新規作成します(名前は何でもOKです)
    • スプレッドシートのURLが https://docs.google.com/spreadsheets/d/<ここがID>/edit... になっているので、/d//edit の間を SHEET_ID に入れます
    • ランキング用のタブ名(例:シート1)を SHEET_NAME に入れます
  2. 拡張機能 → Apps Script を開く
  3. gas/leaderboard.gs を貼り付ける
  4. SHEET_IDSHEET_NAME を設定する
  5. (初回のみ)実行して権限を承認する
    • GASは最初の実行時に「スプレッドシートへのアクセス」などの承認が必要です
    • doGet / doPost を一度実行して、指示に従って許可します
  6. デプロイ → 新しいデプロイ → ウェブアプリ
    • 実行ユーザー:自分(スプレッドシートへの書き込み権限を確実にするため)
    • アクセス:全員(匿名アクセスを許可したい場合)/ もしくは「Googleアカウントを持つ全員」など、目的に合うもの
  7. 発行された /exec のURLを scripts/main.jsLB_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を通して、再現しやすい作り方を意識して書きました。


リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?