第23章 SpriteAnimationPreview(スプライトアニメーション) - エディター拡張入門

第23章 SpriteAnimationPreview(スプライトアニメーション)

スプライトを再生するアニメーションクリップを作成した場合に、インスペクターのプレビュー画面でスプライトアニメーションの再生を行う仕組みを実装します。


デフォルトの AnimationClip のプレビュー

AnimationClip のインスペクターは3D モデルのためのアニメーションが基準となっており、プレビュー画面も3D アニメーションしか再生できず、2D アニメーションでは使用できません。

3D アニメーションのプレビューがデフォルト

図23.1: 3D アニメーションのプレビューがデフォルト

インスペクターをカスタマイズして、2D のアニメーションにも対応できるようにしていきます。

23.1 カスタムエディター

既存の AnimationClip のカスタムエディター、AnimationClipEditor をオーバーライドする形で新たにカスタムエディターを作成します。今回作成するカスタムエディターの名前は SpriteAnimationClipEditor とします。

まずは以下のようなクラスを作成します。複数選択した場合でも動作するように CanEditMultipleObjects 属性をつけましょう。

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(AnimationClip), CanEditMultipleObjects)]
public class SpriteAnimationClipEditor : Editor
{

}

するとインスペクターの表示が 図23.2 のようになります。

インスペクターで表示できるすべてのものが表示されている状態

図23.2: インスペクターで表示できるすべてのものが表示されている状態

これはすでに Unity 側の AnimationClipEditor で実装されていた OnInspectorGUI メソッドが適用されなくなったためです。カスタマイズされていないの状態で表示されます。

インスペクターの表示はデフォルトのままにしたい

今回はプレビュー画面のみを変更したいので OnInspectorGUI の部分が変更されてしまうのは不本意です。なので、メソッドをオーバーライドしない限りはベースとなる Editor オブジェクト(カスタムエディターで使用するもの)を流用するための OverrideEditor クラスを作成してみましょう。

public abstract class OverrideEditor : Editor
{
    readonly static BindingFlags flags =
        BindingFlags.NonPublic | BindingFlags.Instance;

    readonly MethodInfo methodInfo =
        typeof(Editor).GetMethod("OnHeaderGUI", flags);

    private Editor m_BaseEditor;
    protected Editor baseEditor
    {
        get { return m_BaseEditor ?? (m_BaseEditor = GetBaseEditor()); }
        set { m_BaseEditor = value; }
    }

    protected abstract Editor GetBaseEditor();


    public override void OnInspectorGUI()
    {
        baseEditor.OnInspectorGUI();
    }

    // ... 以下 GetInfoString、OnPreviewSettings というようにカスタムエディターで使用できるメソッド群が列挙する
    // ただし、DrawPreview、OnPreviewGUI、OnInteractivePreviewGUIをすべてオーバーライドしてしまうと挙動が変更されてしまうので注意すること
}

先ほど作成した SpriteAnimationClipEditor の派生クラスを Editor から OverrideEditor に変更します。

[CustomEditor(typeof(AnimationClip)), CanEditMultipleObjects]
public class SpriteAnimationClipEditor : OverrideEditor
{
    protected override Editor GetBaseEditor()
    {
        Editor editor = null;
        var baseType = Types.GetType("UnityEditor.AnimationClipEditor", "UnityEditor.dll");
        CreateCachedEditor(targets, baseType, ref editor);
        return editor;
    }
}

こうすることで従来通りの表示となります。

Sprite の取得

第22章 の 「22.3 AnimationClip が参照しているスプライトを取得する」 と同じ実装でスプライトを取得します。

private Sprite[] GetSprites(AnimationClip animationClip)
{
    var sprites = new Sprite[0];

    if (animationClip != null)
    {
        var editorCurveBinding = EditorCurveBinding.PPtrCurve("", typeof(SpriteRenderer), "m_Sprite");

        var objectReferenceKeyframes = AnimationUtility.GetObjectReferenceCurve(animationClip, editorCurveBinding);

        var _sprites = objectReferenceKeyframes
            .Select(objectReferenceKeyframe => objectReferenceKeyframe.value)
            .OfType<Sprite>();

        foreach (var sprite in _sprites)
        {
            AssetPreview.GetAssetPreview(sprite);
        }
        sprites = _sprites.ToArray();
    }
    return sprites;
}

