こんにちは、AI で作ったゲームの投稿サイト「Asobiai」の案内役、アソビです。
AI に頼めばブラウザゲームはすぐできるけど、ランキングやセーブ、オンライン対戦を付けようとすると、サーバーとログインの仕組みが必要になっちゃうよね。
Asobiai では、公開したゲームに window.AIGame っていう小さな SDK が自動で入るんだ。インストールはいらなくて、ゲームの中から関数を呼ぶだけで、ランキング・セーブ・トロフィー・みんなの集計・オンライン対戦が使えるよ。この記事では、その使い方をまとめるね。
呼ぶときは ?. を付ける
window.AIGame があるのは、Asobiai から配信されているときだけ。手元で HTML を直接開いたときにはないから、window.AIGame?.submitScore(score) みたいに ?. を付けて呼んでね。こうしておけば、手元でもエラーにならずに遊べるよ。
SDK のバージョンは window.AIGame?.version でわかるよ(この記事を書いた時点では "2.4.0")。
ランキング
ゲームオーバーのときに submitScore を呼ぶと、ゲームのページにランキングが出るよ。記録されるのは、ログインしているプレイヤーそれぞれの自己ベストなんだ。
// ゲームオーバーのとき
window.AIGame?.submitScore(score);
// タイム(小さいほど上位)を記録する場合
window.AIGame?.submitScore(seconds, { order: "asc", label: "秒" });
// ランキングを複数持つ場合
window.AIGame?.submitScore(score, { board: "hard", label: "ハード" });
// 結果を受け取る
const result = await window.AIGame?.submitScore(score);
// => { ok: true, rank: 3, best: 1200 } / { ok: false, reason: "login_required" }
ランキング(board)は1つのゲームに5つまで持てるよ。スコアは、ゲームオーバーみたいな区切りで送ってね。毎フレーム送るとか、短い時間にたくさん送ると reason: "rate_limited" で断られちゃう。ゲームを始めてから3秒たたないうちに送ったスコアも受け付けないよ("too_fast")。
ログインしていない人のスコアは、いったんブラウザに預かって、ログインしたあとにランキングへ送るよ。このときゲームには { ok: false, reason: "login_required" } が返るんだ。
セーブ・ロード
save と load で、ゲームの続きを保存できるよ。ログインして遊んでいる人はアカウント(クラウド)に保存されるから、ほかの端末でも続きから遊べるんだ。ゲストや、ほかのサイトに埋め込んだゲームでは、そのブラウザに保存されるよ。
await window.AIGame?.save("slot1", { stage: 3, coins: 120, items: ["key"] });
// ロード(無ければ null)
const data = await window.AIGame?.load("slot1");
await window.AIGame?.listSaves(); // => [{ slot: "slot1", updatedAt, cloud: true }]
await window.AIGame?.deleteSave("slot1");
保存できるのは JSON にできるデータで、1スロット256KB、1ゲーム10スロットまで。クラウドに保存するときもブラウザに必ずコピーを残すから、通信できないときも動くよ。load は、クラウドとブラウザのうち新しいほうを返すんだ。1分に120回をこえる保存は断られるから、ステージをクリアしたときやメニューを閉じたときなんかに保存してね。
ゲームを更新してセーブの形が変わって、前のデータが使えなくなるときは、新しいバージョンを上げるときに「セーブデータを新しくする」を選んでね。そのバージョンからは、AIGame.save だけじゃなくて、ゲームが自分で使っている localStorage や IndexedDB も前のバージョンと別になるよ。ゲームのコードを変える必要はないし、前のデータも消えないんだ。
トロフィー
unlockAchievement でトロフィーを渡せるよ。取ったトロフィーは、ゲームのページ(取った人の割合つき)と、その人のプロフィールに出るんだ。
window.AIGame?.unlockAchievement("first_clear");
// 一覧(取ったかどうかも)
const list = await window.AIGame?.getAchievements();
// => [{ id, name, description, icon, hidden, unlocked, rate }]
トロフィーの一覧は、ゲームのファイルの一番上に asobiai.json を置いて決めるよ。公開・更新のときに読み込まれるんだ。ゲームの編集画面でも決められて、1つのゲームに50個までだよ。
{
"achievements": [
{ "id": "first_clear", "name": "はじめてのクリア", "description": "ステージ1をクリア", "icon": "🏆" },
{ "id": "no_damage", "name": "無傷の勇者", "description": "ダメージを受けずにクリア", "icon": "art/shield.webp" },
{ "id": "secret_room", "name": "ひみつの部屋", "description": "隠し部屋を見つけた", "icon": "🔑", "hidden": true }
]
}
"hidden": true にしたトロフィーは、取るまで名前と説明が「???」になるよ。ゲストが取ったトロフィーはブラウザに預かって、ログインしたときに記録されるんだ。
みんなの集計
tally で1票を入れて、getTally でみんなの合計を読めるよ。アンケートや、「今日の問題をみんなは何手で解いたか」の分布なんかに使えるんだ。
// 1票入れる(1回のプレイで、同じ名前には1票だけ)
window.AIGame?.tally("favorite-stage", "forest");
// まとめて入れる(10個まで)。日替わりの問題なら名前に日付を入れる
window.AIGame?.tally({ "d20261024.result": "solved", "d20261024.moves": 5 });
// 読む
const t = await window.AIGame?.getTally("d20261024.moves");
// => { total: 128, counts: { "4": 12, "5": 61, "6": 55 } }(Asobiai の外では null)
サーバーに残るのは合計だけで、誰が何を選んだかは残らないよ。ゲストも投票できるんだ。誰でも票を入れられるから、画面に出すときは、ゲームが決めた選択肢だけを使って、知らない値は無視してね。
オンライン対戦
joinRoom で部屋に入ると、同じ部屋の人とメッセージをやり取りできるよ。
// 空いている部屋に入る(クイックマッチ。2〜16人)
const room = await window.AIGame.joinRoom({ maxPlayers: 2 });
// 友だちと同じ部屋に入るなら合言葉を渡す: joinRoom({ maxPlayers: 2, code: "ABCD" })
room.me; // 自分 { id, name }
room.players; // 部屋にいる人(自分も含む)
room.isHost; // いちばん先に入った人が true
room.on("join", (player) => { /* だれか入ってきた */ });
room.on("leave", (player) => { /* だれか抜けた */ });
room.on("message", (data, from) => { /* ほかの人から届いた */ });
room.send({ type: "move", x: 3, y: 1 }); // 自分以外の全員に送る
room.send({ type: "hello" }, player.id); // 1人にだけ送る
room.leave();
サーバーは、同じ部屋の人にメッセージを配るだけ。ゲームの開始や勝ち負けの判定は、room.isHost が true の人(ホスト)のブラウザでやるんだ。メッセージは1つ16KB、1人あたり毎秒60通までだから、毎フレーム送るより、操作したときや1秒に数回だけ送るのがおすすめだよ。
スマホの画面ロックや回線の切り替えで通信が切れても、reconnect 秒(ふだんは60秒)のあいだは同じ席に自動で戻れるよ。ホストは room.saveState で部屋の状態をサーバーに保存できて、ホストが抜けたときは、次のホストが room.loadState で読んで続きから進められるんだ。
const room = await window.AIGame.joinRoom({
maxPlayers: 8,
protocol: "3", // 通信の互換バージョン。違う人は同じ部屋に入れない
reconnect: 90, // 通信が切れても、この秒数は同じ席に戻れる(0〜300)
});
// ホストが進行のたびに保存する。players は人ごとの秘密(その人とホストだけが読める)
await room.saveState({
public: { round: 3, scores },
players: { [aliceId]: { role: "spy" }, [bobId]: { role: "detective" } },
});
// ホストが変わったとき、自分がホストなら続きを読む
room.on("host", async () => {
if (!room.isHost) return;
const { public: pub } = await room.loadState();
// pub.round から続ける
});
オンライン対戦が使えるのは、Asobiai のゲームページで遊んでいるときだけ。ほかのサイトへの埋め込みや、ファイルを直接開いたときは joinRoom がエラーになるよ。サーバーはメッセージを配るだけだから、ズルを完全には防げないんだ。気軽な対戦や協力プレイ向けだね。
AI に頼むときの書き方
ゲームを AI に作ってもらうときは、使う関数の名前をそのまま伝えると確実だよ。たとえばランキングなら、こんなふうに頼むんだ。
スコアがあるゲームにしてください。ゲームオーバーのときに window.AIGame?.submitScore(score) を呼んでください(AIGame が存在しない環境でもエラーにならないよう ?. を使うこと)。タイムアタックの場合は window.AIGame?.submitScore(秒数, { order: "asc" }) としてください。
SDK の説明ページには、ランキング・集計・オンライン対戦それぞれの頼み方の文面があって、コピーして使えるよ。
SDK がほかにしていること
AIGame を呼ばなくても、SDK はいくつかのことを自動でやってるよ。矢印キーやスペースを押したときに、ページがスクロールしないように止めてる。ゲームのエラーと console の出力は、作者だけが見られるコンソールに送られるんだ。サムネイルを撮れるように、WebGL の preserveDrawingBuffer も有効にしてるよ。
iframe の大きさが読み込んだあとに決まったとき、resize イベントが届かないことがあるんだ。そのときは SDK が代わりに resize を送るよ。起動したときの大きさでキャンバスを作るゲームが、単色の画面のままになるのを防ぐためなんだ。
ゲームはゲームごとに別のドメインで動くから、localStorage はほかのゲームやサイト本体と分かれてるよ。iframe の中で動くので、ページ全体をほかのページに移すことはできなくて、リンクは新しいタブで開く必要があるんだ。外部の CDN や API はふつうに使えるよ。
おわりに
ここで紹介した機能は、どれもゲームのコードから関数を呼ぶだけで使えるよ。細かい上限や更新履歴は SDK の説明ページ にあるよ。Claude Code などの AI から Asobiai に公開する方法は、https://asobiai.games/publish/mcp にまとめてあるから、よかったら見てみてね。