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


サンプル動画

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

サンプルプロジェクト

以下のGithubに上げてあります
https://github.com/madoramu/ParticleHorming
※サンプル動画は別プロジェクトの物なので、上記URLのプロジェクトは少し内容が異なります。

環境

Unity2018.3
WindowsとAndroidで動作確認済み
重要:ScreenSpaceCamera設定のCanvasのみ対応しています

実装方法

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

メインカメラとUIカメラの用意・設定

メインカメラ

こちらはキャラクターの追尾、およびUI以外の描画を担当。

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

UIカメラ

UIの描画のみ担当。

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

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

キャンバスの用意と設定(重要)

UI表示用・座標変換用で2つのScreenSpaceCamera設定Canvasを用意します。

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

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

ParticleSystemの設定

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

  • ParticleSystemオブジェクトのLayerは「必ず「UI」に設定する事
  • UI表示用のCanvas直下に配置する事

今回は以下のように設定しました(プロジェクトから抜粋)

「何故World設定か?」

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

「Rederer→Order in Layerについて」

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

パーティクル発生処理

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でパーティクル生成。

以下コード抜粋

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

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

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上でパーティクルをいつも通り設定できるメリットは大きいので、個人的には満足度高めです。