# 物理のAPI ルール(`game.js`)から使える、3Dの剛体物理のAPIです。物を落とす、ぶつける、押し合う、乗る、といった動きを、vsboxが計算します。 物理を使わないゲームは、この文書を読まなくてかまいません。ルールの書き方は[ルール(game.js)の仕様](/docs/lang-spec)にあります。 物理の計算には、物理エンジンRapierを使っています。ただし、APIの名前・引数・単位はRapierとは違います。この文書のとおりに書いてください。 --- ## 1. 単位と向き 物理の値は、すべて整数です。 | 量 | 単位 | |---|---| | 長さ | 1000 = 1単位 | | 速さ | 1000 = 1単位/秒 | | 加速度(重力) | 1000 = 1単位/秒² | | 角度 | 3600 = 1周(1 = 0.1度) | | 角速度 | 1 = 0.1度/秒(3600 = 1秒に1周) | | 質量 | 1000 = 1 | | 摩擦・反発・減衰・重力の効き | 1000 = 1.0 | - **yが上です。**地面はx-zの平面です - 見た目の場面では、1単位が長さ1です([見た目(visual.js)の仕様](/docs/visual)) - 体の向きは `[yaw, pitch, roll]` で書きます。y軸 → x軸 → z軸 の順に回します - 体の前は、体から見た+xの向きです。上は+yの向きです ## 2. 世界の宣言 物理の体を1つでも作るゲームは、`meta` に重力を書きます。 ```js const meta = { gravity: [0, -40000, 0], // 下向きに40単位/秒² layers: ["ground", "body", "bullet"], // 任意。衝突レイヤー(4.3) }; ``` | 項目 | 中身 | |---|---| | `gravity` | `[x, y, z]` の整数3つ。各軸 ±1000000(±1000単位/秒²)まで | | `layers` | 衝突レイヤーの名前の配列。最大16個。同じ名前は使えない | - `gravity` を書かずに体を作ると、エラーになります。重力に既定の値はありません - 迷ったら `[0, -40000, 0]` から始めます(9.1) ## 3. 1tickの流れ 物理は、1tickに1回だけ進みます。順番は次のとおりです。 1. `tick` が呼ばれる。読める値(`e.x` や `g.contacts()` など)は、前のtickに物理が進んだあとの結果 2. `tick` の中で呼んだ命令(`g.applyImpulse` など)は、その場では効かず、積まれる 3. `tick` が終わると、積まれた命令が呼んだ順に効き、物理が1/30秒進む > **注意** 命令の結果が読めるのは、次のtickからです。`e.grounded` や `g.contacts()` も、1tick前の結果です。 ## 4. 体を作る 体を作る関数は3つあります。どれも、体を持つエンティティを作って返します。 | 関数 | 体の種類 | 向いているもの | |---|---|---| | `g.dynamicBody({ … })` | 力と衝突で動く | キャラ、玉、箱 | | `g.fixedBody({ … })` | 動かない | 床、壁、坂 | | `g.kinematicBody({ … })` | 作者が位置を決める。ぶつかった物を押しのける | 動く床、回る棒 | ```js const runner = g.dynamicBody({ kind: "runner", owner: "player1", at: [0, 2000, 0], facing: [900, 0, 0], lockRotations: true, mass: 1000, friction: 0, colliders: [{ shape: { type: "capsule", r: 300, half: 200 } }], hp: 3, // 作者のフィールド }); ``` | キー | 中身 | 書かないとき | |---|---|---| | `colliders` | 形の配列(4.1)。1〜8個。**必須** | エラー | | `at` | 位置 `[x, y, z]`。各軸 ±500000(±500単位)まで | `[0, 0, 0]` | | `facing` | 向き `[yaw, pitch, roll]` | `[0, 0, 0]` | | `id`、`owner`、`kind` | エンティティの名前・持ち主・種類([ルール(game.js)の仕様](/docs/lang-spec) 7.2) | | | `friction` | 摩擦。形ごとに書かなければ、これを使う | `500`(0.5) | | `bounce` | 反発。形ごとに書かなければ、これを使う | `0` | | `mass` | 質量(4.4)。1以上 | 形の体積から決まる | | `lockRotations` | `true` なら回らない(4.5) | `false` | | `damping` | 速さの減衰(6.4)。0以上 | `0` | | `angularDamping` | 回転の減衰。0以上 | `0` | | `gravityScale` | 重力の効き。`0` で重力を受けない(まっすぐ飛ぶ弾など) | `1000` | | そのほか | 作者のフィールドとして、そのまま載る | | - `mass`・`damping`・`angularDamping`・`gravityScale` は、`g.dynamicBody` にだけ書けます(ほかに書くとエラー) - 消すときは `g.destroy(e)` です。体も一緒に消えます ### 4.1 形 ```js colliders: [ { shape: { type: "box", size: [4000, 400, 1000] } }, { shape: { type: "ball", r: 600 }, at: [0, 900, 0], sensor: true, layer: "hitbox", hits: ["body"] }, ] ``` 形は4種類です。 | `type` | 大きさ | 全体の大きさ | |---|---|---| | `"ball"` | `r`(半径) | 直径 `2 × r` | | `"box"` | `size: [幅, 高さ, 奥行き]` | 書いたとおり | | `"capsule"` | `r`(半径)、`half`(まっすぐな部分の長さの半分) | 高さ `2 × (half + r)`、y軸に立つ | | `"cylinder"` | `r`(半径)、`half`(高さの半分) | 高さ `2 × half`、y軸に立つ | 形ごとに書けるキーです。 | キー | 中身 | |---|---| | `shape` | 上の形。**必須** | | `at` | 体の中心からの位置 `[x, y, z]` | | `facing` | 体から見た向き `[yaw, pitch, roll]` | | `friction`、`bounce` | この形の摩擦と反発。書かなければ体の値 | | `sensor` | `true` ならセンサー(4.2) | | `layer`、`hits` | 衝突レイヤー(4.3) | - 2つの物が触れたときの摩擦と反発は、両方の値の平均です ### 4.2 センサー `sensor: true` の形は、ほかの物を押し返さず、すり抜けます。代わりに、物が入ったときと出たときに `g.sensorEvents()`(7.2)に出ます。ゴールの線、拾える物、攻撃の当たり判定に使います。動かない体や動く床が入ったときも出ます。 ### 4.3 衝突レイヤー 当たる組み合わせを、`meta.layers` で決めた名前で選べます。 ```js { shape: { type: "ball", r: 200 }, layer: "bullet", hits: ["ground", "body"] } // 弾どうしは当たらない ``` - `layer` はその形が属するレイヤー、`hits` は当たる相手のレイヤーです - **2つの形が当たるのは、お互いの `hits` に相手の `layer` が入っているときだけです** - `layer` を書かない形は、`meta.layers` の最初の名前に属します - `hits` を書かない形は、すべてのレイヤーと当たります - `meta.layers` を書かなければ、すべての形が当たります ### 4.4 質量 `mass` を書かなければ、形の体積から決まります。1単位の立方体(`size: [1000, 1000, 1000]`)の質量が1(`1000`)です。 `mass` を書くと、形の割合はそのままで、全体の重さだけが変わります。 > **ヒント** 動く床や回る棒(`kinematicBody`)にぶつかったときは、重さに関係なく、同じだけ飛ばされます。 ### 4.5 回らない体 `lockRotations` `lockRotations: true` の体は、ぶつかっても回りません。立って歩くキャラに使います。 `true` にしても、vsboxは段差を持ち上げたり、地面に吸い付けたりしません(9.4)。 ## 5. 読む 体を持つエンティティからは、次の値が読めます。どれも読むだけで、代入するとエラーになります。変えるときは命令(6)を使います。 | 名前 | 中身 | |---|---| | `e.x`、`e.y`、`e.z` | 位置 | | `e.vx`、`e.vy`、`e.vz` | 速さ | | `e.angularVelocity` | 角速度 `{ x, y, z }` | | `e.fwd` | 前の向き `{ x, y, z }`(長さ1000) | | `e.up` | 上の向き `{ x, y, z }`(長さ1000) | | `e.grounded` | 足が着いているか(下) | | `e.rotationsLocked` | いま回転を止めているか | | `e.sleeping` | 止まって眠っているか(命令を受けると起きる) | **`e.grounded`** は、前のtickの物理で、この体が上向きから45度以内の面に触れていれば `true` です。センサーは数えません。相手の体の種類は問いません(ほかのキャラの上も地面です)。 45度より急な坂の上では `false` になります。もっと細かく判定したいときは、`g.touching(e)`(7.3)の向きを読んで、自分で決めます。 ## 6. 動かす 体を動かす命令です。どれも1つ目に体を持つエンティティを渡します。命令は積まれて、`tick` が終わったあとに呼んだ順に効きます(3)。 | 命令 | 中身 | 効く体 | |---|---|---| | `g.applyForce(e, fx, fy, fz)` | そのtickの間だけ、力をかける | 動く体 | | `g.applyImpulse(e, ix, iy, iz)` | 一瞬の衝撃を与える(ジャンプ、爆風) | 動く体 | | `g.applyImpulseAt(e, ix, iy, iz, px, py, pz)` | 世界の点 `(px, py, pz)` に衝撃を与える。中心から外れると回る | 動く体 | | `g.setVelocity(e, vx, vy, vz)` | 速さを決める | 動く体 | | `g.setAngularVelocity(e, wx, wy, wz)` | 角速度を決める | 動く体 | | `g.setFacing(e, yaw, pitch, roll)` | 向きを決める | すべて | | `g.lockRotations(e, on)` | 回転を止める(`true`)・戻す(`false`) | 動く体 | | `g.teleport(e, x, y, z)` | その場所へ移す。**速さと角速度は0になる** | すべて | | `g.setNextPosition(e, x, y, z)` | 次の位置。そこへ動く途中の物を押しのける | 作者が動かす体 | | `g.setNextFacing(e, yaw, pitch, roll)` | 次の向き。回る途中の物を押しのける | 作者が動かす体 | - 値はすべて整数です。小数や範囲の外の値は、その場でエラーになります - **「効く体」の列にない体に呼ぶと、その場でエラーになります**(たとえば作者が動かす体に `g.setVelocity`。何も起きないまま進むことはありません) - 位置(`teleport`、`setNextPosition`、`applyImpulseAt` の点)は、各軸 ±500単位までです - 消した体に命令を出すと、エラーになります ### 6.1 力と衝撃で、速さがどれだけ変わるか | 命令 | 速さの変わり方(1000 = 1単位/秒) | |---|---| | `g.applyForce(e, F, 0, 0)` | そのtickに `F × 1000 ÷ mass ÷ 30` 増える | | `g.applyImpulse(e, I, 0, 0)` | すぐに `I × 1000 ÷ mass` 増える | たとえば質量 `1000` の体に、力 `30000` をかけると1tickで速さが `1000`(1単位/秒)増え、衝撃 `5000` を与えると `5000`(5単位/秒)増えます。 `applyForce` は、そのtickだけ効きます。かけ続けるなら、毎tick呼びます。 ### 6.2 動く体の動かし方は2通り 動く体は、力(`applyForce`)でも、速さ(`setVelocity`)でも動かせます。混ぜてもかまいません。 | | 力で動かす | 速さを決めて動かす | |---|---|---| | 加速と最高速度 | 質量と減衰(6.4)で決まる | 作者が書く(書かなければすぐ最高速度) | | 止まり方 | 摩擦と減衰で止まる | 作者が書く(書かなければ止まらない) | | 押されたとき | そのまま流される | 流されにくい(毎tick速さを戻すので) | | 重さ | 効く | 効かない | | 向いているもの | 押し合い、乗り物、重さのあるもの | 操作にすぐ応えてほしいキャラ | - `setVelocity` で決めた速さも、そのあと物理が変えます。床の摩擦で少し遅くなり、壁に当たれば止まります - `setVelocity` は、200単位/秒(`200000`)を超える速さを作れません(すり抜けを防ぐため)。いまの速さより速くしないなら、超えていても通ります(落ち続けて速くなった体の `e.vy` をそのまま渡すときなど) ### 6.3 同じtickの命令は、あとのものが勝つ 命令は呼んだ順に効くので、`setVelocity` は、それより前に積んだ衝撃を上書きします。 ```js // ✗ ジャンプが消える(e.vy はまだ跳ぶ前の速さ) g.applyImpulse(e, 0, JUMP, 0); g.setVelocity(e, vx, e.vy, vz); // ○ 横の速さを決めてから跳ぶ g.setVelocity(e, vx, e.vy, vz); g.applyImpulse(e, 0, JUMP, 0); ``` エラーにはならず、ただジャンプしなくなります。 ### 6.4 減衰 `damping` `damping` は、速さを毎tick割合で減らします。速いほどたくさん減るので、力で動かす体に最高速度ができます。 ``` 毎tick 速さ ← 速さ ÷ (1 + damping ÷ 1000 ÷ 30) ``` | `damping` | 速さが半分になるまで | 1秒後に残る速さ | |---|---|---| | `300` | 約2.3秒 | 約74% | | `1000` | 約0.7秒 | 約37% | | `2000` | 約0.36秒 | 約14% | | `6000` | 約0.13秒 | 約0.4% | - 減衰は質量に関係しません - 床の摩擦は、速さに関係なく毎tick同じだけ減らします。摩擦だけでは最高速度はできません - 力で動かす体の最高速度は、押す力と、摩擦・減衰の釣り合いで決まります。減衰を上げると、止まりやすくなる代わりに最高速度が下がるので、押す力も一緒に上げます ## 7. 調べる ### 7.1 `g.contacts()` 前のtickの物理で、**触れ始めた**体の組の配列です。触れている間ずっとではありません。センサーは入りません。 ```js for (const c of g.contacts()) { // c.a, c.b はエンティティ(順番は決まっていない)。c.force はぶつかった強さ if (c.a.kind === "bullet" || c.b.kind === "bullet") { … } } ``` | キー | 中身 | |---|---| | `a`、`b` | 触れ始めた2つのエンティティ | | `force` | ぶつかったときに押し合った力の大きさ。強くぶつかったほど大きい | ### 7.2 `g.sensorEvents()` 前のtickの物理で、センサー(4.2)に物が入った・出たことの配列です。 | キー | 中身 | |---|---| | `sensor` | センサーを持つエンティティ | | `entity` | 入った・出たエンティティ | | `entered` | 入ったなら `true`、出たなら `false` | ### 7.3 `g.touching(e)` `e` がいま触れている相手の配列です。`[{ entity, normal }]` で、`normal` は相手から `e` へ向かう向き(長さ1000)です。床の上に立っていれば、床の `normal.y` は `1000` に近くなります。 上から乗ったのか、横からぶつかったのかは、`normal` で見分けます。 ### 7.4 `g.rayHit(ox, oy, oz, dx, dy, dz, maxDist, ignore)` 点 `(ox, oy, oz)` から向き `(dx, dy, dz)` へ光線を飛ばし、最初に当たった物を返します。`maxDist` より遠くは見ません。何にも当たらなければ `null` です。`ignore`(省ける)に体を持つエンティティを渡すと、その体には当たりません。 ```js // 自分の体の中心から真下へ5単位まで。自分の体は除く const h = g.rayHit(e.x, e.y, e.z, 0, -1000, 0, 5000, e); if (h !== null) { g.state.height = h.dist; } // 体の中心から地面までの高さ ``` | キー | 中身 | |---|---| | `entity` | 当たったエンティティ | | `point` | 当たった点 `{ x, y, z }` | | `normal` | 当たった面の向き `{ x, y, z }`(長さ1000) | | `dist` | 始点から当たった点までの長さ | - 向きの長さは、いくつでもかまいません(0はエラー) - `ignore` を渡さなければ、**自分の体も除きません。**形の中から出すと、その形に距離0で当たります - センサーにも当たります - 足が着いているかは、光線より `e.grounded`(5)や `g.touching(e)`(7.3)のほうが確かです - 指の先の光線(`input` の `ray`)をそのまま渡すと、指した物が分かります([ルール(game.js)の仕様](/docs/lang-spec) 6.2) ## 8. 上限 | もの | 上限 | |---|---| | 動く体(`dynamicBody`) | 64 | | 動かない体(`fixedBody`) | 256 | | 形の合計 | 512 | | 1つの体の形 | 8 | | 衝突レイヤー | 16 | | 位置 | 各軸 ±500単位 | | `setVelocity` で作れる速さ | 200単位/秒 | | 1tickに作れる体 | 32(`setup` の中は数えない) | | 1tickの `g.rayHit` | 64回 | 超えると、その場でエラーになります。 ## 9. ヒント ### 9.1 立って歩くキャラ 走って跳ぶキャラは、次の値から始めると扱いやすくなります。 | | 値 | |---|---| | 重力 | `meta.gravity: [0, -40000, 0]` | | 形 | `capsule`、`r: 330`、`half: 170`(全高1単位) | | 質量 | `mass: 164` | | 摩擦 | `friction: 0` | | 回転 | `lockRotations: true` | | 走る | 毎tick `g.setVelocity(e, vx, e.vy, vz)`。速さ `6000`(6単位/秒) | | 跳ぶ | `e.grounded` のとき `g.applyImpulse(e, 0, 1882, 0)`。およそ1.6単位跳び、0.6秒弱で着地する | > **注意** `e.grounded` は1tick前の結果なので、跳んだ次のtickにもまだ `true` のことがあります。跳んだら8tickほど、次のジャンプを受け付けないようにします。そうしないと、衝撃が2回重なって高く跳びすぎます。 - 重力の数字は、ほかのゲームエンジンとは比べられません(1単位が何メートルかが違います)。ほかのゲームを参考にするときは、滞空時間と、跳ぶ高さが身長の何倍かで比べます - 重力を上げると、同じ高さを跳んでも滞空が短くなり、きびきびします ### 9.2 摩擦 - **壁に触れても摩擦は効きます。**摩擦が高いと、壁にこすれて遅くなり、壁に押し当てたまま落ちなくなります。壁のあるコースを走るキャラは、摩擦を `0` にすると素直に動きます - **横に押して坂を登るとき、摩擦が高いほど登れる角度は下がります。**押すほど坂に押し付けられて、摩擦も増えるためです。登らせたい坂は、角度をゆるくします - 摩擦が `0` の体は、坂の上で止まっていられず滑り落ちます(`setVelocity` を書くのをやめたときなど) ### 9.3 倒れないキャラ・倒れて起きるキャラ 底の丸い形(`capsule`)は、太くしても倒れやすさが変わりません。立たせておくなら `lockRotations: true` を使います。一度倒れた `capsule` は、自分では起き上がりません。 倒して起こすには、次のように書きます。 ```js // 倒す g.lockRotations(e, false); g.applyImpulseAt(e, ix, iy, iz, e.x, e.y + 400, e.z); // 頭のあたりを押す // 起こす(回転がおさまったら) g.setFacing(e, 0, 0, 0); g.setAngularVelocity(e, 0, 0, 0); g.lockRotations(e, true); ``` 向きを戻さずに回転だけ止めると、寝たまま起き上がれません。起こした直後は足が床に少し入りますが、物理が押し戻します。すぐに立たせたいときは `g.teleport` で高さを決めます。 ### 9.4 段差 vsboxは、段差を持ち上げる補助をしません。キャラは、足の丸みと速さで乗り越えられる、ごく低い段差しか越えられません。それより高い段差は、坂にするか、ジャンプで越えるようにします。 ### 9.5 押し当て続けると、めり込む 動く床や回る棒に向かって押し続けると、キャラが少しめり込むことがあります。押していなければ、めり込みません。気になるときは、`g.touching(e)` の向き(`normal`)に向かう移動を消します。 ### 9.6 ぶつかったあとの勢い 毎tick `setVelocity` で速さを決めていると、弾き飛ばされた勢いは次のtickで消えます。勢いを残したいときは、当たってから数tick、移動の入力を受け付けないようにします。 ### 9.7 物理を使わないゲーム 盤のゲームや、マス目で動くゲームは、物理を使わなくても書けます。そのときは `g.state` と `g.spawn` だけで書き、`meta.gravity` も書きません。