技術解説

TypeScriptで物理シミュレーションを実装する方法

TypeScriptを使って物理シミュレーションを実装する方法を、実際のコード例とともに詳しく解説します。

2200文字
TypeScript物理シミュレーションMatter.jsプログラミング型安全性

TypeScriptで物理シミュレーションを実装する方法

TypeScriptを使って物理シミュレーションを実装する方法を、実際のコード例とともに詳しく解説します。Matter.jsを使った実装から、型安全性を活かした実装パターンまで、実践的な内容を紹介します。

TypeScriptで物理シミュレーションを実装する理由

TypeScriptを使うことで、物理シミュレーションの実装において以下のメリットが得られます:

  1. 型安全性: コンパイル時にエラーを検出できる
  2. 開発効率: IDEの補完機能が強力
  3. 保守性: コードの可読性と保守性が向上
  4. リファクタリング: 型情報を活用した安全なリファクタリングが可能

Matter.jsとTypeScriptの組み合わせ

Matter.jsは、TypeScriptの型定義が提供されているため、型安全に物理シミュレーションを実装できます。

インストール

npm install matter-js
npm install --save-dev @types/matter-js

基本的な型定義

import Matter from 'matter-js';

// 物理エンジンの型
const engine: Matter.Engine = Matter.Engine.create();
const world: Matter.World = engine.world;

// ボディの型
const box: Matter.Body = Matter.Bodies.rectangle(400, 200, 80, 80);

// 制約の型
const constraint: Matter.Constraint = Matter.Constraint.create({
  bodyA: box,
  bodyB: circle,
  stiffness: 0.5,
});

型安全な物理シミュレーションの実装

1. 物理エンジンのラッパークラス

型安全性を高めるため、物理エンジンをラッパークラスで包みます。

import Matter from 'matter-js';

/**
 * 物理エンジンのラッパークラス
 * 型安全性を高め、使いやすくする
 */
export class PhysicsEngine {
  private engine: Matter.Engine;
  private world: Matter.World;
  private runner: Matter.Runner | null = null;

  constructor() {
    this.engine = Matter.Engine.create();
    this.world = this.engine.world;
  }

  /**
   * 重力を設定
   */
  setGravity(x: number, y: number, scale: number = 0.001): void {
    this.world.gravity.x = x;
    this.world.gravity.y = y;
    this.world.gravity.scale = scale;
  }

  /**
   * ボディを追加
   */
  addBody(body: Matter.Body): void {
    Matter.World.add(this.world, body);
  }

  /**
   * 複数のボディを追加
   */
  addBodies(bodies: Matter.Body[]): void {
    Matter.World.add(this.world, bodies);
  }

  /**
   * 制約を追加
   */
  addConstraint(constraint: Matter.Constraint): void {
    Matter.World.add(this.world, constraint);
  }

  /**
   * エンジンを開始
   */
  start(): void {
    if (this.runner) {
      return; // 既に開始されている
    }
    this.runner = Matter.Runner.create();
    Matter.Runner.run(this.runner, this.engine);
  }

  /**
   * エンジンを停止
   */
  stop(): void {
    if (this.runner) {
      Matter.Runner.stop(this.runner);
      this.runner = null;
    }
  }

  /**
   * エンジンを更新
   */
  update(): void {
    Matter.Engine.update(this.engine);
  }

  /**
   * すべてのボディを取得
   */
  getBodies(): Matter.Body[] {
    return this.world.bodies;
  }

  /**
   * エンジンをクリーンアップ
   */
  cleanup(): void {
    this.stop();
    Matter.Engine.clear(this.engine);
  }
}

2. ボディファクトリー

型安全なボディの作成を支援するファクトリー関数を実装します。

import Matter from 'matter-js';

/**
 * ボディの作成オプション
 */
export interface BodyOptions {
  mass?: number;
  friction?: number;
  restitution?: number;
  isStatic?: boolean;
  density?: number;
}

/**
 * ボディファクトリー
 */
export class BodyFactory {
  /**
   * 矩形ボディを作成
   */
  static createRectangle(
    x: number,
    y: number,
    width: number,
    height: number,
    options: BodyOptions = {}
  ): Matter.Body {
    return Matter.Bodies.rectangle(x, y, width, height, {
      mass: options.mass ?? 1,
      friction: options.friction ?? 0.1,
      restitution: options.restitution ?? 0.3,
      isStatic: options.isStatic ?? false,
      density: options.density,
    });
  }

  /**
   * 円形ボディを作成
   */
  static createCircle(
    x: number,
    y: number,
    radius: number,
    options: BodyOptions = {}
  ): Matter.Body {
    return Matter.Bodies.circle(x, y, radius, {
      mass: options.mass ?? 1,
      friction: options.friction ?? 0.1,
      restitution: options.restitution ?? 0.3,
      isStatic: options.isStatic ?? false,
      density: options.density,
    });
  }

