EngineにおけるJavaScript

Engineのスクリプティング API:ライフサイクル、座標の落とし穴、オブジェクトを作る3つの方法、そして壁をすり抜けずに動かす方法。

一言で言うと#

どのエンティティもJavaScriptスクリプトを持てます。これはブラウザの中、あなたのタブの中で、生きたシーンに対して実行され、正規のスクリプティングAPIです。BASIC方言、そしてエディタ内のPython、C#、Rustのビューは、すべて同じ実行面へとコンパイルされます。

網羅的な一覧はAPIリファレンスにあり、50の名前空間にまたがる538個のメンバーが、ページごとに検索可能な形で載っています。このページは、そこを読む前に知っておくべきことです。

ライフサイクル#

function start() {}                     // once, when the entity starts
function update(dt) {}                  // every frame, dt in seconds
function fixedUpdate(dt) {}             // fixed timestep, for physics
function onCollisionEnter(other) {}     // first frame of contact
function onCollisionStay(other) {}      // every frame while touching
function onCollisionExit(other) {}      // contact just ended
function onTriggerEnter(other) {}       // entered a trigger volume
function onTriggerExit(other) {}
function onMessage(msg, data, senderId) {}   // entity-to-entity messaging
function onAnimationEvent(name, data) {}
function onDestroy() {}
function main() {}                      // console-game entry, async allowed

deltaTimetimeentityIdentityはグローバル変数です。getComponent(type)は、そのスクリプトを持つエンティティからコンポーネントを読み取ります。

衝突とトリガーは2Dと3Dの両方で発火し、それぞれの側が自分自身の接触法線を受け取ります。other.contact.normal相手側のエンティティから、こちら側に向かって伸びます。これによりスクリプトは「自分がその上に着地した」のか「相手が自分の上に着地した」のかを区別できます。プラットフォーマーのスタンプ攻撃ならother.contact.normal.y > 0.5と読みます。

2つの座標の落とし穴#

どちらも実際にデバッグの時間を食い、どちらも大きな音を立てて失敗しません。

マウス:ページ座標かキャンバス座標か#

input.mousePositionページ座標系です。scene.pick()が求めるのはキャンバス座標系です。片方をもう片方の代わりに渡しても、外れるのではなく、ウィンドウ内でのビューポートの位置だけずれた点に当たります。そのため、クリックで移動するヒーローは、もっともらしいが間違った場所へ歩いていきます。

scene.pick()を引数なしで呼ぶ(カーソルの位置を狙う)か、数値そのものが必要なときはinput.mouseViewportを使ってください。

バーチャルコントロールはキャンバスのピクセル単位#

input.addVirtualJoystick(id, x, y, size)input.addVirtualButton(...)は、その中心をmouseViewportと同じ座標空間に置きます。ジョイスティックは、放射状のデッドゾーンを経た後の正規化されたX/Yを返し、ボタンはheldpressed-this-framereleased-this-frameを別々に公開します。それらのポインターはキャプチャされ消費されるので、コントロール上のタッチが生のキャンバスアクションとして二重に報告されることはありません。

既定のジョイスティックは統合されています。 input.getJoystickX/Y()は、画面上のスティック、物理ゲームパッド、またはキーボード(WASD/矢印)のうち最も強い信号から、1つの完全なベクトルを選びます。だから接続された未操作のパッドがあっても、キーボード操作が無効になることはありません。そのY軸は2Dスクリーン空間に従い、上方向は-1です。

オブジェクトを作る3つの方法、それらは置き換え可能ではない#

何を作るか寿命
scene.createEntity(def)作者が定義したエンティティ。Scene GraphとInspectorに現れるプロジェクトの寿命、保存される
game.spawn(type, pos)裸のプリミティブ。体力もAIも振る舞いもないプレイセッションの寿命
game.spawnFrom(template, pos, opts)エンティティ丸ごとのコピー。コンポーネント、物理ボディ、タグ、レイヤー、スケール、コンパイル済みの振る舞いスタックプレイセッションの寿命

game.spawnは瓦礫やプロトタイプには適していますが、敵には使えません。 これで供給されたスポナーは、生気のないボールを生み出すだけです。spawnFromがコピーするテンプレートは、エディタの中で組み立て、階層のeyeアイコンでオフにできる、ただの普通のエンティティです。別のフォーマットも、別のエディタも必要ありません。見えているものがそのまま現れます。

