Unity 검색

다루는 주제
공유

Is this article helpful for you?

Thank you for your feedback!

Spotlight Team은 열정이 넘치는 Unity 개발자들과 함께 Unity 기반 게임의 지평을 넓히고자 합니다. 그 노력의 일환으로 그래픽스, 성능, 디자인과 관련된 복잡한 문제를 해결할 수 있는 혁신적인 아이디어를 모색하는 가운데, 똑같은 문제와 해결책을 몇 번이고 반복해서 경험하기도 합니다.

이 블로그 시리즈는 Spotlight Team에서 클라이언트와 작업을 진행하며 가장 자주 경험하는 문제들을 다룹니다. 지금껏 유능한 팀들과 협업하는 과정에서 겪은 시행착오를 통해 얻어낸 결과를 공유할 수 있어 뿌듯함과 기쁨을 느낍니다.

블로그를 통해 소개하는 문제들 중 다수는 콘솔이나 휴대폰에서 게임을 테스트하거나 방대한 양의 게임 콘텐츠를 다루어 보아야만 비로소 발견하게 되는 문제입니다. 이러한 문제들에 대한 해결책과 도구 활용법을 개발 초기 단계부터 적극적으로 활용한다면, 시간과 열정을 들인 만큼 값진 성과를 거둘 수 있을 것입니다.

Scalability

Unity에서 지원하는 멀티 씬 편집 기능을 활용하여 단일 단위의 게임플레이 또는 기능을 정의하는 팀이 계속 늘어나고 있습니다. 이 때문에 다른 씬의 오브젝트를 참조할 수 없는 Unity의 한계를 극복해야 한다는 부담이 더욱 크게 다가옵니다.

이러한 한계를 극복할 수 있는 방법은 매우 다양합니다. 많은 팀에서는 오브젝트를 직접 참조하는 수고를 덜기 위해 생성자(spawner), 프리팹, 절차적 콘텐츠 또는 이벤트 시스템에 크게 의존하고 있습니다. Unity가 지금껏 가장 흔히 볼 수 있었던 해결책은 게임 오브젝트에 고정적이고 고유한 ID 또는 GUID를 할당하는 것입니다. 고유 ID를 보유하고 있다면 위치나 장소, 로딩 여부에 관계없이 알려진 게임 오브젝트의 인스턴스를 참조하거나 저장된 게임 데이터를 실제 런타임 게임 구조에 매핑할 수 있습니다.

수많은 클라이언트가 이와 같은 문제를 동일한 방법으로 해결하는 것을 지켜보며 Unity는 레퍼런스로 참조할 수 있을만한 구현을 진행하기로 결정했습니다. 그리고 다양한 선택지를 충분히 고려한 후, 공개 API와 C#만을 활용하는 사용자의 입장에서 이 문제에 접근하기로 했습니다. 이를 통해 내부적으로는 훌륭한 테스트 사례를 구축할 수 있었으며, 사용자와 코드를 직접 공유할 수 있게 되었습니다. 더 이상 빌드를 기다릴 필요가 없습니다. Github에 접속해서 코드를 다운로드한 후, 마음껏 사용하고 필요에 맞게 변경해 보실 수 있습니다.

이 솔루션에 대한 자세한 정보는 여기에서 확인하실 수 있습니다.

이 솔루션의 기본 구조는 아주 간단합니다. GUID의 모든 오브젝트를 포함하는 전역 정적 Dictionary가 있기 때문에, 필요한 오브젝트가 GUID를 보유하고 있다면 Dictionary에서 해당 정보를 찾아보면 됩니다. 정보가 있다면 검색 결과를 얻을 수 있으며, 그렇지 않다면 null값을 얻게 됩니다. 모든 유형의 클라이언트 프로젝트에 활용할 수 있도록 최대한 단순한 솔루션을 구성하고자 했습니다. 상기 링크의 GUIDManager.cs에서 추가적인 게임 오브젝트 없이 정적 관리자를 설정하는 방법을 확인해 보세요.

지속성

Unity가 직면한 첫 번째 과제는 System.GUID의 GUID를 Unity가 MonoBehaviour의 일부로 직렬화할 수 있는 형식으로 변환하는 것이었습니다. 다행히도 ISerializationCallbackReciever를 추가하는 것으로 손쉽게 해결할 수 있었습니다. 간단한 성능 테스트를 거친 결과, .ToByteArray()가 .ToString()에 비해 2배 정도 빠른 결과를 보였으며, 관련 없는 메모리를 할당하지도 않았습니다. Unity에서 byte[]를 취급하는 데는 문제가 없기 때문에, 이는 보조 스토리지로 활용하기에도 적합했습니다.


