제7장 EditorWindow

2019. 8. 11. 23:49유니티/(Old)에디터 확장

 

Unity エディター拡張入門

Web版無償公開中!Unityエディター拡張の入門書!

anchan828.github.io

에디터 확장을 처음 시작할 때 먼저 윈도우를 표시하는 것으로 시작합니다. 이 장에서는 간단하게 EditorWindow를 표시하는 방법을 목적으로 삼아 EditorWindow의 선택, 특성에 대해서 설명합니다.

7.1 EditorWindow란

씬 윈도우, 게임 윈도우, 인스펙터와 같이 이러한 것들은 전부 EditorWindow입니다. Unity 에디터는 여러 가지 기능을 가진 EditorWindow의 집합입니다.

그림 7.1 : 이들은 전부 EditorWindow

7.2 HostView와 SplitView와 DockArea

EditorWindow는 단일로 그려지는 것이 아닌 부모인 DockArea를 가지고 이 DockArea에 의해 EditorWindow가 그려집니다.

그림 7.2 : DockArea 위에 EditorWindow가 그려짐

DockArea는 Web 웹 브라우저의 탭과 동일한 기능을 제공합니다.

예를 들면, 윈도우는 각기 독립된 3개의 윈도우로써 제공할 수도 있고 3개의 탭으로 해서 1개의 윈도우로 포함하여 제공할 수도 있습니다.

그림 7.3 : Chrome의 탭 기능

보기에도 비슷하고 탭의 기능으로 탭을 집어서 별도의 윈도우로 취급하는 것도 가능합니다.

그림 7.4 : DockArea의 탭 기능

이와 같이 DockArea에는 1개 이상의 EditorWindow를 표시하기 위한 기능이 갖추어져 있습니다. 예를 들면 2개 이상의 EditorWindow가 DockArea에 있을 경우, 탭 기능을 사용하여 각 각의 EditorWindow로 표시하거나 SplitWindow를 통해서 DockArea의 영역을 분할하여 표시할 수 있습니다.

그림 7.5 : 하나의 DockArea에 씬 뷰와 계층 뷰를 분할하여 표시

추가적으로 DockArea는 HostView의 역할도 가지고 있습니다. HostView는 여러 가지 객체와 이벤트의 사이에 정보를 전달하기 위한 기능이 있습니다. 윈도우의 Update 함수, OnSelectionChange 함수에서 처리하기 위한 기능이 포함되어있습니다. 

3개의 윈도우인 HostView, SplitView, DockArea를 지금까지 소개하였습니다. 이들의 클래스는 직접 접근할 수 없습니다. 하지만 이 클래스를 기억해두면 EditorWindow의 구조를 이해하는데 도움이 됩니다.

7.3  EditorWindow의 작성

먼저 일반적인 EditorWindow를 작성해봅니다.

그림 7.6 : 표시했지만 아무것도 없는 EditorWindow

EditorWindow를 표시하기 전까지

EditorWindow를 표시하기 전까지 기본적으로 3개의 흐름의 구성으로 되어있습니다. 먼저 이 3개의 흐름을 알아보겠습니다.

 

1. EditorWindow를 작성하려면 EditorWindow를 계승한 클래스를 작성합니다.

using UnityEditor;
 
public class Example : EditorWindow
{
}

2. 다음은 EditorWindow를 표시하기 위해 트리거로써 메뉴를 추가합니다.

using UnityEditor;
 
public class Example : EditorWindow
{
    //MenuItem에 관해서는 제8장 「MenuItem」을 참고해주세요.
    [MenuItem("Window/Example")]
    static void Open ()
    {
    }
}

3. 마지막으로 EditorWindow를 표시합니다.

EditorWindow는 ScriptableObject를 계승하고 있기에 EditorWindow.CreateInstance로 EditorWndow의 객체를 생성할 수 있습니다. 그리고 생성된 객체를 Show 함수를 호출하여 EditorWindow를 표시합니다.

using UnityEditor;
 
public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        var exampleWindow = CreateInstance<Example> ();
        exampleWindow.Show ();
    }
}

그림 7.7 : "Window/Example" 메뉴를 실행해서 표시할 수 있습니다. 실행하려면 EditorWindow를 신규로 작성해야합니다.

7.4 EditorWindow.GetWindow

EditorWindow를 작성할 경우, 복수의 존재를 허가할 EditorWindow와 단일의 존재만 허가할 EditorWindow의 2종류를 생각할 수 있습니다. 복수의 존재를 허가할 EditorWindow는 앞서 설명한 EditorWindow.CreateInstance를 사용합니다.

 

