Unityエディター拡張のカスタムプレビュー

はじめに

こんにちは、ソーシャルゲーム事業部のUnityエンジニアのです。
この記事はカヤックUnityアドベントカレンダー2018の16日目の記事になります。

今日の記事ではUnityエディター拡張のカスタムプレビューを紹介していきたいと思います。

カスタムプレビュー

TextureやMaterialなどプレビューできるものを選択すると、インスペクターのプレビューウィンドウで中身を確認できます。 プレビューできないものは、エディター拡張を使ってカスタムプレビューを作れば、プレビューができるようになります。

ここから、簡単なSpriteプレビュアーを作りながら、カスタムプレビューを作るためによく使う関数を見てみましょう

最低限の関数

コンポーネントのコード

using UnityEngine;

public class SpritePreviewer : MonoBehaviour
{
    public Sprite sprite;
}

カスタムエディターのコード。Editorフォルダ内へ入れます。
この中で注目してほしいのはOnPreviewGUIです。描画の処理はこの関数に書きます

[CustomEditor(typeof(SpritePreviewer))]
public class SpritePreviewerEditor : Editor
{
    private GUIContent _title = new GUIContent("スプライトプレビュアー");

    // プレビューウィンドウを表示するかどうか
    public override bool HasPreviewGUI()
    {
        return true;
    }

    // 名前通り、プレビューウィンドウ名を設定する関数
    public override GUIContent GetPreviewTitle()
    {
        return _title;
    }

    // プレビューウィンドウで描画させたい内容はここで書く
    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
        var previewer = target as SpritePreviewer;
        GUI.DrawTexture(r, AssetPreview.GetAssetPreview(previewer.sprite));
    }
}

SpritePreviewerコンポーネントをGameObjectに追加して、Sprite画像をコンポーネントにつけたら、こうなりました。
2018-11-15 15 17 41

OnPreviewSettings

AnimationClipのプレビューみたいに、動画の再生や再生速度の調整などのUIはOnPreviewSettings関数で実装されています。 例としては、スプライトを半分のサイズで描画する切り替えボタンを作ります。

   private bool _halfSize = false;

    // プレビューウィンドウで描画させたいものはここで書く
    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
        var previewer = target as SpritePreviewer;
        var rect = _halfSize ? new Rect(r.x, r.y, r.width / 2, r.height / 2) : r;
        GUI.DrawTexture(rect, AssetPreview.GetAssetPreview(previewer.sprite));
    }

    // プレビューウィンドウのヘッダーバーをカスタムする関数
    public override void OnPreviewSettings()
    {
        _halfSize = GUILayout.Toggle(_halfSize, "0.5x");
    }
off on
2018-11-19 18 09 53 2018-11-19 18 10 10

CustomPreviewAttributeとObjectPreview

新コンポーネントではなく、SpriteRendererに直接プレビューウィンドウを追加すればいいじゃないかと思って、Typeを変更してみたら

//[CustomEditor(typeof(SpritePreviewer))]
[CustomEditor(typeof(SpriteRenderer))]
public class SpritePreviewerEditor : Editor
{
    ...

    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
//     var previewer = target as SpritePreviewer;
        var previewer = target as SpriteRenderer;
        var rect = _halfSize ? new Rect(r.x, r.y, r.width / 2, r.height / 2) : r;
        GUI.DrawTexture(rect, AssetPreview.GetAssetPreview(previewer.sprite));
    }
}

2018-11-19 19 41 35
なんか、インスペクターが変わりました。

何故かと言うと、CustomEditorアトリビュートはプレビューだけではなく、エンジン内部で実装されたインスペクターなどの要素も上書きするからです。

一般的に、上書きしないようにそのエディタークラスを継承すればいいですけど、Unity内部のSpriteRendererEditorはinternalで継承できないです。

今回はプレビューのカスタムだけ求めるので、CustomPreview*1ObjectPreviewを使えば解決できます。

// [CustomEditor(typeof(SpritePreviewer))]
[CustomPreview(typeof(SpriteRenderer))]

// CustomPreviewを使うために、ObjectPreviewを継承しなければいけない
// public class SpritePreviewerEditor : Editor
public class SpritePreviewerEditor : ObjectPreview
{
    ...
}
変更前 CustomPreview
2018-11-19 19 41 35 2018-11-19 18 33 30

これでSpriteRendererでスプライトのプレビューができるようになりました。

最終のコードはこうなります

using UnityEngine;
using UnityEditor;

[CustomPreview(typeof(SpriteRenderer))]
public class SpritePreviewerEditor : Editor
{
    private GUIContent _title = new GUIContent("スプライトプレビュアー");
    private bool _halfSize = false;

    // プレビューウィンドウを表示するかどうか
    public override bool HasPreviewGUI()
    {
        return true;
    }

    // 名前通り、プレビューウィンドウ名を設定する関数
    public override GUIContent GetPreviewTitle()
    {
        return _title;
    }

    // プレビューウィンドウで描画させたいものはここで書く
    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
        var previewer = target as SpritePreviewer;
        var rect = _halfSize ? new Rect(r.x, r.y, r.width / 2, r.height / 2) : r;
        GUI.DrawTexture(rect, AssetPreview.GetAssetPreview(previewer.sprite));
    }

    // プレビューウィンドウのヘッダーバーをカスタムする関数
    public override void OnPreviewSettings()
    {
        _halfSize = GUILayout.Toggle(_halfSize, "0.5x");
    }
}

使用例

