第21章 パーティクルを制御する - エディター拡張入門

第21章 パーティクルを制御する

21.1 パーティクルシステムの制御

普段、パーティクルシステムを扱うときは GameObject を作成し、選択しながらパーティクルの再生やパラメーターの調整を行います。

シーン、ヒエラルキー、インスペクターを使ってパーティクルを作成

図21.1: シーン、ヒエラルキー、インスペクターを使ってパーティクルを作成

ですが、複数同時編集に対応していないため面倒なことが多々あります。

インスペクターに「Multi-object editing not supported」と出ており複数同時編集ができない

図21.2: インスペクターに「Multi-object editing not supported」と出ており複数同時編集ができない

エフェクトはさまざまですが、Duration(継続時間)Start Lifetime(存続時間) は統一したり、複数のエフェクトに対して Color Over Lifetime を使って最後は透明にして消えるようにしたりします。

すでに複数のエフェクトを作成済みで、それらのパラメータをすべて変更していかなくてはならない場合、1つ1つ編集していかなければいけません。とても手間な作業となってしまいます。

21.2 複数同時編集を行うには

通常でできないとなれば、エディター拡張を使って拡張できる方法を模索することになります。

パーティクルを制御する Editor API は存在しない

残念ながらエディター拡張用の API は用意されていません。ランタイム用の API を駆使して編集していくことになります。よって getter のみのプロパティーであれば値を設定することができません。

困ったときの SerializedObject

本書では何度も出てきていますが UnityEngine.Object であるものはすべて SerializedObject としてシリアライズされています。今回はシリアライズされたものを直接編集することでパーティクルシステムの複数同時編集を行います。

21.3 ParticleSystem を SerializedObject にする

SerializedObject に変換する方法は簡単です。まずは確認のために MenuItem から SerializedObject を作成してみましょう。

次のコードを試してみてコンソールウィンドウに SerializedObject のログが表示されるか確認してください。

using UnityEngine;
using UnityEditor;
using System.Linq;
public class NewBehaviourScript
{
  [MenuItem ("Assets/Get SerializedObject")]
  static void GetSerializedObject ()
  {
    var particleSystems =
        Selection.gameObjects.Select (o => o.GetComponent<ParticleSystem> ());

    foreach (var particleSystem in particleSystems) {

      var so = new SerializedObject (particleSystem);
      Debug.Log (so);

    }
  }

  [MenuItem ("Assets/Get SerializedObject", true)]
  static bool GetSerializedObjectValidate ()
  {
    return Selection.gameObjects.Any (o => o.GetComponent<ParticleSystem> ());
  }
}

21.4 ParticleSystem のプロパティー名を知る

SerializedObject を取得できたので次は SerializedProperty を取得します。SerializedProperty を取得するにはプロパティー名を知らなければいけません。

Prefab を作成して、テキストエディターで見る

プロパティーを知るうえで最も簡単な方法は、テキストベースで保存されたアセットをテキストエディターで見ることです。ParticleSystem のプロパティーを知りたい場合は、ParticleSystem をアタッチしたゲームオブジェクトをプレハブにしましょう。

ここで、必ずしもインスペクターで表示されているプロパティー名と同じわけではないということに注意してください。以下はプレハブの YAML 形式のデータです。そこに、lengthInSec がありますが、このプロパティーは duration のことを指しています。

--- !u!198 &19810290
ParticleSystem:
  m_ObjectHideFlags: 1
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  m_GameObject: {fileID: 109222}
  lengthInSec: 5
  startDelay: 0
  speed: 1
  randomSeed: 0
  looping: 1
  prewarm: 0
  playOnAwake: 1
  moveWithTransform: 1
  ...

テキストエディターで直接、値を書き換えてもいいのですが Unity の管理外での変更となるため Unity を起動したままテキストエディターで編集を行うとデータが破損してしまう可能性があります。どうしてもテキストエディターで編集したい場合は Unity を終了させてから行うようにしてください。

SaveToSerializedFileAndForget を使う

Prefab やアセットにできない(アセットにする手法が提供されていない)ものである場合は、UnityEditorInternal にある InternalEditorUtility.SaveToSerializedFileAndForget を使用します。SaveToSerializedFileAndForget は、UnityEngine.Object のシリアライズを行いアセットとして保存するための API です。

InternalEditorUtility.SaveToSerializedFileAndForget (
  new Object[]{ particleSystem },
  "particleSystem.txt",
  true);

上記のように API を呼び出すことでプロジェクトフォルダー以下に particleSystem.txt が生成され YAML 形式のデータを見ることができます。

SerializedObject.GetIterator を使う

Iterator により、すべての SerializedProperty を取得します。Unity 上のみで完結させたいのであればこの方法を使います。ですが、特定のプロパティー名のみを把握したい場合にこの方法だと手間となることが多いので、この方法はおすすめしません。

var so = new SerializedObject (particleSystem);

var prop = so.GetIterator ();

while (prop.NextVisible (true)) {
  Debug.Log (prop.propertyPath);
}

編集した値を保存

必要な情報はすべてそろったので、あとは編集して値を適用させるだけです。GUI を実装すると説明が長くなってしまうため、今回は duration に 10 をハードコードして代入させてみます。値を代入したら最後に必ず ApplyModifiedProperties を呼び出して更新を行います。

var so = new SerializedObject (particleSystem);

so.FindProperty ("lengthInSec").floatValue = 10;

so.ApplyModifiedProperties ();

21.5 タイムライン

図21.3:

通常は、ParticleSystem コンポーネントをアタッチしたゲームオブジェクトを選択することで、シーンビューでパーティクルアニメーションのシミュレーションを行うことができます。

図21.4:

ですがこの機能は、複数のパーティクルアニメーションに対応していません。*1

