이전 글에서는 맵툴의 기본 배치 흐름을 개선했다.
프리팹의 기본 크기가 유지되도록 만들고, 배치할 때 머테리얼을 바로 적용할 수 있게 했다.
이제 Map Tool로 프리팹을 찍는 흐름은 조금 더 편해졌다.
하지만 실제로 맵을 찍다 보니 또 다른 불편함이 보였다.
숫자 입력만으로는 크기와 높이를 조절하기가 답답했다.
Scale Multiplier에 숫자를 입력한다.
Scene View를 본다.
다시 숫자를 고친다.
다시 Scene View를 본다.
이런 식으로 작업하면 흐름이 자꾸 끊겼다.
맵을 찍을 때는 수치보다 눈으로 보는 감각이 더 중요할 때가 많다.
그래서 이번에는 Scene View 안에서 직접 크기와 높이를 조절할 수 있게 만들었다.(크기 조절은 실수로 이전 글의 영상에서 보여줬기에 뺏다.)
이번에 추가한 것
이번 글에서 정리할 기능은 다음과 같다.
1. Size 바꾸기
2. Scene View Scale Handle
3. Height 바꾸기
4. Y축 전용 Height Slider
5. 1 / 3 키로 높이 미세 조절
6. 편집 중 배치 클릭 막기
핵심은 Unity 에디터에서 제공하는 Handles를 사용하는 것이다.
Size 바꾸기 버튼
먼저 Map Tool 창에 Size 바꾸기 버튼을 추가했다.
bool nextEditPlacementScale = GUILayout.Toggle(
editPlacementScaleInScene,
new GUIContent("Size 바꾸기", "Scene View에 scale handle을 띄워 현재 배치 scale을 눈으로 조절합니다."),
"Button"
);
if (nextEditPlacementScale != editPlacementScaleInScene)
{
SetScaleEditMode(nextEditPlacementScale);
}
여기서 일반 버튼이 아니라 GUILayout.Toggle을 사용했다.
이유는 Size 바꾸기가 한 번 누르고 끝나는 기능이 아니라, 켜고 끄는 모드이기 때문이다.
Size 바꾸기 ON
→ Scene View에 Scale Handle 표시
Size 바꾸기 OFF
→ 일반 배치 모드로 돌아감
즉, 이 버튼은 스케일 편집 모드 전환 버튼에 가깝다.


