무엇을 찾고 계신가요?
Engine & platform

IMGUI 및 편집기 사용자 지정에 대해 자세히 알아보기

RICHARD FINE / UNITY TECHNOLOGIESContributor
Dec 22, 2015|32 분
IMGUI 및 편집기 사용자 지정에 대해 자세히 알아보기
이 웹페이지는 이해를 돕기 위해 기계 번역으로 제공됩니다. 기계 번역으로 제공되는 콘텐츠에 대한 정확도나 신뢰도는 보장되지 않습니다. 번역된 콘텐츠의 정확도에 관해 의문이 있는 경우 웹페이지의 공식 영어 원문을 참고해 주시기 바랍니다.
새로운 Unity UI 시스템이 출시된 지 1년이 넘었습니다. 그래서 이전 UI 시스템인 IMGUI에 대한 블로그 포스팅을 해볼까 생각했습니다.

타이밍이 이상하다고 생각할 수도 있습니다. 새로운 UI 시스템을 사용할 수 있는데 왜 이전 UI 시스템에 신경을 써야 하나요? 새로운 UI 시스템은 모든 게임 내 사용자 인터페이스 상황에 대응하기 위한 것이지만, 특히 Unity 에디터와 같은 매우 중요한 상황에서는 여전히 IMGUI가 사용됩니다. 커스텀 툴과 기능으로 Unity 에디터를 확장하는 데 관심이 있다면 IMGUI를 직접 사용해야 할 가능성이 높습니다.

즉시 진행

그럼 첫 번째 질문입니다: 왜 'IMGUI'라고 하나요? IMGUI는 즉시 모드 GUI의 줄임말입니다. 좋아요, 그게 뭔가요? GUI 시스템에는 크게 두 가지 접근 방식이 있습니다: '즉시'와 '유지'입니다.

유지 모드 GUI는 레이블, 버튼, 슬라이더, 텍스트 필드 등 다양한 GUI 위젯을 설정하면 시스템이 해당 정보를 유지하여 화면을 렌더링하고 이벤트에 응답하는 등의 작업을 수행하는 데 사용하는 GUI 시스템입니다. 라벨의 텍스트를 변경하거나 버튼을 이동하려는 경우 어딘가에 저장된 일부 정보를 조작하는 것이며, 변경을 완료하면 시스템은 새로운 상태로 계속 작동합니다. 사용자가 값을 변경하고 슬라이더를 움직이면 시스템은 변경 사항을 저장하기만 하면 되고, 값을 쿼리하거나 콜백에 응답하는 것은 사용자의 몫입니다. 새로운 Unity UI 시스템은 리텐션 모드 GUI의 예시로, UI.Labels, UI.Buttons 등을 컴포넌트로 생성하고 설정한 다음 그대로 두면 새 UI 시스템이 나머지 작업을 자동으로 처리합니다.

반면 즉시 모드 GUI는 일반적으로 GUI 시스템이 사용자의 GUI에 대한 정보를 유지하지 않고 대신 컨트롤이 무엇인지, 어디에 있는지 등을 다시 지정하도록 반복적으로 요청하는 GUI입니다. 함수 호출의 형태로 UI의 각 부분을 지정하면 그리기, 클릭 등 모든 사용자 상호작용의 결과가 쿼리할 필요 없이 즉시 처리되어 바로 사용자에게 반환됩니다. 이는 게임 UI에는 비효율적이고 모든 것이 코드에 크게 의존하기 때문에 아티스트가 작업하기에는 불편하지만, 실시간이 아닌 상황(예: 에디터 패널)에서는 매우 편리하며(에디터 패널처럼) 현재 상태에 따라 표시되는 컨트롤을 쉽게 변경하고 싶은 경우(예: 무거운 건설 장비!) 좋은 선택이 될 수 있습니다. 아니, 잠깐만요. 에디터 패널을 위한 좋은 선택이라는 뜻입니다.

더 자세히 알고 싶으시다면 즉시 모드 GUI의 장점과 원리에 대해 설명하는 Casey Muratori의 멋진 동영상을 참고하세요. 아니면 그냥 계속 읽어도 됩니다!

모든 이벤트 결과

IMGUI 코드가 실행될 때마다 '사용자가 마우스 버튼을 클릭했습니다' 또는 'GUI를 다시 칠해야 합니다'와 같은 현재 '이벤트' 가 처리되고 있습니다. Event.current.type을 확인하여 현재 이벤트가 무엇인지 확인할 수 있습니다.

창 어딘가에서 버튼 세트를 만들고 있는데 '사용자가 마우스 버튼을 클릭했습니다'와 'GUI를 다시 그려야 합니다'에 응답하기 위해 별도의 코드를 작성해야 한다면 어떤 모습일지 상상해 보세요. 블록 수준에서는 다음과 같이 보일 수 있습니다:

GUI 다이어그램 1

각각의 개별 GUI 이벤트에 대해 이러한 함수를 작성하는 것은 다소 지루하지만, 함수 간에 구조적 유사성이 있다는 것을 알 수 있습니다. 각 단계마다 동일한 컨트롤(버튼 1, 버튼 2 또는 버튼 3)과 관련된 작업을 수행합니다. 정확히 무엇을 하는지는 이벤트에 따라 다르지만 구조는 동일합니다. 대신 이렇게 할 수 있다는 뜻입니다:

GUI 다이어그램 2

GUI.Button과 같은 라이브러리 함수를 호출하는 단일 OnGUI 함수가 있으며, 이러한 라이브러리 함수는 처리하는 이벤트에 따라 다른 작업을 수행합니다. 간단합니다!

가장 많이 사용되는 이벤트 유형은 5가지입니다:

