プログラミング学習

React Hooksを使った物理シミュレーション

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

2200文字
ReactHooks物理シミュレーションMatter.jsプログラミング

React Hooksを使った物理シミュレーション

React Hooksを使って物理シミュレーションを実装する方法を、実際のコード例とともに詳しく解説します。useState、useEffect、useRef、カスタムフックなど、Hooksを活用した実装パターンを紹介します。

React Hooksとは

React Hooksは、関数コンポーネントで状態管理やライフサイクルを扱うための機能です。クラスコンポーネントを使わずに、関数コンポーネントでReactの機能を活用できます。

Hooksの主な特徴

  1. 状態管理: useStateでコンポーネントの状態を管理
  2. 副作用処理: useEffectでライフサイクルや副作用を処理
  3. 参照管理: useRefでDOM要素や値を保持
  4. カスタムフック: ロジックを再利用可能なフックに抽出

基本的なHooksの使い方

1. useState

コンポーネントの状態を管理します。

'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState<number>(0);

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>増やす</button>
      <button onClick={() => setCount(count - 1)}>減らす</button>
    </div>
  );
}

2. useEffect

副作用(データ取得、イベントリスナーの登録など)を処理します。

'use client';

import { useEffect, useState } from 'react';

export function Timer() {
  const [seconds, setSeconds] = useState<number>(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

    // クリーンアップ
    return () => clearInterval(interval);
  }, []); // 依存配列が空なので、マウント時のみ実行

  return <div>経過時間: {seconds}秒</div>;
}

3. useRef

DOM要素への参照や、再レンダリングを避けたい値を保持します。

'use client';

import { useRef, useEffect } from 'react';

export function CanvasComponent() {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

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

    // 描画処理
    ctx.fillStyle = '#FF6B9D';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }, []);

  return <canvas ref={canvasRef} width={800} height={600} />;
}

物理シミュレーションでのHooks活用

1. usePhysicsEngine

物理エンジンを管理するカスタムフックです。

'use client';

import { useEffect, useRef, useState } from 'react';
import Matter from 'matter-js';

/**
 * 物理エンジンを管理するカスタムフック
 */
export function usePhysicsEngine() {
  const engineRef = useRef<Matter.Engine | null>(null);
  const [bodies, setBodies] = useState<Matter.Body[]>([]);

  useEffect(() => {
    // 物理エンジンの作成
    const engine = Matter.Engine.create();
    engineRef.current = engine;

    // 重力の設定
    engine.world.gravity.x = 0;
    engine.world.gravity.y = 1;
    engine.world.gravity.scale = 0.001;

    // 初期ボディの作成
    const box = Matter.Bodies.rectangle(400, 200, 80, 80);
    const circle = Matter.Bodies.circle(400, 100, 30);
    
    Matter.World.add(engine.world, [box, circle]);
    setBodies([box, circle]);

    // エンジンの実行
    const runner = Matter.Runner.create();
    Matter.Runner.run(runner, engine);

    // アニメーションループ
    const animate = () => {
      Matter.Engine.update(engine);
      setBodies([...engine.world.bodies]);
      requestAnimationFrame(animate);
    };
    animate();

    // クリーンアップ
    return () => {
      Matter.Runner.stop(runner);
      Matter.Engine.clear(engine);
    };
  }, []);

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

2. useCanvasRenderer

Canvas描画を管理するカスタムフックです。

'use client';

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

/**
 * Canvas描画を管理するカスタムフック
 */
export function useCanvasRenderer(bodies: Matter.Body[]) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  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 canvasRef;
}

3. useMouseInteraction

マウス操作を処理するカスタムフックです。

'use client';

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

/**
 * マウス操作を処理するカスタムフック
 */
