物理制約を使った連結システムの実装
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を適切に削除
- 物理ワールドから制約を削除しないと、メモリリークの原因になる