HAKUTAI Tech Notes

IT関連、数学のことなどを主として思いつきで色々書き溜めていきます

matter.jsの基本的な機能を使ったサンプル集

matter.jsはWEBブラウザ上で2次元の物理演算と描画を行うことができるjavascriptのライブラリである。

brm.io

公式リファレンスが整備されているので各モジュールに含まれるプロパティやメソッドなどを確認することができるが、具体的にどのように使っていけばいいのか分かりにくいものも結構ある。
そこで、いくつか簡単なサンプルを自分で作ってみながらmatter.jsの基本的な機能に触れつつ、具体的な使い方や初見で分かりにくかった点などについて調べメモ程度にまとめた。

共通設定

共通のテンプレートファイル。この後出てくる各サンプルのjsファイルを1つずつscriptタグで読み込んでいくことになる。
<div id="canvas-area"></div>の内部に物理演算用canvasが挿入される。

<html lang="ja">

<head>
  <meta charset="utf-8">
  <title>Matter.js Sample</title>
</head>

<body>
  <div id="canvas-area"></div>
  <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
  <script src="./matter.js"></script>
  <!-- 以下、各例のjsファイルをscriptタグで読み込む -->
  <script src="./basicBodyCreation.js"></script>
</html>


次は後述の各サンプルの共通設定にあたる部分で、各jsファイルの先頭に記述しておくものである。

// 使用モジュール
const Engine = Matter.Engine,
  Render = Matter.Render,
  Runner = Matter.Runner,
    Body = Matter.Body,
  Bodies = Matter.Bodies,
  Composite = Matter.Composite,
  Composites = Matter.Composites,
    Vector = Matter.Vector,
  Constraint = Matter.Constraint,
  MouseConstraint = Matter.MouseConstraint,
  Mouse = Matter.Mouse,
  Events = Matter.Events;

// エンジンの生成
const engine = Engine.create();

// 物理演算canvasを挿入する要素
const canvas = $('#canvas-area')[0];

// レンダリングの設定
const render = Render.create({
  element: canvas,
  engine: engine,
  options: {
    width: 800,
    height: 600,
  }
});

// マウス、マウス制約を生成
const mouse = Mouse.create(canvas);
const mouseConstraint = MouseConstraint.create(engine, {
  mouse: mouse,
  constraint: {
    render: {
      visible: false
    }
  }
})

Composite.add(engine.world, mouseConstraint)
render.mouse = mouse

// レンダリングを実行
Render.run(render);

// エンジンを実行
Runner.run(engine);

/**
 * 以下、各例毎に処理を記述する
 */


共通設定で行っていることの詳細については、以前投稿した記事に書いている。 mmsrtech.com

サンプル色々

ここで掲載しているjavascriptのコードは前述の共通設定を除いた部分のみ記述している。

物体の生成と配置

まずは基本中の基本で、オブジェクト(静止、可動)を生成するだけ。


基本形(円、長方形、正多角形)の剛体オブジェクト(Bodyオブジェクト)を生成するときはBodiesモジュールのメソッドを使う。BodiesモジュールにはBodyオブジェクトを生成するための各種メソッドが含まれている。

<円の生成>

Bodies.circle x, y, 半径, オプション

<長方形の生成>

Bodies.rectangle x, y, 幅, 高さ, オプション

<正多角形の生成>

Bodies.polygon x, y, 辺の数, 半径, オプション

x, yはオブジェクトの中心の座標値を指定する。
オプション(任意の引数)は生成されるオブジェクトの各種プロパティを予め設定しておくものであり、そのオブジェクトの物理量(質量、摩擦係数、慣性モーメント、傾きなど)のようなものになると考えて良い。なお、プロパティの更新はBodyモジュールのメソッドで行うことができるので、生成時にオプションで指定しなくても後からプロパティを新規設定・更新することもできる。
設定可能なオプションはMatter.BodyモジュールのProperties / Optionsで確認できる。

// 静止オブジェクト(空中の床と地面)【①】
const floor1 = Bodies.rectangle(400, 200, 500, 30, { isStatic: true });
const floor2 = Bodies.rectangle(150, 350, 300, 30, { angle: Math.PI / 6, isStatic: true });
const floor3 = Bodies.rectangle(650, 350, 300, 30, { angle: -Math.PI / 6, isStatic: true });
const ground = Bodies.rectangle(400, 585, 800, 30, { isStatic: true });

