커스텀 에디터 창을 사용하면 직접 에디터와 워크플로를 만들어 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);
}
}