[Unity] EditorWindow로 오브젝트 이동 툴 만들기

Argonaut·2025년 2월 5일

0. 배경 – "건물 모델을 합쳤다, 분리했다! 더 간편하게 할 수 없을까?"


모델링 팀에서는 건물 작업을 할 때 내부와 외부를 따로 제작하는 경우가 많다.
외부 작업을 하다가 내부 디테일을 조정해야 할 때 또는 내부 작업을 하다가 전체적인 모습을 확인해야 할 때
건물을 합친 상태와 분리된 상태를 반복적으로 전환해야 하는 경우가 생긴다.

처음에는 각 모델을 개별적으로 불러오고 위치를 맞추는 방식으로 작업했지만
이 과정이 반복될수록 점점 비효율적인 문제가 생겼다.

그리고 어느 날, 모델링 팀으로부터 이런 메시지가 왔다.

"이거 매번 수동으로 옮기는 게 너무 번거로워요!
합쳐진 상태랑 분리된 상태를 계속 왔다 갔다 해야 하는데,
위치 맞추는 것도 힘들고 매번 수동으로 조정하는 게 불편해요."

이처럼 모델을 유동적으로 전환하는 기능이 있다면 작업 과정이 훨씬 편해질 거라고 생각했다.
그래서, 이를 해결할 수 있는 기능을 Unity에서 직접 구현해보기로 했다.

1. 어떤 기능이 필요했을까?


모델링 팀과 논의해보니, 다음과 같은 기능이 필요했다.

한번의 조작으로 오브젝트의 위치 전환이 가능해야한다.
오브젝트 별로 위치 지정이 가능해야한다.
오브젝트 수를 추가할 수 있어야한다.
현재 저장된 위치가 유니티를 재실행했을 때 남아있어야한다.

이 요청을 바탕으로 어떻게 하면 모델링 팀이 더 편하게 작업할 수 있을지 고민해보았다.
결과적으로, 한 번의 클릭으로 오브젝트를 전환할 수 있는 기능을 만들기로 했다.

2. 구현 방법


모델링 팀은 Unity의 Runtime 환경에서 작업하는 것이 아니라 에디터 내에서 오브젝트를 배치하고 조정하는 작업이 많았다.
따라서, 이 기능을 게임 실행 중이 아니라 Unity 에디터에서 직접 활용할 수 있도록 Tool로 제작하게 되었다.
Unity에서는 이러한 작업을 위해 EditorWindow를 제공하는데 이는 Inspector나 Scene 뷰처럼 독립적인 커스텀 윈도우를 만들어 컴포넌트를 직접 추가하지 않고도 원하는 기능을 구현할 수 있는 강력한 도구다.

1) EditorWindow 기본 생성 및 설정

Unity에서 EditorWindow를 사용하면 게임을 실행하지 않고도 에디터에서 직접 커스텀 기능을 만들 수 있다.
이를 활용하면 모델링 팀이 오브젝트의 위치를 쉽게 전환할 수 있도록 전용 도구를 제공할 수 있다.

아래 코드를 실행하면 Unity의 Tools 메뉴에 "Object Switch Tool" 이라는 항목이 추가된다.
이 메뉴를 클릭하면 커스텀 창(Object Switch Tool)이 생성되어 오브젝트를 추가하고 관리할 수 있다.

public class ObjectSwitchTool : EditorWindow
{
    // 편집기에서 관리할 오브젝트 리스트
    private readonly List<GameObject> objects = new();

    // 각 오브젝트의 A/B 위치 저장
    private readonly Dictionary<GameObject, (Vector3 A, Vector3 B)> positions = new();

    // 현재 위치가 A인지 B인지 저장
    private readonly Dictionary<GameObject, bool> positionSwitch = new();

    private bool foldout = true;                    // 오브젝트 리스트 펼치기/접기 상태 저장
    private ObjectPositionStorage positionStorage;  // 위치 데이터를 저장할 오브젝트
    private GameObject saveTargetObject;            // 위치 데이터를 저장할 게임 오브젝트
    private Vector2 scrollPosition;                 // 스크롤 위치 저장

    // 0. Unity 메뉴에서 "Tools/Object Switch Tool"을 선택하면 창을 열도록 설정
    [MenuItem("Tools/Object Switch Tool")]
    public static void ShowWindow()
    {
        GetWindow<ObjectSwitchTool>("Object Switch Tool");
    }

    private void OnGUI()
    {
        DrawObjectManagement();
    }