단일의 존재만 허가할 EditorWindow

단일만 생성하려면 [이미 EditorWindow가 존재할 경우 생성하지 않는다]와 같은 체크를 처리를 하면 됩니다. 이 체크를 추가한 코드는 다음과 같습니다.

using UnityEditor;
 
public class Example : EditorWindow
{
    static Example exampleWindow;
 
    [MenuItem("Window/Example")]
    static void Open ()
    {
        if (exampleWindow == null) {
            exampleWindow = CreateInstance<Example> ();
        }
        exampleWindow.Show ();
    }
}

이렇게 해도 상관없지만 이미 EditorWindow가 존재하면 해당 인스턴스를 취득하고 그렇지 않으면 생성, 마지막으로 Show 함수를 호출한다와 같은 과정을 하나의 함수로 처리하는 EditorWindow.GetWindow라는 API가 존재합니다.

using UnityEditor;
 
public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        GetWindow<Example> ();
    }
}

GetWindow를 실행하면 내부에서 인스턴스가 캐시 됩니다. 그리고 GetWindow 함수에는 편리한 기능을 제공하는데 특정 EditorWindow에 탭 윈도우를 표시하는 것이 가능합니다.(DockArea에 EditorWindow를 추가)

그림 7.8 : 씬 윈도우에 탭 윈도우를 추가할 수 있습니다.

using UnityEditor;
 
public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        GetWindow<Example> (typeof(SceneView));
    }
}

7.5 특정 Show함수를 호출함에 따라서 달라지는 특수한 EditorWindow

지금까지 이 장에서 사용한 EditorWindow는 디폴트 상태의 탭 기능을 사용했습니다. 디폴트 이외에도 EditorWindow는 여러가지 종류의 윈도우를 만들 수가 있습니다.

 

Show

디폴드 상태의 탭 윈도우로써 사용됩니다. EditorWindow.GetWindow를 사용하는 경우 내부에서 Show가 호출됩니다.

 

ShowUtility

탭 윈도우로 사용하지 않고 통상 맨 앞에 표시를 하기 위해서 사용하는 윈도우입니다. 예를 들면 다른 윈도우에 포커스를 가지고 있어도 이 윈도우는 뒤쪽으로 빠지지 않습니다. 설정 윈도우와 같은 빈번히 다른 윈도우를 조작하더라도 맨 앞에 표시해야 될 경우 사용하면 됩니다.

그림 7.9 : 계층 뷰의 메인 카메라를 선택하더라도 윈도우는 맨 앞에 표시됩니다.

내부에서 Show를 호출하는 GetWindow로는 사용할 수 없으며 CreateInstance를 사용합니다.

using UnityEditor;
 
public class Example : EditorWindow
{
    static Example exampleWindow;
 
    [MenuItem("Window/Example")]
    static void Open ()
    {
        if (exampleWindow == null) {
            exampleWindow = CreateInstance<Example> ();
        }
        exampleWindow.ShowUtility ();
    }
}

ShowPopup

윈도우 타이틀과 닫기 버튼이 없는 윈도우입니다. 예를 들면 다른 윈도우에 포커스를 가지고 있어도 이 윈도우는 뒤쪽으로 빠지지 않습니다. 닫기 버튼이 없기 때문에 윈도우를 닫으려면 별도로 처리를 해야 합니다.

그림 7.10 : 씬 윈도우 위에 표시합니다.

using UnityEditor;
using UnityEngine;
 
public class Example : EditorWindow
{
    static Example exampleWindow;
 
    [MenuItem("Window/Example")]
    static void Open ()
    {
        if (exampleWindow == null) {
            exampleWindow = CreateInstance<Example> ();
        }
        exampleWindow.ShowPopup ();
    }
 
 
    void OnGUI ()
    {
        //ESC 키를 누르면 팝업이 닫힙니다.
        if (Event.current.keyCode == KeyCode.Escape) {
            exampleWindow.Close();
        }
    }
}

유니티에서 주로 사용되는 경우를 살펴보면 SpriteEditor와 같은 곳에서 슬라이스 메뉴 버튼을 클릭했을 때 Popup이 표시됩니다.

그림 7.11 : 이것도 EditorWindow의 예시

PopupWindow

