【Unity】エフェクトを選定する効率的な方法を考えてみた

# はじめに
Unityでゲームを開発している開発者の皆さん。エフェクト選定するの苦労されていませんか？私は苦労していました...。エフェクトってAssetStoreとかでついつい色々集めがちで、たくさんアセットがあると良い感じのエフェクトを見つけるのに非常に苦労しますよね。今日は、そいった問題を効率的に解決する方法を開発したので共有しようと思います。

# 解決策の方針
おそらく一番簡単に解決する方法は、エフェクトがシーン上にずらっと並んで表示されていればそれでいいわけですよね。ここでは、指定したフォルダ以下に存在するエフェクトをシーン上にずらっと並べるような簡単なスクリプトを作って解決することとします。

# 結論
こうなります。全体を俯瞰して好きなエフェクトを選定するのに便利です。適切と思われるエフェクトをクリックしてInspectorで参照されているPrefabをクリックすることで、Projectウインドウで該当のデータを突き止めることができます。非常に便利というかこれがないと演出を実装するの不可能でした。[Youtube](https://youtu.be/vR_MvTA-yTg)でこの記事の解説も並行して行なっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2803164/b98e48aa-65ff-4a46-a9d0-1bfbfeea34ec.png)

# 実現方法

### スクリプトを準備する

`ExhibitionSystem.cs`と、`ExhibitionEffectController.cs`の２つのスクリプトを用意します。プロジェクトに入れます。以上です、これだけです。あとは、エフェクトの含まれる任意のフォルダをEditorのProjectウインドウから選択し、右クリックして`Exhibition -> Show`で確認できます。

```C#:ExhibitionSystem.cs
namespace HiArda
{
	public static class ExhibitionSystem
	{
		[MenuItem("Assets/Exhibition/Show")]
		public static void DoExhibitionEffects()
		{
			var selectPath = "";
			foreach (var selection in Selection.GetFiltered(typeof(DefaultAsset), SelectionMode.Assets))
			{
				if (selection is not DefaultAsset) continue;
				var path = AssetDatabase.GetAssetPath(selection);
				if (!AssetDatabase.IsValidFolder(path)) continue;
				selectPath = path;
				break;
			}

			var parent = GameObject.Find("ExhibitionEffects");
			if (parent)
			{
				Object.DestroyImmediate(parent);
				parent = null;
			}

			if (!parent)
			{
				parent = new GameObject
				{
					name = "ExhibitionEffects"
				};
				var component = parent.AddComponent<ParticleSystem>();
				var emission = component.emission;
				emission.enabled = false;
			}

			DoExhibition(selectPath, 10, 50, parent.transform);
		}

		private static void DoExhibition(string path, float offset, float maxSize, Transform parent)
		{
			var guids = AssetDatabase.FindAssets("t:Prefab", new string[] { path });
			var prefabPaths = guids.Select(guid => AssetDatabase.GUIDToAssetPath(guid)).ToArray();
			var xx = 0f;
			var zz = 0f;
			foreach (var prefabPath in prefabPaths)
			{
				var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
				var instance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
				if (!instance)
				{
					Debug.LogError("[Exhibition] Failed create instance. prefabPath=" + prefabPath);
					return;
				}
				instance.AddComponent<ExhibitionEffectController>();
				instance.transform.position = new Vector3(xx, 1, zz);
				if (parent)
				{
					instance.transform.SetParent(parent, false);
				}
				xx += offset;
				if (!(xx > maxSize)) continue;
				xx = 0f;
				zz += offset;
			}
		}
	}
}
```

```C#:ExhibitionEffectController.cs
namespace HiArda
{
	[ExecuteInEditMode]
	public class ExhibitionEffectController : MonoBehaviour
	{
		private readonly List<ParticleSystem> particleSystems = new List<ParticleSystem>();

		private void Start()
		{
			particleSystems.AddRange(GetComponentsInChildren<ParticleSystem>());
		}

		private void Update()
		{
			if (particleSystems.Count <= 0) return;
			var top = particleSystems[0];
			var main = top.main; // ここはmainを編集しないので省略可
			if (main.loop) return;
			var allStop = true;
			foreach (var ps in particleSystems.Where(ps => ps.isPlaying))
			{
				allStop = false;
			}
			if (!allStop) return;
			top.Play();
		}
	}
}
```
### 工夫した点

エフェクトには、LoopとLoopしないものとがあります。同じエフェクトでも、含まれる`ParticleSystem`ごとにLoop設定が異なる場合もあります。今回の目的は、エフェクトの選定なので、例えOneShotエフェクトだとしてもループしてももらわないと困ります。そこで、`ExhibitionEffectController`で、OneShotエフェクトの場合は強制的にループを設定しています。同時に、同エフェクト内で再生が完全に終わってから、次のループを開始するように工夫しています。単にループするだけだと、ループの周期が`PerticleSystem`ごとに変わるので、どんどんエフェクトにズレが生じてしまうためです。