物理制約を使った連結システムの実装

2025-12-22 ・ 読了目安 6分

緑のみかんがくっつく仕組みを実装したい

  • ゲームの特徴として、緑のみかんが他のみかんにくっつく挙動を実装したい
    • 物理演算を使った自然な連結を実現したい
    • くっついたみかん同士が鎖のように振る舞う
  • 物理制約を使った実装が適している
    • cannon-esのConstraintを使うことで、物理演算に組み込まれた連結を実現できる

PointToPointConstraintによる連結

  • PointToPointConstraintの特徴
    • 2つの剛体を、指定した点同士で結ぶ制約
    • 「見えない鎖」のように、2つのオブジェクトを接続点で繋ぐ
    • 距離は自動的に保たれるが、回転は自由
  • 接触点の計算
    • 2つのみかんの中心間の距離と方向を計算
    • 緑のみかんの表面(半径分の距離)を接続点として設定
    • ワールド座標の接続点を、それぞれの剛体のローカル座標に変換
  • 制約の強度設定
    • 制約の強度(stiffness)を高く設定することで、しっかりと連結する
    • 値が低いと、連結が緩くなってしまう

自動接続の判定ロジック

  • 距離による判定
    • 緑のみかんと他のみかんの距離を計算
    • 設定した接続可能距離(connectionDistance)以内にあるかチェック
  • 接続対象のフィルタリング
    • 緑のみかん同士はくっつかない(青いみかん同士は除外)
    • 静的オブジェクト(お皿や机など)にはくっつかない
    • 既に接続済みのオブジェクトには再度接続しない
  • 毎フレームのチェック
    • 物理演算の更新後に、緑のみかんが近くのみかんをチェック
    • 接続可能な対象が見つかったら、自動的にJointを作成して接続

Jointクラスの実装

  • Jointクラスの責務
    • 2つの剛体を接続するGameObjectとして実装
    • 物理ワールドに制約を追加・削除する処理を担当
  • ライフサイクル管理
    • start()で物理ワールドに制約を追加
    • onRemove()で物理ワールドから制約を削除
    • 適切なクリーンアップにより、メモリリークを防ぐ
  • 制約の自動維持
    • PointToPointConstraintは、物理演算によって自動的に距離を保つ
    • update()での追加処理は不要(物理エンジンが自動的に処理)

Jointクラスの実装コード

export class Joint extends GameObject {
  private parentBody: CANNON.Body;
  private childBody: CANNON.Body;
  private constraint: CANNON.Constraint;
  private physicsWorld: CANNON.World | null = null;

  constructor(
    parentBody: CANNON.Body,
    childBody: CANNON.Body,
    worldAnchor: CANNON.Vec3
  ) {
    super('Joint');
    this.parentBody = parentBody;
    this.childBody = childBody;

    // 接触点(ワールド座標)を両ボディのローカル座標へ変換
    const pivotA = new CANNON.Vec3();
    const pivotB = new CANNON.Vec3();
    parentBody.pointToLocalFrame(worldAnchor, pivotA);
    childBody.pointToLocalFrame(worldAnchor, pivotB);

    // PointToPointConstraint は「見えない鎖(接触点で繋がる)」のイメージ
    this.constraint = new CANNON.PointToPointConstraint(
      parentBody,
      pivotA,
      childBody,
      pivotB,
      1e5  // stiffness(制約の強度)
    );
  }

  start(): void {
    super.start();
    const scene = this.getScene();
    if (!scene) return;

    this.physicsWorld = scene.getPhysicsWorld();
    if (!this.physicsWorld) return;

    // 制約を物理ワールドに追加
    this.physicsWorld.addConstraint(this.constraint);
  }

  onRemove(): void {
    super.onRemove();
    // 制約を物理ワールドから削除
    if (this.physicsWorld && this.constraint) {
      this.physicsWorld.removeConstraint(this.constraint);
    }
  }
}

BallBlueクラスでの使用例

// 緑のみかん(BallBlue)が他のみかんにくっつく処理
private createJoint(targetBody: CANNON.Body, worldAnchor: CANNON.Vec3): void {
  const blueBody = this.getCannonBody();
  if (!blueBody) return;

  const scene = this.getScene();
  if (!scene) return;

  // Jointを作成して接続
  const joint = new Joint(blueBody, targetBody, worldAnchor);
  scene.addGameObject(joint);
  joint.callStart();

  // 対象をキーとしてJointを保存
  this.joints.set(targetBody, joint);
}

実装のポイント

  • 接続点の正確な計算
    • ワールド座標とローカル座標の変換を正確に行うことで、自然な連結を実現
  • パフォーマンスの考慮
    • 毎フレームすべての剛体をチェックするのではなく、必要な場合のみチェック
    • 既に接続済みの場合は、再度チェックをスキップ
  • リソースの適切な管理
    • ゲームリセット時などに、すべてのJointを適切に削除
    • 物理ワールドから制約を削除しないと、メモリリークの原因になる