はじめに
PlayCanvasの物理演算は、オープンソースのBullet物理エンジンをWebAssemblyに移植したammo.jsによって動作します。読み込みが済んだら、エンティティがどのように動くかを決めるRigidBodyコンポーネントと、形状を与えるCollisionコンポーネントを持つエンティティからシミュレーションを組み立てます。このページでは、ライブラリの読み込み、グローバルな設定、そしてこのセクションの他のすべてのページが前提とするルールを扱います。
物理を有効化
物理演算はオプトインです。ammo.jsは数百キロバイトのサイズがあるため、要求しない限り何も読み込みません。配布は3つのファイルで行われます。WebAssemblyモジュールのammo.wasm.wasm、そのJavaScriptグルーであるammo.wasm.js、そしてWebAssemblyをサポートしないブラウザ向けのasm.jsフォールバックであるammo.jsです。ライブラリが存在するまで、物理コンポーネントは何もしないプレースホルダーです。追加しても何も起こらず、エラーも発生しません。
- Engine
- Editor
- React
- Web Components
アプリケーションを作成する前にモジュールを読み込みます。3つのファイルは、エンジンリポジトリのexamples/assets/wasm/ammo/とsync-ammo npmパッケージに同梱されています。
pc.WasmModule.setConfig('Ammo', {
glueUrl: 'ammo/ammo.wasm.js',
wasmUrl: 'ammo/ammo.wasm.wasm',
fallbackUrl: 'ammo/ammo.js'
});
// モジュールを待ってから、通常どおりアプリケーションを作成する
await new Promise((resolve) => {
pc.WasmModule.getInstance('Ammo', () => resolve());
});
const app = new pc.Application(canvas);
アプリケーションの開始時にRigidBodyシステムがAmmoグローバルを見つけ、ammo.jsバックエンドをインストールします。pc.Applicationはすべてのコンポーネントシステムを登録します。代わりにpc.AppBaseとpc.AppOptionsから構築する場合は、pc.CollisionComponentSystemとpc.RigidBodyComponentSystem(ジョイントを使うならpc.JointComponentSystemも)をcomponentSystemsに追加してください。Falling Shapesの例に完全な手順があります。
Scene Settingsパネルを開き、PHYSICSを展開してIMPORT AMMOをクリックします。これによりPlayCanvasストアから3つのファイルがアセットに追加され、エンジンはシーンの開始前にそれらを読み込みます。