// 可動オブジェクト(正方形と円)【②】
const square = Bodies.rectangle(floor1.bounds.min.x, floor1.bounds.max.y - 50, 50, 50, { friction: 0.001 });
const circle = Bodies.circle(floor1.bounds.max.x, floor1.bounds.max.y - 50, 25, { friction: 0.01 });

// オブジェクトの追加【③】
Composite.add(engine.world, [floor1, floor2, floor3, ground, square, circle]);


【①】静止オブジェクトの生成
オプションでisStatic: trueを指定すれば静止オブジェクト(自ら動くこともドラッグで移動させることもできない)となる。
オプションでangleに角度(ラジアン)を設定すると、その分回転した状態でオブジェクトが生成される。


【②】可動オブジェクトの生成
可動オブジェクトの生成は特別なことは何もない。isStaticはデフォルトがfalseなので、特に何もオプションを指定しなければ可動になる。


【③】オブジェクトの追加

Composite.add 投入先コンポジット, オブジェクト

オブジェクトを生成するだけではcanvasに描画されないので、Compositeモジュールのaddメソッドで追加する必要がある。
第1引数はオブジェクトを投入する先のコンポジットを指定するものであり、基本的にengine.worldを指定しておけばいい。
第2引数は投入するオブジェクトを指定するものだが、複数のオブジェクトを追加する場合は配列にして渡す必要がある。オブジェクト1個だけなら配列でなくて良い。

※コンポジットの詳細については後述

なお、旧版のmatter.jsではオブジェクト等の追加を行うモジュールとしてMatter.Worldがあったが、最新版ではMatter.Compositeに置き換えられたようである。

オブジェクトのドラッグ移動

canvasに投入したオブジェクトをドラッグ移動できるようにする。また、マウスのx, y座標とドラッグしているオブジェクトの種類(RectangleかCircleかPolygonか)をcanvas外に表示している。


共通設定のところでMouseConstraintオブジェクト(マウス制約)を生成してComposite.add()で追加した時点でオブジェクトをドラッグ移動できるようになっているのでそれについては特筆することはない。

$('body').append('<p class="coordinate"></p>');
$('body').append('<p class="target"></p>');

// 静止オブジェクト(空中の床と地面)
const floor = Bodies.rectangle(400, 200, 500, 30, { isStatic: true });
const ground = Bodies.rectangle(400, 585, 800, 30, { isStatic: true });

// 可動オブジェクト(正方形、円、三角形)
const square = Bodies.rectangle(floor.bounds.min.x + 50, floor.bounds.max.y - 50, 50, 50);
const circle = Bodies.circle(floor.position.x, floor.bounds.max.y - 50, 50);
const triangle = Bodies.polygon(floor.bounds.max.x - 50, floor.bounds.max.y - 50, 3, 50);

Composite.add(engine.world, [floor, ground, square, circle, triangle]);

// mousemoveイベントを設定してマウスの座標、ドラッグ対象を表示する【④】
Events.on(mouseConstraint, 'mousemove', e => {
  $('p.coordinate').text(`X: ${e.mouse.position.x} Y: ${e.mouse.position.y}`);
  const label = mouseConstraint.body ? mouseConstraint.body.label : '';
  $('p.target').text(`Dragging ${label}`);
});


【④】マウスイベントの設定

Events.on イベントバインディングの対象, イベント名, コールバック関数

Eventsモジュールを利用してMouseConstraintオブジェクトに各種マウスイベントを設定することができる。

mousedown マウスボタンが押された時に発生する
mousemove マウスが移動した時に発生する
mouseup マウスボタンが離された時に発生する
startdrag オブジェクトのドラッグを開始したときに発生する
enddrag オブジェクトのドラッグを終了したときに発生する

コールバック関数のイベントオブジェクトにはmouseが含まれているので、mouse.positionでマウスポインタの座標を参照することができる。
また、mouseConstraint.bodyでドラッグ中のオブジェクトを参照することができる。

オブジェクトの拘束

2つの物体同士、または物体と空間内の点を接続してそれらを拘束する。

// 静止オブジェクト(右上の長方形)
const rectangle = Bodies.rectangle(600, 70, 50, 30, { isStatic: true });

// 拘束されない正方形
const square = Bodies.rectangle(400, 0, 50, 50);

