アマゾンバナーリンク

ディスプレイ広告

スポンサーリンク

UnityのInspectorを超便利にするアセットodin【第1回】

こんにちは!ジェイです。Unityのインスペクターを超パワーアップさせる便利なアセットodinの紹介をしていきます。今回は第1回Attributes編ということで、情報量が多いので、いくつかにわけて載せていきます。

記事内広告

Attributes

Simple Attribute Examples

Odin Inspector では、属性だけを使ってインスペクタを完全に設計することができます。グループ化、順序付け、複雑な機能、入力検証、さまざまな使いやすさの機能、ボタンなどを実装できます。

// クラスの先頭に以下のusing statemntを追加することを忘れないでください。これにより、Odinのすべての属性にアクセスできるようになります。
using Sirenix.OdinInspector;

public class ExampleScript : MonoBehaviour
{
	[FilePath(Extensions = ".unity")]
	public string ScenePath;

	[Button(ButtonSizes.Large)]
	public void SayHello()
	{
		Debug.Log("Hello button!");
	}
}

HideInInspector属性でフィールドを隠し、ShowInInspector属性でフィールドやプロパティを表示することができます。

[HideInInspector]
public int NormallyVisible;

[ShowInInspector]
private bool normallyHidden;

[ShowInInspector]
public ScriptableObject Property { get; set; }

複数の属性を組み合わせたり、PropertyOrder属性で順番を変えたりすることができます。

[PreviewField, Required, AssetsOnly]
public GameObject Prefab;

[HideLabel, Required, PropertyOrder(-5)]
public string Name { get; set; }

[Button(ButtonSizes.Medium), PropertyOrder(-3)]
public void RandomName()
{
    this.Name = Guid.NewGuid().ToString();
}

グループ属性を使用すると、プロパティのレイアウトを完全に変更することができます。

[HorizontalGroup("Split", Width = 50), HideLabel, PreviewField(50)]
public Texture2D Icon;

[VerticalGroup("Split/Properties")]
public string MinionName;

[VerticalGroup("Split/Properties")]
public float Health;

[VerticalGroup("Split/Properties")]
public float Damage;

また、多くの属性では、他のフィールド、プロパティ、メソッドを参照して、インスペクタを拡張し、ユーザーの使用目的に合わせたカスタム動作を行うことができます。

[LabelText("$IAmLabel")]
public string IAmLabel;

[ListDrawerSettings(
    CustomAddFunction = "CreateNewGUID",
    CustomRemoveIndexFunction = "RemoveGUID")]
public List<string> GuidList;

private string CreateNewGUID()
{
	return Guid.NewGuid().ToString();
}

private void RemoveGUID(int index)
{
    this.GuidList.RemoveAt(index);
}

Group Attributes

多くのインスペクタ変数を持つ非常に大きなスクリプトで、インスペクタですべての変数を追跡するのが難しく、退屈だと感じたことはありませんか?Odinのグループはこの問題を解決するために存在し、インスペクタを管理しやすく、見やすくするために大いに役立ちます。

グループを使うと、関連するプロパティをグループ化し、シンプルなラベル付きボックスやドロップダウンから複数ページのタブグループまで、さまざまな方法で描画することができます。使い方は簡単で、グループ化したいメンバーに同じ名前のグループ属性を付けるだけです。

[TabGroup("First Tab")]
public int FirstTab;

[ShowInInspector, TabGroup("First Tab")]
public int SecondTab { get; set; }

[TabGroup("Second Tab")]
public float FloatValue;

[TabGroup("Second Tab"), Button]
public void Button()
{
	...
}

グループを一緒に組み合わせることができるのです。

[Button(ButtonSizes.Large)]
[FoldoutGroup("Buttons in Boxes")]
[HorizontalGroup("Buttons in Boxes/Horizontal", Width = 60)]
[BoxGroup("Buttons in Boxes/Horizontal/One")]
public void Button1() { }

[Button(ButtonSizes.Large)]
[BoxGroup("Buttons in Boxes/Horizontal/Two")]
public void Button2() { }

[Button]
[BoxGroup("Buttons in Boxes/Horizontal/Double")]
public void Accept() { }

[Button]
[BoxGroup("Buttons in Boxes/Horizontal/Double")]
public void Cancel() { }

Odinでは、インスペクタに独自のグループタイプを簡単に追加することができます。

Meta Attributes

[ValidateInput("IsValid")]
public int GreaterThanZero;

private bool IsValid(int value)
{
	return value > 0;
}

[OnValueChanged("UpdateRigidbodyReference")]
public GameObject Prefab;

private Rigidbody prefabRigidbody;

