ルール(game.js)の仕様
vsboxのゲームは、ルール(game.js)と見た目(visual.js)の2つのファイルでできています。この文書は、ルールの書き方をまとめたものです。見た目は見た目(visual.js)の仕様、物理は物理のAPIにあります。
ルールは、2人の端末でまったく同じ計算をして試合を進めます。そのため、ルールはJavaScriptの一部だけを使う決まった書き方で書きます。決まりに合わない書き方は、投稿したときにエラーになります。
1. 全体の形
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 |
配列やオブジェクトどうしの === は、同じものかどうかで比べます(中身が同じでも、別に作ったものは別)。
要素を条件で取り除くときは、残す要素で新しい配列を作ります。
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 は、読むことしかできません。変数への代入も、中の配列やオブジェクトの書き換えもエラーになります。
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に伝える宣言です。どの項目も省略できます。
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) |
layers | 衝突レイヤーの名前の配列 | すべてが当たる(物理の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)の仕様 3) - 一覧に並ぶカードの下地。サムネイルを撮る前は、この色だけが出ます。横のゲーム(4.4)のサムネイルでは、上下の余白の色になります
空や地面に近い色にしておくと、読み込みの途中やサムネイルの前でも、ゲームの雰囲気が伝わります。
4.4 orientation
試合画面を縦長と横長のどちらで作るかです。書かなければ縦です。
orientation: "landscape",
"landscape"のゲームは、スマホが縦のままでも、vsboxが画面を90度回して横長に描きます。遊ぶ人はスマホを横に持つだけです(画面の回転をロックしていても遊べます)- 指の位置(
inputのxywh)も、横長の画面の向きで届きます。ゲームは回っていることを気にしなくてよく、横向きの1通りだけ作れば足ります - 回すのは試合画面だけです。一覧やゲームの紹介の画面は縦のままです
- サムネイルは横長(4:3)で撮ります。縦の一覧のカードには、上下に余白(
frameColor)を付けて収めます
4.5 devices
どの端末で遊べるゲームかです。書かなければ "mobile" です。
devices: "both",
| 値 | 意味 |
|---|---|
"mobile" | 指で操作する端末(スマホ・タブレット) |
"pc" | マウスとキーボードで操作する端末(パソコン) |
"both" | 両方 |
- 遊べない端末では、ホームの一覧に出ません(
"mobile"のゲームはパソコンの一覧に、"pc"のゲームはスマホの一覧に出ません)。開発者タブの一覧には、いつも出ます - 端末は、主な操作が指かマウスかで見分けます。キーボードを付けたタブレットは
"mobile"の側です - パソコンでは、マウスの左ボタンを押している間が1本の指として届きます(6.1)。押して選ぶだけのゲームなら、何も足さずに
"both"にできます - 動きながらボタンを押すような、2本の指を同時に使う操作は、マウスではできません。パソコンでも遊べるようにするなら、キーボードの操作(6.6)が要ります
- 遊んでいる端末がスマホかパソコンかは、見た目の側でも分かります(見た目(visual.js)の仕様 4.4)。端末で絵を変えるかどうかは自由です
- 遊び方の操作は、
metaではなく作品の「操作」の欄に書きます(スマホとパソコンで別。作って公開するまで 2) "both"や"pc"にするなら、パソコンで開いて確かめてください
4.6 opponents
CPU対戦で選べる相手の一覧です。最大12体です。
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
勝ち負けに関わらない見た目(サイコロの色、キャラの服、背景など)を、遊ぶ人が選べるようにします。選んだものは見た目にだけ届き、ルールには届きません。
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)の仕様にあります
4.8 sounds
g.effect(名前, data)(7.4)を出したときに鳴らす音を決めます。最大20個です。音は3通りで書けます。
| 書き方 | 例 | 中身 |
|---|---|---|
| 棚の音 | "@card-place-1" | vsboxが置いている録音の音。一覧は音の棚 |
| 数の並び | "[,,925,.04,.3,.6,1,.3,,6.27,-184,.09,.17]" | 数から音を作る(ZzFX)。電子音になりやすい |
| ファイル名 | "boom.mp3" | 作品に持ち込んだ音のファイル |
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 の並びを、そのまま " " で囲んで書きます(game.js には小数が無いため、文字にします)。
- 並びの順は ZzFX と同じです:
volumerandomnessfrequencyattacksustainreleaseshapeshapeCurveslidedeltaSlidepitchJumppitchJumpTimerepeatTimenoisemodulationbitCrushdelaysustainVolumedecaytremolofilter。空けた所は ZzFX のいつもの値です - 鳴らすたびに、
randomnessのぶんだけ高さが揺れます - 長さ(
attackdecaysustainreleasedelayの合計)は5秒までです。形が違うと、保存のときにエラーになります - 大きさは ZzFX の作る画面と同じです。
volumeを大きくしても、割れる所で切れます - スマホのスピーカーでは、低い音(
frequencyの小さい音)ほど小さく聞こえます - ZzFX の作る画面(https://killedbyapixel.github.io/ZzFX/)で作った並びも、そのまま貼れます。音の試聴では、並びを貼って鳴らせます
4.9 interpolate
ルールは1秒に30回進みます。true にすると、絵をそれより多く描き、その間の姿を見た目のコードが補えるようになります。書かなければ、絵はルールが進むのと同じ1秒30回までです。
interpolate: true,
- 補うのは見た目のコードです。vsboxは、1つ前の状態と、そこから今の状態までの割合を渡します(見た目(visual.js)の仕様 4.5)。補わなければ、同じ絵が続けて描かれるだけです
- 絵を描く回数は、1秒に60回ほどまでです(画面の書き換えが60回・90回なら毎回、120回以上なら2回に1回)
- 描くのが間に合わない端末では、試合の途中でも1秒30回に戻します。戻したあとも、割合は渡されます(いつも
1) - ルールの動きは変わりません。勝ち負けにも、2人の画面が揃うことにも関わりません
5. 関数
5.1 setup(g)
ラウンドの始まりに1回呼ばれます。試合の始まりも、1ラウンド目の始まりです。
- 呼ばれる前に、物理の世界とエンティティは空になっています。体やエンティティは、ここで毎回作り直します
g.stateは消えません。勝ち数のようにラウンドをまたぐ値はそのまま残ります。ラウンドごとに戻したい値は、ここで入れ直します
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] になります。
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.dynamicBodyg.fixedBodyg.kinematicBodyg.spawng.destroyg.applyForceg.applyImpulseg.applyImpulseAtg.setVelocityg.setAngularVelocityg.setFacingg.lockRotationsg.teleportg.setNextPositiong.setNextFacingg.effectg.randomg.dealg.deckg.showg.endRoundg.winMatchg.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)
- 手元の処理を見てください。
6. 入力
tick(g, input) の input は、席の名前ごとの入力です。
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)で世界に当てると、盤のマスや駒が分かります。光線は入力として相手にも同じ値が届くので、当てた結果も2人で同じになります。
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は操作の部品を用意しません。画面全体が入力の面で、どこをスティックやボタンにするかは作者が決め、絵も見た目で描きます。スティックは次の形で書けます。
- 押した瞬間に丸の中なら、その指の
idを覚える(覚えないと、2本目の指に移ってしまう) - それからはその指だけを追い、中心からのずれを半径で頭打ちにする
- 中心の近くは、倒していない扱いにする
- 離したら忘れる
つまみの位置を見た目に描かせるには、g.state に置きます。見た目は指を直接読めません。
6.4 CPUの入力
CPUの入力は、bot が返したものです。指を作る必要はありません。CPU専用のキーで意図を返し、tick で見分けます。
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)の仕様 3.1)、その人の画面は180度回ります。x・y は画面の座標なので、世界の向きに直すのはルールの仕事です。ray は実際のカメラから作るので、直す必要はありません。
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 と同時に立つ |
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 が同じものだけの配列 |
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)
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で数えて決めます
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)の仕様)
meta.soundsに同じ名前があれば、その音が鳴ります(4.8)data.ownerに席の名前を入れると、{ self, other }の音を鳴らし分けられます
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(名前) で受け取ります。
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 伏せた札
手札のように、本人にしか見えてはいけないものを扱います。札の中身はサーバーが混ぜて、見せる相手の端末にだけ送ります。相手の端末には中身が届かないので、改造しても読めません。
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にあります。物理を使わないゲームは、1つも使わなくてかまいません。
8. 書き方の例
8.1 連鎖の書き方
爆弾の連鎖のように広がっていく処理は、再帰の代わりに「処理待ちの配列」と回数を決めたループで書きます。
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 のように、席ごとには分かれていません。
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だけです - 隠す情報が無いゲームでは書きません