  /**
   * 多角形ボディを作成
   */
  static createPolygon(
    x: number,
    y: number,
    sides: number,
    radius: number,
    options: BodyOptions = {}
  ): Matter.Body {
    return Matter.Bodies.polygon(x, y, sides, radius, {
      mass: options.mass ?? 1,
      friction: options.friction ?? 0.1,
      restitution: options.restitution ?? 0.3,
      isStatic: options.isStatic ?? false,
      density: options.density,
    });
  }
}

3. 制約ファクトリー

型安全な制約の作成を支援するファクトリー関数を実装します。

import Matter from 'matter-js';

/**
 * 制約の作成オプション
 */
export interface ConstraintOptions {
  stiffness?: number;
  damping?: number;
  length?: number;
}

/**
 * 制約ファクトリー
 */
export class ConstraintFactory {
  /**
   * ばね制約を作成
   */
  static createSpring(
    bodyA: Matter.Body,
    bodyB: Matter.Body,
    options: ConstraintOptions = {}
  ): Matter.Constraint {
    return Matter.Constraint.create({
      bodyA,
      bodyB,
      stiffness: options.stiffness ?? 0.5,
      damping: options.damping ?? 0.05,
      length: options.length,
    });
  }

  /**
   * 固定制約を作成
   */
  static createFixed(
    body: Matter.Body,
    point: { x: number; y: number }
  ): Matter.Constraint {
    return Matter.Constraint.create({
      bodyA: body,
      pointA: { x: 0, y: 0 },
      pointB: point,
      stiffness: 1.0,
    });
  }
}

Reactでの実装例

カスタムフックの作成

物理シミュレーションをReactで使うためのカスタムフックを作成します。

import { useEffect, useRef, useState } from 'react';
import { PhysicsEngine } from './PhysicsEngine';
import { BodyFactory } from './BodyFactory';
import { ConstraintFactory } from './ConstraintFactory';
import Matter from 'matter-js';

/**
 * 物理シミュレーション用のカスタムフック
 */
export function usePhysicsSimulation() {
  const engineRef = useRef<PhysicsEngine | null>(null);
  const [bodies, setBodies] = useState<Matter.Body[]>([]);

  useEffect(() => {
    // 物理エンジンの作成
    const engine = new PhysicsEngine();
    engine.setGravity(0, 1, 0.001);
    engineRef.current = engine;

    // 初期ボディの作成
    const box = BodyFactory.createRectangle(400, 200, 80, 80);
    const circle = BodyFactory.createCircle(400, 100, 30);
    
    engine.addBodies([box, circle]);
    setBodies([box, circle]);

    // エンジンを開始
    engine.start();

    // アニメーションループ
    const animate = () => {
      if (engineRef.current) {
        engineRef.current.update();
        setBodies([...engineRef.current.getBodies()]);
      }
      requestAnimationFrame(animate);
    };
    animate();

    // クリーンアップ
    return () => {
      if (engineRef.current) {
        engineRef.current.cleanup();
      }
    };
  }, []);

  return { bodies, engine: engineRef.current };
}

コンポーネントでの使用

'use client';

import { usePhysicsSimulation } from './usePhysicsSimulation';
import { useEffect, useRef } from 'react';
import Matter from 'matter-js';

export function PhysicsSimulation() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const { bodies } = usePhysicsSimulation();

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    // 描画関数
    const draw = () => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      for (const body of bodies) {
        ctx.fillStyle = '#FF6B9D';
        ctx.beginPath();

        if (body.circleRadius) {
          // 円形ボディ
          ctx.arc(
            body.position.x,
            body.position.y,
            body.circleRadius,
            0,
            Math.PI * 2
          );
        } else {
          // 多角形ボディ
          const vertices = body.vertices;
          ctx.moveTo(vertices[0].x, vertices[0].y);
          for (let i = 1; i < vertices.length; i++) {
            ctx.lineTo(vertices[i].x, vertices[i].y);
          }
          ctx.closePath();
        }

        ctx.fill();
      }
    };

    // アニメーションループ
    const animate = () => {
      draw();
      requestAnimationFrame(animate);
    };
    animate();
  }, [bodies]);

  return (
    <canvas
      ref={canvasRef}
      width={800}
      height={600}
      className="border border-gray-300 rounded"
    />
  );
}

型安全なイベント処理

Matter.jsのイベントを型安全に処理する方法を紹介します。

import Matter from 'matter-js';

/**
 * 衝突イベントの型
 */
export interface CollisionEvent {
  pairs: Matter.Pair[];
}

/**
 * イベントハンドラーの型
 */
export type CollisionEventHandler = (event: CollisionEvent) => void;

/**
 * イベントマネージャー
 */
export class EventManager {
  /**
   * 衝突イベントを登録
   */
  static onCollision(
    engine: Matter.Engine,
    handler: CollisionEventHandler
  ): void {
    Matter.Events.on(engine, 'collisionStart', (event) => {
      handler({
        pairs: event.pairs,
      });
    });
  }

  /**
   * 衝突イベントを解除
   */
  static offCollision(engine: Matter.Engine): void {
    Matter.Events.off(engine, 'collisionStart');
  }
}

