콘텐츠로 이동

Unity 에디터 커스텀 윈도우와 Inspector 제작

Unity Editor API를 사용하면 게임 개발 워크플로를 자동화하고 팀원이 사용하기 쉬운 도구를 만들 수 있습니다. 커스텀 Inspector로 컴포넌트를 직관적으로 편집하고, EditorWindow로 전용 도구를 만들 수 있습니다.


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(); // 변경사항 적용
}
}

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();
}
}

[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);
}
}
}
}

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독립적인 도구 창
PropertyDrawerSerializable 구조체 필드 UI
HandlesSceneView 기즈모/핸들
SerializedPropertyUndo/다중 편집 지원 속성 접근