こんにちは、ユーゴです。
今回は、以前紹介した属性に動的な引数を渡すに関して、かなり非効率的で曖昧なお話をしていたので、改訂版として記事を書きました。
Unityエンジニア中級以上の方には、是非一読していただきたいです。
この記事でわかること
・そもそも属性とは?
・属性に「定数」ではなく、「変数」を入れる
対象読者
Unityエンジニア(実務レベル)
環境
Unity2019.4.29f
MacBookPro 13インチ(OSはMonterey)
そもそも属性とは?
属性とは、変数に付与する特性のようなものです。
例えば、Unityではお馴染みの[SerializeField]が属性です。
int a;
と書いてもインスペクターには表示されませんが、
[SerializeField] int a;
と書くと、インスペクターに表示されます。
つまり、[SerializeField]は「インスペクターに表示するよ」という特性を変数aに与えていることになります。
発生している問題
属性は、定数を引数にできます。例えば、
[TestAttribute("ok")] int a;
は定数を引数にしているので問題ないです。
しかし、ダイナミックな変数(通常の変数)を引数に取れません。例えば、
[TestAttribute(str)] int a;
string str = "ok";
と書くと、以下のようなエラーが出ます。
error CS0120: An object reference is required for the non-static field, method, or property '(クラス名).(引数にした変数名)'
なので、属性にダイナミックに値を変える変数を参照させたい。
原因
残念ながら、これは仕様です。属性は、どう足掻いてもダイナミックな変数を引数に取ることができないのです。
なので、参照を以下のようなイメージから、
次のようなイメージに切り替えます。
「変数(任)のある情報」って何?!と思うかもしれませんが、とても単純で、変数名をstring型で渡すだけです。例えば、int a;なら"a"を渡していくだけ。後から描画クラスで、「"a"と変数名が一致しているものをゴリ押しで探す!」というような作戦です。
ゴリ押しと言っても、ちゃんとしたセオリーはあります笑
解決策
今回は以下のようなスクリプトを作りました。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AttributeSample : MonoBehaviour
{
[Test(nameof(str))] public int a;
[SerializeField] string str = "ok";
}
using System;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class TestAttribute : PropertyAttribute
{
public string fieldName;
public TestAttribute(string fieldName)
{
this.fieldName = fieldName;
}
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(TestAttribute))]
public class TestDrawer : PropertyDrawer
{
TestAttribute _attribute { get { return (TestAttribute)attribute; } }
SerializedProperty referedProperty;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
referedProperty = property.serializedObject.FindProperty(_attribute.fieldName);
if (referedProperty != null)
{
EditorGUI.LabelField(position, referedProperty.stringValue);
}
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
if (referedProperty == null) return 0;
else return base.GetPropertyHeight(property, label);
}
}
#endif
複雑に見えますが、前回の属性に動的な引数を渡すに比べれば、記述量・参照関係共にかなり簡潔に書かれています。
解説
(1)SampleAttribute.cs について
まずAttributeSample.csですが、
[Test(nameof(str))] public int a;
[SerializeField] string str = "ok";
となっていて、 [Test("str")] ではなく [Test(nameof(str))] なのはなぜ?となるかもしれません。
もちろん、"str"とnameof(str)はstring型で同じものを指しています。nameof(str)を採用したのは、以下の理由です。
・変数名の写し間違い防止
・nameofなら、置換時に対象に含まれる
が主な理由です。
とは言っても、好みがあると思うので"str"でもいいと思います。
(2)TestAttribute.cs / TestAttribute について
public TestAttribute(string fieldName)
{
this.fieldName = fieldName;
}
ここで、[Test("str")]というような記述を可能としています。コンストラクタと同じ認識です。なので、[TestAttribute("str")]と書くこともできます。
〇〇Attribute.csと書いた時、[〇〇]と属性を省略出来るのもポイントですね。
(3)TestAttribute.cs / TestAttributeDrawer について
解説が多いので、コメントアウトしてみました。
[CustomPropertyDrawer(typeof(TestAttribute))]
public class TestDrawer : PropertyDrawer
{
//TestAttributeにアクセスするための変数
TestAttribute _attribute { get { return (TestAttribute)attribute; } }
//AttributeSampleのstrに関する情報を入れる
SerializedProperty referedProperty;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
//propertyは、属性がついている変数(今回はAttributeSampleのint a;)に関する情報
//serializedObjectは、property含め他の変数情報を含む親の存在?
//serializedObjectの変数情報の中から、変数名がTestAttributeクラスのfieldName、すなわち"str"である変数情報を探す
referedProperty = property.serializedObject.FindProperty(_attribute.fieldName);
if (referedProperty != null)
{
//EditorGUI.LabelFieldは、インスペクターに文字を表示させるだけ。
//referedProperty(変数strに関する情報)で管理される、string型の値を取得
EditorGUI.LabelField(position, referedProperty.stringValue);
}
}
//変数strがアクセスできなかったとき、インスペクターで取るスペースを0にする。そうじゃないときは通常通り。
//ちなみに、1行追加したいならbase.GetPropertyHeightに「EditorGUIUtility.singleLineHeight」を足す
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
if (referedProperty == null) return 0;
else return base.GetPropertyHeight(property, label);
}
}
今回の肝はSerializeProperty型と、SerializedObject型だと思っています。
ここに関して実態を掴めないため、はっきりとしたことが不明です。私の解釈になりますが、クラス&変数の関係と似ていると思います。
ただ、SerializedObjectがクラスに対応しているようには、直感的に感じない...
あくまで構造がたまたま対応関係にある、くらいでしょうか?
詳しい方いらっしゃいましたら、是非教えてください!
実験
うまくいっています。
「あああ」の時に遅延があったのは、Enterを押してなかったからです。全角の場合、Enterを押して入力を完了させるまでは空欄扱いとなります。
まとめ
いかがでしたでしょうか。今回は、属性に動的な引数を渡す第2弾を書きました。
↓第1弾で書いたこれは本当に悪書。
属性に動的な引数を渡す
あとは適宜Editor拡張と組み合わせながら、自前のインスペクターを作ってください!
私はUnityに関連した知識を発信しています。今回のような痒い所の解説から、初級編まで幅広く扱っていく予定です。よろしければ、フォローお願いします。
【参考】
PropertyDrawerが思ったより便利だった
【Unity】隣のSerializedPropertyを知りたくて