🚁

ClusterScript でヘリコプターを作ってみる

2024/12/22に公開

この記事は クラスター Advent Calendar 2024 22日目の記事です。
昨日は @MIRINPR さんの『匂わせ写真コンテスト3』でした。

こんにちは、ねおりんです 🐦

2022年の Advent Calendar では、 cluster に飛行機を実装する【2022年最新版】 という記事を書きました。あれから2年、クラフトアイテムには、物理挙動への対応や $.onSteer コールバック をはじめとしたAPI追加など、多くの機能追加がありました。

今年の記事では、乗って操縦できて物理挙動をするクラフトアイテムとして、ヘリコプターを作ってみます。


完成品

こちらのワールド で体験できます。

unitypackage は こちら からダウンロードできます。

アイテムの仕様

  • 操作方法:
    • W/S (移動パッド): 上昇/下降
    • カメラの向き: ヨー/ピッチ
  • 物理挙動で動く

カメラの向き (=マウス) で操作するのがちょっと難しいですが、慣れると楽しいと思います。

スクリプト全文

https://github.com/noir-neo/ClusterScript_Helicopter/blob/master/Assets/Helicopter/ClusterScripts/Helicopter_Item.js

https://github.com/noir-neo/ClusterScript_Helicopter/blob/master/Assets/Helicopter/ClusterScripts/Helicopter_Player.js

使用上の注意

  • スクリプトは好きに使ってください。商品に使ってもOK 👌
  • ベータAPI (2024/12現在) を使用しています。ベータ機能を有効にしてアップロードしてください
  • Helicopter_CSExtensions.prefabClusterScriptExtensions に対応した prefab です。ClusterScriptExtensions を導入することで (スクリプトを直接編集することなく) インスペクター上でパラメーターを調整できるようになります

解説

ヘリの挙動に関するロジックは大まかに次のように分解できます。(音やアニメーションについては省きます)

  1. $.onSteer() で上昇・下降の入力を受け取る
  2. $.onUpdate() で上昇する力を計算する
  3. PlayerScript 経由で CameraHandle からカメラの回転を受け取る
  4. $.onUpdate() でトルク (回転する力) を計算する
  5. $.onPhysicsUpdate() で重力と抗力を加えて反映する

それぞれ詳しく見ていきましょう。

steer 入力を受け取る

$.onSteer() で受け取った input は state に格納します。計算は $.onUpdate() で行うので、ここでやることはこれだけです。

ClusterScript.js
$.onSteer((input, player) => {
  // state に steer 入力を格納する
  $.state.steerInput = input;
});

上昇する力の計算

$.onSteer() で受け取った input をもとに、 $.onUpdate() で上昇する力の計算をします。

入力を即時上昇力に反映すると、動きが軽く (ラジコンみたいに) なるので、適当な補間を挟んでいます。そうすることで、プロペラの回転速度が徐々に上がっていって離陸するような感じを再現しています。
機体の重厚感は mass と power の調整でも表現できますが、同時に操作性も難しくなるかもしれません。このあたりはいろいろ試しがいがあるところだと思うので、ぜひいろいろといじって遊んでみてください。(mass 定数を調整する際は、 Rigidbody コンポーネントの Mass にも同じ値を設定してください)

ClusterScript.js
$.onUpdate(deltaTime => {
  let steerInput = $.state.steerInput ?? Vector2Zero;
  let force = Vector3Zero;
  let currentRot = $.getRotation();
  let currentForce = $.state.force ?? Vector3Zero;

  // 目標とする上昇する力を計算する
  let targetForce = Vector3Up.clone()
    // 入力がニュートラルのときにホバリングするように、重力を打ち消す力を加える
    .multiplyScalar(steerInput.y * power * mass - gravity * mass)
    .applyQuaternion(currentRot);

  // 現在の力から目標の力に向かって変化させる
  force = currentForce.clone().lerp(targetForce, deltaTime * powerFilterFactor);

  // state に上昇する力を格納する
  $.state.force = force;
});

