Begin
Custom Editor Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(SomeScript))]
public class SomeScriptEditor : UnityEditor.Editor
{
private SomeScript ss;
private void OnEnable()
{
ss = target as SomeScript;
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
}
}
#endif
Note
- UnityEngine.Editor 타입은 ScriptableObject를 상속받는다.
Memo
GUI와 GUILayout
- GUI, EditorGUI
- Rect(x, y, w, h)를 직접 지정하여 윈도우에 그린다.
- 수동으로 그리는 만큼, 번거롭지만 그만큼 자유도가 높다.
- EditorGUIUtility를 이용해 현재 환경의 영역 크기 등을 가져와 사용해야 한다.
- Rect로 그려낸 높이만큼 GUILayout의 Space를 직접 호출하여 윈도우 영역을 넓혀줘야 한다.
- GUILayout, EditorGUILayout
- 영역을 직접 지정할 수 없고, 컨트롤마다 정해진 만큼의 영역이 자동으로 할당된다.
- 자동으로 영역이 그려지는 만큼, 편리하지만 그만큼 자유도가 낮다.
값의 수정과 SerializedObject
- SerializedObject를 수정하는 경우
OnInspectorGUI()
상단에서.Update()
메소드를 호출하여 연결된 오브젝트의 값을 항상 받아온 상태에서 GUI 필드를 그려야 한다.- GUI가 변경된 경우,
OnInspectorGUI()
하단에서.ApplyModifiedProperties()
메소드를 호출하여 변경사항을 연결된 오브젝트에 적용해야 한다. - Undo는 자동으로 적용된다.
- SerializedObject가 아니라 대상 컴포넌트의 필드 값을 직접 수정하는 경우
Undo.RecordObject(대상 컴포넌트, "")
를 호출한 뒤, 값을 수정해야 한다.
필드 값의 보존
- 모든 필드(동적, 정적)가 값을 잃어버리는 경우
- 플레이모드 진입 시
- 컴파일 시
- 동적 필드만 값을 잃어버리는 경우
- 플레이모드 종료 시
- 정적 필드의 값을 유지하고 싶은 경우
- EditorPrefs 이용
- 정적 생성자에서 EditorPrefs의 값을 정적 필드에 적용
- ChangeCheck를 통해, GUI에서 값이 변경될 때마다 정적 필드의 값을 EditorPrefs에 적용
- 동적 필드의 값을 유지하고 싶은 경우
Play Mode Saver
의 방식 사용- SerializedObject에 값을 캐싱해놨다가, 값을 잃어버리는 타이밍(예 : 정적 생성자)에 SO로부터 값을 복원
API
GUI
1
2
3
4
// 버튼 그리기
// 주의 : 레이아웃 요소가 아니므로 Space 직접 추가해야 함
GUI.Button(new Rect(0f, 0f, 100f, 20f), "Button");
GUILayout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/***********************************************************
/* NOTE
/***********************************************************
/* 공통 2번째 파라미터 : GUIStyle
/* EditorStyles.~ 를 통해 다양한 스타일을 곧바로 지정 가능
/*
/* 공통 3번째 파라미터 : params GUILayoutOption[]
/* GUILayout.~ 를 통해 너비, 높이 지정 가능(width, height)
************************************************************/
// 기본 버튼
GUILayout.Button("Button"); // Width: 자동으로 최대치, Height : 19f
GUILayout.Button("Button", GUILayout.Width(200f)); // Width 직접 지정
// 마우스 클릭 유지하는 동안 true 값을 리턴하는 버튼
// 실제로는 누르는 순간, 떼는 순간, 누른 채로 마우스가 스치는 동안에만 true
GUILayout.RepeatButton("Repeat Button");
// 레이블
GUILayout.Label("Label");
// 단순 박스 그리기
// 너비, 높이를 따로 지정하지 않을 경우 : 텍스트에 맞춰서 자동으로 크기 설정됨
GUILayout.Box("Box", GUILayout.Width(EditorGUIUtility.currentViewWidth));
// 기본 체크박스 스타일의 토글
boolField = GUILayout.Toggle(boolField, "Bool Field");
// 버튼 스타일의 토글
boolField = GUILayout.Toggle(boolField, boolField ? "on" : "off", "button");
// 가로로 나열된 토글버튼들 표시(동시에 1개만 선택 가능)
intField = GUILayout.Toolbar(intField, new[] { "A", "B", "C" });
// EditorStyles.toolbar~ 를 통해 다양한 모습의 툴바 지정 가능
intField = GUILayout.Toolbar(intField, new[] { "A", "B", "C" }, EditorStyles.toolbarButton);
// 한 줄을 입력할 수 있는 텍스트 필드(주의사항 : stringFIeld 기본 값이 null이면 안됨)
stringField = GUILayout.TextField(stringField);
stringField = GUILayout.TextField(stringField, 10); // Max Length : 10
// 한 줄 비밀번호 필드
stringField = GUILayout.PasswordField(stringField, '*');
stringField = GUILayout.PasswordField(stringField, '*', 10); // Max Length : 10
// 가로 영역
using (new GUILayout.HorizontalScope())
{
// ..
}
// 세로 영역
using (new GUILayout.VerticalScope())
{
// ..
}
// 스크롤뷰 영역
using (new GUILayout.ScrollViewScope(vector2Field))
{
}
GUILayoutUtility
1
2
3
// 가장 최근에 그린 컨트롤의 Rect 정보 얻어오기
GUILayoutUtility.GetLastRect();
EditorGUI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// GUI 변화 여부 관찰
using (var check = new EditorGUI.ChangeCheckScope())
{
// Draw Somthing..
if (check.changed)
{ } // ..
}
// 비활성화 영역 지정
// 매개변수 true : 비활성화, false : 활성화
using (new EditorGUI.DisabledGroupScope(true))
{
// Draw Something..
}
EditorGUILayout
1
EditorGUIUtility
1
2
// 현재 에디터 GUI의 너비값
EditorGUIUtility.currentViewWidth;
Useful Source Codes
Color Scope
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/// <summary> 영역 내에서 컨텐츠 색상, 배경 색상을 지정한다.
/// <para/> null로 지정한 색상은 영향을 주지 않는다.
/// </summary>
public class ColorScope : GUI.Scope
{
private readonly Color? originalContentColor;
private readonly Color? originalBackgroundColor;
public ColorScope(Color? contentColor, Color? backgroundColor)
{
if (contentColor != null)
{
originalContentColor = GUI.contentColor;
GUI.contentColor = contentColor.Value;
}
else
originalContentColor = null;
if (backgroundColor != null)
{
originalBackgroundColor = GUI.backgroundColor;
GUI.backgroundColor = backgroundColor.Value;
}
else
originalBackgroundColor = null;
}
protected override void CloseScope()
{
if(originalContentColor != null)
GUI.contentColor = originalContentColor.Value;
if (originalBackgroundColor != null)
GUI.backgroundColor = originalBackgroundColor.Value;
}
}
편리하게 사용할 수 있는 Editor Pref 전용 구조체
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/***********************************************************************
* Editor Prefs
***********************************************************************/
#region .
private struct BoolPref
{
private readonly bool defaultValue;
public bool value;
public string name; // Pref 이름
public BoolPref(string name, bool defaultValue)
{
this.name = name;
this.value = this.defaultValue = defaultValue;
}
public static implicit operator bool(BoolPref other) => other.value;
public void SaveToEditorPref()
=> EditorPrefs.SetBool(name, value);
public void LoadFromEditorPref()
=> value = EditorPrefs.GetBool(name, defaultValue);
public void Set(bool newValue)
=> value = newValue;
}
private static BoolPref foldout = new BoolPref("Example_Foldout_", true);
// 플레이모드 진입 시, 컴파일 시 값 다시 읽어와 복원
[InitializeOnLoadMethod]
private static void LoadPrefValues()
{
foldoutA.LoadFromEditorPref();
}
// GUI 내에서 bool 값이 변화하는 경우에 Set(value)로 값 초기화,
// ChangeCheck로 감지 후 SaveToEditorPref()로 EditorPref에 변경사항 저장
#endregion
수동(GUI) 요소와 자동(GUILayout) 요소 함께 다루기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/***********************************************************************
* Manual Editor Control
***********************************************************************/
#region .
/// <summary> [수동] 현재 그려질 컨트롤의 Y 위치 </summary>
private float currentY = 0f; // 반드시 OnInspectorGUI() 상단에서 0으로 초기화
/// <summary> [수동, 자동(Layout)] 모두 Y 공백 삽입 </summary>
private void NextSpace(float value)
{
GUILayout.Space(value);
currentY += value;
}
/// <summary> [수동] Y 공백 삽입</summary>
private void NextY(float value)
{
currentY += value;
}
// GUI 요소를 그리고 난 후의 Space는 NextSpace() 호출
// GUILayout 요소를 그리고 난 후에는 NextY() 호출
#endregion
열고 닫을 수 있는 적당히 무난하게 생긴 박스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// boxY : 그려질 Y 위치
// boxH : 헤더를 제외한, 순수한 박스의 높이(펼쳐졌을 때 표시될 박스 높이)
// foldout : Foldout 상태를 나타낼 bool 필드
// 리턴값 : 현재 Foldout 여부 bool 값
/// <summary> 헤더(Foldout) + 박스 그리기 </summary>
private bool DrawFoldoutHeaderBox(float boxY, float boxH, bool foldout, string titleText,
in Color boxColor, in Color headerColor, in Color titleColor)
{
const float boxX = 14f;
const float padding = 4f;
const float headerX = boxX + padding;
const float headerH = 18f;
// 헤더 높이 + 패딩 * 2
float headerAreaH = headerH + padding * 2f;
float headerY = boxY + padding;
float viewWidth = EditorGUIUtility.currentViewWidth;
float headerW = viewWidth - headerX * 2f + 12f;
float boxW = viewWidth - boxX * 2f + 12f;
// 펼쳤을 때만 박스 보여주기
boxH = foldout ? (boxH + headerAreaH) : (headerAreaH);
using (new ColorScope(null, boxColor))
{
// Outside Box
GUI.Box(new Rect(boxX, boxY, boxW, boxH), "");
}
using (new ColorScope(titleColor, headerColor))
{
// Header Box
GUI.Box(new Rect(headerX, headerY, headerW, headerH), "");
// Foldout
foldout = EditorGUI.Foldout(new Rect(headerX + 16f, headerY, headerW, headerH),
foldout, titleText, true, EditorStyles.boldLabel);
}
// 수동 컨트롤을 그려낸 만큼 공백 삽입
NextSpace(headerH + padding);
return foldout;
}
// 예시
public override void OnInspectorGUI()
{
currentY = 0f;
NextSpace(8f);
foldOut = DrawFoldoutHeaderBox(currentY, 42f, foldOut, "Title",
Color.black, Color.white * 4f, Color.black);
if (foldOut)
{
GUILayout.Button("Button 1");
GUILayout.Button("Button 2");
NextY(GUILayoutUtility.GetLastRect().height * 2f); // 버튼 높이 * 2만큼 수동 Y 이동
}
NextSpace(8f);
GUILayout.Button("Button");
}
.
1