사용자가 마우스 버튼을 방금 눌렀을 때 설정하는 EventType.MouseDown.사용자가 마우스 버튼을 방금 눌렀을 때 설정하는 EventType.MouseUp.사용자가 마우스 버튼을 방금 놓았을 때 설정하는 EventType.KeyDown.사용자가 키를 방금 놓았을 때 설정하는 EventType.KeyUp.사용자가 키를 놓았을 때 설정하는 EventType.Repaint.IMGUI가 화면을 다시 그려야 할 때 설정하는 이벤트 유형입니다.

이 목록은 전체 목록이 아니므로 자세한 내용은 이벤트 유형 설명서를 참조하세요.

GUI.Button과 같은 표준 컨트롤은 이러한 이벤트에 어떻게 반응할 수 있을까요?

EventType.Repaint제공된 직사각형에 버튼을 그립니다.EventType.MouseDown버튼의 직사각형 안에 마우스가 있는지 확인합니다. 그렇다면 버튼을 아래로 플래그를 지정하고 다시 그리기를 트리거하여 버튼이 눌린 대로 다시 그려지도록 합니다.EventType.MouseUp 버튼을 아래로 플래그를 지정 해제하고 다시 그리기를 트리거한 다음 마우스가 여전히 버튼의 사각형 안에 있는지 확인하고, 그렇다면 true를 반환하여 호출자가 버튼 클릭에 응답할 수 있도록 합니다.

버튼이 키보드 이벤트에도 반응하고 처음에 클릭한 버튼만 마우스 업에 반응하도록 하는 코드가 있는 등 현실은 이보다 더 복잡하지만, 대략적인 아이디어를 얻을 수 있습니다. 이러한 각 이벤트에 대해 코드의 동일한 지점에서 동일한 위치와 내용으로 GUI.Button을 호출하기만 하면 서로 다른 동작이 함께 작동하여 버튼의 모든 기능을 제공할 수 있습니다.

서로 다른 이벤트에서 이러한 다양한 동작을 하나로 묶는 데 도움을 주기 위해 IMGUI에는 '제어 ID'라는 개념이 있습니다. 컨트롤 ID의 개념은 모든 이벤트 유형에서 주어진 컨트롤을 일관되게 참조할 수 있는 방법을 제공하는 것입니다. 사소하지 않은 인터랙티브 동작이 있는 UI의 각 부분은 컨트롤 ID를 요청하게 되는데, 이는 현재 키보드 포커스가 있는 컨트롤을 추적하거나 컨트롤과 관련된 소량의 정보를 저장하는 데 사용됩니다. 컨트롤 ID는 단순히 요청하는 순서대로 컨트롤에 부여되므로, 다른 이벤트에서 같은 순서로 동일한 GUI 함수를 호출하는 한 결국 동일한 컨트롤 ID가 부여되고 다른 이벤트가 동기화됩니다.

사용자 지정 제어 수수께끼

커스텀 에디터 클래스, 에디터 윈도우 클래스 또는 프로퍼티 드로어 클래스를 직접 제작하려는 경우, GUI 클래스와 에디터GUI 클래스는 Unity 전체에서 사용할 수 있는 유용한 표준 컨트롤 라이브러리를 제공합니다.

(초보 에디터 코더가 GUI 클래스를 간과하는 것은 흔한 실수이지만, 해당 클래스의 컨트롤은 에디터 확장 시 EditorGUI의 컨트롤과 마찬가지로 자유롭게 사용할 수 있습니다. GUI와 EditorGUI에는 특별히 특별한 점은 없습니다. 단지 사용할 수 있는 두 개의 컨트롤 라이브러리일 뿐입니다. 하지만 차이점은 EditorGUI의 컨트롤은 코드가 에디터의 일부인 반면 GUI는 엔진 자체의 일부이므로 게임 빌드에서 사용할 수 없다는 점입니다.)

하지만 표준 라이브러리에서 제공되는 것 이상의 작업을 하고 싶다면 어떻게 해야 할까요?

사용자 지정 사용자 인터페이스 컨트롤을 만드는 방법을 살펴보겠습니다. 이 작은 데모에서 색상 상자를 클릭하고 드래그해 보세요:

(참고: 여기에 임베드된 원본 WebGL 애플리케이션은 더 이상 브라우저에서 작동하지 않습니다.)

(데모를 보려면 최신 버전의 Firefox와 같이 WebGL을 지원하는 브라우저가 필요합니다).

이러한 사용자 지정 슬라이더는 각각 0과 1 사이의 개별 '실수' 값을 구동합니다. 예를 들어 우주선 오브젝트의 여러 부분에 대한 선체 무결성을 표시하는 또 다른 방법으로 인스펙터에서 1은 '손상 없음', 0은 '완전히 파괴됨'을 나타내는데, 막대가 색으로 값을 나타내면 우주선의 상태를 한 눈에 쉽게 파악할 수 있습니다. 이를 다른 컨트롤처럼 사용할 수 있는 사용자 정의 IMGUI 컨트롤로 빌드하는 코드는 매우 간단하므로 함께 살펴 보겠습니다.

첫 번째 단계는 함수 시그니처를 결정하는 것입니다. 다양한 이벤트 유형을 모두 다루기 위해서는 제어에 세 가지가 필요합니다:

- 스스로 그려야 할 위치와 마우스 클릭에 반응해야 할 위치를 정의하는 Rect입니다.

- 막대가 나타내는 현재 부동 소수점 값입니다.

- 컨트롤에 필요한 간격, 글꼴, 텍스처 등에 대한 필요한 정보가 포함된 GUIStyle입니다. 여기서는 막대를 그리는 데 사용할 텍스처를 포함합니다. 이 매개 변수에 대해서는 나중에 자세히 설명합니다.

또한 사용자가 막대를 드래그하여 설정한 값을 반환해야 합니다. 이는 마우스 이벤트와 같은 특정 이벤트에서만 의미가 있고 다시 칠하기 이벤트와 같은 이벤트에서는 의미가 없으므로 기본적으로 호출 코드가 전달한 값을 반환합니다. 호출 코드에서 발생하는 이벤트에 신경 쓰지 않고 "value = MyCustomSlider(... value ...)"를 수행할 수 있으므로 사용자가 설정한 새 값을 반환하지 않는다면 현재 값을 보존해야 한다는 것입니다.