export function useMouseInteraction(
  engine: Matter.Engine | null,
  canvasRef: React.RefObject<HTMLCanvasElement>
) {
  const mouseConstraintRef = useRef<Matter.MouseConstraint | null>(null);

  useEffect(() => {
    if (!engine || !canvasRef.current) return;

    const canvas = canvasRef.current;
    const mouse = Matter.Mouse.create(canvas);
    const mouseConstraint = Matter.MouseConstraint.create(engine, {
      mouse: mouse,
      constraint: {
        stiffness: 0.2,
        render: {
          visible: false,
        },
      },
    });

    mouseConstraintRef.current = mouseConstraint;
    Matter.World.add(engine.world, mouseConstraint);

    // クリーンアップ
    return () => {
      if (mouseConstraintRef.current) {
        Matter.World.remove(engine.world, mouseConstraintRef.current);
      }
    };
  }, [engine, canvasRef]);

  return mouseConstraintRef.current;
}

実践的な実装例

ゼリーシミュレーションの実装

Hooksを活用したゼリーシミュレーションの実装例です。

'use client';

import { useEffect, useRef, useState } from 'react';
import Matter from 'matter-js';

interface JellyConfig {
  centerX: number;
  centerY: number;
  radius: number;
  pointCount: number;
  stiffness: number;
  damping: number;
}

/**
 * ゼリーシミュレーション用のカスタムフック
 */
