作者向けの文書 › authoring ・ 元の文書(.md)

ゲームを書く人へ

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. 作って公開するまで

  1. Googleでログインし、開発者タブの「新しく作る」を押します。中身が空の下書きができます
  2. 作品のページで game.js と visual.js を書き、「下書き保存」を押します。AIに書かせるなら、開発者タブの「AI をつなぐ」でAI(ChatGPT か Claude)をつなぐと、AIが作品を直接読み書きします(スマホからでも、チャットで頼むだけで作れます)
  3. 「開発者として開く」で遊んで確かめます。CPU戦を player2 側で始める「2P として戦う(開発用)」も出ます
  4. 「サムネイルを撮る」で、一覧に出す絵を撮ります。CPU同士の試合が動くので、よい場面で「ここで撮る」を押します。その場面の絵と、そこから3秒の動きを撮ります。撮れた動きを見て「これにする」か「撮り直す」を選びます。一覧のカードは正方形で、真ん中の明るい四角の中が出ます(画面に入っているカードだけ動きます)。ゲームの紹介の画面には、撮った絵の全体が出ます(CPUの居ないゲームは、始まりの場面の絵だけを撮ります)
  5. 名前と説明を入れて、「公開する」を押します。一覧に並び、だれでも遊べるようになります
将棋の駒を指で動かして、CPU やほかのプレイヤーと対局します。10分の切れ負けです。

【遊び方】
動かしたい駒を押してから、行き先を押します。…

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. 公開する前に

どれもエラーが出ないので、自分で確かめます。

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;
  }
}

8. 文書の一覧

文書中身
ルール(game.js)の仕様言語、meta、入力、g のAPI、CPU
見た目(visual.js)の仕様Three.jsで描く、snapshot、文字、持ち込んだファイル
物理のAPI体の作り方・動かし方・調べ方
音の棚名前で使える録音の効果音の一覧
ゲームづくりのヒント設計する、動くものを作る、完成度を上げる(手触り・釣り合い・CPU・見た目・音)。決まりではなくヒント集

ヒント AIにゲームを書かせるときは、この文書と、ルール・見た目の仕様を渡します。物理を使うなら物理のAPIも渡します。どの文書も、ページの上の「元の文書(.md)」から、そのまま渡せる形で開けます。

作者向けの文書 ・ vsbox について ・ トップ