メニュー

ETIENU NOTE

コーディングの実践メモ

matter.jsはweb制作に使えるのか試してサンプル作った-コード解説

JavaScript
#matter.js
/

前記事

matter.jsに手を出した経緯、動作するサンプル、zipファイルを置いてあります。


matter.jsはweb制作に使えるのか?サンプル作ってみた
2023.04.15

「最小限のサンプル」ではないので記事が長くなります!

この記事では淡々とコードの説明をしていきます。対象者としては、javascriptで物理演算コンテンツが作りたい。matter.jsで何か作りたいので参考になるサンプルが見たい!という方向けの記事となります。

-- test --

ページの準備

githubに上げているコードなので、そちらで見た方がいいかもしれません。

githubでコードを見る

HTML

index.html

特別な事はしていませんがCSS・JSの読み込み、見出しの表示、matterコンテンツ枠、コンテンツからジャンプする用に使うエリアとなっています。

matter枠は背景、matter用canvas、情報表示枠を作っています。

CSS

index.css

この二つのファイルで枠と背景は表示されます。

JavaScript

450行程ありますが、まずは全体像。

mymatter.js

Javascript部分の解説

基本的にはコードにコメントを書いてあります。

初期設定

必要な変数やクラスの用意、ファイルパスや画面サイズなど情報を揃えます。

使用する変数の設定

ワードプレスに組み込む場合は、javascriptを読み込む前に

<script>var wppath = "<?php echo get_template_directory_uri();?>";</script>

として、phpでパスを取得しておき、javascriptの変数に入れる事で受け渡します。

Matter各クラスの用意

matter.jsを出力するHTMLのIDを指定

実行クラスと描画クラスの作成 - EngineとRender

Renderで描画処理、Engineが物理計算処理やフレームを進めます。

開始処理 - Runnerの作成と実行

Render.run()で描画処理、Runner.run()(またはEngine.run())で物理計算やイベントの処理を進めます。

公式デモのサンプルコードを見るとRender.run()Runner.run()と二つ実行しており
Renderを消すと表示されなくなり、Runnerを消すと1フレーム目だけ描かれて動きません。

Runner.run()にはEngine.run()が含まれます。前後にイベント処理を加え、簡単に扱えるようにしたものがRunnerです。よって、基本的にはEngine.run()を直接使わず、Runner.run()を使用しているということです。

https://github.com/liabru/matter-js/blob/master/src/core/Runner.js

画面の初期サイズを保存

本当は、matter空間サイズの固定値と、描画画面のサイズの二つに分けるべきですがここでは横着して一つになってます。その為、固定値の設定(640x480)→render作成(matter空間640x480固定)→その後にページの実際の枠に描画サイズを合わせています。

マウス制御の作成

判定フィルタについてはこちらの公式デモ参考

https://brm.io/matter-js/demo/#collisionFiltering

https://github.com/liabru/matter-js/blob/master/examples/collisionFiltering.js

背景表示、壁の配置

Renderの背景は透明にして、CSSのbackground-imageで画像を設定しています。

空間上の舞台として床と左右に見えない壁を設定しています。

静的キャラクタの配置

〇や□ではなく、人物のような複雑な形状を扱いたい。

画像の配置

判定は別に作る為、画像用の極小点を用意します。( 判定は省略できない為)

fromPathの数値は、x,y,x,y...と繰り返されています。

口パク差分が欲しいので、二つの物体を作りました。

複雑な当たり判定をどう作るか?

前述のように単純な三角形、四角形を設定する事はできます。しかし人物はもっと曲線的で複雑です。

判定はどのように行われているのか

1.[{x,y},{x,y}..]の点のデータがあります。

2.点のデータが配列になっています。これが線分です。今回はVerticesクラスのfromPathという関数を使用しています。

3.線分をボディに設定します。

4.この線分で囲まれた(終点と始点が繋がっている)内側をボディの判定とします。

5.ボディの判定と、他の線分や円などが重なっているかを計算して、衝突判定が行われます。

これはいわゆる2Dゲームの多角形と線分の衝突計算です。

時計周りに頂点を配置しないと裏表の判定が逆になって貫通してしまいます。

工夫
画像切り抜きのように、人物にそって単純に点を配置するという方法だと失敗します。連続した点の位置関係が、時計周りと半時計周りの部分に分かれてしまう為です。

そこで三つに分割する事にしました。

ペイントソフトで画像に点を打ち、その座標を手入力するという地道な作業。
三つの矩形が時計回りに組まれるように調整。地道すぎてマジ辛い。
ちなみに反時計周りの点があった場合、該当点は存在しない扱いになりパスされます。

そして画像1つに対して判定を3つ付けたかったのですが、Verticesを複数設定する方法はわかりませんでした。よって画像の方に設定せず、同じ位置にある別の透明オブジェクトとしました。

