1. サイトトップ
  2. ブログ
  3. Unity
  4. 【Unity】小さな手間と事故を減らすAttributeの使い方

【Unity】小さな手間と事故を減らすAttributeの使い方

こんにちは!
情熱開発部・プログラム課の根冝です。

厳しかった冬の寒さも和らぎ、春らしい日差しを感じられる季節になりました。

さて、今回はUnityのAttribute(属性)について紹介します。
Unityのバージョンは6000.3.10f1を使用しています。

Attributeとは?

皆さんの中にはUnity開発中に以下の問題に引っかかった人も多いのではないでしょうか。

  • コンポーネントを付け忘れていてNullを参照してしまった。
  • コンポーネントが複数付けてあり、意図しない挙動になっていた。
  • 意図しない値が入っていて不具合を起こしていた。
  • デバッグ用の特別な処理を呼び出す度に毎回#ifで処理を囲っている or 囲い忘れてビルドエラーになった。

こうした少しの手間やミスは
「まぁ動くし」「慣れたら問題ないから」
と放置されがちです。

しかし、これらの小さな手間も開発期間や作業量次第では無視できないコストになります。

Attributeを適切に使うことで、こういった人的に発生しがちな小さな手間をエディタ上で防ぎ
将来的な開発効率の向上を狙うことができます。

Attributeの仕組み

ですが、そもそもAttributeはどんな仕組みで動いているのでしょうか?

Attribute(属性)とは、メタデータをコード(クラス、関数、変数など)と関連付ける方法を提供するC#の機能です。

属性は通常のクラスと同様にSystem.Attributeクラスを継承して作成されます。

実は、System.Attributeを継承した各属性のクラス自体は特別な処理を行っていません。
属性自身は、どのコードに何の属性が付与されているかをメタデータ内に記録しています。

SerializeFieldなどの属性をコードに付与すると、その属性の情報がメタデータに保存されます。
コンパイラやフレームワークはこのメタデータを読み取り、属性の内容に応じた振る舞いを行います。
読み取りはコンパイル時に行われることもあれば、プログラムの実行時にリフレクションを用いて行われることもあります。


リフレクションとは

プログラムが実行時に自身のメタデータから型や関数、属性などを取得する仕組みです。


Unityで使われるSerializeFieldやRequireComponentなどの属性も、C#のAttribute機能を利用して実装されています。

Unityではこのリフレクションで属性を取得できる仕組みを利用して、変数やクラスなどに付与されたAttributeに応じてインスペクターの表示を拡張したり、
コンポーネントの付け外しを監視したり、メニューバーに新しい項目を追加したりしてエディタ機能を拡張しています。

手間と事故を減らせる機能達

では、実際にどんなAttributeがあるのか、それぞれどういったミスを防げるのか見ていきましょう。

事故防止系

【SerializeField】

[SerializeField]
private int hogeValue;

 
Unityで作成された属性で、privateなフィールド(変数)のシリアライズを強制させ、インスペクターで値を設定できるようになります。

publicな変数は自動的にインスペクターに公開されるため
SerializeField属性を付けることで、privateにして他のクラスから値が意図せず変更される事故を予防しつつ、エディタで値の調整がしやすくなります。


シリアライズとは

C#(.NET)のシリアライズはプログラム実行時のデータを保存や通信に適した形に変換することを指しますが、
Unityでは変数に設定された値をシーンやプレハブが保持できるように変換することを指します。


【DisallowMultipleComponent】

[DisallowMultipleComponent]
public class HogeClass

Unityで作成された属性で、DisallowMultipleComponent属性が付与されたコンポーネントを複数アタッチできないようにします。
オブジェクトの移動やステータス管理を行うクラスなど、同じコンポーネントがあることで不具合が起きるケースを予防できます。


【RequireComponent】

[RequireComponent(typeof(Rigidbody))]
public class HogeClass


Unityで作成された属性で、コンポーネント付与時に必須コンポーネントも同時に付与されるようにします。
処理に必要なコンポーネントの付与忘れによる実行時エラーを防止できます。

インスペクターでアタッチした瞬間のほか、スクリプト中でAddComponentした場合にも付与されるため
RequireComponent属性を持つクラスをAddComponentした直後から必須コンポーネントも取得することができます。


【Min】、【Range】

[Min(0)]
public int minValue;

[Range(1.0f, 100.0f)]
public float rangeValue;


Unityで作成された属性で、intやfloat型で宣言されている変数の値の範囲を制限することができます。
想定外の値を入れられないようになるため、不具合があった場合の調査を楽にします。


【Conditional】

[Conditional("UNITY_EDITOR")]
public void DebugHogeMethod() {}


C#の属性で、指定されたシンボル(UNITY_EDITORやDEVELOPMENT_BUILDなど)が関数呼び出し側で定義されている場合に
関数の呼び出しが行われるようにします。
テスト用に仕込んだ関数をリリース前に消し忘れたことによるビルドエラーや不具合を防止できます。

シンボルが定義されていない時は呼び出しのコード自体がビルド時に削除されるため
関数の呼び出しを#ifで囲う必要が無くなります。


using System.Diagnostics;
using UnityEngine;

public class AttributeSample : MonoBehaviour
{
    public void LoadAsset()
    {
        Debug_StartLogging(); // シンボルが無い場合は呼び出されない

        // 通常の処理

        Debug_AssetLoad(); // シンボルが無い場合は呼び出されない
    }

