개인프로젝트 일지

유니티로 "젤다의 전설: 꿈꾸는 섬" 모작 5일차

종잇장 2022. 9. 10. 23:33

 

1. SubScene 툴 만들기

저번까지 만들었던 SubSceneSetting을 설정해주기 위해 툴을 개발해볼까 합니다.

 

public class SubSceneTool : EditorWindow
{
    [MenuItem("Tools/SubScene Tool")]
    public static void Open()
    {
        var window = (SubSceneTool)EditorWindow.GetWindow(typeof(SubSceneTool), false, "SubScene Tool", true);
        window.Show();
    }


    public Vector2 SubSceneScoll = Vector2.zero;

    private void OnGUI()
    {
        //TODO - 메인씬 설정 기능
        //TODO - 서브씬 세팅 파일 생성/변경

        SubSceneScoll = EditorGUILayout.BeginScrollView(SubSceneScoll);
        OnGUI_SubScenes();
        EditorGUILayout.EndScrollView();
    }

    private void OnGUI_SubScenes()
    {
        //TODO - 리스트로 보여줌
        //TODO - SubSceneData, 생성, 삭제, 이동
        //TODO - SubSceneData, 씬 등록
        //TODO - SubSceneData, 씬 로드, 언로드, 세이브
        //TODO - SubSceneData, 중심, 거리 계산
    }
}

우선은 간단하게 구조만 잡아주었습니다.

 

상단 메뉴와 툴 창이 열리는 것을 확인했습니다.

이제부터 내부에 내용물을 하나씩 채워나가 보겠습니다.

 


 

2. 메인 씬 설정 기능 추가

 

SelectedSceneAsset = (SceneAsset)EditorGUILayout.ObjectField("Main Scene", SelectedSceneAsset, typeof(SceneAsset), false);

우선은 메인 씬을 설정해주기 위해 SceneAsset을 받아오는 필드를 만들었습니다.

 

이 정도야 어렵지 않게 동작하고 쉽게 만들 수 있습니다.

하지만, 일일이 메인씬을 등록해주기에는 번거롭기에 자동으로 등록하는 기능을 만들어볼까 합니다.

 

private void AutoSelectMainScene()
{
    for (int i = 0; i < UnityEditor.SceneManagement.EditorSceneManager.sceneCount; i ++)
    {
        var sceneData = UnityEditor.SceneManagement.EditorSceneManager.GetSceneAt(i);
        if (sceneData.IsValid() == false) continue;

        SelectedMainScene = AssetDatabase.LoadAssetAtPath<SceneAsset>(sceneData.path);
        break;
    }
}

우선은 현재 열려있는 씬으로 자동으로 등록해주는 함수를 만들어주었습니다.

 

private void OnEnable()
{
    EditorSceneManager.sceneOpened += OnSceneOpened;
}

private void OnDisable()
{
    EditorSceneManager.sceneOpened -= OnSceneOpened;
}

private void OnSceneOpened(Scene scene, OpenSceneMode mode)
{
    if (mode == OpenSceneMode.Single)
        AutoSelectMainScene();
}

씬이 열리는 콜백에 등록하여 이벤트를 받아오도록 작업했습니다.

이렇게 짜두면 에디터에서 씬이 변경되면, AutoSelectMainScene함수가 호출되어 자동으로 등록될 겁니다.

 

 

이렇게 짜두면 씬을 열 때마다 교체가 됩니다.

 


 

3. 서브씬 설정 파일 로드 기능 추가

 

