【Unity】タグをインスペクターから選択可能にする

こじゃらこじゃら

次のようにタグ名をインスペクターから選択させるようにさせたいの…

このはこのは

エディタ拡張を実装すれば可能だわ。EditorGUI.TagFieldメソッドを使うのがポイントよ。

タグ名をインスペクターから選択式で指定できるようにする方法の解説記事です。

結論を述べると、タグを表す独自の属性や構造体を実装し、Property Drawerとしてエディタ拡張を実装すれば可能です。

// Tag属性を設定すればUIが置き換わる
[SerializeField, Tag] private string _tagName;

// エディタ拡張が実装された独自構造体を使っても可能
[SerializeField] private Tag _tag;

エディタ拡張からは、EditorGUI.TagFieldメソッドで専用UIを表示できます。

// タグフィールドを表示
string tag = EditorGUI.TagField(position, label, property.stringValue);

// タグ名を反映
property.stringValue = tag;

本記事では、次の2通りの方法でインスペクターからタグ名を選択式で指定できるようにする方法を解説します。

本記事で解説する実現方法
  • 専用の属性Property Drawerを自作する
  • 専用の構造体Property Drawerを自作する
動作環境
  • Unity 2023.1.16f1

スポンサーリンク

専用の属性とProperty Drawerを自作する

1つ目は、独自属性を実装して文字列フィールドをタグ専用のUIに置き換える方法です。

次のような流れで実装できます。

実装の流れ
  • PropertyAttributeクラスを継承した属性クラスを実装する
  • 上記クラスに対してPropertyDrawerクラスを継承したエディタ拡張を実装する

実装例

string型フィールドをタグ選択用UIに置き換える専用属性の実装例です。

TagAttribute.cs
using System;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

/// <summary>
/// タグの専用UIを表示させるための属性
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class TagAttribute : PropertyAttribute
{
}

#if UNITY_EDITOR
/// <summary>
/// タグ名の専用UIを表示させるためのPropertyDrawer
/// </summary>
[CustomPropertyDrawer(typeof(TagAttribute))]
public class TagAttributeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        // 対象のプロパティが文字列かどうか
        if (property.propertyType != SerializedPropertyType.String)
        {
            EditorGUI.PropertyField(position, property, label);
            return;
        }

        // タグフィールドを表示
        var tag = EditorGUI.TagField(position, label, property.stringValue);

        // タグ名を反映
        property.stringValue = tag;
    }
}
#endif

上記をTagAttribute.csなどというファイル名でUnityプロジェクトに保存すると使えるようになります。

使用例

実際に使用する側は、次のようにシリアライズされるstring型フィールドに対して[Tag]属性を付加すれば良いです。

UseAttributeExample.cs
using UnityEngine;

public class UseAttributeExample : MonoBehaviour
{
    // タグ名の専用UIを表示
    [SerializeField] [Tag] private string _tagName;
}

実行結果

string型で定義されたフィールドがタグ選択用UIに置き換わっています。

単なるドロップダウンではなく、新しいタグを追加するAdd Tag…項目も表示されています。

スクリプトの説明

例では、1つのソースファイルに属性とそのエディタ拡張を実装しています。

そのため、エディタ拡張で使われる名前空間UnityEditorは#if UNITY_EDITOR ~ #endifディレクティブで囲んでusingする必要があります。

#if UNITY_EDITOR
using UnityEditor;
#endif

注意

UnityEditor名前空間はUnityエディタでのみ有効なため、UNITY_EDITORが未定義の時(ビルド時)ではコンパイルエラーとなってしまいます。

正しくビルドできない原因になるため注意が必要です。

[Tag]属性を使えるようにするために、次のようにTagAttributeクラスを定義しています。

/// <summary>
/// タグの専用UIを表示させるための属性
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class TagAttribute : PropertyAttribute
{
}

今回はフィールドに対して機能させる属性のため、[AttributeUsage(AttributeTargets.Field)]属性をTagAttributeクラスに付加しています。

また、Unityでフィールド用の属性として使えるようにするために、PropertyAttributeクラスを継承しています。

