원하는 위치에 오브젝트들을 배치하고 싶다.
하지만 씬은 비워놓아야하고, 로딩이 끝난 다음 객체를 생성해야한다.
그래서, 에디터에서 씬에 배치해둔 오브젝트들의 위치를 싹 다 받아와서 Scriptable Object로 받아와서 저장하게 만들었다.
public class ResourceObjectSpawnData : ScriptableObject
{
[SerializeField] private List<ResourceObjectParent> _resourceObjectList = new();
private Dictionary<string, ResourceObjectParent> _dict = new();
public Dictionary<string, ResourceObjectParent> Dict => _dict;
[System.Serializable]
public struct DataTuple
{
public ResourceObjectParent _object;
public Vector3 spawnPosition;
public readonly GameObject Prefab => _object.gameObject;
}
[SerializeField] private List<DataTuple> _spawnList;
public List<DataTuple> SpawnList => _spawnList;
public void Initialize()
{
foreach (var item in _resourceObjectList)
_dict.TryAdd(item.name, item);
}
}
인스펙터에서 ResourceObjectList에 등록하면, 초기화 시 딕셔너리로 바꾸도록 했다.
이유는, DataTuple에 저장되는 오브젝트의 참조가 씬에 있는 오브젝트면 안되기 때문이다.
DataTuple은 프로젝트 파일 안에 있는 프리팹 에셋을 가리켜야 하기 떄문에, 씬에 올라가 있는 객체를 저장할 때 자신과 같은 프리팹 객체를 딕셔너리에서 찾아서 SpawnList에 저장해야한다.
유니티에서는 Editor 폴더에 Editor 스크립트를 만들 수 있다.
이를 이용해서 인스펙터를 커스터마이징하거나, 어떤 창을 띄우거나 할 수 있다.
보통, 목표가 되는 클래스 이름 뒤에 Editor 접미사를 붙여서 명명한다.
[CustomEditor(typeof(ResourceObjectSpawnData))]
public class ResourceObjectSpawnDataEditor : Editor
{
...
}
CustomEditor(Type)
어트리뷰트를 붙여주면, 해당 컴포넌트의 인스펙터를 커스터마이징 할 수 있다.
GUILayout.Button()
을 이용하면 인스펙터에 사용자 지정 버튼을 만들 수 있다.
bool을 반환하는데, 사용자가 버튼을 누르면 true를 반환한다.
EditorUtility.DisplayDialog()
를 이용하면 다이얼로그 창을 띄울 수 있다.
역시 bool을 반환하는데, 사용자가 OK버튼을 누르면 true를 반환한다.
이 둘을 이용하면, 버튼을 누르면 확인 창이 뜨고, 확인 창에서 또 OK를 눌러 이중확인 안전장치를 만들 수 있다.
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (GUILayout.Button("Save") && EditorUtility.DisplayDialog("ResourceObjectSpawnData", _saveMessage, "OK", "Cancel"))
{
SaveObjects();
}
if (GUILayout.Button("Load") && EditorUtility.DisplayDialog("ResourceObjectSpawnData", _clearMessage, "OK", "Cancel"))
{
LoadObjects();
}
if (GUILayout.Button("Clear Hierarchy") && EditorUtility.DisplayDialog("ResourceObjectSpawnData", _clearMessage, "OK", "Cancel"))
{
ClearObjects();
}
}
OnInpectorGUI()
를 오버라이딩하면 된다.
base를 지우면, 기존에 보여지던 필드나 프로퍼티들도 싹 다 안보여지게 되니까, 일일히 다시 직접 작성할 게 아니라면 base를 호출해주는 것도 잊지말자.
private void SaveObjects()
{
var data = target as ResourceObjectSpawnData;
data.Initialize();
List<ResourceObjectSpawnData.DataTuple> list = data.SpawnList;
list.Clear();
foreach (var e in FindAllResourceObjects())
{
string key = e.name.Split('(')[0];
key = key.TrimEnd(' ');
list.Add(new() { _object = data.Dict[key], spawnPosition = e.transform.position });
}
}
Editor 클래스에는 target이라는 프로퍼티가 있다.
이 프로퍼티를 형변환하여 커스터마이징할 객체에 접근할 수 있다.
우선, 초기화를 해서 딕셔너리를 만들어주고, 씬에 있는 모든 ResourceObject들을 가져와서 spawnList에 저장했다.
private void LoadObjects()
{
ClearObjects();
var data = target as ResourceObjectSpawnData;
var root = GetRoot();
foreach (var e in data.SpawnList)
{
var obj = Instantiate(e.Prefab, e.spawnPosition, Quaternion.identity);
obj.name = e.Prefab.name;
obj.transform.parent = root;
}
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
}
로드 버튼을 누르면, 먼저 기존에 씬에 있던 오브젝트들을 지워지게 했다. 안그러면 계속 오브젝트가 불어날 테니까...
그 다음은 뭐 별거없다... spawnList를 순회하며 오브젝트를 Instantiate한다.
작업을 하고 보니, 이렇게 해서 씬에 오브젝트를 생성하거나 삭제해도 씬이 저장되지 않는다.
버튼을 누르고 다른 씬으로 갖다 오면, 버튼을 누르기 전 상태 그대로다.
EditorSceneManager.MarkSceneDirty()
를 이용하면, 그 씬에 Dirty를 남길 수 있다. ( 저장 안됐을 때 *표시하는 그것 )
private void ClearObjects()
{
foreach (var e in FindAllResourceObjects())
DestroyImmediate(e.gameObject, false);
DestroyImmediate(GetRoot().gameObject, false);
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
}
클리어도 간단하다. 근데 Destroy()
메서드는 런타임 환경이 아니면 오브젝트의 삭제가 안되더라.
그래서 DestroyImmediate()
를 썼는데, 이 녀석은 씬에 올라간 녀석 뿐만 아니라 프로젝트 폴더 내부에 있는 에셋 파일도 삭제할 수 있는 무서운 녀석이다.
테스트하려고 버튼 누르려니까 내가 실수해서 에셋 삭제될까봐 정말 무서웠다.