
Custom Inspector로 기획자용 시각화 에디터 만들기
Unity의 Custom Inspector와 Scene GUI를 이용해 기획자가 코드 없이 데이터를 편집하고 결과를 즉시 확인하는 시각화 에디터를 만드는 방법을 정리합니다.
왜 Custom Inspector가 필요한가
게임 데이터가 Vector3 배열이나 숫자 목록으로만 보이면 기획자는 값의 의미와 결과를 연결하기 어렵다. 예를 들어 스폰 지점, 순찰 경로, 공격 범위는 Inspector에서 숫자로 입력할 수 있지만 실제 레벨 위에서 어떤 모습인지 확인하려면 플레이 모드에 들어가야 한다.
Custom Inspector는 이 간격을 줄인다. 일반 Inspector는 데이터 입력을 맡고 Custom Inspector와 Scene GUI는 데이터의 의미를 시각적으로 보여 주며 직접 조작하게 만든다. 목표는 개발자가 쓰기 좋은 구조를 유지하면서 기획자가 실수하기 어려운 편집 환경을 만드는 것이다.
flowchart LR
A[기획자 입력] --> B[SerializedProperty]
B --> C[게임 데이터 컴포넌트]
C --> D[Scene GUI 시각화]
D --> E[핸들 조작]
E --> B
C --> F[런타임 시스템]
이 흐름에서 중요한 점은 편집기 코드가 런타임 로직을 대신하지 않는다는 것이다. 편집기는 데이터를 만들고 검증하며 보여 주는 도구이고 실제 게임은 저장된 데이터를 해석해 동작한다.
예제: 순찰 경로 데이터 만들기
먼저 런타임에서 사용할 데이터를 컴포넌트로 정의한다. 경로 지점은 로컬 좌표로 저장하면 오브젝트를 통째로 이동했을 때도 경로가 함께 이동한다.
using System.Collections.Generic;
using UnityEngine;
public class PatrolPath : MonoBehaviour
{
[SerializeField] private List<Vector3> points = new();
[SerializeField, Min(0f)] private float waitTime = 1f;
[SerializeField] private bool loop = true;
public IReadOnlyList<Vector3> Points => points;
public float WaitTime => waitTime;
public bool Loop => loop;
}
처음에는 public 필드로 빠르게 구현하고 싶을 수 있다. 하지만 Inspector 확장과 검증이 필요한 데이터는 SerializeField와 프로퍼티를 조합하는 편이 안전하다. 외부 시스템은 읽기 전용으로 접근하고 데이터 변경은 Inspector나 명시적인 메서드를 통해 제한할 수 있다.
SerializedProperty를 기준으로 Inspector 그리기
Editor 클래스는 반드시 Editor 폴더 아래에 두거나 Editor 전용 어셈블리 정의에 포함해야 한다. 그렇지 않으면 빌드에 UnityEditor 참조가 섞일 수 있다.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(PatrolPath))]
public class PatrolPathEditor : Editor
{
private SerializedProperty pointsProperty;
private SerializedProperty waitTimeProperty;
private SerializedProperty loopProperty;
private void OnEnable()
{
pointsProperty = serializedObject.FindProperty("points");
waitTimeProperty = serializedObject.FindProperty("waitTime");
loopProperty = serializedObject.FindProperty("loop");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.LabelField("순찰 경로", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(waitTimeProperty, new GUIContent("대기 시간"));
EditorGUILayout.PropertyField(loopProperty, new GUIContent("반복"));
EditorGUILayout.Space();
EditorGUILayout.PropertyField(pointsProperty, new GUIContent("경로 지점"), true);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("현재 위치 추가"))
{
pointsProperty.arraySize++;
pointsProperty.GetArrayElementAtIndex(pointsProperty.arraySize - 1).vector3Value =
((PatrolPath)target).transform.InverseTransformPoint(SceneView.lastActiveSceneView.pivot);
}
if (GUILayout.Button("모두 비우기"))
{
pointsProperty.ClearArray();
}
}
serializedObject.ApplyModifiedProperties();
}
}
직접 필드를 대입하는 대신 SerializedObject와 SerializedProperty를 사용하면 Undo, Prefab Override, 다중 오브젝트 편집 같은 Unity 편집기 기능을 자연스럽게 활용할 수 있다. 특히 기획자가 반복해서 값을 바꾸는 도구라면 Undo 지원은 선택이 아니라 기본 품질 조건에 가깝다.
SceneView.lastActiveSceneView는 활성 Scene View가 없는 상황에서 null일 수 있다. 위 예제처럼 버튼 기능을 제공할 때는 실제 프로젝트에서 null 검사와 기본값을 추가하는 편이 좋다.