바로 앞에서 소개한 ShowPopup과 거의 비슷하며 Popup을 표시하기 위한 기능입니다. PopupWindow는 popup을 범용적으로 다루기 위한 Window라고 생각해주세요.

그림 7.12 : 버튼을 눌렀을 때 아래와 같이 팝업이 표시됩니다.

사용 방법은 먼저 PopupWindowContent를 계승한 클래스를 작성합니다. 그리고 PopupWinodw.Show로 Popup을 표시합니다.

using UnityEditor;
using UnityEngine;
 
public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        GetWindow<Example> ();
    }
 
    //인스턴스 화
    ExamplePupupContent popupContent = new ExamplePupupContent ();
 
    void OnGUI ()
    {
        if (GUILayout.Button ("PopupContent",GUILayout.Width(128))) {
            var activatorRect = GUILayoutUtility.GetLastRect ();
            //Popup를 표시합니다.
            PopupWindow.Show (activatorRect, popupContent);
        }
    }
}
 
public class ExamplePupupContent : PopupWindowContent
{
    public override void OnGUI (Rect rect)
    {
        EditorGUILayout.LabelField ("Lebel");
    }
 
    public override void OnOpen ()
    {
        Debug.Log ("표시되었을 때 호출되는 함수입니다.");
    }
 
    public override void OnClose ()
    {
        Debug.Log ("닫혀질 때 호출되는 함수입니다.");
    }
 
    public override Vector2 GetWindowSize ()
    {
        //Popup의 크기
        return new Vector2 (300100);
    }
}

ShowAuxWindow

탭 윈도우로 사용하지 않는 단독 윈도우를 작성할 때 사용합니다. 모양은 ShowUtility와 같지만 다른 윈도우에 포커스가 가면 다른 윈도우에 가려져서 보이지 않는 것이 아니라 제거가 됩니다. 윈도우의 제거를 잊을 필요도 없고 수를 늘릴 필요도 없기에 설정이나 간단한 조작을 목적으로 사용하는 것을 추천합니다.

그림 7.13 : 단순히 보기에는 ShowUtility 인지 ShowAuxWindow인지 판단하기가 어렵습니다.

 

ShowAsDropDown

Popup과 동일하며 윈도우의 타이틀, 닫기 버튼이 없는 윈도우입니다. 다만 PC의 화면 사이즈를 고려하여 윈도우를 표시할 위치를 충분한 확보하지 못했을 경우 표시할 영역을 화면 내에 배치할 수 있도록 x, y축의 위치를 자동적으로 보정합니다. 즉 화면 구석에 윈도우를 두더라도 반드시 PC의 해상도 내에 위치가 맞춰집니다.

그림 7.14 : 검은색은 ShowAsDropDown로 표시된 윈도우입니다.

 

using UnityEditor;
using UnityEngine;
 
public class Example : EditorWindow
{
    static Example exampleWindow;
 
    [MenuItem("Window/Example")]
    static void Open ()
    {
        if (exampleWindow == null) {
            exampleWindow = CreateInstance<Example> ();
        }
 
        var buttonRect = new Rect (100100300100);
        var windowSize = new Vector2 (300100);
        exampleWindow.ShowAsDropDown (buttonRect, windowSize);
    }
}

이 것 이외에는 Popup과 동일한 기능입니다.

ScriptableWizard

GameObject, Prefab, 에셋과 같은 것들을 제작할 때 사용하는 윈도우입니다. ScriptableWizard는 지금까지 소개한 윈도우와 조금 다릅니다.

 

ScriptableWizard의 제작 방법

1. ScriptableWizard를 계승한 클래스를 작성합니다.

using UnityEditor;
 
public class Example : ScriptableWizard
{
}

2. 다음은 ScriptableWizard를 표시하기 위하여 트리거로써 메뉴를 추가합니다.

using UnityEditor;
 
public class Example : ScriptableWizard
{
    //MenuItem에 대해서는 제8장 MenuItem을 참고해주세요.
    [MenuItem("Window/Example")]
    static void Open ()
    {
    }
}

3. ScriptableWizard를 표시합니다. 표시는 ScriptableWizard.DisplayWizard로 표시할 수 있습니다.

using UnityEditor;
 
public class Example : ScriptableWizard
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        //매개변수는 윈도우의 타이틀 명입니다.
        DisplayWizard<Example> ("Example Wizard");
    }
}

그림 7.15 : 표준으로 오른쪽 하단에 Create 버턴이 표시됩니다.