なのでアニメーションを確認するには1つ1つ確認するか、複数同時に確認したいときはゲームを再生させて確認する必要があります。

これでは、作業効率的にもよくないので、タイムラインを作成してみましょう。

21.6 タイムラインを作成してみた

図21.5:

本章で説明するために作成したものなので簡素ではありますが、やりたいこと(複数のパーティクルを同時に再生)はできています。

画像だけではわかりにくいと思うので動画を用意しました。

Youtube - パーティクルシステムの自作タイムライン

今回作成したタイムラインで扱った手法や API などを紹介していきます。

21.7 シーン内の ParticleSystem をすべて取得

シーン内にある ParticleSystem は FindObjectsOfType で取得します。

21.8 ParticleSystem.Simulate でパーティクルの再生

パーティクルの再生は Simulate 関数で行うことが可能です。Simulate 関数には第2引数に withChildren があり、子要素のパーティクルも同時に再生してくれます。なので先ほど取得したすべてのパーティクルから子要素のパーティクルを省きます。

21.9 親の ParticleSystem かどうかを判断する

判断の方法は簡単です。

bool IsRoot (ParticleSystem ps)
{
  var parent = ps.transform.parent;
  //親のいない ParticleSystem であればルート
  if (parent == null)
    return true;

  //親がいても ParticleSystem コンポーネントがなければルート
  return parent.GetComponent<ParticleSystem> () == false;
}

21.10 要の TimeControl

時間制御の基板となるものは 第25章「時間を制御する TimeControl」 で紹介しています。続きを読む前に 第25章 を一読すると理解が深まるかもしれません。

GUI を作る

タイムラインは GUILayout.HorizontalSlider をカスタマイズしたものを扱います。EditorGUILayout.Slider は右側に FloatField が付属しており必要ないので扱いません。

通常の HorizontalSlider は、エディター上では 図21.1 のように、つまみの部分が小さなボタンに見えます。これでは小さすぎて扱いづらいかもしれません。そこで HorizontalSlider のスタイルをカスタマイズして見た目を大きくします。*2

Box in Box

背景をボックス、さらにつまみ部分もボックスにします。その時にスライダーの高さを変更、スタイルをボックスにしたことで幅いっぱいに広がる設定が失われたので GUILayout.ExpandWidth を設定します。

GUILayout.HorizontalSlider (value, leftValue, rightValue,
    "box", "box", GUILayout.Height (40), GUILayout.ExpandWidth (true));

これで大きなスライダーで、つまみ部分がつかみやすくなりました。

目盛を付ける

簡素なものですが目盛を付けておおよそどのあたりにあるかを確認できるようにします。*3

var timeLength = timeControl.maxTime - timeControl.minTime; //時間の長さ
var gridline = timeLength * 2; //0.5目盛ごと
var sliderRect = new Rect(lastRect); //タイムライン Slider の Rect

for (int i = 1; i < gridline; i++) {

    var x = (sliderRect.width / gridline) * i;
    x += 4f - 1.5f * (i - 1);

    Handles.DrawLine (
        new Vector2 (sliderRect.x + x, sliderRect.y),
        new Vector2 (sliderRect.x + x, sliderRect.y + sliderRect.height));
    Handles.Label (
        new Vector2 (sliderRect.x + x - 10, sliderRect.y - 10),
        (timeLength / gridline * i).ToString ("0.0"));
}

目盛(ライン)は Handles クラスを使用して描画します。また、Handles.Label というラベルを描画する API があり、これを使うと Vector2 の座標のみでラベルを描画します。このように、Handles は普段 Scene ビューで描画するために用意されたものですが、EditorWindow やインスペクターの GUI 描画としても扱うことが可能です。

下側にあるものは別途実装したもの。真ん中が FloatField なので値を入力できる。

図21.6: 下側にあるものは別途実装したもの。真ん中が FloatField なので値を入力できる。

矢印キーで時間移動

最後に矢印キーで時間を調整できるようにしてみます。今回は 0.01 秒刻みにします。

if (Event.current.type == EventType.KeyDown) {

    //再生中であれば一時停止させる
    timeControl.Pause ();

    if (Event.current.keyCode == KeyCode.RightArrow)
      timeControl.currentTime += 0.01f;

    if (Event.current.keyCode == KeyCode.LeftArrow)
      timeControl.currentTime -= 0.01f;

    GUI.changed = true;
    Event.current.Use ();
    Repaint ();
}

時間が変化したことによって GUI が変化する(しなければならない)ので、GUI.changed を true にして通知します。そうすることで、EditorGUI.BeginChangeCheck で監視しているイベントを発火できます。

そして必ず Event.current.Use (); を呼び出してください。矢印キーで時間移動をさせ、GUI を更新したのでイベントを1つ消費したことになります。すでに消費したイベントでその後のイベント処理を行おうとすると何かしらの不具合が生じてきます。なので「すでにイベントを消費した(Used)。なので今後のイベント処理は無視するようにする」ようにしなければいけません。

21.11 再生リスト

あまり説明することでもありませんが、再生リストには各パーティクルのトグル(見た目はボタン)があり、オン/オフをすることで再生するか否かを選択できます。

playlist [key] = GUILayout.Toggle (
                     playlist [key], key.name,
                     EditorStyles.miniButton,
                     GUILayout.MaxWidth (position.width / 3));

[*1] 子要素の SubEmitter である ParticleSystem は再生される

[*2] 本当はもっとリッチに作りたいのですが説明するために簡素なものとしています。

[*3] パッと見てわかればいい設計になっているので、メモリの位置が正確ではないです。ごめんなさい!

第20章 Overwriter 第22章 SpriteAnimationPreview(スプライト一覧の表示)