Amazonで商品を見るセール会場へ

Unityパーティクルの使い方:UI重ね・文字・サイズ・衝突の設定

UnityのParticle Systemを、UIに重ねる、画面内のサイズを調整する、文字Textureを使う、Cameraの背景を切り分ける、パスへ沿わせる、衝突で別のエフェクトを出す用途に分けて説明します。まず1粒・1Camera・1Materialで動作を確認し、用途ごとに必要な設定を追加します。

参照仕様はUnity 6.3 LTSです。元記事の画像とGIFは旧環境の資料です。今回Unityで実行した検証画像ではありません。RenderTextureの透過や外部パス用コードはパイプライン・依存ファイルを確認する必要があり、掲載C#の修正版もUnityでのコンパイル・動作確認は未実施です。

目次

UIの前にParticleを表示する:RenderTextureとRawImage

Screen Space – OverlayのCanvas上へParticle Systemを表示する方法の一つは、専用Cameraの映像をRenderTextureへ描き、RawImageとして表示する構成です。旧GIFはこの方式の資料で、現在のすべてのパイプラインで透過が保たれることを示したものではありません。

uGUIの前にパーティクルを表示
  1. Particle専用Layerを用意し、Particle SystemをそのLayerへ移します。専用CameraのCulling MaskをそのLayerだけにし、通常のMain Cameraからは外して二重描画を避けます。
  2. Alphaを持つフォーマットのRenderTextureを作り、専用CameraのTarget Textureへ割り当てます。最初はBuilt-inの小さな検証シーンなど、透明背景の出力条件を把握できる構成で確認します。
  3. Overlay CanvasにRawImageを追加し、TextureへそのRenderTextureを設定します。背景より後、前面に残す文字より前となるようHierarchy内の描画順を確認します。
  4. 装飾だけのRawImageならRaycast Targetをオフにし、Button操作を妨げないことを確認します。縦横比や伸びはAspect Ratio Fitterの記事も参照します。
RawImageの設定

専用Cameraは背景を毎フレーム消す設定で確認します。旧Built-inのSolid Colorでは背景色のAlphaを0にする例がありますが、RenderTexture、Shaderの出力Alpha、ポストプロセスも関係します。URPのCamera設定は公式のCameraコンポーネント資料を確認し、旧Clear Flagsの欄名をそのまま当てはめません。

カメラの設定

期待する結果は、Particle映像が指定したUIの位置へ重なり、背景UIとButton操作が保たれることです。階層図は旧資料です。専用CameraにRawImage自身や別の不要なCanvasを映す循環構成にしないよう、LayerとCanvasの描画方式を確認します。

階層構造
項目・症状設定・確認先
RawImageが黒い四角になるCameraの背景、RenderTextureのAlpha、Particle Material、後処理を順に確認。背景色Alpha 0だけで透過を保証しない。
加算Particleが見えないRGBはあるがAlphaが0の出力では通常のUIブレンドで消える場合がある。Particle ShaderとUI側の合成方法を確認。
Particleが二重に見えるMain Cameraと専用CameraのCulling Mask、対象Layer、RawImageを確認。
Buttonを押せないRawImageのRaycast Target、重なるGraphic、EventSystemを確認。
縦横に伸びるRenderTextureの寸法とRawImageの縦横比を確認。

Particleが大きくならない:Max Particle Sizeを確認する

Rendererモジュールの公式説明で、Max Particle Sizeは対象のBillboard系Render Modeに対する画面内の制限です。Start Sizeのワールド寸法とは異なり、Meshモードにも同じ制限があると決めつけません。

Max Particle Sizeのテスト1
  1. RendererのRender Modeを確認し、Start Size、TransformのScale、CameraのProjectionと距離を記録します。
  2. Billboard、Stretched Billboard、Horizontal Billboard、Vertical BillboardではMax Particle Sizeの値を確認し、制限が原因か比較します。
  3. 必要な範囲まで表示できることを確認してから、Start SizeとCameraの条件を調整します。画面を大きく覆う透明Particleの負荷は別に測定します。
Max Particle Sizeのテスト2

元資料にあるStart Size 100とPlane Scale 10、Orthographic Size 15とStart Size 25などは当時の比較条件です。投影、Meshの寸法、Scale、Render Modeが変われば意味も変わるため、普遍的な換算式として使いません。今回その旧条件を再現していません。

文字をParticleのTextureとして表示する

文字用Canvasを専用CameraでRenderTextureへ描き、そのTextureをParticle Materialへ渡す方式です。旧画像は文字がTextureになった資料で、実行時に各Particleが独立した文字を保持する仕組みではありません。

