이번에는 유니티에서 제공하는 UnityWebRequest와 Coroutine 말고 다른 스레드를 만들어서 다운로드하도록 해보겠습니다.
1. 다른 스레드를 사용하는 이유
1.1 프레임 드랍 최소화

위 스크린샷은 다른 게임에 패치 화면인데 미니게임을 진행합니다.
최근 출시되는 게임들에는 이처럼 패치 용량이 너무 많아 패치 화면에서 튜토리얼을 진행하거나 미니게임을 진행하거나 단순히 동영상을 재생시키거나 하는 게임들이 많이 늘어나고 있고 있어 패치 화면에서의 프레임드랍도 체크해야 하는 상황이 생깁니다.
UnityWebRequest로 다운로드하는 동안 성능을 많이 먹거나 하지 않지만 다운로드 이후에 패치 리스트 갱신, 압축파일인 경우 압축해제, 파일 검증 등등 다른 작업들로 인해 프레임드랍이 자주 발생하게 됩니다. 아마도 UnityWebRequest 내부에서는 다른 스레드에서 동작해서 큰 문제없지 않나 싶습니다.
또, 에디터나 PC환경에서는 속도에 큰 차이는 없지만 모바일(Android, iOS) 환경에서는 다운로드 속도에서도 큰 차이가 나는데 정확한 이유까지는 모릅니다. (혹시 아시는 분은 덧글로 남겨주세요!)
1.2 백그라운드 상태에서 다운로드

