아트분께 맵툴을 보여드렸는데 사이즈 확대, 축소가 안된다는 피드백을 받았다.
따라서 이번 작업의 목표는 단순했다.
맵툴을 아트가 모델을 올려 맵의 분위기를 보기에 편하게 만드는 것.
기능을 엄청 크게 추가했다기보다는, 아트의 입장에서 맵을 찍다 보면 계속 거슬리는 부분들을 정리했다.
이번에 개선한 것
이번 글에서 정리할 내용은 다음과 같다.
1. 프리팹 기본 크기 유지(사이즈가 1,1,1로 나오는 의도치않은 동작이 있었다;)
2. 배치 크기 조절
3. 배치할 때 머테리얼 적용
4. 툴 옵션이 많아져서 스크롤 추가
랜덤 스케일과 면 고정 보정도 같이 작업하긴 했지만, 그건 내용이 꽤 길어져서 따로 정리할 생각이다.
프리팹을 툴로 배치했더니 크기가 달랐다
처음 문제는 프리팹 크기였다.
Project 창에서 프리팹을 직접 Scene View로 끌어다 놓으면 정상적인 크기로 나온다.
그런데 Map Tool에 같은 프리팹을 넣고 배치하면 이상하게 작게 나오는 경우가 있었다.
처음에는 단순히 배치 위치 문제인가 싶었다.
하지만 원인은 scale 처리였다.
기존에는 툴에서 사용하는 placementScale 값을 사실상 최종 scale처럼 다루고 있었다.
placementScale = 실제 배치 scale
이렇게 생각하면 단순해 보인다.
하지만 프리팹 자체가 이미 scale 값을 가지고 있는 경우 문제가 생긴다.
예를 들어 어떤 프리팹의 기본 scale이 이런 상태라고 해보자.
Prefab localScale = (60, 47, 60)
그런데 맵툴에서 placementScale을 (1, 1, 1)로 적용해버리면, 프리팹의 원래 크기를 무시하고 1, 1, 1 크기로 찍히게 된다.
그래서 생각을 바꿨다.
placementScale은 최종 크기가 아니라, 프리팹 기본 크기에 곱하는 배율로 보자.
최종 배치 크기 = 프리팹 기본 scale * placementScale
이렇게 하면 placementScale이 (1, 1, 1)일 때 프리팹 원래 크기가 유지된다.
프리팹 기본 크기 가져오기
먼저 선택된 프리팹의 기본 scale을 가져오는 함수를 만들었다.
private Vector3 GetSelectedPrefabBaseScale()
{
return selectedPrefab != null ? selectedPrefab.transform.localScale : Vector3.one;
}
선택된 프리팹이 있으면 그 프리팹의 localScale을 가져온다.
없으면 기본값으로 Vector3.one을 반환한다.
그리고 실제 배치에 사용할 scale은 이렇게 계산한다.
private Vector3 GetPlacementLocalScale()
{
// placementScale은 최종 localScale이 아니라 prefab 기본 scale에 곱하는 배율이다.
// 그래야 Project 창에서 직접 끌어놓은 크기와 Map Tool로 찍은 기본 크기가 같아진다.
return Vector3.Scale(GetSelectedPrefabBaseScale(), placementScale);
}
여기서 Vector3.Scale은 각 축끼리 곱해준다.
Prefab Base Scale = (60, 47, 60)
placementScale = (1, 1, 1)
결과 = (60, 47, 60)
이제 툴에서 Scale Multiplier가 1, 1, 1이면 프리팹 원래 크기로 배치된다.