// 拘束される物体(左の正六角形、右の円2つ、中央の正方形)
const boundHex = Bodies.polygon(200, 300, 6, 50);
const boundCircle1 = Bodies.circle(700, 220, 20);
const boundCircle2 = Bodies.circle(700, 370, 20);
const boundSquare = Bodies.rectangle(400, 450, 100, 100);

Composite.add(engine.world, [rectangle, square, boundHex, boundCircle1, boundCircle2, boundSquare]);

// 矩形と2個の円の直列接続(各オブジェクトの中心の接続)【⑤】
const constraint1Upper = Constraint.create({
  bodyA: rectangle,
  bodyB: boundCircle1,
  stiffness: 1
});
const constraint1Lower = Constraint.create({
  bodyA: boundCircle1,
  bodyB: boundCircle2,
  stiffness: 1
});
Composite.add(engine.world, [constraint1Upper, constraint1Lower]);

// 正方形と2本のバネ接続(正方形の表面と空間点の接続)【⑥】
const constraint2Left = Constraint.create({
  pointA: { x: 50, y: 400 },
  bodyB: boundSquare,
  pointB: { x: -50, y: 0 },
  stiffness: 0.1
});
const constraint2Right = Constraint.create({
  pointA: { x: 750, y: 400 },
  bodyB: boundSquare,
  pointB: { x: 50, y: 0 },
  stiffness: 0.1
});
Composite.add(engine.world, [constraint2Left, constraint2Right]);

// 正六角形のピン接続【⑥】
const constraint3 = Constraint.create({
  pointA: { x: 200, y: 200 },
  bodyB: boundHex,
  length: 0,
  stiffness: 0.5,
});
Composite.add(engine.world, constraint3);


【⑤】オブジェクト同士の接続

Constraint.create オプション

create()メソッドに渡すオプションで様々な拘束条件を設定する。
接続する2つのBodyオブジェクトをbodyAbodyBに指定すると、両オブジェクトはそれぞれの中心を接続点として拘束される。接続点の位置をずらしたい場合は中心を原点(0, 0)とした相対的な座標をpointApointBに指定する。
stiffnessは拘束の硬さを表すプロパティ、lengthは「目標静止長」という拘束対象のオブジェクトの初期位置から自動計算されるプロパティであり、stiffnesslengthの値によって拘束の種類を変えることができる。

拘束の種類 条件 特徴
spring 0 < stiffness ≦ 0.1 伸び縮みのあるバネのような拘束
line 0.9 ≦ stiffness ≦ 1 伸び縮みのない棒のような拘束
pin length = 0かつ0.1 < stiffness 長さのない1点での拘束

Composite.add()で拘束対象のオブジェクトと拘束条件をそれぞれ追加すると拘束が有効になる。


【⑥】オブジェクトと空間の接続
bodyAまたはbodyBの一方だけを指定してもう一方はポイントを指定すると、そのポイントに対応した空間内の1点とオブジェクトが接続される。

複数の物体の結合

複数の長方形を結合して箱型ブランコのようなものを作る。


予め┗┛の形になるように位置を調整した長方形を3つ生成し、左右に立つ長方形を少し傾ける。

// 静止オブジェクト(地面)
const ground = Bodies.rectangle(400, 585, 800, 30, { isStatic: true });

Composite.add(engine.world, [ground]);

// 可動オブジェクト(ボール)
const ball = Bodies.circle(400, 500, 20, { restitution: 0.5 });

// 可動の複合オブジェクト
const bottom = Bodies.rectangle(400, 550, 200, 20);
const leftSide = Bodies.rectangle(310, 510, 20, 100);
const rightSide = Bodies.rectangle(490, 510, 20, 100);
// 側面の回転、位置の調整【⑦】
Body.setAngle(leftSide, -Math.PI / 6);
Body.translate(leftSide, { x: -100 / 2 * Math.tan(Math.PI / 6), y: 0 });
Body.setAngle(rightSide, Math.PI / 6);
Body.translate(rightSide, { x: 100 / 2 * Math.tan(Math.PI / 6), y: 0 });

// 部品を結合したオブジェクトの生成【⑧】
const compoundBody = Body.create({
  parts: [bottom, leftSide, rightSide],
  inertia: Infinity,
  frictionAir: 0.001
});

const constraint1 = Constraint.create({
  bodyA: compoundBody,
  pointA: { x: -100, y: 30 },
  pointB: { x: 300, y: 330 },
  stiffness: 1
});

