Unity 에디터 커스텀 윈도우와 Inspector 제작
Unity Editor API를 사용하면 게임 개발 워크플로를 자동화하고 팀원이 사용하기 쉬운 도구를 만들 수 있습니다. 커스텀 Inspector로 컴포넌트를 직관적으로 편집하고, EditorWindow로 전용 도구를 만들 수 있습니다.
1. 커스텀 Inspector
섹션 제목: “1. 커스텀 Inspector”using UnityEngine;
// 타겟 컴포넌트public class EnemyController : MonoBehaviour{ public float health = 100f; public float speed = 3f; public EnemyType type; public Transform[] patrolPoints;}
public enum EnemyType { Normal, Elite, Boss }using UnityEditor;using UnityEngine;
[CustomEditor(typeof(EnemyController))]public class EnemyControllerEditor : Editor{ private SerializedProperty _health; private SerializedProperty _speed; private SerializedProperty _type; private SerializedProperty _patrolPoints;
void OnEnable() { _health = serializedObject.FindProperty("health"); _speed = serializedObject.FindProperty("speed"); _type = serializedObject.FindProperty("type"); _patrolPoints = serializedObject.FindProperty("patrolPoints"); }
public override void OnInspectorGUI() { serializedObject.Update(); // 직렬화 객체 업데이트
EditorGUILayout.LabelField("적 설정", EditorStyles.boldLabel); EditorGUILayout.Space(4);
// 체력 슬라이더 (0~200) EditorGUILayout.Slider(_health, 0f, 200f, "체력");
// 속도 필드 EditorGUILayout.PropertyField(_speed, new GUIContent("이동 속도"));
// 타입에 따라 다른 UI EditorGUILayout.PropertyField(_type, new GUIContent("적 종류")); if ((EnemyType)_type.enumValueIndex == EnemyType.Boss) { EditorGUILayout.HelpBox("보스 적은 특수 AI를 사용합니다.", MessageType.Info); }
EditorGUILayout.Space(8); EditorGUILayout.LabelField("순찰 경로", EditorStyles.boldLabel); EditorGUILayout.PropertyField(_patrolPoints, true);
// 버튼 if (GUILayout.Button("순찰 경로 초기화")) { var enemy = (EnemyController)target; enemy.patrolPoints = new Transform[0]; }
serializedObject.ApplyModifiedProperties(); // 변경사항 적용 }}2. 커스텀 EditorWindow
섹션 제목: “2. 커스텀 EditorWindow”using UnityEditor;using UnityEngine;
public class LevelBuilderWindow : EditorWindow{ private string _levelName = "New Level"; private int _width = 20; private int _height = 20; private GameObject _tilePrefab; private Vector2 _scrollPos;
// 메뉴에 등록 [MenuItem("Tools/Level Builder")] public static void ShowWindow() { var window = GetWindow<LevelBuilderWindow>("레벨 빌더"); window.minSize = new Vector2(300, 400); }
void OnGUI() { _scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
EditorGUILayout.LabelField("레벨 설정", EditorStyles.boldLabel); EditorGUILayout.Space(4);
_levelName = EditorGUILayout.TextField("레벨 이름", _levelName); _width = EditorGUILayout.IntSlider("가로 크기", _width, 5, 100); _height = EditorGUILayout.IntSlider("세로 크기", _height, 5, 100); _tilePrefab = (GameObject)EditorGUILayout.ObjectField( "타일 프리팹", _tilePrefab, typeof(GameObject), false);
EditorGUILayout.Space(8);
if (GUILayout.Button("레벨 생성", GUILayout.Height(30))) { CreateLevel(); }
EditorGUILayout.EndScrollView(); }
private void CreateLevel() { if (_tilePrefab == null) { EditorUtility.DisplayDialog("오류", "타일 프리팹을 선택하세요.", "확인"); return; }
// Undo 등록 var parent = new GameObject(_levelName); Undo.RegisterCreatedObjectUndo(parent, "Create Level");
for (int x = 0; x < _width; x++) { for (int y = 0; y < _height; y++) { var tile = (GameObject)PrefabUtility.InstantiatePrefab(_tilePrefab, parent.transform); tile.transform.position = new Vector3(x, 0, y); Undo.RegisterCreatedObjectUndo(tile, "Create Tile"); } }
Selection.activeObject = parent; Debug.Log($"레벨 '{_levelName}' 생성 완료: {_width}x{_height}"); }}3. PropertyDrawer — 커스텀 직렬화 필드
섹션 제목: “3. PropertyDrawer — 커스텀 직렬화 필드”// 타겟 속성[System.Serializable]public struct MinMaxRange{ public float Min; public float Max;}
// PropertyDrawer[CustomPropertyDrawer(typeof(MinMaxRange))]public class MinMaxRangeDrawer : PropertyDrawer{ public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property);
var min = property.FindPropertyRelative("Min"); var max = property.FindPropertyRelative("Max");
position = EditorGUI.PrefixLabel(position, label);
float fieldWidth = (position.width - 10) / 2;
var minRect = new Rect(position.x, position.y, fieldWidth, position.height); var maxRect = new Rect(position.x + fieldWidth + 10, position.y, fieldWidth, position.height);
EditorGUI.PropertyField(minRect, min, GUIContent.none); EditorGUI.PropertyField(maxRect, max, GUIContent.none);
EditorGUI.EndProperty(); }}4. SceneView Handles
섹션 제목: “4. SceneView Handles”[CustomEditor(typeof(EnemyController))]public class EnemyControllerEditor : Editor{ void OnSceneGUI() { var enemy = (EnemyController)target;
// 공격 범위 시각화 Handles.color = new Color(1, 0, 0, 0.2f); Handles.DrawSolidDisc(enemy.transform.position, Vector3.up, 3f);
Handles.color = Color.red; Handles.DrawWireDisc(enemy.transform.position, Vector3.up, 3f);
// 순찰 경로 시각화 if (enemy.patrolPoints != null && enemy.patrolPoints.Length > 1) { Handles.color = Color.yellow; for (int i = 0; i < enemy.patrolPoints.Length; i++) { if (enemy.patrolPoints[i] == null) continue;
int next = (i + 1) % enemy.patrolPoints.Length; if (enemy.patrolPoints[next] != null) { Handles.DrawLine( enemy.patrolPoints[i].position, enemy.patrolPoints[next].position); }
// 드래그 가능한 핸들 enemy.patrolPoints[i].position = Handles.PositionHandle( enemy.patrolPoints[i].position, Quaternion.identity); } } }}5. EditorPrefs로 설정 저장
섹션 제목: “5. EditorPrefs로 설정 저장”public class MyEditorWindow : EditorWindow{ private bool _showAdvanced;
void OnEnable() { _showAdvanced = EditorPrefs.GetBool("MyWindow_ShowAdvanced", false); }
void OnDisable() { EditorPrefs.SetBool("MyWindow_ShowAdvanced", _showAdvanced); }}| 클래스 | 용도 |
|---|---|
Editor | 컴포넌트의 Inspector UI 커스텀 |
EditorWindow | 독립적인 도구 창 |
PropertyDrawer | Serializable 구조체 필드 UI |
Handles | SceneView 기즈모/핸들 |
SerializedProperty | Undo/다중 편집 지원 속성 접근 |