배치할 때 scale 적용하기
실제 배치 함수에서는 계산된 scale을 사용한다.
private void TryPlacePrefab(Vector2 mousePosition, bool isDragPlacement = false)
{
// 실제 배치는 preview와 동일한 계산 경로를 타야
// 클릭 직전 화면과 결과물이 어긋나지 않는다.
if (selectedPrefab == null)
{
Debug.LogWarning("Map Tool: Assign a prefab before placing.");
return;
}
if (!TryGetPlacementPosition(mousePosition, out Vector3 surfacePosition, out Vector3 surfaceNormal))
{
return;
}
Quaternion placementRotation = GetPlacementRotation(surfaceNormal);
// 프리팹 기본 크기 * 배치 배율
Vector3 baseLocalScale = GetPlacementLocalScale();
bool useNeighborSnap = snapToNeighbor || Event.current.alt;
Vector3 snappedPosition = GetAlignedPreviewPosition(
surfacePosition,
placementRotation,
surfaceNormal,
baseLocalScale);
}
여기서 중요한 점은 위치 계산에도 scale이 필요하다는 것이다.
단순히 오브젝트를 만든 뒤 scale만 바꾸면 안 된다.
왜냐하면 오브젝트의 크기에 따라 바닥에 닿는 위치도 달라지기 때문이다.
그래서 preview 위치를 계산할 때도 baseLocalScale을 넘겨준다.
Vector3 snappedPosition = GetAlignedPreviewPosition(
surfacePosition,
placementRotation,
surfaceNormal,
baseLocalScale);
이렇게 해야 긴 오브젝트나 큰 오브젝트를 배치할 때도 바닥에 제대로 붙는다.
실제 프리팹 생성
프리팹 생성은 PrefabUtility.InstantiatePrefab을 사용한다.
GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(selectedPrefab);
if (instance == null)
{
Debug.LogError("Map Tool: Failed to instantiate prefab.");
return;
}
Undo.RegisterCreatedObjectUndo(instance, "Place Map Prefab");
instance.transform.position = snappedPosition;
instance.transform.rotation = placementRotation;
instance.transform.localScale = placementLocalScale;
여기서 PrefabUtility.InstantiatePrefab()을 사용한 이유는, 맵툴이 런타임 기능이 아니라 에디터에서 프리팹을 배치하는 도구이기 때문이다.
Instantiate()로도 오브젝트를 복사할 수는 있지만, 에디터에서 프리팹을 배치할 때는 원본 프리팹과의 연결을 유지하는 것이 중요하다.
PrefabUtility.InstantiatePrefab()으로 생성한 오브젝트는 Unity가 프리팹 인스턴스로 인식한다.
그래서 원본 프리팹 수정, Prefab Override, Inspector에서의 프리팹 연결 표시 같은 Unity의 프리팹 워크플로우를 그대로 사용할 수 있다.
그리고 Undo.RegisterCreatedObjectUndo를 등록해둔다.
그래야 툴로 배치한 오브젝트도 Unity에서 Ctrl + Z로 되돌릴 수 있다.
배치 툴은 실수하면서 쓰는 도구이기 때문에 툴을 만들 때 Undo는 은근 중요하다!
Scale Multiplier 추가
이제 툴에서 배치 배율을 조절할 수 있게 했다.
Vector3 nextPlacementScale = EditorGUILayout.Vector3Field(
new GUIContent(
"Scale Multiplier",
"prefab의 기본 scale에 곱해지는 X/Y/Z 배율입니다. 1, 1, 1이면 prefab 원래 크기를 유지합니다."),
placementScale);
placementScale = ApplyPlacementScaleInput(nextPlacementScale);
이 값은 최종 scale이 아니라 배율이다.
Scale Multiplier = (1, 1, 1)
→ 프리팹 원래 크기
Scale Multiplier = (2, 1, 1)
→ X축만 2배
Scale Multiplier = (0.5, 0.5, 0.5)
→ 전체 절반 크기
처음에는 그냥 X/Y/Z를 따로 입력하면 충분하다고 생각했다.
하지만 블록아웃 작업에서는 전체 크기를 일정하게 키우거나 줄이는 경우도 많다.
그래서 Uniform Scale 옵션을 추가했다.
(없으면 섭섭할 것 같기에.)
Uniform Scale
Uniform Scale이 켜져 있으면 한 축을 바꿔도 나머지 축이 같이 바뀐다.
private Vector3 ApplyPlacementScaleInput(Vector3 nextScale)
{
nextScale = SanitizePlacementScale(nextScale);
if (!uniformPlacementScale)
{
return nextScale;
}
// Uniform Scale이 켜져 있으면 가장 크게 바뀐 축의 값을 기준으로 세 축을 모두 맞춘다.
// Inspector에서 X만 입력하거나 Scene View에서 한 축 손잡이를 잡아도 같은 크기로 유지하기 위함이다.
float uniformValue = GetMostChangedScaleAxisValue(placementScale, nextScale);
return MakeUniformPlacementScale(uniformValue);
}
여기서 그냥 X 값만 기준으로 삼지 않았다.
왜냐하면 사용자가 Y나 Z를 직접 바꿀 수도 있기 때문이다.
그래서 이전 scale과 새 scale을 비교해서 가장 크게 바뀐 축을 기준으로 삼았다.
private float GetMostChangedScaleAxisValue(Vector3 previousScale, Vector3 nextScale)
{
float xDelta = Mathf.Abs(nextScale.x - previousScale.x);
float yDelta = Mathf.Abs(nextScale.y - previousScale.y);
float zDelta = Mathf.Abs(nextScale.z - previousScale.z);
if (yDelta > xDelta && yDelta >= zDelta)
{
return nextScale.y;
}
if (zDelta > xDelta && zDelta > yDelta)
{
return nextScale.z;
}
return nextScale.x;
}
이렇게 하면 어떤 축을 만지든 가장 의도적으로 바꾼 값이 전체 scale이 된다.
너무 작은 scale 막기
scale 값이 0 이하가 되면 오브젝트가 사라지거나 뒤집힐 수 있다.
그래서 최소값을 두었다.
private const float MinimumPlacementScale = 0.01f;
private Vector3 SanitizePlacementScale(Vector3 scale)
{
// 0 이하 scale은 preview bounds 계산과 실제 배치가 뒤집히거나 사라지는 원인이 된다.
// 맵툴에서는 "prefab 기본 scale에 곱하는 배율"로 쓰기 위해 아주 작은 양수까지로 제한한다.
return new Vector3(
Mathf.Max(MinimumPlacementScale, scale.x),
Mathf.Max(MinimumPlacementScale, scale.y),
Mathf.Max(MinimumPlacementScale, scale.z));
}
이런 방어 코드는 작지만 중요하다.
에디터 툴은 실수로 이상한 값을 넣기 쉽다.
값 하나 잘못 넣었다고 Scene View가 이상해지면 작업 흐름이 끊긴다.
배치할 때 머테리얼 적용하기
맵을 찍다 보니 또 하나 불편한 점이 있었다.
블록을 배치한 뒤 매번 머테리얼을 따로 바꾸는 게 귀찮았다.
특히 블록아웃 단계에서는 색으로 영역을 구분하는 일이 많다.
그래서 배치 시점에 머테리얼을 바로 적용할 수 있게 했다.
placementMaterial = (Material)EditorGUILayout.ObjectField(
new GUIContent(
"Placement Material",
"배치할 때 바로 적용할 material입니다. 비워두면 prefab의 기존 material을 유지합니다."),
placementMaterial,
typeof(Material),
false
);
그리고 실제 배치가 끝난 뒤 이 함수를 호출한다.
ApplyPlacementMaterial(instance);
구현은 단순하다.
private void ApplyPlacementMaterial(GameObject targetObject)
{
if (placementMaterial == null)
{
return;
}
ApplyMaterialToObject(targetObject, placementMaterial, "Apply Placement Material");
}
placementMaterial이 비어 있으면 아무것도 하지 않는다.
즉, 기존 프리팹 머테리얼을 유지하고 싶으면 비워두면 된다.
실제로 머테리얼을 바꾸는 코드는 공통 함수로 뺐다.
private void ApplyMaterialToObject(GameObject targetObject, Material material, string undoName)
{
if (targetObject == null || material == null)
{
return;
}
Renderer[] renderers = targetObject.GetComponentsInChildren<Renderer>();
foreach (Renderer renderer in renderers)
{
Undo.RecordObject(renderer, undoName);
Material[] materials = renderer.sharedMaterials;
for (int i = 0; i < materials.Length; i++)
{
materials[i] = material;
}
renderer.sharedMaterials = materials;
EditorUtility.SetDirty(renderer);
}
}
여기서 GetComponentsInChildren<Renderer>()를 사용한 이유는 프리팹이 반드시 하나의 MeshRenderer만 가진다는 보장이 없기 때문이다.
자식 오브젝트에 여러 Renderer가 있을 수도 있다.
그래서 하위 Renderer까지 전부 찾아서 머테리얼을 적용한다.

