[Unity][ParticleSystem]3D座標を起点にuGUI上にパーティクルを発生・収束させる

# サンプル動画
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">移動や連発にも対応してようやく納得いくものが出来た<br>色々妥協してる部分もあるけど個人的には満足 <a href="https://t.co/hWVOqxVhAw">pic.twitter.com/hWVOqxVhAw</a></p>&mdash; madoramu (@madoramu_f) <a href="https://twitter.com/madoramu_f/status/1104004423271895042?ref_src=twsrc%5Etfw">2019年3月8日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

3Dアクションゲームなどで良くある、
「敵を攻撃したらパーティクルが発生して、UI上のゲージに向かって飛んでゲージが溜まる。」
というものを実装してみました。
(割と需要あると思っているのですが、どこにもサンプルが無かったので自作することに)

# サンプルプロジェクト
以下のGithubに上げてあります
https://github.com/madoramu/ParticleHorming
※サンプル動画は別プロジェクトの物なので、上記URLのプロジェクトは少し内容が異なります。
#### 環境
Unity2018.3
WindowsとAndroidで動作確認済み
<b><font color="Red">重要：ScreenSpaceCamera設定のCanvasのみ対応しています</font></b>

# 実装方法
サンプルプロジェクトの中身を抜粋して説明していきます。
細かい部分などはプロジェクトを閲覧してください。

## メインカメラとUIカメラの用意・設定
### メインカメラ
こちらはキャラクターの追尾、およびUI以外の描画を担当。

* CullingMaskで「UI」を除外する。
* Depthを「0」にする。
 * UIカメラより低ければおｋ

### UIカメラ
UIの描画のみ担当。

* メインカメラには絶対映らない場所に移動させる
 * AudioListenerは外しておく
* ClearFlagsを「Depth Only」にする。
* CullingMaskで「UI」だけにする。
* Depthを「10」にする。
 * メインカメラより高ければおｋ

上記二つのカメラ設定により、メインカメラの描画の後にUIレイヤーの情報が上に描画されます。

## キャンバスの用意と設定(<b><font color="Red">重要</font></b>)
UI表示用・座標変換用で<b><font color="Red">2つのScreenSpaceCamera設定Canvasを用意</font></b>します。

* UI表示用にはUIカメラ、座標変換用にはメインカメラをそれぞれ設定する
* RectTransform以外のパラメーターは全て同じにする事
 * 一応念のため

※何故設定カメラ以外同じパラメーターのCanvasを二つ使用したのかは感想に記載しています。

## ParticleSystemの設定
ここは好みに合わせて弄って問題ありませんが、以下の2点は必須です。

* ParticleSystemオブジェクトのLayerは「<b><font color="Red">必ず「UI」に設定する事</font></b>」
* <b><font color="Red">UI表示用のCanvas直下に配置する事</font></b>

今回は以下のように設定しました(プロジェクトから抜粋)
![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/332769/e502cbc8-e6da-89ee-fc75-cd16d0193d6a.png)

「何故World設定か？」

* 後述のパーティクル発生処理で解説しますが、ParticleSystemそのものを移動させるので、Localだとその移動にパーティクルも追従してしまうため。

「Rederer→Order in Layerについて」

* 上記画像には映っていませんが、今回はUIより前面に出したかったため値を増やしました。ここはお好みでおｋ。

## パーティクル発生処理
ParticleSystem.Emitを使用して、意図的にパーティクルを発生させることが出来ます。
https://docs.unity3d.com/ja/current/ScriptReference/ParticleSystem.Emit.html

#### 処理の流れ

* 3D座標からScreenSpaceCanvas上の2D座標に変換
 * ゲームだと主に敵に攻撃が当たった時の、敵座標位置が3D座標になります。
 * テラシュールブログを参考(というかほぼ丸パクリ)にして作成しました。
 * 下記ページの「World Space を Screen Space Cameraへ」です。
 * http://tsubakit1.hateblo.jp/entry/2016/03/01/020510
* 変換した2D座標にParticleSystemを移動
* Emitでパーティクル生成。

以下コード抜粋

```c#:ParticleHorming.cs
public void CreateParticle()
{
    // 3D空間座標からカメラスクリーン上の座標に変換する
    Vector3 basePos = m_Emit3DTransformList.GetRandom().position;   // GetRandom()についてはListExtensionsを参照
    Vector2 screenPos = m_Camera.WorldToScreenPoint(basePos);

    // カメラスクリーン座標をキャンバス上のローカル座標に変換する
    Vector2 cameraCanvasPos = Vector2.zero;
    RectTransformUtility.ScreenPointToLocalPointInRectangle(m_RaycasterCameraCanvasRectTransform, screenPos, m_Camera, out cameraCanvasPos);

    // 座標確認
    Debug.LogFormat("rectPos{0}", cameraCanvasPos);

    // ParticleSystemを放出位置に移動させてEmit
    m_ParticleSystem.transform.localPosition = cameraCanvasPos;
    m_ParticleSystem.Emit(m_EmitParticleCount);
}
```

## パーティクル更新(収束)処理
普段パーティクルをスクリプト側で操作することは余り無いですが、今回は特定位置に収束させたかったのでググった結果、公式サイトで以下のページが出てきました。
https://docs.unity3d.com/ja/current/ScriptReference/ParticleSystem.GetParticles.html

このページを参考に以下の様な処理で更新させることにしました。

```c#:ParticleHorming.cs
// 更新
for(int i = 0; i < m_ActiveParticleCount; ++i)
{
    float rate = (1.0f - m_ParticleList[i].remainingLifetime / m_ParticleList[i].startLifetime);
    rate = Mathf.Pow(rate, m_ReactionDistance); // 指数関数を加えることにより、収束する勢いを変更できるようにしてる
    m_ParticleList[i].position = Vector3.Lerp(m_ParticleList[i].position, m_TargetTransform.position, rate);
}
m_ParticleSystem.SetParticles(m_ParticleList, m_ActiveParticleCount);
```

今回は指数関数を使って、等速直線移動ではなく少しアレンジをしています。
ここに関しては書き方によって幾通りも表現が出来ると思います。

# 感想
当初はScreenSpaceCameraのCanvas一つで解決できないか試行錯誤していましたが、キャラを追尾する = Canvasが移動する関係上、パーティクルが毎フレームぶれたり座標がおかしなことになったりした結果、今の形に落ち着きました。
冒頭でも言った通りScreenSpaceOverlay設定のCanvasには対応できないので、結構使いどころは限定的かも。
ですが、ParticleSystem上でパーティクルをいつも通り設定できるメリットは大きいので、個人的には満足度高めです。