公式Matter.Bodies.fromVerticesの引数の指定方法を見ると

Matter.Bodies.fromVertices(x, y, vertexSets, [options], [flagInternal=false], [removeCollinear=0.01], [minimumArea=10], [removeDuplicatePoints=0.01]) → Body
vertexSets Array
One or more arrays of vertex points e.g. [[{ x: 0, y: 0 }...], ...]

とあるので、多重配列を突っ込めば成立しそうですが、うまくいきませんでした。

ロゴの配置

キャラクタと同じように四角のパスを作りコンポジットに入れています。

動的な物体の追加

空から降ってくるゲームオブジェクトを作成して追加します。

アイテムごとの設定が被って冗長ですが、個別に設定もできるという事で。
物体の追加については以前World.add()だったようですが、現在はComposite.add()となっています。
公式ドキュメントでそう書かれています。

Matter.World

このモジュールはMatter.Compositeに置き換えられました。

すべての使用法は、Matter.Compositeにある同等の関数に移行する必要があります。たとえば、World.add(world, body)今はComposite.add(world, body)になります。
プロパティworld.gravityengine.gravityに移動されました。

Matter.Compositeの下位互換性の目的で、このモジュールは移行中の短期間の直接エイリアスとして残ります。最終的に、このエイリアスモジュールは非推奨としてマークされ、将来のリリースで削除されます。

Matter.js Physics Engine API Docs - matter-js 0.18.0

matterのイベント

Events自体はon()off()trigger()しか存在しません。
onでイベント名と対応関数のセット
offで破棄
triggerでイベント名指定するとセットされた関数実行
というもの。

今回EngineではなくRunnerを使っています。

RunnerというのはEngineを簡単に使えるようにループ処理を組まれているもので
Runnerで実行され続けるtick()関数( 1フレームの計算と描画を進める処理 )の中に
Event.trigger()で既に指定されているイベント名があります。


Engine.update()の前後

 // update
 Events.trigger(runner, 'beforeUpdate', event);
 Engine.update(engine, delta, correction);
 Events.trigger(runner, 'afterUpdate', event);

Engine.render.controller.world()の前後

Events.trigger(runner, 'beforeRender', event);
Events.trigger(engine, 'beforeRender', event); // back compatibility
engine.render.controller.world(engine.render);
Events.trigger(runner, 'afterRender', event);
Events.trigger(engine, 'afterRender', event); // back compatibility

これらのRunner用意済みのイベント名を指定する事で、Runnerループ内でイベントが発生するようになっています。

自前でループ処理を作る場合はこの限りではなく、triggerでイベント名を好きに指定して呼び出せると思われます。

beforeUpdate

Engine.update()前、ワールド内物理演算の前。物が動く前にしたい処理。変数の初期化等
マウスカーソルと物体の判定に、カーソルY-1から現在位置の縦線を作っています。

afterUpdate

Engine.update()後、ワールド内物理演算の後。物が動いた後にしたい処理。ゲーム用ループ処理等

afterRender

engine.render.controller.world()の後。ワールド描画の後。
世界を描画した後、上書き描画したい場合はここで描画します。
beforeRenderの場合その次に世界の描画がある為、描画しても消されます。

ウインドウのイベント

イベント登録
マウスクリック、スマホのタッチ、カーソルインアウト、ウインドウリサイズのイベントを設定。

イベント : 画面のリサイズ
htmlのcanvasのサイズと、matterの描画領域の倍率を合わせます。matterはcanvasのstyleを直接設定しているので、サイズを変更する為にはこちらもstyleを上書きしていきます。

イベント : クリックの処理
EventsのafterRender内で、接触判定がある場合にホバー状態(選択中カテゴリ)を設定しているので、変数を確認して機能を実行します。

イベント : キャンバス領域イン・アウト判定

カーソルアイコンの変更

セリフ

処理の流れとしては

1.降ってくるアイテムをマウス・タッチで掴みドラッグ

2.ドラッグ中のアイテムの判別と保存

3.離す。クリックイベントが発火するので処理

4.セリフ開始処理

5.セリフ処理ループ

という流れです。

1.マウスと物体の判定
EventbeforeUpdate中で、線分を飛ばし判定を行い、カーソル位置に物体があるか判定配列を取得しています

2.アイテムの判別と保存
EventafterRender中で判定配列の長さだけ処理を繰り返しています。その中で判定データのカテゴリフラグを判別して、「今カーソルの下に何があるか」を判断しています。

3.クリックイベント時の処理
2でカテゴリIDを保存しているmev_hover_idに数値が入っている場合、IDに合わせた吹き出し処理を開始します。

4.セリフ開始処理
画像を透明化して入れ替えているのと、HTMLのメッセージ枠を表示して文章を設定しています。

