# ルール(game.js)の仕様 vsboxのゲームは、ルール(`game.js`)と見た目(`visual.js`)の2つのファイルでできています。 この文書は、ルールの書き方をまとめたものです。見た目は[見た目(visual.js)の仕様](/docs/visual)、物理は[物理のAPI](/docs/physics-api)にあります。 ルールは、2人の端末でまったく同じ計算をして試合を進めます。そのため、ルールはJavaScriptの一部だけを使う決まった書き方で書きます。決まりに合わない書き方は、投稿したときにエラーになります。 --- ## 1. 全体の形 ```js const meta = { ... }; // このゲームの宣言(4. meta) function setup(g) { ... } // ラウンドの始まりに1回 function tick(g, input) { ... } // 1秒に30回 function bot(g, owner) { ... } // CPU の入力を返す function visualState(g) { ... } // 任意。見た目に渡す中身を返す function controls(m, input) { ... } // 任意。手元の処理(9. 手元の処理) ``` | 関数 | 必須 | 呼ばれるとき | |---|---|---| | `tick(g, input)` | ○ | 1tickごと(1秒に30回) | | `bot(g, owner)` | ○ | CPUがいる試合で、1tickごとに `tick` の前 | | `setup(g)` | | ラウンドの始まり(試合の始まりを含む) | | `visualState(g)` | | 見た目に中身を渡すとき | | `controls(m, input)` | | 自分の端末で、入力を送る前 | - 1tickは1/30秒です。試合の時間は、すべてtickで数えます - トップレベルに書けるのは `const`・`let`・`function` だけです。それ以外の文(`if` など)を書くと、投稿でエラーになります - 関数は、トップレベルにだけ書けます。関数の中に関数を書くと、投稿でエラーになります - `bot` はCPU対戦のために必須です。CPU対戦が無いと、作者が手元で遊んで確かめられません 試合中にエラーが出ると、試合はそこで止まり、結果の板に理由と行番号が出ます。 ``` ゲームが止まりました: 計算の結果が数になりません(undefined(そのプロパティは存在しません) * 600 → NaN) (7行目) ``` ## 2. 言語 ### 2.1 値 | 値 | 例 | |---|---| | 整数 | `0`、`-250`、`1500` | | 文字列 | `"player1"`、`'red'` | | 真偽値 | `true`、`false` | | `null` | `null` | | 配列 | `[1, 2, 3]` | | オブジェクト | `{ x: 0, hp: 100 }` | **小数はありません。**長さや速さは、1単位を1000とした整数で書きます(1.5なら `1500`)。 `if` などで偽として扱われるのは、`false`・`0`・`""`・`null` と、無いプロパティを読んだときの値です。それ以外は真です。 ### 2.2 書けるもの | 種類 | 書き方 | |---|---| | 変数 | `const`、`let` | | 算術 | `+` `-` `*` `%`、単項の `-` | | 代入 | `=`、`+=` `-=` `*=` `%=`、`++` `--`(前にも後ろにも付けられる。数にだけ使える) | | 比較 | `===` `!==` `<` `>` `<=` `>=` | | 論理 | `&&` `\|\|` `!` | | 分岐 | `if`、`else`、`else if`、三項演算子 `a ? b : c` | | 繰り返し | `for (const x of 配列)`、`for (const i of range(0, n))`、`break`、`continue` | | 関数 | トップレベルの `function` 宣言、`return` | | 配列・オブジェクト | リテラル、`a[i]`、`o.k`、`o[k]`、代入 | | 文字列の連結 | `"P" + n` | | コメント | `//`、`/* */` | 文の終わりは `;` か改行です。1行に2つ以上の文を書くときは、`;` で区切ります。`return` のすぐ後で改行すると、値は返りません(JavaScriptと同じ)。 ### 2.3 書けないもの どれも、投稿したときにエラーになります。 | 書けないもの | 代わりに | |---|---| | `/`(割り算) | `div(a, b)` | | 小数・指数(`1e3`)・16進数(`0xff`) | 整数 | | `==`、`!=` | `===`、`!==` | | `while`、`do`、`for (let i = 0; …)` | `for (const i of range(0, n))` と `break` | | 再帰(関数が自分を呼ぶ・呼び合う) | 処理待ちの配列と、回数を決めたループ(8.1) | | アロー関数 `=>`、関数式、関数の中の関数 | トップレベルの `function` 宣言 | | `/=` | `a = div(a, b)` | | テンプレート文字列 `` `${x}` `` | `"a" + x` | | `switch` | `if` / `else if` | | 分割代入 `const { x } = o`、`...` | 1つずつ取り出す | | 1つの宣言で複数の変数 `const a = 1, b = 2` | 1行に1つ | | オブジェクトの省略記法 `{ x }` | `{ x: x }` | | 計算したキー `{ [key]: 1 }` | `const o = {};` のあと `o[key] = 1;` | | 数のキー `{ 0: "a" }` | `{ "0": "a" }`(読むときは `o[0]` でよい) | | ビット演算 `&` `\|` `^` `~` `<<` `>>` | `%` と `div` | | `?.`、`??` | `if` か三項演算子で、`null` でないかを確かめる | | `typeof`、`instanceof`、`in`、`delete`、`void` | (なし) | | `var` | `const`、`let` | | `class`、`new`、`this` | オブジェクトと関数 | | `try`、`catch`、`throw` | (なし) | | `async`、`await`、`yield` | (なし) | | `Math`、`Date`、`require`、`import`、`export`、`eval`、`Function`、`globalThis`、`window` | 3. 組み込みの関数、`g.random` | ### 2.4 配列・文字列・オブジェクトでできること **配列**で使えるのは、次のものだけです。関数を受け取るもの(`map`・`filter`・`sort` など)と、`splice`・`join` などはありません。 | 書き方 | 中身 | |---|---| | `a[i]`、`a.length` | 要素と長さ | | `a.push(x, …)` | 後ろに足す。新しい長さを返す | | `a.pop()` | 後ろから1つ取り出して返す。空なら `null` | | `a.slice(始め, 終わり)` | 一部の写し(元は変えない)。引数は省ける。負の数は後ろから数える | | `a.indexOf(x)` | `===` で比べて最初の位置。無ければ `-1` | | `a.includes(x)` | `===` で比べて、あれば `true` | 配列やオブジェクトどうしの `===` は、同じものかどうかで比べます(中身が同じでも、別に作ったものは別)。 要素を条件で取り除くときは、残す要素で新しい配列を作ります。 ```js const kept = []; for (const b of g.state.bullets) { if (b.life > 0) { kept.push(b); } } g.state.bullets = kept; ``` **文字列**は、比較、`+` での連結、`s.length`(長さ)ができます。`s[0]` のように1文字ずつ取り出すことはできません。 **オブジェクト**は、`o.k`・`o[k]` で読み書きできます。キーがあるかは `has(o, k)`、キーの一覧は `keys(o)` で調べます(3)。キーを消す方法はありません。 > **注意** 無いプロパティを読むと「値が無い」が返りますが、それと比べる方法がありません(`undefined` という名前は使えません)。キーがあるかは `has(o, k)` で確かめるか、最初に `null` で用意しておいて `=== null` で確かめます。 `__proto__`・`constructor`・`prototype` という名前は、読むことも書くこともできません。 ### 2.5 整数の計算 - `div(a, b)` は0の方向へ切り捨てます(`div(-7, 2)` は `-3`)。`b` が `0` なら、エラーになります - `%` の符号は左の数に従います(`-7 % 3` は `-1`) - `+` `-` `*` `%` と単項の `-` の結果が数でなくなると(`NaN` や無限大)、その場でエラーになります - 大きすぎる数はずれます。掛け算の結果は、およそ±9,000,000,000,000,000に収めてください ### 2.6 状態は `g.state` に置く **試合の状態は `g.state` に置きます。**エンティティ(7.2)の自分のフィールドも状態になります。 トップレベルの `const` と `let` は、読むことしかできません。変数への代入も、中の配列やオブジェクトの書き換えもエラーになります。 ```js const SPEED = 300; // ○ 定数として読む const plan = { target: 0 }; function tick(g, input) { plan.target = 5; // ✗ エラー g.state.target = 5; // ○ } ``` 関数の中で作った配列やオブジェクトは、自由に書き換えられます。 > **ヒント** ルールは、通信の遅れを埋めるために、過去のtickに戻ってやり直すことがあります。戻るのは `g.state` とエンティティと物理だけなので、状態をほかに置くと、やり直しのたびにずれていきます。 ### 2.7 命令数の上限 1回の呼び出し(`tick`・`bot` など)で実行できる文と関数呼び出しは、合わせて2,000,000回までです。超えると「命令数の上限を超えました」というエラーになります。 ## 3. 組み込みの関数 `g.` を付けずに呼べる関数です。数を受け取る関数(`div`・`abs`・`min`・`max`・`sqrt`・`normalize`・`range`)に数以外を渡すと、エラーになります。 | 関数 | 返すもの | |---|---| | `div(a, b)` | 割り算の商(0の方向へ切り捨て)。`b` が `0` ならエラー | | `abs(a)` | 絶対値 | | `min(a, b)`、`max(a, b)` | 小さいほう、大きいほう | | `sqrt(a)` | 平方根(切り捨て)。`a` が負ならエラー | | `normalize(dx, dy)` | 同じ向きで長さ1000の `{ x, y }`。`(0, 0)` なら `{ x: 0, y: 0 }` | | `range(a, b)` | `[a, a+1, …, b-1]` | | `has(o, k)` | オブジェクト(か配列)が、キー `k` を持っていれば `true` | | `keys(o)` | オブジェクトのキーの配列(書いた順。整数の形のキーは小さい順に先に並ぶ) | ## 4. meta `const meta = { … }` は、このゲームについてvsboxに伝える宣言です。どの項目も省略できます。 ```js const meta = { countdown: 90, view: { width: 16000, height: 24000 }, frameColor: "#7fb069", sounds: { explode: "boom.mp3" }, }; ``` | 項目 | 中身 | 書かないとき | |---|---|---| | `countdown` | 各ラウンドの始まりに世界を止めておくtick数。0〜1800(60秒) | `0` | | `view` | 画面に収める世界の広さ `{ width, height }`。それぞれ500〜10000000 | 12000×12000 | | `frameColor` | 背景の色(試合画面と一覧のカード)。`#rgb`・`#rrggbb`・`rgb(r,g,b)`・色名 | `#161b22` | | `orientation` | 試合画面の向き。`"portrait"`(縦)か `"landscape"`(横)(4.4) | `"portrait"` | | `devices` | 遊べる端末。`"mobile"`(スマホ・タブレット)・`"pc"`(パソコン)・`"both"`(両方)(4.5) | `"mobile"` | | `interpolate` | 間を補って描くか。`true` か `false`(4.9) | `false` | | `gravity` | 重力 `[x, y, z]`。物理の体を作るなら必須 | 無し([物理のAPI](/docs/physics-api)) | | `layers` | 衝突レイヤーの名前の配列 | すべてが当たる([物理のAPI](/docs/physics-api)) | | `opponents` | CPUの一覧(4.6) | CPUは1体 | | `looks` | 見た目の変更の欄(4.7) | 見た目の変更は無し | | `sounds` | 出来事に鳴らす音(4.8) | 音は鳴らない | - ここに無い項目を書くと、投稿がエラーになります(綴り違いにすぐ気づけます) - 値も確かめます。範囲の外や形の違う値はエラーになります - 名前と説明は `meta` に書きません。開発者タブで入力します(書くとエラー) ### 4.1 `countdown` ラウンドが始まる前に、世界を止めておく長さです。この間は `tick` も物理も止まります。止まっている間に「3・2・1」を出すのは見た目の仕事です。 ### 4.2 `view` 画面に収める世界の広さです(1単位 = 1000)。vsboxはこの範囲が画面に収まる位置にカメラを置きます。見た目でカメラを動かすゲームでは、最初の位置とサムネイルにだけ効きます。 ### 4.3 `frameColor` 背景の色です。次の2か所に使います。 - 試合画面の背景。見た目で `scene.background` を塗らなければ、この色が見えます([見た目(visual.js)の仕様](/docs/visual) 3) - 一覧に並ぶカードの下地。サムネイルを撮る前は、この色だけが出ます。横のゲーム(4.4)のサムネイルでは、上下の余白の色になります 空や地面に近い色にしておくと、読み込みの途中やサムネイルの前でも、ゲームの雰囲気が伝わります。 ### 4.4 `orientation` 試合画面を縦長と横長のどちらで作るかです。書かなければ縦です。 ```js orientation: "landscape", ``` - `"landscape"` のゲームは、スマホが縦のままでも、vsboxが画面を90度回して横長に描きます。遊ぶ人はスマホを横に持つだけです(画面の回転をロックしていても遊べます) - 指の位置(`input` の `x` `y` `w` `h`)も、横長の画面の向きで届きます。ゲームは回っていることを気にしなくてよく、横向きの1通りだけ作れば足ります - 回すのは試合画面だけです。一覧やゲームの紹介の画面は縦のままです - サムネイルは横長(4:3)で撮ります。縦の一覧のカードには、上下に余白(`frameColor`)を付けて収めます ### 4.5 `devices` どの端末で遊べるゲームかです。書かなければ `"mobile"` です。 ```js devices: "both", ``` | 値 | 意味 | |---|---| | `"mobile"` | 指で操作する端末(スマホ・タブレット) | | `"pc"` | マウスとキーボードで操作する端末(パソコン) | | `"both"` | 両方 | - 遊べない端末では、ホームの一覧に出ません(`"mobile"` のゲームはパソコンの一覧に、`"pc"` のゲームはスマホの一覧に出ません)。開発者タブの一覧には、いつも出ます - 端末は、主な操作が指かマウスかで見分けます。キーボードを付けたタブレットは `"mobile"` の側です - パソコンでは、マウスの左ボタンを押している間が1本の指として届きます(6.1)。押して選ぶだけのゲームなら、何も足さずに `"both"` にできます - 動きながらボタンを押すような、2本の指を同時に使う操作は、マウスではできません。パソコンでも遊べるようにするなら、キーボードの操作(6.6)が要ります - 遊んでいる端末がスマホかパソコンかは、見た目の側でも分かります([見た目(visual.js)の仕様](/docs/visual) 4.4)。端末で絵を変えるかどうかは自由です - 遊び方の操作は、`meta` ではなく作品の「操作」の欄に書きます(スマホとパソコンで別。[作って公開するまで](/docs/authoring) 2) - `"both"` や `"pc"` にするなら、パソコンで開いて確かめてください ### 4.6 `opponents` CPU対戦で選べる相手の一覧です。最大12体です。 ```js opponents: [ { name: "Easy", description: "ゆっくり動く", speed: 70, rating: 900 }, { name: "Hard", description: "先を読む", speed: 100 }, ] ``` | キー | 必須 | 中身 | |---|---|---| | `name` | ○ | 名前(60文字まで) | | `description` | ○ | 説明(60文字まで) | | `rating` | | CPU戦レートの目盛り。0〜4000の整数 | | そのほか | | 自由。`bot` の中で読んで、強さの差を作る | - `rating` を書かなければ、並び順に1100・1300・1500…(200ずつ)になります - 遊んだ人の記録(CPU戦レート・どこまで倒したか)は、相手を**何番目か**で覚えます。公開したあとに相手を増やすときは**後ろに足して**ください。並べ替えたり途中の相手を消したりすると、記録が別の相手に付きます - 公開したあとに `rating` を変えても、効くのはまだ CPU 対戦をしていない人だけです(遊んだ人の数字は、その人の勝ち負けで動いた値のまま続きます) - `bot` の中では、いまの相手が何番目かを `g.cpu[owner]` で読めます(7.1) ### 4.7 `looks` 勝ち負けに関わらない見た目(サイコロの色、キャラの服、背景など)を、遊ぶ人が選べるようにします。選んだものは見た目にだけ届き、ルールには届きません。 ```js looks: [ { id: "dice", name: "サイコロ", owner: "each", options: [ { id: "white", name: "白", description: "定番の白" }, // 最初が標準 { id: "red", name: "赤" }, ] }, { id: "table", name: "卓", owner: "player1", options: [ … ] }, ] ``` | キー | 中身 | |---|---| | `id` | 英小文字・数字・`_`・`-` の1〜24文字。頭は英小文字か数字 | | `name` | 画面に出す名前(20文字まで) | | `owner` | 誰の選択を使うか(下) | | `options` | 選択肢の配列(1〜24個)。最初が標準 | 選択肢は `{ id, name, description }` です。`description` は任意で、80文字までです。欄は最大8つです。同じ `id` を2回使うことはできません。 | `owner` | 使う選択 | |---|---| | `"each"` | 2人それぞれの選択(自分のサイコロ、自分のキャラ) | | `"player1"` | `player1` の選択を2人とも使う(マップ、背景、2人で使う札) | | `"player2"` | `player2` の選択を2人とも使う | - 選べるのはログインした人だけです。選んでいなければ標準(最初の選択肢)になります - CPUとサムネイルは標準を使います - 見た目がどう受け取るかは[見た目(visual.js)の仕様](/docs/visual)にあります ### 4.8 `sounds` `g.effect(名前, data)`(7.4)を出したときに鳴らす音を決めます。最大20個です。音は3通りで書けます。 | 書き方 | 例 | 中身 | |---|---|---| | 棚の音 | `"@card-place-1"` | vsboxが置いている録音の音。一覧は[音の棚](/docs/sounds) | | 数の並び | `"[,,925,.04,.3,.6,1,.3,,6.27,-184,.09,.17]"` | 数から音を作る(ZzFX)。電子音になりやすい | | ファイル名 | `"boom.mp3"` | 作品に持ち込んだ音のファイル | ```js sounds: { place: "@card-place-1", // 棚の音 shoot: "[,,925,.04,.3,.6,1,.3,,6.27,-184,.09,.17]", // 数の並び explode: "boom.mp3", // ファイル hit: { self: "[,,537,.02,.02,.22,1,1.59,-6.98,4.97]", other: "hurt.wav" }, // 持ち主が自分なら self、相手なら other } ``` `{ self, other }` の形にすると、出来事の持ち主(`data.owner`)が画面を見ている本人かどうかで、鳴らす音を変えられます。片方だけでもかまいません。持ち主の無い出来事では鳴りません。 - 1つの音は5秒までです。ファイルの音は、置くときに長さを確かめます - 音は全部あわせて、遊ぶために展開したあとの大きさで24MBまでです(5秒のモノラルの音なら約26本ぶん。ステレオはその半分)。置いてある音のファイルは、使っていなくても数えます。置くときと保存するときに確かめます - 同じ音は、60ms以内に続けて鳴りません。同時に鳴るのは8つまでです - 投稿の時点でファイルが無くてもかまいません。置くまでは鳴らないだけです - 棚に無い名前は、保存のときにエラーになります #### 数の並び(ZzFX) [ZzFX](https://github.com/KilledByAPixel/ZzFX) の並びを、そのまま `" "` で囲んで書きます(`game.js` には小数が無いため、文字にします)。 - 並びの順は ZzFX と同じです:`volume` `randomness` `frequency` `attack` `sustain` `release` `shape` `shapeCurve` `slide` `deltaSlide` `pitchJump` `pitchJumpTime` `repeatTime` `noise` `modulation` `bitCrush` `delay` `sustainVolume` `decay` `tremolo` `filter`。空けた所は ZzFX のいつもの値です - 鳴らすたびに、`randomness` のぶんだけ高さが揺れます - 長さ(`attack` `decay` `sustain` `release` `delay` の合計)は5秒までです。形が違うと、保存のときにエラーになります - 大きさは ZzFX の作る画面と同じです。`volume` を大きくしても、割れる所で切れます - スマホのスピーカーでは、低い音(`frequency` の小さい音)ほど小さく聞こえます - ZzFX の作る画面(https://killedbyapixel.github.io/ZzFX/)で作った並びも、そのまま貼れます。[音の試聴](/listen.html)では、並びを貼って鳴らせます ### 4.9 `interpolate` ルールは1秒に30回進みます。`true` にすると、絵をそれより多く描き、その間の姿を見た目のコードが補えるようになります。書かなければ、絵はルールが進むのと同じ1秒30回までです。 ```js interpolate: true, ``` - 補うのは見た目のコードです。vsboxは、1つ前の状態と、そこから今の状態までの割合を渡します([見た目(visual.js)の仕様](/docs/visual) 4.5)。補わなければ、同じ絵が続けて描かれるだけです - 絵を描く回数は、1秒に60回ほどまでです(画面の書き換えが60回・90回なら毎回、120回以上なら2回に1回) - 描くのが間に合わない端末では、試合の途中でも1秒30回に戻します。戻したあとも、割合は渡されます(いつも `1`) - ルールの動きは変わりません。勝ち負けにも、2人の画面が揃うことにも関わりません ## 5. 関数 ### 5.1 `setup(g)` ラウンドの始まりに1回呼ばれます。試合の始まりも、1ラウンド目の始まりです。 - 呼ばれる前に、物理の世界とエンティティは空になっています。体やエンティティは、ここで毎回作り直します - `g.state` は消えません。勝ち数のようにラウンドをまたぐ値はそのまま残ります。ラウンドごとに戻したい値は、ここで入れ直します ```js function setup(g) { if (g.round === 1) { g.state.wins = { player1: 0, player2: 0 }; } // 最初だけ g.state.timeLeft = 1800; // 毎ラウンド } ``` ### 5.2 `tick(g, input)` 1tickごとに呼ばれ、試合を1歩進めます。`input` には2人の入力が入っています(6. 入力)。 ### 5.3 `bot(g, owner)` CPUの席(`owner` は `"player1"` か `"player2"`)の入力を返します。返したものが、そのtickの `input[owner]` になります。 ```js function bot(g, owner) { const me = meta.opponents[g.cpu[owner]]; // いまの相手 return { cpu: { move: 1, fire: g.state.ready } }; } ``` - 人の入力と同じ形にする必要はありません。CPU専用のキー(上の `cpu`)で意図を返し、`tick` で見分けます(6.4) - `g.state` を読み、書くこともできます(「しばらく同じ目標を追う」のような記憶に使えます) - 世界を変える `g.*` は呼べません(呼ぶとエラー):`g.dynamicBody` `g.fixedBody` `g.kinematicBody` `g.spawn` `g.destroy` `g.applyForce` `g.applyImpulse` `g.applyImpulseAt` `g.setVelocity` `g.setAngularVelocity` `g.setFacing` `g.lockRotations` `g.teleport` `g.setNextPosition` `g.setNextFacing` `g.effect` `g.random` `g.deal` `g.deck` `g.show` `g.endRound` `g.winMatch` `g.drawMatch` - CPUに揺らぎを持たせるときは、`tick` か `setup` で `g.random` を引いて `g.state` に置き、`bot` はそれを読みます - 伏せた札のうち、そのCPUの席に見せたものは `g.peek(名前)` で読めます(7.6) ### 5.4 `visualState(g)` 見た目に渡す中身を返します。書かなければ `g.state` がそのまま渡ります。 見た目には毎tick中身の写しが渡るので、`g.state` に大きなデータ(CPUの読みの表など)があると重くなります。そのときは、見た目が使う欄だけを返します。 - 読むだけの関数です。`g.state` への書き込みと、世界を変える `g.*`(5.3と同じもの)はエラーになります - 関数の中で作った配列やオブジェクトには書けます(`range`・`keys`・`normalize`・`slice` が返したものも)。`abs` などに状態を渡して書き込み先にすることはできません ### 5.5 `controls(m, input)` 9. 手元の処理を見てください。 ## 6. 入力 `tick(g, input)` の `input` は、席の名前ごとの入力です。 ```js input.player1 = { w: 1000, h: 2164, // 画面の広さ dp: 393, // 画面の短い辺の長さ(dp) touches: [ { id, x, y, sx, sy, ray, pressed, released }, … ], keys: [ { code, pressed, released }, … ], } ``` ### 6.1 画面と指 | 名前 | 中身 | |---|---| | `w`、`h` | 画面の広さ。**短いほうの辺が1000**。縦持ちなら `w` は1000、`h` は2000前後 | | `dp` | 画面の短い辺の実際の長さ(dp。160dpがおよそ1インチ) | | `touches` | いま触れている指(最大20) | | `keys` | いま押しているキー(最大10。6.6) | | 指のキー | 中身 | |---|---| | `id` | 指の番号。押してから離すまで同じ。押すたびに新しい番号 | | `x`、`y` | 指の位置。画面の左上から、`w`・`h` と同じ物差し | | `sx`、`sy` | その指を押し始めた位置 | | `pressed` | 触れた瞬間のtickだけ `true` | | `released` | 離した瞬間のtickだけ `true`。速いタップでは `pressed` と同時に立つ | | `ray` | 指の先の世界の光線 `{ ox, oy, oz, dx, dy, dz }`。始点はカメラのすぐ前の点で、物理と同じ物差し(1000 = 見た目の場面の長さ1)。向きは長さ1000 | - 縦と横は同じ物差しなので、斜めの角度や円の当たり判定がそのまま使えます - 下の端からの位置は `h - 210` のように書きます。画面の縦横比は端末ごとに違います - 1tickの間に離して押し直すと、別の番号の2つの指として届きます - 速いフリックは1tickより短いことがあります。向きは `(x - sx, y - sy)` で取ります - 見た目の準備ができる前は、指は届きません(`touches` が空) - パソコンでは、マウスの左ボタンを押している間が1本の指として届きます(指と見分けは付きません)。押していないときのマウスの位置は届きません。2本の指を同時に使う操作はマウスではできないので、キーボード(6.6)で代えます - `dp` は、指の太さに合わせたい物を、どの端末でも同じ実寸にしたいときに使います。たとえば51.4dpの高さは `div(51400, dp)` です。届かないときがあるので、無ければ決めた値で代えます ### 6.2 世界の何かを指す `ray` を `g.rayHit`([物理のAPI](/docs/physics-api))で世界に当てると、盤のマスや駒が分かります。光線は入力として相手にも同じ値が届くので、当てた結果も2人で同じになります。 ```js for (const t of input[owner].touches) { if (!t.pressed) { continue; } const r = t.ray; const hit = g.rayHit(r.ox, r.oy, r.oz, r.dx, r.dy, r.dz, 200000); if (hit !== null) { g.state.picked = { x: hit.point.x, z: hit.point.z }; } } ``` ### 6.3 スティックとボタン vsboxは操作の部品を用意しません。画面全体が入力の面で、どこをスティックやボタンにするかは作者が決め、絵も見た目で描きます。スティックは次の形で書けます。 1. 押した瞬間に丸の中なら、その指の `id` を覚える(覚えないと、2本目の指に移ってしまう) 2. それからはその指だけを追い、中心からのずれを半径で頭打ちにする 3. 中心の近くは、倒していない扱いにする 4. 離したら忘れる つまみの位置を見た目に描かせるには、`g.state` に置きます。見た目は指を直接読めません。 ### 6.4 CPUの入力 CPUの入力は、`bot` が返したものです。指を作る必要はありません。CPU専用のキーで意図を返し、`tick` で見分けます。 ```js function intentOf(g, input, owner) { const inp = input[owner]; if (inp.cpu) { return inp.cpu; } // CPU return readPad(g, inp, owner); // 人(6.3 のスティック) } ``` ### 6.5 カメラを反対側に置いたとき 見た目で `player2` のカメラを反対側に回すと([見た目(visual.js)の仕様](/docs/visual) 3.1)、その人の画面は180度回ります。`x`・`y` は画面の座標なので、世界の向きに直すのはルールの仕事です。`ray` は実際のカメラから作るので、直す必要はありません。 ```js if (owner === "player2") { return { dx: -stick.dx, dy: -stick.dy }; } ``` ### 6.6 キーボード パソコンで押しているキーです。指と同じく、押した瞬間と離した瞬間に印が立ちます。 | キーのキー | 中身 | |---|---| | `code` | キーの場所の名前。`"KeyW"`、`"ArrowUp"`、`"Space"`、`"Enter"`、`"Digit1"` など | | `pressed` | 押した瞬間のtickだけ `true` | | `released` | 離した瞬間のtickだけ `true`。速く押して離すと `pressed` と同時に立つ | ```js let dx = 0; for (const k of input[owner].keys) { if (k.code === "Space" && k.pressed) { jump = true; } if (k.released) { continue; } if (k.code === "KeyA" || k.code === "ArrowLeft") { dx = dx - 1; } if (k.code === "KeyD" || k.code === "ArrowRight") { dx = dx + 1; } } ``` - `code` は文字ではなく、キーの**場所**です。英字の配列やかな入力、日本語入力のオンオフで変わりません(`"KeyA"` は、いつも A の位置のキー) - 届くのは、英字・数字・矢印・記号のキーと、スペース・Enter・Backspace・Tab・Shift です。Ctrl・Alt・Esc・F1〜F12 と、Ctrl や ⌘ と一緒に押したキーは、ブラウザの操作に使うので届きません - スマホでは、いつも空の配列です。指の操作は残したまま、キーの操作を足すのがふつうです - 押しっぱなしにしても、`pressed` は最初の1回だけです - 上下左右のキーは画面の向きです。カメラを反対側に置いたゲームでは、スティックと同じく向きを直します(6.5) ## 7. `g` のAPI ### 7.1 状態と時間 | 名前 | 中身 | |---|---| | `g.state` | 作者が自由に使う状態。入れ子にしてよい | | `g.tick` | いまのtick(試合の始まりが0) | | `g.round` | いまのラウンド(1から) | | `g.cpu` | 席ごとのCPUの番号 `{ player1, player2 }`。人の席は `-1`。`meta.opponents` が無ければCPUは `0` | ### 7.2 エンティティ エンティティは、物を入れておく入れ物です。使わなくてもかまいません(盤のゲームは `g.state` だけで書けます)。 | API | 中身 | |---|---| | `g.spawn({ … })` | 体を持たないエンティティを作って返す | | `g.destroy(e)` | エンティティを消す(体があれば体も) | | `g.entities` | すべてのエンティティの配列(作った順) | | `g.entitiesOfKind(kind)` | `kind` が同じものだけの配列 | ```js const coin = g.spawn({ kind: "coin", owner: null, x: 0, z: 3000, value: 10 }); coin.value = 20; // 自分のフィールドは書き換えられる ``` - `id`・`owner`・`kind` は、書かなければ `"e" + 番号`・`null`・`"entity"` になります。番号は、ラウンドが始まるたびに1から振り直します - それ以外のキーは、そのままエンティティのフィールドになります - `g.entities` は読むだけにします。足すのは `g.spawn`、消すのは `g.destroy` です - 物理の体を持つエンティティは `g.dynamicBody` などで作ります([物理のAPI](/docs/physics-api)) ### 7.3 ラウンドと決着 | API | 中身 | |---|---| | `g.endRound()` | ラウンドを区切る | | `g.winMatch(owner)` | `owner` の勝ちで試合を終える | | `g.drawMatch()` | 引き分けで試合を終える | - どれも、呼んだtickの終わりに効きます。1tickに効くのは最初の1回だけです - `g.endRound()` のあと1.5秒(45tick)止まり、物理の世界とエンティティを空にして `setup` が呼ばれます。`g.round` は1増えます - **vsboxは勝ち数を数えません。**何本先取か、引き分けをどう扱うかは、ゲームが `g.state` で数えて決めます ```js function scoreRound(g, winner) { if (winner !== null) { g.state.wins[winner] = g.state.wins[winner] + 1; if (g.state.wins[winner] >= 2) { g.winMatch(winner); return; } } g.endRound(); } ``` ### 7.4 見た目への出来事 `g.effect(name, data)` 爆発や得点のような「その瞬間に起きたこと」を、見た目に伝えます。`data` には好きな値を入れられます。 - 見た目には一度だけ届きます([見た目(visual.js)の仕様](/docs/visual)) - `meta.sounds` に同じ名前があれば、その音が鳴ります(4.8) - `data.owner` に席の名前を入れると、`{ self, other }` の音を鳴らし分けられます ```js g.effect("hit", { owner: "player1", x: e.x, z: e.z }); ``` ### 7.5 乱数 乱数は2種類あります。使い分けの基準は、**相手より先に値を知ると、選び方が変わるかどうか**です。 | | `g.random(n)` | `g.deal(名前, 個数, n)` | |---|---|---| | 値が出るとき | 呼んだその場 | 少しあと(届くまで0.1〜0.3秒ほど) | | 先に読めるか | 読める(改造した端末なら、これから出る値を計算できる) | 読めない | | 向いているもの | 盤面の配置、演出の揺らぎ | サイコロ、カードの山、くじ | | API | 中身 | |---|---| | `g.random(n)` | 0〜n-1の整数を返す。`n` を省くと、0〜4294967295の整数を返す | | `g.deal(名前, 個数, n)` | 0〜n-1の整数を、個数ぶん頼む | | `g.dealt(名前)` | 頼んだ値。届くまでは `null`、届くと整数の配列 | `g.random(n)` は、0〜n-1の整数を返します。`n` を省くと、0〜4294967295の整数を返します。 `g.deal(名前, 個数, n)` で頼み、届いたら `g.dealt(名前)` で受け取ります。 ```js if (rollPressed) { g.deal("dice", 5, 6); g.state.rolling = 1; } // 0〜5 を5個 const v = g.dealt("dice"); // 届くまで null。届くと [3, 0, 5, 5, 2] のような配列 if (g.state.rolling === 1 && v !== null) { g.state.dice = v; g.state.rolling = 0; } ``` - 名前は1〜40文字、個数は1〜64、`n` は1〜2147483647です - 届く前に同じ名前でもう一度頼むと、エラーになります - 次に同じ名前で頼むまで、`g.dealt` は同じ値を返します - 届くのを待てるのは、`g.show` と合わせて8つまでです - 待っている間は、見た目でつなぎます(サイコロを転がしておく、など) - CPU対戦では、次のtickに届きます ### 7.6 伏せた札 手札のように、**本人にしか見えてはいけないもの**を扱います。札の中身はサーバーが混ぜて、見せる相手の端末にだけ送ります。相手の端末には中身が届かないので、改造しても読めません。 ```js g.deck("cards", 52); // 0〜51 が1つずつ入った、誰にも見えない山 g.show("cards", [0, 1, 2], "player1"); // 位置0〜2の札を player1 にだけ見せる(配る) g.show("cards", [5], "all"); // 位置5の札を全員に明かす(出す) const s = g.shown("cards"); // 全員に明かした札の中身。ほかの位置は null if (s[5] !== null) { /* 明かした札で判定する */ } ``` | API | 中身 | |---|---| | `g.deck(名前, 枚数)` | 0〜枚数-1が1つずつ入った山を作る。同じ名前で呼ぶと、新しく混ぜ直した山になる | | `g.show(名前, 位置の配列, 誰に)` | その位置の札を見せる。誰に = `"player1"`・`"player2"`・`"all"`。中身は少しあとに届く | | `g.shown(名前)` | 全員に明かした札の中身の配列(ほかの位置は `null`)。山が無ければ `null` | | `g.peek(名前)` | `bot` の中でだけ使える。そのCPUの席に見せた札の中身の配列 | - **ルールが持つのは「位置」だけです。**本人だけに見せた札の中身は、ルールからは読めません(読めるのは見た目と、`controls` と、`bot` の `g.peek`) - 位置は相手に伝わってもかまいません。「位置3を伏せて出した」のような書き方ができます - 枚数は1〜256、1試合に作れる山は64個までです。`g.show` の位置は1回に64個までです - `bot`・`visualState`・`controls` の中では、`g.deck` と `g.show` を呼べません ### 7.7 整数の幾何 | API | 中身 | |---|---| | `g.sin(angle)`、`g.cos(angle)` | 角度のsin・cosを1000倍した整数。角度は1周 = 3600(0.1度きざみ) | | `g.rotate(x, y, angle)` | 点 `(x, y)` を原点のまわりに回した `{ x, y }` | 回るものは、前の位置から少しずつ回すのではなく、毎回 `g.rotate` で元の位置から角度ぶん回します。少しずつ回すと、丸めの誤差がたまって縮んでいきます。 ### 7.8 物理 体を作る・動かす・調べるAPI(`g.dynamicBody`・`g.applyImpulse`・`g.rayHit` など)は、[物理のAPI](/docs/physics-api)にあります。物理を使わないゲームは、1つも使わなくてかまいません。 ## 8. 書き方の例 ### 8.1 連鎖の書き方 爆弾の連鎖のように広がっていく処理は、再帰の代わりに「処理待ちの配列」と回数を決めたループで書きます。 ```js function explodeAll(g, first) { let queue = [first]; for (const step of range(0, 100)) { // 回数の上限を決める if (queue.length === 0) { break; } const next = []; for (const b of queue) { for (const other of g.entitiesOfKind("bomb")) { if (other.lit === 0 && near(b, other)) { other.lit = 1; next.push(other); } } } queue = next; } } ``` ## 9. 手元の処理(controls) 手札を強さの順に並べて見せるゲームでは、**触った場所から札の中身が分かってしまいます**(「右端を触った」は、いちばん強い札)。触った指は相手の端末にも届くので、そういうゲームは `controls(m, input)` を書きます。 `controls` は自分の端末でだけ動き、触った指を「行動」(整数の配列)に直します。相手に届くのは、返した配列だけです。ルールには `input[席].pick` として届きます。 `controls` の `input` は、**自分1人ぶんの指の入力**(`{ w, h, dp, touches }`。6.1)です。`tick` の `input` のように、席ごとには分かれていません。 ```js function controls(m, input) { const hand = m.view("cards"); // 自分に見えている札の中身 // 並べ替え・触った札の判定・選んでいる札の印(m.memo)・出せるかの確認 if (pressedPlay && legal) { return chosenPositions; } // 出す札の位置 return []; // 何もしないtick } function tick(g, input) { const p = input[g.state.turn]; if (p && p.pick && p.pick.length > 0) { g.show("cards", p.pick, "all"); } // 明かしてから判定する } ``` | `m` | 中身 | |---|---| | `m.role` | 自分の席 | | `m.state` | ルールの状態(写し。読むだけ) | | `m.view(名前)` | 自分に見えている山の中身(位置ごと。見えていない位置は `null`) | | `m.memo` | 自分だけの覚え書き。相手には届かない。見た目には届く | - `controls` を書いたゲームでは、人の入力は `{ pick: [...] }` だけになります。指と画面の広さはルールに届かないので、当たり判定は `controls` と見た目で行います - 返すのは、そのtickの行動です(整数を最大64個)。何もしないtickは `[]` を返します - CPUの `bot` も `{ pick: [...] }` を返すと、人とCPUを `tick` で分けずに済みます。何もしないtickは `{}` でかまいません - **返した数は相手に見えます。**中身ではなく位置を返します - 相手は嘘の行動も送れます。ルールは明かした中身で確かめます(出せない組なら反則にする、など) - 読むだけの関数です。書けるのは、関数の中で作ったものと `m.memo` だけです - 隠す情報が無いゲームでは書きません