    // 1. 오브젝트 관리 UI
    private void DrawObjectManagement()
    {
        GUILayout.BeginHorizontal(); // 가로 정렬 시작

        GUILayout.Label("오브젝트 관리", EditorStyles.boldLabel); // 레이블 표시 (글씨 Bold)
        if (GUILayout.Button("+ 오브젝트 추가")) objects.Add(null); // 버튼 표시

        GUILayout.EndHorizontal(); // 가로 정렬 끝

    }
}
  • 실행 결과 – Object Switch Tool 창
    위 코드를 실행하면 Unity의 Tools 메뉴에 새로운 항목이 추가된다.
    이제 Tools > Object Switch Tool을 클릭하면 아래와 같은 커스텀 창이 생성된다.

  • Tools > Object Switch Tool 메뉴 추가됨

  • Object Switch Tool 창이 뜨고 ‘+ 오브젝트 추가’ 버튼이 표시됨
    (이곳에서 이후 기능을 추가하여 오브젝트의 A/B 위치를 관리할 예정)

2) UI 및 기능 추가

이제 오브젝트를 관리하기 위한 UI와 기능을 추가해보자
필요한 기능을 정리해보면

1) 오브젝트 리스트를 표시하고 추가/삭제 가능
2) A/B 위치를 저장하고 이동하는 버튼 제공
3) 모든 오브젝트의 위치를 한 번에 변경하는 기능
4) 현재 저장 내역 유지 기능

와 같다. 이를 구현해보자

2-1. 오브젝트 리스트를 표시하고 추가/삭제 가능

오브젝트를 편집기에 추가하는 기능은 DrawObjectManagement()에서 구현
✔️ "+ 오브젝트 추가" 버튼을 눌러 새로운 오브젝트 슬롯을 추가
✔️ 각 오브젝트 옆에 삭제 버튼을 추가하여 관리 가능

📌 DrawObjectManagement() 코드

// UI에서 오브젝트를 추가 및 삭제하는 부분
GUILayout.Label("오브젝트 관리", EditorStyles.boldLabel);
if (GUILayout.Button("+ 오브젝트 추가")) objects.Add(null);

// 오브젝트 리스트를 표시하는 UI
foldout = EditorGUILayout.Foldout(foldout, $"오브젝트 리스트 ({objects.Count})");
if (!foldout) return;

List<GameObject> toRemove = new();
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300));

for (int i = 0; i < objects.Count; i++)
{
    GUILayout.BeginVertical("box");
    objects[i] = (GameObject)EditorGUILayout.ObjectField($"Element {i}", objects[i], typeof(GameObject), true);
    if (GUILayout.Button("삭제")) toRemove.Add(objects[i]);
    if (objects[i] != null) HandleObjectEntry(objects[i]);
    GUILayout.EndVertical();
}
EditorGUILayout.EndScrollView();

// 삭제할 오브젝트 리스트 업데이트
toRemove.ForEach(obj => { objects.Remove(obj); positions.Remove(obj); positionSwitch.Remove(obj); });

📌 HandleObjectEntry() 코드

    /// <summary>
    /// 개별 오브젝트에 대한 UI 및 기능 처리
    /// </summary>
    private void HandleObjectEntry(GameObject obj)
    {
        if (!positions.ContainsKey(obj)) InitializeObjectPosition(obj); // 오브젝트 위치 초기화
        DisplayPositionInfo(obj);                                       // 오브젝트 위치 정보 표시
        DrawPositionControls(obj);                                      // 오브젝트 위치 제어 버튼 표시
    }
  • 📌 버튼으로 오브젝트 슬롯 추가/삭제가 가능

2-2. A/B 위치 저장 & 전환 UI

오브젝트의 A/B 위치를 저장하고 이동하는 기능은 DrawPositionControls()에서 구현
✔️ "A 위치 저장" / "B 위치 저장" 버튼을 눌러 현재 위치를 저장
✔️ "A 위치 이동" / "B 위치 이동" 버튼으로 지정한 위치로 이동 가능
✔️ "위치 토글" 버튼으로 A/B 전환 가능
✔️ 위치정보를 표시하는 기능 추가 DisplayPositionInfo() 에서 구현

📌 DrawPositionControls() 코드

    /// <summary>
    /// 오브젝트의 위치를 저장, 이동, 토글하는 UI 버튼 추가
    /// </summary>
    private void DrawPositionControls(GameObject obj)
    {
        GUILayout.BeginHorizontal();    // 가로 정렬 시작
        if (GUILayout.Button("A 위치 저장")) SavePosition(obj, true);
        if (GUILayout.Button("B 위치 저장")) SavePosition(obj, false);
        GUILayout.EndHorizontal();      // 가로 정렬 끝

        GUILayout.BeginHorizontal();
        if (GUILayout.Button("A 위치 이동")) MoveObject(obj, true);
        if (GUILayout.Button("B 위치 이동")) MoveObject(obj, false);
        if (GUILayout.Button("위치 토글")) ToggleObjectPosition(obj);
        GUILayout.EndHorizontal();
    }