ScriptableWizard에는 클래스의 필드를 표시할 수 있습니다.

다른 EditorWindow는 GUI의 표시를 EditorGUI 클래스를 사용하지만 ScriptableWizard에서는 사용할 수 없습니다.

ScriptableWizard에는 인스팩터에 표시하려면 public 필드 혹은 직렬화를 통하여 필드를 표시합니다.

그림 7.16 : 인스펙터에 표시된 필드

using UnityEditor;
 
public class Example : ScriptableWizard
{
    public string gameObjectName;
 
    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }
}

OnWizardCreate

ScriptableWizard의 오른쪽 하단에 있는 Create 버튼을 누렀을 때 호출되는 함수입니다.

using UnityEditor;
using UnityEngine;
 
public class Example : ScriptableWizard
{
    public string gameObjectName;
 
    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }
 
    void OnWizardCreate ()
    {
        new GameObject (gameObjectName);
    }
}

OnWizardOtherButton

Create 버튼 이외의 버튼을 하나 더 추가합니다. 추가적인 기능이 필요하여 2개의 버튼이 필요할 경우 사용합니다.

버튼을 추가하려면 ScriptableWizard.DisplayWizard의 3번째 인수에 버튼의 이름을 추가해서 넘겨주면 됩니다.

using UnityEditor;
using UnityEngine;
 
public class Example : ScriptableWizard
{
    public string gameObjectName;
 
    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard""Create""Find");
    }
 
    void OnWizardCreate ()
    {
        new GameObject (gameObjectName);
    }
 
    void OnWizardOtherButton ()
    {
        var gameObject = GameObject.Find (gameObjectName);
 
        if (gameObject == null)
        {
            Debug.Log ("해당 객체를 찾을 수가 없습니다.");
        }
    }
}

OnWizardUpdate

모든 필드의 값을 대상으로 값이 변경이 되었을 때 호출되는 함수입니다.

using UnityEditor;
using UnityEngine;
 
public class Example : ScriptableWizard
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }
 
    void OnWizardUpdate ()
    {
        Debug.Log ("Update");
    }
}

DrawWizardGUI

Wizard 내의 GUI를 그리기 위한 함수입니다. 이 함수를 오버라이드 하는 것으로 UI를 커스텀마이즈를 할 수 있습니다.

단 리턴 값으로 true를 반환을 해야합니다. true를 반환하지 않으면 OnWizardUpdate를 호출하지 않게 됩니다.

그림 7.17 : 지금까지 표시된 프로퍼티가 표시되지 않습니다.

using UnityEditor;
using UnityEngine;
 
public class Example : ScriptableWizard
{
    public string gameObjectName;
 
    [MenuItem ("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }
 
    protected override bool DrawWizardGUI ()
    {
        EditorGUILayout.LabelField ("Label");
        //false를 리턴하면 OnWizardUpdate가 호출되지 않습니다.
        return true;
    }
}

OnGUI 함수는 사용하면 안 됩니다.

ScriptableWizard 클래스는 EditorWindow를 계승하지 않습니다. 그렇기에 OnGUI 함수를 사용하면 EditorWindow를 표시하지 않을 뿐만 아니라 필드의 값, Create 버튼 역시 표시되지 않습니다.

그림 7.18 : OnGUI 함수를 기술하면 Create 버튼이 사라집니다.

PreferenceItem

PreferenceItem는 Unity Preferences의 메뉴를 추가하기 위한 기능입니다. Unity Preferences에는 Unity 에디터 전체의 영향을 줄 수 있는 설정을 하기 위해 존재합니다.

그림 7.19 : 추가된 메뉴는 최하단 위치에 추가됩니다.

using UnityEditor;
 
public class Example
{
    [PreferenceItem("Example")]
    static void OnPreferenceGUI ()
    {
 
    }
}

7.6 메뉴를 추가하는 IHasCustomMenu

탭 상단에 오른쪽 클릭, 혹은 ≡ 모양을 클릭하면 표시되는 컨텍스트 메뉴에 메뉴를 추가할 수 있습니다.

그림 7.20 : example 과 example2가 추가되었습니다.

IHasCustomMenu는 인터페이스로 처리됩니다.

using UnityEditor;
using UnityEngine;
 
public class Example : EditorWindow, IHasCustomMenu
{
 
    public void AddItemsToMenu (GenericMenu menu)
    {
        menu.AddItem (new GUIContent ("example"), false, () => {
 
        });
 
        menu.AddItem (new GUIContent ("example2"), true, () => {
 
        });
    }
 
    [MenuItem("Window/Example")]
    static void Open ()
    {
        GetWindow<Example> ();
    }
}

7.7 EditorWindow의 크기를 변경하지 못하게 하기

그림 7.21 오른쪽 하단에 크기를 조절하는 삼각 마크가 사라져 있습니다.

EditorWindow.minSize와 EditorWindow.maxSize에 의해 EditorWindow의 크기를 제한할 수 있습니다. 최솟값과 최댓값이 같을 경우 EditorWindow의 크기를 변경할 필요가 없다고 판단하여 오른쪽 하단에 표시되는 삼각 마크가 비 표시가 됩니다.

using UnityEditor;
using UnityEngine;
 
public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        var window = GetWindow<Example> ();
        window.maxSize = window.minSize = new Vector2 (300300);
    }
}

