ParticleSystemの粒子のアルファをスクリプトで変更

2023/03/11 : 初稿
2023/03/13 : コード修正
Unity : 2021.3.15f1

# やりたいこと

Unityで再生中のParticleSystemに対して、
スクリプトでアルファを変化させたい。

# 実現方法

ググると大体
`ParticleSystem.main.startColor`をいじる記事が出ますが、
どうも新たに放出される粒子には効くものの、
既存の粒子には影響なさそう。

粒子の寿命が短いならそれでもいいんですが、
今すぐ、パーティクル全体をフェードイン/アウトさせたい！
といった場合に対処しづらい。

なので、colorOverLifetimeの方をいじることにしました。
（大体のパーティクルはこれ使ってるでしょうし）

```cs:ParticleUtils.cs
///
/// @file   ParticleUtils.cs
/// @author KyYukimoto
/// 
using UnityEngine;

namespace Utils
{
    public class ParticleUtils : MonoBehaviour
    {
        // system
        ParticleSystem System;

        // gradient
        struct GradientData
        {
            Gradient Src;
            int AlphaKeyNum;
            GradientAlphaKey[] StoredAlphaKeys;
            GradientAlphaKey[] AlphaKeys;
            bool IsAlphaModified;

            // Setup
            public void Setup(Gradient src)
            {
                if (src == null || Src != null) {
                    return;
                }
                Src = src;
                StoredAlphaKeys = src.alphaKeys;
                AlphaKeyNum = (StoredAlphaKeys == null) ? 0 : StoredAlphaKeys.Length;
            }

            // Cleanup
            public void Cleanup()
            {
                if (Src == null) {
                    return;
                }
                if (IsAlphaModified) {
                    Src.alphaKeys = StoredAlphaKeys;
                }
                this = default;
            }

            // SetAlphaRate
            public void SetAlphaRate(float alpha)
            {
                if (Src == null || AlphaKeyNum <= 0) {
                    return;
                }
                if (AlphaKeys == null) {
                    AlphaKeys = new GradientAlphaKey[AlphaKeyNum];
                }
                for (int i = 0; i < AlphaKeyNum; i++) {
                    var key = StoredAlphaKeys[i];
                    key.alpha *= alpha;
                    AlphaKeys[i] = key;
                }
                Src.alphaKeys = AlphaKeys;
                IsAlphaModified = true;
            }
        }

        // MinMaxGradientData
        struct MinMaxGradientData
        {
            public ParticleSystem.MinMaxGradient Src;
            public bool HasSetup { get; private set; }
            Color Color;
            Color ColorMin;
            Color ColorMax;
            GradientData Gradient;
            GradientData GradientMin;
            GradientData GradientMax;

            // Setup
            public void Setup(ParticleSystem.MinMaxGradient src)
            {
                if (HasSetup) {
                    return;
                }
                HasSetup = true;
                Src = src;
                Color = src.color;
                ColorMin = src.colorMin;
                ColorMax = src.colorMax;
                Gradient.Setup(src.gradient);
                GradientMin.Setup(src.gradientMin);
                GradientMax.Setup(src.gradientMax);
            }

            // Cleanup
            public void Cleanup()
            {
                if (!HasSetup) {
                    return;
                }
                HasSetup = false;
                Gradient.Cleanup();
                GradientMin.Cleanup();
                GradientMax.Cleanup();
                this = default;
            }

            // SetAlphaRate
            public void SetAlphaRate(float alpha)
            {
                if (!HasSetup) {
                    return;
                }
                Src.color = new Color(Color.r, Color.g, Color.b, Color.a * alpha);
                Src.colorMin = new Color(ColorMin.r, ColorMin.g, ColorMin.b, ColorMin.a * alpha);
                Src.colorMax = new Color(ColorMax.r, ColorMax.g, ColorMax.b, ColorMax.a * alpha);
                Gradient.SetAlphaRate(alpha);
                GradientMin.SetAlphaRate(alpha);
                GradientMax.SetAlphaRate(alpha);
            }
        }
        MinMaxGradientData StartColor;
        MinMaxGradientData LifeColor;

        // アルファ
        float Alpha = 1;

        // Start
        void Start()
        {
            // パラメータを保持
            StoreOriginalColor();
        }

        // OnDestroy
        void OnDestroy()
        {
            // パラメータをもとに戻す
            RestoreOriginalColor();
        }

        // 元の色を保存
        void StoreOriginalColor()
        {
            // すでに保存済み
            if (System != null) {
                return;
            }

            // 保存
            System = GetComponent<ParticleSystem>();
            if (System == null) {
                return;
            }

            // life or main
            var life = System.colorOverLifetime;
            if (life.enabled) {
                LifeColor.Setup(life.color);
            } else {
                var main = System.main;
                StartColor.Setup(main.startColor);
            }

            // 既にリクエストされていたらここで適用
            Apply(Alpha, true);
        }

        // 元の色に戻す
        void RestoreOriginalColor()
        {
            // alpha
            Alpha = 1;

            // 復元
            StartColor.Cleanup();
            LifeColor.Cleanup();
        }

        // アルファ変更
        public void SetAlphaRate(float alpha)
        {
            // check
            if (System == null) {
                Alpha = alpha;
                return;
            }

            // 変更
            Apply(alpha, false);
        }

        // アルファ戻す
        public void ResetAlphaRate() { SetAlphaRate(1); }

        // Apply
        void Apply(float alpha, bool force)
        {
            // alpha
            if (!force && Mathf.Approximately(alpha, Alpha)) {
                return;
            }
            Alpha = alpha;

            // life
            if (LifeColor.HasSetup) {
                var life = System.colorOverLifetime;
                if (life.enabled) {
                    LifeColor.SetAlphaRate(alpha);
                    life.color = LifeColor.Src;
                }
            }

            // main
            if (StartColor.HasSetup) {
                var main = System.main;
                StartColor.SetAlphaRate(Alpha);
                main.startColor = StartColor.Src;
            }
        }
    }
}
```

このスクリプトをParticleSystemのあるgameObjectにAddComponentして、
必要に応じてSetAlphaRate()に０から１の値を入れてあげれば、
即座に粒子のアルファが変化するはず。

注意点として、
少なくともGradient.alphaKeysをreadするとGC.Allocがよばれてしまう。
上の実装では、write時にはよばれてなさそうなんだけどよばれてたら残念なことに
結局のところ、専用のシェーダー作ってそっちで対応したほうがいいのかも。