個人ゲーム開発スタジオ

個人開発しているゲームの情報発信をします。

Unity Editor拡張 値変更可能なプログレスバー(SlidableProgressbar)

AttributeとPropertyDrawerを用いて下記のようなInspector表示をできるようにしました。

完成品サンプル

SlidableProgressbar

備忘録ついでに作り方を解説していきます。

attributeクラスの作成

[AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple = true)]
public class SlidableProgressbarAttribute : PropertyAttribute
{
    public string label = "";
    public float maxValue = 1f;
    public string maxValueProperty;
    public EditorColor barColor = EditorColor.Green;
    public EditorColor backgroundColor = EditorColor.Gray;

    public Dictionary<string, bool> mouseState = new Dictionary<string, bool>();

    public SlidableProgressbarAttribute
    (
        float maxValue = 1f,
        string label = "",
        string maxValueProperty = null,
        EditorColor barColor = EditorColor.Unknown,
        EditorColor backgroundColor = EditorColor.Unknown
    )
    {
        this.maxValue = maxValue;
        this.label = label;
        this.maxValueProperty = maxValueProperty;
        this.barColor = barColor == EditorColor.Unknown ? this.barColor : barColor;
        this.backgroundColor = backgroundColor == EditorColor.Unknown ? this.backgroundColor : backgroundColor;
    }
}

Attributeで引数を持たせて見た目のカスタマイズをできるようにしています。
barColorやbackgroundColorはそのままColorを引数としたいのですが、Attributeの引数では指定できないのでEnumを引数とし、別でEnumからColorに変換するクラスを用意します。

●propertydrawerの作成

inspectorに実際に表示する部分を作ります。
ここではポイントとなる部分だけ抜粋してますが、記事の末尾に全体を載せておきます。

やっているのは、下記の3つですね。
GUI.Boxを使ってプログレスバーの見た目を背景とバー部分に分けて描画. ②Event.currentを使用してUnityEditor上でのマウスクリックやクリック座標を取得し、値を変更させる。
③好みの見た目になるようにデコる。

[CustomPropertyDrawer(typeof(SlidableProgressbarAttribute))]
[CustomPropertyDrawer(typeof(IntSlidableProgressbarAttribute))]
public class SlidableProgressbarDrawer : PropertyDrawer
{

    Type _valueType { get => attribute.GetType(); }
    SlidableProgressbarAttribute _attribute { get => (SlidableProgressbarAttribute)attribute; }
    SerializedProperty _targetProperty;

    const float _barSizeRate = 1.5f;
    static Vector2 NullVector = new Vector2(-1, -1);


    public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label)
    {
        〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜    
  labelRect,backRect,barRectなどの計算   
        〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜       
 
        // backRect内のどこをマウスでクリック・ドラッグしたかによって0〜1のVector2を返す。
   // マウス状態の保持のためにpropertyPathも渡しておく
   // 今回は対応していないが、縦向きのプログレスバーを表示することもあるかもしれないのでVector2で受けておく
        Vector2 delta = GetProgress(backRect, property.propertyPath);

        // ラベルの描画
   // LabelString() は_attibute.labelまたはフィールド名を取得する関数
        GUI.Label(labelRect, $"{LabelString()}", CustomStyle.TextStyle(TextAnchor.MiddleLeft));

        // Progressbarの描画(int型)
   // float型もやることは同じ
        if (_valueType == typeof(IntSlidableProgressbarAttribute))
        {
     // マウスのイベントが発生かつ有効値であるかどうかを判定(NullVectorかどうかのチェックだけでも問題ないはず)
     // 有効値なら値を変更
            if (Event.current.isMouse && delta != NullVector)
            {
                property.intValue = Mathf.RoundToInt(delta.x * MaxValueInt());
            }
     // プログレスバーの背景
            GUI.Box(backRect, GUIContent.none, CustomStyle.ColorBox(_attribute.backgroundColor.GetColor()));
     // プログレスバーの本体
            if (barRect.width != 0)
                GUI.Box(barRect, GUIContent.none, CustomStyle.ColorBox(_attribute.barColor.GetColor()));
     // プログレスバーのラベル(現在値/最大値 )
            GUI.Label(backRect, $"{property.intValue}/{MaxValueInt()}", TextStyle());
        }
        〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜       
        〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜       

    }

   private Vector2 GetProgress(Rect targetRect, string propertyPath)
    {

        if (!Event.current.isMouse) return NullVector;

        switch (Event.current.type)
        {
            case EventType.MouseDown:
                SetMouseState(propertyPath, IsClickControl(targetRect));
                break;
            case EventType.MouseUp:
                if (Event.current.button == 0)
                {
      // _attribute.mouseStateにマウスダウンしていることを保持させる。
                    SetMouseState(propertyPath, false);
                }
                break;
        }


        if (GetMouseState(propertyPath))
        {
            return new Vector2(
                Math.Min(1f, MathF.Max(0f, (Event.current.mousePosition.x - targetRect.x) / targetRect.width)),
                Math.Min(1f, MathF.Max(0f, (Event.current.mousePosition.y - targetRect.y) / targetRect.height))
                );
        }
        else
        {
            return NullVector;

        }

    }

●詰まった点

PropertyDrawerを使用すると、Drawerが呼ばれる度に変数が初期化されるようです。
そのため、SlidableProgressbarAttributeでマウスクリックされた状態かどうかを保持するDictionary(mouseState)を用意しています。
PropertyPathをキーにすることでリスト表示した時にうまいこと表示してくれるようになります。

●使用例

 使用したい変数に下記のようなAttribute属性をつけるだけです。

public class ProgressBarSample : MonoBehaviour
{
    //Property から最大値を取得するfloat型
    [SlidableProgressbar(maxValueProperty = "floatMaxValue")]
    [SerializeField] float floatProgress;

    // 固定で最大値をしていするint型
    [IntSlidableProgressbar(maxValue = 5)]
    [SerializeField] int intProgress;
    // float型の最大値。Serialize可能な状態にしておく必要がある。
    [SerializeField] float floatMaxValue = 10;
}

作っておいてなんですが、通常はSliderを使えば機能的には十分だと思います。
でもやっぱり自分好みの見た目や少しでもみやすいようにしておくことでモチベ維持や開発の効率化につながるので積極的にEditor拡張していきたいですね。