따라서 결과 서명은 다음과 같습니다:

public static float MyCustomSlider(Rect controlRect, float value, GUIStyle style)

이제 함수 구현을 시작합니다. 첫 번째 단계는 제어 ID를 검색하는 것입니다. 마우스 이벤트에 응답할 때 특정 용도로 사용하겠습니다. 그러나 처리 중인 이벤트가 실제로 관심 있는 이벤트가 아니더라도 이 특정 이벤트에 대한 다른 제어에 할당되지 않도록 하기 위해 어쨌든 ID를 요청해야 합니다. IMGUI에서는 요청된 순서대로 ID를 제공하기 때문에 ID를 요청하지 않으면 다음 컨트롤에 대신 제공되어 해당 컨트롤이 다른 이벤트에 대해 다른 ID를 갖게 되어 깨질 가능성이 높다는 점을 기억하세요. 따라서 ID를 요청할 때는 모든 이벤트 유형에 대해 ID를 요청하거나 전혀 요청하지 않을 수 있습니다(매우 단순하거나 비대화형인 컨트롤을 만드는 경우에는 괜찮을 수 있습니다).

{
	int controlID = GUIUtility.GetControlID (FocusType.Passive);

여기서 파라미터로 전달되는 FocusType.Passive는 키보드 탐색에서 이 컨트롤이 어떤 역할을 하는지, 즉 키 누름에 반응하는 현재 컨트롤이 될 수 있는지 여부를 IMGUI에 알려줍니다. 내 사용자 지정 슬라이더는 키 누름에 전혀 반응하지 않으므로 패시브가 지정되어 있지만 키 누름에 반응하는 컨트롤은 네이티브 또는 키보드를 지정할 수 있습니다. 이에 대한 자세한 내용은 FocusType 문서를 확인하세요.

다음으로, 대부분의 사용자 지정 컨트롤이 구현의 어느 시점에서 수행하는 작업을 수행합니다. 스위치 문을 사용하여 이벤트 유형에 따라 분기합니다. 이벤트 유형을 필터링하여 예를 들어 특정 상황에서 키보드 이벤트가 잘못된 컨트롤로 전송되는 것을 방지하기 위해 Event.current .type을 직접 사용하는 대신 Event.current.GetTypeForControl()을 사용하여 컨트롤 ID를 전달합니다. 하지만 모든 것을 필터링하는 것은 아니므로 자체적으로도 몇 가지 확인 작업을 수행해야 합니다.

switch (Event.current.GetTypeForControl(controlID))
	{

이제 다양한 이벤트 유형에 대한 구체적인 동작 구현을 시작할 수 있습니다. 컨트롤 그리기부터 시작하겠습니다:

case EventType.Repaint:
		{
			// Work out the width of the bar in pixels by lerping
			int pixelWidth = (int)Mathf.Lerp (1f, controlRect.width, value);

			// Build up the rectangle that the bar will cover
			// by copying the whole control rect, and then setting the width
			Rect targetRect = new Rect (controlRect){ width = pixelWidth };

			// Tint whatever we draw to be red/green depending on value
			GUI.color = Color.Lerp (Color.red, Color.green, value);

			// Draw the texture from the GUIStyle, applying the tint
			GUI.DrawTexture (targetRect, style.normal.background);

			// Reset the tint back to white, i.e. untinted
			GUI.color = Color.white;

			break;
		}

이 시점에서 함수를 완성하면 0과 1 사이의 실수 값을 시각화할 수 있는 '읽기 전용' 컨트롤이 작동하게 됩니다. 하지만 계속 진행하여 인터랙티브한 컨트롤을 만들어 보겠습니다.

컨트롤에 쾌적한 마우스 동작을 구현하려면 컨트롤을 클릭하고 드래그하기 시작하면 마우스를 컨트롤 위에 계속 올려놓을 필요가 없어야 한다는 요구사항이 있습니다. 사용자가 커서가 수평으로 어디에 있는지에만 집중할 수 있고 수직 이동에 대해 걱정하지 않아도 되니 훨씬 더 편리합니다. 즉, 드래그하는 동안 다른 컨트롤 위로 마우스를 움직일 수 있으며, 사용자가 버튼을 다시 놓을 때까지 마우스를 무시하는 컨트롤이 필요합니다.

이에 대한 해결책은 GUIUtility.hotControl을 사용하는 것입니다. 마우스를 캡처한 컨트롤의 컨트롤 ID를 저장하기 위한 간단한 변수일 뿐입니다. IMGUI에서는 이 값을 GetTypeForControl()에서 사용하며, 0이 아닌 경우 전달되는 컨트롤 ID가 핫컨트롤이 아닌 한 마우스 이벤트는 필터링됩니다.

따라서 핫컨트롤을 설정하고 재설정하는 것은 매우 간단합니다:

case EventType.MouseDown:
		{
			// If the click is actually on us...
			if (controlRect.Contains (Event.current.mousePosition)
			// ...and the click is with the left mouse button (button 0)...
			 && Event.current.button == 0)
				// ...then capture the mouse by setting the hotControl.
				GUIUtility.hotControl = controlID;

			break;
		}

		case EventType.MouseUp:
		{
			// If we were the hotControl, we aren't any more.
			if (GUIUtility.hotControl == controlID)
				GUIUtility.hotControl = 0;

			break;
		}

다른 컨트롤이 핫 컨트롤인 경우, 즉 GUIUtility.hotControl이 0과 자체 컨트롤 ID가 아닌 다른 컨트롤인 경우, GetTypeForControl()이 마우스업/마우스다운 이벤트 대신 '무시'를 반환하므로 이러한 경우는 실행되지 않습니다.

핫컨트롤을 설정하는 것은 괜찮지만, 마우스가 내려간 상태에서 값을 변경하는 것은 아직 아무것도 하지 않았습니다. 이를 수행하는 가장 간단한 방법은 실제로 스위치를 닫은 다음 핫컨트롤에 있는 동안(따라서 클릭+드래그 중이지만 위의 경우 핫컨트롤을 0으로 만들었으므로 릴리즈는 아니지만) 발생하는 모든 마우스 이벤트(클릭, 끌기 또는 놓기)가 값을 변경해야 한다고 말하는 것입니다:

if (Event.current.isMouse && GUIUtility.hotControl == controlID) {

		// Get mouse X position relative to left edge of the control
		float relativeX = Event.current.mousePosition.x - controlRect.x;

		// Divide by control width to get a value between 0 and 1
		value = Mathf.Clamp01 (relativeX / controlRect.width);

		// Report that the data in the GUI has changed
		GUI.changed = true;

		// Mark event as 'used' so other controls don't respond to it, and to
		// trigger an automatic repaint.
		Event.current.Use ();
	}

마지막 두 단계인 GUI.changed 설정과 Event.current.Use() 호출은 이 컨트롤이 올바르게 작동하도록 하는 것뿐만 아니라 다른 IMGUI 컨트롤 및 기능과 잘 작동하도록 하는 데에도 특히 중요합니다. 특히 GUI.changed를 true로 설정하면 호출 코드에서 EditorGUI.BeginChangeCheck( ) 및 EditorGUI.EndChangeCheck( ) 함수를 사용하여 사용자가 실제로 컨트롤의 값을 변경했는지 여부를 감지할 수 있지만, 이전 컨트롤의 값이 변경되었다는 사실을 숨길 수 있으므로 GUI.changed를 false로 설정하지 않아야 합니다.

마지막으로 함수에서 값을 반환해야 합니다. 수정된 실수 값 또는 변경된 사항이 없는 경우 원래 값을 반환한다고 말씀드린 것을 기억하실 것입니다:

return value;
}

이제 끝났습니다. MyCustomSlider는 이제 사용자 정의 편집기, 속성 서랍, 편집기 창 등에서 사용할 수 있는 간단한 기능의 IMGUI 컨트롤입니다. 멀티 편집 지원 등 아직 개선할 수 있는 부분이 더 있지만, 이에 대해서는 아래에서 자세히 설명하겠습니다.

감당할 수 있는 것 이상

IMGUI에서 특히 중요하지만 명확하지 않은 한 가지가 더 있는데, 바로 씬 뷰와의 관계입니다. 오브젝트를 이동, 회전 및 크기 조정할 때 씬 뷰에 그려지는 도우미 UI 요소인 직교 화살표, 링, 클릭하고 드래그하여 오브젝트를 조작할 수 있는 상자 안의 선은 모두 익숙하실 것입니다. 이러한 UI 요소를 '핸들'이라고 합니다.

분명하지 않은 것은 핸들도 IMGUI로 구동된다는 것입니다!

결국 지금까지 IMGUI에 대해 설명한 내용에는 2D나 에디터/편집기 윈도우에만 국한된 것은 없습니다. 물론 GUI 및 EditorGUI 클래스에서 찾을 수 있는 표준 컨트롤은 모두 2D이지만, 이벤트 유형 및 컨트롤 ID와 같은 기본 개념은 2D에 전혀 의존하지 않습니다. 따라서 GUI와 EditorGUI가 인스펙터의 컴포넌트용 에디터윈도우와 에디터를 대상으로 하는 2D 컨트롤을 제공하는 반면, 핸즈 클래스는 씬 뷰에서 사용하기 위한 3D 컨트롤을 제공합니다. EditorGUI.IntField가 사용자가 단일 정수를 편집할 수 있는 컨트롤을 그리는 것처럼, 다음과 같은 함수가 있습니다:

벡터3 위치 핸들(벡터3 위치, 쿼터니언 회전);

를 사용하면 씬 뷰에서 대화형 화살표 세트를 제공하여 시각적으로 Vector3 값을 편집할 수 있습니다. 이전과 마찬가지로 사용자 정의 사용자 인터페이스 요소를 그리는 자체 핸들 함수를 정의할 수도 있습니다. 마우스가 사각형 안에 있는지 여부를 확인하는 것만으로는 충분하지 않기 때문에 마우스 상호작용을 처리하는 것은 조금 더 복잡하지만 - 핸들 유틸리티 클래스가 도움이 될 수 있습니다 - 기본 구조와 개념은 모두 동일합니다.

커스텀 에디터 클래스에 OnSceneGUI 함수를 제공하면 거기서 핸들 함수를 사용하여 씬 뷰에 그릴 수 있으며, 예상대로 월드 스페이스에 올바르게 배치됩니다. 커스텀 에디터와 같은 2D 컨텍스트에서 핸들을 사용하거나 씬 뷰에서 GUI 함수를 사용할 수 있지만, 사용하기 전에 GL 행렬을 설정하거나 Handles.BeginGUI()Handles.EndGUI()를 호출하여 컨텍스트를 설정하는 등의 작업이 필요할 수 있습니다.

GUInion 현황

내 사용자 정의 슬라이더의 경우, 추적해야 할 정보는 슬라이더의 현재 값(사용자가 전달하고 반환한 값)과 사용자가 값을 변경하는 중인지 여부(핫컨트롤을 효과적으로 사용하여 추적) 두 가지뿐이었습니다. 하지만 제어가 그보다 더 많은 정보를 보관해야 한다면 어떻게 해야 할까요?

IMGUI에서는 컨트롤과 연결된 '상태 객체'를 위한 간단한 저장 시스템을 제공합니다. 값을 저장하기 위한 자체 클래스를 정의한 다음, 컨트롤의 ID와 연결된 인스턴스를 관리하도록 IMGUI에 요청합니다. 컨트롤 ID당 하나의 상태 객체만 허용되며, 직접 인스턴스화하지 않고 상태 객체의 기본 생성자를 사용하여 IMGUI가 대신 인스턴스화합니다. 또한 상태 객체는 코드를 다시 컴파일할 때마다 발생하는 에디터 코드를 다시 로드할 때 직렬화되지 않으므로 수명이 짧은 작업에만 사용해야 합니다. (상태 객체를 [직렬화 가능]으로 표시해도 마찬가지입니다. 직렬화기는 힙의 이 특정 구석을 방문하지 않습니다).

다음 예시를 확인해 보겠습니다. 버튼을 누를 때마다 참으로 반환되지만 2초 이상 누르고 있으면 빨간색으로 깜박이는 버튼이 필요하다고 가정해 보겠습니다. 버튼을 처음 눌렀던 시간을 추적해야 하는데, 이를 상태 객체에 저장하여 추적할 수 있습니다. 여기 상태 객체 클래스가 있습니다:

public class FlashingButtonInfo
{
      private double mouseDownAt;

      public void MouseDownNow()
      {
      		mouseDownAt = EditorApplication.timeSinceStartup;
      }

      public bool IsFlashing(int controlID)
      {
            if (GUIUtility.hotControl != controlID)
                  return false;

            double elapsedTime = EditorApplication.timeSinceStartup - mouseDownAt;
            if (elapsedTime < 2f)
                  return false;

            return (int)((elapsedTime - 2f) / 0.1f) % 2 == 0;
      }
}

MouseDownNow()가 호출될 때 마우스를 누른 시간을 'mouseDownAt'에 저장한 다음 IsFlashing 함수를 사용하여 '지금 버튼이 빨간색으로 변해야 하는지'를 알려줍니다. 보시다시피 핫컨트롤이 아니거나 클릭 후 2초 미만이면 빨간색이 아니지만 그 이후에는 0.1초마다 색이 변하도록 합니다.

실제 버튼 컨트롤 자체에 대한 코드는 다음과 같습니다:

public static bool FlashingButton(Rect rc, GUIContent content, GUIStyle style)
{
        int controlID = GUIUtility.GetControlID (FocusType.Native);

        // Get (or create) the state object
        var state = (FlashingButtonInfo)GUIUtility.GetStateObject(
                                             typeof(FlashingButtonInfo),
                                             controlID);

        switch (Event.current.GetTypeForControl(controlID)) {
                case EventType.Repaint:
                {
                        GUI.color = state.IsFlashing (controlID)
                            ? Color.red
                            : Color.white;
                        style.Draw (rc, content, controlID);
                        break;
                }
                case EventType.MouseDown:
                {
                        if (rc.Contains (Event.current.mousePosition)
                         && Event.current.button == 0
                         && GUIUtility.hotControl == 0)
                        {
                                GUIUtility.hotControl = controlID;
                                state.MouseDownNow();
                        }
                        break;
                }
                case EventType.MouseUp:
                {
                        if (GUIUtility.hotControl == controlID)
                                GUIUtility.hotControl = 0;
                        break;
                }
        }

        return GUIUtility.hotControl == controlID;
}

마우스다운/마우스업의 경우의 코드는 위의 커스텀 슬라이더에서 마우스를 캡처할 때 사용한 것과 매우 유사하다는 것을 알 수 있습니다. 유일한 차이점은 마우스를 아래로 누를 때 state.MouseDownNow()를 호출하고 다시 칠하기 이벤트에서 GUI.color를 변경하는 것입니다.

눈썰미가 좋은 분들은 다시 그리기 이벤트에 스타일.Draw() 호출이라는 또 다른 중요한 차이점이 있다는 것을 눈치챘을 것입니다. 무슨 일이죠?

스타일로 GUI 작업하기

커스텀 슬라이더 컨트롤을 빌드할 때 GUI.DrawTexture를 사용하여 막대 자체를 그렸습니다. 정상적으로 작동했지만 깜박이는 버튼에는 버튼 자체인 '둥근 직사각형' 이미지 외에 캡션이 있어야 합니다. GUI.DrawTexture를 사용하여 버튼 이미지를 그린 다음 그 위에 GUI.Label을 사용하여 캡션을 그릴 수도 있지만 더 나은 방법을 사용할 수 있습니다. GUI.Label이 자체적으로 그리는 데 사용하는 것과 동일한 기술을 사용하여 중간자를 잘라낼 수 있습니다.

GUIStyle에는 GUI 요소의 시각적 속성에 대한 정보(사용해야 하는 글꼴이나 텍스트 색상과 같은 기본적인 것부터 간격을 얼마나 두어야 하는지와 같은 보다 미묘한 레이아웃 속성까지)가 포함되어 있습니다. 이 모든 정보는 스타일을 사용하여 일부 콘텐츠의 너비와 높이를 계산하는 함수 및 실제로 콘텐츠를 화면에 그리는 함수와 함께 GUIStyle에 저장됩니다.

실제로 GUIStyle은 컨트롤에 대해 하나의 스타일만 처리하는 것이 아니라 마우스를 올려놓았을 때, 키보드 포커스가 있을 때, 비활성화되었을 때, '활성' 상태(예: 버튼이 누르고 있는 중일 때) 등 GUI 요소가 처할 수 있는 다양한 상황에 따라 렌더링을 처리할 수 있습니다. 이러한 모든 상황에 대한 색상 및 배경 이미지 정보를 제공하면 GUIStyle이 컨트롤 ID를 기반으로 그리기 시점에 적절한 것을 선택합니다.

컨트롤을 그리는 데 사용할 수 있는 GUIStyle을 얻는 방법은 크게 네 가지입니다:

- 코드에서 하나를 생성하고(새로운 GUIStyle()) 그 위에 값을 설정합니다.

- 에디터스타일 클래스의 기본 제공 스타일 중 하나를 사용합니다. 자신만의 도구 모음, 인스펙터 스타일 컨트롤 등을 그리는 등 사용자 지정 컨트롤을 기본 제공 컨트롤처럼 보이게 하려면 이곳을 살펴보세요.

- 기존 스타일에 약간의 변형(예: 일반 버튼이지만 텍스트가 오른쪽으로 정렬된 스타일)만 만들고 싶다면 EditorStyles 클래스에서 스타일을 복제(새로운 GUIStyle(기존 스타일))한 다음 변경하려는 속성만 변경하면 됩니다.

- GUISkin에서 검색합니다.

GUISkin은 기본적으로 GUIStyle 오브젝트의 큰 묶음이며, 중요한 것은 프로젝트에서 에셋으로 생성하고 인스펙터를 통해 자유롭게 편집할 수 있다는 점입니다. 컨트롤을 만들고 살펴보면 상자, 버튼, 레이블, 토글 등 모든 표준 컨트롤 유형에 대한 슬롯이 표시되지만 사용자 정의 컨트롤 작성자는 하단의 '사용자 정의 스타일' 섹션에 주의를 기울이세요. 여기에서 사용자 지정 GUIStyle 항목을 원하는 만큼 설정하고 각 항목에 고유한 이름을 지정한 다음 나중에 GUISkin.GetStyle("nameOfCustomStyle")을 사용하여 해당 항목을 검색할 수 있습니다. 퍼즐에서 빠진 유일한 조각은 애초에 코드에서 GUISkin 오브젝트를 가져오는 방법을 알아내는 것입니다. 스킨을 '에디터 기본 리소스' 폴더에 보관하는 경우 EditorGUIUtility.LoadRequired()를 사용할 수 있으며, 프로젝트의 다른 곳에서 로드하기 위해 AssetDatabase.LoadAssetAtPath( ) 같은 메서드를 사용할 수도 있습니다. (편집기 전용 에셋을 실수로 에셋 번들이나 리소스 폴더에 넣지 마세요!).

GUIStyle로 무장하면 GUIStyle.Draw()를 사용하여 텍스트, 아이콘, 도구 설명이 혼합된 GUIContent를 그릴 수 있으며, 그리려는 사각형, 그리려는 GUIContent, 콘텐츠에 키보드 포커스 같은 것이 있는지 파악하는 데 사용할 컨트롤 ID를 전달할 수 있습니다.

포지션 배치

지금까지 살펴보고 작성한 모든 GUI 컨트롤에는 화면에서 컨트롤의 위치를 결정하는 Rect 매개변수가 포함되어 있다는 것을 눈치챘을 것입니다. GUIStyle에 대해 설명했으니 이제 GUIStyle에 "필요한 간격과 같은 레이아웃 속성"이 포함되어 있다고 했을 때 잠시 멈칫하셨을 것입니다. 이렇게 생각할 수도 있습니다: "어, 이런. 간격 값이 준수되도록 Rect 값을 계산하기 위해 많은 작업을 해야 한다는 뜻인가요?"

하지만 더 쉬운 방법도 있습니다. IMGUI에는 간격 등을 고려하여 컨트롤에 적합한 렉트 값을 자동으로 계산하는 '레이아웃' 메커니즘이 포함되어 있습니다. 그렇다면 어떻게 작동할까요?

비결은 컨트롤이 응답할 이벤트 유형 값을 추가로 지정하는 것입니다: EventType.Layout. IMGUI가 이벤트를 GUI 코드로 전송하면 호출한 컨트롤은 IMGUI 레이아웃 함수( GUILayoutUtility.GetRect(), GUILayout. BeginHorizonal/VerticalGUILayout. EndHorizonal/Vertical 등)를 호출하여 응답하며, IMGUI가 기록하여 레이아웃의 컨트롤 트리와 필요한 공간을 효과적으로 구축합니다. 트리가 완성되고 트리가 완전히 빌드되면 IMGUI가 트리를 재귀적으로 통과하여 요소의 실제 너비와 높이, 서로에 대한 위치를 계산하고 연속적인 컨트롤을 서로 옆에 배치하는 등의 작업을 수행합니다.

그런 다음 EventType.Repaint 이벤트 또는 실제로 다른 종류의 이벤트를 수행할 때가 되면 컨트롤은 동일한 IMGUI 레이아웃 함수를 호출합니다. 이번에는 호출을 기록하는 대신 IMGUI가 이전에 레이아웃 이벤트에 기록한 호출을 '재생'하여 계산한 사각형을 반환합니다. 레이아웃 이벤트 중에 GUILayoutUtility.GetRect()를 호출하여 사각형이 필요하다는 것을 등록한 후 다시 그리기 이벤트 중에 다시 호출하면 실제로 사용해야 하는 사각형을 반환하는 방식입니다.

컨트롤 ID와 마찬가지로 레이아웃 이벤트와 다른 이벤트 간에 레이아웃 호출에 일관성을 유지해야 합니다. 그렇지 않으면 잘못된 컨트롤에 대해 계산된 직사각형을 검색하게 됩니다. 또한 이벤트가 완료되고 레이아웃 트리가 처리될 때까지 IMGUI가 제공해야 하는 사각형을 실제로 알지 못하기 때문에 레이아웃 이벤트 중에 GUILayoutUtility.GetRect()가 반환하는 값은 쓸모가 없습니다.

커스텀 슬라이더 컨트롤은 어떤 모습일까요? IMGUI에서 직사각형을 가져오면 이미 작성한 코드를 호출하기만 하면 되므로 실제로 레이아웃 지원 버전의 컨트롤을 매우 쉽게 작성할 수 있습니다:

public static float MyCustomSlider(float value, GUIStyle style)
{
	Rect position = GUILayoutUtility.GetRect(GUIContent.none, style);
	return MyCustomSlider(position, value, style);
}

레이아웃 이벤트 중에는 주어진 스타일을 사용하여 빈 콘텐츠(공간을 만들어야 할 특정 텍스트나 이미지가 없기 때문에 비어 있는 상태)를 그리는 데 사용하고, 다른 이벤트 중에는 사용할 실제 사각형을 검색합니다. 이것은 레이아웃 이벤트 중에 가짜 직사각형을 사용하여 MyCustomSlider를 호출한다는 것을 의미하지만, 레이아웃 이벤트 중에 GetControlID()에 대한 일반적인 호출이 이루어지고 직사각형이 실제로 아무것도 사용되지 않도록 하기 위해 여전히 그렇게 해야 한다는 것을 의미합니다.

'비어 있는' 콘텐츠와 스타일만 주어졌을 때 IMGUI가 실제로 슬라이더의 크기를 어떻게 계산할 수 있는지 궁금할 것입니다. 필요한 모든 정보가 지정된 스타일에 의존하고 있으며, IMGUI가 할당할 사각형을 계산하는 데 사용할 수 있기 때문에 정보가 많지 않습니다. 하지만 사용자가 이를 제어할 수 있도록 하거나 스타일에서 고정된 높이를 사용하되 사용자가 너비를 제어할 수 있도록 하려면 어떻게 해야 할까요? 어떻게 할 수 있을까요?

그 답은 GUILayoutOption 클래스에 있습니다. 이 클래스의 인스턴스는 특정 사각형이 특정 방식으로 계산되어야 한다는 레이아웃 시스템에 대한 지시문(예: "높이 30" 또는 "공간을 채우기 위해 가로로 확장되어야 함" 또는 "너비가 20픽셀 이상이어야 함")을 나타냅니다. GUILayout 클래스에서 팩토리 함수( GUILayout.ExpandWidth(), GUILayout.MinHeight( ) 등)를 호출하여 생성한 다음, 이를 배열로 GUILayoutUtility.GetRect()에 전달합니다. 레이아웃 트리에 저장되며 레이아웃 이벤트가 끝날 때 트리가 처리될 때 고려됩니다.

사용자가 직접 배열을 생성하고 관리할 필요 없이 원하는 만큼의 GUILayoutOption 인스턴스를 쉽게 제공할 수 있도록, C# 'params' 키워드를 활용하여 원하는 수의 파라미터를 전달하는 메서드를 호출하고 해당 파라미터가 자동으로 배열에 패킹되도록 할 수 있습니다. 이제 수정된 슬라이더를 소개합니다:

public static float MyCustomSlider(float value, GUIStyle style, params GUILayoutOption[] opts)
{
	Rect position = GUILayoutUtility.GetRect(GUIContent.none, style, opts);
	return MyCustomSlider(position, value, style);
}

보시다시피, 사용자가 제공한 모든 것을 가져와서 GetRect에 전달하기만 하면 됩니다.

여기서 사용한 접근 방식(수동으로 배치된 IMGUI 컨트롤 함수를 자동 레이아웃 버전으로 래핑하는 방식)은 GUI 클래스에 내장된 컨트롤을 포함한 거의 모든 IMGUI 컨트롤에 적용됩니다. 실제로 GUILayout 클래스는 바로 이 접근 방식을 사용하여 GUI 클래스에서 컨트롤의 자동 레이아웃 버전을 제공합니다(그리고 EditorGUI 클래스에서 컨트롤을 래핑하기 위해 해당하는 EditorGUILayout 클래스도 제공합니다). 자체 IMGUI 컨트롤을 빌드할 때 이 트윈 클래스 규칙을 따르는 것이 좋습니다.

또한 자동 레이아웃과 수동 위치 지정 컨트롤을 혼합하여 사용할 수도 있습니다. GetRect를 호출하여 공간을 예약한 다음 자체 계산을 수행하여 해당 사각형을 하위 사각형으로 분할한 다음 여러 컨트롤을 그리는 데 사용할 수 있습니다. 레이아웃 시스템은 컨트롤 ID를 사용하지 않으므로 레이아웃 사각형당 여러 컨트롤(또는 컨트롤당 여러 레이아웃 사각형)이 있어도 아무런 문제가 없습니다. 때로는 레이아웃 시스템을 완전히 사용하는 것보다 훨씬 빠를 수 있습니다.

또한 PropertyDrawers를 작성하는 경우 레이아웃 시스템을 사용해서는 안 되며, PropertyDrawer.OnGUI() 오버라이드로 전달된 직사각형을 사용해야 한다는 점에 유의하세요. 그 이유는 에디터 클래스 자체가 성능상의 이유로 실제로 레이아웃 시스템을 사용하지 않고 단순한 직사각형 자체를 계산하여 연속되는 각 프로퍼티에 대해 아래로 이동하기 때문입니다. 따라서 PropertyDrawer에서 레이아웃 시스템을 사용하면 이전에 그려진 프로퍼티에 대한 지식이 없어 결국 그 위에 위치하게 됩니다. 여러분이 원하는 것은 아닙니다!

리루 댈러스 멀티 프로퍼티

지금까지 설명한 모든 내용을 통해 매우 원활하게 작동하는 자신만의 IMGUI 컨트롤을 구축할 수 있습니다. Unity에 내장된 컨트롤과 동일한 수준으로 빌드한 콘텐츠를 다듬고 싶을 때를 대비하여 몇 가지 사항을 더 논의할 수 있습니다.

첫 번째는 SerializedProperty를 사용하는 것입니다. 이 글에서 SerializedProperty 시스템에 대해 너무 자세히 설명하기보다는 다음 기회에 다루도록 하되 간단히 요약해 보겠습니다: SerializedProperty는 Unity의 직렬화(로드 및 저장) 시스템에서 처리하는 단일 변수를 '래핑'합니다. 인스펙터에 표시되는 모든 스크립트의 모든 변수는 물론 인스펙터에 표시되는 모든 엔진 오브젝트의 모든 변수는 적어도 에디터에서 SerializedProperty API를 통해 액세스할 수 있습니다.

SerializedProperty는 변수 값에 대한 액세스뿐만 아니라 변수의 값이 그 변수가 나온 프리팹의 값과 다른지, 자식 필드(예: 구조체)가 있는 변수가 인스펙터에서 확장 또는 축소되는지 등의 정보도 제공하므로 유용합니다. 또한 값에 대한 모든 변경 사항을 실행 취소 및 장면 더티 시스템에 통합합니다. 관리되는 버전의 개체를 실제로 만들지 않고도 이 작업을 수행할 수 있으므로 성능에 큰 도움이 될 수 있습니다. 따라서 실행 취소, 씬 더티, 프리팹 오버라이드 등 다양한 에디터 기능으로 IMGUI 컨트롤을 쉽고 멋지게 구현하려면 SerializedProperty를 지원해야 합니다.

SerializedProperty를 인수로 받는 EditorGUI 메서드를 살펴보면 서명이 약간 다르다는 것을 알 수 있습니다. 이전 커스텀 슬라이더의 '플로트를 취하고 플로트를 반환하는' 접근 방식 대신, SerializedProperty 지원 IMGUI 컨트롤은 SerializedProperty 인스턴스를 인수로 취하고 아무것도 반환하지 않습니다. 값에 변경이 필요한 경우 SerializedProperty 자체에 직접 적용하면 되기 때문입니다. 따라서 이전의 사용자 지정 슬라이더는 이제 다음과 같이 보일 수 있습니다:

public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style)

