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를 그려서 보여주도록 해볼까 합니다.
'개인프로젝트 일지' 카테고리의 다른 글
유니티로 "젤다의 전설: 꿈꾸는 섬" 모작 7일차 (1) | 2022.09.19 |
---|---|
유니티로 "젤다의 전설: 꿈꾸는 섬" 모작 6일차 (0) | 2022.09.12 |
유니티로 "젤다의 전설: 꿈꾸는 섬" 모작 4일차 (0) | 2022.09.08 |
유니티로 "젤다의 전설: 꿈꾸는 섬" 모작 3일차 (0) | 2022.09.08 |
유니티로 "젤다의 전설: 꿈꾸는 섬" 모작 2일차 (0) | 2022.09.04 |