lime雑記

ゲーム開発、その他雑記。

【Unity】ParticleSystemを用いたエフェクトの再生・停止・停止待ち

この記事でのバージョン

Unity 2021.3.11f1
UniRx 7.1.0
UniTask 2.3.1

概要

Unityでエフェクトを表現する手法として、ParticleSystemコンポーネントが用いられることが多いかと思いますが、その際の再生・停止・停止待ちの方法について紹介します。

前提

ParticleSystemコンポーネントが階層上にいくつも用いられたGameObjectのPrefabが用意されているとします。

再生

public class ParticleObject : MonoBehaviour {
  // ParticleSystemを直接Serialize
  // ScriptableObjectを経由してもOK
  [SerializeField]
  private ParticleSystem _effectPrefab;

  private ParticleSystem _effectInstance;

  private void Awake() {
    _effectInstance = Instantiate(_effectPrefab, transform);
  }

  public void Play() {
    if (_effectInstance) {
      _effectInstance.Play();
    }
  }
}

ParticleSystemをInstantiateしたeffectInstanceのPlay関数を呼ぶことで、ルート以下全てのParticleSystemが再生されます。
Play関数を呼ぶタイミングによって再生タイミングをコントロールできますが、ParticleSystem側の設定でPlayOnAwakeがオンになっている場合はInstantiateしたタイミングで自動で再生されてしまうため、PlayOnAwakeの設定は基本的にオフにしておいた方が無難かもしれません。

停止

public class ParticleObject : MonoBehaviour {
  [SerializeField]
  private ParticleSystem _effectPrefab;

  private ParticleSystem _effectInstance;

  private void Awake() {
    _effectInstance = Instantiate(_effectPrefab, transform);
  }

  public void Play() {
    if (_effectInstance) {
      _effectInstance.Play();
    }
  }

  public void Stop() {
    if (_effectInstance) {
      _effectInstance.Stop();
    }
  }
}

停止も同様にeffectInstanceのStop関数を呼ぶことで、ルート以下全てのParticleSystemが停止されます。

停止待ち

ワンショットエフェクトの再生終わりを待ちたいとき、ループエフェクトに対してStopを呼び完全に停止するのを待ちたいときはParticleSystemのStopActionを使用します。
また、そのままではStopActionのコールバックを受け取れないため、動的にComponentを追加しつつ、UniRxを利用してイベントを通知するようにしています。
なお、ここでいう停止待ちとはParticleの放出が止まっているかつ、Particleが全て消滅している状態まで待つことを指します。

ObservableParticleSystemTrigger.cs

[DisallowMultipleComponent]
public class ObservableParticleSystemTrigger : ObservableTriggerBase {
  private Subject<Unit> _onParticleSystemStopped;

  private void OnParticleSystemStopped() {
    _onParticleSystemStopped.OnNext(Unit.Default);
  }

  public IObservable<Unit> OnParticleSystemStoppedAsObservable() {
    return _onParticleSystemStopped ??= new Subject<Unit>();
  }

  protected override void RaiseOnCompletedOnDestroy() {
    _onParticleSystemStopped?.OnCompleted();
  }
}

まず、ObservableTriggerBaseを継承したクラスを用意し、OnParticleSystemStoppedのコールバックを受け取ってそれをObservableで通知できるようにします。

UniRxExtensions.cs

public static class UniRxExtensions {
  public static IObservable<Unit> OnParticleSystemStoppedAsObservable(this ParticleSystem particleSystem) {
    if (particleSystem == null) return Observable.Empty<Unit>();

    // StopActionをCallbackに変更
    var mainModule = particleSystem.main;
    mainModule.stopAction = ParticleSystemStopAction.Callback;

    var trigger = particleSystem.gameObject.GetComponent<ObservableParticleSystemTrigger>();
    if (trigger == null) trigger = particleSystem.gameObject.AddComponent<ObservableParticleSystemTrigger>();
    return trigger.OnParticleSystemStoppedAsObservable();
  }
}

次に、ParticleSystemの拡張メソッドを用意し、先ほど作成したObservableParticleSystemTriggerを取得もしくは追加すると同時にIObservableを返すようにします。
また、ParticleSystemのStopActionもCallbackに変更します。

public class ParticleObject : MonoBehaviour {
  [SerializeField]
  private ParticleSystem _effectPrefab;

  private ParticleSystem _effectInstance;

  private void Awake() {
    _effectInstance = Instantiate(_effectPrefab, transform);
    _effectInstance.Play();
  }

  public void Stop() {
    if (_effectInstance) {
      _effectInstance.Stop();
    }
  }

  public async UniTask WaitStop(CancellationToken cancellationToken) {
    if (_effectInstance == null) return;
    // OnCompleteは呼んでいないので第1引数はtrue
    await _effectInstance.OnParticleSystemStoppedAsObservable().ToUniTask(true, cancellationToken);
  }
}

ParticleSystemからOnParticleSystemStoppedAsObservable関数が呼べるようになったので、あとはこのObservableを利用して停止したときの処理を記述します。
(今回はUniTaskに変換しawaitで待てるようにしています)

なお、戻り値がUniTaskで問題ない場合はUniTaskの中で拡張メソッドがすでに用意されており、以下のように書けば待つことが可能ですが、あらかじめParticleSystemのStopActionをCallbackにしておく必要があります。

public async UniTask WaitStop(CancellationToken cancellationToken) {
    if (_effectInstance == null) return;
    await _effectInstance.GetAsyncParticleSystemStoppedTrigger().OnParticleSystemStoppedAsync(cancellationToken);
  }