この取得したスプライトをアニメーション再生ボタンを押すことで、プレビュー画面でスプライトアニメーションを行うようにします。

まずは1つのスプライトをプレビュー画面に表示してみましょう。

public override bool HasPreviewGUI()
{
                return true;
}

public override void OnInteractivePreviewGUI(Rect r, GUIStyle background)
{
                //スプライトがなければ通常(3D)のプレビュー画面にする
                if (sprites.Length != 0)
                {
                                var texture = AssetPreview.GetAssetPreview(sprites[0]);
                                EditorGUI.DrawTextureTransparent(r, texture, ScaleMode.ScaleToFit);
                }
                else
                                baseEditor.OnInteractivePreviewGUI(r, background);
}
スプライトが表示されるようになった

図23.3: スプライトが表示されるようになった

スプライトアニメーションの再生ボタンを作成する

3D の時と同じくプレビュー描画内のところに再生ボタンを作成すると見栄えは良くなりそうですが、今回は OnPreviewSettings 内に再生ボタンを作成します。

右上の部分が OnPreviewSettings で実装できる部分

図23.4: 右上の部分が OnPreviewSettings で実装できる部分

OnPreviewSettings では GUILayout を使用できます。早速再生ボタンを表示してみましょう。再生ボタンは 再生 [ss07] と一時停止 [ss08] というように「オン/オフ」の状態が存在するので GUILayout.Button ではなく GUILayout.Toggle を使用します。

private bool isPlaying = false;

public override void OnPreviewSettings()
{
                var playButtonContent = EditorGUIUtility.IconContent("PlayButton");
                var pauseButtonContent = EditorGUIUtility.IconContent("PauseButton");
                var previewButtonSettingsStyle = new GUIStyle("preButton");
                var buttonContent = isPlaying ? pauseButtonContent : playButtonContent;
                isPlaying = GUILayout.Toggle(isPlaying, buttonContent, previewButtonSettingsStyle);
}
右端に再生ボタンができた

図23.5: 右端に再生ボタンができた

次は時間を管理する「TimeControl」クラスを作成します。

時間を管理する「TimeControl」クラスを作成する

今回の用途にあった時間を管理するクラスを作成します。時間の更新処理は EditorApplication.update を使って行います。

public class TimeControl
{
    public bool isPlaying { get; private set; }
    private float currentTime { get; set; }
    private double lastFrameEditorTime { get; set; }
    public float speed { get; set; }

    public TimeControl()
    {
        speed = 1;
        EditorApplication.update += Update;
    }

    public void Update()
    {
        if (isPlaying)
        {
            var timeSinceStartup = EditorApplication.timeSinceStartup;
            var deltaTime = timeSinceStartup - lastFrameEditorTime;
            lastFrameEditorTime = timeSinceStartup;
            currentTime += (float)deltaTime * speed;
        }
    }

    public float GetCurrentTime(float stopTime)
    {
        return Mathf.Repeat(currentTime, stopTime);
    }

    public void Play()
    {
        isPlaying = true;
        lastFrameEditorTime = EditorApplication.timeSinceStartup;
    }

    public void Pause()
    {
        isPlaying = false;
    }
}

再生する

TimeControl を使用して再生を行うためのトリガーは以下のように実装します。

private void DrawPlayButton()
{
                var playButtonContent = EditorGUIUtility.IconContent("PlayButton");
                var pauseButtonContent = EditorGUIUtility.IconContent("PauseButton");
                var previewButtonSettingsStyle = new GUIStyle("preButton");
                var buttonContent = timeControl.isPlaying ? pauseButtonContent : playButtonContent;

                EditorGUI.BeginChangeCheck();

                var isPlaying =
                                GUILayout.Toggle(timeControl.isPlaying,
                                                                                                                buttonContent, previewButtonSettingsStyle);

                if (EditorGUI.EndChangeCheck())
                {
                                if (isPlaying) timeControl.Play();
                                else timeControl.Pause();
                }
}

再生するスプライトを取得し表示する

現在どのスプライトを再生すべきかは AnimationClip の frameRate (フレームレート) と AnimationClipSettings にある stopTime を使って導きます。

var currentSpriteNum = Mathf.FloorToInt(timeControl.GetCurrentTime(settings.stopTime) * settings.frameRate);

Sprite の保持の仕方などは省いていますが、実際に使用すると以下のようになります。

