物理演算を使った軌道予測の実装方法
2025-12-22 ・ 読了目安 5分
投げる前に軌道を表示したい
- ユーザーが投げる前に、みかんがどのように飛ぶかを視覚的に示したい
- 投げる方向や強さを調整しやすくなる
- ゲームの操作性が向上する
- 物理演算を使った正確な予測が必要
- 実際の物理演算と同じ計算式を使うことで、予測と実際の挙動を一致させる
物理演算の公式を使った軌道計算
- 等加速度運動の公式を使用
- p(t) = p0 + v0*t + 0.5*g*t^2
- p0: 初期位置、v0: 初速度、g: 重力加速度、t: 経過時間
- p(t) = p0 + v0*t + 0.5*g*t^2
- サンプリングによる軌道の生成
- 一定時間間隔(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してメモリリークを防ぐ