멀티 게임 전략

고객 성공 팀의 컨설턴트로서 프로젝트를 검토하는 동안 저는 종종 게임 전환 애플리케이션을 만드는 고객과 함께 일합니다. 이러한 애플리케이션에는 하나의 메인 메뉴 또는 테마 메뉴가 있으며, 플레이어가 선택할 수 있는 다양한 게임을 제공합니다. 이러한 설정에서 주요 관심사는 게임 전환 사이의 시간을 최대한 짧게 유지하는 방법과 게임 전반에서 최적의 성능을 보장하는 방법입니다. 이 블로그 게시물에서는 프로젝트 요구 사항에 따른 다양한 접근 방식과 게임 전환 설정 유무에 관계없이 모든 게임 환경에 유용할 수 있는 몇 가지 모범 사례를 살펴봅니다.
게임, 엔터테인먼트, 산업 시뮬레이션 등 다중 애플리케이션 환경을 계획할 때 가장 중요한 결정은 게임 실행 파일을 어떻게 관리할 것인가 하는 것입니다. 이 결정에 영향을 미칠 수 있는 요인은 여러 가지가 있습니다:
- 플랫폼에서 처리할 수 있는 게임은 몇 개인가요?
- 게임의 규모는 어느 정도인가요?
- 게임이 동일한 Unity 버전으로 제작되나요? 애플리케이션의 병목 현상은 무엇인가요?
- 다른 요인으로는 대상 하드웨어, 메모리 및 CPU, 디스크 속도(SSD 대 HDD 대 SD 카드)가 있습니다.
이러한 질문에 답하고 실행 파일을 처리하는 방법을 결정하려면 각 게임에 대해 별도의 실행 파일이 필요한지, 여러 게임에 대해 하나의 공유 실행 파일이 필요한지, 아니면 애플리케이션의 성능을 최적으로 보장하기 위해 두 가지를 모두 조합해야 하는지 파악하는 것이 중요합니다.
여러 개의 실행 파일을 보유하는 것은 다양한 Unity 버전으로 제작된 게임을 처리하는 데 유용한 옵션입니다. 이 접근 방식을 사용하면 실행 파일을 메모리에 캐싱하고 각 인스턴스를 백그라운드에 남겨두어 게임 간 전환 시간을 줄일 수 있습니다. 그러나 모든 실행 파일을 메모리에 보관하는 것은 메모리에 부담을 줄 수 있으므로 항상 최선의 선택은 아닙니다. 개별 게임의 메모리 사용량이 많거나 게임 전환 애플리케이션에 많은 게임이 있는 경우에는 이 방법을 사용하지 않는 것이 좋습니다.
메모리 제약을 완화하기 위해 게임에서 단일 실행 파일을 공유할 수 있습니다. 게임이 동일한 Unity 버전을 공유하는 한, 게임은 단일 Unity 프로젝트에 포함되거나 각각 별도의 프로젝트에 포함될 수 있습니다. Windows의 Unity 2022 LTS부터는 명령줄( -datafolder <path_to_folder> )을 통해 가변 경로를 전달하여 선택한 게임 데이터 폴더를 지정하여 변경 사항을 전환할 수 있는 -datafolder 인수를 사용할 수 있습니다. 이 접근 방식의 한 가지 잠재적 단점은 게임 전환 시간이 느려진다는 것이므로 이러한 단점을 줄이기 위해 로딩 모범 사례를 따르는 것이 중요합니다.
개발 중인 게임의 성격이나 플랫폼에 관계없이 게임을 선택하는 순간부터 화면에 게임이 완전히 로드될 때까지 시간을 최대한 단축하는 것이 중요합니다. 이 목표는 게임 전환 애플리케이션에서 특히 중요합니다.
로딩을 처리하는 가장 좋은 방법은 어드레서블을 사용하는 것입니다. 어드레서블을 사용하면 필요에 따라 콘텐츠를 다운로드하고 릴리스할 수 있습니다. 이 지연 로딩 전략은 초기 시작 시 로드해야 하는 데이터의 양을 제한하므로 게임의 로드 시간을 단축하는 가장 효율적인 방법입니다. 또한 CPU 병목 현상의 원인이 될 수 있는 백그라운드 게임과 관련된 CPU 백그라운드 활동을 방지하는 데 도움이 될 수 있습니다. 주소 지정 가능: 기획 및 모범 사례 블로그 게시물은 어드레서블에 대해 자세히 알아보고 게임 개선에 어떻게 도움이 되는지 알아볼 수 있는 좋은 출발점입니다.
사용 중인 실행 파일의 수에 관계없이 더 빠른 로딩을 보장하는 가장 좋은 방법은 비동기 로딩 API를 사용하는 것입니다. 비동기 로딩 시 Unity 메인 스레드는 "메인 스레드 통합"이라는 프로세스를 실행하여 네이티브 및 관리형 오브젝트의 초기화를 시간 분할 방식으로 담당합니다. 이 프로세스는 스레드 안전하지 않은 일부 작업을 수행하므로 메인 스레드에서 발생하며, 게임이 장시간 정지되는 것을 방지하기 위해 메인 스레드 통합을 실행할 수 있는 시간이 제한되어 있습니다. 통합에 소요될 수 있는 시간은 Application.backgroundLoadingPriority 속성에 의해 정의됩니다. 로딩 화면에서는 backgroundLoadingPriority를 높음(50밀리초)으로 설정한 다음 로딩이 완료되면 보통 (4밀리초) 또는 낮음 (2밀리초)으로 되돌릴 것을 권장합니다.
로딩 속도를 높이는 또 다른 방법은 비동기 텍스처 업로드를 사용하는 것입니다. 비동기 텍스처 로드는 텍스처와 메시를 업로드하는 데 사용되는 시간과 메모리를 GPU 설정에 맞게 조정하여 로드 시간을 줄일 수 있습니다. 비동기 업로드 파이프라인 이해 블로그 게시물에서 이 프로세스의 작동 방식에 대한 자세한 정보를 확인할 수 있습니다.
이러한 관행은 로딩 시간을 단축하는 데 도움이 됩니다:
- 씬 콘텐츠를 최대한 최소화하세요. 부트스트랩 씬을 사용하여 게임을 플레이 가능한 상태로 만드는 데 필요한 것만 로드한 다음 필요할 때 추가 씬을 로드합니다.
- 로딩 화면에서는 카메라를 비활성화합니다.
- 로딩 중 UI 캔버스가 채워지는 동안에는 비활성화합니다.
- 네트워크 요청을 병렬화합니다.
- 복잡한 깨우기/시작 구현을 피하고 워커 스레드를 활용하세요.
- 항상 텍스처 압축을 사용하세요.
- 오디오 파일이나 텍스처와 같은 대용량 미디어 파일을 메모리에 저장하지 않고 스트리밍하세요.
- JSON 직렬화기를 피하고 대신 바이너리 직렬화기를 사용하세요.
앞서 언급했듯이 멀티 게임 환경에서는 메모리만이 문제가 아니라 백그라운드 CPU 활동도 플레이어의 게임 경험에 영향을 미칠 수 있습니다. 게임을 활발하게 플레이하지 않을 때에도 CPU는 계속 실행되고 있으며, 이로 인해 활성 게임이 CPU 고갈을 일으켜 최적이 아닌 성능을 발휘하게 됩니다. 활성 게임 및 기타 백엔드 플랫폼 프로세스의 CPU 고갈을 방지하는 방법은 Unity 설정에서 플레이어를 백그라운드에서 실행을 false로 설정하는 것입니다. 백그라운드에서 실행을 선택하면 게임에 초점이 맞춰지지 않은 상태에서 Unity 게임 루프가 중지됩니다. 설정은 스크립트를 통해 동적으로 변경할 수도 있습니다.
public class ExampleClass : MonoBehaviour
{
void Example()
{
Application.runInBackground = false;
}
}
한 가지 주의할 점은 백그라운드에서 실행 설정은 사용자 지정 스크립팅 스레드의 실행을 중지하지 않으므로 Thread.Sleep C# 메서드를 통해 플레이하지 않는 게임의 모든 스레드를 절전 모드로 설정하는 것이 중요합니다. Unity에서 백그라운드 스레드로 작업하려면 세심한 프로그래밍이 필요하다는 점을 기억하세요. 이러한 스레드는 Unity API에 직접 액세스할 수 없으므로 교착 상태 및 경합 조건과 같은 문제가 발생할 가능성이 더 높습니다. 이를 방지하려면 메인 Unity 스레드와 적절하게 동기화해야 합니다. 멀티스레딩을 올바르게 구현하려면 Unity의 .NET 개요 매뉴얼 페이지의 비동기 및 대기 작업의 제한 사항 섹션과 스레드 및 스레딩 사용에 대한 MSDN 문서를 검토하세요. Unity 6에는 비동기/대기 기능을 더 잘 지원하는 Awaitable 클래스가 도입되었습니다.
특히 개발 후반 단계에서 메모리 누수의 원인을 파악하고 수정하는 것은 어렵고 시간이 많이 소요될 수 있습니다. 진부하게 들릴지 모르지만 예방은 언제나 치료보다 낫습니다. 다음은 모든 게임 환경에서 유출을 방지하는 데 도움이 될 수 있는 몇 가지 권장 사항입니다:
- 메모리에 새 객체/자산을 만들 때는 필요하지 않은 경우 반드시 삭제하세요. 어드레서블을 사용하는 경우 사용하지 않는 에셋을 해제해야 합니다.
- 씬을 로드/언로드할 때 에셋을 메모리에서 올바르게 제거해야 합니다. 레벨이 언로드될 때 Unity는 에셋을 자동으로 언로드하지 않으므로 메모리에서 모든 액세스를 제거하는 것이 중요합니다. Resources.UnloadUnusedAssets API는 에셋 정리에 도움이 될 수 있습니다. 그러나 연산이 완료될 때까지 반환하는 객체를 반환하기 때문에 CPU 스파이크를 유발할 수 있으므로 성능에 민감하지 않은 곳에서 사용해야 합니다.
- 게임 오브젝트 인스턴스화 및 파괴를 자주 사용하지 마십시오. 이렇게 하면 불필요한 관리 할당으로 이어질 수 있을 뿐만 아니라 CPU 작업에도 많은 비용이 소요될 수 있습니다. 그러나 파괴를 사용해야 하는 경우 셸 오브젝트 유출을 방지하기 위해 오브젝트에 대한 모든 참조를 제거해야 합니다. 오브젝트 또는 그 부모가 Destroy를 통해 소멸되면 C# 코드는 Unity 오브젝트에 대한 참조를 유지하면서 관리형 래퍼 오브젝트, 즉 관리형 셸을 메모리에 보관합니다. 네이티브 메모리는 해당 메모리가 있는 씬이 언로드되거나 해당 메모리가 연결된 게임 오브젝트 또는 그 부모가 파괴를 통해 파괴되면 언로드됩니다. 따라서 언로드되지 않은 다른 것이 여전히 참조하는 경우 관리되는 메모리는 유출된 셸 객체로 계속 남아있을 수 있습니다.
- 싱글톤을 사용하여 이벤트를 구현할 때는 주의하세요. 싱글톤 인스턴스는 해당 이벤트에 가입한 모든 객체에 대한 참조를 보유합니다. 이러한 객체가 싱글톤 인스턴스만큼 오래 살지 않고 이러한 이벤트에서 구독을 취소하지 않으면 메모리에 남아 메모리 누수를 일으킵니다. 이벤트 소스가 리스너보다 먼저 처리되면 참조가 지워지고, 리스너가 제대로 등록 취소되면 참조도 남지 않습니다. 이 문제를 해결하고 예방하려면 싱글톤 이벤트를 수신하는 모든 객체에 약한 이벤트 패턴 또는 IDisposable을 구현하고 코드에서 적절하게 처리되는지 확인하는 것이 좋습니다. 약한 이벤트 패턴은 이벤트 중심 프로그래밍에서 메모리와 가비지 컬렉션을 관리하는 데 도움이 되는 디자인 패턴으로, 특히 수명이 긴 객체와 관련하여 유용합니다. 구독자의 수명은 짧지만 퍼블리셔의 수명은 긴 경우에 특히 유용합니다. 이러한 솔루션은 C# 전용 솔루션이며 C# 이벤트에서만 작동하며 UnityEvents 또는 Unity UI 툴킷에서 직접 지원하지 않는다는 점에 유의하시기 바랍니다. 따라서 이러한 솔루션은 모노비헤이비어가 아닌 스크립트에서만 구현하는 것이 좋습니다.
마지막으로, 개발 초기 단계부터 프로파일링, CI/CD 테스트 및 스트레스 테스트를 수행하면 누수가 발생할 때 즉시 문제를 해결하고 디버깅 시간을 절약하며 최적의 성능을 보장할 수 있으므로 시간을 크게 절약할 수 있습니다.