export function useJellySimulation(config: JellyConfig) {
  const engineRef = useRef<Matter.Engine | null>(null);
  const pointsRef = useRef<Matter.Body[]>([]);
  const constraintsRef = useRef<Matter.Constraint[]>([]);
  const [isRunning, setIsRunning] = useState<boolean>(false);

  useEffect(() => {
    // 物理エンジンの作成
    const engine = Matter.Engine.create();
    engine.world.gravity.x = 0;
    engine.world.gravity.y = 1;
    engine.world.gravity.scale = 0.001;
    engineRef.current = engine;

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

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

      points.push(point);
    }
    pointsRef.current = points;

    // ばねの作成
    const constraints: Matter.Constraint[] = [];
    for (let i = 0; i < config.pointCount; i++) {
      const nextIndex = (i + 1) % config.pointCount;
      const constraint = Matter.Constraint.create({
        bodyA: points[i],
        bodyB: points[nextIndex],
        stiffness: config.stiffness,
        damping: config.damping,
      });

      constraints.push(constraint);
    }
    constraintsRef.current = constraints;

    // エンジンに追加
    Matter.World.add(engine.world, points);
    Matter.World.add(engine.world, constraints);

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

  const start = () => {
    if (!engineRef.current || isRunning) return;

    const runner = Matter.Runner.create();
    Matter.Runner.run(runner, engineRef.current);
    setIsRunning(true);
  };

  const stop = () => {
    if (!engineRef.current || !isRunning) return;

    // Runnerの停止処理
    setIsRunning(false);
  };

  const update = () => {
    if (engineRef.current) {
      Matter.Engine.update(engineRef.current);
    }
  };

  return {
    points: pointsRef.current,
    constraints: constraintsRef.current,
    isRunning,
    start,
    stop,
    update,
  };
}

コンポーネントでの使用

'use client';

import { useJellySimulation } from './useJellySimulation';
import { useCanvasRenderer } from './useCanvasRenderer';
import { useMouseInteraction } from './useMouseInteraction';
import { useEffect } from 'react';

export function JellySimulationComponent() {
  const config = {
    centerX: 400,
    centerY: 300,
    radius: 100,
    pointCount: 30,
    stiffness: 0.5,
    damping: 0.05,
  };

  const { points, constraints, start, update } = useJellySimulation(config);
  const canvasRef = useCanvasRenderer(points);
  useMouseInteraction(engine, canvasRef);

  useEffect(() => {
    start();

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

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

パフォーマンス最適化

1. useMemoで計算結果をキャッシュ

重い計算をuseMemoでキャッシュします。

'use client';

import { useMemo } from 'react';
import Matter from 'matter-js';

export function useOptimizedPhysics(bodies: Matter.Body[]) {
  // 重い計算をuseMemoでキャッシュ
  const bounds = useMemo(() => {
    return bodies.map((body) => ({
      minX: body.bounds.min.x,
      minY: body.bounds.min.y,
      maxX: body.bounds.max.x,
      maxY: body.bounds.max.y,
    }));
  }, [bodies]);

  return { bounds };
}

2. useCallbackで関数をメモ化

コールバック関数をuseCallbackでメモ化します。

'use client';

import { useCallback, useState } from 'react';

export function usePhysicsControls() {
  const [isPaused, setIsPaused] = useState<boolean>(false);

  const pause = useCallback(() => {
    setIsPaused(true);
  }, []);

  const resume = useCallback(() => {
    setIsPaused(false);
  }, []);

  const reset = useCallback(() => {
    // リセット処理
    setIsPaused(false);
  }, []);

  return { isPaused, pause, resume, reset };
}

3. 適応的レンダリング

FPSを監視して、必要に応じてレンダリングを調整します。

'use client';

import { useEffect, useRef, useState } from 'react';

export function useAdaptiveRendering() {
  const [fps, setFps] = useState<number>(60);
  const frameCountRef = useRef<number>(0);
  const lastTimeRef = useRef<number>(performance.now());

  useEffect(() => {
    const measureFPS = () => {
      frameCountRef.current++;
      const now = performance.now();
      const delta = now - lastTimeRef.current;

      if (delta >= 1000) {
        setFps(frameCountRef.current);
        frameCountRef.current = 0;
        lastTimeRef.current = now;
      }

      requestAnimationFrame(measureFPS);
    };

    measureFPS();
  }, []);

  // FPSに応じてレンダリング品質を調整
  const quality = fps < 30 ? 'low' : fps < 50 ? 'medium' : 'high';

  return { fps, quality };
}

カスタムフックの設計パターン

1. 単一責任の原則

各カスタムフックは、単一の責任を持つように設計します。

// 良い例: 単一責任
export function usePhysicsEngine() {
  // 物理エンジンの管理のみ
}

export function useCanvasRenderer() {
  // Canvas描画の管理のみ
}

// 悪い例: 複数の責任
export function usePhysicsAndRenderer() {
  // 物理エンジンとCanvas描画の両方を管理(避ける)
}

2. 依存関係の最小化

カスタムフックの依存関係を最小限にします。

// 良い例: 最小限の依存関係
export function usePhysicsEngine(config: PhysicsConfig) {
  // configのみに依存
}

// 悪い例: 多くの依存関係
export function usePhysicsEngine(
  config: PhysicsConfig,
  renderer: Renderer,
  eventHandler: EventHandler
) {
  // 多くの依存関係(避ける)
}

3. 再利用性の確保

カスタムフックは、複数のコンポーネントで再利用できるように設計します。

// 良い例: 再利用可能
export function useCounter(initialValue: number = 0) {
  const [count, setCount] = useState(initialValue);
  const increment = () => setCount((prev) => prev + 1);
  const decrement = () => setCount((prev) => prev - 1);
  const reset = () => setCount(initialValue);
  return { count, increment, decrement, reset };
}

// 使用例
function Counter1() {
  const { count, increment } = useCounter(0);
  // ...
}

function Counter2() {
  const { count, increment } = useCounter(10);
  // ...
}

まとめ

React Hooksを使うことで、物理シミュレーションを以下のように実装できます:

  1. カスタムフックの活用: 物理エンジン、Canvas描画、マウス操作などをカスタムフックに分離
  2. 状態管理: useStateでシミュレーションの状態を管理
  3. 副作用処理: useEffectでライフサイクルやクリーンアップを処理
  4. 参照管理: useRefでDOM要素や値を保持
  5. パフォーマンス最適化: useMemouseCallbackで最適化

主な実装パターン:

  • usePhysicsEngine: 物理エンジンの管理
  • useCanvasRenderer: Canvas描画の管理
  • useMouseInteraction: マウス操作の処理
  • useJellySimulation: ゼリーシミュレーションの実装
  • パフォーマンス最適化: useMemo、useCallback、適応的レンダリング

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

関連記事

関連ツール

関連記事