팀프로젝트가 시작되어서 우리팀은 '궁수의 전설' 같은 게임을 만들자고 정하고,
나는 스테이지 관련된 기능 개발을 맡게 되었다.
그래서 예전 대학교 때 멘토링을 하던 시절 멘토님이
"현업에서는 디자이너들 작업하기 편하게 에디터를 개발자들이 만들어 준다."
라고 이야기하셨던게 기억이 나서 한 번 시도해보려고 한다!
특정 컴포넌트나 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;
}
SerializedObject 와 SerializedProperty를 사용하여 인스펙터에서 스크립트의 변수를 수동으로 제어한다.
SerializedObject : 대상 객체의 직렬화된 데이터를 래핑한 객체
SerializedProperty: SerializedObject 내부에 있는 하나의 변수를 표현하는 객체
⚠️ 커스텀 에디터 스크립트는 반드시 Editor 폴더 안에 두어야 한다.
(Assets/Editor/)
UnityEditor 네임스페이스는 빌드에 포함되지 않기 때문.
내가 생각했던 스테이지는 우선 기본적인 틀 위에 몬스터나 장애물들이 생성될 위치를 지정만 해두고 스테이지가 시작될 때 해당 위치에 랜덤한 몬스터들과 장애물들을 생성하는 방법을 생각하였다.
그러니까, 우선 Stage의 기본이 될 Tilemap을 간단하게 그려준다.

그리고 이제 이 위에 생성될 몬스터의 위치, 장애물의 위치, 아이템의 위치 값들을 저장할 ScriptableObject를 만들어준다.
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 스크립트가 참조할 스크립트를 작성해준다.
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 스크립트 작성
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() 에서 위치 데이터들을 ScriptableObject인 StagePreset로 생성해준다.

몬스터 -> 빨간색
장애물 -> 파란색
SceneView 에서 위치가 잘 보이고,

데이터를 저장한 ScriptableObject도 잘 생성된 것을 확인할 수 있다.
이제 간단하게 스테이지 프리셋을 만들어 저장까지 했으니 그것을 활용하여 오브젝트들을 해당 위치에 생성해줄 차례이다.
먼저 생성된 프리셋들을 관리해줄 스크립트를 작성해준다.
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() 메서드를 만들고,
저 랜덤한 프리셋을 받아 스테이지에 오브젝트를 생성해줄 스크립트 작성한다.
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 데이터로 저장하는 방식으로 변경할 예정이다 ~~