맛난호빵
Unity3D에서의 부드러운 씬 스트리밍 본문
출처: Smooth Scene Streaming with Unity3D (80.lv)
저자: Dimitar Popov, 게임 개발자
개요
저는 불가리아의 소규모 독립 게임 스튜디오인 Sixth Hammer의 Dimitar입니다. 현재 Unity3D 엔진을 사용한 2D 게임 개발에 주력하고 있습니다. 저희의 가장 큰 게임인 Moo Lander는 일반적으로 더 복잡한 게임에서 발생하는 많은 어려운 문제에 대한 창의적인 솔루션을 찾아야 했습니다. 저희는 Moo Lander가 로딩 화면을 포함하지 않고 대신 하나의 거대한 열린 2D 세계가 되기를 원했습니다. 그래서 저희는 게임 플레이 중에 콘텐츠를 성능 좋게 스트리밍 하는 문제에 대한 해결책을 찾아야 했습니다.
Unity에는 씬(Scene)이라는 개념이 있으니 게이머가 게임을 플레이하는 동안 백그라운드에서 해당 씬을 로드하는 방법을 찾아야 했습니다. 충분히 간단해 보이죠? 아닙니다!
- 씬을 언제 언로드 하는지? 예를 들어 멀리 떨어진 배경 물체가 보이지 않고 씬에서 더 이상 필요하지 않다면?
- 여러 플레이어가 게임 세계의 다른 위치에 있을 수 있는 경우 로드해야 하는 씬을 추적하는 방법은?
- 성능 스파이크 없이 백그라운드에서 씬을 로드 및 언로드 하는 방법은?
이것들은 이 글에서 제가 답하고자 하는 몇 가지 질문입니다. 자, 이 주제에 자세히 알아봅시다.
Unity가 제공하는 현재 즉시 사용 가능한 기능
Unity의 씬을 게임 개체의 "묶음"으로 생각할 수 있습니다. Unity는 씬 로드의 두 가지 유형인 가산형[Additively]과 비가산형[Non-Additively]을 제공합니다. 비가산적 로딩은 현재 씬을 로드될 씬으로 대체하는 반면에, 가산적 로딩은 이미 로드된 씬을 유지합니다. 이 경우에는 가산적 로딩이 필요했죠.
또한 Unity는 두 가지 유형의 로딩 방법을 제공하는데 각각 동기[Synchronously] (기본적으로 로드가 완료될 때 까지 다른 모든 것을 일시 중지함.)와 비동기[Asynchronously] (백그라운드에서)가 있습니다. 우리 게임의 경우 가산적 비동기 로딩(Additive Asynchronous loading)을 사용했습니다.
저희의 솔루션
저희는 개발 중에 발생한 앞서 언급한 문제를 해결하기 위해 장면 로딩을 위한 게임 오브젝트 및 헬퍼 클래스 전체 시스템을 구축해야 했습니다.
씬 전환기
저희는 적절한 순간에 장면을 로드 및 언로드 하기 위한 최적의 솔루션이 무엇인지 오랫동안 고민했습니다(액션이 최소화될 때, 장면 자체가 보이지 않을 때, 장면이 다른 곳에서 더 이상 필요하지 않을 때 등). 결국 저희는 씬 전환기[Scene Switcher]에 대한 아이디어를 생각해 냈습니다.
각 씬 전환기는 자식으로 두 개의 "게이트" 게임 오브젝트를 보유합니다. 각각에 2D 물리적 트리거 콜라이더가 있습니다(일반적으로 직사각형이지만 장면 레이아웃에 맞게 필요한 경우 어떤 식으로든 모양이 지정될 수 있음). 씬 전환기에는 로드 및 언로드 해야 하는 씬 이름 필드도 있습니다. 그런 다음에 게이트와의 상호작용을 추적합니다. 1 => 2 방식의 플레이어와의 상호작용은 씬을 로드 해야하고 2 => 1의 상호작용은 장면을 언로드해야 합니다.
씬 시스템
씬 전환기는 씬을 로드하거나 언로드하려는 플레이어의 의도를 알 수 있습니다. 그러나 저희는 씬을 로드하는 실제 프로세스를 다른 싱글톤 시스템 클래스로 분리하여 플레이어의 모든 의도와 이미 로드된 장면을 추적합니다.
이것은 다음과 같은 몇 가지 작업을 수행합니다.
- 씬 이름과 씬이 필요한 플레이어의 목록이 키와 값인 필요한 씬이 포함된 딕셔너리(Dictionary)를 저장합니다. 그런 다음 딕셔너리를 갱신하기 위해 UpdateGameObjectNeededScenes(GameObject GO, string sceneName, bool shouldLoad) 메서드를 제공합니다.
- Unity 내장 씬 관리 이벤트를 구독하여 최신 상태로 유지하는 현재 로드된 씬(씬 이름과 로드 상태를 키와 값으로)이 포함된 딕셔너리를 저장합니다.
- 게임의 업데이트 루프에서 로드된 씬과 현재 필요한 씬이 일치하는지 확인하는 기능을 실행하고, 일치하지 않으면 씬의 로드 및 언로드를 시작합니다.
물론 실제 구현에는 수십 개의 헬퍼 함수와 기타 변수가 포함되지만 어쨌든 이것이 핵심입니다. 이 아키텍처를 사용하여 처음 두 가지의 문제(이 글의 시작 부분에 언급됨)를 정상적으로 해결하고 있습니다.
반면에 성능에 대한 세 번째 문제는 따로 장문의 글이 필요합니다. 그러면 어떤 문제점과 도전이 있었고 저희가 어떻게 해결했는지 알아보겠습니다.
가산적 로딩 성능 개선
Unity가 백그라운드에서 씬을 로드하는 방식은 씬이 로드되는 동안 잘 작동합니다. 그러나 마지막 프레임에서 Unity는 모든 게임 개체를 생성하고 활성화합니다(엔진이 개체 간의 종속성을 알 수 없기 때문에 정상적인 동작). 이로 인해 상당한 지연[Rag]이 발생하게 됩니다.
지연의 심각성은 주로 다음 두 가지 사항에 따라 달라집니다.
- 각각 사용된 에셋 및 리소스 수와 씬의 게임 오브젝트 수
- 연결된 MonoBehaviour 컴포넌트의 수(특히 Awake 혹은 Start 메서드에서 실행되는 논리가 있는 경우에 해당됨)
따라서 저희는 이를 기반으로 성능을 개선하기 위한 몇 가지 다른 접근 방식이 있습니다.
- 씬을 더 작게 만듭니다. 예를 들면 그 안에 있는 게임 오브젝트의 수를 줄이면 성능이 향상될 수 있습니다. 하지만 전체 게임에서 씬 전환 및 로딩이 더 자주 발생해서 게임의 관리가 훨씬 더 어려워집니다.
- MonoBehaviour의 수를 줄입니다. 이는 성능 향상을 위한 유효한 접근 방식입니다. 하지만 저희의 경우에는 Moo Lander에 있는 대부분의 게임 오브젝트가 수행하는 복잡한 동작 때문에 쉽지 않았습니다.
- 어떻게든 씬의 모든 게임 오브젝트를 동시에 활성화하지 않는 방법을 찾습니다. 그렇게 하면 복잡성을 줄일 필요도 없었고 엄청난 성능 향상을 달성했기 때문에 이것이 우리에게 가장 좋은 접근 방식이었습니다.
게임 개체의 지연된 활성화
그렇게 하려면 먼저 Unity가 씬 로드 시 자동으로 이 작업을 수행하지 않도록 해야 합니다. 불행스럽게도 저희는 그렇게 할 수 있는 내장된 방법이 없었기 때문에 그냥 진행했고 다음 작업을 수행해야만 했습니다.
- 씬의 모든 게임 오브젝트를 하나의 씬 게임 오브젝트의 자식으로 만들었습니다.
- 모든 물리적 상호 작용 가능한 게임 오브젝트는 씬 게임 오브젝트의 직계 자식인 Interactables 게임 오브젝트로 이동시켰습니다.
- 게임 빌드를 만들 때 모든 씬 게임 오브젝트를 비활성화하는 툴을 만들었습니다. 이렇게 하면 Unity가 씬을 로드할 때 비활성화된 상태로 로드됩니다.
그런 다음에 씬이 로드된 후 다음 동작으로 장면 로드 및 언로드를 담당하는 씬 시스템을 확장했습니다.
- 로드된 장면의 맨 위에 비활성화된 씬 오브젝트가 있는지 먼저 확인합니다.
- 하나가 있으면 하위 게임 개체의 지연된 활성화를 수행하려는 것을 의미하므로 모든 다중 수준 하위를 반복하고 하나씩 비활성화합니다.
- 상위 씬 오브젝트를 다시 활성화합니다. 이 시점에서는 모든 하위 항목이 비활성화되어 있기 때문에 아무것도 표시되지 않습니다.
- 모든 자식을 순회하는 코루틴을 시작하고 프레임마다 일정량씩 활성화합니다. 매우 중요한 점은 이 코루틴 루프가 Interactables 홀더를 건너뛰어야 한다는 것입니다. 시간이 지남에 따라 물리 오브젝트를 활성화하면 잠재적으로 잘못된 동작이 발생할 수 있기 때문입니다(땅보다 먼저 적을 활성화하면 적을 떨어뜨리는 것처럼).
- 그런 다음 모든 Interactable 게임 오브젝트를 한 번에 활성화합니다(여전히 약간의 성능 지연이 발생하지만 처음과 같이 수 천 개의 오브젝트 대신에 수 십 개만 활성화하기 때문에 훨씬 더 작음).
이를 통해 저희는 Unity로 씬을 추가 및 비동기식으로 로드할 때 불쾌한 성능 스파이크를 제거하여 플레이어 경혐의 품질을 개선했습니다.
보너스 팁 - 씬을 언로드해도 실제로 씬에서 사용된 텍스쳐 및 기타 에셋이 모두 언로드되는 것은 아닙니다. 그렇기 때문에 게임 플레이 중에 액션이 많지 않은 순간에 내장된 UnloadUnusedAssets 메서드를 때때로 호출해주어야 합니다. 이는 성능에 약간의 스파이크를 일으키지만 저희는 여전히 Unity 동작을 최적화해야하는 지점에 도달하지 못했습니다.
또한 씬의 비동기 로드 개선에 대한 다른 통찰을 보고싶다면 이 링크를 확인하세요.
결론
보셨다시피 게임 내내 플레이어가 필요로 하는 모든 것을 로드하기 위해 많은 일이 일어나고 있습니다. 그리고 Moo Lander를 마친 후에는 이 모든 것을 플러그인으로 패키징하여 미래의 동료 개발자를 위해 많은 시간을 절약할 것입니다.
물론 게임 씬과 오브젝트(씬 사이를 이동할 수 있는 영구 오브젝트[Persistent objects]등)에 더 많은 일이 발생하지만 향후 글에서 이에 대한 통찰을 공유해보겠습니다. 현재로서는 이 글이 추가 씬 로딩 문제에 대한 훌륭한 솔루션을 구축하는데 충분한 지침이 되기를 바랍니다.
Dimitar Popov, The Sixth Hammer의 공동 설립자이자 게임 개발자
번역 후기
저도 유니티에서 씬 스트리밍 까지는 아니지만 로딩 화면 구현과 관련해서 고생한 적이 있었는데요. 처음 구현하는 것 이기도 했고, 로딩하는 과정 자체가 게임 진행에 부하가 큰 작업이기 때문에 씬 간에 전이 효과를 주려고 해도 버벅거리는 문제가 있었습니다. 그 이후에 원글을 보게 되었는데요. 유니티의 성능 한계를 뛰어넘기 위해 고군분투한 모습에 감명받았고 잠이 확 달아난 저는 바로 블로그와 번역기를 켜서 한땀 한땀 번역해버렸습니다.
의역이 많은데, 완전 의미가 다른 의역이 있다면 이 글을 읽으시는 다른 분들을 위해 댓글로 달아주시면 감사하겠습니다.