TypeScriptで物理シミュレーションを実装する方法
TypeScriptを使って物理シミュレーションを実装する方法を、実際のコード例とともに詳しく解説します。
TypeScriptで物理シミュレーションを実装する方法
TypeScriptを使って物理シミュレーションを実装する方法を、実際のコード例とともに詳しく解説します。Matter.jsを使った実装から、型安全性を活かした実装パターンまで、実践的な内容を紹介します。
TypeScriptで物理シミュレーションを実装する理由
TypeScriptを使うことで、物理シミュレーションの実装において以下のメリットが得られます:
- 型安全性: コンパイル時にエラーを検出できる
- 開発効率: IDEの補完機能が強力
- 保守性: コードの可読性と保守性が向上
- リファクタリング: 型情報を活用した安全なリファクタリングが可能
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を組み合わせることで、以下のような実装が可能になります:
- 型安全な物理エンジンのラッパー: エンジンの操作を型安全に
- ボディ・制約のファクトリー: 型安全なボディ・制約の作成
- Reactカスタムフック: 物理シミュレーションをReactで使いやすく
- 型安全なイベント処理: イベントの型定義
- パフォーマンス最適化: 型安全なパフォーマンス設定
これらの実装パターンを活用することで、保守性の高い物理シミュレーションを実装できます。
関連記事
関連ツール
関連記事
Canvas APIを使った描画の最適化
Canvas APIを使った描画を最適化する方法を、実際のコード例とともに詳しく解説します。
2026年1月8日
Next.js App Routerでの実装パターン
Next.js App Routerを使った実装パターンを、実際のコード例とともに詳しく解説します。
2026年1月8日
Matter.jsを使った物理シミュレーション入門
Matter.jsを使った物理シミュレーションの実装方法を、実際のコード例とともに解説します。
2026年1月7日