パフォーマンス最適化

型安全なパフォーマンス設定

import Matter from 'matter-js';

/**
 * パフォーマンス設定の型
 */
export interface PerformanceConfig {
  positionIterations: number;
  velocityIterations: number;
  constraintIterations: number;
  enableSleeping: boolean;
}

/**
 * パフォーマンスマネージャー
 */
export class PerformanceManager {
  /**
   * パフォーマンス設定を適用
   */
  static applyConfig(
    engine: Matter.Engine,
    config: PerformanceConfig
  ): void {
    const engineAny = engine as any;
    engineAny.positionIterations = config.positionIterations;
    engineAny.velocityIterations = config.velocityIterations;
    engineAny.constraintIterations = config.constraintIterations;
    engine.enableSleeping = config.enableSleeping;
  }

  /**
   * デフォルト設定を取得
   */
  static getDefaultConfig(): PerformanceConfig {
    return {
      positionIterations: 6,
      velocityIterations: 4,
      constraintIterations: 2,
      enableSleeping: true,
    };
  }

  /**
   * 高パフォーマンス設定を取得
   */
  static getHighPerformanceConfig(): PerformanceConfig {
    return {
      positionIterations: 8,
      velocityIterations: 6,
      constraintIterations: 3,
      enableSleeping: true,
    };
  }

  /**
   * 低パフォーマンス設定を取得(軽量端末用)
   */
  static getLowPerformanceConfig(): PerformanceConfig {
    return {
      positionIterations: 4,
      velocityIterations: 2,
      constraintIterations: 1,
      enableSleeping: true,
    };
  }
}

実践的な実装例:ゼリーシミュレーション

型安全性を活かしたゼリーシミュレーションの実装例です。

import { PhysicsEngine } from './PhysicsEngine';
import { BodyFactory } from './BodyFactory';
import { ConstraintFactory } from './ConstraintFactory';
import Matter from 'matter-js';

/**
 * ゼリーの設定
 */
export interface JellyConfig {
  centerX: number;
  centerY: number;
  radius: number;
  pointCount: number;
  stiffness: number;
  damping: number;
}

/**
 * ゼリーシミュレーションクラス
 */
export class JellySimulation {
  private engine: PhysicsEngine;
  private points: Matter.Body[] = [];
  private constraints: Matter.Constraint[] = [];
  private config: JellyConfig;

  constructor(config: JellyConfig) {
    this.config = config;
    this.engine = new PhysicsEngine();
    this.engine.setGravity(0, 1, 0.001);
    this.createJelly();
  }

  /**
   * ゼリーを作成
   */
  private createJelly(): void {
    const { centerX, centerY, radius, pointCount, stiffness, damping } =
      this.config;

    // 質点の作成
    for (let i = 0; i < pointCount; i++) {
      const angle = (Math.PI * 2 * i) / pointCount;
      const x = centerX + Math.cos(angle) * radius;
      const y = centerY + Math.sin(angle) * radius;

      const point = BodyFactory.createCircle(x, y, 5, {
        mass: 1,
        friction: 0.1,
        restitution: 0.3,
      });

      this.points.push(point);
    }

    // ばねの作成
    for (let i = 0; i < pointCount; i++) {
      const nextIndex = (i + 1) % pointCount;
      const constraint = ConstraintFactory.createSpring(
        this.points[i],
        this.points[nextIndex],
        {
          stiffness,
          damping,
        }
      );

      this.constraints.push(constraint);
    }

    // エンジンに追加
    this.engine.addBodies(this.points);
    this.engine.addConstraint(this.constraints[0]);
    for (let i = 1; i < this.constraints.length; i++) {
      this.engine.addConstraint(this.constraints[i]);
    }
  }

  /**
   * シミュレーションを開始
   */
  start(): void {
    this.engine.start();
  }

  /**
   * シミュレーションを更新
   */
  update(): void {
    this.engine.update();
  }

  /**
   * 質点を取得
   */
  getPoints(): Matter.Body[] {
    return this.points;
  }

  /**
   * 制約を取得
   */
  getConstraints(): Matter.Constraint[] {
    return this.constraints;
  }

  /**
   * クリーンアップ
   */
  cleanup(): void {
    this.engine.cleanup();
  }
}

まとめ

TypeScriptを使うことで、物理シミュレーションの実装において型安全性を確保し、開発効率と保守性を向上させることができます。

Matter.jsとTypeScriptを組み合わせることで、以下のような実装が可能になります:

  1. 型安全な物理エンジンのラッパー: エンジンの操作を型安全に
  2. ボディ・制約のファクトリー: 型安全なボディ・制約の作成
  3. Reactカスタムフック: 物理シミュレーションをReactで使いやすく
  4. 型安全なイベント処理: イベントの型定義
  5. パフォーマンス最適化: 型安全なパフォーマンス設定

これらの実装パターンを活用することで、保守性の高い物理シミュレーションを実装できます。

関連記事

関連ツール

関連記事