Pixi.jsで円同士の衝突判定を実装する

# はじめに

Pixi.jsを使って円同士の衝突判定を実装する機会があったので、初めてPixi.jsを使用する方に出来るだけ分かりやすくまとめていきます。

# ① PIXI.Application の作成

まずは、PIXI.Applicationを作成します。

PIXI.Applicationとは土台のようなもので、ここに色々なオブジェクトを追加することで、画像を表示させたり、図形を描画したりできます。
PIXI.Applicationには色々なオプションを設定することができますが、今回はサイズ、背景色を設定します。
Canvasの縦幅、横幅は画面いっぱいにして、背景色は白色にします。
Pixi.jsでの色の指定は全て16進数で行うので、白色を設定する場合には0xffffffと記述します。


```jsx
import * as PIXI from 'pixi.js'

const app = new PIXI.Application({
  width: window.innerWidth,
  height: window.innerHeight,
  backgroundColor: 0xffffff
});

// bodyにApplication追加
document.body.appendChild(app.view);
```

# ② 衝突させる円を作成する

次に、衝突させる円を作成していきます。

円を作成するにはPIXI.Graphicsの [drawCircle](https://pixijs.download/dev/docs/PIXI.Graphics.html#drawCircle) を使用し、色指定にはbeginFillを使用して ff00ff に塗りつぶします。

```jsx

// 円の半径
const radius = 50

const circle1 = new PIXI.Graphics()
     .beginFill(0xff00ff)
     .drawCircle(0, 0, radius)
     .endFill()
```

次に、作成した円をPIXI.Spriteに追加します。

PIXI.Spriteは基本的に画像を表示させるために使用しますが、今回はGraphicを追加します。
PIXI.Graphicsで座標の情報を保持してしまうと、基準点が左上になってしまうので Drag & Dropの際にズレが生じてしまいます。
そのため、描画する座標の情報はPIXI.Spriteで保持し、基準点は中心に設定します。

```jsx
circleSprite1.anchor.set(0.5)
circleSprite1.position.set(200, 200)
```

また、物体の衝突判定の際に使用するため、作成したSpriteの情報を配列に格納します。
最後にapp.stageに作成したSpriteを追加して表示させます。

以下、ここまでのコードの全容です。

```jsx

// 円の情報をもつ配列
let circles = []

// 円の半径
const radius = 50

// 衝突させる円1
const circleSprite1 = new PIXI.Sprite()
const circle1 = new PIXI.Graphics()
     .beginFill(0xff00ff)
     .drawCircle(0, 0, radius)
     .endFill()
circleSprite1.addChild(circle1)
circleSprite1.anchor.set(0.5)
circleSprite1.position.set(200, 200)
circles.push(circleSprite1)

// 衝突させる円2
const circleSprite2 = new PIXI.Sprite()
const circle2 = new PIXI.Graphics()
     .beginFill(0xff00ff)
     .drawCircle(0, 0, radius)
     .endFill()
circleSprite2.addChild(circle2)
circleSprite2.anchor.set(0.5)
circleSprite2.position.set(400,400)
circles.push(circleSprite2)

app.stage.addChild(circleSprite1)
app.stage.addChild(circleSprite2)
```

![スクリーンショット 2021-11-26 16.08.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704193/49de8e00-895f-0342-70b0-ec8da6bc789e.png)


# ③ drag and dropで操作する円を作成する

次にdrag and dropで操作する円を作成します。
①と同じ要領でPIXI.Graphicsで円を描画し、Spriteに追加します。

```jsx
// dragさせる円の半径
const dragCircleRadius = 50

// dragさせる円
const dragCircle = new PIXI.Graphics()
     .beginFill(0x0000ff)
     .drawCircle(0, 0, dragCircleRadius)
     .endFill()

const dragCircleSprite = new PIXI.Sprite()
dragCircleSprite.addChild(dragCircle)
```

Spriteのpointer up や pointer moveイベントを有効化するために、interactive を true に設定します。
わかりやすいように buttonMode もtrueにして、カーソルがpointerになるようにします。

```jsx
dragCircleSprite.interactive = true
dragCircleSprite.buttonMode = true
```

次に作成したSpriteにイベントを登録させます。
第一引数にイベントの種類、第二引数に実行する関数を指定します。

```jsx
dragCircleSprite.on('pointerdown', onPointerDown)
dragCircleSprite.on('pointerup', onPointerUp)
```

今ドラッグしているか否かの状態は isDragging で保持します。

```jsx

// ドラッグ状態を管理
let isDragging = false

// pointer down時のevent
function onPointerDown(){
  isDragging = true
  dragCircleSprite.on('pointermove', onPointerMove)
}

// pointer up時のevent
function onPointerUp() {
  isDragging = false
  dragCircleSprite.removeListener('pointermove')
}
```

pointer moveイベントではドラッグ状態がtrueの場合のみ、Spriteの座標を現在のマウスの位置に合わせます。
引数からInteraction Eventを参照できるので、座標の情報はそこから取得します。

```jsx
// pointer move時のevent
function onPointerMove(e) {
  if(isDragging) {
    const target = e.currentTarget
    const position = e.data.getLocalPosition(app.stage)
    target.position.set(position.x, position.y)
  }
}
```

以下ここまでのコードの全容です。
公式Exampleの [drag and drop](https://pixijs.io/examples/#/events/drag-n-drop.js) も参考にしてみてください。

```jsx

// ドラッグ状態を管理
let isDragging = false

// dragさせる円の半径
const dragCircleRadius = 50

// dragさせる円
const dragCircle = new PIXI.Graphics()
     .beginFill(0x0000ff)
     .drawCircle(0, 0, dragCircleRadius)
     .endFill()

const dragCircleSprite = new PIXI.Sprite()
dragCircleSprite.addChild(dragCircle)

// interactiveをtrueにしないとイベントが発火しない
dragCircleSprite.interactive = true
dragCircleSprite.buttonMode = true
// 初期のポジション
dragCircleSprite.position.set(200,400)
// アンカーポイントを真ん中に設定
dragCircleSprite.anchor.set(0.5)

// pointerイベントの登録
dragCircleSprite.on('pointerdown', onPointerDown)
dragCircleSprite.on('pointerup', onPointerUp)

// pointer down時のevent
function onPointerDown(){
  isDragging = true
  dragCircleSprite.on('mousemove', onPointerMove)
}

// pointer up時のevent
function onPointerUp() {
  isDragging = false
  dragCircleSprite.removeListener('mousemove')
}

// pointer move時のevent
function onPointerMove(e) {
  if(isDragging) {
    const target = e.currentTarget
    const position = e.data.getLocalPosition(app.stage)
    target.position.set(position.x, position.y)
  }
}

app.stage.addChild(dragCircleSprite)
```

![画面収録-2021-11-26-16.00.42.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704193/bf4c6444-b221-3412-a0a3-23da75d4f7e3.gif)


# ③ 円同士の衝突を判定するロジック

では、本題の円同士の衝突判定です。
まず、当たり判定となる基準ですが、**ドラッグしている円が既に描画されている円に衝突した時（重なった時）に当たり判定**とします。
これを実現するために、2つの円それぞれの中心からの距離と半径の和を比べて、**半径の和 > 2つの円の距離**となれば当たり、と判定します。

![スクリーンショット 2021-11-27 11.45.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704193/98c7dafa-6550-e9b5-7717-dd8a564fce11.png)


2つの円の距離は三平方の定理を使って求めます。

![スクリーンショット 2021-11-27 12.10.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704193/72d79df8-60a0-c3f5-86e8-a65959f54a14.png)


直角三角形では、斜辺cの2乗は，他の辺a，bをそれぞれ2乗した数の和に等しい、というやつです。
この定理をどのように使用するかと言いますと、、、

![スクリーンショット 2021-11-27 11.57.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704193/ce34d1e0-270d-f0f0-b776-59d486dfe215.png)


図のように、2つの円の中心を結んだものを斜辺cとし直角三角形を作ると、それぞれx座標同士、y座標同士の距離が2辺 a, bとなり、
**直角三角形では，2つの辺の長さがわかると，三平方の定理を使って他の1辺の長さが計算できる**ので、中心座標の数字がわかればcの長さを計算することが可能になります。

**`Math.hypot()`** 関数は、各引数の二乗の合計値の平方根を返しますので、これを使用すると簡単に上記の式が実現できそうです。
コードに落とし込むと、、、

```jsx

const a = x1 - x2
const b = y1 - y2
const c = Math.hypot(a, b)
if(c < radius + dragCircleRadius) {
	// 当たり
  return
} else {
  // 当たってない
}
```

# ④ pointer moveの時に衝突判定

上記のロジックをpointer move時のイベント関数内に実装します。
衝突判定させる円が複数個あるため、円をまとめた配列をループして判定するようにします。
衝突した時にSpriteの透明度を下げるようにします。

```jsx
function onPointerMove(e) {
  if(isDragging) {
    const target = e.currentTarget
    const position = e.data.getLocalPosition(app.stage)
    target.position.set(position.x, position.y)

    for(let i = 0; i < circles.length; i++) {
      const centerPosition = {
        x: circles[i].x,
        y: circles[i].y
      }

      const a = centerPosition.x - position.x
      const b = centerPosition.y - position.y
      const c = Math.hypot(a, b)
      if(c < radius + dragCircleRadius) {
        dragCircleSprite.alpha = 0.3
        return
      } else {
        dragCircleSprite.alpha = 1
      }
    }
  }
}
```

 ![画面収録-2021-11-27-15.18.40.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704193/c4eb8bb6-8948-9a79-1e7a-a162f8a328da.gif)


いい感じになりました。

コードの全容はこちらの[codepen](https://codepen.io/bonji810/pen/XWawapL?editors=0010) に載せましたのでよければご参考までに！