Unity3D AssetBundle 생성부터 패치 및 적용까지. 2 - Coroutine 활용해서 AssetBundle Patch하기

 지난 포스팅에서 유니티3D 에셋 번들을 간단하게 빌드하는 것을 정리해었습니다.


 구글링을 해보면 위 링크와 같이 코루틴과 yield역시 개념과 활용글이 많이 있습니다. 또한 에셋 번들도 개념과 생성, 로딩에 대한 정리도 많았죠. 하지만 둘을 활용해서 게임 패치에 활용하는 내용은 아직 없는것 같습니다. 이번에는 코루틴(Coroutine)과 yield를 가지고 에셋 번들 패치 시스템에 활용해보는 것을 간단히 정리를 해봅니다. 참고로 패치 툴이나 패치 메니져 세부 내용들에 대한 내용은 없습니다. 그리고 이미 코루틴과 패치관련 제작 경험이 있으신 분들에게는 싱거울 수 있습니다.

 이건 지난번에도 올렸던 스샷입니다. 에셋 번들을 패치받아 로드된 에셋(박스 프리펩)을 발사하는 간단한 데모죠. 사실 패치 기능이 나오기까지는 작년에 cocos2d-x 베이스에서 작업할 때 만들었던 C# 패치 툴과 cocos2d-x에서 게임 패치하려고 curl로 http, ftp 파일 받기 정리 과정이 있었기에 지금의 이 포스팅도 있게되네요. 다행히 cocos2d-x용 프로젝트 패치 툴을 C#으로 만들어서 유니티3D 프로젝트에 수정을 조금? 해서 바로 사용할 수 있었습니다. 잡설은 고만하고, 위 스샷에 사용된 GameGUI C# 코드를 보겠습니다. 이 소스는 에셋 번들 메니져나 패치 메니져 컴포넌트 속살이 드러나지 않는 샘플 소스라 풀로 올려봤습니다.


1. 패치 시작과 Client UI 갱신

using UnityEngine;
using System.Collections;

public class GameGUI : MonoBehaviour {
 
 public Texture textureLevel;
 public Texture textureBar;
 public Texture textureButton;
 
 AssetBundle bundle;
 
 void Start () {
  GameDataManager.Instance.Init();
 }
 
 void OnGUI () {
  int iPatchRate = PatchManager.Instance.GetPatchProcessRate();
  LevelBar(iPatchRate);
  
  float fXPos = 100;
    
  if( GUI.Button(new Rect(fXPos, 100, 100, 100), "Start Patch") )
  {
   string strUrl = "http://assetbundle.patch.server";
   string strRootDir = "MyGameRootDir";
   StartCoroutine(PatchManager.Instance.StartPatch(strUrl, strRootDir));
  }
  fXPos += 100;
  if (GUI.Button (new Rect(fXPos, 100, 100, 100), "Get\nAssetBundle") )
  {
   bundle = AssetBundleManager.Instance.GetAssetBundle("testAssetBundle");
  }
 }

 void LevelBar (int percent) {
  GUI.BeginGroup(new Rect(10,35,textureLevel.width,textureLevel.height));
                 
   GUI.DrawTexture(new Rect(50,20,130*((float)percent/100.0f),textureBar.height), textureBar);
   GUI.DrawTexture(new Rect(0,0,textureLevel.width,textureLevel.height), textureLevel);
  
   GUIStyle style = new GUIStyle();
   style.normal.textColor = Color.black;
   GUI.Label(new Rect(30, 20, 100, 20), percent.ToString(), style);
  
  GUI.EndGroup();
 }
 
 void Update () {
  if (bundle == null) {
   return;
  }
  
  if (Input.GetKeyUp( KeyCode.Space )) {
   GameObject gameObj = bundle.mainAsset as GameObject;
   Object obj = Instantiate(gameObj, transform.position, transform.rotation);
   DestroyObject( obj, 5.0f );
  }
 }
}

 Start Patch를 눌러서 패치를 시작하는데 패치 서버 url과 패치 파일이 있는 게임별 루트 디렉터리를 설정해서 넘겨줍니다. 이건 패치 시스템 만드는 분에 따라 다를테니 별로 중요하지 않고 코루틴을 사용하는데 중요한 것은 yield return StartCoroutine이 아니고 그냥 StartCoroutine을 썼다는거죠. 

 제가 만든 패치메니져 컴포넌트를 사용하는 다른 개발자분 입장에서는 패치 진행률 같은 이슈처리가 쉬워야합니다. 코루틴 개념 설명 링크들을 보셔도 아시겠지만, StartCoroutine으로 해줘야 패치메니져 내부에서 진행중 첫 yield return null; 같은 문을 만나서 클라이언트에게 스크립트 실행을 양보하게 되는거죠. 다음부터 클라이언트에서는 UI 갱신이나 패치 완료등을 확인하기위해 패치메니져로부터 진행률을 받아서 갱신하면 끝인거죠. cocos2d-x에서 패치할 때는 쓰레드가 안되서 페이크 쓰레드로 처리했었는데 유니티3D는 코루틴으로 하니 좋네요.


2. 패치 프로세스

 패치가 시작되서 패치 정보 파일을 받아 패치를 진행해야할지 버전을 비교하고 진행한다면 현재버전부터 최신버전까지의 업데이트 정보를 파일을 받아 패치 파일 목록을 만들고 실제로 에셋 번들을 내려받고, 마지막으로 PlayerPrefs에 패치 정보를 저장해야겠죠. 물론 패치 진행률도 갱신해주고요. 아래는 이와 관련된 풀 소스는 아니고 딱 위에 나열한 부분을 확인할 수 있는 StartPatch만 공개해봅니다.

