Editor Customize
유니티에서 에디터를 커스터마이징 하는 방법이다. 에디터 커스터마이징은 개발자로 하여금 유니티 에디터 자체에 특정 기능을 추가하는 방법이다. 예를 들면 인스펙터의 스크립트에 어떤 버튼을 누르면 자동으로 에셋들을 로드해준다거나, 자동으로 맵을 생성해준다거나 등등 잘 활용한다면 개발하는데 굉장한 편의성을 갖출 수 있다. (이전에 근무하던 팀에서는 따로 에디터만 작업하는 개발자들도 있었다.)
유니티 에디터 커스터마이징을 하기에 앞서 몇가지 알아두어야 하는 개념들이 있다. 먼저 유니티의 에디터 코드가 동작하는 레벨에 대해 알아두어야 한다. 우리가 일반적으로 이용하는 C# 스크립트들은 모든 레벨에서 이용된다. 즉 에디터에서도 동작하고, 빌드된 디바이스에서도 동작한다. 하지만 에디터 코드는 에디터에서만 동작하고 빌드된 디바이스에는 아예 코드 바이너리가 포함되지 않도록 되어있다. 이는 에디터 관련한 코드들은 실제 게임에는 필요없기 때문에 당연한 부분일 것이고 유니티에서는 따로 Editor라는 폴더를 인식하여 해당 폴더 내부 코드들은 빌드 시 포함되지 않도록 한다. 이 큰 그림을 이해했다면 왜 에디터 관련 코드들을 Editor 폴더에 집어넣어야 하는지 이해가 될 것이다.
다음으로 유니티 에디터 커스터마이징에서는 여러가지 방법들을 제공하지만, 그 중 가장 효과적이고 자주 사용되는 방법이 특정 스크립트를 타게팅하는 방법이다. 이 방법은 어떤 정의된 스크립트를 타게팅하는 1:1 대응 에디터 스크립트를 따로 정의하고, 그 에디터 스크립트에서 타겟 스크립트에 대한 커스터마이징 기능들을 제공해주는 방식이다.
기본 예제
예를 들어 Stage라는 스크립트가 있다고 가정해보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
namespace EnumTypes
{
public enum StageTypes
{
CommonBattle,
EliteBattle
}
}
namespace ScriptableObjects
{
[Serializable]
[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/Stage", order = 1)]
public class Stage : ScriptableObject
{
[SerializeField]
private EnumTypes.StageTypes _stageType;
[SerializeField]
private Sprite _stageIcon;
[SerializeField]
private ScriptableObjects.WaveGroup _waveGroup;
public EnumTypes.StageTypes StageType
{
get { return _stageType; }
set { _stageType = value; }
}
public Sprite StageIcon
{
get { return _stageIcon; }
}
public ScriptableObjects.WaveGroup WaveGroup
{
get { return _waveGroup; }
}
}
}
이 스크립트가 붙은 오브젝트를 선택한다면 우리는 인스펙터 창에서 이 스크립트의 프로퍼티들을 수정할 수 있다. 여기에서 만족하지 않고 뭔가 추가적인 작업을 하고싶은 경우 이 Stage라는 스크립트에 대한 에디터 스크립트 StageEditor를 정의한다. (일반적으로 에디터 스크립트는 타겟 스크립트의 이름+Editor 라는 네이밍을 사용한다.)
StageEditor는 Editor 클래스를 상속받고, Stage를 타게팅하도록 한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
// Stage를 타게팅
[CustomEditor(typeof(ScriptableObjects.Stage))]
[CanEditMultipleObjects]
public class StageEditor : Editor
{
}
StageEditor는 Stage 스크립트만 바라보며, Stage 스크립트에 대한 에디터 기능을 확장시켜줄 수 있다.
먼저 다음과 같은 방법으로 타게팅 스크립트를 참조해올 수 있다. 참조해온 후에는 대상의 데이터를 사용 및 변경할 수 있다.
private void OnEnable()
{
ScriptableObjects.Stage stage = target as ScriptableObjects.Stage;
Debug.Log($"{stage.StageType}");
stage.StageType = StageTypes.CommonBattle;
// target의 변경사항 저장
serializedObject.ApplyModifiedProperties();
}
여기서 궁금할 수 있는 부분은 OnEnable() 함수는 언제 호출되는지이다. 로그를 찍어보면 알겠지만 대상 스크립트가 붙어있는 오브젝트를 선택하여 인스펙터에 스크립트 관련 프로퍼티가 나타날때 OnEnable() 함수가 호출된다.
프로퍼티 예제
실제로 에디터 커스터마이징을 해보면 좀 더 다양한 상황에 대한 처리가 필요함을 느낄 수 있다. 이 예제에서는 Stage의 StageType을 변경했을 때, 해당 StageType에 대응되는 Sprite를 자동으로 선택해주는 예제를 작성해보도록 한다.
먼저 에디터에서 기본적으로 프로퍼티들의 ui를 만들어주는 부분을 알아야한다. 이 작업은 OnInspectorGUI() 라는 함수에서 처리된다. OnInspectorGUI() 함수는 인스펙터의 GUI에 대해 일어날 수 있는 모든 이벤트에 대해 호출된다. 인스펙터 ui 위에서 마우스를 움직이는것부터 버튼을 누른다거나, 값을 변경한다거나 하는 모든 이벤트에 반응한다. 이를 활용하여 다음과 같이 구현하면 기본적으로 제공되는 인스펙터 화면을 볼 수 있다.
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
}
함수를 비워둔다면 어떻게 될까? 아무것도 출력되지 않을 것이다.
public override void OnInspectorGUI()
{
}
우리가 원하는 형태로 에디터 프로퍼티 ui를 구성하려면 먼저 base를 이용하지 않고 기본 세팅이 되도록 작업해야할 것이다. 즉 유니티가 기본적으로 해주는 작업을 직접 구현해야한다. 이를 위해 유니티에서 SerializedProperty라는 대상 클래스의 특정 프로퍼티를 직렬화하는 클래스를 제공해준다. 다음과 같이 작업하면 된다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(ScriptableObjects.Stage))]
[CanEditMultipleObjects]
public class StageEditor : Editor
{
// Stage의 프로퍼티들에 대한 직렬화 객체들 정의
private SerializedProperty _stageType;
private SerializedProperty _stageIcon;
private SerializedProperty _waveGroup;
private void OnEnable()
{
// Stage의 프로퍼티들을 직렬화 객체로 참조한다.
_stageType = serializedObject.FindProperty("_stageType");
_stageIcon = serializedObject.FindProperty("_stageIcon");
_waveGroup = serializedObject.FindProperty("_waveGroup");
}
// 인스펙터 GUI에서의 모든 이벤트에 대해
public override void OnInspectorGUI()
{
// 각 프로퍼티들에 대한 기본 ui 필드들을 생성한다.
EditorGUILayout.PropertyField(_stageType);
EditorGUILayout.PropertyField(_stageIcon);
EditorGUILayout.PropertyField(_waveGroup);
}
}
여기까지 작업이 되었다면, 유니티가 기본적으로 생성해주는 인스펙터 GUI를 직접 구현했다고 볼 수 있다. 이제 StageType이 변경될 때, 대응되는 Sprite를 자동으로 채워주는 코드를 추가해보자. 이 작업을 위해 StageType이 변경되는 시점을 알아야 하는데, 유니티에서는 EditorGUI.BeginChangeCheck(), EditorGUI.EndChangeCheck() 이라는 함수들을 통해 특정 구간에서의 변경에 대한 이벤트들을 수집할 수 있도록 지원해준다. (ChangeIcon() 함수에 대한 부분은 각자 알아서 만들도록 하자.)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(ScriptableObjects.Stage))]
[CanEditMultipleObjects]
public class StageEditor : Editor
{
private SerializedProperty _stageType;
private SerializedProperty _stageIcon;
private SerializedProperty _waveGroup;
private void OnEnable()
{
_stageType = serializedObject.FindProperty("_stageType");
_stageIcon = serializedObject.FindProperty("_stageIcon");
_waveGroup = serializedObject.FindProperty("_waveGroup");
}
public override void OnInspectorGUI()
{
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(_stageType);
// _stageType 프로퍼티 필드에 변경사항이 있다면
if (EditorGUI.EndChangeCheck())
{
// StageType에 맞는 Sprite이미지로 교체해준다.
ChangeStageIcon();
}
EditorGUILayout.PropertyField(_stageIcon);
EditorGUILayout.PropertyField(_waveGroup);
Debug.LogError("OnInspectorGUI");
}
private void ChangeStageIcon()
{
string path = "ScriptableObjects/GamePrefabs";
ScriptableObjects.GamePrefabs gamePrefabs = Resources.Load(path) as ScriptableObjects.GamePrefabs;
EnumTypes.StageTypes stageType = (EnumTypes.StageTypes)_stageType.intValue;
_stageIcon.objectReferenceValue = gamePrefabs.spriteGroup.GetStageSprite(stageType);
serializedObject.ApplyModifiedProperties();
}
}
버튼 예제
유니티를 사용하다 보면 에디터 상에서 수동으로 특정 스크립트의 데이터를 생성시켜주는 기능을 구현해야하는 경우가 종종 생긴다. 이 예제에서는 그런 상황을 위한 도구를 구현해본다. 다음처럼 유니티 에디터 인스펙터 상에서 대상 스크립트의 특정 버튼을 생성하고, 해당 버튼을 누르면 자동으로 데이터를 생성해주는 예제를 구현해볼 것이다.
DataTable.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
[Serializable]
public struct WeaponData
{
public enum Types
{
Bow, Spear, Sword, Staff, TwoHandSword
}
[SerializeField]
private string name;
[SerializeField]
private Types type;
public WeaponData(string name, Types type)
{
this.name = name;
this.type = type;
}
}
public class DataTable : MonoBehaviour
{
[SerializeField]
public List<WeaponData> weaponDataList;
}
DataTableEditor.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(DataTable))]
[CanEditMultipleObjects]
public class DataTableEditor : Editor
{
private SerializedProperty weaponDataList;
private SerializedProperty armorDataList;
private DataTable dataTable;
void OnEnable()
{
weaponDataList = serializedObject.FindProperty("weaponDataList");
armorDataList = serializedObject.FindProperty("armorDataList");
dataTable = (target as DataTable);
}
public override void OnInspectorGUI()
{
serializedObject.Update();
GUIStyle style = EditorStyles.helpBox;
GUILayout.BeginVertical(style);
EditorGUILayout.PropertyField(weaponDataList, true);
if (GUILayout.Button("Generate WeaponData"))
{
dataTable.weaponDataList.Add(new WeaponData("a", WeaponData.Types.Bow));
dataTable.weaponDataList.Add(new WeaponData("b", WeaponData.Types.Spear));
dataTable.weaponDataList.Add(new WeaponData("c", WeaponData.Types.TwoHandSword));
}
GUILayout.EndVertical();
GUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.PropertyField(armorDataList, true);
if (GUILayout.Button("Generate ArmorData"))
{
dataTable.armorDataList.Add(new ArmorData("aa", ArmorData.Types.Armor));
dataTable.armorDataList.Add(new ArmorData("bb", ArmorData.Types.Shield));
dataTable.armorDataList.Add(new ArmorData("cc", ArmorData.Types.Shield));
}
GUILayout.EndVertical();
serializedObject.ApplyModifiedProperties();
}
}
결과 화면
'게임 엔진 > Unity' 카테고리의 다른 글
[Unity] Hp Bar 만들기 (0) | 2020.07.13 |
---|---|
[Unity] 글로벌 이벤트 관리 방법 (1) | 2020.07.06 |
[Unity] 전략게임 카메라 제작 방법 (0) | 2020.07.06 |
[Unity] 동적타임에 NavMesh 생성하기 (0) | 2020.07.02 |
[Unity] 길찾기 시스템을 통한 이동과 애니메이션 컨트롤러 (3) | 2020.06.29 |