7.8 윈도우에 아이콘을 추가하기

아이콘을 추가하려면 EditorWindow.titleContent에 아이콘을 가진 GUIContent를 대입합니다.

그림 7.22 : 아이콘은 무척 작기 때문에 알기 쉬운 모양을 추천합니다.

using UnityEditor;
using UnityEngine;
 
public class Example : EditorWindow
{
    [MenuItem ("Window/Example")]
    static void SaveEditorWindow ()
    {
        var window = GetWindow<Example> ();
 
        var icon = AssetDatabase.LoadAssetAtPath<Texture> ("Assets/Editor/Example.png");
 
        window.titleContent = new GUIContent ("Hoge", icon);
    }
}

7.9 GetWindow를 사용하지 않고 이미 존재하는 EditorWindow를 취득하기

자체적으로 싱글톤을 처리하고 있는 GetWindow를 사용하면 내부에서 캐시 하여 EdiorWindow를 취득할 수 있습니다. 하지만 특정 상황에서는 GetWindow와 같은 방식을 사용할 수 없는 상황도 존재합니다. 바로 Resources 클래스에 있는 Resorces.FindObjectsOfTypeAll을 사용하는 경우입니다.

 

FindObjectsOfTypeAll는 현재 불러온 모든 객체로부터 특정 객체를 검색해 얻어옵니다. 이는 런타임 때 사용할 객체뿐만 아니라 에디터를 사용할 때도 객체를 얻어올 수 있는 함수입니다.

using UnityEditor;
using UnityEngine;
 
public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        //모든 씬 뷰를 취득
        var sceneViews = Resources.FindObjectsOfTypeAll<SceneView> ();
    }
}

7.10 EditorWindow도 ScriptableObject입니다.

그림 7.2 : 에셋 브라우저로 보면 ScriptableObject를 계승하고 있는 것을 알 수 있습니다.

EditorWindow는 ScriptableObject의 동작에 따라 EditorWindow 객체를 에셋으로 보존할 수 있습니다. 이렇게 보존된 에셋은 인스펙터에 직렬화되어 관련된 속성들을 표시합니다.

그림 7.24 : Example 윈도우를 에셋으로 보존하여 인스펙터에서 확인

using UnityEditor;
using UnityEngine;
public class Example : EditorWindow
{
    [MenuItem ("Assets/Save EditorWindow")]
    static void SaveEditorWindow ()
    {
        AssetDatabase.CreateAsset (CreateInstance<Example> (), "Assets/Example.asset");
        AssetDatabase.Refresh ();
    }
 
    [SerializeField]
    string text;
 
    [SerializeField]
    bool boolean;
}

윈도우의 위치, 크기 등을 보존합니다. 이후 데이터를 YAML 형식의 파일을 텍스트 에디터를 살펴보면 다음과 같습니다.

m_MinSize: {x: 100, y: 100}
m_MaxSize: {x:
4000, y: 4000}
m_TitleContent:
    m_Text: Example
    m_Image: {fileID:
0}
    m_Tooltip:
m_DepthBufferBits:
0
m_AntiAlias:
0
m_Pos:
    serializedVersion:
2
    x:

    y:
0
    width:
320
    height:
240

'유니티 > (Old)에디터 확장' 카테고리의 다른 글

제9장 CustomEditor  (1) 2019.08.14
제8장 MenuItem  (0) 2019.08.13
제 6장 EditorGUI (EditorGUILayout)  (0) 2019.07.28
제 5장 SerializedObject에 대해서  (0) 2019.07.28
제4장 ScriptableObject  (0) 2019.07.28