// Once, at start: a missing template should say so, not fail every wave silently
if (!game.hasTemplate('Skeleton')) console.warn('no Skeleton template');

const mob = game.spawnFrom('Skeleton', { x, y, z }, { tag: 'Enemy' });

templateはid、名前、タグのいずれかを受け付けます。プレイ中に作られたコピーはプレイスコープなので、stop()でそれらは除去され、編集用のシーンが汚れることはありません。

エンティティをオフにするとは、見えなくなることと、静かになることの両方を意味します。 ノードは無効化され、スクリプトは停止し、物理ボディは応答しなくなります。非表示にするだけでは、目に見えない敵がまだ攻撃してくることになります。game.setEnabled(id, on)はスクリプトから駆動し、eyeアイコンは階層から駆動します。

何かを動かす:ボディを押す、テレポートしない#

transform.positionを毎フレーム書き換えると、壁も、他のモンスターも、床もすり抜けてしまいます。代わりにvelocityを駆動してください。ただし駆動すべき対象があるときだけです。

if (physics.hasBody) {
  const v = physics.getVelocity();
  physics.setVelocity({ x: vx, y: v.y, z: vz });   // pushes a real body
} else {
  transform.position.x += vx * dt;                 // legitimate for a flyer
}

physics.setVelocityをボディなしで呼ぶと、誰も積分しないフィールドを埋めるだけで、エンティティはぴたりと止まります。physics.hasBodyが存在し、カタログ内のあらゆる移動系の振る舞いが答えを決めつけずに問いを発するのはそのためです。止めるとはvelocityを打ち消すことです。 何もしなければ、ボディは滑り続けます。

位置を書き込むと、今では実際に物理ボディが動きます。 Havokがtransformを駆動するのであって、逆はありません。だから動的ボディに対するtransform.positionへの代入は、以前は次のステップで上書きされていました。物理エンティティに対するテレポートはすべて静かに失敗していたのです。position用のプロキシは、rotation用のプロキシが常にそうしてきたように、ボディを再同期します。

パスファインディングはスナップショット#

pathfinding.createGrid()は、呼ばれた場所を中心に、グリッドに対して一度だけレイキャストします。その範囲を出るとnullが返り、たいていの呼び出し側はそこで、何も言わずに、壁を突き抜ける直線経路にフォールバックします。

グリッドは既定で、要求された移動のたびに中心を取り直します。固定された世界では、再構築のたびに無駄なレイキャストパスがかかるので、setAutoRecenter(false)でこれをオプトアウトできます。

console.logはログを出し、Printは描画する#

コンソールは、他と同じようにconsole.log / warn / errorです。BASICのPrint画面に描画するもので、毎フレーム呼ばれます。この2つは意図的に別のコマンドとして分けられており、両者を混同することが、コンソールをフレームループで埋め尽くす原因です。

スクリプトがどこで動くか#

エディタとWebビルドでは、スクリプトはブラウザの中で動きます。ネイティブのデスクトップまたはモバイルビルドでは、同じソースが移植版ではなく、そのまま組み込みのQuickJSランタイム上で動きます。

スクリプトは編集中、ブラウザ自身のデータベースに保存されるので、保存していないバッファもリロードを生き延びます。プロジェクトを保存すれば、他のすべてと一緒に保存されます。

制限とよくあるつまずき#

  • 引数付きのscene.pick()はキャンバス座標を求めます。 上記参照。もっともよくある「場所を間違えた」バグです。
  • game.spawnはプリミティブを作るだけで、キャラクターは作りません。 spawnFromを使ってください。
  • ゲーム途中で生まれたエンティティにもボディが作られます。 以前は物理ボディがPlay開始時にしか構築されず、手で配置された隣人は正しく振る舞うのに、その後スポーンされたものだけが静かに世界をすり抜けていました。
  • 8ステップはAIチームメイトの上限であり、あなたの上限ではありません。 スクリプトのループにそのような制限はなく、暴走したwhileはどこでも同じようにフレームをハングさせます。
  • 他の4つの言語ビューはこのビューから変換されたものです。 PythonビューやRustビューを編集してから戻ると、保存されているJavaScriptを経由します。理由はBASICのページを参照してください。