Unity PropertyAttribute와 커스텀 에디터 확장
Unity 에디터 확장은 PropertyAttribute + PropertyDrawer로 개별 필드 UI를 바꾸거나, CustomEditor로 컴포넌트 전체 인스펙터를 재정의합니다. UI Toolkit(UIElements) 기반으로 전환하면 UXML/USS로 복잡한 에디터 UI를 선언적으로 구성할 수 있습니다.
1. 커스텀 PropertyAttribute
섹션 제목: “1. 커스텀 PropertyAttribute”// Runtime 코드public class RangeStepAttribute : PropertyAttribute{ public float Min, Max, Step;
public RangeStepAttribute(float min, float max, float step = 0.1f) { Min = min; Max = max; Step = step; }}
// 사용public class PlayerStats : MonoBehaviour{ [RangeStep(0f, 100f, 5f)] public float Health = 100f;
[RangeStep(0f, 50f, 1f)] public float Speed = 10f;}2. PropertyDrawer 구현
섹션 제목: “2. PropertyDrawer 구현”// Editor 폴더 또는 Editor 어셈블리에 배치using UnityEditor;using UnityEngine;
[CustomPropertyDrawer(typeof(RangeStepAttribute))]public class RangeStepDrawer : PropertyDrawer{ public override void OnGUI( Rect position, SerializedProperty property, GUIContent label) { var attr = (RangeStepAttribute)attribute;
if (property.propertyType != SerializedPropertyType.Float) { EditorGUI.HelpBox(position, "RangeStep은 float에만 사용 가능합니다", MessageType.Error); return; }
EditorGUI.BeginProperty(position, label, property);
float val = EditorGUI.Slider(position, label, property.floatValue, attr.Min, attr.Max);
// Step으로 스냅 val = Mathf.Round(val / attr.Step) * attr.Step; property.floatValue = val;
EditorGUI.EndProperty(); }
// 높이 재정의 (기본: 1줄) public override float GetPropertyHeight( SerializedProperty property, GUIContent label) { return EditorGUIUtility.singleLineHeight; }}3. 유용한 내장 Attribute
섹션 제목: “3. 유용한 내장 Attribute”public class ExampleComponent : MonoBehaviour{ [Header("전투 설정")] [Tooltip("초당 공격 횟수")] [Range(0.1f, 10f)] public float AttackRate = 1f;
[Space(10)] [Header("이동 설정")] public float MoveSpeed = 5f;
[Multiline(3)] public string Description;
[HideInInspector] public int InternalId;
[SerializeField] private float _privateField;
// 조건부 표시 (커스텀 구현 필요 또는 Odin Inspector) [ShowIf("_showAdvanced")] // Odin Inspector public float AdvancedSetting; private bool _showAdvanced;}4. CustomEditor — 컴포넌트 전체 재정의
섹션 제목: “4. CustomEditor — 컴포넌트 전체 재정의”[CustomEditor(typeof(WaypointPath))]public class WaypointPathEditor : Editor{ private WaypointPath _path;
void OnEnable() => _path = (WaypointPath)target;
public override void OnInspectorGUI() { serializedObject.Update();
EditorGUILayout.LabelField("웨이포인트 경로", EditorStyles.boldLabel);
// 기본 인스펙터 DrawDefaultInspector();
EditorGUILayout.Space();
// 커스텀 버튼 if (GUILayout.Button("웨이포인트 추가")) { Undo.RecordObject(_path, "웨이포인트 추가"); _path.AddWaypoint(); }
if (GUILayout.Button("경로 초기화")) { if (EditorUtility.DisplayDialog( "초기화 확인", "모든 웨이포인트를 삭제합니까?", "확인", "취소")) { Undo.RecordObject(_path, "경로 초기화"); _path.Clear(); } }
serializedObject.ApplyModifiedProperties(); }
// 씬 뷰에서 핸들 그리기 void OnSceneGUI() { for (int i = 0; i < _path.Waypoints.Count; i++) { EditorGUI.BeginChangeCheck();
Vector3 pos = Handles.PositionHandle( _path.Waypoints[i], Quaternion.identity);
if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(_path, "웨이포인트 이동"); _path.Waypoints[i] = pos; }
Handles.Label(_path.Waypoints[i] + Vector3.up * 0.3f, $"WP {i}"); } }}5. UI Toolkit 기반 PropertyDrawer
섹션 제목: “5. UI Toolkit 기반 PropertyDrawer”[CustomPropertyDrawer(typeof(RangeStepAttribute))]public class RangeStepDrawerUIE : PropertyDrawer{ public override VisualElement CreatePropertyGUI( SerializedProperty property) { var attr = (RangeStepAttribute)attribute; var root = new VisualElement();
var slider = new Slider( property.displayName, attr.Min, attr.Max) { value = property.floatValue, showInputField = true };
slider.RegisterValueChangedCallback(evt => { float stepped = Mathf.Round(evt.newValue / attr.Step) * attr.Step; property.floatValue = stepped; property.serializedObject.ApplyModifiedProperties(); });
root.Add(slider); return root; }}6. EditorWindow — 독립 도구 창
섹션 제목: “6. EditorWindow — 독립 도구 창”public class LevelDesignTools : EditorWindow{ [MenuItem("Tools/레벨 디자인 도구 %#l")] public static void ShowWindow() => GetWindow<LevelDesignTools>("레벨 도구");
private float _spawnRadius = 5f; private GameObject _prefab;
void OnGUI() { GUILayout.Label("오브젝트 배치 도구", EditorStyles.boldLabel);
_prefab = (GameObject)EditorGUILayout.ObjectField( "프리팹", _prefab, typeof(GameObject), false);
_spawnRadius = EditorGUILayout.Slider( "배치 반경", _spawnRadius, 1f, 50f);
if (GUILayout.Button("원형 배치") && _prefab != null) PlaceInCircle(); }
void PlaceInCircle() { int count = 8; for (int i = 0; i < count; i++) { float angle = i * 360f / count; var dir = Quaternion.Euler(0, angle, 0) * Vector3.forward; var pos = dir * _spawnRadius;
var go = (GameObject)PrefabUtility.InstantiatePrefab(_prefab); go.transform.position = pos; Undo.RegisterCreatedObjectUndo(go, "원형 배치"); } }}간단한 필드 커스터마이징은 PropertyAttribute + PropertyDrawer로 구현하고, 컴포넌트 전체 인스펙터는 CustomEditor로 재정의하세요. 씬 뷰 조작이 필요하면 OnSceneGUI에서 Handles를 사용하고, 반드시 Undo.RecordObject를 호출해 Ctrl+Z가 동작하도록 하세요. 새 에디터 도구는 IMGUI보다 UI Toolkit(VisualElement)으로 구현하면 재사용성과 유지보수성이 높아집니다.