【PixiJS 覚書】第六回 衝突判定

前回のラストで「次回は落穂拾い」と書いたな？
**あれはウソだ。**

・・・すみません、拾い上げる内容を検討した結果、衝突判定とAnimatedSpriteはサラッと流すには重い内容だと判断し、それぞれ単独の記事とすることにしました。

そんなわけで、今回は衝突判定についてです。

##PixiJSには描画オブジェクト同士の衝突判定機能はない
いきなり身も蓋もないですが、PixiJSの標準機能ではスプライトなどの描画オブジェクト同士での判定機能は用意されていません。
ただし、「点」と「オブジェクト」の衝突判定であれば用意されているので、それを使ってなんとかしていくことになります。

ここでは基礎的な衝突判定として以下３つのパターンを紹介します。

* 円と円の衝突
* 点とオブジェクトの衝突
* 点とオブジェクトの衝突（hitArea使用）

##円と円の衝突
これはPixiJSとは特に関係なく、単純に円の中心の座標から互いの距離を計算して半径の和より小さければ衝突しているとする、という簡単な算数です。
なので、ここでは下記図を貼るのみとし、サンプルコードは記載しません。
![hit1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107226/a5d7521d-6573-6a50-9c0b-60d8e402019f.png)
この方法の良いところは、見ての通りシンプルでわかりやすいという点です。
円に限らずとも、縦横比がほぼ等しくて厳密な衝突判定を必要としない場合にはこれで乗り切れます。
また、(r1-r2)^2の部分を好みに応じて変更することで、実際の当たり判定の大きさを見た目と関係なく調整することが可能です。
一方で縦横比に偏りがあったり、複雑な形状の判定が必要な場合には使えません。

##点とオブジェクトの衝突
ここからPixiJSの機能です。
冒頭でも述べたように、PixiJSにはオブジェクト同士、つまりスプライトや図形同士の衝突判定が用意されていません。
代わりに「点（PIXI.Point）」と描画オブジェクトの衝突判定が用意されています。
まずはその機能の確認から。点と三角形を衝突させてみましょう。

####今回のサンプルコード
```JavaScript:
const app = new PIXI.Application({
    width: 600,
    height: 400,
    backgroundColor: 0x1199aa
});
document.body.appendChild(app.view);

// 三角形の頂点
const path = [50, 0 , 0, 87, 100, 87];

const triangle = new PIXI.Graphics();
triangle.lineStyle(3, 0x000000)
  .beginFill(0xffff00)
  .drawPolygon(path)
  .endFill()
  .lineStyle();

triangle.interactive = true;
triangle.pivot.set(50, 44);
triangle.position.set(500, 200);

app.stage.addChild(triangle);

// 点
const point = new PIXI.Point(100, 200);

// 視認用の点
const pointForVisibility = new PIXI.Graphics();
pointForVisibility.beginFill(0x000000)
  .drawCircle(0, 0, 3)
  .endFill();

pointForVisibility.pivot.set(3, 3);

app.stage.addChild(pointForVisibility);

// 当たりを通知する文字
const text = new PIXI.Text('点と三角形が衝突した！', {fontFamily : 'Arial', fontSize: 28, fill : 0x101010,});
text.alpha = 0;
text.position.set(280, 300);

app.stage.addChild(text);

// ループ
app.ticker.add(movePoint);

function movePoint() {
    const hitObj = app.renderer.plugins.interaction.hitTest(point);
    if(hitObj) {
        text.alpha = 1;
        app.ticker.stop();
    } else {
        point.x += 4;
        pointForVisibility.position.set(point.x, point.y);
    }
}
```
Playgroundでの実行結果。
![point.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107226/f5c794ca-63e5-a802-c677-716a0547715c.png)

####PIXI.InteractionクラスとhitTestメソッド
三角形（多角形）や点（円）の描画は第二回、文字の描画は第三回の内容です。
もしこの辺に疑問が残っている場合はそれぞれの回を振り返ってみてください。
**また、図形の衝突判定もインタラクティブ要素なので、判定対象の描画オブジェクトでは`interactive`プロパティをtrueにしておく必要があります。**
（`interactive`プロパティについては第五回）