System.Guid guid = System.Guid.Empty;
[SerializeField]
private byte[] serializedGuid;

public void OnBeforeSerialize()
{
   if (guid != System.Guid.Empty)
   {
       serializedGuid = guid.ToByteArray();
   }
}

// On load, we can go ahead and restore our system guid for later use
public void OnAfterDeserialize()
{
   if (serializedGuid != null && serializedGuid.Length == 16)
   {
       guid = new System.Guid(serializedGuid);
   }
}

씬에서 게임 오브젝트를 생성하고 GuidComponent를 추가하면 GuidComponent는 새 GUID를 얻게 되고, 해당 GUID는 런타임 메모리에 저장됩니다. 해당 씬이 디스크에 직렬화되면 GUID도 바이트 배열로 직렬화됩니다. 이후 씬이 직렬화가 해제되면 동일한 GUID 역시 직렬화가 해제된 후에 런타임 메모리에 저장됩니다. 하지만 게임 오브젝트를 복사하여 붙여넣기 할 때 문제가 발생합니다.

 

씬에서 게임 오브젝트를 복사하여 붙여넣기 하면 GUID가 충돌하게 됩니다. GUID 충돌을 탐지하고 GuidManager.InternalAdd에서 코드를 수정하여 이 문제를 해결할 수 있습니다.

 

한 씬에서 GuidComponent를 가지는 게임 오브젝트의 프리팹 에셋을 생성하고 Unity 에디터를 다시 시작한 상황에서 다른 씬에서 프리팹 에셋을 인스턴스화한다면 GUID 충돌이 발생할 수 있으며, 해당 충돌을 GuidManager가 바로 탐지하지 못합니다. 이 문제는 다음 코드를 사용하여 프리팹 에셋에서 빈 GUID를 가지도록 하여 수정할 수 있습니다.


#if UNITY_EDITOR
// This lets us detect if we are a prefab instance or a prefab asset.
// A prefab asset cannot contain a GUID since it would then be duplicated when instanced.
PrefabType prefabType = PrefabUtility.GetPrefabType(this);
if (prefabType == PrefabType.Prefab || prefabType == PrefabType.ModelPrefab)
{
    serializedGuid = new byte[0];
    guid = System.Guid.Empty;
}
else
#endif

그리고 계속되는 테스트에서 이상한 점을 발견했습니다. 백업 프리팹이 손상된 프리팹 인스턴스가 새로운 GUID의 인스턴스 데이터를 저장하지 않는 경우가 종종 발생했습니다. 여기서 PrefabUtility가 다시 한번 해결사로 나섭니다. CreateGuid 함수를 확인해 보시죠.


#if UNITY_EDITOR
// If we are creating a new GUID for a prefab instance of a prefab, but we have somehow lost our prefab connection
// force a save of the modified prefab instance properties
PrefabType prefabType = PrefabUtility.GetPrefabType(this);
if (prefabType == PrefabType.PrefabInstance)
{
   PrefabUtility.RecordPrefabInstancePropertyModifications(this);
}
#endif

도구

일단 저장이 정상적으로 완료되면 사용자가 시스템에 데이터를 가져오는 방법을 알 수 있도록 해야 했습니다. 새로운 GUID를 만들기는 쉬웠지만, 이를 참조하는 일은 여전히 까다로운 작업이었습니다. 여러 씬에 걸쳐 레퍼런스를 설정하려면 사용자가 각 씬을 로드해야 한다는 점은 명백합니다. 초기 설정은 간단했습니다. 스탠다드 오브젝트 선택 필드를 찾으려는 GuidComponent로 설정하면 됩니다. 하지만 해당 레퍼런스는 크로스 씬이 될 것이므로 저장하지 않기로 했습니다. Unity는 이를 받아들이지 않고 null을 출력했습니다. 그래서 GuidReference를 일반 오브젝트 필드인 것처럼 드로우하고, 레퍼런스가 아닌 GUID를 저장해야 했습니다.

다행히도 특정 유형의 커스텀 드로어를 구축하고 [PropertyDrawer] 태그를 설정하는 작업은 아주 간단했습니다. 피커를 구축하고, 그 결과로 발생한 옵션을 선택한 후, 본래 목적대로 GUID 데이터를 직렬화했습니다. 하지만 로드되지 않은 씬에 타겟 게임 오브젝트가 포함된 경우에는 어떻게 해야 할까요? 설정된 값이 있음을 사용자에게 알려주고, 해당 오브젝트에 대해 가능한 한 많은 정보를 제공할 방법을 고민했습니다.