public override void OnInteractivePreviewGUI(Rect r, GUIStyle background)
{
                SpriteAnimationSettings settings;

                if (dic.TryGetValue(target, out settings))
                {
                                var currentSpriteNum =
                                                Mathf.FloorToInt(timeControl.GetCurrentTime(settings.stopTime)
                                                                                                                                                                                                                                                        * settings.frameRate);
                                var sprite = settings.sprites[currentSpriteNum];
                                var texture = AssetPreview.GetAssetPreview(sprite);

                                if (texture != null)
                                                EditorGUI.DrawTextureTransparent(r, texture, ScaleMode.ScaleToFit);
                }
                else
                                baseEditor.OnInteractivePreviewGUI(r, background);
}

時間スピードを調整するスライダーを作成する

再生ボタンと同じ所に時間を n 倍速させるスライダーを作成します。

private void DrawSpeedSlider()
{
                var preSlider = new GUIStyle("preSlider");
                var preSliderThumb = new GUIStyle("preSliderThumb");
                var preLabel = new GUIStyle("preLabel");
                var speedScale = EditorGUIUtility.IconContent("SpeedScale");

                GUILayout.Box(speedScale, preLabel);
                timeControl.speed =
                                GUILayout.HorizontalSlider(timeControl.speed, 0, 10, preSlider, preSliderThumb);
                GUILayout.Label(timeControl.speed.ToString("0.00"), preLabel, GUILayout.Width(40));
}
再生ボタンの隣にスライダーが追加された

図23.6: 再生ボタンの隣にスライダーが追加された

完成

プレビュー画面でスプライトアニメーションの再生を確認できるようになりました。

再生ボタンを押すとスプライトアニメーションが再生される

図23.7: 再生ボタンを押すとスプライトアニメーションが再生される

CanEditMultipleObjects を実装すれば複数のスプライトアニメーションを同時に再生可能です。

23.2 さらに機能を追加していく

さらに改良したものを、Sprite Animation Preview*1 としてアセットストアで配布しています。配布するまでに行った細かな実装について説明していきます。

AssetPreview.GetAssetPreview のテクスチャはサイズが小さい

もともとは、AssetPreview.GetAssetPreview はプロジェクトビューなどで表示するための機能です。プロジェクトビューではそこまで高解像度なテクスチャサイズは必要としていません。なので強制的に 128x128 へとリサイズされます。

「Sprite Animation Preview」では、品質面でプレビューの表示に AssetPreview.GetAssetPreview を使用せず、SpriteEditor から直接プレビュー画像を取得するようにしました。

Editor オブジェクトを動的に作成

Editor オブジェクトは、任意のタイミングでユーザーが作成できます。

Unity 標準で使用されている Sprite オブジェクトのための SpriteInspector を作成します。

private List<Editor> GetSpriteEditors(params Sprite[] sprites)
{
    var type = Types.GetType("UnityEditor.SpriteInspector", "UnityEditor.dll");
    var editors = new List<Editor>();

        foreach (var sprite in sprites)
    {
                Editor _editor = Editor.CreateEditor(sprite, type);

        if (_editor != null)
            editors.Add(_editor);
    }

    return editors;
}

プレビュー用テクスチャの取得

プレビューのテクスチャは RenderStaticPreview で取得できます。

var editor = spriteEditors[i];
var previewTexture = editor.RenderStaticPreview("", null,
                        (int)previewRect.width,
                        (int)previewRect.height);

動的に作成したオブジェクトの破棄を忘れないこと

必ず、作成した Editor オブジェクトを破棄するコードを実装するようにしてください。破棄するタイミングは OnDisable など、オブジェクトが必要なくなったタイミングで呼び出します。

public void OnDisable()
{
    foreach (var spriteEditor in spriteEditors)
    {
        Object.DestroyImmediate(spriteEditor);
    }
}
破棄しないと作成した Editor オブジェクトが残り続けてしまい、メモリを圧迫する

図23.8: 破棄しないと作成した Editor オブジェクトが残り続けてしまい、メモリを圧迫する

また、プレビューで使用したテクスチャも破棄するようにします。

テクスチャが残り続けてしまい、メモリを圧迫する

図23.9: テクスチャが残り続けてしまい、メモリを圧迫する

第22章 SpriteAnimationPreview(スプライト一覧の表示) 第24章 シーンアセットにスクリプトをアタッチ