반환값과 함께 사용하던 'value' 매개변수가 사라지고 대신 'prop' 매개변수가 SerializedProperty에 전달할 수 있게 되었습니다. 슬라이더 막대를 그리기 위해 프로퍼티의 현재 값을 검색하려면 prop.floatVue에 액세스하고 사용자가 슬라이더 위치를 변경하면 prop.floatVue에 할당하기만 하면 됩니다.

하지만 IMGUI 제어 코드에 전체 SerializedProperty를 포함하면 다른 이점이 있습니다. 예를 들어 프리팹 인스턴스에서 수정된 프로퍼티가 굵은 글씨로 표시되는 방식을 생각해 보세요. Serial화된 프로퍼티에서 prefabOverride 프로퍼티를 확인하고, 참이면 컨트롤을 다르게 표시하는 데 필요한 작업을 수행하면 됩니다. 다행히도 텍스트를 굵게 만드는 것이 정말 원하는 전부라면 그림을 그릴 때 GUIStyle에서 글꼴을 지정하지 않는 한 IMGUI가 자동으로 처리해줍니다. (GUIStyle에 글꼴을 지정하는 경우 일반 및 굵은 글꼴 버전을 준비하고 그리기를 원할 때 prefabOverride에 따라 글꼴을 선택하는 등 직접 처리해야 합니다).