노력 끝에 찾아낸 해결책은 다음과 같습니다.

다른 씬에 저장된 오브젝트에 대한 레퍼런스.

미리 설정한 GuidComponent에 대한 비활성 오브젝트 선택자와 타겟이 있는 씬을 포함한 비활성 필드, 마지막으로 현재 선택한 타겟을 삭제(Clear)하는 버튼입니다. 사용자가 오브젝트 피커에서 없음(None)을 선택한 경우와 타겟 오브젝트가 로딩에 실패하여 값이 null이 된 경우의 차이를 식별할 수 없기 때문에 부득이하게 이러한 설정이 필요했습니다. 이러한 설정의 매우 큰 장점은 씬 레퍼런스가 실제 에셋 필드이며, 실제 타겟 오브젝트에서 확보하려는 경우 로드할 씬을 강조 표시한다는 점입니다. 예를 들자면 다음과 같습니다.

타겟을 포함한 씬을 강조 표시한 에셋 핑.

Github의 GuidReferenceDrawer.cs에서 모든 코드를 찾아볼 수 있습니다. 필드를 생성하는 트릭을 수정할 수는 없으나, 여기에서는 여전히 실제 필드의 역할을 하고 있죠.


bool cachedGUIState = GUI.enabled;
GUI.enabled = false;
EditorGUI.ObjectField(position, sceneLabel, sceneProp.objectReferenceValue, typeof(SceneAsset), false);
GUI.enabled = cachedGUIState;

테스트

모든 요소가 의도대로 구성되었음을 확인한 후에는 테스터와 사용자를 대상으로 이를 검증하고자 했습니다. Unity 내부용 테스트는 작성한 적이 있었으나, 사용자 코드용 테스트는 만들어 본 적이 없었습니다. 솔직히 말씀드리면, Unity 빌트인 테스트 러너(Test Runner)의 실효성을 간과하고 있었습니다. 정말 놀라울 정도로 유용한 툴인데도 말이죠!

제가 원하는 최종 결과물입니다.

창(Window) > 테스트 러너(Test Runner)로 이동하여 플레이 모드(Play Mode)나 편집 모드(Edit Mode)에서 코드에 대한 테스트를 만들 수 있습니다. 만드는 과정은 정말 쉽습니다.

테스트 예시를 통째로 가져와 봤습니다. GUID를 중복으로 설정했을 때 적절한 메시지를 출력하기 위한 테스트입니다.


[UnityTest]
public IEnumerator GuidDuplication()
{
   LogAssert.Expect(LogType.Warning, "Guid Collision Detected while creating GuidTestGO(Clone).\nAssigning new Guid.");
    
   GuidComponent clone = GameObject.Instantiate<GuidComponent>(guidBase);

   Assert.AreNotEqual(guidBase.GetGuid(), clone.GetGuid());

   yield return null;
}

이 코드는 예상한 경고를 받지 못하면 오류가 발생한다는 메시지를 테스트 도구(test harness)에 전달합니다. 그 다음, 이전에 생성한 GuidComponent를 클로닝하고 GUID 충돌이 발생하지 않았는지 확인한 후 테스트를 종료합니다.

파일 테스트를 시작하기 전에 불러오고자 했던 설정 기능의 [OneTimeSetUp] 속성을 활용하여 테스트를 구성했기 때문에, guidBase에 따라 그 과정은 달라질 수 있습니다. 더 자세한 내용은 여기의 문서에서 참조하실 수 있습니다. 테스트 작성이 얼마나 쉬웠는지, 테스트를 만드는 과정을 거쳐 코드가 얼마나 개선되었는지 확인하며 정말 감탄했습니다. 팀에서 사용 중인 툴이 있다면 종류에 상관없이 테스트해 보기를 적극 권장드립니다.

향후 계획

오늘 살펴본 해결책이 많은 개발자분들께 도움이 되었기를 바랍니다. 곧 Github에서 에셋 스토어 또는 패키지 관리자로 코드를 옮길 예정이며, 모두 무료로 다운로드하실 수 있습니다. 어려움을 겪고 계시거나 제안하실 사항이 있다면 언제든지 자유롭게 공유해 주시기 바랍니다. 앞으로도 Spotlight Team은 Unity를 최대한 활용할 수 있는 다양한 조언과 해결책을 소개해드릴 예정이니 많은 관심 부탁드립니다.

2018년 7월 19일 엔진 & 플랫폼 | 8 분 소요

Is this article helpful for you?

Thank you for your feedback!

다루는 주제
관련 게시물