📌 DisplayPositionInfo() 코드

	/// <summary>
    /// 현재 오브젝트의 위치 정보를 표시
    /// </summary>
    private void DisplayPositionInfo(GameObject obj)
    {
        string positionState 
            = positions.ContainsKey(obj) ? ($"A: {positions[obj].A}, B: {positions[obj].B}") : "위치 미저장";
        EditorGUILayout.LabelField("저장된 위치:", positionState);
        EditorGUILayout.LabelField("현재 위치:", positionSwitch[obj] ? "A 위치" : "B 위치");
    }

📌 기능 코드

	 /// <summary>
    /// 오브젝트의 위치를 저장
    /// </summary>
    /// <param name="obj">대상 오브젝트</param>
    /// <param name="isA">현재 위치가 A인지 여부</param>
	private void SavePosition(GameObject obj, bool isA)
    {
        if (obj != null) positions[obj] = isA ? (obj.transform.position, positions[obj].B) : (positions[obj].A, obj.transform.position);
    }

	/// <summary>
    /// 오브젝트를 이동
    /// </summary>
    /// <param name="obj">대상 오브젝트</param>
    /// <param name="toA">A로 이동시킬지 여부</param>
    private void MoveObject(GameObject obj, bool toA)
    {
        if (obj != null && positions.ContainsKey(obj))
        {
            obj.transform.position = toA ? positions[obj].A : positions[obj].B;
            positionSwitch[obj] = toA;
        }
    }
    
    private void ToggleObjectPosition(GameObject obj)
    {
        if (obj != null && positions.ContainsKey(obj)) MoveObject(obj, !positionSwitch[obj]);
    }
  • A/B 위치 저장 & 전환 UI
  • A/B 위치 정보 UI 추가

2-3. 모든 오브젝트의 위치를 한 번에 변경하는 기능 추가

전체 오브젝트의 위치 변경 기능은 DrawActionsAllObject()에서 구현
✔️ "A / B 위치로 이동" 버튼을 눌러 위치로 이동
✔️ "위치 토글" 버튼으로 A/B 전환 가능

📌 DrawActionsAllObject() 코드

    /// <summary>
    /// 현재 오브젝트의 위치 정보를 표시
    /// </summary>
    private void DrawActionsAllObject()
    {
        GUILayout.Space(10);
        GUILayout.Label("전체 오브젝트 위치 변경", EditorStyles.boldLabel);
        GUILayout.BeginHorizontal();
        if (GUILayout.Button("A 위치로 이동")) MoveAllObjects(true);
        if (GUILayout.Button("B 위치로 이동")) MoveAllObjects(false);
        if (GUILayout.Button("위치 토글")) ToggleAllObjectsPosition();
        GUILayout.EndHorizontal();
    }

📌 기능 코드

	private void MoveAllObjects(bool toA)
    {
        foreach (var obj in objects) MoveObject(obj, toA);
    }

    private void ToggleAllObjectsPosition()
    {
        foreach (var obj in objects) ToggleObjectPosition(obj);
    }
  • 모든 A/B 위치 전환 UI

2-4. 현재 저장 내역 유지 기능

오브젝트의 A/B 위치를 저장하고, 에디터를 닫아도 유지할 수 있도록 구현
이 기능을 통해, Editor를 닫거나 Unity를 재시작해도 오브젝트의 위치 정보를 유지할 수 있다.
즉, 위치를 저장했다가 나중에 다시 불러오는 기능을 제공하며,
이를 위해 ObjectPositionStorage와 ObjectPositionEntry를 활용하여 데이터를 관리한다.


✅ 1) 위치 데이터 저장 구조
위치 저장을 위해 두 개의 클래스를 사용한다.
✔️ ObjectPositionStorage -> 위치 데이터를 저장하는 컴포넌트
✔️ ObjectPositionEntry -> 각 오브젝트별 저장 데이터 구조체

📌 ObjectPositionStorage 코드

public class ObjectPositionStorage : MonoBehaviour
{
    public List<ObjectPositionEntry> objectPositions = new();

    /// <summary>
    /// 오브젝트 위치 저장
    /// </summary>
    /// <param name="obj">대상 오브젝트</param>
    /// <param name="posA">위치 A의 좌표</param>
    /// <param name="posB">위치 B의 좌표</param>
    /// <param name="isAtA">현 위치가 A인지 여부</param>
    public void SavePosition(GameObject obj, Vector3 posA, Vector3 posB, bool isAtA)
    {
        var entry = objectPositions.Find(e => e.gameObject == obj);
        if (entry != null)
        {
            entry.positionA = posA;
            entry.positionB = posB;
            entry.isAtA = isAtA;
        }
        else
        {
            objectPositions.Add(new ObjectPositionEntry()
            {
                gameObject = obj,
                positionA = posA,
                positionB = posB,
                isAtA = isAtA
            });
        }
    }

