Property Drawer
ツリービュー

カスタムエディター

ゲーム開発のスピードを向上させる鍵は、よく使うコンポーネント用にカスタムエディターを作成することです。ここで、ごく簡単なスクリプトの例を挙げましょう。このスクリプトでは、特定のオブジェクトが常に一定のポイントを向くようにしています。

//C# の例 (LookAtPoint.cs)
using UnityEngine;
public class LookAtPoint : MonoBehaviour
{
    public Vector3 lookAtPoint = Vector3.zero;

    void Update()
    {
        transform.LookAt(lookAtPoint);
    }
}
//JS Example (LookAtPoint.js)
#pragma strict
var lookAtPoint = Vector3.zero;
function Update()
{
    transform.LookAt(lookAtPoint);
}

これによって、オブジェクトが常にワールド空間の一点を向くようになります。現時点では、このスクリプトはプレイモードでのみ(つまりゲーム起動中にのみ)機能します。しかし、エディタースクリプトを書く場合、特定のスクリプトにはエディットモード(つまりゲームが起動していないとき)でも機能して欲しいことがあります。この設定は、属性 ExecuteInEditMode を追加することで行えます。

//C# の例 (LookAtPoint.cs)
using UnityEngine;
[ExecuteInEditMode]
public class LookAtPoint : MonoBehaviour
{
    public Vector3 lookAtPoint = Vector3.zero;

    void Update()
    {
        transform.LookAt(lookAtPoint);
    }
}
//JS Example (LookAtPoint.js)
#pragma strict
@script ExecuteInEditMode()
var lookAtPoint = Vector3.zero;
function Update()
{
    transform.LookAt(lookAtPoint);
}

上記によって、プレイモードでなくても、このスクリプトを持ったオブジェクトをエディター上で動かしたり、インスペクター上で “Look At Point” の値を変更したりすると、それに応じてオブジェクトの向きが変わるようになりました。つまり、オブジェクトが常にワールド空間の一定のポイントを向き続けるようになりました。

カスタムエディターの作成

上記を参照すると、エディットタイムでの簡単なスクリプトの実行方法が分かります。しかしこれだけではユーザー独自のエディターツールの作成はできません。そこで、次のステップとして、今作成したスクリプト用に カスタムエディター を作成してみましょう。

Unity でスクリプトを作成すると、デフォルトでは MonoBehaviour から継承されるので Component となります。これはゲームオブジェクトに配置することができます。ゲームオブジェクトに配置されると、インスペクター上に、表示され得るすべてのパブリック変数(整数・浮遊少数・文字列・ Vector3 など)を表示・編集するためのデフォルトのインターフェースが表示されます。

上記のスクリプトは、デフォルトのインスペクターでは以下のように表示されます。

デフォルトのインスペクターに表示される Vector3 フィールド
デフォルトのインスペクターに表示される Vector3 フィールド

カスタムエディターは、このデフォルトレイアウトの 代わりに 任意のエディター制御を表示できるようにする、追加のスクリプトです。

LookAtPoint スクリプト用にカスタムエディターを作成するには、同じ名前に “Editor” という言葉を付け足した名前のスクリプトを、別にひとつ作成する必要があります。この参考例の場合は、名前は “LookAtPointEditor” となります。

//c# の例 (LookAtPointEditor.cs)
using UnityEngine;
using UnityEditor;
​
[CustomEditor(typeof(LookAtPoint))]
[CanEditMultipleObjects]
public class LookAtPointEditor : Editor 
{
    SerializedProperty lookAtPoint;
    
    void OnEnable()
    {
        lookAtPoint = serializedObject.FindProperty("lookAtPoint");
    }
​
    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.PropertyField(lookAtPoint);
        serializedObject.ApplyModifiedProperties();
    }
}
//JS Example (LookAtPointEditor.js)
#pragma strict
@CustomEditor(LookAtPoint)
@CanEditMultipleObjects
class LookAtPointEditor extends Editor {

    var lookAtPoint : SerializedProperty;

    function OnEnable()
    {
        lookAtPoint = serializedObject.FindProperty("lookAtPoint");
    }

    function OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.PropertyField(lookAtPoint);
        serializedObject.ApplyModifiedProperties();
    }
}

このクラスは Editor クラスから派生している必要があります。CustomEditor 属性は、それがどのコンポーネントのエディターとして機能するべきかを Unity に伝えます。CanEditMultipleObjects 属性は、複数のオブジェクトを選択し、同じ値を同時に変更することを Unity に伝えることができます。

OnInspectorGUI 内のコードは、Unity がインスペクター上でエディターを表示するたびに実行されます。ここにはどんな GUI コードでも入れることができます。ゲームに対して OnGUI と同じように機能しますが、こちらはインスペクターの中で実行されます。Editor は、インスペクトされているオブジェクトへのアクセスに使用できるターゲットプロパティーを定義します。カスタマイズしたインスペクターは以下のようになります。

ここまでに行ったのは、デフォルトのインスペクターの表示とまったく同様の Vector3 フィールドを再現することだけなので、結果の見た目もほとんど代わりません。(“Script” フィールドは、表示するためのインスペクターコードを加えなかったため、表示されていませんが。)

