React Hooksを使った物理シミュレーション
React Hooksを使って物理シミュレーションを実装する方法を、実際のコード例とともに詳しく解説します。
React Hooksを使った物理シミュレーション
React Hooksを使って物理シミュレーションを実装する方法を、実際のコード例とともに詳しく解説します。useState、useEffect、useRef、カスタムフックなど、Hooksを活用した実装パターンを紹介します。
React Hooksとは
React Hooksは、関数コンポーネントで状態管理やライフサイクルを扱うための機能です。クラスコンポーネントを使わずに、関数コンポーネントでReactの機能を活用できます。
Hooksの主な特徴
- 状態管理:
useStateでコンポーネントの状態を管理 - 副作用処理:
useEffectでライフサイクルや副作用を処理 - 参照管理:
useRefでDOM要素や値を保持 - カスタムフック: ロジックを再利用可能なフックに抽出
基本的な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を使うことで、物理シミュレーションを以下のように実装できます:
- カスタムフックの活用: 物理エンジン、Canvas描画、マウス操作などをカスタムフックに分離
- 状態管理:
useStateでシミュレーションの状態を管理 - 副作用処理:
useEffectでライフサイクルやクリーンアップを処理 - 参照管理:
useRefでDOM要素や値を保持 - パフォーマンス最適化:
useMemo、useCallbackで最適化
主な実装パターン:
- usePhysicsEngine: 物理エンジンの管理
- useCanvasRenderer: Canvas描画の管理
- useMouseInteraction: マウス操作の処理
- useJellySimulation: ゼリーシミュレーションの実装
- パフォーマンス最適化: useMemo、useCallback、適応的レンダリング
これらのパターンを組み合わせることで、保守性の高い物理シミュレーションを実装できます。
関連記事
関連ツール
関連記事
TypeScriptの型安全性を活かした開発
TypeScriptの型安全性を活かした開発方法を、実際のコード例とともに詳しく解説します。
2026年1月8日
パフォーマンス最適化の実践
Webアプリケーションのパフォーマンスを最適化する実践的な方法を、実際のコード例とともに詳しく解説します。
2026年1月8日
ゼリーシミュレーションの使い方・活用方法
ゼリーシミュレーションの基本的な使い方から、物理シミュレーションの仕組みまで詳しく解説します。
2026年1月8日