private void UpdateRigidbodyReference()
{
	if (this.Prefab != null)
	{
		this.prefabRigidbody = this.Prefab.GetComponent<Rigidbody>();
	}
	else
	{
		this.prefabRigidbody = null;
	}
}

このようなメタ属性を使用することで、プログラマーは強力で使いやすいツール群を手に入れることができ、必要なルールを施行するだけでなく、フィードバックや関連情報、ツールチップを適切なタイミングで提供することで、インスペクタを使用するデザイナーを支援、制約、ガイドすることができ、結果として、エラーやミスが発生しにくい、全体的にスムーズなワークフローを実現することができます。

[Required]
public GameObject RequiredReference;

[InfoBox("This message is only shown when MyInt is even", "IsEven")]
public int MyInt;

private bool IsEven()
{
	return this.MyInt % 2 == 0;
}

OdinSerializeとShowInInspectorについて

OdinSerializeやSerializeFieldとShowInInspectorには、最初はわからないかもしれませんが、非常に大きな違いがあります。どちらも通常はインスペクタに何かを表示しますが、ShowInInspectorでは表示されたものは保存されません。インスペクタに何かを表示し、それを保存したい場合は、OdinSerializeを使用してください。

OdinSerialize をメンバーに適用しても、インスペクタに表示されない場合は、その属性がメンバーを実際にシリアライズしていないことが原因です。また、定義しているクラスやその継承クラスが実装しているインターフェース上で宣言されているプロパティも同様です(プロパティのメソッドが仮想化されてしまうため)。

HideInInspectorとOdinSerializeを組み合わせることで、インスペクタに表示されないものを保存することができます。

Attribute Expressions

多くの属性では、メンバーを参照したり、評価されるC#式を含む文字列パラメータを渡すことができます。これは、インスペクタに基本的なロジックを素早く簡単に注入することができるので、非常に便利です。属性の式は、@記号で始まる文字列で示されます。

式をサポートしている数多くの属性の一つにInfoBoxがあります。これは、ボックスのコンテンツとして使用される文字列値に評価される式を受け入れます。

次の属性宣言は、属性式の最も単純な使用法の1つであり、myStrフィールドの変更されていない内容を表示するインフォボックスになります。

[InfoBox("@myStr")]
public string myStr;

より複雑なロジックを書くこともできます。以下の属性宣言を行うと、常に現在の時刻を表示するインフォボックスになります。

[InfoBox(@"@""The current time is: "" + DateTime.Now.ToString(""HH:mm:ss"")")]
public string myStr;

ShowIfやHideIfのような属性も式をサポートしています。

[ShowIf("@this.someNumber >= 0f && this.someNumber <= 10f")]
public string myStr;

public float someNumber;

また、属性式からさまざまなコンテキスト値にアクセスするための特別な式キーワードもあります。例えば、$property キーワードを使用すると、式が評価されているメンバーのInspectorPropertyインスタンスにアクセスできます。

[Serializable]
public class Example
{
	[InfoBox(@"@""This member's parent property is called "" + $property.Parent.NiceName")]
    public string myStr;
}

// これで、どこで宣言しても、myStrは親の名前を動的に知ることができます。
public Example exampleInstance;

また、$value キーワードを使用すると、メンバーの名前を入力しなくても、式の対象となるメンバーの値にアクセスできます。これにより、同じ式 (おそらく const で宣言されたもの) を複数の異なるメンバで再利用できます。例えば、最初の例にある InfoBox 式のより汎用的なバージョンは次のようになります。

[InfoBox("@$value")]
public string myStr;

これらの値は「名前付き式引数」と呼ばれ、式を処理するValueResolverまたはActionResolverによって式システムに渡される名前付きの値に対応しています。式で利用可能な名前付き値、および独自の値を追加する方法の詳細については、「名前付き値」を参照してください。

また、特殊なプロパティ・クエリ構文を使用して、式のスコープ内の他のメンバのプロパティを簡単に取得することもできます。#(memberName).

public List<string> someList;

[OnValueChanged("@#(someList).State.Expanded = $value")]
public bool expandList;

上記の式は、someList メンバのプロパティを検索し、expandList メンバの値が変更されたときにその展開状態を変更します。Odinの属性式は、C#の式構文の大部分をサポートする軽量コンパイラによって支えられています。式は解釈されず、エミットされたILに完全にコンパイルされ、それがさらにネイティブマシンコードにJITされます。無効な式を書いた場合には、何が間違っているかを示す親切なコンパイラエラーが提供されます。

属性式は非常に柔軟で強力なツールであり、無数のメンバーを作成して参照することなく、Odinのすべての属性にカスタムロジックや動作を追加することが非常に容易になります。

Property States

