【2022/6/18追記】
本記事のやり方は非推奨です。以下をご覧になることをお勧めします。
属性(Attribute)に動的な引数を渡す その2
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
こんにちは、ユーゴです。
この記事では、属性に動的な値を渡す方法を紹介します。
「Unityでインスペクタ書き換えたい!」と思い、「どうやったら動的な値を渡せるんだ?」という疑問にたどり着き、しかし調べても答えがなかなか出てきませんでした。今回は、そんな方達のために本記事を書きました。Unityのエディタ拡張周りに手を出すと沼ですよね...?
参考にさせていただいた動画
Show your List as Popup in Unity Tutorial
0.本記事を読むにあたって
Unityの扱い方は、実務レベルで理解できているものとします。
また、「エディタ拡張、カスタム属性もなんとなく知ってる」くらいでないと読むのはキツいです。
1.結論
まず、結論です。これだけ読んで、「あっ、察した」となれば幸いです。
あるMonobehavior継承クラスで、
1) dynamicな変数 → staticな変数 → 属性の引数でstaticな変数を「うまく」代入
2) ISerializationCallbackReceiverを実装
また、記事で最終的に完成したスクリプトは以下2つです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ShowIfTest : MonoBehaviour, ISerializationCallbackReceiver
{
[SerializeField] bool show;
public static bool _show;
[ShowIf(typeof(ShowIfTest), "_show")][SerializeField] int num;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
void OnValidate()
{
_show = show;
}
public void OnBeforeSerialize()
{
_show = show;
}
public void OnAfterDeserialize()
{
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class ShowIfAttribute : PropertyAttribute
{
public Type type;
public string conditionName;
public ShowIfAttribute(Type _type, string _conditionName)
{
type = _type;
conditionName = _conditionName;
}
}
//ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(ShowIfAttribute))]
public class ShowIfDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
ShowIfAttribute showIfAttribute = (ShowIfAttribute)attribute;
bool condition;
condition = (bool)showIfAttribute.type.GetField(showIfAttribute.conditionName).GetValue(showIfAttribute.conditionName);
if (condition) EditorGUI.PropertyField(position, property, label);
}
}
#endif
2.手順
2-1.まずは簡単に
今回は、インスペクタでチェックを入れると表示、外すと非表示になる属性を作ってみます。
名前は、「ShowIfAttribute」とでもしましょう。
作りたいもののイメージとしては、以下のようなビジョンです。
[SerializeField] bool show;
[ShowIf(show)] [SerializeField] int num;
そして、インスペクタから「show」のtrue/falseを切り替えて、「num」の表示/非表示を切り替えたいわけですが...まあ書いてみましょう。
オブジェクトにアタッチするクラス
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ShowIfTest : MonoBehaviour
{
[SerializeField] bool show;
[ShowIf(show)][SerializeField] int num;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
カスタム属性+描画処理 (まとめて書いちゃいましょう)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class ShowIfAttribute : PropertyAttribute
{
public bool condition;
public ShowIfAttribute(bool _condition)
{
condition = _condition;
}
}
//ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(ShowIfAttribute))]
public class ShowIfDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
ShowIfAttribute showIfAttribute = (ShowIfAttribute)attribute;
if (showIfAttribute.condition) EditorGUI.PropertyField(position, property, label);
}
}
#endif
一応説明すると、変数を「ShowIfTest の show」→「ShowIfAttribute の condition」→「ShowIfDrawer の showIfAttribute.condition」という順番で引き渡します。そして、 if(showIfAttribute.condition)EditorGUI.PropertyField(position, property, label); で本来の描画処理を実行するかしないかを判断します。
よし、これで...あれ?
どうも、showを代入できないようです。
エラーを見てみると、
error CS0120: An object reference is required for the non-static field, method, or property 'ShowIfTest.show'
本来は、staticなクラスやメソッドにdynamicな変数を渡せない、というエラーです。
今回のエラーでは、どうも「属性」という存在自体がstaticであるために、dynamicな変数を引き渡せないのではないか、と予想しています。(違ったら教えてください。合っても教えていただけると幸いです笑)
2-2.修正
解決策として、代入する数字をpublicでstaticにしてしまえはいいわけですね!
ということで、ShowIfTestクラスで、dynamic→static→属性と引き渡していきましょう。
ShowIfTest.csを書き換えます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ShowIfTest : MonoBehaviour
{
[SerializeField] bool show;
public static bool _show;
[ShowIf(_show)][SerializeField] int num;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
void OnValidate()
{
_show = show;
}
}
OnValidateで、インスペクタを変えるごとに_showも変わるようにしています。
さてこれで...なんで?!
error CS0182: An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type
どうやら、属性引数にはなんでも入れられるわけではなく、定数式(123,"aaa",trueなど)、typeof式、属性パラメーター型の配列の作成式(?)のみが使用できるそうで、そもそも変数を入れたらダメだということです。
ということで、「typeof式で自身のクラスを指定&stringで変数名を指定」→「後でtypeからstring名の変数を参照」ということをしましょう。
2-3.修正 take2
よって、ShowIfTest.csとShowIfAttribute.csを書き換えます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ShowIfTest : MonoBehaviour
{
[SerializeField] bool show;
public static bool _show;
[ShowIf(typeof(ShowIfTest), "_show")][SerializeField] int num;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
void OnValidate()
{
_show = show;
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class ShowIfAttribute : PropertyAttribute
{
public Type type;
public string conditionName;
public ShowIfAttribute(Type _type, string _conditionName)
{
type = _type;
conditionName = _conditionName;
}
}
//ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(ShowIfAttribute))]
public class ShowIfDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
ShowIfAttribute showIfAttribute = (ShowIfAttribute)attribute;
bool condition = (bool)showIfAttribute.type.GetField(showIfAttribute.conditionName).GetValue(showIfAttribute.conditionName);
if (condition) EditorGUI.PropertyField(position, property, label);
}
}
#endif
ShowIfDrawwerでめっちゃ長いチェーンがあります。会社でやると可読性が低くて怒られそうですが...
このチェーンの説明をすると、「showIfAttribute」インスタンスの変数「type」型のクラスの変数名「showIfAttributeのcondition」という変数情報の「showIfAttributeのcondition」という名前の変数の値を取得。そしてそれはObject型なので、boolにキャストする、ということです。私も何言ってるのかわかりません。
鍵になるのは、Type型とObject型です。私では十分説明できないので、気になる方は調べてみてください。ここまでC#を勉強している人は、いないのでは?とも思ったりします。
では、インスペクタを見ていきましょう。
はい、うまくいきました!
...とはいきません。
同じゲームオブジェクトに、もう一個のShowIfTestをAddComponentで足してみましょう。
片方はうまくいきましたが、もう片方が自身のshowに関係なく表示/非表示が切り替わってます。
わかりやすく同じゲームオブジェクトでやりましたが、別のゲームオブジェクトにShowIfTestがついていても同じ現象が起きます。それではまずい。
初心者にありがちな「public static」多用と、それに伴う弊害の例です。もちろん、スパゲティコードも良くない。基本的にpublic staticを使っていいのはシングルトンとかサービスロケータのようなカプセル化の時だけです。
2-4.修正 take3
どうすればいいか?
ShowIfTest.csにISerializationCallbackReceiverを追加で継承させます。
理由は、もはやわかりません。私はType型とObject型で止まったままです。誰か教えてください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ShowIfTest : MonoBehaviour, ISerializationCallbackReceiver
{
[SerializeField] bool show;
public static bool _show;
[ShowIf(typeof(ShowIfTest), "_show")][SerializeField] int num;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
void OnValidate()
{
_show = show;
}
//シリアライズされる前に処理
public void OnBeforeSerialize()
{
_show = show;
}
public void OnAfterDeserialize()
{
}
}
3.まとめ
ということで、動的な変数を属性の引数に渡す方法でした。Unityをやり始めて1年くらいしたらUnityのエディタ拡張にも手を出したくなるのではないでしょうか?エディタ拡張とカスタム属性について、根本的な解説をした記事を書くかもしれません。
私自身が理解できていない所が多いので、情けない解説になりましたが...気になる方は、元動画を見てください。英語ですが、字幕を駆使して見れます。
Show your List as Popup in Unity Tutorial
私はこのように、Unityを使っていて手が届かない痒いところの記事も書いていこうと思っています。気に入っていただけましたら、フォローしていただけると幸いです。