선택한 오브젝트에 머테리얼 일괄 적용
배치할 때만 머테리얼을 바꾸는 것으로는 부족할 수 있다.
이미 찍어둔 오브젝트들의 색을 나중에 바꾸고 싶을 때도 있다.
그래서 선택한 오브젝트들에 머테리얼을 일괄 적용하는 기능도 유지했다.
private void ApplyMaterialToSelection()
{
// 블록아웃 단계에서는 배치 직후 material을 여러 개 한 번에 갈아끼우는 일이 잦다.
// 그래서 배치 툴 안에서 바로 material preset 적용까지 처리한다.
if (selectedMaterial == null)
{
Debug.LogWarning("Map Tool: Assign a material preset before applying.");
return;
}
foreach (GameObject selectedObject in Selection.gameObjects)
{
ApplyMaterialToObject(selectedObject, selectedMaterial, "Apply Material Preset");
}
}
이 기능은 배치와는 별개다.
Placement Material
→ 앞으로 배치할 오브젝트에 적용
Material Preset + Apply Material To Selection
→ 이미 선택된 오브젝트에 적용
둘의 역할을 나누었다.
옵션이 많아져서 스크롤 추가
기능이 하나씩 늘어나자 Map Tool 창이 점점 길어졌다.
처음에는 괜찮았다.
하지만 프리팹 프리셋, 배치 옵션, 크기 조절, 머테리얼, 스냅 옵션, 디버그 상태까지 들어가니 창 아래쪽이 잘릴 수 있었다.
그래서 전체 OnGUI()를 스크롤 영역으로 감쌌다.
private Vector2 scrollPosition;
스크롤 위치를 저장할 변수를 하나 두고:
private void OnGUI()
{
// EditorWindow 높이가 작을 때도 모든 배치 옵션을 확인할 수 있게 전체 UI를 스크롤 영역으로 감싼다.
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
// 여러 배치 옵션들...
EditorGUILayout.EndScrollView();
}
이제 창 크기를 작게 줄여도 아래 옵션까지 스크롤해서 볼 수 있다.
작은 기능이지만, 에디터 툴에서는 이런 사용성 개선이 중요하다.
툴은 결국 계속 열어두고 쓰는 창이기 때문이다.

