Explorer 2D Game Kit 분석 (2)
-
Start 씬 해부하기 (1)
작성 기준 버전 :: 2019.1.4f1
Start 씬
2D 게임 키트에서 제일 처음으로 분석해볼 것은 바로 Start 씬이다. Start 씬은 게임 키트에서 게임이 시작되는 메인 메뉴를 구성하고 있다. 이 씬에서 하이어라키 뷰에 게임 오브젝트들은 어떻게 배치되어 있는지, UI는 어떻게 구성하고 있는지, 그리고 스크립트들은 어떻게 짜여 있는지를 알아볼 것이다.
하이어라키 뷰(Hierarchy view)
하이어라키 뷰에서는 시작 씬에 배치된 게임 오브젝트들을 리스트 형식으로 모두 한 번에 볼 수 있다. 실제로 게임을 만들면서 씬에 게임 오브젝트들을 배치하다보면 제대로 된 정리가 이루어지지 않고 난장판이 되는 경우가 다반사이다. 그에 반해 유니티 콘텐츠 팀에서 제작한 2D 게임 키트의 경우에는 위의 이미지와 같이 깔끔하게 하이어라키 뷰를 정리해두었다.
우선 빈 게임 오브젝트를 이용하여 경계선을 지어서 분류별로 게임 시스템과 관련된 게임 오브젝트, UI 게임 오브젝트, 해당 씬에서만 사용되는 게임 오브젝트 등으로 구분해두었다. 이렇게 해둠으로써 어디에 어떤 게임 오브젝트가 있는지 일일이 찾을 필요없이 카테고리별로 빠르게 찾을 수 있게 된다. 시작 씬 이외에도 게임을 플레이하는 씬인 Zone1~5 씬이 있는데 대부분의 씬에서도 약간의 차이점은 있지만 이러한 분류를 따르고 있다.
개발자 별로 자신에게 적절하거나, 팀원과 상의한 후 팀의 씬 정리 규칙을 세우고 그 규칙에 따라 게임 오브젝트를 배치한다면 한결 보기 좋게 씬을 관리할 수 있다.
System
하이어라키 뷰에서 시스템으로 분류된 게임 오브젝트는 씬 컨트롤러(Scene Controller), 트랜지션 스타트(Transition Start), 백그라운드 뮤직 플레이어(Background Music Player)가 있다. 시스템 쪽으로 분류해둔 게임 오브젝트들은 게임 시스템과 관련된 오브젝트이며, 대부분 모든 씬에서 존재해야되는 오브젝트들이 많다.
씬 컨트롤러 게임 오브젝트(Scene Controller Game Object)
씬 컨트롤러 게임 오브젝트에는 씬 컨트롤러 컴포넌트와 씬 컨트롤러 래퍼 컴포넌트가 부착되어 있다. 우선 씬 컨트롤러 컴포넌트는 다른 씬을 불러오는 씬 로드 기능을 관리하고 있으며 씬 컨트롤러 래퍼 컨트롤러 컴포넌트는 씬 컨트롤러 컴포넌트를 감싸는 역할을 한다(이 감싸는 역할이란 무엇인가는 잠시 후에 설명하도록 하겠다).
씬 컨트롤러 컴포넌트(Scene Controller Component)
public class SceneController : MonoBehaviour
씬 컨트롤러 컴포넌트는 앞서 이야기 했듯이 다른 씬을 불러오는 씬 로드 관리를 담당하는 컴포넌트이다.
protected static SceneController instance;
public static SceneController Instance
{
get
{
if (instance != null)
return instance;
instance = FindObjectOfType<SceneController>();
if (instance != null)
return instance;
Create ();
return instance;
}
}
public static SceneController Create ()
{
GameObject sceneControllerGameObject = new GameObject("SceneController");
instance = sceneControllerGameObject.AddComponent<SceneController>();
return instance;
}
void Awake()
{
if (Instance != this)
{
Destroy(gameObject);
return;
}
DontDestroyOnLoad(gameObject);
m_PlayerInput = FindObjectOfType<PlayerInput>();
if (initialSceneTransitionDestination != null)
{
SetEnteringGameObjectLocation(initialSceneTransitionDestination);
ScreenFader.SetAlpha(1f);
StartCoroutine(ScreenFader.FadeSceneIn());
initialSceneTransitionDestination.OnReachDestination.Invoke();
}
else
{
m_CurrentZoneScene = SceneManager.GetActiveScene();
m_ZoneRestartDestinationTag = SceneTransitionDestination.DestinationTag.A;
}
}
씬 컨트롤러 컴포넌트에서 제일 먼저 살펴볼 부분은 이것이다. 씬을 불러오는 기능은 모든 씬에서 존재하며 다른 씬을 불러올 수 있어야 하기 때문에 싱글톤 패턴과 DontDestoryOnLoad가 적용되어 있다.
protected IEnumerator Transition(string newSceneName, bool resetInputValues, SceneTransitionDestination.DestinationTag destinationTag, TransitionPoint.TransitionType transitionType = TransitionPoint.TransitionType.DifferentZone)
{
m_Transitioning = true;
PersistentDataManager.SaveAllData();
if (m_PlayerInput == null)
m_PlayerInput = FindObjectOfType<PlayerInput>();
m_PlayerInput.ReleaseControl(resetInputValues);
yield return StartCoroutine(ScreenFader.FadeSceneOut(ScreenFader.FadeType.Loading));
PersistentDataManager.ClearPersisters();
yield return SceneManager.LoadSceneAsync(newSceneName);
m_PlayerInput = FindObjectOfType<PlayerInput>();
m_PlayerInput.ReleaseControl(resetInputValues);
PersistentDataManager.LoadAllData();
SceneTransitionDestination entrance = GetDestination(destinationTag);
SetEnteringGameObjectLocation(entrance);
SetupNewScene(transitionType, entrance);
if(entrance != null)
entrance.OnReachDestination.Invoke();
yield return StartCoroutine(ScreenFader.FadeSceneIn());
m_PlayerInput.GainControl();
m_Transitioning = false;
}
씬 컨트롤러 컴포넌트의 가장 중심 기능인 씬 로드 기능은 Transition() 코루틴 함수에 정의되어 있다. 그 외의 함수들은 씬을 다시 시작하는 함수, 목표 지점 태그로 이동할 위치를 가져오는 함수 등 부가적인 기능을 구현하고 있다.
씬 컨트롤러 래퍼 컴포넌트(Scene Controller Wrapper Component)
public class SceneControllerWrapper : MonoBehaviour
{
public void RestartZone (bool resetHealth)
{
SceneController.RestartZone (resetHealth);
}
public void TransitionToScene (TransitionPoint transitionPoint)
{
SceneController.TransitionToScene (transitionPoint);
}
public void RestartZoneWithDelay(float delay)
{
SceneController.RestartZoneWithDelay (delay, false);
}
public void RestartZoneWithDelayAndHealthReset (float delay)
{
SceneController.RestartZoneWithDelay (delay, true);
}
}
씬 컨트롤러 래퍼 컴포넌트는 씬 컨트롤러 컴포넌트를 감싸는 컴포넌트로 호출 방향을 구분하기 위해서 만들어졌다.
호출 방향의 구분의 개념은 위의 이미지와 같다. 위의 이미지에서 볼 수 있듯이 씬에 배치된 게임 오브젝트의 이벤트로 호출될 때는 씬 컨트롤러 래퍼 컴포넌트를 통해서 호출되도록 만들고, 스크립트 내부에서 호출될때는 씬 컨트롤러를 직접 호출하게 설계되어 있다. 굳이 이렇게 나누어서 설계를 할 필요가 있는가 싶겠지만, 이렇게 씬 쪽에서 호출되는 방향과 내부 스크립트에서 호출되는 방향을 구분함으로써 문제가 발생했을 때, 어느 쪽 호출에서 문제가 발생했는지 빠르게 발견할 수 있다는 장점이 있다.
트랜지션 포인트 게임 오브젝트(Transition Point Game Object)
트랜지션 포인트 게임 오브젝트는 박스 콜라이더(Box Collider)와 트랜지션 포인트(Transition Poiont) 컴포넌트를 가진 게임 오브젝트로 실제로는 박스 콜라이더에 접촉한 플레이어를 다른 씬으로 전송하는 역할을 담당하는 게임 오브젝트이다.
단, 현재 씬은 플레이어의 캐릭터가 존재하지 않는 메인 메뉴 씬이기 때문에, 플레이어 캐릭터 오브젝트가 박스 콜라이더에 충돌하는 상황은 존재하지 않을 것이다.
트랜지션 포인트 컴포넌트(Transition Point Component)
[RequireComponent(typeof(Collider2D))]
public class TransitionPoint : MonoBehaviour
트랜지션 포인트 컴포넌트는 해당 컴포넌트가 부착된 게임 오브젝트가 소유한 콜라이더 2D(Collider2D)에 접촉한 플레이어 캐릭터를 다른 지역으로 보내는 역할을 한다. 그렇기 때문에 RequireComnent 어트리뷰트를 이용해서 트랜지션 포인트 컴포넌트가 부착되는 게임 오브젝트에는 반드시 Collider2D 컴포넌트가 부착되어 있어야 함을 정의하고 있다.
public enum TransitionType
{
DifferentZone, DifferentNonGameplayScene, SameScene,
}
public enum TransitionWhen{ ExternalCall, InteractPressed, OnTriggerEnter, }
그리고 트랜지션 포인트 클래스 내부에는 Transition Type과 Transition When이라는 열거형 두 가지가 정의되어 있다. Transition Type은 트랜지션 포인트가 어떤 종류의 씬으로 전환되는지를 의미한다. DifferentZone 타입은 다른 게임 플레이 씬으로 이동하는 것을 의미한다. 스타트 씬에 있는 트랜지션 포인트 역시 DifferentZone으로 설정되어 있는 것을 볼 수 있는데, 메인 메뉴 씬을 기준으로 시작 게임 씬 역시 "다른 게임 플레이 씬"이기 때문에 DifferentZone으로 설정되는 것이 맞다. DifferentNonGameplayScene 타입은 다른 씬이지만, 게임 플레이 씬은 아닌 경우이다. 예를 들자면 게임 플레이 씬에서 다시 메인 메뉴 씬으로 돌아오는 경우이다. SameScene은 같은 씬의 다른 지점으로 이동할 때를 의미한다.
TransitionWhen 열거형은 어느 시점에 전송을 시작할 것인가에 대한 것인데, ExternalCall은 외부에서 호출이 있을 경우를 의미한다. 앞에서 메인 메뉴에서는 플레이어 캐릭터 오브젝트가 없기 때문에 박스 콜라이더에 충돌하는 상황이 벌어지지 않을 것이라고 말했다. 그렇기 때문에 위의 이미지에서 스타트 씬의 트랜지션 포인트 게임 오브젝트에 부착된 트랜지션 포인트 컴포넌트의 Transition When의 값이 ExternalCall으로 설정되어 있는 것을 볼 수 있다. 즉, 콜라이더의 충돌을 이용하지 않는 경우라면 ExternalCall을 사용하는 것이다. InteractPressed는 플레이어 캐릭터가 트랜지션 포인트에 접촉한 상태에서 상호작용 키를 눌렀을 때를 의미한다. OnTriggerEnter 타입은 캐릭터가 트랜지션 포인트의 콜라이더에 접촉하는 순간에 바로 전송을 시작한다.
protected void TransitionInternal ()
{
if (requiresInventoryCheck)
{
if(!inventoryCheck.CheckInventory (inventoryController))
return;
}
if (transitionType == TransitionType.SameScene)
{
GameObjectTeleporter.Teleport (transitioningGameObject, destinationTransform.transform);
}
else
{
SceneController.TransitionToScene (this);
}
}
트랜지션 포인트 컴포넌트에서 다른 씬으로 이동시키는 주요 기능은 TransitionInternal() 함수에서 처리하고 있으며, 여기에서 다른 씬을 로드하는 기능을 담당하는 씬 컨트롤러를 호출한다. 그리고 때에 따라서 트랜지션 타입이 SameSceme이라면 이동시키고자 하는 게임 오브젝트(예를 들어 플레이어)를 같은 씬 내의 목표 위치로 이동시키는 기능 역시 함께 담당한다.
프리팹화
트랜지션 포인트 게임 오브젝트는 파란색 육면체 아이콘을 보면 프리팹화되어 있는 것을 볼 수 있다. 이렇게 함으로써 플레이되고 있는 씬이나 캐릭터의 위치를 이동시키기 위해서 트랜지션 포인트를 일일이 만들 필요없이 트랜지션 포인트 프리팹을 원하는 위치에 가져다 놓고 프로퍼티만 설정하면 언제든 위치 이동 장치를 만들 수 있는 것으로 재활용성을 극대화했음을 알 수 있다.
씬 이름 활용법
이 트랜지션 포인트의 구현법 중에 가장 유용하다고 평할만한 포인트는 바로 씬 이름을 다루는 부분이다. 보통 다른 씬을 호출할 때, 씬 이름을 문자열로 호출하거나 빌드 세팅에 등록된 씬 인덱스를 이용해서 호출하는 경우가 많은데 이런 방법들은 몇 가지 문제점을 내포하고 있다.
우선 씬 인덱스를 사용하는 방법은 등록된 씬의 순서가 변경되면 의도하지 않은 다른 씬이 호출되는 문제가 쉽게 발생한다.
그리고 일반적인 문자열을 사용하는 방식은 사용자의 오타 문제가 있을 수 있고, 특히 코드 난독화를 사용할 때, 상수 타입의 고정된 문자열을 코드에서 직접 사용한다면, 코드 난독화가 상수 문자열로 코드에 들어있는 씬 이름을 암호화해서 원하는 씬을 불러오지 못하는 경우도 발생할 수 있다.
그렇다면 2D 게임 키트에서는 어떻게 씬 이름을 다루어서 이런 문제를 해결했는지 살펴보자.
[SceneName]
public string newSceneName;
트랜지션 포인트 스크립트에는 해당 포인트가 플레이어를 어떤 씬으로 보낼지에 대한 변수인 new Scene Name 변수가 선언되어 있다.
일반적인 공개된 문자열이라면 인스펙터 뷰에서 위의 이미지와 같이 보여야할 것이다.
하지만 인스펙터 뷰에서 트랜지션 포인트 컴포넌트의 New Scene Name 프로퍼티를 보면 일반적인 string과는 다르게 팝업 선택 필드 방식으로 빌드 세팅에 등록된 씬 이름들을 선택할 수 있게 되어 있다. 이런 식으로 빌드 세팅에 등록된 이름을 선택하는 방식이면 등록된 씬의 순서가 변경될 때의 문제나 오타 문제, 씬 이름이 암호화될 문제 역시 발생하지 않는다.
[SceneName]
보통의 공개된 문자열과 다른 부분은 바로 이 SceneName 어트리뷰트가 붙어있다는 점이다. 바로 이 SceneName 어트리뷰트를 사용해서 이 어트리뷰트가 붙은 string은 인스펙터 창에서 인풋 필드(Input Field) 대신에 등록된 씬 이름이 드롭다운 형식으로 표현되게 만든 것이다.
public class SceneNameAttribute : PropertyAttribute
{}
SceneName 어트리뷰트를 [F12] 키로 따라가보면 씬 네임 어트리뷰트는 정의만 되어있고 내부에는 아무 것도 없다.
[CustomPropertyDrawer(typeof(SceneNameAttribute))]
public class SceneNameDrawer : PropertyDrawer
씬 네임 어트리뷰트의 실제 기능을 구현하는 코드는 씬 네임 드로워(Scene Name Drawer)에 있다. 씬 네임 드로워는 씬 네임 어트리뷰트가 부착된 프로퍼티를 인스펙터 뷰에서 어떻게 보여줄 것인가를 정의한다.
int m_SceneIndex = -1;
GUIContent[] m_SceneNames;
readonly string[] k_ScenePathSplitters = { "/", ".unity" };
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
void Setup(SerializedProperty property)
씬 네임 드로워는 3개의 멤버 변수와 2개의 함수로 이루어져있는데, m_SceneIndex는 현재 인스펙터 뷰에서 선택한 인덱스를 m_SceneNames는 팝업 선택 필드에서 보여줄 씬 이름들을 담는다. 그리고 k_ScenePathSplitters는 위의 빌드 세팅 이미지에서 볼 수 있듯이 [2D Game Kit/Scene/씬이름]으로 나타나는 씬 경로를 [ / ]로 쪼개고 씬 이름만 가져오기 위해서 정의된 것이다.
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (EditorBuildSettings.scenes.Length == 0) return;
if (m_SceneIndex == -1)
Setup(property);
int oldIndex = m_SceneIndex;
m_SceneIndex = EditorGUI.Popup(position, label, m_SceneIndex, m_SceneNames);
if (oldIndex != m_SceneIndex)
property.stringValue = m_SceneNames[m_SceneIndex].text;
}
OnGUI() 함수는 에디터의 GUI가 그려질 때 호출되는 함수로, 씬 네임 드로워에서는 씬 네임 어트리뷰트가 부착된 프로퍼티의 GUI를 개발자가 정의한 대로 인스펙터 뷰에 그려주는 역할을 한다.
void Setup(SerializedProperty property)
{
EditorBuildSettingsScene[] scenes = EditorBuildSettings.scenes;
m_SceneNames = new GUIContent[scenes.Length];
for (int i = 0; i < m_SceneNames.Length; i++)
{
string path = scenes[i].path;
string[] splitPath = path.Split(k_ScenePathSplitters, StringSplitOptions.RemoveEmptyEntries);
string sceneName = "";
if (splitPath.Length > 0)
sceneName = splitPath[splitPath.Length - 1];
else
sceneName = "(Deleted Scene)";
m_SceneNames[i] = new GUIContent(sceneName);
}
if (m_SceneNames.Length == 0)
m_SceneNames = new[] { new GUIContent("[No Scenes In Build Settings]") };
if (!string.IsNullOrEmpty(property.stringValue))
{
bool sceneNameFound = false;
for (int i = 0; i < m_SceneNames.Length; i++)
{
if (m_SceneNames[i].text == property.stringValue)
{
m_SceneIndex = i;
sceneNameFound = true;
break;
}
}
if (!sceneNameFound)
m_SceneIndex = 0;
}
else m_SceneIndex = 0;
property.stringValue = m_SceneNames[m_SceneIndex].text;
}
Setup() 함수는 OnGUI() 함수에서 m_SceneIndex가 -1일 때, 즉 프로퍼티가 선택되지 않았을 때, 호출된다. Setup() 함수는 EditorBuildSettings에서 빌드 세팅에 등록된 씬의 목록을 가져와 OnGUI() 함수에서 팝업 선택 필드에서 그릴 수 있는 GUIContent로 가공하는 역할을 한다. 이런 과정을 통해서 빌드 세팅에 등록된 씬의 이름을 가져와서 팝업 선택 필드에 넣어주는 것이다.
이 이야기는 분석을 진행하면서 계속 말하겠지만, 이런 씬 네임 어트리뷰트와 씬 네임 드로워를 정의함으로써 프로그래머는 약간의 귀찮음을 감수하고 추후에 발생할 수 있는 버그와 문제 등을 예방할 수 있고 더 나아가서 에디터에서 주 작업을 진행할 디자이너의 편의와 작업 효율을 향상시킬 수 있게 된다.
백그라운드 뮤직 플레이어 게임 오브젝트(Background Music Player Game Object)
백그라운드 뮤직 플레이어에는 백그라운드 뮤직 플레이어 컴포넌트(Backgroung Music Player Component)가 부작되어 있다. 이 컴포넌트는 이름 그대로 게임에서 흘러나오는 배경 음악을 관리한다.
백그라운드 뮤직 플레이어 컴포넌트(Background Music Player Component)
public class BackgroundMusicPlayer : MonoBehaviour
이 컴포넌트는 배경 음악을 관리하는 컴포넌트로 배경 음악 역시 모든 씬에서 흘러나와야 하기 때문에 싱글톤 패턴으로 작성되어 있다. 다만 이번 예제인 2D 게임 키트에서는 배경 음악의 변경이 거의 없기 때문에 크게 언급할 부분은 없다. 다만, 직접 내부 코드나 오디오 믹서를 사용하는 부분은 참고해 볼만하다.
Explorer 2D Game Kit 분석 (1) - 개요
Explorer 2D Game Kit 분석 (2) - Start 씬 해부하기 (1)
Explorer 2D Game Kit 분석 (3) - Start 씬 해부하기 (2)
Explorer 2D Game Kit 분석 (4) - 게임플레이 요소 (1)
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
[투네이션]
[Patreon]
[디스코드 채널]
'Unity3D > Tutorial' 카테고리의 다른 글
[Unity3D] Explorer 2D Game Kit 분석 (4) - 게임플레이 요소 (1) (0) | 2019.12.10 |
---|---|
[Unity3D] Explorer 2D Game Kit 분석 (3) - Start 씬 해부하기 (2) (0) | 2019.11.07 |
[Unity3D] Explorer 2D Game Kit 분석 (1) - 개요 (0) | 2019.11.04 |
[Unity3D] Tutorial (7) - 애니메이션 (0) | 2019.08.19 |
[Unity3D] Tutorial (6) - 게임 오브젝트와 컴포넌트 (0) | 2019.02.04 |