또 다른 주요 기능은 다중 개체 편집 지원, 즉 컨트롤에 여러 값을 동시에 표시해야 할 때 우아하게 처리하는 기능입니다. EditorGUI.showMixedValue의 값을 확인하여 이를 테스트하고, 참이면 컨트롤이 여러 개의 다른 값을 동시에 표시하는 데 사용되고 있으므로 이를 표시하는 데 필요한 작업을 수행합니다.

볼드온 프리팹 오버라이드와 쇼믹싱된 값 메커니즘은 모두 프로퍼티에 대한 컨텍스트가 EditorGUI.BeginProperty()EditorGUI.EndProperty( ) 를 사용하여 설정되어 있어야 합니다. 권장되는 패턴은 제어 메서드가 SerializedProperty를 인수로 받는 경우 BeginProperty와 EndProperty 자체를 호출하는 반면, '원시' 값을 처리하는 경우(예: 인트를 직접 받아 반환하고 프로퍼티와 작동하지 않는 EditorGUI.IntField와 유사) 호출 코드가 BeginProperty와 EndProperty 호출을 담당한다는 것입니다. (실제로 컨트롤이 '원시' 값을 처리하는 경우 어쨌든 BeginProperty로 전달할 수 있는 SerializedProperty 값이 없으므로 의미가 있습니다).