3.0では、プロパティの状態システムを導入しました。これは、ドロワー(および、リゾルバやプロセッサなど、プロパティに接続された他のもの)が、外部から問い合わせたり変更したりできる名前付きの状態を作成して公開する方法です。また、3.0では、属性式にプロパティクエリ構文が追加され、ステートシステムとの強力な相乗効果を発揮しています。プロパティの状態は、PropertyState 型の InspectorProperty インスタンスの State メンバに含まれます。

すべてのプロパティには、デフォルトで 3 つの状態がハードコードされています。 これは、非常によく使われるためです。これらの状態は、Visible 状態、Enabled 状態、Expanded 状態です。

Visible 状態は、対象となるプロパティの可視性を制御します。つまり、Visible 状態が false の場合は、InspectorProperty.Draw() を呼び出しても、何も描画されずにすぐに返されます。グループに含まれるすべてのプロパティの可視状態が false の場合、そのグループは自身の可視状態をfalseに切り替え、すべての子プロパティが非表示になったときに自身も自動的に非表示になります。

Enabled状態は、そのプロパティのGUIをデフォルトで有効にするか無効にするかを制御します。個々のドロワーはこれを上書きすることができます。また、Enabledステートは、GUIが無効から有効に切り替わることはなく、その逆になることもあります。ReadOnlyであることなど、他の多くの要因によって、Enabled状態が影響しない形でプロパティのGUIが無効になることがあります。Enabled 状態が false のとき、InspectorPropert.Draw()を呼び出すと、そのプロパティのドロワーチェーンが描画されている間、GUI.enabled が false に設定されます。

Expanded 状態は、プロパティのドロワーがその実装でこの状態を利用している場合に、プロパティが UI で展開されるかどうかを制御します。Odinに含まれるすべてのドロワーは、プロパティが展開されるかどうかを制御するためにこの状態を使用しており、独自のカスタムドロワーを作成する場合は、ローカルのドロワーフィールドを使用して制御するのではなく、この状態を使用することをお勧めします。Visible および Enabled の状態とは異なり、Expanded の状態は永続的です。変更された場合、新しい値は PersistentContext キャッシュを介して再読み込み後も永続的に保持されます。

すべてのプロパティがこれらの状態をすべて使用するわけではありません。例えば、文字列用のシンプルなプロパティには Expanded 状態がありますが、この状態を使用するドロワーはないため、状態を変更しても文字列の描画方法には影響しません。

それでは、状態システムを簡単に利用した属性宣言の例を見てみましょう。あるフラグを持っているかどうかでリストの Expanded 状態を制御する enum です。この例では、Odinの属性表現機能である#(exampleList)のプロパティクエリ構文を使って、リストメンバーのInspectorPropertyインスタンスを簡単に操作し、その状態を変更しています。

// 一般的に、プロパティの状態を制御するには OnStateUpdate 属性を使用することをお勧めします。
[OnStateUpdate("@#(exampleList).State.Expanded = $value.HasFlag(ExampleEnum.UseStringList)")]
public ExampleEnum exampleEnum;

public List<string> exampleList;

[Flags]
public enum ExampleEnum
{
    None,
    UseStringList = 1 << 0,
    // ...
}
Image from Gyazo

また、カスタムステートを作成することも可能です。カスタムステートを作成したいドロワー、プロセッサ、リゾルバは、InspectorProperty.State.Create()を一度呼び出す必要があります。その後は、InspectorProperty.State.Get()メソッドやInspectorProperty.State.Set()メソッドを使って、カスタムステートにアクセスしたり変更したりすることができます。

たとえば、TabGroup 属性のドロワーでは、3 つのカスタムステートが公開されています。例えば、TabGroup 属性のドロワーでは、CurrentTabName、CurrentTabIndex、TabCount という 3 つのカスタムステートが公開されており、以下の例のように、現在選択されているタブの照会や変更が可能です。

// すべてのグループは、メンバーとの名前の衝突を避けるために、パス識別子の前に「#」が静かに付けられます。
// したがって、「Tabs」グループには「#(#Tabs)」という構文でアクセスすることになります。
[OnStateUpdate("@#(#Tabs).State.Set<int>(\"CurrentTabIndex\", $value + 1)")]
[PropertyRange(1, "@#(#Tabs).State.Get<int>(\"TabCount\")")]
public int selectedTab = 1;

[TabGroup("Tabs", "Tab 1")]
public string exampleString1;

[TabGroup("Tabs", "Tab 2")]
public string exampleString2;

[TabGroup("Tabs", "Tab 3")]
public string exampleString3;
Image from Gyazo

まとめ

odinのAttributesについて書きましたが、まだまだ機能はたくさんあります。引き続き記事をまとめていきますので、次回も楽しみにしてください。

アイコンは星川ヤヨイさんからお借りしました。

+3