간단한 스테이지 에디터 만들기 (Unity)

장민제·2025년 5월 8일

내일배움캠프

목록 보기
22/41

💪 팀 프로젝트 시작

팀프로젝트가 시작되어서 우리팀은 '궁수의 전설' 같은 게임을 만들자고 정하고,
나는 스테이지 관련된 기능 개발을 맡게 되었다.
그래서 예전 대학교 때 멘토링을 하던 시절 멘토님이

"현업에서는 디자이너들 작업하기 편하게 에디터를 개발자들이 만들어 준다."

라고 이야기하셨던게 기억이 나서 한 번 시도해보려고 한다!


💻 Unity Custom Editor ?

특정 컴포넌트나 ScriptableObject의 인스펙터를 직접 커스터마이징할 수 있는 기능

예시

[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : Editor
{
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector(); // 기본 UI도 같이 출력
        if (GUILayout.Button("버튼 이름"))
        {
            // 버튼 클릭 시 실행할 기능
        }
    }
}

DrawDefaultInspector() or base.OnInspectorGuI() 로 기본적인 인스펙터의 UI를 그릴 수 있지만,
변수들을 직접 표시하고 조작하고 싶다면

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : Editor
{
	// 변수들에 대한 직렬화 객체들 정의
    SerializedProperty health;
    SerializedProperty speed;

    void OnEnable()
    {
        // 스크립트 내의 변수들을 연결
        health = serializedObject.FindProperty("health");
        speed = serializedObject.FindProperty("speed");
    }

    public override void OnInspectorGUI()
    {
        // 최신 값과 동기화
        serializedObject.Update();

        // 변수들을 직접 인스펙터에 표시
        EditorGUILayout.PropertyField(health);
        EditorGUILayout.PropertyField(speed);

        // 기타 커스텀 버튼
        if (GUILayout.Button("Print Info"))
        {
            Debug.Log($"Health: {health.intValue}, Speed: {speed.floatValue}");
        }

        // 변경 사항 반영
        serializedObject.ApplyModifiedProperties();
    }
}

테스트 컴포넌트 스크립트

public class MyComponent : MonoBehaviour
{
    public int health = 100;
    public float speed = 5f;
}

SerializedObjectSerializedProperty를 사용하여 인스펙터에서 스크립트의 변수를 수동으로 제어한다.

SerializedObject : 대상 객체의 직렬화된 데이터를 래핑한 객체
SerializedProperty: SerializedObject 내부에 있는 하나의 변수를 표현하는 객체

⚠️ 커스텀 에디터 스크립트는 반드시 Editor 폴더 안에 두어야 한다. (Assets/Editor/)
UnityEditor 네임스페이스는 빌드에 포함되지 않기 때문.


✅ 간단한 스테이지 프리셋 에디터 만들기

내가 생각했던 스테이지는 우선 기본적인 틀 위에 몬스터나 장애물들이 생성될 위치를 지정만 해두고 스테이지가 시작될 때 해당 위치에 랜덤한 몬스터들과 장애물들을 생성하는 방법을 생각하였다.

그러니까, 우선 Stage의 기본이 될 Tilemap을 간단하게 그려준다.

그리고 이제 이 위에 생성될 몬스터의 위치, 장애물의 위치, 아이템의 위치 값들을 저장할 ScriptableObject를 만들어준다.

🔹 StagePreset.cs

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "StagePreset", menuName = "Stage/StagePreset")]
public class StagePreset : ScriptableObject
{
    public int stageID;	// 프리셋의 ID
    public List<Vector2> monsterPoints = new();	// 몬스터가 생성될 위치
    public List<Vector2> obstaclePoints = new(); // 장애물이 생성될 위치
    public List<Vector2> itemPoints = new(); // 아이템이 생성될 위치
    
}

그리고 이제 각 위치들을 눈으로 보기쉽게 표시해주고 Editor 스크립트가 참조할 스크립트를 작성해준다.

🔹 StagePresetTool.cs

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace Jang
{
    public class StagePresetTool : MonoBehaviour
    {
        public int stageID = 101;

        [SerializeField] Transform monsterPoint;
        [SerializeField] Transform obstaclePoint;
        [SerializeField] Transform itemPoint;

        public List<Transform> monsterPoints { private set; get; }
        public List<Transform> obstaclePoints { private set; get; }
        public List<Transform> itemPoints { private set; get; }

        // 각 위치들 Gizmo 표시
        void OnDrawGizmos()
        {
            monsterPoints = monsterPoint.GetComponentsInChildren<Transform>().Where(t => t != monsterPoint).ToList();
            obstaclePoints = obstaclePoint.GetComponentsInChildren<Transform>().Where(t => t != obstaclePoint).ToList();
            itemPoints = itemPoint.GetComponentsInChildren<Transform>().Where(t => t != itemPoint).ToList();

            Gizmos.color = Color.red;
            foreach (var point in monsterPoints)
                if (point != null) Gizmos.DrawCube(point.position, Vector3.one);

            Gizmos.color = Color.blue;
            foreach (var point in obstaclePoints)
                if (point != null) Gizmos.DrawCube(point.position, Vector3.one);
            Gizmos.color = Color.green;
            foreach (var point in itemPoints)
                if (point != null) Gizmos.DrawCube(point.position, Vector3.one);
        }
    }
}

오브젝트들이 생성될 위치를 담고 있을 상위 오브젝트 monsterPoint, obstaclePoint, itemPoint 를 만들어 두고 그 자식들을 담아둘 List를 생성

OnDrawGizmos() 메서드를 활용해 Scene View 에서 오브젝트들이 생성될 위치를 표시해준다.

그리고 이제 위 스크립트에 붙여줄 Editor 스크립트 작성

🔹 StagePresetToolEditor.cs