public IEnumerator StartPatch(string strIp, string strRootDir) {
  this.strServerUrl = strIp + "/" + strRootDir + "/";
  AssetBundleManager.Instance.Init(this.strServerUrl);
  Debug.Log ("StartPatch " + this.strServerUrl);
 
  string strUpdateInfoUrl = this.strServerUrl + this.UpdateStr + "/" + this.LastUpdateFile;
  // 최신 패치 버전 정보를 다운로드 한다. 다운할 동안 다른 스크립트에 양보.
  yield return StartCoroutine(DownloadUpdateInfo(strUpdateInfoUrl));
  // 받은 파일은 XML로 되어있다. 패치 서버 버전정보만 딸랑 들어있다.
  this.iServerVer = GetServerVer();
  // 현재 클라이언트에 적용된 패치 버전
  int iLocalVer = PlayerPrefs.GetInt(this.CurPatchVerKey, 0);
  Debug.Log ("Patch Current Ver - " + iLocalVer.ToString());
 
  if (IsPatchProcessing(iLocalVer) == false) {
   this.bPatchEnd = true;
   UpdatePatchProcessRate(100, 100);
   Debug.Log ("Patch End. Same Update Version.");
   //같은 버전이면 이 코루틴을 빠져나간다.
   yield break;
  }
  //서버와 차이나는 업데이트 정보(xml)을 다운받아 패치 목록을 만든다.
  yield return StartCoroutine(ProcessUpdateInfo(iLocalVer));
 
  int iTotalPatchFiles = this.dicMakePatchFile.Count;
  if (iTotalPatchFiles == 0) {
   //패치 목록을 만들었는데 패치 갯수가 0이면 이상하자나?
   Debug.LogError("Patch Ver Diff. But Patch File Count 0 ?");
   yield break;
  }
 
  Debug.Log("MakePatchList End. Start Download AssetBundle");
  int iCurPatchNum = 0;
 
  foreach (KeyValuePair<string int=""> pair in this.dicMakePatchFile) {
   //에셋 번들을 다운받는다.
   yield return StartCoroutine(AssetBundleManager.Instance.Download(pair.Key, pair.Value));
 
   GameDataStringValue gameData = new GameDataStringValue();
   gameData.Key = pair.Key;
   gameData.Value = pair.Value.ToString();
   //최신의 패치된 에셋 번들 정보를 스트림으로 PlayerPrefs에 저장하기 전 일단 갱신한다.
   GameDataManager.Instance.UpdateGameData(GameDataManager.AssetBundleGameDataKeyName, ref gameData);
   // 패치 진행률 갱신
   UpdatePatchProcessRate(++iCurPatchNum, iTotalPatchFiles);
  }
  // 마지막 패치한 버전과 에셋 번들의 정보(이름과, 버전)을 PlayerPrefs에 저장한다.
  EndPatch();
 }

 설명은 주석으로도 충분할 듯 합니다. 내부 구현은 개인 프로젝트도 아니라서 공개할 수 없으니 양해바랍니다 ^^; 패치메니져 내부는 뭔가를 다운받거나하면 아까와는 다르게 전부 yield return StartCoroutine를 사용합니다. 위에서 AssetBundleManager와 GameDataManager에 대한 것은 따로 다른 포스팅에서 정리를 해볼까합니다.

 샘플용으로 만들었던 패치 파일들 스샷입니다. 위에것이 패치 툴로 생성한 서버의 패치 버전 정보이고 밑에것이 그 버전에 따른 에셋 번들 패치 정보들입니다. 지금은 단순 테스트 상태라 버전도 1이고 파일도 딸랑 1개뿐이죠. 하지만 테스트는 여러 파일과 여러버전으로 해본 상태입니다.


3. 최적화 이슈?

 중요한 것은 모바일 게임이다보니 다운받아야할 정보들을 최소화, 최적화 해야한다는 것이죠. 지금은 Name에 testAssetBundle과 같이 이름만 있는데 전 버전에서는 .unity 확장자까지 넣었더랬죠. 단 1바이트라도 최적화 중요하겠죠?

 급 생각난건데 더 최적화를 한다면 에셋 번들 이름 옆에 구분자를 줘서 버전을 붙이는 거죠. testAssetBundle.1 이나 testAssetBundle:1 이렇게 말이죠. 패치메니져에서는 파싱해주면 되겠구요.


4. 마무리

 이렇게해서 에센 번들을 코루틴을 사용해서 패치하는 과정을 정리해봤습니다. 참고로 제가 정리한 내용이 완벽한 해결책?은 아닙니다. 프로그래밍이라는게 그렇죠. 해결책은 많다는거. 그 중 하나의 해법일 뿐이죠.

 다음 포스팅은 언급했던 AssetBundleManager와 GameDataManager에 대한 것을 정리해볼까합니다. AssetBundleManager는 에셋 번들을 다운로드 받는 것 정도가 될 것이고, GameDataManager는 PlayerPrefs에 객체를 스트림으로 저장하는 내용이 될 듯합니다.

 추후에는 C#으로 만든 패치 툴을 아예 유니티툴에 통합을 할 예정인데 그때도 이슈가 있으면 정리를 해보겠습니다.

댓글

이 블로그의 인기 게시물

라즈베리파이 라즈비안 한글 입력하기 - IBus

'xxx.exe' 프로그램을 시작할 수 없습니다. 지정된 파일을 찾을 수 없습니다.

라즈베리파이 라즈비안 한글 깨짐 수정하기