Imageの上にParticleSystemを表示して、さらに上にImageを表示する

　uGUI上でParticleエフェクトを表示したい！というのはよくある要望だと思います。昔はかなり面倒くさい方法で実現していた気がするのですが、久々に調べてみたら簡単そうだったのでまとめました。（CanvasのRenderModeをScreenSpace-Cameraにすることが前提です。ScreenSpace-Overlayの場合はRenderTextureを使ったりとか相変わらず面倒くさそうです）
※環境はUnity2017.3.1p4でC#6.0を使っています。

# CanvasとParticleSystemのSortingLayerとOrder in Layerを適切に設定する
　序文でも書いたとおり、CanvasのRenderModeはScreenSpace-Cameraである必要があります。さらに、このModeではCanvasを映すCameraが必要なので、CanvasのRenderCameraにUI用のCameraを設定します。
　CanvasとParticleSystemの表示順序はSortingLayerとOrder in Layerで決まります。例えば、以下のように設定するとCanvas上にParticleSystemが表示されます

- Canvas（SortingLayer=Default, Order in Layer=0）
- ParticleSystem（SortingLayer=Default, Order in Layer=1）

<img width="264" alt="1.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/143232/8e1bfdbd-13af-9bee-488e-0962c6fba02a.jpeg"><img width="246" alt="2.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/143232/be596d85-fbc2-39ba-ae4b-05aef99eb6a4.jpeg"><img width="488" alt="3.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/143232/8833af0e-6bf3-cc48-98c4-9865fbd9f01b.jpeg">

　SortingLayerの方が優先順位は高いですが、あらかじめLayerを定義しておく必要があるので自由度は低いです。Order in Layerは数値で指定できるので柔軟に表示優先度を決めることができます。

# Canvasの子供にCanvasを置いてParticleSystemの上にImageを表示する
　ParticleSystemのさらに上にImageなどのUIを表示するには、Order in Layerが大きいCanvasを用意します。Canvasの子供にCanvasを設置した場合、Override SortingをOnにすることによりSortingLayerとOrder in Layerを上書きすることができます。
<img width="262" alt="4.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/143232/6280fdf8-a32f-0b7b-9670-75bc6bac3e7f.jpeg"><img width="488" alt="5.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/143232/bef2dbe7-e801-cb46-a663-29c3aad5920f.jpeg">

　Canvasの子供にCanvasを設置するとHierarchy上の順序と表示順序が一致しなくなってしまうのが欠点です。乱発すると混乱の元になるので程々に。

# Order in Layerを自動で管理する（SortingOrderUpdater）
　開発規模が大きくなってくると、CanvasのOrder in Layerを変更することもあるかと思います。この変更時にParticleや子供Canvasの設定を変更し忘れると「Particleが消えた！」といった事態を招きます。
　そこでトップのCanvasの設定を変更すると自動で影響下にあるParticleSystemや子供Canvasの設定を変更するSortingOrderUpdaterを作りました。CanvasにはCanvasOrderUpdater、ParticleSystemにはParticleOrderUpdaterを設定します。

```C#:SortingOrderUpdater.cs
using UnityEngine;

public abstract class SortingOrderUpdater : MonoBehaviour
{
    [SerializeField]
    SortingOrderUpdater[] childSortingOrderUpdaters;

    [SerializeField]
    int relativeOrder = 0;

    public void SetOrder(int order, int layerId)
    {
        int newOrder = order + relativeOrder;

        SetOrderInDerivedClass(newOrder, layerId);

        foreach (var updater in childSortingOrderUpdaters)
            updater.SetOrder(newOrder, layerId);
    }

    protected abstract void SetOrderInDerivedClass(int order, int layerId);

#if UNITY_EDITOR
    public void Reset()
    {
        this.childSortingOrderUpdaters = ResetUtility.GetDirectChildComponents<SortingOrderUpdater>(this);
        foreach (var updater in childSortingOrderUpdaters)
            updater.Reset();
    }
#endif
}
```
```C#:CanvasOrderUpdater.cs
using UnityEngine;

[RequireComponent(typeof(Canvas))]
public class CanvasOrderUpdater : SortingOrderUpdater
{
    [SerializeField]
    Canvas canvas;

    protected override void SetOrderInDerivedClass(int order, int layerId)
    {
        canvas.sortingOrder = order;
        canvas.sortingLayerID = layerId;
    }
}
```
```C#:ParticleOrderUpdater.cs
using UnityEngine;

[RequireComponent(typeof(ParticleSystem))]
public class ParticleOrderUpdater : SortingOrderUpdater
{
    [SerializeField]
    ParticleSystemRenderer particleSystemRenderer;

    protected override void SetOrderInDerivedClass(int order, int layerId)
    {
        particleSystemRenderer.sortingOrder = order;
        particleSystemRenderer.sortingLayerID = layerId;
    }
}
```
```C#:ResetUtility.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

#if UNITY_EDITOR
public class ResetUtility
{
    public static T[] GetDirectChildComponents<T>(Component component) where T : Component
    {
        return GetDirectChildComponents<T>(component.transform).ToArray();
    }

    static IEnumerable<T> GetDirectChildComponents<T>(Transform transform) where T : Component
    {
        for (int i = 0; i < transform.childCount; i++)
        {
            Transform child = transform.GetChild(i);
            T component = child.GetComponent<T>();
            if (component != null)
            {
                yield return component;
            }
            else
            {
                foreach (T t in GetDirectChildComponents<T>(child))
                    yield return t;
            }
        }
    }
}
#endif
```
　設置が終わったらトップにあるCanvasOrderUpdaterをResetします。これで影響下のSortingOrderUpdaterが登録されます。
　このコードは一例です。このコードは動的にトップCanvasのOrder in Layerを変えることを前提にしています。実際は運用に合わせて改造することになるでしょう。
　この例では、トップのSetOrderを呼び出すことで、影響下のCanvasとParticleSystemの値が更新されます。Order in Layerの差はrelativeOrderという値をInspector上で設定します。Particleには+1、さらに上に表示するCanvasには+2を設定しています。

# Ex. Particleの大きさ
　Particleの大きさはlocaScaleで決まっているようです。つまり親のscaleがいくら変わっても、Particleの大きさは連動しないということになります。これによりトップCanvasの設定次第では意図しない表示になる可能性があります（大きさが変わる）。

- CanvasのPlaneDistance
- CanvasScalerの設定

　このあたりが変わるとトップCanvasのscaleが変わるので、Particleの表示上の大きさは変わってしまいます。特にCanvasScalerに起因するscale変化は、端末ごとに値が変わってしまう可能性があるので厄介です。画面の大きさに合わせてParticleのlocalScaleを自動調整するような仕組みが必要になります。