public static SubSceneSetting GenerateSubSceneSetting(string assetName)
{
    SubSceneSetting asset = ScriptableObject.CreateInstance<SubSceneSetting>();

    UnityEditor.AssetDatabase.CreateAsset(asset, $"Assets/Resources/SubSceneSettings/{assetName}.asset");
    UnityEditor.AssetDatabase.SaveAssets();

    return asset;
}
if (SelectedSubSceneSetting == null)
{
    using (new EditorGUI.DisabledScope(SelectedMainScene == null))
    {
        if (GUILayout.Button("Create Setting"))
        {
            var setting = SubSceneSetting.GenerateSubSceneSetting(SelectedMainScene.name);
            SelectedSubSceneSetting = setting;
        }
    }
}
else
{
    using (new EditorGUI.DisabledScope(true))
    {
        EditorGUILayout.ObjectField("Setting", SelectedSubSceneSetting, typeof(SubSceneSetting), false);
    }
}

설정 파일이 없는 경우 "Create Setting" 버튼을 출력하고, 있는 경우 현재 세팅 파일을 보여주도록 작업했습니다.

설정 파일이 있는 경우에 Disable로 출력하는 이유는 툴 사용자가 직접 수정할 필요는 없지만 어떤 파일을 로드 중인지 알 수는 있어야 한다고 생각돼서 Disable 상태로 출력했습니다.

 

 

 


 

4. 서브 씬 설정 기능 추가

 

public static class SceneUtility
{

#if UNITY_EDITOR

    public static string GetEditorBuildSettingsScenePathBySceneName(string sceneName)
    {
        if (string.IsNullOrWhiteSpace(sceneName))
            return string.Empty;

        var lowerSceneName = sceneName.ToLower() + ".unity";
        var buildSettingScenes = UnityEditor.EditorBuildSettings.scenes;
        for (int i = 0; i < buildSettingScenes.Length; i ++)
        {
            var pathSplit = buildSettingScenes[i].path.Replace('\\','/').Split('/');
            if (pathSplit[pathSplit.Length - 1].StartsWith(sceneName) && pathSplit[pathSplit.Length - 1].ToLower() == lowerSceneName)
                return buildSettingScenes[i].path;
        }

        return string.Empty;
    }

    public static Scene GetEditorOpenedSceneBySceneName(string sceneName)
    {
        if (string.IsNullOrWhiteSpace(sceneName))
            return default;

        for (int i = 0; i < UnityEditor.SceneManagement.EditorSceneManager.sceneCount; i ++)
        {
            var sceneData = UnityEditor.SceneManagement.EditorSceneManager.GetSceneAt(i);
            if (sceneData.IsValid() == false) continue;

            if (sceneData.name == sceneName)
                return sceneData;
        }
        return default;
    }

#endif
}

우선은 SceneUtility라는 static클래스를 만들어두었습니다.

이유는 앞으로 계속 관련 비슷한 코드들이 사용될 것 같아서 만들었습니다.

 

var data = SelectedSubSceneSetting.SubSceneDatList[i];
var sceneData = SceneUtility.GetEditorOpenedSceneBySceneName(data.sceneName);

GUILayout.Label($"[{i}]", GUILayout.ExpandWidth(false));
data.sceneName = EditorGUILayout.TextField(data.sceneName, GUILayout.ExpandWidth(true));
if (sceneData.IsValid() == true && sceneData.isLoaded == true)
{
    if (GUILayout.Button("Close", GUILayout.ExpandWidth(false)))
    {
        UnityEditor.SceneManagement.EditorSceneManager.CloseScene(sceneData, false);
    }
}
else
{
    if (GUILayout.Button("Open", GUILayout.ExpandWidth(false)))
    {
        var path = SceneUtility.GetEditorBuildSettingsScenePathBySceneName(data.sceneName);
        UnityEditor.SceneManagement.EditorSceneManager.OpenScene(path, OpenSceneMode.Additive);
    }
}
if (GUILayout.Button("X", GUILayout.ExpandWidth(false)))
{
    SelectedSubSceneSetting.SubSceneDatList.RemoveAt(i);
    i --;
    continue;
}

간단하게 에디터로 출력하는 코드입니다.

