読者です 読者をやめる 読者になる 読者になる

チリペヂィア

リンクフリー。サンプルコードなどは関連記事内でライセンスについて明示されない限り商用利用なども自由に行って構いませんが、自己責任でお願いします。またこれら日記内容の著作権自体は放棄していません。引用部分については引用元の権利に従ってください。

Unityでフィールド属性のRangeAttributeにNaNとかInfinityとか突っ込むと

注意:この記事は古い記事で、一度間違って削除したり復帰させたりしています。なんかいろいろ怪しいところがあるのが発覚しているので、こちらのエントリーあたりから、問題点含めてじわじわ仕切りなおす予定です。

public class Temp : MonoBehaviour {
    [Range(0f, (float)int.MaxValue)]
    public int test;
}

インスペクタ上にメンバを表示する時、範囲を制限しつつ、数値をスライダーで設定できる便利なRangeAttribute。
でもmin/max片方だけ値を設定するというのは出来ません。例えば絶対値に制限したくてmin=0、max=NaN/Infinityに…するとどうでしょう。スライダーがNaNとInfinityだけを行ったり来たりする仕様っぽいのです。NaNを0より小さいとするならminの範囲外になっちゃうとも言えます。

そもそも「PositiveInfinityより小さい最大の浮動小数点」は、精度問題など諸々あるのでスライダー操作するには微妙です。だったらムリにGUIでSlider使わなくても良いので、正直、こういうケースでは、フィールド名のところでグリグリ増減できるだけの普通の数値入力に戻るんじゃないかなぁと予想していたんですが、なぜかNaN-Infinityをスイッチするスライダーが表示されました。ありゃりゃ、うぅーむ。わからなくはないものの事故るぞこれは。

しかし、そもそもRangeAttributeのベースたるPropertyAttributeは自前で継承カスタマイズしてナンボという性格に見え、
http://docs.unity3d.com/Documentation/ScriptReference/PropertyDrawer.html
といったページを見る限り「気に入らんのやったら勝手に作ったらんかい」というのがUnityの中の人の意向なんだろうか。

というわけで、リファレンスでRangeAttributeを再発明しているのに誘われるままを参考に、こんな具合にBetterRangeAttributeクラスを作ってみます。

  • floatを処理するときmin/maxにNaN/Infinityが混じってたら通常のテキストボックスタイプに戻す
    • 通常のテキストボックスモードで、NaN/Infinityがセットされてる側は範囲制限チェックを行わない。
    • 通常のテキストボックスモードで、片方にNaN/Infinityがセットされて、もう片方の値は有効な時、有効側の範囲制限はチェックを行う。
  • intを処理する時、min/maxのNaNはそれぞれint.MinValue/int.MaxValueとして解釈
  • intを処理する時、min/maxのそれぞれが、infinityを含めint.MinValue〜int.MaxValueの範囲をオーバーする時はint.MinValue〜int.MaxValueの範囲に丸める
  • メモ
    • SerializedProperty.propertyTypeはただ単に対応するフィールドの型定義情報っぽいです(インスタンスをたどってバリバリに動的な型解決みたいなのを仕込みたければ自前でやるしかないのかな???)。

要するにNaNやInfinityが無制限指定として機能する作戦。

/// <summary>プロパティの範囲属性</summary>
public class BetterRangeAttribute : PropertyAttribute
{
    public readonly float min, max;
    
    /// <summary>コンストラクタ</summary>
    public BetterRangeAttribute(float min, float max)
    {
        if (float.IsNaN(min) || float.IsNaN(max))
        {
            this.min = min;
            this.max = max;
        }
        else
        {
            this.min = System.Math.Min(min, max);
            this.max = System.Math.Max(min, max);
        }
    }

    /// <summary>minをint型に解釈して返します。NaNとint.MinValue以下のminはint.MinValue、int.MaxValue以上のminはint.MaxValueに切り詰められます。</summary>
    public int integralMin
    {
        get
        {
            if (float.IsNaN(min) || (min < ((float)int.MinValue))) return int.MinValue;
            if (((float)int.MaxValue) < min) return int.MaxValue;
            return (int)min;
        }
    }

    /// <summary>maxをint型に解釈して返します。int.MinValue以下のmaxはint.MinValue、NaNとint.MaxValue以上のmaxはint.MaxValueに切り詰められます。</summary>
    public int integralMax
    {
        get
        {
            if (float.IsNaN(max) || (((float)int.MaxValue) < max)) return int.MaxValue;
            if (max < ((float)int.MinValue)) return int.MinValue;
            return (int)max;
        }
    }
}

リファレンスによると下記のPropertyDrawerは「Editorフォルダに入れてね」とのこと。

[CustomPropertyDrawer(typeof(BetterRangeAttribute))]
public class BetterRangeDrawer : PropertyDrawer
{
    public override void OnGUI (Rect position, SerializedProperty prop, GUIContent label)
    {
        BetterRangeAttribute range = attribute as BetterRangeAttribute;
        GUIStyle style = UnityEditor.EditorStyles.numberField;
        Rect inner_position = position;
        inner_position.x += style.margin.left;
        inner_position.width -= style.margin.horizontal;
        if (prop.propertyType == SerializedPropertyType.Float)
        {
            if ((range == null) || float.IsNaN(range.min) || float.IsInfinity(range.min) || float.IsNaN(range.max) || float.IsInfinity(range.max))
            {
                float value = EditorGUI.FloatField(inner_position, label, prop.floatValue, style);
                if (range != null)
                {
                    if ((float.IsNaN(range.min) == false) && (float.IsInfinity(range.min) == false) && (value < range.min))
                    {
                        value = range.min;
                    }
                    if ((float.IsNaN(range.max) == false) && (float.IsInfinity(range.max) == false) && (range.max < value))
                    {
                        value = range.max;
                    }
                }
                prop.floatValue = value;
            }
            else
            {
                EditorGUI.Slider(inner_position, prop, range.min, range.max, label);
            }
        }
        else if (prop.propertyType == SerializedPropertyType.Integer)
        {
            int min = int.MinValue, max = int.MaxValue;
            if (range != null)
            {
                min = range.integralMin;
                max = range.integralMax;
            }
            EditorGUI.IntSlider(inner_position, prop, min, max, label);
        }
        else
        {
            // type match is missing
            EditorGUI.LabelField(inner_position, label.text, "Use BetterRange with float or int.");
        }
    }
}

追記 2013.08.20

ちなみにIntSliderで最大値にint.MaxValueを設定すると、インスペクタ上で (int)(((unsigned int)int.MaxValue) + 1) が入力できちゃうバグがあります(IntSliderを0〜int.MaxValueに設定して、最大値側でさらに増加させてみてください@WinXP/Unity4.1.5f1)。とりあえずIntSliderのところはこんな風にすれば一応対処できます。

EditorGUI.IntSlider(inner_position, prop, min, max, label);
if (prop.intValue < min) prop.intValue = min;
if (max < prop.intValue) prop.intValue = max;