내일배움캠프 Unity 64일차 TIL - 팀 9와 4분의 3 - 개발일지

Wooooo·2024년 1월 25일
0

내일배움캠프Unity

목록 보기
66/94

[오늘의 키워드]

원하는 위치에 오브젝트들을 배치하고 싶다.
하지만 씬은 비워놓아야하고, 로딩이 끝난 다음 객체를 생성해야한다.

그래서, 에디터에서 씬에 배치해둔 오브젝트들의 위치를 싹 다 받아와서 Scriptable Object로 받아와서 저장하게 만들었다.


[설계]

필요한 기능

  • 세이브 버튼을 누르면 현재 씬에 있는 자원 오브젝트들이 모두 SpawnList로 들어온다.
  • 로드 버튼을 누르면 SpawnList에 있는 오브젝트들이 씬에 Instantiate 된다.
  • Clear Hierarchy 버튼을 누르면 씬에 있는 오브젝트들이 사라진다.

[Data 클래스]

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 스크립트를 만들 수 있다.
이를 이용해서 인스펙터를 커스터마이징하거나, 어떤 창을 띄우거나 할 수 있다.

보통, 목표가 되는 클래스 이름 뒤에 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를 호출해주는 것도 잊지말자.

SaveButton 만들기

    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에 저장했다.

LoadButton 만들기

    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를 남길 수 있다. ( 저장 안됐을 때 *표시하는 그것 )

ClearButton 만들기

    private void ClearObjects()
    {
        foreach (var e in FindAllResourceObjects())
            DestroyImmediate(e.gameObject, false);
        DestroyImmediate(GetRoot().gameObject, false);
        EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
    }

클리어도 간단하다. 근데 Destroy() 메서드는 런타임 환경이 아니면 오브젝트의 삭제가 안되더라.

그래서 DestroyImmediate()를 썼는데, 이 녀석은 씬에 올라간 녀석 뿐만 아니라 프로젝트 폴더 내부에 있는 에셋 파일도 삭제할 수 있는 무서운 녀석이다.
테스트하려고 버튼 누르려니까 내가 실수해서 에셋 삭제될까봐 정말 무서웠다.


[구현 모습]

profile
game developer

0개의 댓글