const constraint2 = Constraint.create({
  bodyA: compoundBody,
  pointA: { x: 100, y: 30 },
  pointB: { x: 500, y: 330 },
  stiffness: 1
});

Composite.add(engine.world, [ball, compoundBody, constraint1, constraint2]);


【⑦】部品の回転、位置の調整

Body.setAngle オブジェクト, 回転角度

オブジェクトのangleを設定・更新するメソッドで、これも回転角度をラジアンで指定する。 オブジェクト生成時にangleを指定するのと結果的には同じである。


Body.translation オブジェクト, 変位

対象のオブジェクトを変位分だけ平行移動する。 変位はベクトル(現在位置のx, y座標からの相対的な移動距離)で指定する。
オブジェクトの位置を変更するメソッドとしてBody.setPosition()もあるが、移動後の位置の絶対座標を指定しないといけないので、今回のようにずらしたい距離(変位)から設定したい場合はtranslation()の方が扱いやすい。


【⑧】部品を結合したオブジェクトの生成

Body.create オプション

partsプロパティに部品となるオブジェクトの配列を指定すると、それらが結合された複合体のオブジェクトを生成することができる。個々の部品の位置や角度調整が必要になるものの、この方法であれば凹面や空洞などを含む複雑な形状も作ることができる。
Composite.add()には部品全てを指定する必要はなく、生成された複合体オブジェクト1つを指定すれば良い。
なお、create時に他にもプロパティを設定している。
inertiaは慣性モーメント(物体を回転させるために必要な力の量、つまり物体の回転しにくさ)であり、Infinity(無限大)にすればオブジェクトは回転しなくなるので常に地面に対し水平を保ったまま動かせる。
frictionAirは空気摩擦(空気抵抗)であり、値が大きいほどオブジェクトの運動速度の低下が大きくなる。

衝突フィルター

正方形、円、三角形が2つずつあり、同じ種類の図形同士でしか衝突できないようにしている。 また、これらの図形と床・地面の衝突の有無をチェックボックスで切り替えられるようにしている。


BodyオブジェクトはcollisionFilter(衝突フィルター)というプロパティを持っており、さらにcollisionFiltergroup(衝突グループ)、category(衝突カテゴリ)、mask(マスク)というプロパティを持っている。matter.jsではこれらのプロパティの値に応じて「グループ」または「カテゴリ/マスク」のいずれかのルールに基づく衝突の演算が行われる。

「グループ」に基づく衝突

2つのBodyオブジェクトについて、 両者のgroupが等しい且つgroup > 0である場合は衝突し、 両者のgroupが等しい且つgroup < 0である場合は衝突しない。 両者のgroupが異なる、または少なくとも一方がgroup = 0である場合、衝突は次の「カテゴリ/マスク」ルールに基づくことになる。
「グループ」ルールに基づく衝突はあまり細かく条件を設定することができない。

「カテゴリ/マスク」に基づく衝突

collisionFilter.groupのデフォルト値は0なので、基本的にgroupを変更していなければ「カテゴリ/マスク」ルールになる。複雑な衝突の条件を設定したい場合はこちらを利用する。
2つのオブジェクトがある時に、それぞれのmaskにもう一方のオブジェクトのcategoryを含む場合のみ2つは衝突する。
例えば下の図で、円(categoryがAでmaskにBを含む)と正方形(categoryがBでmaskにAを含む)は衝突する。一方で、円(maskにCを含まない)と三角形(categoryがCでmaskにAを含まない)は衝突しない。
長方形(categoryがD)はmaskを指定していないが、その場合はmaskに全ての衝突カテゴリ(最大32種類)が指定されているのと同じことになる。つまり、長方形は円、正方形、三角形の全てと衝突することになる。

$('body').append('<p>床との衝突あり<input type="checkbox" value="1" checked></p>');

// 衝突のカテゴリ【⑨】
const commonCategory = 0x0001, // 全オブジェクト共通のカテゴリ
  staticCategory = 0x0002, // 静止オブジェクトのカテゴリ
  squareCategory = 0x0004, // 正方形のカテゴリ
  circleCategory = 0x0008, // 円のカテゴリ
  triangleCategory = 0x0010; // 三角形のカテゴリ

// 静止オブジェクト(空中の床と地面)【⑩】
const floor = Bodies.rectangle(400, 300, 500, 30, {
  collisionFilter: {
    category: commonCategory
  },
  isStatic: true
});
const ground = Bodies.rectangle(400, 585, 800, 30, {
  isStatic: true,
  collisionFilter: {
    category: commonCategory
  },
});