NGUIのUISpriteとUISpriteAnimationに基づいて実装したUISpriteAnimationプレビュアーを紹介します。 UISpriteAnimationはコマアニメを作るためのコンポーネントですが、エディター再生中しか動きを見れないので、いつでも見れるようにプレビュアーを作りました

11 -21-2018 11-29-08

サンプルコード

using UnityEngine;
using UnityEditor;
using System.Reflection;
using System.Collections.Generic;

[CustomEditor(typeof(UISprite))]
// UISpriteInspectorのOnPreviewGUIを利用したいので継承する
public class UISpriteAnimationPreviewer : UISpriteInspector
{
    AnimationSetting _animSetting = null;

    bool _isPlaying = false;
    bool _hasAnimation = false;
    float _speedScale = 1f;

    protected override void OnEnable()
    {
        base.OnEnable();
        _hasAnimation = false;

        var targetSprite = target as UISprite;
        var spriteAnim = targetSprite.GetComponent<UISpriteAnimation>();
        if (spriteAnim != null)
        {
            _animSetting = new AnimationSetting(spriteAnim);
            _hasAnimation = true;
        }
    }

    // プレビューウィンドウで描画させたい内容はここで書く
    public override void OnPreviewGUI(Rect rect, GUIStyle background)
    {
        var t = target as UISprite;
        AnimationSetting setting = _animSetting;

        if (!Application.isPlaying && _hasAnimation)
        {
            var spriteAnim = setting.anim;

            BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
            var field = typeof(UISpriteAnimation).GetField("mSpriteNames",flags);
            var spriteNames = field.GetValue(spriteAnim) as List<string>;

            // UISpriteAnimationのUpdate関数を参考にして、
            // 何枚目のUISpriteを表示するか経過時間で計算する
            setting.delta += ((float)EditorApplication.timeSinceStartup - setting.lastTime) * _speedScale;
            setting.lastTime = (float)EditorApplication.timeSinceStartup;

            if (spriteNames.Count > 0)
            {
                if (_isPlaying)
                {
                    float rate = 1f / spriteAnim.framesPerSecond;
                    if (rate < setting.delta)
                    {
                        setting.delta = Mathf.Repeat(setting.delta, rate);
                        setting.index++;
                    }
                    setting.index %= spriteNames.Count;
                    // プレビューウィンドウで表示するために、UISpriteのspriteNameを変える
                    // 保存する時spriteNameの差分が出るかもしれない
                    t.spriteName = spriteNames[setting.index];
                }

                // どの画像を表示しているか分かるように、画像名を表示する
                EditorGUI.DropShadowLabel(rect, spriteNames[setting.index]);
                rect.height -= 15;
            }
            else
            {
                return;
            }
        }

        // UISpriteのプレビューを利用して、spriteNameで指定しているUISpriteを描画させる
        base.OnPreviewGUI(rect, background);
    }

    // プレビューウィンドウのヘッダーバーをカスタムする関数
    public override void OnPreviewSettings()
    {
        base.OnPreviewSettings();
        if (!_hasAnimation)
        {
            return;
        }

        // 再生するボタン
        var playButton = EditorGUIUtility.IconContent("preAudioPlayOn");
        var pauseButton = EditorGUIUtility.IconContent("preAudioPlayOff");

        EditorGUI.BeginChangeCheck();
        _isPlaying = GUILayout.Toggle(_isPlaying, _isPlaying ? playButton : pauseButton, (GUIStyle)"preButton");
        if (EditorGUI.EndChangeCheck())
        {
            _animSetting.lastTime = (float)EditorApplication.timeSinceStartup;
        }

        // AnimationClipのような再生速度を制御するUI
        // 実際の作業はUISpriteAnimationのFramerateで調整しているので、ここは練習だけ^_^
        var speedScale = EditorGUIUtility.IconContent("SpeedScale", "Speed Scale");
        if (GUILayout.Button(speedScale, (GUIStyle)"preButton"))
        {
            _speedScale = 1;
        }
        _speedScale = GUILayout.HorizontalSlider(_speedScale, 0f, 5f, (GUIStyle)"preSlider", (GUIStyle)"preSliderThumb");

        GUILayout.Box(_speedScale.ToString("0.000"), new GUIStyle("preLabel"));
    }

    public override bool HasPreviewGUI()
    {
        return true;
    }

    //常に再描画される必要があるかどうか
    public override bool RequiresConstantRepaint()
    {
        return _isPlaying;
    }

    private class AnimationSetting
    {
        public int index;
        public float delta;
        public float lastTime;
        public UISpriteAnimation anim;

        public AnimationSetting(UISpriteAnimation anim)
        {
            index = 0;
            delta = 0;
            lastTime = (float)EditorApplication.timeSinceStartup;
            this.anim = anim;
        }
    }
}

CanEditMultipleObjectsEditor.targetsを利用すれば、下のGIFのように複数のUISpriteAnimationのプレビューもできますが、 実際の作業に使われないし、実現するためにNGUIコードの改造も少し必要なので、今回割愛します。 11 -21-2018 11-19-11

おわりに

ということで、Unityエディター拡張のカスタムプレビューを紹介しました。
上記の例に限らず、パーティクルのプレビューとか、AnimationClipにあるSpriteアニメーションのプレビューとか、3Dモデルのプレビューとか、複数のプレビューを使って各アニメーションのプレビューとか、いろんなことができるでしょう。

明日は浅利さんによる「HTC Vive で両手を使った transform 操作を実現する」の話になります。

*1:CustomPreviewを使えば、1つのObjectで複数のプレビューを作ることができます。