유니티의 메인 스레드가 아니라 다른 스레드를 사용하여 다운로드하면 위와 같은 백그라운드 상태에서도 다운로드가 가능합니다.
백그라운드 상태에서도 다운로드가 진행된다면 패치받는 중에 다른 앱을 진입하더라도 패치는 진행 중이기 때문에 계속해서 패치 화면을 볼 필요가 없죠.
iOS에서 알아야 할 점은 iOS는 위와 같은 상태라도 Backgrounded상태와 Suspended상태로 나뉘는데 백그라운드 상태에서 어느 정도 시간이 지나면 자동으로 Suspended상태로 넘어가고 이 때는 다른 스레드들도 멈춥니다.
Android, iOS에서는 기기의 메모리가 부족해지면 백그라운드 상태의 앱을 종료시켜버려서 완벽하게 백그라운드 다운로드가 가능한 부분은 아니지만 그래도 네이티브 코드를 건드리지 않는 선에서 최선의 방법이 아닐까 생각합니다.
2. UnityWebRequest → WebClient
using (var request = UnityEngine.Networking.UnityWebRequest.Get(uri))
{
request.SetRequestHeader("Cache-Control", "max-age=0, no-cache, no-store");
request.downloadHandler = new UnityEngine.Networking.DownloadHandlerFile(savePath);
request.SendWebRequest();
while (request.isDone == false)
{
m_DownloadProgress = request.downloadProgress;
m_FullProgress = ((float)m_DownloadCount / m_DownloadCountMax) + (m_DownloadProgress / m_DownloadCountMax);
yield return null;
}
// ... 중략 ...
}
using (var webClient = new System.Net.WebClient())
{
var ur = new System.Uri(uri);
webClient.DownloadProgressChanged += (object sender, System.Net.DownloadProgressChangedEventArgs e) => {
m_DownloadProgress = e.ProgressPercentage * 0.01f;
m_FullProgress = ((float)m_DownloadCount / m_DownloadCountMax) + (m_DownloadProgress / m_DownloadCountMax);
};
bool isCompleted = false;
webClient.DownloadFileCompleted += (object sender, System.ComponentModel.AsyncCompletedEventArgs e) => {
isCompleted = true;
};
webClient.Headers.Add("Cache-Control", "max-age=0, no-cache, no-store");
webClient.DownloadFileAsync(ur, savePath);
while(isCompleted == false) System.Threading.Thread.Sleep(100);
// ... 중략 ...
}
이전에 저희가 구현했던 UnityWebRequest를 WebClient로 바꿔서 구현해보았습니다.
함수의 이름, 형식이 조금씩 달라서 그렇지 UnityWebRequest를 사용하여 다운로드를 구현해보셨다면 어렵지 않을 거라 생각됩니다.
Sleep() 함수는 일정 시간 동안 스레드를 정지시키는 함수인데 다운로드를 동기로 실행하면 Progress를 가져올 수 없어서 비동기로 실행하고 Sleep함수를 호출해 0.1초마다 완료되었는지 체크하도록 구현했습니다.
UnityWebRequest와 Coroutine으로 구현한 부분에서 yield return null로 매 프레임 Progress를 가져오는 코드와 비슷한 거라 생각하시면 됩니다.
https://docs.unity3d.com/ScriptReference/Networking.UnityWebRequest.html
Unity - Scripting API: UnityWebRequest
UnityWebRequest handles the flow of HTTP communication with web servers. To download and upload data, use DownloadHandler and UploadHandler respectively. UnityWebRequest includes static utility functions that return UnityWebRequest instances configured for
docs.unity3d.com
https://docs.microsoft.com/ko-kr/dotnet/api/system.net.webclient
WebClient 클래스 (System.Net)
URI로 식별되는 리소스와 데이터를 주고 받기 위한 일반적인 메서드를 제공합니다.
docs.microsoft.com
3. 유니티 관련 함수 호출하지 않게
// 다운로드 스레드 외부에서 변수 저장
m_PersistentDataPath = Application.persistentDataPath;
// 다운로드 스레드 내부에서 변수로 사용
var savePath = System.IO.Path.Combine(m_PersistentDataPath, SAVE_PATCH_PATH, patchData.fileName);
유니티에서 멀티스레드를 사용하게 되면 조금 골치 아픈 부분인데, 다른 스레드에서 유니티의 함수를 호출할 수 없습니다.
그래서 위 코드처럼 스레드 외부에서 변수에 미리 담아주고 스레드 내부에서는 선언된 변수를 통해 사용해야 합니다.
private Queue<System.Action> m_ProcessMainThreadQueue = new Queue<System.Action>();
private void Update()
{
while(m_ProcessMainThreadQueue.Count > 0)
m_ProcessMainThreadQueue.Dequeue()?.Invoke();
}
// 스레드 내부에서 호출
m_ProcessMainThreadQueue.Enqueue(() => {
// 예로들면 팝업을 호출해줘야하는 상황
});
위의 경우에는 단순히 변수로만 담아주면 되는데 복잡한 상황의 경우에는 저의 경우 delegate를 Queue에 담아뒀다 Update, LateUpdate에서 호출해줍니다.
제가 선호하는 방법은 아니라서 정말 필요할 때 외에는 잘 쓰고 있지 않습니다.
4. 다운로드 스레드 시작
var thread = new System.Threading.Thread(() =>
{
// ...
// 중략
// ...
});
thread.Start();
간단하게 스레드를 생성해서 시작하는 코드입니다.
저희가 짠 코드를 스레드 내부에서 호출해주면 다른 스레드에서 동작합니다.
자세한 코드는 길어서 링크로만 남겨두겠습니다.
https://docs.microsoft.com/ko-kr/dotnet/api/system.threading.thread
Thread 클래스 (System.Threading)
스레드를 만들고 제어하며, 해당 속성을 설정하고, 상태를 가져옵니다.
docs.microsoft.com
마무리

일단 실행해보면 UnityWebRequest와 Coroutine으로 구현한 것과 다를게 없이 동작하지만, 에디터에서 다운로드 중 Pause상태로 바꿔도 실제로는 다운로드가 진행 중인 것을 확인하실 수 있습니다.
물론 Android, iOS 앱으로 빌드해서 확인하면 좀 더 확실하게 알 수 있습니다.
패치 다운로드 만들기 마지막 편 까지 읽어주셔서 감사합니다.
https://github.com/PieceOfPaper/Unity_SimplePatchExample
GitHub - PieceOfPaper/Unity_SimplePatchExample: 그냥 심플하게 패치 받는 예제
그냥 심플하게 패치 받는 예제. Contribute to PieceOfPaper/Unity_SimplePatchExample development by creating an account on GitHub.
github.com
'Unity Tips' 카테고리의 다른 글
Unity UI RectTransform 적정 사이즈로 설정하기 (0) | 2022.10.09 |
---|---|
Unity UI LayoutGroup 강제 갱신 (1) | 2022.10.03 |
Unity에서 패치 다운로드 만들기 3편 - 패치 UI 만들기 (0) | 2022.05.07 |
Unity에서 패치 다운로드 만들기 2편 - UnityWebRequest 이용하여 만들기 (0) | 2022.05.07 |
Unity에서 패치 다운로드 만들기 1편 - 패치 리스트 만들기 (0) | 2022.05.07 |