프로퍼티 드로어(Property Drawers)
트리뷰(TreeView)

커스텀 에디터

게임 제작 속도를 높이는 비결은 많이 사용되는 컴포넌트에 대해 커스텀 에디터를 만드는 것입니다. 여기서는 오브젝트가 항상 한 포인트를 바라보도록 하는 매우 간단한 스크립트를 예로 들어보겠습니다. 이 스크립트를 프로젝트에 추가하고 씬에 있는 큐브 게임 오브젝트 위에 배치합니다. 이 스크립트의 이름은 “LookAtPoint”로 지정합니다.

//C# Example (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);
}

그러면 오브젝트가 월드 공간의 한 포인트를 계속 향합니다. 현재 이 스크립트는 play mode에서, 즉 게임이 실행 중일 때만 활성화됩니다. 에디터 스크립트를 작성할 때 게임이 실행되지 않는 edit mode 중에도 특정 스크립트가 실행되도록 하면 유용한 경우가 있습니다. 이 경우 ExecuteInEditMode 속성을 추가하면 됩니다.

//C# Example (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” 값을 변경하면 재생 모드가 아닐 때도 오브젝트가 월드 공간의 타겟 포인트를 계속 바라보도록 오브젝트의 방향이 업데이트됩니다.

커스텀 에디터 생성

위의 예에는 편집 모드 중에 간단한 스크립트가 실행되도록 할 수 있는 방법이 나와 있지만, 이 방법만으로는 에디터 툴을 직접 만들 수 없습니다. 다음 단계에는 방금 작성한 스크립트를 위한 Custom Editor 를 만듭니다.

Unity에서 작성하는 스크립트는 기본적으로 MonoBehaviour에서 상속을 받고, 따라서 게임 오브젝트에 놓을 수 있는 컴포넌트입니다. 게임 오브젝트에 놓으면 인스펙터에 표시 가능한 모든 public 변수(정수, 플로트, 문자열, Vector3 등)를 보고 편집하는 데 사용되는 디폴트 인터페이스가 표시됩니다.

디폴트 인스펙터에서는 위의 스크립트를 다음과 같이 찾습니다.

퍼블릭 Vector3 필드가 있는 디폴트 인스펙터
퍼블릭 Vector3 필드가 있는 디폴트 인스펙터

커스텀 에디터는 이 디폴트 레이아웃을 사용자가 선택하는 에디터 컨트롤로 대체하는 별도의 스크립트입니다.

LookAtPoint 스크립트를 위한 커스텀 에디터를 만드려면 이름이 같지만 “Editor”가 추가된 다른 스크립트를 작성해야 합니다. 즉, 위 예에서는 “LookAtPointEditor”입니다.

//c# Example (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가 작동하는 것처럼 작동하지만 인스펙터 내에서 실행됩니다. 에디터는 검사 중인 오브젝트에 액세스하기 위해 사용할 수 있는 타겟 프로퍼티를 정의합니다. 커스텀 인스펙터는 다음과 같이 표시됩니다.

지금까지는 Vector3 필드를 디폴트 인스펙터에 표시되는 것과 똑같이 재현했을 뿐이므로 (“스크립트” 필드를 표시하기 위한 인스펙터 코드를 추가하지 않았기 때문에 이제는 이 필드가 없기는 하지만) 결과가 매우 비슷해 보여서 그다지 흥미롭지 않습니다.

하지만 이제는 인스펙터가 에디터 스크립트에 표시되는 방식을 제어할 수 있기 때문에 원하는 코드를 사용하여 인스펙터 필드를 레이아웃하고 값을 조정하거나 그래픽스 또는 다른 시각적 요소를 표시할 수도 있습니다. 실질적으로 터레인 시스템 및 애니메이션 임포트 설정 등 훨씬 복잡한 인스펙터를 포함한 Unity 에디터 내에 나타나는 모든 인스펙터는 커스텀 에디터를 생성하는 동안 액세스하는 동일한 API를 사용해 만들어집니다.

다음은 타겟 포인트가 게임 오브젝트 위에 있는지 아래에 있는지 나타내는 메시지를 표시하기 위해 에디터 스크립트를 확장하는 간단한 예입니다.

//c# Example (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# Example (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();
        }
    }
}

2D GUI 오브젝트(GUI, EditorGUI 및 동종 계열)를 입력하려면 Handles.BeginGUI() 및 Handles.EndGUI() 대상 호출에 래핑해야 합니다.

프로퍼티 드로어(Property Drawers)
트리뷰(TreeView)