public class MySliderDrawer : PropertyDrawer
{
    public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
    {
        return EditorGUIUtility.singleLineHeight;
    }

    private GUISkin _sliderSkin;

    public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    {
        if (_sliderSkin == null)
            _sliderSkin = (GUISkin)EditorGUIUtility.LoadRequired ("MyCustomSlider Skin");

        MyCustomSlider (position, property, _sliderSkin.GetStyle ("MyCustomSlider"), label);

    }
}

// Then, the updated definition of MyCustomSlider:
public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style, GUIContent label)
{
    label = EditorGUI.BeginProperty (controlRect, label, prop);
    controlRect = EditorGUI.PrefixLabel (controlRect, label);

    // Use our previous definition of MyCustomSlider, which we’ve updated to do something
    // sensible if EditorGUI.showMixedValue is true
    EditorGUI.BeginChangeCheck();
    float newValue = MyCustomSlider(controlRect, prop.floatValue, style);
    if(EditorGUI.EndChangeCheck())
        prop.floatValue = newValue;

    EditorGUI.EndProperty ();
}
여기까지입니다.

이 포스팅을 통해 편집기 사용자 지정 기능을 한 단계 더 발전시키고 싶다면 이해해야 할 IMGUI의 핵심 부분에 대해 조금이나마 도움이 되었기를 바랍니다. 에디터 전문가가 되기 전에 다뤄야 할 내용은 SerializedObject/SerializedProperty 시스템, 커스텀 에디터와 에디터윈도우 및 프로퍼티 드로어 사용, 실행 취소 처리 등 더 많지만, 에셋 스토어 판매와 팀 내 개발자의 역량 강화를 위해 커스텀 툴 제작에 대한 유니티의 무한한 잠재력을 실현하는 데 IMGUI가 큰 역할을 담당하고 있습니다.

댓글로 질문과 피드백을 남겨주세요!