パーティクルでテキストを発生
  1. 文字用Layerを用意し、専用CameraがそのLayerだけを描く構成にします。文字CanvasはScreen Space – Cameraなど対象Cameraへ描かれる方式を選び、Overlayのまま専用Cameraへ映ると決めつけません。
  2. 専用CameraのTarget TextureへRenderTextureを設定し、背景とAlphaを確認します。文字が読める寸法、余白、色を用意します。
  3. Particle MaterialのTextureへRenderTextureを割り当て、1粒・停止位置で字形とAlphaを確認します。
  4. 文字を更新し、既に出ているParticleも同じTextureの内容へ変わることを確認します。出現時の文字を各粒へ固定したい場合は、別TextureやAtlasの管理など追加の設計が必要です。

次のTimeScriptは旧uGUIのText専用です。TextMeshProのTMP_Textへそのまま付けるコードではありません。フォントとTMPの記事も参照してください。ファイル名をTimeScript.csにしてTextと同じGameObjectへ追加し、Durationを10にする確認用の例です。

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Text))]
public class TimeScript : MonoBehaviour
{
    [SerializeField] private float duration = 10f;
    private Text label;
    private float remaining;
    private int previousValue = -1;

    private void Awake() { label = GetComponent<Text>(); }

    private void OnEnable()
    {
        if (label == null) label = GetComponent<Text>();
        remaining = Mathf.Max(0f, duration);
        previousValue = -1;
        Refresh();
    }

    private void Update()
    {
        remaining = Mathf.Max(0f, remaining - Time.deltaTime);
        Refresh();
    }

    private void Refresh()
    {
        int value = Mathf.CeilToInt(remaining);
        if (label == null || value == previousValue) return;
        label.text = value.ToString();
        previousValue = value;
    }
}

有効化時に残り時間をリセットし、切り上げた整数が変わるときだけTextを更新します。期待する表示は10から0までのカウントで、Time.deltaTimeを使うためTime Scaleが0なら止まります。負の値を表示せず、再有効化すると最初へ戻る設計です。旧更新画像は今回のコードの実行結果ではありません。

テキストの更新

残像が出る:CameraのクリアとTrailを分ける

Cameraが前のフレームを消さない設定では映像が蓄積することがあります。旧画像はClear Flagsの資料です。URPではBackground Typeなど、その版の項目で確認します。すべての残像がクリア設定の問題とは限らず、ParticleのTrailsやポストプロセスも切り分けます。

Clear Flagsの設定
  1. Particleを止め、Trailsや残像を作る後処理を外した比較を作ります。
  2. 対象Cameraが描く先と、背景を毎フレーム消す設定を確認します。
  3. RenderTextureに残った画像か、Particleの生存中の表示かを区別します。
  4. UI透過が必要なら、背景を消した後のAlphaとRawImageの合成も再確認します。

パスに沿わせる:旧外部コードの依存関係

次の画像は、外部のParticle Pathコードを使った旧資料です。標準Particle Systemへ設定値を一つ入れるだけの機能ではありません。

パスに沿ったパーティクル

元記事の紹介動画Unity Particles along a Path – Codeの公開メタ情報では、作者はBLINDED AM ME、配布先はUnityAssetsリポジトリです。今回動画の操作を視聴・再現したわけではありません。取得した現在のParticlePathFlow.csはMonoBehaviour2を継承し、PathやMathExtensionsなどにも依存しています。旧記事の3ファイル名だけをコピーすれば使えるとは案内しません。

パスに沿わせる設定
  1. まずリポジトリの実際のファイル構成、依存する型、利用条件と対象Unity版を確認します。
  2. 既存プロジェクトへ直接追加せず、複製した検証用プロジェクトで依存関係とConsoleのエラーを確認します。
  3. パスとParticleの座標空間、速度、寿命、経路長をそろえます。取得したコードは粒子の経過寿命と速度の大きさからパス上の距離を求めるため、パスだけを設定して結果が確定するものではありません。
  4. ゼロ長の経路、粒子数変更、再生・停止なども確認し、実行結果を保存してから本番の演出へ移します。

現在の依存ファイル一式とUnityでのコンパイル・動作確認は未完了です。この節は旧資料と調査結果の整理であり、現行版で動くことを確認した完成手順ではありません。

衝突位置へ別のParticleを生成する