using UnityEditor;
using UnityEngine;

namespace Jang
{
    [CustomEditor(typeof(StagePresetTool))]
    public class StagePresetToolEditor : Editor
    {
        const string PATH = "Assets/Resources/StagePresets/";
        public override void OnInspectorGUI()
        {
            DrawDefaultInspector(); // 기본 인스펙터 그리기

            StagePresetTool editor = (StagePresetTool)target;

            if (GUILayout.Button("Save StagePreset")) // Save StagePreset 버튼 생성
            {
                SavePreset(editor); // 버튼을 누르면 SavePreset 메서드 실행
            }
        }

        // 프리셋 저장
        private void SavePreset(StagePresetTool editor)
        {
            // ScriptableObject 생성
            StagePreset preset = CreateInstance<StagePreset>();

            // 스테이지 아이디 저장
            preset.stageID = editor.stageID;

            // 각 몬스터, 장애물, 아이템 위치 저장
            foreach (var point in editor.monsterPoints)
                if (point != null) preset.monsterPoints.Add(point.position);

            foreach (var point in editor.obstaclePoints)
                if (point != null) preset.obstaclePoints.Add(point.position);

            foreach (var point in editor.itemPoints)
                if (point != null) preset.itemPoints.Add(point.position);

            string path = $"{PATH}Stage_{editor.stageID}.asset";

            // 같은 ID의 프리셋이 존재한다면 생성 X
            if (AssetDatabase.LoadAssetAtPath<StagePreset>(path) != null)
            {
                Debug.LogWarning("이미 존재하는 ID 입니다.");
                return;
            }

            // 프리셋 생성
            AssetDatabase.CreateAsset(preset, path);
            AssetDatabase.SaveAssets();

            Debug.Log($"스테이지 프리셋이 생성되었습니다. 저장 경로: {path}");
        }
    }
}

간단하게 각 오브젝트가 생성될 위치를 저장할 버튼만 하나 생성해주고,
SavePreset() 에서 위치 데이터들을 ScriptableObjectStagePreset로 생성해준다.

몬스터 -> 빨간색
장애물 -> 파란색

SceneView 에서 위치가 잘 보이고,

데이터를 저장한 ScriptableObject도 잘 생성된 것을 확인할 수 있다.


✅ 데이터를 활용하여 오브젝트 생성하기

이제 간단하게 스테이지 프리셋을 만들어 저장까지 했으니 그것을 활용하여 오브젝트들을 해당 위치에 생성해줄 차례이다.

먼저 생성된 프리셋들을 관리해줄 스크립트를 작성해준다.

🔹 StagePresetManager.cs

using UnityEngine;

namespace Jang
{
    public class StagePresetManager : MonoBehaviour
    {
        [SerializeField] StagePreset[] presets; // 스테이지 프리셋들

        void Awake()
        {   
            // Resources/StagePresets/ 경로에 존재하는 모든 StagePreset 파일 불러오기
            presets = Resources.LoadAll<StagePreset>("StagePresets");
        }

        // 랜덤한 프리셋 반환
        public StagePreset GetRandomPreset()
        {
            int idx = Random.Range(0, presets.Length);

            return presets[idx];
        }
    }
}

Resources.LoadAll()로 Assets/StagePresets/ 경로에 있는 모든 프리셋을 불러와 저장
랜덤한 프리셋을 반환해줄 GetRandomPreset() 메서드를 만들고,
저 랜덤한 프리셋을 받아 스테이지에 오브젝트를 생성해줄 스크립트 작성한다.

🔹 StageManager.cs

using UnityEngine;

namespace Jang
{
    public class StageManager : MonoBehaviour
    {
        [SerializeField] StagePreset currentPreset; // 현재 스테이지 프리셋
        private StagePresetManager stagePresetManager; // 프리셋 랜덤으로 가져오기 위한 Manager

        [Header("Object Prefabs")]
        [SerializeField] GameObject monsterPrefab; 
        [SerializeField] GameObject obstaclePrefab;
        [SerializeField] GameObject itemPrefab;


        public void Init()
        {
            if (stagePresetManager == null)
                stagePresetManager = GetComponentInChildren<StagePresetManager>();

            currentPreset = stagePresetManager.GetRandomPreset();
            InitStage(currentPreset);
        }

        
        [ContextMenu("TestInitStage")]
        public void TestInitStage()
        {
            InitStage(currentPreset);
        }
        
        // 스테이지 설정
        void InitStage(StagePreset preset)
        {
            if (preset.monsterPoints != null)
            {
                foreach (var point in preset.monsterPoints)
                {
                    // 몬스터 소환
                    GameObject obj = Instantiate(monsterPrefab);
                    obj.transform.position = point;
                    obj.transform.rotation = Quaternion.identity;
                }
            }

            if (preset.obstaclePoints != null)
            {
                foreach (var point in preset.obstaclePoints)
                {
                    // 장애물 소환
                    GameObject obj = Instantiate(obstaclePrefab);
                    obj.transform.position = point;
                    obj.transform.rotation = Quaternion.identity;
                }
            }

            if (preset.itemPoints != null)
            {
                foreach (var point in preset.itemPoints)
                {
                    // 아이템 소환
                    GameObject obj = Instantiate(itemPrefab);
                    obj.transform.position = point;
                    obj.transform.rotation = Quaternion.identity;
                }
            }
        }
    }
}

Init() 메서드에서 초기화와 랜덤한 프리셋을 받아와서 오브젝트 배치
InitStage() 메서드 에서 오브젝트 실제 배치

💻 테스트

해당 프리셋에 맞게 잘 생성되는 것을 볼 수 있다 😁

나중에는 Json 데이터로 저장하는 방식으로 변경할 예정이다 ~~

profile
Unity, C#

0개의 댓글