そして次が衝突判定を行う点、[PIXI.Point](https://pixijs.download/dev/docs/PIXI.Point.html)です。

```JavaScript:
// 点
const point = new PIXI.Point(100, 200);
```
二次元上のx、y座標を表すという点では`PIXI.DisplayObject#position`の中身であるPIXI.ObservablePointクラスと良く似ているのですが、異なるクラスなので注意しましょう。
（ちなみにPIXI.Point、PIXI.ObservablePointのいずれもPIXI.IPointインターフェースの実装です）

この点を`view`の左から右に向けて移動させ、三角形に衝突するタイミングを判定させる、というのがサンプルコードの趣旨です。
ただ、PIXI.Pointはあくまで座標情報にすぎませんので我々の目には見えません。
そこを可視化するために今回は別途`drawCircle`で点（小さな円）を描いてPIXI.Pointの座標に被せています。
この`drawCircle`はあくまで目視で確認するための措置で合って、今回の衝突判定とは関係がないことをご承知おきください。

その後、衝突した際に表示させる文字を生成し、点を移動させる処理を`app.ticker`へ登録しています。（`ticker`は第四回の内容）

さて、ここからが今回の主題です。

```JavaScript:
    const hitObj = app.renderer.plugins.interaction.hitTest(point);
```

「app」から「interaction」の途中に「renderer」「plugins」がありますが、ここでは飛ばして「interaction」から解釈していきます。
`interaction`プロパティの中身は「PIXI.Interaction」クラスのインスタンスです。

リファレンス：[PIXI.InteractionManager](https://pixijs.download/dev/docs/PIXI.InteractionManager.html)

この説明によれば、PIXI.Interactionクラスはマウスやタッチ等の各種インタラクションを管理するクラスであり、そのインスタンスは自動的に生成されて`renderer.plugins.interaction`にセットされるそうです。

**要点１：インタラクションはPIXI.Interactionクラスのインスタンスで管理されており、そのインスタンスは`app.renderer.plugins.interaction`に自動的に用意される。**

今回の主題である衝突判定もインタラクションとしてここで管理されているわけですね。

で、この`interaction`プロパティに対して`hitTest`メソッドを実行しています。

・hitTest(point, root)

hitTestメソッドは、与えられた点（PIXI.Point）に対して衝突している描画オブジェクトを探索し、最初に見つけた描画オブジェクトを返します。何もなければ`null`を返します。
衝突しているオブジェクトが複数存在する場合は、デフォルト（rootを省略した場合）では一番手前に描画されている物を返します。

rootは描画ツリー内で探索開始するコンテナの指定です。任意パラメータなので省略可能です。特別な事情がない限り、省略することがほとんどだと思います。

**要点２：`PIXI.InteractionManager#hitTest`メソッドは与えた点（PIXI.Point）と衝突する描画オブジェクトを一つ返す。何もなければnullを返す。
（デフォルトだと一番手前に表示されている物）**

この`hitTest`メソッドにより点と衝突しているオブジェクトを探し、後は衝突したオブジェクトが存在していれば通知を表示しループを停止（`app.ticker.stop()`）、衝突していなければ点を移動させています。

##点とオブジェクトの衝突（hitArea使用）
####サンプルコード
上のサンプルコードで三角形を描画していた箇所（８行目以降）を、以下のようにスプライトに書き換えます。
また、衝突時のメッセージは好みで変更してください。

```JavaScript:
// バニーさんスプライト
const bunny = new PIXI.Sprite.from('https://pixijs.io/examples/examples/assets/bunny.png')

bunny.texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;
bunny.anchor.set(0.5);
bunny.position.set(500, 170);
bunny.scale.set(3);
bunny.interactive = true;

bunny.hitArea = new PIXI.Ellipse(0, 0, 10, 14);

app.stage.addChild(bunny);
```

####hitAreaプロパティとPIXI.IHitAreaインターフェース
ここまで点と描画オブジェクトの衝突判定を見てきましたが、描画オブジェクトがスプライトやテキストだと少し厄介なことになります。
というのもスプライトやテキストの場合、デフォルトでは衝突判定が画像領域全体に対して適用されるからです。
![hit2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107226/4e650854-13d3-c78c-90e5-c5e229538e88.png)
これを回避する機能として、PIXI.DisplayObjectには[`hitArea`プロパティ](https://pixijs.download/dev/docs/PIXI.DisplayObject.html#hitArea)が用意されています。
これは衝突判定が行われる領域を図形として持たせるためのプロパティです。
設定方法はPIXI.IHitAreaインターフェースに対する実装となっている４つのクラスのいずれかを指定します。

リファレンス：[PIXI.IHitArea](https://pixijs.download/dev/docs/PIXI.IHitArea.html)

[その実装にあたる４つのクラス]

* PIXI.Circle（円）
* PIXI.Ellipse（楕円）
* PIXI.Polygon（多角形）
* PIXI.RoundedRectangle（角丸四角）

これらはPIXI.Graphicsのような描画機能ではなく、図形の**情報**を生成するためのクラスです。
たとえばPIXI.Circleクラスのインスタンスを生成すると、何も描画はされずに**円の情報**だけが作られます。
（ちなみにそうやって生成された図形情報をPIXI.Graphicsの`drawShape`メソッドに渡すと目に見える図形として描画してくれます。）

これらのクラスのインスタンスを生成し、`hitArea`プロパティに代入することで衝突判定の領域を個別に指定することができます。

**要点３：衝突判定の領域は`PIXI.DisplayObject#hitArea`プロパティで指定できる。
その領域はPIXI.IHitAreaインターフェースに対する実装である４つのクラスのインスタンスで行う。**

####結果確認
今回は`PIXI.Ellipse`クラスで楕円を作り、以下の図のような当たり判定にしてみました。
![hit3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107226/e15d66ef-bbbd-172b-a145-72e9904636fb.png)
ちゃんと余白でなく胴体部分で反応していますね。
![hit4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107226/2330cd2f-ffac-cbe4-6443-fd91a1a18e61.png)
なお、サンプルコード内でPIXI.Ellipseインスタンスの中心座標を(0, 0)としているのは、hitAreaも`pivot`や`anchor`の影響を受けるためです。

##今回のまとめ
**要点１：インタラクションはPIXI.Interactionクラスのインスタンスで管理されており、そのインスタンスは`app.renderer.plugins.interaction`に自動的に用意される。**

**要点２：`PIXI.InteractionManager#hitTest`メソッドは与えた点（PIXI.Point）と衝突する描画オブジェクトを一つ返す。何もなければnullを返す。
（デフォルトだと一番手前に表示されている物）**

**要点３：衝突判定の領域は`PIXI.DisplayObject#hitArea`プロパティで指定できる。
その領域はPIXI.IHitAreaインターフェースに対する実装である４つのクラスのインスタンスで行う。**

PixiJSに描画オブジェクト同士の衝突判定がない以上、これらをベースに判定していくことになりそうです。
点とオブジェクトの衝突ではなく、幅のある物同士での衝突判定を行うには、衝突判定用の複数の点の集合を作り、その点集合を一括で判定するような仕組みを構築する必要がありそう。
（もしそのようなモジュールが既にありましたらお教えください。）

-----
さて、今回は予定を変更し、PixiJSにおける衝突判定について追ってきました。

次回はアニメーションするスプライトについて調べます。