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