그 와중에 조금 특별한 코드라면 열고 닫는 기능인 Open, Close인데, 마치 서브 씬을 로드한 것 처럼 보기 위해 추가 한 기능입니다.

 

 


 

5. 서브씬 영역 계산

 

저번에 구상했던 방식 그대로 코드를 짜 보기로 했습니다.

 

Scene sceneData = default;
bool isLoadedScene = false;
for (int i = 0; i < UnityEditor.SceneManagement.EditorSceneManager.sceneCount; i ++)
{
    sceneData = UnityEditor.SceneManagement.EditorSceneManager.GetSceneAt(i);
    if (sceneData.name == sceneName)
    {
        isLoadedScene = sceneData.isLoaded;
        break;
    }
}

// 서브 씬 로드
if (isLoadedScene == false)
{
    sceneData = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(SceneUtility.GetEditorBuildSettingsScenePathBySceneName(sceneName), UnityEditor.SceneManagement.OpenSceneMode.Additive);
}

if (sceneData.IsValid() == false)
{
    center = Vector3.zero;
    range = 0f;
    if (isLoadedScene == false)
        UnityEditor.SceneManagement.EditorSceneManager.CloseScene(sceneData, true);
    return;
}

// 서브씬에 있는 모든 Renderer 검색
List<Renderer> rendererList = new List<Renderer>();
var rootObjs = sceneData.GetRootGameObjects();
for (int i = 0; i < rootObjs.Length; i ++)
{
    if (rootObjs[i] == null) continue;

    var renderers = rootObjs[i].GetComponentsInChildren<Renderer>();
    if (renderers == null || renderers.Length == 0) continue;

    rendererList.AddRange(renderers);
}

// Renderer에서 각 Mesh의 Vertex들의 World좌표들을 가져옴.
List<Vector3> pointList = new List<Vector3>();
foreach (var renderer in rendererList)
{
    if (renderer == null) continue;
    if (renderer.enabled == false) continue;
    if (renderer.transform == null) continue;
    // if (meshRenderer.gameObject.isStatic == false) continue;

    Mesh mesh = null;
    if (renderer is MeshRenderer)
    {
        var meshFilter = renderer.GetComponent<MeshFilter>();
        if (meshFilter == null) continue;

        mesh = meshFilter.sharedMesh;
    }
    else if (renderer is SkinnedMeshRenderer skinnedMeshRenderer)
    {
        mesh = skinnedMeshRenderer.sharedMesh;
    }
    
    if (mesh == null) continue;

    var vertices = new List<Vector3>();
    mesh.GetVertices(vertices);

    foreach (var vertex in vertices)
    {
        var point = renderer.transform.TransformPoint(vertex);
        point.y = 0f; //쿼터뷰 게임이라 y를 계산하는 것 자체가 사치.
        pointList.Add(point);
    }
}

//중심 및 거리 게산
center = Vector3.zero;
foreach (var point in pointList)
{
    center += point;
}
center /= pointList.Count;

range = 0f;
foreach (var point in pointList)
{
    if (Vector3.SqrMagnitude(point - center) > range * range)
    {
        range = Vector3.Distance(point, center);
    }
}

// 서브씬 언로드
if (isLoadedScene == false)
    UnityEditor.SceneManagement.EditorSceneManager.CloseScene(sceneData, true);

좀 길고 설명이라곤 주석뿐이지만 어렵지 않은 코드라 쉽게 보실 수 있으실 겁니다.

설명이 필요하다면 언제든지 댓글로 남겨주세요.

 

일단은 값이 세팅되었고 얼추 제가 생각한 값이 나온 것 같습니다.

하지만, 시각적으로 확인할 수 없어서 조금 아쉽네요.

 


 

6. 다음에 할 일

툴을 이용하여 서브 씬을 세팅하는 것까지 완료되었는데, 계산된 값이 실제 얼마큼의 범위인지 감이 오지 않습니다.

이 부분을 Gizmos를 그려서 보여주도록 해볼까 합니다.