作者向けの文書 › physics-api ・ 元の文書(.md)

物理の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

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個。同じ名前は使えない

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({ … })作者が位置を決める。ぶつかった物を押しのける動く床、回る棒
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以上形の体積から決まる
lockRotationstrue なら回らない(4.5)false
damping速さの減衰(6.4)。0以上0
angularDamping回転の減衰。0以上0
gravityScale重力の効き。0 で重力を受けない(まっすぐ飛ぶ弾など)1000
そのほか作者のフィールドとして、そのまま載る

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この形の摩擦と反発。書かなければ体の値
sensortrue ならセンサー(4.2)
layer、hits衝突レイヤー(4.3)

4.2 センサー

sensor: true の形は、ほかの物を押し返さず、すり抜けます。代わりに、物が入ったときと出たときに g.sensorEvents()(7.2)に出ます。ゴールの線、拾える物、攻撃の当たり判定に使います。動かない体や動く床が入ったときも出ます。

4.3 衝突レイヤー

当たる組み合わせを、meta.layers で決めた名前で選べます。

{ shape: { type: "ball", r: 200 }, layer: "bullet", hits: ["ground", "body"] }   // 弾どうしは当たらない

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)次の向き。回る途中の物を押しのける作者が動かす体

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速さを戻すので)
重さ効く効かない
向いているもの押し合い、乗り物、重さのあるもの操作にすぐ応えてほしいキャラ

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%

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始点から当たった点までの長さ

8. 上限

もの上限
動く体(dynamicBody)64
動かない体(fixedBody)256
形の合計512
1つの体の形8
衝突レイヤー16
位置各軸 ±500単位
setVelocity で作れる速さ200単位/秒
1tickに作れる体32(setup の中は数えない)
1tickの g.rayHit64回

超えると、その場でエラーになります。

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回重なって高く跳びすぎます。

9.2 摩擦

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 も書きません。

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