Scene GUI로 경로를 직접 편집하기
Inspector만 정리해도 입력 과정은 좋아지지만 시각화 에디터의 핵심은 Scene View 조작이다. OnSceneGUI에서 선과 라벨을 그린 뒤 Handles.PositionHandle로 지점을 옮길 수 있다.
private void OnSceneGUI()
{
var path = (PatrolPath)target;
serializedObject.Update();
Handles.color = new Color(0.2f, 0.8f, 1f, 1f);
for (int i = 0; i < pointsProperty.arraySize; i++)
{
SerializedProperty pointProperty = pointsProperty.GetArrayElementAtIndex(i);
Vector3 worldPoint = path.transform.TransformPoint(pointProperty.vector3Value);
EditorGUI.BeginChangeCheck();
Vector3 movedPoint = Handles.PositionHandle(worldPoint, Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(path, "Move Patrol Point");
pointProperty.vector3Value = path.transform.InverseTransformPoint(movedPoint);
}
Handles.Label(worldPoint + Vector3.up * 0.25f, $"지점 {i + 1}");
if (i > 0)
{
Vector3 previous = path.transform.TransformPoint(
pointsProperty.GetArrayElementAtIndex(i - 1).vector3Value);
Handles.DrawLine(previous, worldPoint);
}
}
if (loopProperty.boolValue && pointsProperty.arraySize > 1)
{
Vector3 first = path.transform.TransformPoint(
pointsProperty.GetArrayElementAtIndex(0).vector3Value);
Vector3 last = path.transform.TransformPoint(
pointsProperty.GetArrayElementAtIndex(pointsProperty.arraySize - 1).vector3Value);
Handles.DrawDottedLine(last, first, 4f);
}
serializedObject.ApplyModifiedProperties();
}
좌표계 변환을 명확히 구분해야 한다. 데이터가 로컬 좌표라면 Handle에 전달할 때 TransformPoint를 사용하고 수정된 월드 좌표를 저장할 때 InverseTransformPoint로 되돌린다. 이 규칙을 지키지 않으면 오브젝트를 이동하거나 회전했을 때 경로가 어긋난다.
기획자에게 필요한 것은 기능보다 피드백이다
좋은 에디터는 값을 입력할 수 있게 하는 데서 끝나지 않는다. 잘못된 값을 빠르게 발견하고 다음 행동을 알 수 있게 해야 한다. 순찰 경로라면 다음 검사를 추가할 수 있다.
- 지점이 2개 미만이면 경고를 표시한다.
- 동일하거나 지나치게 가까운 지점은 강조한다.
- 반복 경로와 편도 경로를 선 스타일로 구분한다.
- 경로 길이와 예상 이동 시간을 읽기 전용 정보로 표시한다.
private void DrawValidation(PatrolPath path)
{
if (pointsProperty.arraySize < 2)
{
EditorGUILayout.HelpBox("순찰 경로에는 최소 두 개의 지점이 필요합니다.", MessageType.Warning);
return;
}
float totalLength = 0f;
for (int i = 1; i < pointsProperty.arraySize; i++)
{
Vector3 a = pointsProperty.GetArrayElementAtIndex(i - 1).vector3Value;
Vector3 b = pointsProperty.GetArrayElementAtIndex(i).vector3Value;
totalLength += Vector3.Distance(a, b);
}
EditorGUILayout.HelpBox($"경로 길이: {totalLength:F1}m", MessageType.Info);
}
이 메서드는 OnInspectorGUI에서 ApplyModifiedProperties 전에 호출하면 된다. 다만 편집기에서 계산한 길이를 별도 필드에 저장할 필요는 없다. 런타임이 원본 지점으로부터 다시 계산할 수 있는 파생 값이라면 화면에만 표시해 데이터 중복을 피하는 편이 낫다.

Undo와 Prefab 편집에서 자주 생기는 문제
SerializedProperty 변경은 ApplyModifiedProperties를 통해 Undo 기록과 연결된다. 하지만 Transform처럼 직렬화 프로퍼티 밖의 객체를 직접 수정할 때는 Undo.RecordObject 또는 상황에 맞는 Undo API를 명시적으로 호출해야 한다.
또한 Prefab 인스턴스에서 경로를 수정할 수 있으므로 다음 원칙을 지키는 것이 좋다.
- 런타임 데이터는
MonoBehaviour필드에 직렬화한다. - Editor 클래스에서는 필드 이름을 문자열로 찾되 이름 변경 시 함께 수정한다.
- 변경 전후를
EditorGUI.BeginChangeCheck와EditorGUI.EndChangeCheck로 감싼다. - 대량 생성이나 자동 정렬은 즉시 실행하기보다 확인 버튼과 Undo 기록을 제공한다.
OnSceneGUI는 Scene View가 다시 그려질 때 자주 호출된다. 여기서 파일 저장, 에셋 생성, 무거운 탐색처럼 부수 효과가 큰 작업을 실행하면 편집기가 느려지고 예측하기 어려워진다. 그리기와 입력 처리만 수행하고 명시적인 버튼 동작에서만 생성이나 정리를 실행하는 편이 안정적이다.
확장 방향
순찰 경로 예제의 구조는 스폰 영역, 카메라 레일, 대화 트리 노드, 적 배치 구역에도 그대로 적용할 수 있다. 데이터 모델을 먼저 단순하게 설계하고 Inspector에서 속성을 정리한 뒤 Scene GUI에서 의미 있는 시각화와 직접 조작을 더하면 된다.
처음부터 별도 EditorWindow와 복잡한 노드 그래프를 만들 필요는 없다. 기획자가 자주 수정하는 데이터 하나를 골라 Inspector와 Scene View에서 완결된 작업 흐름을 제공하는 것이 좋은 출발점이다. 그 도구가 실제 작업에서 반복 사용될 때 필요한 기능을 관찰해 범위 선택, 일괄 편집, 자동 배치 같은 기능을 단계적으로 추가하면 유지보수 비용도 낮출 수 있다.
마무리
Custom Inspector는 단순히 Inspector의 모양을 바꾸는 기능이 아니다. 데이터 구조, 검증 규칙, Scene View 피드백을 연결해 기획자가 게임 의도를 직접 편집할 수 있도록 만드는 도구다. SerializedProperty로 Unity의 편집 흐름을 지키고 좌표계와 Undo를 정확히 처리하며 오류를 눈에 보이게 만들면 작은 컴포넌트도 실용적인 시각화 에디터로 발전시킬 수 있다.


