순찰 경로, 카메라 이동 라인, 트리거 영역 같은 데이터를 인스펙터에서 Vector3 배열 숫자로 입력해 본 사람은 안다. X, Y, Z 값을 손으로 치고, 씬 뷰로 시선을 옮겨 위치를 확인하고, 다시 인스펙터로 돌아와 숫자를 고친다. 점이 다섯 개만 넘어가도 이 왕복은 고통스럽다. 머릿속에는 "그냥 씬에서 마우스로 끌면 안 되나?"라는 생각이 떠오른다.
된다. 유니티 에디터에는 씬 뷰 위에 조작 가능한 손잡이(핸들)를 그리는 Handles API가 있고, 컴포넌트마다 씬 뷰 그리기 로직을 끼워 넣는 OnSceneGUI 콜백이 있다. 이 둘을 합치면 인스펙터 숫자 입력을 마우스 드래그로 바꾸는 인-씬(in-scene) 에디터를 만들 수 있다. 데이터는 그대로 두고, 편집 경험만 바꾸는 것이다.
이 글에서는 웨이포인트(경유점) 경로를 예시로, 점을 드래그로 옮기고 경로를 선으로 보여주고 클릭으로 점을 추가하는 작은 비주얼 에디터를 직접 만들어 본다. 핵심은 코드 양이 아니라 빠지기 쉬운 함정 세 가지를 피하는 것이다.
데이터 컴포넌트부터 분리한다
먼저 편집할 데이터를 담는 평범한 MonoBehaviour를 만든다. 에디터 코드는 이 데이터를 건드리기만 할 뿐, 데이터 자체는 런타임 로직과 똑같이 쓰인다.
using UnityEngine;
public class WaypointPath : MonoBehaviour
{
public Vector3[] points = new Vector3[0]; // 로컬 좌표로 저장
public bool loop = false;
}
점을 로컬 좌표로 저장하는 게 포인트다. 부모 오브젝트를 통째로 옮겨도 경로가 따라오게 하려면 월드 좌표가 아니라 로컬 좌표여야 한다.
OnSceneGUI에서 핸들 그리기
에디터 스크립트는 반드시 Editor 폴더 안에 둔다. CustomEditor 특성으로 대상 타입을 지정하고, OnSceneGUI 메서드를 만들면 씬 뷰가 그려질 때마다 호출된다.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(WaypointPath))]
public class WaypointPathEditor : Editor
{
void OnSceneGUI()
{
var path = (WaypointPath)target;
Transform tr = path.transform;
for (int i = 0; i < path.points.Length; i++)
{
Vector3 world = tr.TransformPoint(path.points[i]); // 로컬 to 월드
EditorGUI.BeginChangeCheck();
Vector3 moved = Handles.PositionHandle(world, Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(path, "Move Waypoint");
path.points[i] = tr.InverseTransformPoint(moved); // 다시 로컬로
}
Handles.Label(world + Vector3.up * 0.3f, $"P{i}");
}
}
}
이 짧은 코드 안에 세 가지 함정 회피가 모두 들어 있다.
첫째, 좌표 변환. 핸들은 월드 좌표로 그려지므로, 저장된 로컬 좌표를 TransformPoint로 월드로 바꿔 그리고, 드래그 결과는 InverseTransformPoint로 다시 로컬로 되돌려 저장한다. 이 변환을 빼먹으면 부모를 옮기는 순간 경로가 엉뚱한 곳으로 튄다.
둘째, BeginChangeCheck와 EndChangeCheck로 감싸 핸들이 실제로 움직였을 때만 값을 쓴다. 매 프레임 무조건 대입하면 불필요한 변경 기록이 쌓인다.
셋째이자 가장 중요한 것, Undo.RecordObject다. 이 한 줄을 빼면 드래그한 위치가 저장되지 않고 Ctrl+Z도 먹지 않는다. 이게 초보가 가장 많이 놓치는 부분이다. RecordObject는 변경 직전 상태를 되돌리기 스택에 넣고, 동시에 해당 오브젝트를 "수정됨" 상태로 표시해 씬 저장에도 반영해 준다.
경로를 선으로 보여주고 클릭으로 추가하기
점만 떠 있으면 경로가 한눈에 안 들어온다. 점들을 선으로 잇고, 마지막 점 옆에 추가 버튼을 띄워 보자. 위 OnSceneGUI 안에 이어 붙이면 된다.
Handles.color = Color.cyan;
for (int i = 0; i < path.points.Length - 1; i++)
{
Vector3 a = tr.TransformPoint(path.points[i]);
Vector3 b = tr.TransformPoint(path.points[i + 1]);
Handles.DrawAAPolyLine(3f, a, b);
}
if (path.points.Length > 0)
{
Vector3 last = tr.TransformPoint(path.points[^1]);
float size = HandleUtility.GetHandleSize(last) * 0.25f;
if (Handles.Button(last, Quaternion.identity, size, size, Handles.SphereHandleCap))
{
Undo.RecordObject(path, "Add Waypoint");
ArrayUtility.Add(ref path.points, path.points[^1] + Vector3.right);
}
}
여기서 HandleUtility.GetHandleSize가 또 하나의 실전 팁이다. 화면 픽셀 기준으로 크기를 정하면 카메라를 줌인하거나 줌아웃할 때 버튼이 너무 커지거나 사라진다. GetHandleSize는 카메라 거리에 비례한 값을 돌려주므로, 거기에 곱해 주면 줌 배율과 상관없이 버튼이 항상 비슷한 크기로 보인다.
ArrayUtility는 UnityEditor 네임스페이스에 있는 편의 함수로, 배열 끝에 항목을 손쉽게 더해 준다. path.points[^1]처럼 끝에서 첫 번째 원소를 가리키는 인덱스 문법은 최신 C#에서 코드를 한결 깔끔하게 만들어 준다.
정리
인-씬 에디터의 뼈대는 의외로 단순하다. CustomEditor로 대상을 잡고, OnSceneGUI에서 Handles로 그리고, 저장 데이터는 로컬 좌표로 두고 변환은 그릴 때만 한다. 그리고 값을 바꾸는 모든 지점에 Undo.RecordObject를 둔다.
오늘 본 세 가지 함정만 기억하면 대부분의 씬 편집 도구를 만들 수 있다. 좌표 변환을 빼먹으면 부모 이동에 깨지고, 변경 감지를 빼먹으면 기록이 더럽혀지고, RecordObject를 빼먹으면 저장과 되돌리기가 동시에 죽는다. 숫자 입력의 고통을 마우스 드래그로 바꾸는 작은 투자가, 레벨 디자인 반복 작업의 속도를 눈에 띄게 끌어올린다.
'Unity·Unreal 실전' 카테고리의 다른 글
| Unreal Engine C++ Data Validation: UEditorValidator로 에셋 오류와 빌드 크래시 사전 방지하기 (0) | 2026.06.25 |
|---|---|
| Unity 6 URP Render Graph로 커스텀 패스 마이그레이션하기: Compatibility Mode 졸업 (0) | 2026.06.22 |
| Unreal Enhanced Input 실전: Modifier와 Trigger로 입력 로직을 코드에서 데이터로 빼기 (0) | 2026.06.15 |
| Unity UI Toolkit 실전 — IMGUI 없이 커스텀 EditorWindow 제작하기 (0) | 2026.06.11 |
| Unity URP Renderer Feature로 적 피격 하이라이트 분리하기 (0) | 2026.06.08 |