物理のAPI
ルール(game.js)から使える、3Dの剛体物理のAPIです。物を落とす、ぶつける、押し合う、乗る、といった動きを、vsboxが計算します。物理を使わないゲームは、この文書を読まなくてかまいません。ルールの書き方はルール(game.js)の仕様にあります。
物理の計算には、物理エンジン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)の仕様)
- 体の向きは
[yaw, pitch, roll]で書きます。y軸 → x軸 → z軸 の順に回します - 体の前は、体から見た+xの向きです。上は+yの向きです
2. 世界の宣言
物理の体を1つでも作るゲームは、meta に重力を書きます。
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回だけ進みます。順番は次のとおりです。
tickが呼ばれる。読める値(e.xやg.contacts()など)は、前のtickに物理が進んだあとの結果tickの中で呼んだ命令(g.applyImpulseなど)は、その場では効かず、積まれるtickが終わると、積まれた命令が呼んだ順に効き、物理が1/30秒進む
注意 命令の結果が読めるのは、次のtickからです。
e.groundedやg.contacts()も、1tick前の結果です。
4. 体を作る
体を作る関数は3つあります。どれも、体を持つエンティティを作って返します。
| 関数 | 体の種類 | 向いているもの |
|---|---|---|
g.dynamicBody({ … }) | 力と衝突で動く | キャラ、玉、箱 |
g.fixedBody({ … }) | 動かない | 床、壁、坂 |
g.kinematicBody({ … }) | 作者が位置を決める。ぶつかった物を押しのける | 動く床、回る棒 |
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)の仕様 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 形
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 で決めた名前で選べます。
{ 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 は、それより前に積んだ衝撃を上書きします。
// ✗ ジャンプが消える(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の物理で、触れ始めた体の組の配列です。触れている間ずっとではありません。センサーは入りません。
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(省ける)に体を持つエンティティを渡すと、その体には当たりません。
// 自分の体の中心から真下へ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)の仕様 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 は、自分では起き上がりません。
倒して起こすには、次のように書きます。
// 倒す
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 も書きません。