そして、[Tag]属性として指定されたフィールドに対して専用UIを表示させるために、カスタムのProperty Drawerを実装します。

#if UNITY_EDITOR
/// <summary>
/// タグ名の専用UIを表示させるためのPropertyDrawer
/// </summary>
[CustomPropertyDrawer(typeof(TagAttribute))]
public class TagAttributeDrawer : PropertyDrawer

これは、PropertyDrawerクラスを継承すれば良いです。また、[Tag]属性に作用させるために[CustomPropertyDrawer(typeof(TagAttribute))]属性をクラスに付加しています。

実際の表示部分の実装は、PropertyDrawer.OnGUIメソッドをオーバーライドすることで行います。

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
[Tag]属性が指定されるフィールドはstring型であってほしいため、それ以外の型ではデフォルトUIを表示させるために、次の部分で型チェックを行っています。

// 対象のプロパティが文字列かどうか
if (property.propertyType != SerializedPropertyType.String)
{
    EditorGUI.PropertyField(position, property, label);
    return;
}

そして、string型であることが分かったら、EditorGUI.TagFieldメソッドでタグの選択式UIを表示させます。

// タグフィールドを表示
var tag = EditorGUI.TagField(position, label, property.stringValue);

戻り値として選択されたタグ名が返るため、これをプロパティに反映すれば良いです。

// タグ名を反映
property.stringValue = tag;

専用の構造体とProperty Drawerを自作する

タグ名をstring型ではなく、専用の型として扱うことで、インスペクター表示を変更させることも可能です。

この方法では、次のメリットが得られます。

主なメリット
  • 専用の型を設けることによって、コードの可読性向上が期待できる
  • 目的の異なるstring型変数への間違った代入を防げる
  • 独自のプロパティやメソッドを実装できる

実装の流れは以下のようになります。

実装の流れ
  • シリアライズ可能な専用構造体を実装する
  • 上記構造体に対してPropertyDrawerクラスを継承したエディタ拡張を実装する

最初の属性を実装する方法との主な相違点は、属性ではなく構造体を実装するといった点です。

実装例

Tagという名前の構造体を実装した例です。エディタ拡張でタグ専用の選択式UIに置き換えるコードも含まれています。

Tag.cs
using System;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

[Serializable]
public struct Tag
{
    // タグ名
    [SerializeField] private string _tagName;

    // タグ名のプロパティ
    public string Name
    {
        get => _tagName;
        set => _tagName = value;
    }

    // タグ付けされているかどうか
    public bool IsTagged => !string.IsNullOrEmpty(_tagName) && _tagName != "Untagged";

    // タグ名の比較
    public static bool operator ==(Tag tag, string tagName) => tag._tagName == tagName;
    public static bool operator !=(Tag tag, string tagName) => tag._tagName != tagName;
    public bool Equals(Tag other) => _tagName == other._tagName;
    public override bool Equals(object obj) => obj is Tag other && Equals(other);

    // ハッシュコードの取得
    public override int GetHashCode() => (_tagName != null ? _tagName.GetHashCode() : 0);

    // 文字列への変換
    public override string ToString() => _tagName;

#if UNITY_EDITOR
    // タグ名のプロパティを表示するためのPropertyDrawer
    [CustomPropertyDrawer(typeof(Tag))]
    public class TagDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            var tagNameProperty = property.FindPropertyRelative("_tagName");

            // タグフィールドを表示
            var tag = EditorGUI.TagField(position, label, tagNameProperty.stringValue);

            // タグ名を反映
            tagNameProperty.stringValue = tag;
        }
    }
#endif
}

上記をTag.csなどのファイル名でUnityプロジェクトに保存すると使えるようになります。

使用例

上記Tag構造体の使用例は以下の通りです。

UseStructExample.cs
using UnityEngine;

public class UseStructExample : MonoBehaviour
{
    // タグ名の専用UIを表示
    [SerializeField] private Tag _tag;