旧コードのParticleSystem.CollisionEvent[]を、現行APIに合うList<ParticleCollisionEvent>へ変更しています。GetCollisionEventsで得たイベント数だけを処理します。OnParticleCollisionの呼び出し回数と衝突イベントの数を同じと考えないようにします。

  1. ビーム側のParticle SystemへFX_Test.csを追加します。CollisionモジュールをWorld、3Dとして、対象ColliderのLayerをCollides Withへ含め、Send Collision Messagesを有効にします。
  2. 衝突相手に3D Colliderを付けます。2D Colliderを同じ例へ混ぜず、Particleが実際に相手へ到達する位置と速度を用意します。
  3. 衝突時のParticle Systemを別のPrefabにし、Hit Effect Prefabへ割り当てます。Scene内のExplosionという名前を検索する方式ではありません。ビーム自身を割り当てず、衝突用PrefabへFX_Testを付けない構成にします。
  4. 衝突用Prefabの本体と子のLoopをオフにし、単独再生で終了できることを確認します。Cleanup TimeoutはStart Delay、Duration、Lifetime、子の演出を含めて十分な値へ調整します。
  5. Play On Awakeで確認するか、ButtonのOn ClickへこのコンポーネントのPlayBeamを登録します。このコードは旧Input.GetMouseButtonDownを使いません。
  6. 最初は少ない粒子で衝突点と生成数を確認し、1コールバックの上限、同時生成数、停止時の後始末を確認します。
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[RequireComponent(typeof(ParticleSystem))]
public class FX_Test : MonoBehaviour
{
    [SerializeField] private ParticleSystem hitEffectPrefab;
    [SerializeField, Min(1)] private int maxHitsPerCallback = 4;
    [SerializeField, Min(1)] private int maxLiveHits = 32;
    [SerializeField, Min(0.1f)] private float cleanupTimeout = 10f;
    private ParticleSystem source;
    private readonly List<ParticleCollisionEvent> collisionEvents = new List<ParticleCollisionEvent>(16);
    private readonly List<ParticleSystem> liveHits = new List<ParticleSystem>();

    private void Awake() { source = GetComponent<ParticleSystem>(); }

    public void PlayBeam()
    {
        if (source != null && isActiveAndEnabled) source.Play(true);
    }

    private void OnParticleCollision(GameObject other)
    {
        if (!isActiveAndEnabled || source == null || other == null
            || hitEffectPrefab == null || hitEffectPrefab == source) return;

        liveHits.RemoveAll(item => item == null);
        int count = source.GetCollisionEvents(other, collisionEvents);
        int spawnCount = Mathf.Min(count, Mathf.Max(1, maxHitsPerCallback));
        for (int i = 0; i < spawnCount && liveHits.Count < Mathf.Max(1, maxLiveHits); i++)
        {
            ParticleSystem hit = Instantiate(hitEffectPrefab, collisionEvents[i].intersection, Quaternion.identity);
            hit.gameObject.SetActive(true);
            hit.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
            hit.Play(true);
            liveHits.Add(hit);
            StartCoroutine(ReleaseAfterPlayback(hit));
        }
    }

    private IEnumerator ReleaseAfterPlayback(ParticleSystem hit)
    {
        float deadline = Time.realtimeSinceStartup + Mathf.Max(0.1f, cleanupTimeout);
        yield return null;
        while (hit != null && hit.IsAlive(true) && Time.realtimeSinceStartup < deadline)
            yield return null;
        if (hit != null)
        {
            liveHits.Remove(hit);
            Destroy(hit.gameObject);
        }
    }

    private void OnDisable()
    {
        StopAllCoroutines();
        foreach (ParticleSystem hit in liveHits)
            if (hit != null) Destroy(hit.gameObject);
        liveHits.Clear();
    }
}

衝突位置へPrefabを作り、IsAlive(true)で子を含めて生存を確認します。通常の終了まで待ちますが、実時間のタイムアウトもあるため、設定が短いと演出を途中で破棄します。無効化時はこのスクリプトが生成した衝突用オブジェクトを破棄します。ビーム本体の停止や全体のリセットまで行うコードではありません。

上限を超える衝突イベントは省略されます。これは無制限な生成を避ける例で、Object Poolや性能測定を実装したものではありません。頻繁な衝突へ使う場合は対象端末で測定し、必要ならPool化など別の設計を検討してください。

表示できないときの確認順

項目・症状設定・確認先
何も見えないCameraのCulling Mask、Layer、ParticleのPlay状態、MaterialのShaderとAlphaを確認。
UIに重ならないRenderTextureのTarget Texture、RawImageのTextureと描画順、背景のAlphaを確認。
文字が変わらないTextとTMPの型、文字Cameraが描くCanvas、共有Textureの更新を確認。
衝突用エフェクトが出ない3D Collider、Collides With、Send Collision Messages、Hit Effect Prefab、Consoleを確認。
衝突用エフェクトが消えない/途中で消える子を含むLoop、IsAlive、Cleanup Timeout、コンポーネントの有効状態を確認。

既存の元コード置き場は原版の資料です。本文のC#修正版と同一とは限らず、ここでは外部リポジトリを更新していません。素材のUV・連番・Billboardは次の記事、描画方式はShaderの記事で確認してください。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

Unityのおすすめ書籍

開発PCの選び方

周辺機器の優先度

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次