// 可動オブジェクト(上下の正方形、円、三角形)【⑪】
const square1 = Bodies.rectangle(floor.bounds.min.x + 50, 100, 50, 50, {
  collisionFilter: {
    category: squareCategory,
    mask: commonCategory | squareCategory
  },
});
const circle1 = Bodies.circle(floor.position.x, 100, 50, {
  collisionFilter: {
    category: circleCategory,
    mask: commonCategory | circleCategory
  },
});
const triangle1 = Bodies.polygon(floor.bounds.max.x - 50, 100, 3, 50, {
  collisionFilter: {
    category: triangleCategory,
    mask: commonCategory | triangleCategory
  },
});
const square2 = Bodies.rectangle(floor.bounds.min.x + 50, ground.bounds.min.y - 25, 50, 50, {
  collisionFilter: {
    category: squareCategory,
    mask: commonCategory | squareCategory
  },
});
const circle2 = Bodies.circle(floor.position.x, ground.bounds.min.y - 50, 50, {
  collisionFilter: {
    category: circleCategory,
    mask: commonCategory | circleCategory
  },
});
const triangle2 = Bodies.polygon(floor.bounds.max.x - 50, ground.bounds.min.y - 25, 3, 50, {
  collisionFilter: {
    category: triangleCategory,
    mask: commonCategory | triangleCategory
  },
});

Composite.add(engine.world, [floor, ground, square1, circle1, triangle1, square2, circle2, triangle2]);

// 静止物体の衝突あり・なしを切り替える【⑫】
$('input').change(() => {
  const isChecked = $('input').prop('checked');
  floor.collisionFilter.category = isChecked ? commonCategory : staticCategory;
  ground.collisionFilter.category = isChecked ? commonCategory : staticCategory;
})


【⑨】使用する衝突カテゴリの用意
categoryの値は2の冪乗の数で指定され、最大32個(20〜231)まで使用することができる。なお、この値は20, 21, 22, 23, 24…231つまり1, 2, 4, 8, 16…2147483648をそれぞれn進数のリテラル表記にするようで、
2進数なら0b000010b000100b001000b010000b10000
16進数なら0x000010x000020x000040x000080x00010
のように設定する。(が、実は普通に10進数で指定しても目的の挙動にはなる)


【⑩】静止オブジェクトの衝突カテゴリの設定
静止オブジェクトの衝突カテゴリには、この例に含まれる全オブジェクト共通のcommonCategoryを指定する。
静止オブジェクトにはmaskを指定していないのでsquareCategorycircleCategorytriangleCategoryは全て含まれていることになる。したがって、正方形、円、三角形の各オブジェクトのmaskcommonCategoryを含めておけば(⑪で設定)静止オブジェクトと衝突させることができる。


【⑪】可動オブジェクトの衝突カテゴリの設定
オブジェクトの衝突対象となる衝突カテゴリをmaskに指定する。
ORビット演算子|を用いて衝突カテゴリを複数指定することができる。


【⑫】可動オブジェクトの衝突カテゴリの設定
チェックボックスのON/OFFを切り替える度に静止オブジェクトの衝突カテゴリがcommonCategorystaticCategoryに切り替わる。可動オブジェクトのmaskにはstaticCategoryが含まれないので、静止オブジェクの衝突カテゴリがstaticCategoryである間は可動オブジェクトが衝突せず突き抜けることになる。

オブジェクトの表示切り替え、削除、総数のカウント

canvasをクリックした位置にランダムな大きさのボールを生成し、存在しているボールの個数をcanvas外に表示する。canvasの表示領域より下に落ちたボールは随時消していく。


この例ではボール用のコンポジットを用意する。
コンポジットとは、個々のBodyオブジェクトや拘束条件などをまとめた配列のようなもので、コンポジットに別のコンポジットを追加して階層構造のようにすることもできる。
これまで色々な場所で利用してきたengine.worldもコンポジットの一つであり、全コンポジットの親に当たるものと考えられる。

$('body').append('<p class="body-counter">Number of balls : <span></span></p>');
$('body').append('<button>Clear</button>');

// ボール用Compositeを生成する【⑬】
const ballComposite = Composite.create();
Composite.add(engine.world, ballComposite);