5.セリフループ処理
一定時間後に非表示にしたかったので、ループ処理を行っています。ループはEventafterUpdate内で行われています。

IDジャンプ・別ページ移動

流れとしては前述のセリフイベント発生と同じです。
1.バナーにカーソルが乗っている時にIDを保存
2.クリックイベント時にIDに応じた処理

1.IDの保存

2.クリックイベント時にIDに応じた処理

IDジャンプではなくページ移動したい場合は、window.location.hrefで行います。

スムーススクロール処理

以上。

お疲れ様です。仕組みをざっと説明させて頂きました。

補足

解説に挟むと長くなるけど知っておきたい情報

フィルターについて

物体は16進数のカテゴリフィルタを設定する事で識別されています。

collisionFilterの設定でcategoryを指定します。
maskを指定すると、それ以外のカテゴリは素通りになります。

衝突判定の取り方と、フィルターの使い方

EventのbeforeUpdate内にて線判定を飛ばし、collsion配列を取得。線に1つでも接触していれば配列が取得できます。

EventのafterRender内にて、取得したcollision配列の数だけループ処理します。この配列要素をcollisionとすると

collision.bodyA.collisionFilter.category

このプロパティが先に設定したカテゴリフィルタです。要素は既に接触しているので、後はIDによる分岐処理を行うだけです。

参考

https://brm.io/matter-js/docs/classes/Collision.html#property_bodyA

https://brm.io/matter-js/docs/classes/Body.html#property_collisionFilter

フィルター - フラグのマスク

フラグの判別はビット演算です。フラグを立てているビットを&でマスクします。

if((cat & 0x00f0) == 0x0010){}

0x00f0 = 0000 0000 1111 0000

0x0010 = 0000 0000 0001 0000

&は論理積、掛け算です。0&0=0、0&1=0、1&0=0、1&1=1

つまり上記の(cat & 0x00f0)は、変数catの5~8の4ビットだけが残るので

それに対応した数値と比較して一致しているかを調べる事ができるという事です

試行錯誤 - 失敗や改善点

解説に書くと余計だけど、matter.jsをいじるにあたってつまづいた点、知りたかった事など

キャラクタの画像変化について

最終的に、透明度を0にして隠す事で非表示処理としました。

他にも

Composite.translate(c_yoyoimg1, { x: 1000, y: 0 });
Composite.translate(c_yoyoimg2, { x:-1000, y: 0 });

translateで画面外に配置してあるものとの入れ替える方法

c_yoyoimg1.bodies[0].render.visible =false;

といったvisible非表示の方法も試しました。

しかし、どうしても最初の一回だけ読み込み待ちのような、1フレーム画像消失が発生してしまいます。
これでは魅力的なサンプルとは言えません。

模索した結果、先に画像を読み込みつつ透明度を0にして隠す方法で落ち着いています。

透明度0の時、処理や計算がされているかはわかりません。実質画像二つとも表示している可能性があります。
負荷は大きくなりますが、見た目の質を重視しました。

タッチエラー?

枠ギリギリでドラッグするとtouchstart、touchendのキャンセルに失敗したというエラーが発生していますが、失敗報告だけで処理に影響はなかったので黙認しています。

処理速度の話

長文記事に配置すると激重になるという事を書いたのですが、どうも要因はカーソルの変更方法にあったようです。

 document.body.style.cursor = i_name;

と、直接bodyのカーソルを変化させていました。chromeF12で見ていたところ、bodyのstyle="cursor"がautoとpointerに切り替わる瞬間のみ激重になっている事に気が付きました。

container.style.cursor = i_name;

と、matterを扱っている枠のみに修正したところ無事速度は普通に戻ったので「長文記事に使えない」という事態は回避できています。

画像と実体の拡大縮小

collision.bodyA.render.sprite.xScale = 1.1;

で画像の拡大

// Composite.scale([composites], [X拡大率], [Y拡大率], [中心]);
Composite.scale( c_logo, 1.1, 1.1, { x: px, y: py });

で実体(衝突判定)の拡大

しかしComposite.scale()は使用の度に拡大し続けてしまいます。

辿っていくとBody.scale()→Vertices.scale()となって本体の頂点を直接計算してしまっているので戻しようがないです。

対策としては
1.Verticesまで辿って、全ての頂点を初期に設定した数値で上書きする
2.掛けた値で割る
 変化前 × 倍率A = 変化後
 例)1234 * 1.23 = 1517.82
 変化後 ÷ 倍率A = 変化前
 例)1517.82 / 1.23 = 1234
という方法が考えられます。

物体が移動するための変数はどこ?

