커스텀 에디터 창을 사용하면 직접 에디터와 워크플로를 만들어 Unity를 확장할 수 있습니다. 이 가이드는 코드를 사용하여 에디터 창을 만들고, 사용자 입력에 응답하고, UI의 크기를 조절할 수 있게 만들고 핫 리로드를 처리하는 방법을 설명합니다.
이 튜토리얼에서는 프로젝트 내 모든 스프라이트를 찾아 리스트로 표시하는 스프라이트 브라우저를 만듭니다. 리스트의 스프라이트를 선택하면 창 오른쪽에 이미지가 표시됩니다.
에디터 창 스크립트 섹션에서 완성된 예제를 확인할 수 있습니다.
이 가이드는 Unity에 익숙하지만 UI 툴킷은 처음 접하는 개발자를 위해 만들어졌습니다. Unity와 C# 스크립팅에 관한 기본적인 이해가 있는 것이 좋습니다.
또한 이 가이드는 다음 컨셉을 참조합니다.
이 가이드에서 사용된 컨트롤:
이 가이드에서는 다음을 수행해봅니다.
Tip |
---|
Unity 에디터에서 에디터 창 스크립트를 만드는 데 필요한 코드를 생성할 수 있습니다. 프로젝트 창을 마우스 오른쪽 버튼으로 클릭하여 Create > UI Toolkit > Editor Window를 선택하십시오. 이 가이드의 작업을 위해 UXML 및 USS 체크박스를 비활성화하십시오. 또한 아래 표시된 것처럼 파일 맨 위에 using 지시문을 더 추가해야 합니다. |
프로젝트의 C# 스크립트를 통해 에디터 창을 만들 수 있습니다. 커스텀 에디터 창은 EditorWindow
클래스에서 파생되는 클래스입니다.
Assets/Editor 폴더 아래에 새 스크립트 파일 MyCustomEditor.cs
를 만드십시오. 다음 코드를 스크립트에 붙여넣으십시오.
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
public class MyCustomEditor : EditorWindow
{
}
참고 |
---|
이 창은 UnityEditor 네임스페이스를 포함하는 에디터 전용 창이므로, Editor 폴더 아래 또는 에디터 전용 어셈블리 정의 안에 파일을 넣어야 합니다. |
새 에디터 창을 열려면 에디터 메뉴에 엔트리를 만들어야 합니다.
정적 메서드에 MenuItem
속성을 추가하십시오. 이 예제에서는 정적 메서드의 이름이 ShowMyEditor()
입니다.
ShowMyEditor()
내에서 EditorWindow.GetWindow() 메서드를 호출하여 창을 만들고 표시하십시오. 이 메서드는 EditorWindow 오브젝트를 반환합니다. 창의 제목을 설정하려면 EditorWindow.titleContent 프로퍼티를 변경하십시오.
이전 단계에서 만든 MyCustomEditor
클래스 내에 다음 함수를 추가하십시오.
[MenuItem("Tools/My Custom Editor")]
public static void ShowMyEditor()
{
// This method is called when the user selects the menu item in the Editor
EditorWindow wnd = GetWindow<MyCustomEditor>();
wnd.titleContent = new GUIContent("My Custom Editor");
}
Unity 에디터의 Tools > My Custom Editor 메뉴에서 새 창을 열어 테스트하십시오.
UI 툴킷은 CreateGUI 메서드를 사용하여 에디터 UI에 컨트롤을 추가하며, Unity는 창이 표시되어야 할 때 CreateGUI
메서드를 자동으로 호출합니다. 이 메서드는 Awake
또는 Update
와 같은 메서드와 동일한 방식으로 작동합니다.
시각적 트리에 시각적 요소를 추가하여 UI에 UI 컨트롤을 추가할 수 있습니다. VisualElement.Add() 메서드는 기존의 시각적 요소에 자식을 추가하는 데 사용됩니다. 에디터 창의 시각적 트리는 rootvisualElement
프로퍼티를 통해 액세스합니다.
시작하려면 커스텀 에디터 클래스에 CreateGUI()
함수를 추가한 다음 ‘Hello’ 레이블을 추가하십시오.
public void CreateGUI()
{
rootVisualElement.Add(new Label("Hello"));
}
참고 |
---|
스프라이트 리스트를 나타내려면 AssetDatabase 함수를 사용하여 프로젝트의 모든 스프라이트를 찾으십시오. |
CreateGUI()
내 코드를 아래 코드로 대체하여 프로젝트 내 모든 스프라이트를 열거하십시오.
public void CreateGUI()
{
// Get a list of all sprites in the project
var allObjectGuids = AssetDatabase.FindAssets("t:Sprite");
var allObjects = new List<Sprite>();
foreach (var guid in allObjectGuids)
{
allObjects.Add(AssetDatabase.LoadAssetAtPath<Sprite>(AssetDatabase.GUIDToAssetPath(guid)));
}
}
스프라이트 브라우저의 경우, 최상위 수준의 시각적 요소는 TwoPaneSplitView입니다. 이 컨트롤은 가용 창 공간을 크기가 고정된 창 하나와 크기를 조절할 수 있는 창 하나로 분할합니다. 창 크기를 조절하면 유연한 창의 크기만 조절되며, 크기가 고정된 창은 같은 크기를 유지합니다.
TwoPaneSplitView
컨트롤이 작동하려면 정확히 두 개의 자식이 있어야 합니다. TwoPaneSplitview
를 만들려면 CreateGUI()
내에 코드를 추가하고, 서로 다른 컨트롤의 플레이스홀더로 두 개의 자식 요소를 추가하십시오.
// Create a two-pane view with the left pane being fixed with
var splitView = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);
// Add the view to the visual tree by adding it as a child to the root element
rootVisualElement.Add(splitView);
// A TwoPaneSplitView always needs exactly two child elements
var leftPane = new VisualElement();
splitView.Add(leftPane);
var rightPane = new VisualElement();
splitView.Add(rightPane);
아래 이미지는 빈 패널이 두 개 있는 커스텀 창을 나타냅니다. 디바이더 바는 움직일 수 있습니다.
스프라이트 브라우저의 경우, 왼쪽 창은 프로젝트에서 찾는 모든 스프라이트의 이름이 포함된 리스트입니다. ListView 컨트롤은 VisualElement
에서 파생되므로, 코드를 간편하게 수정하여 빈 VisualElement
대신 ListView
를 사용할 수 있습니다.
CreateGUI()
함수 내 코드를 수정하여 VisualElement
대신 왼쪽 창을 위한 ListView
컨트롤을 만드십시오.
public void CreateGUI()
{
...
var leftPane = new ListView();
splitView.Add(leftPane);
...
}
ListView 컨트롤은 선택 가능한 항목의 리스트를 표시합니다. 이 컨트롤은 보이는 영역을 커버하기에 충분한 정도의 요소만 만들고, 리스트를 스크롤하면 시각적 요소를 모아 재사용하는 데 최적화되어 있습니다. 이를 통해 항목 수가 많은 리스트에서도 성능을 최적화하고 메모리 풋프린트를 줄일 수 있습니다.
이 컨트롤을 활용하려면 ListView
가 다음으로 적절하게 초기화되어야 합니다.
리스트의 각 요소를 위해 복잡한 UI 구조를 만들 수 있으나, 이 예제에서는 간단한 텍스트 레이블을 사용하여 스프라이트 이름을 표시합니다.
ListView
를 초기화하는 하단 CreateGUI()
함수에 코드를 추가하십시오.
public void CreateGUI()
{
...
// Initialize the list view with all sprites' names
leftPane.makeItem = () => new Label();
leftPane.bindItem = (item, index) => { (item as Label).text = allObjects[index].name; };
leftPane.itemsSource = allObjects;
}
아래 이미지는 스크롤 가능한 리스트 뷰와 선택 가능한 항목이 있는 에디터 창을 나타냅니다.
아래는 CreateGUI()
함수를 위한 현재 코드 전체입니다(참조용).
public void CreateGUI()
{
// Get a list of all sprites in the project
var allObjectGuids = AssetDatabase.FindAssets("t:Sprite");
var allObjects = new List<Sprite>();
foreach (var guid in allObjectGuids)
{
allObjects.Add(AssetDatabase.LoadAssetAtPath<Sprite>(AssetDatabase.GUIDToAssetPath(guid)));
}
// Create a two-pane view with the left pane being fixed with
var splitView = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);
// Add the panel to the visual tree by adding it as a child to the root element
rootVisualElement.Add(splitView);
// A TwoPaneSplitView always needs exactly two child elements
var leftPane = new ListView();
splitView.Add(leftPane);
var rightPane = new VisualElement();
splitView.Add(rightPane);
// Initialize the list view with all sprites' names
leftPane.makeItem = () => new Label();
leftPane.bindItem = (item, index) => { (item as Label).text = allObjects[index].name; };
leftPane.itemsSource = allObjects;
}
왼쪽 창의 리스트에서 스프라이트를 선택하면 스프라이트의 이미지가 오른쪽 창에 표시되어야 합니다. 이를 수행하려면 사용자가 스프라이트를 선택할 때 ListView
가 호출할 수 있는 콜백 함수를 제공해야 합니다. 이를 위해 ListView
에 onSelectionChange
프로퍼티가 있습니다.
콜백 함수는 사용자가 선택한 항목을 하나 이상 포함하는 리스트를 수신합니다. 여러 항목 선택을 허용하도록 ListView
를 설정할 수 있으나, 기본적으로 선택 모드는 항목 한 개로 제한됩니다.
사용자가 왼쪽 창에 있는 리스트에서 선택한 항목을 변경하면 콜백 함수를 추가하십시오.
public void CreateGUI()
{
...
// React to the user's selection
leftPane.onSelectionChange += OnSpriteSelectionChange;
}
private void OnSpriteSelectionChange(IEnumerable<object> selectedItems)
{
}
참고 |
---|
창이 유실되고 메뉴가 다시 열리지 않는다면 Window > Panels > Close all floating panels 메뉴에서 모든 플로팅 패널을 닫거나 창 레이아웃을 재설정하십시오. |
창 오른쪽에 선택한 스프라이트의 이미지를 표시하려면 함수가 TwoPaneSplitView
의 오른쪽 창에 액세스할 수 있어야 합니다. 콜백 함수 내에서 액세스할 수 있도록 이 컨트롤을 클래스의 멤버 변수로 만들 수 있습니다.
CreateGUI()
내에서 만든 rightPane
을 멤버 변수로 전환하십시오.
private VisualElement m_RightPane;
public void CreateGUI()
{
...
m_RightPane = new VisualElement();
splitView.Add(m_RightPane);
...
}
TwoPaneSplitView
에 대한 레퍼런스로 flexedPane
프로퍼티를 통해 오른쪽 창에 액세스할 수 있습니다.
오른쪽 창에 새 Image
컨트롤을 만들기 전에 VisualElement.Clear()
를 사용하여 이전 이미지를 제거하십시오. 이 메서드는 기존의 시각적 요소에서 모든 자식을 제거합니다.
이전의 모든 콘텐츠에서 오른쪽 창을 지우고 선택한 스프라이트를 위한 새 이미지 컨트롤을 만드십시오.
private void OnSpriteSelectionChange(IEnumerable<object> selectedItems)
{
// Clear all previous content from the pane
m_RightPane.Clear();
// Get the selected sprite
var selectedSprite = selectedItems.First() as Sprite;
if (selectedSprite == null)
return;
// Add a new Image control and display the sprite
var spriteImage = new Image();
spriteImage.scaleMode = ScaleMode.ScaleToFit;
spriteImage.sprite = selectedSprite;
// Add the Image control to the right-hand pane
m_RightPane.Add(spriteImage);
}
참고 |
---|
selectedItems 매개변수에 First() 메서드를 사용하려면 반드시 파일 맨 위에 using System.Linq; 를 포함하십시오. |
에디터에서 스프라이트 브라우저를 테스트하십시오. 아래 이미지는 실행 중인 커스텀 에디터 창을 나타냅니다.
허용된 최소 크기최대 크기 내에서 에디터 창의 크기를 조절할 수 있습니다. EditorWindow.minSize 및 EditorWindow.maxSize 프로퍼티에 작성하여 창을 만들 때 C#에서 이러한 크기를 설정할 수 있습니다. 창 크기가 조절되지 않도록 하려면 두 프로퍼티에 동일한 크기를 할당하십시오.
ShowMyEditor()
함수의 맨 아래에 다음 줄을 추가하여 커스텀 에디터 창의 크기를 제한하십시오.
[MenuItem("Tools/My Custom Editor")]
public static void ShowMyEditor()
{
// This method is called when the user selects the menu item in the Editor
EditorWindow wnd = GetWindow<MyCustomEditor>();
wnd.titleContent = new GUIContent("My Custom Editor");
// Limit size of the window
wnd.minSize = new Vector2(450, 200);
wnd.maxSize = new Vector2(1920, 720);
}
창 크기가 UI 전체를 표시하기에 너무 작은 경우, ScrollView
요소를 사용하여 창 스크롤 기능을 제공해야 합니다. 그렇지 않으면 콘텐츠에 액세스할 수 없습니다.
왼쪽 창의 ListView
는 내부적으로 ScrollView
를 사용하지만, 오른쪽 창은 일반 VisualElement
입니다. 이를 ScrollView
컨트롤로 변경하면 창이 이미지 전체를 원본 크기로 나타내기에 너무 작은 경우 자동으로 스크롤바가 표시됩니다.
오른쪽 창의 VisualElement
를 양방향 스크롤 기능이 있는 ScrollView
로 교체하십시오.
public void CreateGUI()
{
...
m_RightPane = new ScrollView(ScrollViewMode.VerticalAndHorizontal);
splitView.Add(m_RightPane);
...
}
아래 이미지는 스크롤 바가 있는 스프라이트 브라우저 창을 나타냅니다.
커스텀 에디터 창을 닫았다가 다시 열어 새로운 크기 제한을 테스트하십시오.
참고 |
---|
Unity 2021.2 이상 버전에서는 창이 도킹된 경우 Unity가 minSize 및 maxSize 프로퍼티를 따르지 않습니다. 따라서 사용자는 독 영역의 크기를 제한 없이 조절할 수 있습니다. ScrollView 를 최상위 수준 요소 중 하나로 만들고 모든 UI를 이 요소 안에 넣어 UI의 응답성을 최대화해보십시오. |
올바른 에디터 창은 Unity 에디터에서 일어나는 핫 리로드 워크플로와 함께 작동해야 합니다. 스크립트가 다시 컴파일되거나 에디터가 플레이 모드에 진입하면 C# 도메인 리로드가 발생합니다. 스크립트 직렬화 페이지에서 이 주제에 대해 자세히 알아보십시오.
방금 만든 에디터 창에서 이 액션을 보려면 스프라이트 브라우저를 열고, 스프라이트를 선택한 다음 플레이 모드에 진입하십시오. 창이 재설정되고 선택 항목이 사라집니다.
VisualElement
오브젝트는 직렬화할 수 없으므로, Unity에서 리로드가 발생할 때마다 UI를 재생성해야 합니다. 즉, 리로드가 완료된 후 CreateGUI()
메서드가 호출됩니다. 따라서 필요한 데이터를 EditorWindow
클래스에 저장하여 리로드 전에 UI 상태를 복원할 수 있습니다.
스프라이트 리스트에서 선택한 인덱스를 저장하려면 멤버 변수를 추가하십시오.
public class MyCustomEditor : EditorWindow
{
[SerializeField] private int m_SelectedIndex = -1;
....
}
항목을 선택할 때, 리스트 뷰의 새 선택 인덱스를 이 멤버 변수 내에 저장할 수 있습니다. CreateGUI()
함수 내에 UI를 만드는 중에 선택 인덱스를 복원할 수 있습니다.
선택한 리스트 인덱스를 저장하고 복원하려면 CreateGUI()
함수 끝부분에 코드를 추가하십시오.
public void CreateGUI()
{
...
// Restore the selection index from before the hot reload
leftPane.selectedIndex = m_SelectedIndex;
// Store the selection index when the selection changes
leftPane.onSelectionChange += (items) => { m_SelectedIndex = leftPane.selectedIndex; };
}
리스트에서 스프라이트를 선택하고 플레이 모드에 진입하여 핫 리로드를 테스트하십시오.
아래의 코드는 이 가이드에서 생성한 에디터 창의 최종 스크립트입니다.Assets/Editor 폴더 안에 있는 MyCustomEdtior.cs
파일에 이 코드를 바로 붙여넣으면 만든 에디터 창을 Unity 에디터에서 볼 수 있습니다.
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
public class MyCustomEditor : EditorWindow
{
[SerializeField] private int m_SelectedIndex = -1;
private VisualElement m_RightPane;
[MenuItem("Tools/My Custom Editor")]
public static void ShowMyEditor()
{
// This method is called when the user selects the menu item in the Editor
EditorWindow wnd = GetWindow<MyCustomEditor>();
wnd.titleContent = new GUIContent("My Custom Editor");
// Limit size of the window
wnd.minSize = new Vector2(450, 200);
wnd.maxSize = new Vector2(1920, 720);
}
public void CreateGUI()
{
// Get a list of all sprites in the project
var allObjectGuids = AssetDatabase.FindAssets("t:Sprite");
var allObjects = new List<Sprite>();
foreach (var guid in allObjectGuids)
{
allObjects.Add(AssetDatabase.LoadAssetAtPath<Sprite>(AssetDatabase.GUIDToAssetPath(guid)));
}
// Create a two-pane view with the left pane being fixed with
var splitView = new TwoPaneSplitView(0, 250, TwoPaneSplitViewOrientation.Horizontal);
// Add the panel to the visual tree by adding it as a child to the root element
rootVisualElement.Add(splitView);
// A TwoPaneSplitView always needs exactly two child elements
var leftPane = new ListView();
splitView.Add(leftPane);
m_RightPane = new ScrollView(ScrollViewMode.VerticalAndHorizontal);
splitView.Add(m_RightPane);
// Initialize the list view with all sprites' names
leftPane.makeItem = () => new Label();
leftPane.bindItem = (item, index) => { (item as Label).text = allObjects[index].name; };
leftPane.itemsSource = allObjects;
// React to the user's selection
leftPane.onSelectionChange += OnSpriteSelectionChange;
// Restore the selection index from before the hot reload
leftPane.selectedIndex = m_SelectedIndex;
// Store the selection index when the selection changes
leftPane.onSelectionChange += (items) => { m_SelectedIndex = leftPane.selectedIndex; };
}
private void OnSpriteSelectionChange(IEnumerable<object> selectedItems)
{
// Clear all previous content from the pane
m_RightPane.Clear();
// Get the selected sprite
var selectedSprite = selectedItems.First() as Sprite;
if (selectedSprite == null)
return;
// Add a new Image control and display the sprite
var spriteImage = new Image();
spriteImage.scaleMode = ScaleMode.ScaleToFit;
spriteImage.sprite = selectedSprite;
// Add the Image control to the right-hand pane
m_RightPane.Add(spriteImage);
}
}