// 静止オブジェクト(空中の床と画面外落下判定オブジェクト)【⑭】
const floor = Bodies.rectangle(400, 400, 500, 30, { isStatic: true });
const pit = Bodies.rectangle(400, 900, 50000, 30, { isStatic: true, label: 'pit' });
Composite.add(engine.world, [floor, pit]);

// クリックした位置に円を生成とballCompositeへの追加
Events.on(mouseConstraint, 'mousedown', e => {
  // ドラッグ中は生成しない
  if (mouseConstraint.body) { return }
  // 半径はランダム(10〜30)
  const min = 10;
  const max = 30;
  const radius = Math.random() * (max - min) + min;
  const ball = Bodies.circle(e.mouse.position.x, e.mouse.position.y, radius, { restitution: 0.5 });
  Composite.add(ballComposite, ball);
});

// Engineモジュールに対するイベント/衝突の発生を検知する【⑮】
Events.on(engine, 'collisionStart', e => {
  $.each(e.pairs, (i, pair) => {
    // 画面外落下判定オブジェクトに衝突したボールを削除する
    if (pair.bodyA.label === 'pit') {
      Composite.remove(ballComposite, pair.bodyB);
    }
  })
});

// Compositeへのオブジェクト追加を検知してボール総数の表示を更新する【⑯】
Events.on(ballComposite, 'afterAdd', e => {
  // Eventオブジェクトを直接参照してCompositeに含まれる全bodyを取得
  $('p.body-counter span').text(e.source.bodies.length);
});
// Compositeからのオブジェクト削除を検知してボール総数の表示を更新する【⑯】
Events.on(ballComposite, 'afterRemove', () => {
  // Composite#allBodies()を利用してCompositeに含まれる全bodyを取得
  $('p.body-counter span').text(Composite.allBodies(ballComposite).length);
});

$('button').on('click', () => {
  // ボールを一括削除する【⑰】
  Composite.clear(ballComposite);
  $('p.body-counter span').text(0);
});


【⑬】ボール用Compositeを生成する
生成したボールの個数をカウントしたり一括削除したりするのに便利なので、ボール専用のballCompositeを生成する。このコンポジットもengine.worldに追加する。


【⑭】画面外への落下を判定するオブジェクト
canvasの表示領域よりも下にボールが落下したかどうかを判定するための仕込み。
十分な横幅を持った長方形オブジェクトをcanvasの表示領域より下に設置しておき、それに衝突したボールは画面外に落下したものとみなす。衝突判定の時に必要になるので長方形オブジェクトにはlabelプロパティでpitという名前を付けている。


【⑮】衝突の発生を検知する
Engineモジュールには空間内のオブジェクトの衝突を検知するイベントが用意されている。 物理演算・描画の更新は一定時間毎(デフォルトは16.666ミリ秒)に行われており、その更新後にイベントが発生する。

collisionActive オブジェクトが衝突している間発生し続ける
collisionStart オブジェクトが衝突し始めた瞬間に発生する
collisionEnd オブジェクトが衝突し終わった瞬間に発生する

イベントオブジェクト内のpairsには衝突しているオブジェクトのペアの配列が含まれているので、衝突ペアのうちbodyAが落下判定用の長方形オブジェクト(labelpit)であればもう一方のbodyBのボールを削除する。
Composite.remove(オブジェクトを削除するコンポジット, 削除するオブジェクト)
なお、衝突している2つのオブジェクトのうち、idの小さい方がbodyAになる。この例ではボールよりも先に落下判定用の長方形オブジェクトを生成するので、常に衝突ペアのbodyAは長方形オブジェクト、bodyBはボールオブジェクトになる。


【⑯】オブジェクト追加と削除を検知する
Compositeモジュールにはオブジェクトの追加や削除を検知するイベントが用意されている。

beforeAdd オブジェクトが追加される前に発生する
beforeRemove オブジェクトが削除される前に発生する
afterAdd オブジェクトが追加された後に発生する
afterRemove オブジェクトが削除された後に発生する

コンポジット内のオブジェクトを参照する時は、e.source.bodiesでイベントオブジェクトを直接参照するかComposite.allBodies()メソッドを利用する。


【⑰】コンポジット内のオブジェクトを一括削除する

Composite.clear オブジェクトを削除するコンポジット

特定のコンポジットに含まれるオブジェクトを一括削除することができる。その他のコンポジットには影響はない。 clear()メソッドによるオブジェクトの削除はbeforeRemoveafterRemoveイベントで拾われないので注意が必要。

