【Matter.js】Matter.jsを使った2Dブラウザゲーム実装で詰まったところまとめ

# はじめに

Matter.jsをdoc見ながら初めて実装したところ、色々と気づきがあったのでメモ
※筆者はVue.jsで書いているのでrefとかが出てきますが、環境に応じてよしなに変えてください。

# 画面リサイズへの対応
がっつり画面リサイズをしたい場合のtipsが世の中にあんまりないので自前で実装した。
これがベストプラクティスじゃないと思うが基礎的な実装として考えてほしい

```js
// scene.valueはcanvasを生成したいdivを指定したrefなので無視して良いです。実態は下記のような形
// const scene = ref<HTMLDivElement | null>(null)

// 基準とする画面サイズ
const originalSceneWidth = 500

// 前回のスケール（画面の倍率）を覚えておく
let resetScale = 1

function handleResize = () => {
    const newWidth = scene.value.clientWidth
    const newHeight = scene.value.clientHeight
    const newScale = newWidth / originalSceneWidth

    render.bounds.max.x = newWidth
    render.bounds.max.y = newHeight
    render.options.width = newWidth
    render.options.height = newHeight
    render.canvas.width = newWidth
    render.canvas.height = newHeight

    // 床のサイズを更新
    // 物体のスケールは一度基準サイズに戻してから新しい倍率で大きくする
    // 位置は中心点ベースの座標になっているので、幅や高さを2で割ると位置の調整がしやすい
    Matter.Body.scale(ground, resetScale, 1)
    Matter.Body.scale(ground, newScale, 1)
    Matter.Body.setPosition(ground, {
      x: newWidth / 2,
      y: newHeight + (ground.bounds.max.y - ground.bounds.min.y) / 2
    })

    // 壁のサイズを更新
    Matter.Body.scale(wall, 1, resetScale)
    Matter.Body.scale(leftWall, 1, newScale)
    Matter.Body.setPosition(leftWall, {
      x: newWidth / 2,
      y: newHeight / 2
    })

    // リセット用スケールの更新
    resetScale = originalSceneWidth / newWidth
}
```

# staticな要素をマウスで動かしたい

`isStatic=true`に設定した物体（主に壁、床など）は`MouseConstraint`では操作できないが、
プレイヤーに物理演算の影響を及ぼさずに移動だけさせたい場合に対応できないのがネック。

なので、下記のようにjsのMouseEventを利用してドラッグしている間だけ動かすこととした。
```js
const isTouching = ref(false)
const lastMouseXPosition = ref<number | null>(null)

const startDrag = (event: MouseEvent | TouchEvent) => {
    isTouching.value = true
    const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX
    lastMouseXPosition.value = clientX
  }

const endDrag = () => {
    isTouching.value = false
    lastMouseXPosition.value = null
}

const runMoveManager = (event: MouseEvent | TouchEvent) => {
    if (!isTouching.value || lastMouseXPosition.value == null) return
    const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX

    const deltaX = clientX - lastMouseXPosition.value

    const currentX = player.position.x + deltaX
    // プレイヤーは画面外には移動しない
    if (currentX < 0 || currentX > window.innerWidth) {
      return
    }

    // プレイヤーのY軸は固定したい場合
    Matter.Body.setPosition(player, {
      x: currentX,
      y: player.position.y
    })
    
    lastMouseXPosition.value = clientX
}

scene.value.addEventListener('mousedown', startDrag)
scene.value.addEventListener('mouseup', endDrag)
scene.value.addEventListener('mousemove', runMoveManager)

scene.value.addEventListener('touchstart', startDrag)
scene.value.addEventListener('touchend', endDrag)
scene.value.addEventListener('touchmove', runMoveManager)
```

# 壁抜け対策
上のようにstaticな物体を動かすことには成功したが、これだとプレイヤーを高速で動かしてと物とぶつかったときに壁抜けする問題が発生した。

同じ問題に直面した方が幸運にも対策を書いていた。
https://codersblock.com/blog/javascript-physics-with-matter-js/#collision-detection-issues

内容としては、Matter.jsは継続的な衝突判定機能を持たないため、細い物体が高速で衝突すると判定が抜けることがあるとのこと。
この方と同じように衝突する部分を厚くすることで対応した。

# 動的なテクスチャ変更
物体のテクスチャを衝突時に変更したかったが、これをMatter.jsで行うと初回のテクスチャ読み込み時にちらつきが発生してしまうため、テクスチャ画像をpreloadしキャッシュすることで問題を解消した。

```js
const preloadTextures = (callback: () => void) => {
    // ここに画像パスを追加
    const imagePaths: string[] = []

    const images: Record<string, HTMLImageElement> = {};
    let loadedImages = 0;
    const totalImages = imagePaths.length;

    imagePaths.forEach((path: string) => {
      let img = new Image();
      img.src = path;
      img.onload = () => {
        loadedImages++;
        if (loadedImages === totalImages) {
          // ログでもなんでも良いが一度呼び出さないとプリロードされない模様
          console.log(images)
          callback();
        }
      };
      images[path] = img;
    });
}

preloadTextures(() => {
    Matter.Events.on(engine, 'collisionStart', function(event) {
    //　ここで衝突判定とテクスチャ変更を行う
    }
})
```