AI/Unity

[Unity] 타워디펜스 맵 & 웨이브 에디터

blacknabis 2026. 3. 2. 03:00
반응형

타워디펜스 게임을 개발하다 보면 맵 데이터를 관리하는 방법이 항상 문제다.

처음에는 GameBattlefield.prefab 하위에 PathRoot/Path0/Waypoint_0, PathRoot/Path0/Waypoint_1... 식으로 씬 계층(Transform 트리)에 직접 경로를 박아넣었다. 타워 슬롯도 마찬가지로 TowerRoot 하위에 빈 게임오브젝트를 하나씩 배치했다.

이 방법은 처음엔 빠르지만, 금방 한계가 온다.

  • 새 맵을 추가하려면 프리팹을 복사하고 Transform을 일일이 수정해야 한다
  • 웨이브 밸런스를 조정하려면 WaveConfig 인스펙터를 열고 리스트를 손으로 뒤져야 한다
  • 전체 밸런스가 어떤지 한눈에 보이지 않는다
  • 기획자가 직접 조작하기 어렵다 (개발자가 옆에 있어야 한다)

맵이 1개일 때는 그냥 참을 수 있다. 하지만 Stage 2, 3...을 추가하고 싶어지는 순간, 이 구조는 관리 비용이 폭증한다.

그래서 에디터 도구를 직접 만들기로 했다.


목표

  • 씬 뷰에서 클릭 몇 번으로 경로와 타워 슬롯을 편집할 수 있을 것
  • 맵 데이터를 ScriptableObject(SO) 로 분리해서 프리팹과 독립적으로 관리할 것
  • 웨이브 구성과 밸런스 지표를 한 화면에서 볼 수 있을 것
  • 새 맵 추가 시 프리팹을 복사하지 않고 SO만 만들면 게임이 돌아갈 것

구성 요소

총 5개의 에디터 파일을 만들었다.

1. MapConfigData (ScriptableObject)

맵의 모든 정적 데이터를 담는 SO.

public class MapConfigData : ScriptableObject
{
    public int StageId;
    public List<PathData> Paths;              // 경로 목록 (웨이포인트 리스트)
    public List<Vector3> TowerSlotPositions;  // 타워 설치 가능 위치
    public string BackgroundSpritePath;       // Resources 상대 경로
}

이 파일 하나만 있으면 해당 스테이지를 런타임에서 완전히 복원할 수 있다.

저장 위치: Assets/Resources/Kingdom/Configs/Maps/Stage_1_MapConfig.asset


2. MapEditorWindow — 맵 편집 창

Unity 상단 메뉴 Tools > Kingdom > Map > Map Editor로 열린다.

주요 기능:

  • SO 에셋 드래그 앤 드롭으로 불러오기 / 새로 만들기
  • 경로(Path) 추가/삭제, 경로별 웨이포인트 편집
  • 타워 슬롯 위치 편집
  • 배경 이미지 경로 설정 (스프라이트 드래그하면 Resources 경로 자동 추출)
  • 창 상단에 유효성 검사 배너 표시 (오류 = 빨강, 경고 = 노랑)

3. PathHandleEditor — 씬 뷰 핸들 편집기

MapEditorWindow에서 경로 편집 모드를 켜면 씬 뷰에서 직접 조작할 수 있다.

동작 방법
웨이포인트 이동 씬 뷰 핸들 드래그
웨이포인트 추가 Shift + 클릭
웨이포인트 삭제/삽입 우클릭 컨텍스트 메뉴

경로마다 색이 다르게 표시된다: Path0=초록, Path1=파랑, Path2=주황...
시작점은 초록 구, 도착점은 빨간 구로 구분된다.

핵심 구현 포인트:
SceneView.duringSceneGui 이벤트를 구독해서 씬 뷰에 직접 그린다. Handles.FreeMoveHandle()로 드래그 가능한 핸들을 생성하고, Undo.RecordObject()로 실행 취소를 지원한다.

private void OnSceneGui(SceneView sv)
{
    if (_target == null) return;

    for (int i = 0; i < _target.Paths.Count; i++)
    {
        PathData path = _target.Paths[i];
        DrawPathHandles(path, _pathColors[i % _pathColors.Length]);
    }
}

4. TowerSlotHandleEditor — 타워 슬롯 핸들 편집기

타워 슬롯 편집 모드에서 동작한다.

  • Shift + 클릭 → 슬롯 추가
  • 노란 사각형 핸들 드래그 → 이동
  • 우클릭 → 삭제
  • GameplayBounds 바깥에 있는 슬롯은 빨간색으로 경고 표시
  • 기본 사거리 가이드 원 표시 (Archer 기준 반경 4.0f)

5. WaveBalanceWindow — 웨이브 밸런스 편집 창