Compositesによるオブジェクトの拘束

Compositesモジュールを利用して鎖のように複数のオブジェクトを一列に繋いだり、縦横の格子状に繋いだりして複合体を生成する。

/**
 * 図形の列を数珠繋ぎにする
 */
// 図形の列を生成する【⑱】
const chain = Composites.stack(650, 60, 7, 1, 0, 50, (x, y) => {
  return Bodies.polygon(x, y, 8, 30);
});
// 図形の列を接続する【⑲】
Composites.chain(chain, 0.5, 0, -0.5, 0, { stiffness: 0.5, length: 1 });
const chainConstraint = Constraint.create({
  pointA: { x: 650, y: 50 },
  bodyB: chain.bodies[0],
  pointB: { x: -30, y: 0 },
  stiffness: 1
})
Composite.add(engine.world, [chain, chainConstraint]);

/**
 * 図形の行列を格子状に繋ぐ
 */
const rows = 10;
const columns = 10;
const dx = 30;
const dy = 30;

// 縦横に並んだ図形の集合を生成する【⑱】
const mesh = Composites.stack(100, 50, columns, rows, dx, dy, (x, y, column, row) => {
  // 最上行のみ固定する
  const isStatic = row === 0;
  return Bodies.circle(x, y, 10, { isStatic: isStatic});
});

// 図形の集合を接続する【⑲】
Composites.mesh(mesh, columns, rows, false, { stiffness: 0.9 });
Composite.add(engine.world, [mesh]);


【⑱】縦横に並ぶオブジェクトの集合を生成する

Composites.stack x, y, 列数, 行数, 列方向の間隔, 行方向の間隔, コールバック関数

コールバック関数内の処理で生成されるオブジェクトを、座標、列数、行数などの条件に基づき並べたコンポジットを生成する。 複数のオブジェクトを整然と並んだ状態でまとめて生成したい場合はこれを使うと便利。
xyは生成されるコンポジット内の先頭のオブジェクトの中心位置を指定するものであり、ここを基準に後続のオブジェクトが並べられる。この時点ではまだオブジェクトは結合されていないので動かせば簡単に崩れてしまう。
なお、オブジェクトは図のような順の1次元配列としてbodiesに格納されているので、1次元のインデックスを指定すれば各々のオブジェクトを参照することができる。

コールバック関数の引数はx, y, column, row, lastBody, iを渡すことができ、それぞれ各オブジェクトの「中心のx, y座標、列番号、行番号、1つ前のオブジェクト、1次元の番号」である。


【⑲】オブジェクトの集合を繋ぐ
stack()で生成したコンポジットに対してchain()mesh()メソッドを利用してオブジェクトを繋いでいく。 これらのメソッドを実行すると、オブジェクトを結合するための拘束条件が対象のコンポジットに追加される。

<オブジェクトを直列に繋ぐ>

Composites.chain コンポジット, xOffsetA, yOffsetA, xOffsetB, yOffsetB, オプション

xOffsetA, yOffsetA, xOffsetB, yOffsetBbodyAbodyB(1つ後ろのオブジェクト)を拘束する時の両者の接続点の位置を指定するものであるが、座標値ではなく中心からの変位の割合(0〜1)を指定する。 オブジェクトの表面を接続点にする場合はxOffsetを±0.5にしておけばいい。(初期が縦並びの場合はyOffsetを±0.5にする)

<オブジェクトを格子状に繋ぐ>

Composites.mesh コンポジット, 列数, 行数, crossBrace, オプション

crossBraceは行列の格子構造に斜めの梁を入れるかどうかのフラグである。斜めの梁が入ると頑丈になり、オブジェクトの格子形状が変形しにくくなる。

外力を加える

前述の箱型ブランコの例は空気抵抗により振動が減衰していくだけだったので、外部から瞬間的に力を加えてブランコを漕げるようにしてみる。

// 箱型ブランコを生成する部分は同じなので省略

$('body').append(
  '<div style="width: 800px; margin-top: 10px; display: flex; justify-content: center;">' +
  '<button id="left" style="width: 100px; height: 30px;">◀︎◀︎</button>' +
  '<button id="right" style="width: 100px; height: 30px;">▶︎▶︎</button>' +
  '</div>'
);

