物理演算を使った軌道予測の実装方法

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

投げる前に軌道を表示したい

  • ユーザーが投げる前に、みかんがどのように飛ぶかを視覚的に示したい
    • 投げる方向や強さを調整しやすくなる
    • ゲームの操作性が向上する
  • 物理演算を使った正確な予測が必要
    • 実際の物理演算と同じ計算式を使うことで、予測と実際の挙動を一致させる

物理演算の公式を使った軌道計算

  • 等加速度運動の公式を使用
    • p(t) = p0 + v0*t + 0.5*g*t^2
      • p0: 初期位置、v0: 初速度、g: 重力加速度、t: 経過時間
  • サンプリングによる軌道の生成
    • 一定時間間隔(30fps相当)で位置を計算し、点の集合として軌道を表現
    • お皿の上面に接触する高さまで計算を続ける
  • 線形補間による正確な着地点の計算
    • お皿の上面を跨いだ時点で、線形補間を使って正確な着地点を計算

実装コード

private predictTrajectory(
  start: THREE.Vector3,
  initialVelocity: THREE.Vector3,
  gravityY: number
): { points: THREE.Vector3[]; end: THREE.Vector3; endTangent: THREE.Vector3 } {
  // お皿の上面に接触する高さ
  const plateTopY = GameParams.plate.height / 2;
  const targetY = plateTopY + GameParams.ball.radius;

  // サンプリング設定(見た目用途なので軽め)
  const dt = 1 / 30; // 30fps相当
  const maxSteps = 120; // 最大4秒

  const g = new THREE.Vector3(0, gravityY, 0);
  const points: THREE.Vector3[] = [];
  let prev = start.clone();
  points.push(prev.clone());

  for (let i = 1; i <= maxSteps; i++) {
    const t = dt * i;
    // p(t) = p0 + v0*t + 0.5*g*t^2
    const p = start
      .clone()
      .add(initialVelocity.clone().multiplyScalar(t))
      .add(g.clone().multiplyScalar(0.5 * t * t));

    points.push(p);

    // 下向きに targetY を跨いだら、線形補間で交点を作ってそこで終了
    if (prev.y >= targetY && p.y <= targetY) {
      const denom = prev.y - p.y;
      const alpha = denom !== 0 ? (prev.y - targetY) / denom : 1;
      const hit = prev.clone().lerp(p, alpha);
      points[points.length - 1] = hit;
      break;
    }

    prev = p;
  }

  const end = points[points.length - 1].clone();
  const endTangent = points.length >= 2
    ? end.clone().sub(points[points.length - 2]).normalize()
    : new THREE.Vector3(0, 0, -1);

  return { points, end, endTangent };
}

視覚的なプレビュー表示

  • 軌道の線を表示
    • three.jsのLineオブジェクトを使って、計算した点の集合を線で結ぶ
    • みかんの色(オレンジまたは緑)に合わせて線の色を変更
  • 矢印の先端を表示
    • 軌道の終点に、進行方向を示すコーン形状の矢印を表示
    • 軌道の接線方向を計算して、矢印の向きを決定
  • プレビューの更新タイミング
    • ジョイスティックの位置が変わるたびに、軌道を再計算して表示を更新
    • カメラが回転した場合も、一定頻度でプレビューを更新

軌道線の描画実装

private buildTrajectoryLine(
  points: THREE.Vector3[],
  color: number
): THREE.Line {
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
  const material = new THREE.LineBasicMaterial({
    color,
    transparent: true,
    opacity: 0.85,
    depthTest: true,
  });
  const line = new THREE.Line(geometry, material);
  line.name = 'TrajectoryLine';
  return line;
}

// 矢印の先端(コーン)の描画
private buildArrowHead(
  tipPosition: THREE.Vector3,
  direction: THREE.Vector3,
  color: number
): THREE.Mesh {
  const headLength = 0.22;
  const headRadius = 0.08;
  const geometry = new THREE.ConeGeometry(headRadius, headLength, 16);
  const material = new THREE.MeshStandardMaterial({
    color,
    metalness: 0.2,
    roughness: 0.6,
    transparent: true,
    opacity: 0.95,
  });
  const cone = new THREE.Mesh(geometry, material);
  
  // 方向を設定
  const q = new THREE.Quaternion().setFromUnitVectors(
    new THREE.Vector3(0, 1, 0),
    direction.normalize()
  );
  cone.quaternion.copy(q);
  cone.position.copy(tipPosition);
  
  return cone;
}

実装のポイント

  • 物理演算のパラメータを正確に反映
    • 重力加速度は物理ワールドの設定から取得し、予測計算でも同じ値を使用
  • パフォーマンスを考慮した実装
    • 見た目用途なので、サンプリング間隔を30fps相当に設定して軽量化
    • 最大計算時間を制限して、無限ループを防ぐ
  • リソースの適切な管理
    • プレビューが更新されるたびに、古い軌道表示を削除してから新しいものを追加
    • three.jsのオブジェクトを適切にdisposeしてメモリリークを防ぐ