Tools > Kingdom > Map > Wave Balance Editor

창 상단 밸런스 지표 패널:

총 웨이브: 5     총 적 수: 87
총 HP: 18,400    총 골드: 1,240

이 숫자들이 실시간으로 자동 계산된다. 스폰 수를 늘리면 바로 반영되니 밸런스 조정이 직관적이다.

웨이브 헤더 색상:
일반 웨이브 = 파랑, 보스 웨이브 = 붉은색으로 즉시 구분된다.

스폰 엔트리 편집:
EnemyConfig 에셋을 드래그하면 자동으로 HP, 골드 수치가 계산에 반영된다.
PathId를 드롭다운으로 선택할 수 있다 (MapConfig가 연결되어 있을 때).


가장 까다로웠던 부분

SceneView에 배경 이미지 미리보기

맵 에디터에서 배경 스프라이트를 설정하면 씬 뷰에 실시간으로 표시되게 했다.

문제는 씬 뷰는 EditorWindow GUI와 좌표계가 다르다는 점이다.
월드 좌표 → GUI 좌표 변환이 필요하다.

private void OnSceneGui(SceneView sv)
{
    if (string.IsNullOrWhiteSpace(_target.BackgroundSpritePath)) return;
    Sprite sprite = Resources.Load<Sprite>(_target.BackgroundSpritePath);
    if (sprite == null) return;

    Handles.BeginGUI();

    Rect bounds = _target.GameplayBounds;
    Vector2 guiMin = HandleUtility.WorldToGUIPoint(
        new Vector3(bounds.xMin, bounds.yMin, 0));
    Vector2 guiMax = HandleUtility.WorldToGUIPoint(
        new Vector3(bounds.xMax, bounds.yMax, 0));

    Rect guiRect = new Rect(
        Mathf.Min(guiMin.x, guiMax.x),
        Mathf.Min(guiMin.y, guiMax.y),
        Mathf.Abs(guiMax.x - guiMin.x),
        Mathf.Abs(guiMax.y - guiMin.y));

    // Atlas 스프라이트 대응: 텍스처 내 UV 좌표 계산
    Rect texCoords = new Rect(
        sprite.rect.x / sprite.texture.width,
        sprite.rect.y / sprite.texture.height,
        sprite.rect.width / sprite.texture.width,
        sprite.rect.height / sprite.texture.height);

    GUI.color = new Color(1f, 1f, 1f, 0.85f);
    GUI.DrawTextureWithTexCoords(guiRect, sprite.texture, texCoords);
    GUI.color = Color.white;

    Handles.EndGUI();
}

GUI.DrawTexture()가 아니라 GUI.DrawTextureWithTexCoords()를 써야 atlas 스프라이트에서도 올바른 영역이 잘려서 표시된다.


데이터 소스 이원화 문제

초기에는 맵 데이터가 두 곳에 있었다.

  • 프리팹 Transform 계층 (기존)
  • MapConfigData SO (신규)

런타임 코드가 SO를 먼저 보고, 없으면 프리팹 계층을 폴백으로 읽는 이중 구조였는데, 이게 오히려 혼란을 만들었다.

결론: 프리팹의 PathRoot, TowerRoot 하위 오브젝트를 전부 삭제하고, SO가 없으면 에러 로그 방식으로 단순화했다.

// 이제 폴백 없음
MapConfigData mapConfig = ConfigResourcePaths.LoadMapConfigByStageId(stageId);
if (mapConfig == null || mapConfig.Paths.Count == 0)
{
    Debug.LogError($"[GameScene] Stage {stageId} MapConfigData를 찾을 수 없습니다.");
    return;
}
_pathManager.SetPaths(mapConfig.Paths);

결과

새 맵을 추가하는 절차:

1. Map Editor → "새로 만들기"
2. 씬 뷰에서 경로 웨이포인트 배치 (Shift+클릭)
3. 씬 뷰에서 타워 슬롯 배치 (Shift+클릭)
4. Wave Balance Editor에서 웨이브 편집
5. 저장 → 끝

프리팹 복사 없음. 씬 복제 없음. SO 하나가 맵의 전부다.

 

맵에디터


마무리

Unity 에디터 확장은 EditorWindow, Handles, SceneView.duringSceneGui 세 가지를 조합하면 대부분 원하는 걸 만들 수 있다. 처음엔 좌표계 변환(월드↔GUI)이 헷갈리지만, HandleUtility.WorldToGUIPoint()GUIUtility.GUIToScreenPoint()의 역할 차이를 구분하면 감이 잡힌다.

비슷한 구조의 레벨 에디터를 만들 계획이 있다면 참고가 되면 좋겠다.


이 글이 도움이 됐다면 댓글로 알려주세요.

반응형