ライブラリがインポートされるまで、CollisionとRigidBodyコンポーネントのインスペクターには、同じボタン付きのAmmo module not foundという警告が表示されます。独自のammo.jsビルドを使用するには、ストア版の代わりにWASMモジュールアセットとして追加してください。
ammo.jsがモジュールアセットとして配布される前に作成されたプロジェクトは、ランチャーが自動的に挿入するレガシー版のライブラリを使用していました。プロジェクトにammo.jsのアセットがないのに物理演算が動作する場合は、これが理由です。IMPORT AMMOをクリックするとモジュールアセットが追加され、同時にレガシーライブラリが無効になるため、プロジェクトが両方を読み込むことはありません。インポートされるビルドはより新しく、小さく、高速なので、できるだけ早く切り替えてください。
モジュールをインストールし、ルートの<Application>にusePhysicsプロパティを設定します。
npm install sync-ammo
import { Application } from '@playcanvas/react';
<Application usePhysics>
{/* <RigidBody> と <Collision> コンポーネントを持つエンティティ */}
</Application>
usePhysicsを設定するとammo.jsは遅延読み込みされるため、ライブラリが届く前にシーンが描画され、届いた時点で物理コンポーネントが有効になります。そのタイミングを待つ必要があれば、usePhysicsフックのisPhysicsLoadedを参照してください。詳細はReactの物理演算ガイドを参照してください。
<pc-app>の中で<pc-wasm>タグを使ってモジュールを宣言します。
<pc-app>
<pc-wasm name="Ammo"
glue="https://developer.playcanvas.com/assets/modules/ammo/ammo.wasm.js"
wasm="https://developer.playcanvas.com/assets/modules/ammo/ammo.wasm.wasm"
fallback="https://developer.playcanvas.com/assets/modules/ammo/ammo.js"></pc-wasm>
<pc-scene>
<!-- <pc-rigid-body> と <pc-collision> コンポーネントを持つエンティティ -->
</pc-scene>
</pc-app>
アプリケーションはモジュールを待ってから開始するため、シーン内のすべての物理タグは最初のフレームから機能します。
重力 (Gravity)
重力は、すべてのDynamicなRigidBodyに適用される一定の加速度です。デフォルトのワールドY軸方向(真下)に-9.81という値は地球の重力に近いものです。宇宙を舞台にしたゲームではゼロに、月ならもう少し小さな値に設定します。個々のボディに対する重力の強さを変えるには、重力スケールを設定します。
- Engine
- Editor
- React
- Web Components
// 月の重力。古い setGravity() メソッドは非推奨で、このプロパティに置き換えられている。
app.systems.rigidbody.gravity = new pc.Vec3(0, -1.62, 0);
Scene SettingsパネルのPHYSICSセクションでGravityを設定します。
重力のプロパティはないため、<Application>の中のコンポーネントからアプリケーションに設定します。
import { useEffect } from 'react';
import { useApp } from '@playcanvas/react/hooks';
function LunarGravity() {
const app = useApp();
useEffect(() => {
app.systems.rigidbody.gravity.set(0, -1.62, 0);
}, [app]);
return null;
}
<pc-scene gravity="0 -1.62 0">
<!-- ... -->
</pc-scene>
測定の単位
物理エンジンは1ユニットを1メートルとして解釈し、質量はキログラムで測ります。オブジェクトが自然な速さで落下するように、シーンのサイズをそれに合わせてください。身長1.8mのキャラクターは1.8ユニットの高さにします。大きく異なるスケールで作られたシーンもシミュレートはされますが、重力が弱すぎたり強すぎたりして見え、非常に小さな形状は互いをすり抜けやすくなります。
シミュレーションの仕組み
エンジンがシミュレーションを進める方法から、いくつかのルールが導かれます。このセクションの残りはこれらを前提としています。
- シミュレーションは固定レートでステップします。 毎フレーム、物理ワールドは1/60秒の固定ステップで進み、フレーム時間が必要とする回数(上限あり)だけステップします。そのため、ボディは30fpsでも144fpsでも同じように振る舞います。60Hzより速いディスプレイではステップが1回も実行されないフレームがあり、その場合はボディのトランスフォームが補間されて動きが滑らかに保たれます。
- Dynamicなボディは物理エンジンが所有します。 各ステップの後、エンジンは動いているすべてのDynamicなボディの位置と回転をエンティティに書き戻します。エンティティのトランスフォームを設定してもボディは動かないため、Dynamicなボディは力、速度、または
teleportで動かします(RigidBodyを参照)。 - Kinematicなボディは開発者が所有します。 トランスフォームは各ステップの開始時にエンティティから読み取られるため、他のエンティティと同じように動かせます。
- Staticなボディは作成された場所に留まります。 エンジンはStaticなボディの位置と回転をボディの作成時に読み取り、その後は確認しません。そのため、作成後にエンティティを動かすとボディは元の場所に取り残されます。プレイ中に動くものはKinematicにしてください。
- ボディにスケールはありません。 エンティティからボディに渡されるのは位置と回転だけです。プリミティブのコリジョン形状は、エンティティをどうスケールしてもサイズが変わりません(スケールを参照)。
- イベントはステップごとに発火します。 衝突イベントとトリガーイベントは、フレームごとではなく物理ステップごとに1回通知されます。床の上で静止している箱は、静止している間ずっと毎ステップ
contactイベントを発生させます。
一時停止とスローモーション
RigidBodyシステムのtimeScaleは、シミュレーションが毎フレーム進める時間に掛かる倍率です。デフォルトは1です。1未満の値では物理がスローモーションになり、1を超える値では速くなります。0にすると一時停止します。ボディはその場で止まって速度を保ち、Kinematicなボディはエンティティへの追従をやめ、再開するまで衝突イベントもトリガーイベントも発火しません。スクリプトやアニメーションを含め、アプリケーションの他の部分は動き続けるため、物理を止めたまま操作できる状態を保つ必要があるポーズメニューに適しています。
- Engine
- Editor
- React
- Web Components
// ポーズメニューを開いている間、物理を止める
app.systems.rigidbody.timeScale = 0;
// スローモーション効果として、物理を4分の1の速度で動かす
app.systems.rigidbody.timeScale = 0.25;
エディターにはこの設定はありません。this.appがアプリケーションを指すスクリプトから設定します。
// ポーズメニューを開いている間、物理を止める
this.app.systems.rigidbody.timeScale = 0;
このためのプロパティはないため、<Application>の中のコンポーネントからアプリケーションに設定します。
import { useEffect } from 'react';
import { useApp } from '@playcanvas/react/hooks';
function PhysicsTimeScale({ scale }) {
const app = useApp();
useEffect(() => {
app.systems.rigidbody.timeScale = scale;
}, [app, scale]);
return null;
}
// ポーズメニューを開いている間、物理を止める
<PhysicsTimeScale scale={paused ? 0 : 1} />
<pc-app>のphysics-time-scale属性を設定します。属性または要素のphysicsTimeScaleプロパティを通して、いつでも変更できます。
<pc-app physics-time-scale="0">
<!-- ... -->
</pc-app>
// スローモーション効果として、4分の1の速度で再開する
document.querySelector('pc-app').physicsTimeScale = 0.25;
シミュレーションは引き続き1/60秒の固定ステップで進みます。スローモーションでは数フレームに1回だけステップが実行され、その間はボディのトランスフォームが補間されるため、動きは滑らかに保たれます。速くできる度合いは、1フレームに実行できるステップ数によって制限されます。60fpsでは物理は最大で10倍までしか速くならず、それより大きな値では指定より遅く動きます。
物理のタイムスケールは、アプリケーションのtimeScaleに重ねて適用されます。アプリケーションのtimeScaleは、スクリプト、アニメーション、物理がすべて受け取るデルタタイムに掛かる倍率です。アプリケーションのものを0にすると時間とともに進むものがすべて止まり、RigidBodyシステムのものを0にすると物理だけが止まります。
どちらもスクリプトは一時停止しません。スクリプトのupdateメソッドとpostUpdateメソッドは毎フレーム実行され続け、アプリケーションのタイムスケールが0の間はdtが0になります。そのため、入力処理、オブジェクトの生成など、dtを掛けていない処理はそのまま続きます。ゲームプレイのコードには独自の一時停止チェックを設けてください。これは物理の呼び出しにも当てはまります。一時停止中に行ったインパルスと速度の変更はすぐに適用され、時間が再開するとボディは新しい速度で動き出します。物理のタイムスケールが0の間にapplyForceで加えた力は、シミュレーションが再びステップするまでクリアされないため、毎フレーム力を加えるスクリプトは大きな力をため込み、物理の再開時にそれが一度に解放されることがあります。
手動でのステップ
app.systems.rigidbody.step(dt)は、シミュレーションをただちにdt秒進めます。自動更新と同じ処理をすべて行います。Kinematicなボディとトリガーがエンティティに追従し、シミュレーションはdtに収まる数だけ固定ステップを実行し、Dynamicなボディがエンティティに書き戻され、衝突イベントとトリガーイベントが発火します。dtにはタイムスケールが適用されません。
タイムスケールが0より大きい間は自動ステップが続くため、毎フレームstepも呼び出すと、シミュレーションは1フレームに2回進みます。ステップを完全に自分で制御するには、先にタイムスケールを0にしてください。こうするとstepはデバッグ用のツールになります。たとえば、一時停止したシミュレーションをキーを押すたびに1ステップずつ進めて、衝突の様子をフレームごとに確認できます。
const physics = app.systems.rigidbody;
physics.timeScale = 0;
window.addEventListener('keydown', (event) => {
// ピリオドキーを押すたびに、一時停止したシミュレーションを固定ステップ1回分進める
if (event.key === '.') {
physics.step(1 / 60);
}
});
固定ステップの長さである1/60を渡すと、ちょうど1ステップ実行されます。それより短いdtではステップが1回も実行されず、ボディが補間された経路に沿って動くだけの場合があります。長いdtでは複数のステップが実行されますが、1回の呼び出しにつき最大10回で、それを超える時間は切り捨てられます。stepを続けて何回か呼び出すとシミュレーションを早送りでき、たとえばプレイヤーに見せる前に積み重なったオブジェクトを落ち着かせることができます。
コードはどの環境でも同じで、appの取得方法だけが異なります。エディターのスクリプトではthis.app、ReactではuseApp()が返す値、Web ComponentsではwhenReady('pc-app')が解決する<pc-app>要素のappプロパティです。
関連情報
- RigidBody - ボディのタイプとプロパティ。次に読むページ
- コリジョン形状 - ボディに形状を与える
- RigidBodyComponentSystem - 重力、タイムスケール、手動ステップのAPIリファレンス
- Physics設定 - エディターのシーン設定のPHYSICSセクション
- 衝突とトリガー - エディターで完全な物理シーンを構築するチュートリアル