後述するconsole.log()を利用し、座標に関する変数を調べていました。
c_yoyoimg2.bodies[0].position.x = c_yoyoimg1.bodies[0].position.x;	//	元画像と位置を合わす
c_yoyoimg2.bodies[0].positionPrev.x = c_yoyoimg1.bodies[0].position.x;
c_yoyoimg2.bodies[0].vertices[0].x = c_yoyoimg1.bodies[0].position.x;
c_yoyoimg2.bodies[0].vertices[1].x = c_yoyoimg1.bodies[0].position.x+1;
c_yoyoimg2.bodies[0].vertices[2].x = c_yoyoimg1.bodies[0].position.x;
c_yoyoimg2.bodies[0].bounds.max.x = c_yoyoimg1.bodies[0].position.x+1;

このように試して行ったところ、変数は変化しているが実際に座標が移動されることはありませんでした。

DocsのBodyを見ると

Body.setPosition()

という関数があります。


Body.setPosition((変更する物体)c_yoyoimg2.bodies[0], (セットするx,yの配列)c_yoyoimg1.bodies[0].position );

これを使用すると一発で移動できました。何故なのか?

github上のbody.jsを覗いてみると

Body.setPosition = function(body, position) {
  var delta = Vector.sub(position, body.position);
  body.positionPrev.x += delta.x;
  body.positionPrev.y += delta.y;
  for (var i = 0; i < body.parts.length; i++) {
    var part = body.parts[i];
    part.position.x += delta.x;
    part.position.y += delta.y;
    Vertices.translate(part.vertices, delta);
    Bounds.update(part.bounds, part.vertices, body.velocity);
  }
};

partsという変数を操作しているようです。

bodies[0].parts配列が座標操作の変数だったという事でしょうか。

画面サイズとマウスのずれ

最初から最後の最後まで修正し続けていた要素の一つ、画面のずれとマウスのずれというものがあります。

画面のずれ

matter.jsはRender.create()時にelement要素(div)とcanvas要素を指定しますが、指定したcanvas要素に対して、styleでwidthとheightを強制しています。

PCで作業し続けていた為、長い事これに気が付かず、element要素の拡縮に合わせてrender.optionsのwidthとheightを調整していました。

結果、レスポンシブ時に枠と画面の描画は合っているのに、canvasは透明なままページからはみ出している、といった事になっていました。

正解は「render.optionsは操作せず、canvas要素のstyleからwidthとheightを書き換える」です。

マウスのずれ

マウスのずれは本来ないはずなのです。しかし、前述のrender.optionの操作で画面サイズが一見直っているが、canvasサイズは変わっていないという事に気付かなかった為、マウスはあくまでcanvas基準で処理をしている事から映像とのずれが発生していたのでした。

また、style修正して一見直った後も、最初のRender.create()の幅と高さをレスポンシブに合わせて調整してしまった為、スマホ時に崩れました。

Render.create()のoptions指定はあくまでmatter空間のサイズであり、レスポンシブに合わせて調整するものではなかったと気付いたのは本当に最後の方でした。

その他

キャラクタのアニメーション

多関節や布を使っての衣類アニメーション等が確立できればmatter.jsは選択肢の一つになると思います。

https://brm.io/matter-js/demo/#chains

https://brm.io/matter-js/demo/#cloth

console.log 物体の構造、操作可能変数を知りたい場合

console.log( オブジェクト );
とすると、オブジェクト中の構造が出力されてイメージしやすいです。
例)
console.log( collision.bodyA.render );
//出力
{
  "visible": true,
  "opacity": 1,
  "strokeStyle": "#ccc",
  "fillStyle": "#063e7b",
  "lineWidth": 0,
  "sprite": {
    "xScale": 1,
    "yScale": 1,
    "xOffset": 0.49999999999999983,
    "yOffset": 0.5,
    "texture": "./img/item03.png",
    "visible": false
  }
}

console.log( "文章" + オブジェクト );

とすると表示されません。変数単体を入れる必要があります。

matter.jsを深く知りたい場合

公式Docs→各クラスのUsages(使用法)からgithubに飛んでどう組まれているのか直接見た方が謎が解けやすいです。

https://brm.io/matter-js/docs/

あとはイメージに近いDemoを分析し、今回のサンプルのように画像を乗せ、イベントを記述していくのが近道と思います。

https://brm.io/matter-js/demo/#car

まとめ

今回matter.jsのサンプルを作り、解説してみました。実際には半年以上前に作った物ですが、出すに当ってまた不具合だらけで、細かい修正を繰り返しました。

答えがでれば案外当たり前のような事に見えますが、海外記事でもよくわからないし沼っている時は本当に沼です。もう少しちゃんとした記事があってもいいやん・・。

当時私が「こういう解説記事があれば!」というものを書きました。いかがだったでしょうか?沼から抜け出し、作りたい物に近づければ幸いです。

matter.jsは工夫すれば作品的、面白味のあるサイトも作れそう。
明確に作りたいイメージがあり、今回のサンプルがイメージに近いのであれば
選択肢に入れてみるのもありかもしれません。