    [Conditional("UNITY_EDITOR")]
    public void Debug_AssetLoad() 
    {
#if UNITY_EDITOR
        // UNITY_EDITORが定義されている場合に呼び出される
#endif
    }

    [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
    public void Debug_StartLogging()
    {
#if UNITY_EDITOR || DEVELOPMENT_BUILD
        // UNITY_EDITORかDEVELOPMENT_BUILDが定義されている場合に呼び出される
#endif
    }
}

注意:Conditional属性を付けただけでは定義側のコードがビルドの成果物に含まれてしまうため
デバッグ用の処理などがビルドに含まれないようにしたい場合は、上記コードのように処理を#ifで囲う必要があります。


見た目改善系

【Space】

[Space(20)]


Unityで作成された属性で、指定したピクセル数分の空白を入れます。
レイアウトを整理したいときに便利です。


【Header】

[Header("魔力")]
public int mp;


Unityで作成された属性で、変数にタイトルを設定します。
特に変数が増えてきた場合に大活躍します。

タイトルの上部には少し空白ができるため、Header単体でもある程度見やすくなります。


【InspectorName】

public enum FruitType
{
    [InspectorName("リンゴ")] Apple
}

 
Unityで作成された属性で、enum(列挙体)のインスペクターでの表示名を設定できるようにします。
非プログラマーが設定する場合など、コード上の名前だけでは分かりづらい可能性がある場合に便利です。


その他便利系

【HelpURL】

[HelpURL("https://docs.unity3d.com/ScriptReference/HelpURLAttribute.html")]
public class HogeClass

Unityで作成された属性で、インスペクター右上にあるヘルプアイコンを押した際、設定した任意のページを開くようにできます。
参考にしたサイトや、配布するアセットの場合はドキュメントへの導線として便利です。

注意:ビルドに含まれないようにしたい場合には、UNITY_EDITORのようなビルドに含まれないシンボルで囲う必要があります。

実際にインスペクターを触りやすくしてみよう

Unityはインスペクターの表示を属性によって拡張していると最初の章で説明しました。
この章では、属性の有無によってインスペクターがどう変わるのかを実際に見ていきたいと思います。

まずはAttributeを入れていないクラスを作って、インスペクターの見た目を確認してみます。

using UnityEngine;

public class EnemyController : MonoBehaviour
{
    public enum EnemyType
    {
        Goblin,
        Skeleton,
        Zombie
    }

    public EnemyType enemyType = EnemyType.Goblin;

    public int maxHp = 0;
    public int stamina = 0;

    public float moveSpeed = 0.0f;
    public float jumpPower = 0.0f;

    public int attackPower = 0;
    public float attackInterval = 0.0f;

    public int dropItemId = 0;
}

そこそこパラメータ数の多い敵クラスを作ってみました。
これをインスペクターで見てみると、こんな見た目になります。

スペースは最小限ですが、このままでは値がプラスもマイナスも自由に入力できたり、他のクラスから値を自由に変更できたり、
将来的にパラメータ数が増えた時、どの値が何のバランスを調整できるのか分からなくなったりしてしまいます。

今度はAttributeを入れて、それぞれの見た目を確認してみましょう。

using UnityEngine;

public class EnemyController : MonoBehaviour
{
    public enum EnemyType
    {
        [InspectorName("ゴブリン")] Goblin,
        [InspectorName("スケルトン")] Skeleton,
        [InspectorName("ゾンビ")] Zombie
    }

    [Header("ステータス")]
    [SerializeField]
    private EnemyType enemyType = EnemyType.Goblin;

    [Space(8)]
    [SerializeField, Min(1)]
    private int maxHp = 0;

    [SerializeField, Range(1, 100)]
    private int stamina = 0;

    [Header("移動設定")]
    [SerializeField, Min(0.0f)]
    private float moveSpeed = 0.0f;

    [SerializeField, Min(0.0f)]
    private float jumpPower = 0.0f;

    [Header("攻撃設定")]
    [SerializeField, Range(0, 100)]
    private int attackPower = 0;

    [SerializeField, Min(0f)]
    private float attackInterval = 0.0f;

    [Header("ドロップ設定")]
    [SerializeField]
    private int dropItemId = 0;
}

元からあった改行の位置は変化していませんが、この時点で少しコードが見やすくなった気もします。
こちらもインスペクターで見てみましょう。

分かりやすいように、変更前の画像も右に置いてみます。

Attributeを加えただけで、かなり見やすい見た目になったのではないでしょうか。

書くコードが増える分時間もかかりますが、後々のうっかりミスや確認の手間を減らせると思うとそこまで大きな手間ではないと私は思います。

まとめ

Attribute自体はコードの挙動を大きくは変えません。
ですが、Unityにおいてはエディタ側でどんな値を入れてほしいのか、どんな設定にしたいかを明確にすることができる機能です。

コードに数行追加するだけでうっかりミスを予防できるので
ぜひ活用してみてください!

また、カスタムエディタやエディタウィンドウなら
実際のオブジェクトに干渉して値を書き換えたり、デバッグ用のボタンや専用の画面を作成することも可能です。
もっともっとエディタを便利にしたい方はぜひ調べてみてください!

参考


【免責事項】

本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。