Scale 편집 모드 켜기
버튼을 누르면 SetScaleEditMode()가 호출된다.
private void SetScaleEditMode(bool enabled)
{
editPlacementScaleInScene = enabled;
if (editPlacementScaleInScene)
{
editPlacementHeightInScene = false;
hasHeightEditPose = false;
CaptureScaleEditPose();
}
else
{
hasScaleEditPose = false;
}
Repaint();
SceneView.RepaintAll();
}
여기서 중요한 점은 Height 편집 모드를 꺼준다는 것이다.
(크기 조절과 높이 조절을 동시에 켜두면 Scene View에 여러 handle이 겹칠 수 있다.
그래서 Size 바꾸기를 켜면 Height 바꾸기는 꺼지게 했다.)
editPlacementHeightInScene = false;
hasHeightEditPose = false;
반대로 Size 바꾸기를 끄면 현재 scale 편집 위치도 초기화한다.
hasScaleEditPose = false;
그리고 마지막에 Repaint()와 SceneView.RepaintAll()을 호출한다.
Repaint();
SceneView.RepaintAll();
에디터 툴에서는 상태값만 바꿔도 화면이 바로 갱신되지 않을 수 있다.
그래서 툴 창과 Scene View를 다시 그리도록 요청한다.
현재 Preview 위치 저장하기
Scale Handle을 띄우려면 기준 위치가 필요하다.
그 기준은 현재 preview의 위치와 회전이다.
private void CaptureScaleEditPose()
{
if (!hasLastPreviewTransform)
{
hasScaleEditPose = false;
return;
}
scaleEditPosition = lastPreviewPosition;
scaleEditRotation = lastPreviewRotation;
hasScaleEditPose = true;
}
hasLastPreviewTransform이 없다면 아직 Scene View에 유효한 preview 위치가 없다는 뜻이다.
그 상태에서 handle을 띄우면 기준점이 이상해질 수 있다.
그래서 preview 위치가 없으면 scale 편집 상태를 만들지 않는다.
if (!hasLastPreviewTransform)
{
hasScaleEditPose = false;
return;
}
preview 위치가 있다면 그 위치와 회전을 저장한다.
scaleEditPosition = lastPreviewPosition;
scaleEditRotation = lastPreviewRotation;
이렇게 해두면 마우스를 움직여도 handle이 계속 따라다니지 않는다.
Size 바꾸기를 누른 순간의 위치에 고정된다.
이 부분이 중요했다.
처음에는 preview가 마우스를 따라 움직이듯이 handle도 같이 움직이면 될 것 같았다.
하지만 그렇게 하면 손잡이를 잡으려는 순간에도 위치가 계속 바뀌어서 조작이 불편해진다.
그래서 편집 모드에 들어갈 때 현재 preview 위치를 고정했다.
Scale Handle 그리기
실제로 Scene View에 Scale Handle을 그리는 코드는 DrawPlacementScaleHandle()에 있다.
private void DrawPlacementScaleHandle(SceneView sceneView)
{
if (!editPlacementScaleInScene || selectedPrefab == null || !hasScaleEditPose)
{
return;
}
EditorGUI.BeginChangeCheck();
float handleSize = HandleUtility.GetHandleSize(scaleEditPosition);
Vector3 nextLocalScale = Handles.ScaleHandle(
GetPlacementLocalScale(),
scaleEditPosition,
scaleEditRotation,
handleSize);
if (!EditorGUI.EndChangeCheck())
{
return;
}
placementScale = ApplyPlacementScaleInput(
GetPlacementScaleMultiplierFromLocalScale(nextLocalScale));
Repaint();
sceneView.Repaint();
}
먼저 scale 편집 모드가 아니면 아무것도 하지 않는다.
if (!editPlacementScaleInScene || selectedPrefab == null || !hasScaleEditPose)
{
return;
}
이 조건을 둔 이유는 명확하다.
Size 바꾸기가 켜져 있어야 한다.
선택된 프리팹이 있어야 한다.
기준 위치가 저장되어 있어야 한다.
셋 중 하나라도 없으면 handle을 띄울 수 없다.
Handles.ScaleHandle
Vector3 nextLocalScale = Handles.ScaleHandle(
GetPlacementLocalScale(),
scaleEditPosition,
scaleEditRotation,
handleSize);
Handles.ScaleHandle은 Scene View에 scale 조절 손잡이를 그려준다.
인자로 넘긴 값은 다음과 같다.
GetPlacementLocalScale()
→ 현재 표시할 scale
scaleEditPosition
→ handle이 표시될 위치
scaleEditRotation
→ handle의 회전
handleSize
→ Scene View 거리 기준 handle 크기
여기서 HandleUtility.GetHandleSize()를 사용했다.
float handleSize = HandleUtility.GetHandleSize(scaleEditPosition);
이걸 쓰면 카메라가 가까이 있든 멀리 있든 handle이 적당한 크기로 보인다.
그냥 고정 숫자를 넣으면 카메라 거리마다 너무 작거나 너무 크게 보일 수 있다.
변경이 있을 때만 반영하기
EditorGUI.BeginChangeCheck()와 EditorGUI.EndChangeCheck()는 값이 실제로 바뀌었는지 확인할 때 사용한다.
EditorGUI.BeginChangeCheck();
Vector3 nextLocalScale = Handles.ScaleHandle(...);
if (!EditorGUI.EndChangeCheck())
{
return;
}
사용자가 handle을 건드리지 않았다면 scale 값을 다시 계산할 필요가 없다.
변경이 있을 때만 아래 코드가 실행된다.
placementScale = ApplyPlacementScaleInput(
GetPlacementScaleMultiplierFromLocalScale(nextLocalScale));
여기서 nextLocalScale은 최종 local scale이다.
하지만 맵툴 내부에서는 placementScale을 프리팹 기본 크기에 곱하는 배율로 다루고 있다.
그래서 다시 배율로 변환해줘야 한다.
private Vector3 GetPlacementScaleMultiplierFromLocalScale(Vector3 localScale)
{
Vector3 baseScale = GetSelectedPrefabBaseScale();
return SanitizePlacementScale(new Vector3(
SafeDivideScale(localScale.x, baseScale.x),
SafeDivideScale(localScale.y, baseScale.y),
SafeDivideScale(localScale.z, baseScale.z)));
}
즉:
localScale = prefabBaseScale * placementScale
였다면, 반대로:
placementScale = localScale / prefabBaseScale
을 계산하는 것이다.
이렇게 해야 Scene View에서 handle로 조절한 값도 기존 Scale Multiplier 규칙과 맞는다.
Uniform Scale도 그대로 적용하기
Scale Handle로 크기를 조절할 때도 Uniform Scale 옵션이 적용된다.
placementScale = ApplyPlacementScaleInput(
GetPlacementScaleMultiplierFromLocalScale(nextLocalScale));
여기서 ApplyPlacementScaleInput()을 다시 거치기 때문이다.
그래서 Inspector에서 숫자를 입력하든, Scene View에서 handle을 잡든 같은 규칙을 따른다.
Uniform Scale ON
- 한 축을 바꿔도 전체 비율 유지
Uniform Scale OFF
- X/Y/Z를 따로 조절
이런 식으로 입력 방식이 달라도 내부 규칙은 하나로 유지하는 게 중요하다고 느꼈다.
Height 바꾸기
크기 조절 다음에는 높이 조절도 필요했다.
기존에는 Height Offset 숫자를 직접 입력하거나 버튼으로 올리고 내렸다.
if (GUILayout.Button("Height -"))
{
ChangeHeightOffset(-heightStep);
}
if (GUILayout.Button("Height +"))
{
ChangeHeightOffset(heightStep);
}
그리고 단축키로도 조절할 수 있게 했다.
else if (currentEvent.keyCode == KeyCode.Alpha1 || currentEvent.keyCode == KeyCode.Keypad1)
{
ChangeHeightOffset(-heightStep);
currentEvent.Use();
}
else if (currentEvent.keyCode == KeyCode.Alpha3 || currentEvent.keyCode == KeyCode.Keypad3)
{
ChangeHeightOffset(heightStep);
currentEvent.Use();
}
여기서 1은 높이를 내리고, 3은 높이를 올린다.
처음에는 [ / ] 키를 사용했지만, 나중에 조작키를 정리하면서 1 / 3만 남겼다.
Height 편집 모드
높이도 크기와 마찬가지로 toggle 버튼으로 만들었다.
bool nextEditPlacementHeight = GUILayout.Toggle(
editPlacementHeightInScene,
new GUIContent("Height 바꾸기", "Scene View에 transform handle을 띄워 현재 배치 높이를 눈으로 조절합니다."),
"Button"
);
if (nextEditPlacementHeight != editPlacementHeightInScene)
{
SetHeightEditMode(nextEditPlacementHeight);
}
Height 바꾸기를 켜면 SetHeightEditMode()가 호출된다.
private void SetHeightEditMode(bool enabled)
{
editPlacementHeightInScene = enabled;
if (editPlacementHeightInScene)
{
editPlacementScaleInScene = false;
hasScaleEditPose = false;
CaptureHeightEditPose();
}
else
{
hasHeightEditPose = false;
}
Repaint();
SceneView.RepaintAll();
}
이번에는 반대로 scale 편집 모드를 꺼준다.
editPlacementScaleInScene = false;
hasScaleEditPose = false;
즉, 한 번에 하나의 편집 모드만 켜질 수 있다.
Height 기준 위치 저장하기
높이 편집도 시작 시점의 preview 위치를 저장한다.
private void CaptureHeightEditPose()
{
if (!hasLastPreviewTransform)
{
hasHeightEditPose = false;
return;
}
heightEditPosition = lastPreviewPosition;
heightEditRotation = lastPreviewRotation;
heightEditBaseOffset = heightOffset;
heightEditBaseY = lastPreviewPosition.y;
hasHeightEditPose = true;
}
여기서는 위치와 회전뿐 아니라 두 값을 더 저장한다.
heightEditBaseOffset = heightOffset;
heightEditBaseY = lastPreviewPosition.y;
이유는 handle을 위아래로 움직였을 때, 현재 heightOffset을 얼마나 바꿔야 하는지 계산하기 위해서다.
처음 handle Y 위치 = heightEditBaseY
현재 handle Y 위치 = nextHandlePosition.y
차이 = nextHandlePosition.y - heightEditBaseY
이 차이를 기존 heightOffset에 더해준다.
Y축 Slider만 사용하기
높이 조절에는 PositionHandle을 쓰지 않았다.
처음에는 위치 조절이니까 Handles.PositionHandle을 쓰면 되지 않을까 생각할 수 있다.
하지만 이번 기능에서 필요한 것은 위치 전체가 아니라 Y축 높이만 조절하는 것이다.
PositionHandle을 쓰면 X/Z 이동도 같이 가능해진다.
그러면 높이를 조절하려다가 배치 위치가 옆으로 밀릴 수 있다.
그래서 Y축 전용 slider를 사용했다.
private void DrawPlacementHeightHandle(SceneView sceneView)
{
if (!editPlacementHeightInScene || selectedPrefab == null || !hasHeightEditPose)
{
return;
}
// 높이 편집은 X/Z 이동이 섞이면 안 된다.
// PositionHandle은 카메라 방향 평면 이동까지 허용하므로 Y축 Slider만 사용한다.
Handles.color = new Color(0.2f, 0.9f, 1.0f, 0.95f);
float handleSize = HandleUtility.GetHandleSize(heightEditPosition);
EditorGUI.BeginChangeCheck();
Vector3 nextHandlePosition = Handles.Slider(
heightEditPosition,
Vector3.up,
handleSize * 0.85f,
Handles.ArrowHandleCap,
0.0f);
if (!EditorGUI.EndChangeCheck())
{
return;
}
float yDelta = nextHandlePosition.y - heightEditBaseY;
SetHeightOffset(heightEditBaseOffset + yDelta);
sceneView.Repaint();
}
Vector3 nextHandlePosition = Handles.Slider(
heightEditPosition,
Vector3.up,
handleSize * 0.85f,
Handles.ArrowHandleCap,
0.0f);
Vector3.up을 넘겨서 Y축 방향으로만 움직이는 slider를 만든다.
이렇게 하면 높이 조절 중 X/Z 좌표가 섞이지 않는다.


