ゲームを書く人へ
vsboxで1対1の対戦ゲームを作る人が、はじめに読む文書です。ゲームのでき方、作って公開するまでの流れ、エラーにならないのに壊れる落とし穴をまとめています。細かい決まりは、ほかの文書にあります(8. 文書の一覧)。ゲームを面白くするヒントは、ゲームづくりのヒントにあります。
1. ゲームのでき方
ゲームは、2つのファイルでできています。
| ファイル | 役目 | 書き方 |
|---|---|---|
game.js(ルール) | 盤面、得点、勝ち負け、CPUの考え方 | JavaScriptの一部だけを使う決まった書き方。整数だけ |
visual.js(見た目) | ルールの状態を3Dの絵にする | ふつうのJavaScriptとThree.js |
対戦では、2人の端末がそれぞれ同じルールを計算して、試合を進めます。届くのはお互いの入力(指の動き)だけです。そのためルールは、どの端末でもまったく同じ結果になる書き方に限られています。見た目は勝ち負けに関わらないので、自由に書けます。
通信のコードは1行も要りません。tick(g, input) は、2人の入力がそろった状態で呼ばれます(5. vsboxがやること)。
2. 作って公開するまで
- Googleでログインし、開発者タブの「新しく作る」を押します。中身が空の下書きができます
- 作品のページで
game.jsとvisual.jsを書き、「下書き保存」を押します。AIに書かせるなら、開発者タブの「AI をつなぐ」でAI(ChatGPT か Claude)をつなぐと、AIが作品を直接読み書きします(スマホからでも、チャットで頼むだけで作れます) - 「開発者として開く」で遊んで確かめます。CPU戦を
player2側で始める「2P として戦う(開発用)」も出ます - 「サムネイルを撮る」で、一覧に出す絵を撮ります。CPU同士の試合が動くので、よい場面で「ここで撮る」を押します。その場面の絵と、そこから3秒の動きを撮ります。撮れた動きを見て「これにする」か「撮り直す」を選びます。一覧のカードは正方形で、真ん中の明るい四角の中が出ます(画面に入っているカードだけ動きます)。ゲームの紹介の画面には、撮った絵の全体が出ます(CPUの居ないゲームは、始まりの場面の絵だけを撮ります)
- 名前と説明を入れて、「公開する」を押します。一覧に並び、だれでも遊べるようになります
- 公開するには、名前(40文字まで)と説明(5〜3000文字)が要ります
- 説明は二段です。1行目が短い説明で、箱の一番上に出るほか、検索の結果や SNS で共有したときの札にもそのまま出ます。どんなゲームかを1〜2文(80文字くらいまで)で書きます。2行目からが長い説明で、「もっと見る」で開きます。遊び方・操作・コツなど、遊ぶ人が知りたいことを書きます(無くてもかまいません)
将棋の駒を指で動かして、CPU やほかのプレイヤーと対局します。10分の切れ負けです。
【遊び方】
動かしたい駒を押してから、行き先を押します。…
- 遊べる端末(スマホ・タブレット、パソコン、両方)は
meta.devicesで決めます。書かなければスマホ・タブレットだけで、パソコンの一覧には出ません(ルール(game.js)の仕様 4.5) - 「操作」の欄(任意)は、スマホ・タブレットとパソコンで別々に書けます(それぞれ300文字まで)。ゲームの紹介の画面では、説明の下に、遊んでいる端末のぶんだけが出ます。押して選ぶだけのゲームなら、空でかまいません
game.jsは1MB、visual.jsは256KBまでです。作品は1人10本まで(下書きも数えます)、合計はすべての作品で100MBまでです(画像・音・3Dモデルも含みます)- 下書きは、作者のほかには見えません
- 公開したあとに直して保存すると、作業中の版に入ります。遊ぶ人に届くのは「更新を公開」を押したときです。「開発者として開く」では、作業中の版で遊べます
- 保存と公開のたびに版が残ります(保存ごとと公開したもの、それぞれ20個まで)。作品のページの「これまでの版」から戻せます。AI をつないでいれば、AI に、前の版に戻すよう頼めます
- 止めるときは「公開をやめる」、消すときは「作品を削除」です(削除は元に戻せません)
3. エラーになること
決まりに合わない書き方は、エラーになります。エラーには理由と行番号が付くので、そのまま直せます。AIに書かせているなら、エラー文をそのまま渡せば直せます。
| いつ | 何が止まるか | 決まりのある場所 |
|---|---|---|
| 保存したとき | 書けない構文、meta の知らない項目や範囲外の値、tick や bot が無い、大きすぎるファイル | ルール(game.js)の仕様 2・4、見た目(visual.js)の仕様 7、2節の大きさ |
| 試合中 | トップレベルの値の書き換え、計算の結果が数でなくなる、bot の中で世界を変える、物理の値の範囲外、命令数の上限 | ルール(game.js)の仕様、物理のAPI |
試合中のエラーは、試合がそこで止まり、結果の板に理由と行番号が出ます。
4. エラーにならないのに壊れること
ここに挙げるものは、保存も通り、自分の端末でも動きます。それでも壊れていて、気づく手段がほとんどありません。公開する前に確かめてください(6. 公開する前に)。
4.1 player2 の画面を見ていない
CPU戦では、自分はふつう player1 です。snapshot.myRole で表示を分けていると、ひとりで遊んでいるかぎり、player2 の側は一度も描かれません。オンライン対戦で初めて、player2 の画面が壊れていると分かります。
確かめ方 「開発者として開く」で開き、「2P として戦う(開発用)」で遊びます。画面の隅の数字の「自分」が
player2になります。
4.2 カメラを反対側に回して、指の向きを直していない
player2 に反対側から見せると、その人の画面は180度回ります。指の x・y は画面の座標なので、世界の向きに直すのはルールの仕事です。カメラだけ回して指を直さないと、player2 だけ操作が逆になります。4.1の理由で、ひとりでは気づけません。
if (owner === "player2") { return { dx: -stick.dx, dy: -stick.dy }; }
カメラの回し方は見た目(visual.js)の仕様 3.1にあります。
4.3 画面の形を決め打ちしている
画面の縦横比は端末ごとに違います。決め打ちの数は、ある形では正しく、別の形では壊れます。
| もの | 書き方 |
|---|---|
| 指の位置 | 短いほうの辺が1000。下の端からは h - 210 のように書く |
| 霧のような距離で効くもの | ctx.cameraDistance を基準にする(画面の形で大きく変わる) |
| 文字の板の大きさ | カメラの画角と距離から計算する |
確かめ方 縦長と横長の両方で開きます。
4.4 先に知られると困る乱数を g.random で引いている
g.random は、改造した端末なら、これから出る値を先に計算できます。サイコロを振り直す前に何が出るかを知る、山の次の札を知る、といったことができてしまいます。ふつうに遊ぶぶんには何も起きないので、気づけません。
相手より先に知ると選び方が変わる乱数は、g.deal で引きます(ルール(game.js)の仕様 7.5)。
4.5 相手に見せない情報を、絵で隠しているだけ
g.state に入れたものは、2人の端末の両方にあります。絵に描かなければ見えませんが、改造した端末なら読めます。
| 隠し方 | 改造した端末から |
|---|---|
g.state に入れて、絵に描かない | 読める |
g.deck と g.show で配る(ルール(game.js)の仕様 7.6) | 読めない |
決め方は、そのずるでゲームが成り立たなくなるかどうかです。手札が見えるカードゲームは成り立たないので、g.deck を使います。手札を並べて見せるなら、触った場所でも中身が分かるので、controls も書きます(ルール(game.js)の仕様 9)。霧の向こうの敵の位置が分かる程度なら、不利になるだけなので、絵で隠すのでもかまいません。
4.6 g.state に絵のための値を置いている
ルールの状態は、巻き戻しのために10秒ぶん取っておかれます。g.state を大きくすると、そのぶん重くなります。粒子の寿命や、なめらかに動かすための途中の値のように、絵にしか要らないものは見た目の中で持ちます(見た目は巻き戻されません)。
指で動かすつまみの位置は例外で、g.state に置きます。見た目は指を読めないためです。
逆に、ルールにしか要らない大きなデータ(CPUの読みの表など)があるときは、visualState(g) で見た目に渡す欄を絞ります(ルール(game.js)の仕様 5.4)。
4.7 同じ数を game.js と visual.js の2か所に書いている
スティックの半径や位置は、当たり判定がルールに、絵が見た目にあるので、2か所に書くことになります。片方だけ直すと、指と絵がずれます。触れば分かりますが、直すときは両方を直します。
4.8 絵を毎回作り直している
形・材質・文字の絵を update のたびに作ると、重くなり、遊んでいるうちに遅くなります。init で作って使い回します。文字の絵は、中身が変わったときだけ描き直します(見た目(visual.js)の仕様 8)。
確かめ方 「開発者として開く」で20秒以上遊びます。画面の隅の「プログラム」と「転送」に ✓ が付けば大丈夫です。⚠ のときは、AI をつないでいれば AI にも届きます。
4.9 持ち込んだモデルを clone() で増やしている
骨のあるモデルは、Three.jsの clone() で増やすと2体が同じ動きになります。ctx.clone(obj) を使います(見た目(visual.js)の仕様 7.2)。
4.10 文字の色だけで読ませている
背景はゲームごと、場面ごとに違うので、文字の色だけではどこかで読めなくなります。文字の後ろに下地を敷きます。
5. vsboxがやること(書かなくてよいこと)
次のことは、vsboxが自動で行います。ゲームの側で同じことを書くと、二重になって壊れます。
| こと | 中身 |
|---|---|
| 通信 | 2人の入力を送り合う。ゲームには通信のコードが要らない |
| 遅れの吸収 | 入力を2tick(約66ms)遅らせて使い、相手の入力を予想して進め、外れたら巻き戻してやり直す |
| ずれの検出 | 2人の計算が食い違ったら、試合を止めて知らせる(直そうとはしない) |
| 切断 | 通信が止まっても、1試合に合わせて10秒までは待つ。超えると止まった側の負け |
| 開始の合図 | meta.countdown のあいだ世界を止め、2人が同じtickから始める |
| カメラと明かり | meta.view が収まる位置にカメラを置き、明かりを用意する(見た目(visual.js)の仕様 3) |
| 描く回数 | 1秒に30回ほどまで |
| 準備 | 試合の前に、画面を何枚か描いておき、材質や音を読んでおく(最初の爆発でかくつかないように) |
| サムネイル | ゲームを実際に動かして撮る |
| レート | オンライン対戦とCPU戦の勝ち負けから、レートを計算して表示する |
| 見た目の変更 | meta.looks の選択の画面と保存 |
6. 公開する前に
どれもエラーが出ないので、自分で確かめます。
- ☐ 「2P として戦う(開発用)」で遊び、
player2の画面を見た - ☐ カメラを回したなら、指の向きも直した
- ☐ 画面の隅の「プログラム」と「転送」に ✓ が付いた(試合の最初の20秒で判定します)
- ☐ 先に知られると困る乱数は
g.deal、相手に見せない札はg.deckにした
7. いちばん小さいゲーム
そのまま保存して遊べる、いちばん小さい組です。2人で画面をたたき、先に20回たたいたほうが勝ちます。ここから書き足していくと、決まりに合った形のまま育てられます。
これは書き方の見本で、ゲームの大きさや面白さの見本ではありません。どんなゲームにするかのヒントは、ゲームづくりのヒントにあります。
game.js
const meta = {
countdown: 90,
view: { width: 8000, height: 6000 },
opponents: [
{ name: "ゆっくり", description: "1秒に4回たたく", every: 8 },
{ name: "はやい", description: "1秒に6回たたく", every: 5 },
],
};
const GOAL = 20;
function setup(g) {
g.state.taps = { player1: 0, player2: 0 };
}
function tick(g, input) {
for (const owner of ["player1", "player2"]) {
const inp = input[owner];
if (!inp) { continue; }
let n = 0;
if (inp.cpu) {
n = inp.cpu.tap;
} else {
for (const t of inp.touches) {
if (t.pressed) { n = n + 1; }
}
}
if (n > 0) {
g.state.taps[owner] = g.state.taps[owner] + n;
g.effect("tap", { owner: owner });
}
}
const p1 = g.state.taps.player1 >= GOAL;
const p2 = g.state.taps.player2 >= GOAL;
if (p1 && p2) { g.drawMatch(); }
else if (p1) { g.winMatch("player1"); }
else if (p2) { g.winMatch("player2"); }
}
function bot(g, owner) {
const me = meta.opponents[g.cpu[owner]];
if (g.tick % me.every === 0) { return { cpu: { tap: 1 } }; }
return { cpu: { tap: 0 } };
}
visual.js
let THREE = null;
let ctx = null;
const bars = {};
const GOAL = 20; // game.js と同じ数(4.7)
export function init(c) {
ctx = c;
THREE = c.THREE;
ctx.scene.background = new THREE.Color(0x203040);
const floor = new THREE.Mesh(new THREE.BoxGeometry(8, 0.2, 4),
new THREE.MeshStandardMaterial({ color: 0x556677 }));
floor.position.y = -0.1;
floor.receiveShadow = true;
ctx.scene.add(floor);
const box = new THREE.BoxGeometry(1, 1, 1);
bars.player1 = new THREE.Mesh(box, new THREE.MeshStandardMaterial({ color: 0x3fa7ff }));
bars.player2 = new THREE.Mesh(box, new THREE.MeshStandardMaterial({ color: 0xff6b5b }));
bars.player1.position.x = -1.5;
bars.player2.position.x = 1.5;
for (const k of ["player1", "player2"]) {
bars[k].castShadow = true;
ctx.scene.add(bars[k]);
}
}
export function update(snapshot, dt) {
for (const k of ["player1", "player2"]) {
const h = 0.05 + (snapshot.state.taps[k] / GOAL) * 4;
bars[k].scale.y = h;
bars[k].position.y = h / 2;
}
}
- 左の青い柱が
player1、右の赤い柱がplayer2です。たたくたびに伸びます - 音を鳴らすなら、
meta.sounds: { tap: "@impactWood_light_000" }のように音の棚の名前で足します(数の並びや、持ち込んだファイルでもかまいません。ルール(game.js)の仕様 4.8) - 自分の柱がどちらかを分かるようにするなら、
snapshot.myRoleで色や位置を変えます(見た目(visual.js)の仕様 4)
8. 文書の一覧
| 文書 | 中身 |
|---|---|
| ルール(game.js)の仕様 | 言語、meta、入力、g のAPI、CPU |
| 見た目(visual.js)の仕様 | Three.jsで描く、snapshot、文字、持ち込んだファイル |
| 物理のAPI | 体の作り方・動かし方・調べ方 |
| 音の棚 | 名前で使える録音の効果音の一覧 |
| ゲームづくりのヒント | 設計する、動くものを作る、完成度を上げる(手触り・釣り合い・CPU・見た目・音)。決まりではなくヒント集 |
ヒント AIにゲームを書かせるときは、この文書と、ルール・見た目の仕様を渡します。物理を使うなら物理のAPIも渡します。どの文書も、ページの上の「元の文書(.md)」から、そのまま渡せる形で開けます。