// 地面に対して水平右向きを正とする一定な大きさの力を生成する【⑳】
const magnitude = 0.2;
const centerX = (compoundBody.bounds.max.x + compoundBody.bounds.min.x) / 2;
const centerY = (compoundBody.bounds.max.y + compoundBody.bounds.min.y) / 2;
const workingPoint = Matter.Vector.create(centerX, centerY);
const forceVector = Matter.Vector.create(magnitude, 0);

$('#left').on('click', () => {
  // 左向きに力を加える【㉑】
  Body.applyForce(compoundBody, workingPoint, Matter.Vector.neg(forceVector));
});

$('#right').on('click', () => {
  // 右向きに力を加える【㉑】
  Body.applyForce(compoundBody, workingPoint, forceVector);
});


【⑳】力を生成する
力を作用させるためにはその力の作用点と終点の座標を指定する必要があるので、あらかじめそれらの点のVectorオブジェクトを生成する。終点は作用点を原点に取った相対的な座標値を指定する。
余談だが、Vectorオブジェクトとして生成されるものは空間内の1点を表すx, y座標のセットであり、大きさと向きの情報を持つ本来の「ベクトル」の感覚と異なり少し紛らわしい気がする。(「位置ベクトル」を意図しているのだろうか?)


【㉑】力を加える

Body.applyForce 対象のオブジェクト, 力の作用点, 力

ここで言う「力」とは、先ほど生成した終点のVectorオブジェクトのことである。 作用点と力のVectorオブジェクトを引数として渡せば対象のオブジェクトに力を作用させることができる。 Matter.Vector.neg()メソッドを利用すると力の方向を180°反転することができる。


次は空間内のオブジェクトに継続的に作用する風のような力を再現してみる。
しかし、applyForce()で加えることができる力は瞬間的なものであり、次の演算ステップになると0に戻ってしまう。そのため、継続的に作用させるためには演算ステップの度にapplyForce()を使う必要がある。

$('body').append(
  '<div style="width: 100px; margin-top: 10px;">' +
  '<select style="width: 100px; height: 30px;">' +
  '<option value="2">2</option>' +
  '<option value="1" selected>1</option>' +
  '<option value="0">0</option>' +
  '<option value="-1">-1</option>' +
  '<option value="-2">-2</option>' +
  '</select>' +
  '</div>'
);

// 静止オブジェクト(地面とハンプ)
const ground = Bodies.rectangle(400, 585, 10000, 30, { isStatic: true });
const hump = Bodies.fromVertices(400, 555, [{ x: 0, y: 0 }, { x: 150, y: 50 }, { x: -150, y: 50 }], { isStatic: true });
Composite.add(engine.world, [ground, hump]);

// 力の確認用に吹流しを生成する
const streamer = Composites.stack(400, 60, 4, 1, 0, 50, (x, y) => {
  return Bodies.rectangle(x, y, 60, 20, { isSensor: true });
});
Composites.chain(streamer, 0.5, 0, -0.5, 0, { stiffness: 1, length: 0 });
Composite.add(streamer, Constraint.create({
  pointA: { x: 400, y: 50 },
  bodyB: streamer.bodies[0],
  pointB: { x: -30, y: 0 },
  stiffness: 1
}));
Composite.add(engine.world, streamer);

// 地面に対して水平右向きを正とする一定な大きさの力を生成する【㉒】
const order = 1e-3;
Events.on(engine, 'beforeUpdate', () => {
  $.each(Composite.allBodies(engine.world), (i, body) => {
    const magnitude = Number($('option:selected').val());
    const workingPoint = Matter.Vector.create(body.position.x, body.position.y);
    const forceVector = Matter.Vector.create(magnitude * order, 0);
    Body.applyForce(body, workingPoint, forceVector);
  });
});

// クリックした位置に多角形を生成
Events.on(mouseConstraint, 'mousedown', e => {
  if (mouseConstraint.body) { return }
  const ball = Bodies.polygon(e.mouse.position.x, e.mouse.position.y, 8, 25, { restitution: 0.5 });
  Composite.add(engine.world, ball);
});


【㉒】継続的に力を作用させる
Engineオブジェクトには演算ステップの更新直前に発火するbeforeUpdateイベントが用意されている。
このイベントのコールバック関数の内部でコンポジットの全オブジェクトに対して力を加えるようにすれば、空間内で一様に継続的な力が作用しているのと近い状況を作ることができる。
プルダウンの整数値を参照しているので、力の大きさや方向(+が右向き、−が左向き)を変化させることができる。