    private void Start()
    {
        // タグ名を出力
        print($"_tag.Name: {_tag.Name}");

        // タグ付けされているかどうかを出力
        print($"_tag.IsTagged: {_tag.IsTagged}");

        // タグ名を変更
        var prevTagName = _tag.Name;
        _tag.Name = "Player";

        // タグ名を出力
        print($"_tag.Name: {_tag.Name} (スクリプトから変更)");

        // スクリプト上でTagインスタンスを生成
        var tagFromScript = new Tag {Name = "Enemy"};
        
        // タグ名を出力
        print($"tagFromScript.Name: {tagFromScript.Name} (スクリプトから生成)");
        
        // タグ同士の比較
        var isEqual = _tag == tagFromScript;
        print($"_tag == tagFromScript: {isEqual}");
        
        // タグ名を戻す
        _tag.Name = prevTagName;
    }
}

上記をUseStructExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチすると、インスペクターから設定できるようになります。

実行結果

ゲームを実行すると、次のようにタグに関する情報がログ出力されます。

上記は何もタグを指定しない例です。タグ名(_tag.Name)が空だったり、タグが存在しない判定(_tag.IsTagged: False)だったりします。

試しにTag項目に「Player」を指定すると、以下のようにログ出力が変化します。

途中でスクリプトから生成された「Player」というタグ名が格納されたTag構造体インスタンスと比較していますが、同じタグ名のため最後の出力「_tag == tagFromScript」がTrueになっています。

別の「Finish」などのタグ名にすると、「_tag == tagFromScript」はFalseとなります。

スクリプトの説明

Tag構造体は、シリアライズ可能な型である必要があるため、[Serializable]属性を指定して定義します。

[Serializable]
public struct Tag

例では、タグ名(string型の文字列)タグ付けされているかどうかを返すプロパティを実装しています。

// タグ名のプロパティ
public string Name
{
    get => _tagName;
    set => _tagName = value;
}

// タグ付けされているかどうか
public bool IsTagged => !string.IsNullOrEmpty(_tagName) && _tagName != "Untagged";

これ以外にも、Tag構造体の等価演算や文字列への変換などもサポートしています。

// タグ名の比較
public static bool operator ==(Tag tag1, Tag tag2) => tag1._tagName == tag2._tagName;
public static bool operator !=(Tag tag1, Tag tag2) => tag1._tagName != tag2._tagName;
public static bool operator ==(Tag tag, string tagName) => tag._tagName == tagName;
public static bool operator !=(Tag tag, string tagName) => tag._tagName != tagName;
public bool Equals(Tag other) => _tagName == other._tagName;
public override bool Equals(object obj) => obj is Tag other && Equals(other);

// ハッシュコードの取得
public override int GetHashCode() => (_tagName != null ? _tagName.GetHashCode() : 0);

// 文字列への変換
public override string ToString() => _tagName;

エディタ拡張により表示を置き換えるコードは以下部分です。

#if UNITY_EDITOR
    // タグ名のプロパティを表示するためのPropertyDrawer
    [CustomPropertyDrawer(typeof(Tag))]
    public class TagDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            var tagNameProperty = property.FindPropertyRelative("_tagName");

            // タグフィールドを表示
            var tag = EditorGUI.TagField(position, label, tagNameProperty.stringValue);

            // タグ名を反映
            tagNameProperty.stringValue = tag;
        }
    }
#endif
[Tag]属性を実装する例とは異なり、TagAttribute型ではなくTag型に対して[CustomPropertyDrawer(typeof(Tag))]として属性を指定するところや、フィールドの型がstring型かどうかのチェックが無くなっていたり_tagNameフィールドにアクセスしていたりといった違いがあります。

使用側では、次のようにTag型のフィールドとして定義するだけでタグ選択用の専用UIに置き換わります。

// タグ名の専用UIを表示
[SerializeField] private Tag _tag;

実際にタグ情報にアクセスする処理は以下部分です。

// タグ名を出力
print($"_tag.Name: {_tag.Name}");

// タグ付けされているかどうかを出力
print($"_tag.IsTagged: {_tag.IsTagged}");

さいごに

タグ名をインスペクターから専用UIで指定可能にするには、属性を自作すれば良いですが、独自構造体を実装することでも実現可能です。

特に後者は専用の型を使うことによる可読性やメンテナンス性の向上が期待できるでしょう。

参考サイト

スポンサーリンク