カメラの回転を受け取る

$.onRide() でプレイヤーがヘリに乗ったときに PlayerScript を適用します。

ClusterScript.js
$.onRide((isGetOn, player) => {
  if (isGetOn) {
    $.setPlayerScript(player);
  }
});

PlayerScript では、 CameraHandle からカメラの回転を取得し、 item に send します。
今回はクラフトアイテムを想定するので、毎 _.onFrame() で _.sendTo() すると頻度制限に引っかかりますが、 send の間引きについては解説を省略します。

PlayerScript.js
_.onFrame(deltaTime => {
  let camRot = _.cameraHandle.getRotation();
  if (camRot == null) {
    return;
  }
  _.sendTo(_.sourceItemId, "update_camera_rotation", camRot);
});

$.onReceive() でカメラの回転を受け取り、 state に格納します。

ClusterScript.js
$.onReceive((messageType, arg, sender) => {
  switch (messageType) {
    case "update_camera_rotation":
      $.state.cameraRotation = arg;
      break;
  }
}, { player: true });

トルクの計算

現在の姿勢から目的の姿勢 (ここではカメラの姿勢) に向かう回転する力を計算します。

ClusterScript.js
$.onUpdate(deltaTime => {
  let torque = Vector3Zero;
  // ヘリの姿勢
  let currentRot = $.getRotation();
  // カメラの姿勢
  let cameraRot = $.state.cameraRotation ?? currentRot;
  torque = calcTorque(currentRot, cameraRot, torquePower * mass);

  // state に回転する力を格納する
  $.state.torque = torque;
});

calcTorque() では from 回転から to 回転へ向かうような回転を計算しています。

ClusterScript.js
// 現在の回転から目標の回転までのトルクを計算する
const calcTorque = (from, to) => {
  let q = to.clone().multiply(from.clone().invert());
  
  // w が負の場合は回転が 180 度以上
  // 距離が短くなるように反転させる (e.g. 270度 -> -90度)
  if (q.w < 0)
  {
      q.x = -q.x;
      q.y = -q.y;
      q.z = -q.z;
      q.w = -q.w;
  }

  let torque = new Vector3(q.x, q.y, q.z);
  return torque;
};

重力と抗力を加えた力とトルクの反映

空気抵抗は速度の2乗に比例するらしいので、 $.velocity.lengthSq() に係数 drag を掛けます。
抗力係数は機体の形状と進行方向によって変わるはずですが、今回は簡単のため定数を使います。その他、揚力などの空気力も無視していますが、それらを加えることでよりリアルな動きが実現できるかもしれません。
揚力や抗力について詳しくは、冒頭にも紹介した 2022年の記事 も参考にしてみてください。

ClusterScript.js
$.onPhysicsUpdate(_ => {
  // state から上昇・回転する力を取り出す
  let force = $.state.force ?? Vector3Zero;
  let torque = $.state.torque ?? Vector3Zero;

  // 進行・回転方向
  let velocity = $.velocity;
  let angularVelocity = $.angularVelocity;

  // 進行・回転方向の逆向きの力を計算する
  let dragForce = velocity.clone().normalize().multiplyScalar(-velocity.lengthSq() * drag);
  let dragTorque = angularVelocity.clone().normalize().multiplyScalar(-angularVelocity.lengthSq() * angularDrag);

  // 上昇・回転する力に進行方向の逆向きの力を加えて、最終的な力を反映する
  $.addForce(force.add(dragForce));
  $.addTorque(torque.add(dragTorque));
});

おわりに

乗って操縦できて物理挙動をするクラフトアイテムとしてヘリコプターを作れました!

物理挙動をする乗り物クラフトアイテムは、スペースへの持ち込みとも相性が良いのではないかと個人的には思っているので、更に更に流行るとうれしいです。

明日は @mariko_c さんの『3Dデザイナーに依頼して自分のアバターを作ろう!』です。私も来年こそは依頼したいので参考にしたいです!

Discussion