이번 작업의 정리
이번 작업은 거대한 기능 추가라기보다는 맵툴을 실제 작업에 맞게 다듬는 과정이었다.
처음에는 단순히 프리팹을 찍는 툴이었다.
하지만 직접 맵을 찍어보니 이런 문제가 보였다.
프리팹 원래 크기가 유지되지 않는다.
배치할 때 크기를 바로 바꾸고 싶다.
색 구분을 위해 머테리얼을 바로 적용하고 싶다.
옵션이 많아져서 창이 길어진다.
그래서 이번에 다음을 추가했다.
1. 프리팹 기본 scale 유지
2. Scale Multiplier
3. Uniform Scale
4. Placement Material
5. 선택 오브젝트 머테리얼 일괄 적용
6. Map Tool 창 스크롤
가장 중요한 변화는 scale을 바라보는 방식이었다.
이전에는 scale을 최종 크기로 생각했다.
하지만 이제는 이렇게 본다.
최종 크기 = 프리팹 기본 크기 * 배치 배율
이렇게 바꾸니 Project 창에서 직접 끌어다 놓은 것과 Map Tool로 찍은 결과가 더 일관되게 맞았다.
그리고 배치 시점에 머테리얼을 적용할 수 있게 하면서 블록아웃 속도도 조금 더 빨라졌다.
다음 글에서는 Scene View에서 직접 크기와 높이를 조절하는 기능을 정리할 생각이다.
Inspector에 숫자를 입력하는 것도 좋지만, 맵을 찍을 때는 눈으로 보면서 손잡이를 잡아 조절하는 쪽이 훨씬 직관적이기 때문이다.
'Unity - '한 우산 아래'' 카테고리의 다른 글
| 맵툴 제작기 6 - 랜덤 스케일과 면 고정 배치 보정 (0) | 2026.05.01 |
|---|---|
| 맵툴 제작기 5 - Scene View에서 크기와 높이를 직접 조절하기 (0) | 2026.05.01 |
| 양팔저울 퍼즐 만들기 - 무게 조건과 시각 연출 분리하기 (0) | 2026.04.24 |
| 회전 발판 위에서 점프가 이상했던 이유 (0) | 2026.04.17 |
| 회전 플랫폼 위에서 상자의 대각선 밀림 고쳐보기 (0) | 2026.04.17 |