    /// <summary>
    /// 오브젝트 위치를 가져옴
    /// </summary>
    /// <param name="obj">대상 오브젝트</param>
    /// <returns>(A좌표, B좌표. A여부)</returns>
    public (Vector3 A, Vector3 B, bool isAtA)? GetPosition(GameObject obj)
    {
        var entry = objectPositions.Find(e => e.gameObject == obj);
        if (entry != null)
        {
            return (entry.positionA, entry.positionB, entry.isAtA);
        }
        return null;
    }
}

📌 ObjectPositionEntry 코드

[System.Serializable]
public class ObjectPositionEntry
{
    public GameObject gameObject;
    public Vector3 positionA;
    public Vector3 positionB;
    public bool isAtA;
}

✅ 2) UI – 위치 데이터 저장 & 불러오기
데이터를 저장/불러오는 UI 구현을 진행
✔️ "세이브" → 현재 위치 데이터를 저장
✔️ "로드" → 저장된 데이터를 불러와 복구
✔️ "세이브 오브젝트 설정" → 데이터를 저장할 오브젝트를 선택

📌 DrawSaveObjectSettings() 코드

	/// <summary>
    /// 위치 저장 대상 오브젝트를 선택하고 데이터를 저장/불러오는 UI
    /// </summary>
    private void DrawSaveObjectSettings()
    {
        GUILayout.Label("게임 세이브 오브젝트 설정", EditorStyles.boldLabel);
        saveTargetObject = (GameObject)EditorGUILayout.ObjectField("세이브 오브젝트", saveTargetObject, typeof(GameObject), true);
            
        GUILayout.BeginHorizontal();
        if (GUILayout.Button("세이브")) SavePositionData();
        if (GUILayout.Button("로드")) LoadPositionData();
        if (GUILayout.Button("세이브 오브젝트 설정")) SetStorageTarget();
        GUILayout.EndHorizontal();
    }
  • 세이브&로드 기능

✅ 3) 위치 데이터 저장 & 불러오기 기능
UI 버튼을 눌렀을 때 실행되는 저장 및 불러오기 기능을 구현

📌 기능 코드

	/// <summary>
    /// 위치 데이터를 저장
    /// </summary>
    private void SavePositionData()
    {
        foreach (var obj in objects) positionStorage.SavePosition(obj, positions[obj].A, positions[obj].B, positionSwitch[obj]);
        EditorUtility.SetDirty(positionStorage);
    }
    
    /// <summary>
    /// 저장된 위치 데이터를 불러옴
    /// </summary>
    private void LoadPositionData()
    {
        objects.Clear();
        foreach (var entry in positionStorage.objectPositions)
        {
            if (entry.gameObject == null) continue;
            objects.Add(entry.gameObject);
            positions[entry.gameObject] = (entry.positionA, entry.positionB);
            positionSwitch[entry.gameObject] = entry.isAtA;
        }
    }

3. 최종 코드 & 다운로드


전체 코드 및 패키지는 GitHub에서 다운로드할 수 있습니다.
🔗 GitHub Repository : https://github.com/ArgonautJH/ObjectSwitchTool

📌 설치 방법:

1. Package Manager로 설치

  1. Unity에서 Window > Package Manager를 엽니다.
  2. 왼쪽 상단의 + 버튼을 클릭하고 "Add package from git URL..." 선택합니다.
  3. 아래 URL을 입력한 후 Add 버튼을 클릭하세요.
https://github.com/ArgonautJH/ObjectSwitchTool.git
  1. 설치가 완료되면 Tools > Object Switch Tool을 실행하여 사용할 수 있습니다.

2. .unitypackage로 설치

  1. GitHub Releases 페이지에서 .unitypackage 파일을 다운로드하세요.
  2. Unity에서 Assets > Import Package > Custom Package...를 선택합니다.
  3. 다운로드한 .unitypackage 파일을 선택하고 Import 버튼을 클릭합니다.

4. 향후 개선할 수 있는 부분


현재 구현된 기능은 에디터에서 오브젝트의 위치를 A/B로 전환하는 기능을 제공하지만,
몇 가지 추가하면 더욱 유용하게 활용할 수 있을 것 같습니다.

1) Undo 기능 추가

  • Undo.RecordObject()를 활용하여 위치 변경 시 Ctrl + Z로 되돌릴 수 있도록 개선

2) 오브젝트 그룹 지정 기능

  • 특정 오브젝트들을 그룹으로 묶어서 한 번에 전환할 수 있도록 기능 추가
profile
성장하는 개발자가 되기 위한 기록 일지

0개의 댓글