特定の範囲内の値を設定する場合は、最小値と最大値の2つの変数を管理しなくちゃいけないのが面倒くさい。
例えば、攻撃するたびに一定の範囲内のダメージがランダム算出される場合とか。
1種類ぐらいならいいのだけど、種類の数が増えてくるとさすがにごちゃごちゃしてくる。
// ああ、変数がどんどん増えていく……。 [SerializeField] private float m_attackMax; [SerializeField] private float m_attackMin; [SerializeField] private int m_elementMax; [SerializeField] private int m_elementMin; [SerializeField] private int m_lifeMax; [SerializeField] private int m_lifeMin;
Vector2
のように、1つの構造体で最小値と最大値を管理できれば少しはすっきりするだろうか。
今回はそんな構造体づくりのお話です。
この記事にはUnity5.6.1f1を使用しています。
必要最低限の機能のみのシンプル設計
では早速作っていきますよー。
まずはシンプルに、最小値と最大値を持つだけの構造体をパパっと作ってしまいます。
int
型のRangeInteger
とfloat
型のRangeFloat
を作っていきます。
RangeInteger
[System.Serializable] public struct RangeInteger { public RangeInteger( int i_value ) { m_minValue = i_value; m_maxValue = i_value; } public RangeInteger( int i_min, int i_max ) { m_minValue = i_min; m_maxValue = i_max; } [SerializeField] private int m_minValue; [SerializeField] private int m_maxValue; public int MinValue { get { return m_minValue; } set { m_minValue = value; } } public int MaxValue { get { return m_maxValue; } set { m_maxValue = value; } } }
RangeFloat
[System.Serializable] public struct RangeFloat { public RangeFloat( float i_value ) { m_minValue = i_value; m_maxValue = i_value; } public RangeFloat( float i_min, float i_max ) { m_minValue = i_min; m_maxValue = i_max; } [SerializeField] private float m_minValue; [SerializeField] private float m_maxValue; public float MinValue { get { return m_minValue; } set { m_minValue = value; } } public float MaxValue { get { return m_maxValue; } set { m_maxValue = value; } } }
んー、ほとんど同じ内容だ。
ジェネリックを使いたいところだけど、ジェネリック使うとUnityエディタ上で表示されないしなぁー……、残念。
しかもint
型の方は、本当はRangeInt
という名前にしようと思ったら、すでにUnityEngine
内に同名とクラスがある始末。
むむむ、先行き不安だ。
これ使ってみるとこんな感じに。
public class TestComponent : MonoBehaviour { [SerializeField] private RangeInteger m_int; [SerializeField] private RangeInteger[] m_ints; [SerializeField] private RangeFloat m_float; [SerializeField] private RangeFloat[] m_floats; } // class TestComponent
配列で使用してもうまく表示されていますね。
最小値と最大値の関係をしっかりと
ただこのままだと、m_minValue
の方に大きい値を、m_maxValue
の方に小さい値を設定できてしまいます。
設定するわけで気を付けれいい話ではあるのですが、構造体側でサポートしてあげるようにしてみます。
RangeInteger
[System.Serializable] public struct RangeInteger { public RangeInteger( int i_value ) { m_minValue = i_value; m_maxValue = i_value; } public RangeInteger( int i_min, int i_max ) { // 小さい方を基準にします。 m_minValue = i_min; m_maxValue = Mathf.Max( i_min, i_max ); } [SerializeField] private int m_minValue; [SerializeField] private int m_maxValue; public int MinValue { get { return m_minValue; } set { m_minValue = Mathf.Min( value, m_maxValue ); } } public int MaxValue { get { return m_maxValue; } set { m_maxValue = Mathf.Max( value, m_minValue ); } } }
RangeFloat
は同じような内容の変更なので省略します。
これでスクリプト上で値を設定する場合は、m_minValue
の方が大きくなることも、m_maxValue
の方が小さくなることもなくなりました。
他にもいろいろな機能をつけたい
最低限の機能は備わったのですが、折角なのでもう少しいろいろな機能を付けていきましょう。
インターフェースを作ってみる
追加したい機能をインターフェースにまとめてみましょう。
今回追加しようと思っているの以下の項目です。
- 中央値
- 最小値と最大値の範囲の長さ
- 最小値と最大値の範囲内のランダム値
スクリプトにおこしてみましょう。
public interface IRange<TValue> where TValue : struct { TValue MinValue { get; set; } TValue MaxValue { get; set; } TValue MidValue { get; } TValue Length { get; } TValue RandomValue { get; } }
これをRangeInteger
とRangeFloat
は継承するようにしましょう。
RangeInteger
[System.Serializable] public struct RangeInteger : IRange<int> { // 中身はそのまま }
RangeFloat
[System.Serializable] public struct RangeFloat : IRange<float> { // 中身はそのまま }
では各機能を組み込んでいきます。
中央値プロパティを追加するよ
最小値と最大値の範囲内の真ん中の値を取得するプロパティを作ってみましょう。
RangeInteger
public int MidValue { get { return m_minValue + ( m_maxValue - m_minValue ) / 2; } }
int
型ため、整数で表せない場合は正確には中央値ではなくなっちゃいますが、こんな感じでしょうか。
RangeFloat
の作りも返り値がfloat
になるだけの同じ式でいけるかと。
範囲の長さプロパティを追加するよ
次に、最小値と最大値の範囲の長さを取得するプロパティを作ってみましょう。
RangeInteger
public int Length { get { return Mathf.Abs( m_maxValue - m_minValue ); } }
長さなので必ず正数になるかな。
RangeFloat
の作りも、やっぱり同じで大丈夫なはず。
ランダム値プロパティを追加するよ
次に、最小値と最大値の範囲の中から、ランダムな数値を取得するプロパティを作ってみましょう。
RangeInteger
public int RandomValue { get { return m_minValue < m_maxValue ? Random.Range( m_minValue, m_maxValue + 1 ) : m_minValue; } }
ランダムの場合はint
型とfloat
型でRandom.Range()
の挙動が違うため計算式を少し変えます。
RangeFloat
public float RandomValue { get { return m_minValue < m_maxValue ? Random.Range( m_minValue, m_maxValue ) : m_minValue; } }
これで大丈夫でしょうか。
欲しかった機能が大体揃いましたね。
エディタも改良しよう
さて、これでスクリプト上で使う際に必要な機能は揃いましたが、せっかくなのでエディタ上の見た目も少し変えていきましょう。
エディタ上にて設定範囲を一列で表示したい
Vector2
などの構造体をエディタ上で見てみると、数値の設定が1列表示されています。
今回作成した構造体も同じような感じにしてみましょう。
RangeEditor
[CustomPropertyDrawer( typeof( RangeInteger ) )] public class RangeIntEditor : RangeEditor { } // class RangeIntEditor [CustomPropertyDrawer( typeof( RangeFloat ) )] public class RangeFloatEditor : RangeEditor { } // class RangeFloatEditor public class RangeEditor : PropertyDrawer { private static readonly GUIContent MIN_LABEL = new GUIContent( "Min" ); private static readonly GUIContent MAX_LABEL = new GUIContent( "Max" ); public override void OnGUI( Rect i_position, SerializedProperty i_property, GUIContent i_label ) { var minProperty = i_property.FindPropertyRelative( "m_minValue" ); var maxProperty = i_property.FindPropertyRelative( "m_maxValue" ); i_label = EditorGUI.BeginProperty( i_position, i_label, i_property ); // プロパティの名前部分を描画。 Rect contentPosition = EditorGUI.PrefixLabel( i_position, i_label ); // MinとMaxの2つのプロパティを表示するので、残りのフィールドを半分こ。 contentPosition.width /= 2.0f; // Rangeを配列でもっている際は、その分インデントが深くなっている。揃えたいので0に。 EditorGUI.indentLevel = 0; // 3文字なら、これぐらいの幅があればいいんじゃないかな。 EditorGUIUtility.labelWidth = 30f; EditorGUI.PropertyField( contentPosition, minProperty, MIN_LABEL ); contentPosition.x += contentPosition.width; EditorGUI.PropertyField( contentPosition, maxProperty, MAX_LABEL ); EditorGUI.EndProperty(); } public override float GetPropertyHeight( SerializedProperty i_property, GUIContent i_label ) { return EditorGUIUtility.singleLineHeight; } } // class RangeEditor
コンパクトに収まりました。
最小値と最大値の関係をしっかりと(エディタ編)
スクリプト上では最小値と最大値の関係はしっかりさせましたが、エディタ上ではまだ設定できてしまいます。
それを何とかしましょう。
今回は最小値を基準として判定を行うように作っていきましょう。
RangeEditor
[CustomPropertyDrawer( typeof( RangeInteger ) )] public class RangeIntEditor : RangeEditor { protected override void ApplyValue( SerializedProperty i_minProperty, SerializedProperty i_maxProperty ) { // 小さい数値を基準にして、大きい数値が小さい数値より小さくならないようにしてみよう。 if( i_maxProperty.intValue < i_minProperty.intValue ) { i_maxProperty.intValue = i_minProperty.intValue; } } } // class RangeIntEditor [CustomPropertyDrawer( typeof( RangeFloat ) )] public class RangeFloatEditor : RangeEditor { protected override void ApplyValue( SerializedProperty i_minProperty, SerializedProperty i_maxProperty ) { // 小さい数値を基準にして、大きい数値が小さい数値より小さくならないようにしてみよう。 if( i_maxProperty.floatValue < i_minProperty.floatValue ) { i_maxProperty.floatValue = i_minProperty.floatValue; } } } // class RangeFloatEditor public class RangeEditor : PropertyDrawer { private static readonly GUIContent MIN_LABEL = new GUIContent( "Min" ); private static readonly GUIContent MAX_LABEL = new GUIContent( "Max" ); public override void OnGUI( Rect i_position, SerializedProperty i_property, GUIContent i_label ) { var minProperty = i_property.FindPropertyRelative( "m_minValue" ); var maxProperty = i_property.FindPropertyRelative( "m_maxValue" ); ApplyValue( minProperty, maxProperty ); i_label = EditorGUI.BeginProperty( i_position, i_label, i_property ); // プロパティの名前部分を描画。 Rect contentPosition = EditorGUI.PrefixLabel( i_position, i_label ); // MinとMaxの2つのプロパティを表示するので、残りのフィールドを半分こ。 contentPosition.width /= 2.0f; // Rangeを配列でもっている際は、その分インデントが深くなっている。揃えたいので0に。 EditorGUI.indentLevel = 0; // 3文字なら、これぐらいの幅があればいいんじゃないかな。 EditorGUIUtility.labelWidth = 30f; EditorGUI.PropertyField( contentPosition, minProperty, MIN_LABEL ); contentPosition.x += contentPosition.width; EditorGUI.PropertyField( contentPosition, maxProperty, MAX_LABEL ); EditorGUI.EndProperty(); } protected virtual void ApplyValue( SerializedProperty i_minProperty, SerializedProperty i_maxProperty ) { } public override float GetPropertyHeight( SerializedProperty i_property, GUIContent i_label ) { return EditorGUIUtility.singleLineHeight; } } // class RangeEditor
いい感じですね。
蛇足? MinMaxSliderを使ってみよう
エディタの機能の中にEditorGUI.MinMaxSlider()
というものがあります。
これを使えば、もっと楽しいエディタになるかも。
あっ、でも引数にfloat
型しか渡せないや……。
とりあえるfloat
のみ使ってみましょう。
RangeEditor
public override void OnGUI( Rect i_position, SerializedProperty i_property, GUIContent i_label ) { var minProperty = i_property.FindPropertyRelative( "m_minValue" ); var maxProperty = i_property.FindPropertyRelative( "m_maxValue" ); ApplyValue( minProperty, maxProperty ); i_label = EditorGUI.BeginProperty( i_position, i_label, i_property ); // プロパティの名前部分を描画。 Rect contentPosition = EditorGUI.PrefixLabel( i_position, i_label ); // MinとMaxの2つのプロパティを表示するので、残りのフィールドを半分こ。 contentPosition.width /= 2.0f; // Rangeを配列でもっている際は、その分インデントが深くなっている。揃えたいので0に。 EditorGUI.indentLevel = 0; // 3文字なら、これぐらいの幅があればいいんじゃないかな。 EditorGUIUtility.labelWidth = 30f; contentPosition.height = EditorGUIUtility.singleLineHeight; EditorGUI.PropertyField( contentPosition, minProperty, MIN_LABEL ); contentPosition.x += contentPosition.width; EditorGUI.PropertyField( contentPosition, maxProperty, MAX_LABEL ); contentPosition.x -= contentPosition.width; contentPosition.y += EditorGUIUtility.singleLineHeight; contentPosition.width *= 2.0f; DrawSlider( contentPosition, minProperty, maxProperty ); EditorGUI.EndProperty(); } protected virtual void DrawSlider( Rect i_drawPosition, SerializedProperty i_minProperty, SerializedProperty i_maxProperty ) { }
RangeFloatEditor
protected override void DrawSlider( Rect i_drawPosition, SerializedProperty i_minProperty, SerializedProperty i_maxProperty ) { float min = i_minProperty.floatValue; float max = i_maxProperty.floatValue; EditorGUI.MinMaxSlider( i_drawPosition, ref min, ref max, -100, 100 ); i_minProperty.floatValue = min; i_maxProperty.floatValue = max; } public override float GetPropertyHeight( SerializedProperty i_property, GUIContent i_label ) { return EditorGUIUtility.singleLineHeight * 2.0f; }
楽しくはなりましたが、スライダーの幅を指定するリミット値をどのように設定したものか。
今回は固定値を使いましたが、どのような値を設定するかわからない以上、リミット値を固定値にするのも問題です。
エディタ上で設定させるようにすると、設定する項目が4つになってしまい、正直邪魔です。
うーん、どうしたものか。
無理やりなんとかしてみましょう。
RangeFloat
[System.Serializable] public struct RangeFloat : IRange< float > { public RangeFloat( float i_value ) : this( i_value, i_value ) { } public RangeFloat( float i_min, float i_max ) : this( i_min, i_max, -100.0f, 100.0f ) { } public RangeFloat( float i_min, float i_max, float i_minLimit, float i_maxLimit ) { m_minValue = i_min; m_maxValue = Mathf.Max( i_min, i_max ); m_minLimitValue = i_minLimit; m_maxLimitValue = i_maxLimit; } [SerializeField] private float m_minValue; [SerializeField] private float m_maxValue; [SerializeField, HideInInspector] private float m_minLimitValue; [SerializeField, HideInInspector] private float m_maxLimitValue; }
RangeEditor
protected virtual void DrawSlider( Rect i_drawPosition, SerializedProperty i_baseProperty, SerializedProperty i_minProperty, SerializedProperty i_maxProperty ) { }
RangeFloatEditor
protected override void DrawSlider( Rect i_drawPosition, SerializedProperty i_baseProperty, SerializedProperty i_minProperty, SerializedProperty i_maxProperty ) { float min = i_minProperty.floatValue; float max = i_maxProperty.floatValue; float minLimit = i_baseProperty.FindPropertyRelative( "m_minLimitValue" ).floatValue; float maxLimit = i_baseProperty.FindPropertyRelative( "m_maxLimitValue" ).floatValue; EditorGUI.MinMaxSlider( i_drawPosition, ref min, ref max, minLimit, maxLimit ); i_minProperty.floatValue = min; i_maxProperty.floatValue = max; }
TestComponent
public class TestComponent : MonoBehaviour { [SerializeField] private RangeFloat m_a = new RangeFloat( 0.0f, 0.0f, -100.0f, 100.0f ); [SerializeField] private RangeFloat m_b = new RangeFloat( 0.0f, 0.0f, -10.0f, 200.0f ); [SerializeField] private RangeFloat m_c = new RangeFloat( 0.0f, 0.0f, -200.0f, 0.0f ); } // class TestComponent
同じ数値を指定ているのに、スライダーの位置が違います。
成功です。
ただ、この方法は大技過ぎる……。
一度設定したリミット値を変更するにはリセットをしないと、シリアライズされた値が更新されないので何とも使い勝手が悪いなぁ。
うーん、EditorGUI.MinMaxSlider()
は無くてもいいかなぁー……。
感想
最終的に、EditorGUI.MinMaxSlider()
なしの状態が1番汎用性が高い気がしますね。
これで最小値と最大値を1つの変数で管理できるので、クラスが少しはすっきりするかも。
参考リンク
https://docs.unity3d.com/ScriptReference/EditorGUI.MinMaxSlider.htmldocs.unity3d.com