Height Offset 갱신
높이값은 SetHeightOffset()으로 갱신한다.
private void SetHeightOffset(float nextHeightOffset)
{
if (Mathf.Approximately(heightOffset, nextHeightOffset))
{
return;
}
float delta = nextHeightOffset - heightOffset;
heightOffset = nextHeightOffset;
if (hasHeightEditPose)
{
heightEditPosition += Vector3.up * delta;
}
Repaint();
SceneView.RepaintAll();
}
먼저 값이 거의 같으면 아무것도 하지 않는다.
if (Mathf.Approximately(heightOffset, nextHeightOffset))
{
return;
}
그리고 기존 높이와 새 높이의 차이를 계산한다.
float delta = nextHeightOffset - heightOffset;
heightOffset = nextHeightOffset;
만약 height 편집 모드 중이라면 handle 위치도 같이 올리거나 내린다.
if (hasHeightEditPose)
{
heightEditPosition += Vector3.up * delta;
}
이걸 하지 않으면 숫자나 단축키로 높이를 바꿨을 때, 실제 preview와 handle 위치가 서로 어긋날 수 있다.
편집 중에는 배치 클릭 막기
이 기능을 만들면서 중요한 문제가 하나 있었다.
Scene View에서 handle을 잡으려면 마우스 클릭을 해야 한다.
그런데 Map Tool도 마우스 좌클릭으로 프리팹을 배치한다.
즉, 아무 처리도 하지 않으면 이런 일이 생길 수 있다.
Size Handle을 잡으려고 클릭
→ 프리팹이 하나 배치됨
그래서 Size/Height 편집 중에는 배치 입력을 막았다.
// Size/Height 편집 중에는 Scene View handle이 좌클릭 드래그를 써야 한다.
// 배치 클릭과 겹치면 손잡이를 잡다가 prefab이 찍힐 수 있어서 잠깐 배치를 막는다.
bool isPlaceInput =
currentEvent.button == 0 &&
!currentEvent.alt &&
!editPlacementScaleInScene &&
!editPlacementHeightInScene;
이제 editPlacementScaleInScene이나 editPlacementHeightInScene이 켜져 있으면 isPlaceInput이 false가 된다.
그래서 아래 배치 로직이 실행되지 않는다.
if (currentEvent.type == EventType.MouseDown && isPlaceInput)
{
if (currentEvent.control)
{
TryDeleteUnderCursor(currentEvent.mousePosition);
}
else
{
TryPlacePrefab(currentEvent.mousePosition);
}
currentEvent.Use();
}
이 작은 조건이 없으면 툴 사용감이 꽤 나빠진다.
에디터 툴은 기능만 되는 것보다, 실수하기 어렵게 만드는 것도 중요하다고 느꼈다.
편집 중 Preview 고정
또 하나 중요한 점은 편집 중 preview가 마우스를 따라가지 않게 했다는 것이다.
일반 배치 모드에서는 preview가 마우스를 따라다닌다.
하지만 Size/Height 편집 모드에서는 고정된 위치에서 handle을 조작해야 한다.
그래서 preview를 그리는 쪽에서 편집 모드가 켜져 있으면 별도 함수를 호출하고 바로 return한다.
if (editPlacementScaleInScene && hasScaleEditPose)
{
DrawLockedScaleEditPreview();
return;
}
if (editPlacementHeightInScene && hasHeightEditPose)
{
DrawLockedHeightEditPreview();
return;
}
Scale 편집 중에는 DrawLockedScaleEditPreview()를 사용한다.
private void DrawLockedScaleEditPreview()
{
lastPreviewOccupied = IsCellOccupied(
scaleEditPosition,
scaleEditRotation,
GetPlacementLocalScale());
UpdatePreviewTransform(scaleEditPosition, scaleEditRotation);
UpdatePreviewMaterial(lastPreviewOccupied);
lastPreviewPosition = scaleEditPosition;
lastPreviewRotation = scaleEditRotation;
hasLastPreviewTransform = true;
}
Height 편집 중에도 같은 방식으로 고정된 위치를 사용한다.
private void DrawLockedHeightEditPreview()
{
lastPreviewOccupied = IsCellOccupied(
heightEditPosition,
heightEditRotation,
GetPlacementLocalScale());
UpdatePreviewTransform(heightEditPosition, heightEditRotation);
UpdatePreviewMaterial(lastPreviewOccupied);
lastPreviewPosition = heightEditPosition;
lastPreviewRotation = heightEditRotation;
hasLastPreviewTransform = true;
}
이렇게 해서 편집 중인 preview는 마우스를 따라가지 않고, 사용자가 조작 중인 위치에 고정된다.
전체 흐름
이번 기능의 전체 흐름은 이렇다.
1. 일반 배치 모드에서 preview를 움직인다.
2. 원하는 위치에서 Size 바꾸기 또는 Height 바꾸기를 누른다.
3. 그 순간의 preview 위치와 회전을 저장한다.
4. preview가 그 위치에 고정된다.
5. Scene View handle을 조작한다.
6. 조작값을 placementScale 또는 heightOffset에 반영한다.
7. 편집 모드를 끄면 다시 일반 배치 모드로 돌아간다.
코드로 보면 크게 세 부분이다.
MapToolWindow.cs
- 버튼, 상태값, 편집 모드 전환
MapToolWindow.Scene.cs
- Scene View handle 그리기, 단축키 처리
MapToolWindow.Preview.cs
- preview transform 갱신
정리
이번 작업은 맵툴을 숫자 입력 중심에서 Scene View 조작 중심으로도 옮기는 작업이었다.
이전에는 크기와 높이를 바꾸려면 대부분 Inspector 숫자나 버튼에 의존해야 했다.
하지만 이제는 Scene View에서 직접 손잡이를 잡고 조절할 수 있다.
이번에 추가한 핵심은 다음과 같다.
1. Size 바꾸기
2. Handles.ScaleHandle
3. Height 바꾸기
4. Handles.Slider
5. 1 / 3 높이 미세 조절
6. 편집 중 배치 입력 차단
7. 편집 중 preview 위치 고정
단순히 handle을 그리는 것만으로는 충분하지 않았다.
언제 preview를 고정할 것인가?
언제 배치 클릭을 막을 것인가?
크기 조절과 높이 조절을 동시에 켜지 않게 하려면 어떻게 할 것인가?
handle로 바뀐 값을 기존 Scale Multiplier 규칙과 어떻게 맞출 것인가?
이런 부분을 정리해야 실제로 쓸 수 있는 툴이 된다.
다음 글에서는 랜덤 스케일을 넣으면서 생긴 문제를 정리할 예정이다. (아주 힘든..ㅠ)
처음에는 단순히 X/Y/Z에 랜덤값만 주면 될 줄 알았다.
하지만 막상 배치해보니 블록 사이가 벌어지거나, 바닥에서 살짝 뜨는 문제가 생겼다.
그래서 랜덤 스케일을 넣으면서도 붙어 있는 면은 유지하는 보정이 필요했다.
'Unity - '한 우산 아래'' 카테고리의 다른 글
| 맵툴 제작기 7 - 회전축과 겹침 표시를 옵션으로 분리하기 (0) | 2026.05.01 |
|---|---|
| 맵툴 제작기 6 - 랜덤 스케일과 면 고정 배치 보정 (0) | 2026.05.01 |
| 맵툴 제작기 4 - Unity 에디터 툴로 프리팹 배치 개선하기 (0) | 2026.05.01 |
| 양팔저울 퍼즐 만들기 - 무게 조건과 시각 연출 분리하기 (0) | 2026.04.24 |
| 회전 발판 위에서 점프가 이상했던 이유 (0) | 2026.04.17 |