しかし、上記によってエディタースクリプト内でのインスペクターの表示を制御できるようになったので、好きなコードを使用して、インスペクターフィールドをレイアウトしたり、ユーザー自身が値を変更できるようにしたり、グラフィックスその他のビジュアル要素を表示させたりすることが可能になりました。Unity エディター内のインスペクターは、地形システムやアニメーション・インポート設定などの複雑なものも含め、すべて同じ API で作られており、カスタムエディターを作成する場合もこの API を使用することができます。

以下に簡単な例を挙げます。これは、ターゲットポイントがゲームオブジェクトの上にあるか下にあるかを示すメッセージが表示されるように、エディタースクリプトを拡張するものです。

//c# の例 (LookAtPointEditor.cs)
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(LookAtPoint))]
[CanEditMultipleObjects]
public class LookAtPointEditor : Editor
{
    SerializedProperty lookAtPoint;

    void OnEnable()
    {
        lookAtPoint = serializedObject.FindProperty("lookAtPoint");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.PropertyField(lookAtPoint);
        serializedObject.ApplyModifiedProperties();
        if (lookAtPoint.vector3Value.y > (target as LookAtPoint).transform.position.y)
        {
            EditorGUILayout.LabelField("(Above this object)");
        }
        if (lookAtPoint.vector3Value.y < (target as LookAtPoint).transform.position.y)
        {
            EditorGUILayout.LabelField("(Below this object)");
        }
    }
}
//JS Example (LookAtPointEditor.js)
#pragma strict
@CustomEditor(LookAtPoint)
@CanEditMultipleObjects
class LookAtPointEditor extends Editor {

    var lookAtPoint : SerializedProperty;

    function OnEnable()
    {
        lookAtPoint = serializedObject.FindProperty("lookAtPoint");
    }

    function OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.PropertyField(lookAtPoint);
        serializedObject.ApplyModifiedProperties();
        if (lookAtPoint.vector3Value.y > (target as LookAtPoint).transform.position.y)
        {
            EditorGUILayout.LabelField("(Above this object)");
        }
        if (lookAtPoint.vector3Value.y < (target as LookAtPoint).transform.position.y)
        {
            EditorGUILayout.LabelField("(Below this object)");
        }
    }
}

これで、ターゲットポイントがゲームオブジェクトの上にあるか下にあるか通知するための新しい要素が、インスペクターに加わりました。

これは、エディタースクリプトでできることのほんの一例にすぎません。すべての IMGUI コマンドが完全に使用可能となっていますので、それらを使用してどんなタイプのインターフェースでも描くことができます。(例. エディターウィンドウ内でカメラを使用してシーンを描画するなど。)

シーンビューへの追加

カスタムエディターに OnSceneGUI を実装すれば、シーンビューにコードを追加することができます。

OnSceneGUI はちょうど OnInspectorGUI と同じように機能しますが、シーンビュー内で実行されます。エディティング インターフェースの作成には、Handles クラスで定義される関数を使用できます。この中にあるすべての関数は 3D シーンビューでの作業用に作られたものです。

//C#の例 (LookAtPointEditor.cs)
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(LookAtPoint))]
[CanEditMultipleObjects]
public class LookAtPointEditor : Editor
{
    SerializedProperty lookAtPoint;

    void OnEnable()
    {
        lookAtPoint = serializedObject.FindProperty("lookAtPoint");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.PropertyField(lookAtPoint);
        if (lookAtPoint.vector3Value.y > (target as LookAtPoint).transform.position.y)
        {
            EditorGUILayout.LabelField("(Above this object)");
        }
        if (lookAtPoint.vector3Value.y < (target as LookAtPoint).transform.position.y)
        {
            EditorGUILayout.LabelField("(Below this object)");
        }
        
            
        serializedObject.ApplyModifiedProperties();
    }

    public void OnSceneGUI()
    {
        var t = (target as LookAtPoint);

        EditorGUI.BeginChangeCheck();
        Vector3 pos = Handles.PositionHandle(t.lookAtPoint, Quaternion.identity);
        if (EditorGUI.EndChangeCheck())
        {
            Undo.RecordObject(target, "Move point");
            t.lookAtPoint = pos;
            t.Update();
        }
    }
}
//JS Example (LookAtPointEditor.js)
#pragma strict
@CustomEditor(LookAtPointJS)
@CanEditMultipleObjects
class LookAtPointEditorJS extends Editor {

    var lookAtPoint : SerializedProperty;

    function OnEnable()
    {
        lookAtPoint = serializedObject.FindProperty("lookAtPoint");
    }

    function OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.PropertyField(lookAtPoint);
        serializedObject.ApplyModifiedProperties();
        if (lookAtPoint.vector3Value.y > (target as LookAtPointJS).transform.position.y)
        {
            EditorGUILayout.LabelField("(Above this object)");
        }
        if (lookAtPoint.vector3Value.y < (target as LookAtPointJS).transform.position.y)
        {
            EditorGUILayout.LabelField("(Below this object)");
        }
    }


    function OnSceneGUI()
    {
        var t : LookAtPointJS = (target as LookAtPointJS);

        EditorGUI.BeginChangeCheck();
        var pos = Handles.PositionHandle(t.lookAtPoint, Quaternion.identity);
        if (EditorGUI.EndChangeCheck())
        {
            Undo.RecordObject(target, "Move point");
            t.lookAtPoint = pos;
            t.Update();
        }
    }
}

3D 空間の中に 2D GUI(EditorGUI 系)を配置したい場合は、Handles.BeginGUI() 前後で Handles.EndGUI() を呼び出す必要があります。

Property Drawer
ツリービュー