카테고리 보관물: unity

하이퍼 캐주얼 게임 “Hoppy Japan”을 Unity에서 1 개월로 만든 이야기

unity UnityAdventCalendar2018

시작하기

f : id : takashicompany : 20181203102625p : plain

안녕하세요! Unity 엔지니어의 사토 입니다.
이 기사는 카약 Unity 강림절 달력 2018 4 일째의 기사입니다.
올해 10 월에 출시 한 KAYAC의 신작 하이퍼 캐주얼 게임 “Hoppy Japan」의 개발에 대해 이야기합니다.

AppStore
Google Play

Hoppy Japan은

“Hoppy Japan”은 당사의 소셜 게임 사업부의 “천하 제일 기획위원회 ‘라는 제도에서 태어난 게임입니다.

f : id : takashicompany : 20181203102932p : plain

Hoppy Japan은 쉽게 속도감과 고양감을 체험 할 수있는 원 터치 액션 게임입니다.


Hoppy Japan

개발 멤버는

  • 프로듀서
  • 디쿠레타
  • 플래너 / Unity 엔지니어 (나)
  • 3D 아티스트 / 디자이너
  • 사운드 아티스트

5 명입니다.

제품 판의 제작은 1 개월에서 실시했습니다.

기술적 인 개념

  • 전 세계적으로 배달을 상정하고 있기 때문에 저사양 단말에서 작동하도록
  • 단기간에 확실하게 개발을 추진하기 위해 간단한 구조와 설계를 의식하는
  • 아티스트가 Unity 작업 · 확인을 할 수 있도록

라는 것을 염두에 두었습니다.

배경 객체 재사용

“Hoppy Japan」은 거리를 즐기는 게임입니다.
캐릭터가 진행됨에 따라 “배경 오브젝트를 배치 · 후방에서 보이지 않게 된 배경 내용 지우기 ‘라는 구현을하고 있습니다.
그 때 Instantiate 함수와 Destroy 함수를 사용하면 CPU 처리에 부하가 걸려 카쿠 붙는 원인이 게임의 경험을 해쳐 버립니다.

f : id : takashicompany : 20181203102654p : plain

위의 그림과 같이 카메라에서 얻을 수 없게 된 도시의 3D 모델을 Destory하지 않고 앞으로 다시 재정렬하는 구조로되어 있습니다.

f : id : takashicompany : 20181203102843g : plain

위의 그림과 같이 캐릭터가 진행에 맞추어 숨겨진 화와 배치 (재사용)을 순서대로 실행합니다.

장면을 분할 / 대량로드

Hoppy Japan에서는 타이틀 화면 게임 플레이 화면 · 결과 화면 스킨 목록 화면의 4 개의 장면으로 게임을 구성하고 있습니다.

하나의 장면 파일로도 만들 수있을 것 컴팩트 한 게임이지만, 장면을 분할하여

  • 기능의 분리가 가능한
  • 기능의 확인을 혼자있다
  • 여러 사람이 작업 할 때 변경 부딪치지

등을 실현하는 목적이 있습니다.

f : id : takashicompany : 20181203102829p : plain

또한, 응용 프로그램 시작시 모든 장면을로드하고 원활하게 놀 수 있도록 노력했습니다. 

이것은 “Hoppy Japan”의 총 메모리 사용량이 약 60MB 였기 때문에 채용 할 수 있었다 …라는 측면도 있습니다.

만든 것을 확인하고 조정하는 사이클을 빨리하기 위해 각 장면은 단독으로 편집기 재생해도 작동하도록했습니다.

자신의 타임 스케일

이 게임은 “슈퍼 슬로우 ‘라는 기능이 포함되어 있습니다.
길게을하면 캐릭터의 이동 속도가 느린되어 착지점이 목적 쉬워진다 …라는 것입니다.
Unity는 Time.timeScale는 게임 전체의 속도를 조절하는 기능이 있지만, 이번에는 사용하지 않았습니다.
“캐릭터 이동과 이동 만 발생하고 UI는 보통의 속도로 움직이게」처럼 요소마다 조정할 수있는 것이 유연하게 대응할 수 있다고 생각했기 때문입니다.
“Hoppy Japan」에서는 Time.timeScale는 사용하지 않고 자신의 timeScale을 정의하고 애니메이션 인스턴스 ( DOTween )과 Animator 파티클 등 슈퍼 슬로우를 적용 할 요소에 대해 동기화 처리를 썼습니다.

void Update ()
{
    // GameManager.timeScale라는 변수를 정의하여 동기화 할 애니메이션에 적용
    
    // Animator의 경우
    _animator.speed = _gameManager.timeScale;

    // DOTween의 Sequence 인스턴스의 경우
    _doTweenSequence.timeScale = _gameManager.timeScale;

}

void LateUpdate ()
{
    // ParticleSystem에만 LateUpdate 동기화시킨다 (Update이라고 움직임이 거칠어 때문에)
    _particleSystem.Simulate (
        Time.deltaTime * _gameManager.timeScale,
        true ,
         false
    );
}

아티스트와의 연계

아티스트가 스스로 만든 아트를 게임에 통합 확인할 수 있도록 여러 가지 궁리를했습니다.

우선, 「Header 속성에서 예술의 반영 방법을 적는다 ‘라는 것입니다.
Header 속성은 직렬화 변수로 정의함으로써 Unity의 Inspector 뷰에 텍스트를 표시 할 수 있습니다.
주로 Inspectr 뷰에서의 변수의 설명에 사용됩니다.

[Header ( "예술을 게임에 통합 단계" )]
[Header ( "1. 만든 Block에 GameBlock 구성 요소를 설정" )]
[Header ( "2. StageCheckScene 열기" )]
[Header ( "3. GameStage 구성 요소의 조립식 수정" )]
[Header ( "- 필드를 수정하려면 Field 계층 다음과 같이 설정" )]
[Header ( "- 단계를 추가하려면 StepPool에 조립식 추가" )]
[Header ( "- 블록을 추가하는 경우 BlockPools에 조립식 추가" )]
[Header ( "4. Stage 조립식 저장하면"StageBundleData "를 검색" )
[Header ( "5. StageBundleData에 조립식를 설정하면 SaveProject과 장면 저장" )
[Header ( "6. GameScene을 재생하면 게임에 등장하는" )]
f : id : takashicompany : 20181203102629p : plain

다소 힘 기술 느낌이지만, 프로그래머는 별도 wiki 등을 준비하지 않고 설명을 쓸 때문에 쉽게 아닐까 생각합니다.

또한 개발 초기 장면의 객체에 Inspector에서 만든 아트를 설정 준다고하는 방식 이었지만 장면 파일을 서로 만지는 경우 충돌이 발생하게되므로 ScriptableObject을 사용하여 아티스트가 쉽게 아트를 설정할 수 있도록했습니다.

f : id : takashicompany : 20181203102639p : plain

성능 조정

일부 단말기는 GPU의 부하가 높고 FPS의 저하를 초래했기 때문에 특정 조건을 만족하는 단말 (FPS가 일정 수를 연속 밑돌아 계속 경우 등)에서는 해상도를 조정하여 묘화 부하를 경감하는 방법을 취했습니다.

Screen.SetResolution (Screen.width / 2, Screen.height / 2, true);

이상과 같이하여 “Hoppy Japan ‘은 만들어졌습니다.
하이퍼 캐주얼 게임은 Unity와 궁합이 잘 기존의 기능을 적절하게 사용만으로도 좋은 게임을 만들 수 있습니다.
꼭 도전 해보세요!

내일은 미야 유사 “Bluetooth에서 Unity와 연결하는 컨트롤러를 ESP32 마이크로 컴퓨터로 만드는”입니다!

[번역] 에셋번들 사용 패턴

이 글은 유니티 튜토리얼을 번역한 글 입니다. 원문은 여기에서 확인하실 수 있습니다.

이 글은 총 5개의 챕터로 구성되며 아래와 같습니다.

  1. [번역] 에셋번들과 리소스에 대한 가이드
  2. [번역] 에셋과 오브젝트, 그리고 직렬화
  3. [번역] RESOURCES 폴더
  4. [번역] 에셋번들 기초
  5. [번역] 에셋번들 사용 패턴

이 글은 [번역] 에셋번들과 리소스에 대한 가이드 시리즈의 5번째 챕터입니다.

이 시리즈의 이전 챕터에서는 에셋번들 기초에 대해 다뤘는데, 특히 다양한 로딩 API의 저수준 동작들에 대해 이야기했습니다. 이번 챕터에는 에셋번들을 실제로 사용하는 방법에 있어서 발생하는 문제와 해결법에 대해 이야기할 것 입니다.

4.1 로드된 에셋의 관리

메모리에 민감한 환경에서 로드된 오브젝트의 크기와 갯수를 조심스레 다루는 것은 특히 중대한 문제입니다. 유니티는 현재 씬에서 오브젝트가 제거되더라도 자동으로 해당 오브젝트를 언로드하지 않습니다. 에셋의 정리는 특정 시간에 발생하게 되고, 수동으로도 발생시킬 수 있습니다.

에셋번들 자체는 조심스럽게 다뤄져야 합니다. 로컬 저장 공간에 있는(유니티 캐쉬에 있거나AssetBundle.LoadFromFile에 의해 로드된 것) 파일의 에셋번들은 최소한의 메모리 오버헤드가 있는데, 거의 10~40 KB 를 넘지 않습니다. 이러한 숫자도 에셋번들이 많은 경우에는 문제가 될 수 있습니다.

대부분의 프로젝트에서 유저들은 컨텐츠를 다시 경험할 수 있기 때문에(특정 레벨을 다시 플레이하는 등), 에셋번들을 언제 로드하고 언로드하는지를 아는 것이 중요합니다. 만약 에셋번들이 부적절하게 언로드된다면, 메모리에서 오브젝트가 중복되는 문제를 야기시킬 수 있습니다. 부적절한 에셋번들의 언로드는 특정 환경에서 바람직하지 못한 동작을 할 수도 있는데, 예를 들면 텍스쳐가 없는 상태(missing)가 될 수도 있다는 말입니다. 왜 이러한 일이 일어나는지에 대해 알고 싶으면, 에셋과 오브젝트, 그리고 직렬화 챕터의 오브젝트 내부의 참조들 섹션을 참고해 보십시오.

에셋과 에셋번들을 관리할 때 가장 중요한 것은 AssetBundle.Unload 를 true나 false 인자를 사용해서 호출할 때의 동작의 차이를 이해해야 한다는 것 입니다.

이 API는 호출된 에셋번들의 헤더 정보를 언로드합니다. 여기에 사용되는 매개변수는 이 에셋번들의 모든 생성된 오브젝트를 같이 언로드할 것인지를 가리킵니다. true인 경우에 그 에셋번들에서 만들어진 모든 객체들 또한 즉시 언로드 됩니다 – 그 객체들이 현재 씬에서 사용되고 있더라도 말이죠.

예를 들어, 매터리얼 M이 에셋번들 AB에서 로드 되었고, 매터리얼 M이 현재 씬에서 사용되고 있다고 가정해보겠습니다.

그림 1


만약 AB.Unload(true)가 호출되면, M이 씬에서 제거되고, 파괴되며 언로드되게 됩니다. 하지만, 만약 AB.Unload(false)가 호출되면, AB의 헤더만 언로드되고 M은 여전히 씬에 남아서 동작할 것 입니다. AssetBundle.Unload(false)를 호출하는 것은 M과 AB 사이의 링크를 깨트립니다. 만약 AB가 그 후에 다시 로드되면, AB에 포함되어 있는 오브젝트의 새로운 복제본이 메모리에 올라가게 됩니다.

그림 2


만약 AB가 그 후에 다시 로드되게 된다면, 에셋번들의 헤더 정보가 새로 복제되어 다시 로드되게 됩니다. 그런데, M은 새로운 AB의 복제본이 아닌 이전 AB에서 생성된 것 입니다. 유니티는 AB의 새로운 복제본과 M 사이의 링크를 형성해주지 않습니다.

그림 3


만약 M을 다시 로드하기 위해 AB.LoadAsset()을 호출하면, 유니티는 이전에 생성한 M을 AB의 인스턴스라고 해석하지 않습니다. 따라서, 유니티는 M에 대한 새로운 복제본을 생성하게 되고, 씬에는 똑같은 2개의 M에 대한 복제본이 존재하게 되는 것 입니다.

그림 4


대부분의 프로젝트에서, 이러한 동작은 바람직하지 못 합니다. 대부분의 프로젝트에서는 AssetBundle.Unload (true)를 사용해야하고, 오브젝트가 중복되지 않을 수 있는 방법을 채택해야 합니다. 흔한 두 가지 방법은 다음과 같습니다 :

  1. 레벨 사이나 로딩 화면처럼 애플리케이션의 생명 주기 동안 일시적 에셋번들이 언로드되는 지점을 체크하는 것 입니다. 이는 간단하고 가장 흔한 옵션입니다.
  2. 각 오브젝트의 참조 개수(reference-count)를 유지하고, 에셋번들은 그 오브젝트들이 하나도 사용되지 않을 때에만 언로드시킵니다. 이는 애플리케이션이 메모리의 중복없이 각 오브젝트를 로드하고 언로드할 수 있게 해줍니다.

만약 애플리케이션이 AssetBundle.Unload (false)를 사용해야만 한다면, 각 오브젝트는 다음의 두 가지 방법으로만 언로드될 수 있습니다 :

  1. 씬과 코드에서 원치않는 오브젝트에 대한 모든 참조를 제거하십시오. 이 작업이 끝나고 나면 Resources.UnloadUnusedAssets을 호출하십시오.
  2. 씬을 additive 방식이 아닌 방식으로 로드하십시오. 이는 현재 씬의 모든 오브젝트를 파괴시키고 Resources.UnloadUnusedAssets를 자동으로 호출합니다.

만약 프로젝트에 오브젝트를 로드하고 언로드하기 위해 유저를 기다리게 할 수 있는 잘 정의된 지점이 있다면(예를 들어 게임 모드나 레벨 사이), 이러한 지점들은 가능한 많은 오브젝트를 언로드하고 새로운 오브젝트를 로드하도록 사용되어야 합니다.

이렇게 하기 위한 가장 간단한 방법은 프로젝트의 불연속적인 덩어리들을 씬에 넣고, 이 씬과 연결된 모든 참조들을 에셋번들로 빌드하는 것 입니다. 그리고나서 애플리케이션은 “로딩” 씬으로 들어가서 이전 씬에 있던 에셋번들을 모두 언로드하고, 새로운 씬의 에셋번들을 로드합니다.

이 방식이 가장 간단한 방법이지만, 일부 프로젝트는 좀 더 복잡한 에셋번들 관리가 필요합니다. 통괄적으로(universal) 쓰일 수 있는 에셋번들 디자인 패턴은 없습니다. 각 프로젝트의 데이터는 다릅니다. 에셋번들에 오브젝트를 어떻게 그룹화해서 넣을지를 결정할 때, 동시에 로드되거나 업데이트 되어야하는 오브젝트들을 에셋번들에 넣는 것이 일반적으로 최선입니다.

예를 들어, RPG(role-playing game)를 생각해보자. 각 맵과 컷씬은 에셋번들로 그룹화될 수 있는데, 일부 오브젝트는 대부분의 씬에서 필요할 것 입니다. 에셋번들은 초상화, 인게임 UI, 다른 캐릭터 모델과 텍스쳐를 제공하기 위해 만들어 질 수 있습니다. 이런 오브젝트과 에셋들은 에셋번들의 두번 째 셋으로 그룹화될 수 있고, 이는 시작 시에 로드되고 앱의 생명주기 동안 로드된 채 남아 있습니다.

또 다른 문제는 에셋번들이 언로드 된 후에 다시 그 에셋번들로부터 오브젝트를 다시 로드하려고 하는 경우에 발생할 수 있습니다. 이러한 경우에, 다시 로드하는 것은 실패하게 되고 그 오브젝트는 유니티 에디터의 hierarchy에서 (Missing) 오브젝트 상태로 보이게 됩니다.

이는 주로 유니티가 그래픽 컨텍스트에 대한 제어를 잃었다가 다시 얻었을 때 발생하는데, 모바일 앱이 중지 되거나 유저가 PC를 잠금 상태로 변환했을 때가 이에 해당합니다. 이러한 경우에, 유니티는 텍스쳐와 셰이더를 GPU에 다시 업로드해야 합니다. 만약 이런 에셋을 위한 소스 에셋번들이 사용 불가능한 상태라면, 애플리케이션은 씬의 오브젝트를 “missing shader” 상태인 magenta 색상으로 그리게 됩니다.

4.2 배포

프로젝트의 에셋번들을 고객에게 배포하는 기본적인 2가지 방법이 있습니다 : 프로젝트와 동시에 설치하는 것 또는 설치 후에 다운로드하는 방법입니다. 에셋번들을 설치에 포함 시킬 지 아니면 설치 후에 전달할 지에 대한 결정은 프로젝트가 구동할 플랫폼의 수용성(capabilities)과 제한(restrictions)에 따르게 됩니다. 모바일 프로젝트는 주로 설치 후에 다운로드 하는 방식을 선택하는데, 이는 초기 설치 크기를 줄이기 위함과 무선 다운로드 크기 제한1을 맞추기 위함입니다. 콘솔과 PC 프로젝트는 일반적으로 에셋번들을 초기 설치에 포함시킵니다.

적절한 아키텍쳐는 초기에 에셋번들을 어떤 형태로 전달했는지에 상관없이 설치 후에 새로운 컨텐츠나 수정된 컨텐츠를 패치할 수 있도록 해줍니다. 여기에 대한 더 자세한 내용을 알고 싶으면, 이 글의 에셋번들로 패치하기 섹션을 살펴보세요.

4.2.1. 프로젝트와 같이 전달

에셋번들을 프로젝트에 같이 담아서 전달하는 것이 가장 간단한 배포 방법입니다. 왜냐하면 어떤 추가적인 다운로드 관리 코드가 필요없기 때문이죠. 설치 시에 왜 에셋번들을 포함시켜야 하는 지에 대한 2가지 주요한 이유는 다음과 같습니다 :

  • 프로젝트의 빌드 시간을 줄이고 더 간단한 반복 개발을 가능하게 하기 위함입니다. 만약 에셋번들이 애플리케이션으로 부터 독립적으로 업데이트 될 필요가 없다면, 그 에셋번들은 Streaming Assets으로 저장함으로써 애플리케이션에 포함될 수 있습니다. 아래의 Streaming Assets 섹션을 보세요.
  • 업데이트 가능한 컨텐츠의 초기 버전을 같이 전달하기 위함입니다. 이는 최초 설치 후 유저의 시간을 절약하기 위해서, 또는 추후의 패치를 위한 기본을 제공하기 위해서 주로 행해집니다. Streaming Assets은 이 경우에는 이상적인 방법이 아닙니다. 하지만, 커스텀 다운로드 코드와 캐시 시스템을 작성하는 것이 선택 사항이 아니라면, 업데이트 가능한 컨텐츠의 최초 버전은 Streaming Assets로부터 유니티 캐쉬에 로드될 수 있습니다.

4.2.1.1. Streaming Assets

설치 시에 유니티 애플리케이션에 어떤 형태의 컨텐츠를 포함시키는 가장 쉬운 방법은 해당 컨텐츠를 빌드 전에 /Assets/StreamingAssets/ 폴더에 넣는 것 입니다. 빌드 시에 StreamingAssets 폴더에 포함되어 있는 모든 것들은 최종 애플리케이션에 복사되어 질 것 입니다. 이 폴더는 최종 애플리케이션에 에셋번들 뿐만 아니라 어떤 종류의 컨텐츠도 저장해서 쓸 수 있습니다.

로컬 저장 공간에 있는 StreamingAssets 폴더의 전체 경로는 런타임에 Application.streamingAssetsPath 속성을 이용해서 접근할 수 있습니다. 그렇게해서 대부분의 플랫폼에서 AssetBundle.LoadFromFile을 통해 에셋번들을 로드할 수 있습니다.

안드로이드 개발자 : 안드로이드에서는, 에셋번들이 압축되어 있더라도, Application.streamingAssetsPath는 압축된 .jar 파일을 가리키게 됩니다. 이런 경우에, 각 에셋번들을 로드하기 위해 WWW.LoadFromCacheOrDownload가 사용되어야 합니다. .jar 파일의 압축을 푼 뒤에 로컬 저장소에 있는 읽기 가능한 위치로 에셋번들을 추출하는 커스텀 코드를 작성하는 것 또한 가능합니다.

메모 : Streaming Assets은 일부 플랫폼에서 쓰기 가능한 위치가 아닙니다. 만약 프로젝트의 에셋번들이 설치 후에 업데이트 되어야 한다면, WWW.LoadFromCacheOrDownload를 쓰거나 커스텀 다운로더를 작성해야 합니다. 더 자세한 내용을 알고 싶다면 커스텀 다운로더 – 저장공간 섹션을 읽어 주세요.

4.2.2. 설치 후 다운로드(Downloaded post-install)

모바일 장치에 에셋번들을 전달할 때 많이 사용되는 방법은 앱 설치 후 번들을 다운로드하는 방법입니다. 이는 또한 설치 후에 전체 애플리케이션을 다시 다운로드할 필요 없이, 새로운 컨텐츠나 정제된 컨텐츠를 업데이트할 수 있도록 해줍니다. 모바일 플랫폼에서는, 애플리케이션 이진파일은 비싸고 긴 재인증 프로세스를 거쳐야만 합니다. 따라서, 설치 후 다운로드를 위한 좋은 시스템을 개발하는 것은 매우 중요합니다.

에셋번들을 전달하는 가장 간단한 방법은 웹서버에 그 번들을 두고 WWW.LoadFromCacheOrDownload나 UnityWebRequest를 통해서 전달하는 것 입니다. 유니티는 다운로드된 에셋번들을 로컬 저장 공간에 자동으로 저장합니다. 만약 다운로드한 에셋번들이 LZMA로 압축되어 있다면, 추후에 빠르게 로딩하기 위해 압축을 해제한 상태로 저장되게 됩니다. 만약 다운로드한 번들이 LZ4로 압축되어 있다면, 압축된 상태 그대로 저장되게 됩니다.

캐쉬가 가득차게 되면, 유니티는 가장 오래 전에 사용된 에셋번들을 캐쉬에서 지우게 됩니다. 더 자세한 내용은 내장 캐쉬 섹션에서 보세요.

WWW.LoadFromCacheOrDownload에는 결함이 있다는 것을 알아두세요. 에셋번들의 로드 섹션에 설명했듯이, WWW 오브젝트는 에셋번들을 다운로드하는 동안 동일한 크기의 메모리를 소비하게 됩니다. 이는 수용할 수 없는 메모리의 급증을 유발할 수 있습니다. 이를 피하기 위한 3가지 방법은 다음과 같습니다 :

  • 에셋번들의 크기를 작게 유지하세요. 에셋번들의 최대 크기는 번들이 다운로드될 때 프로젝트의 메모리 예산에 맞게 결정될 것 입니다. “downloading” 화면이 있는 애플리케이션은 백그라운드에서 에셋번들을 스트리밍하는 애플리케이션보다 보통은 더 많은 메모리를 할당합니다.
  • 유니티 5.3 이상에서는, UnityWebRequest API의 DownloadHandlerAssetBundle을 쓰도록 변경하세요. 이는 다운로드 중에 메모리의 급증을 야기하지 않습니다.
  • 커스텀 다운로더를 작성하세요. 더 자세한 정보를 원한다면, 커스텀 다운로더 섹션을 보세요.

가능하다면 UnityWebRequest를 써서 시작하는 것을 일반적으로 추천드리지만, 유니티 5.2 이하에서는 WWW.LoadFromCacheOrDownload를 추천드립니다. 유니티 기본 시스템 API의 메모리 소비, 캐쉬 동작, 성능이 특정 프로젝트에서 수용할 수 없을 정도이거나, 어떤 요구사항을 만족하기 위해 플랫폼 종속적인 코드를 실행시켜야만 하는 프로젝트의 경우에만 커스텀 다운로드 시스템에 투자를 하세요.

UnityWebRequest나 WWW.LoadFromCacheOrDownload의 사용을 막을 수도 있는 상황에 대한 예 입니다 :

  • 에셋번들 캐쉬에 대한 아주 세밀한 조절이 필요한 경우
  • 프로젝트에서 커스텀 압축 전략을 구현해야하는 경우
  • 비활성화 상태에서 데이터를 스트리밍하는 등의 특별한 요구사항을 만족하기 위해 플랫폼 제한적인 코드를 사용해야하는 경우
    • 예 : 백그라운드 상태일 때 데이터를 다운로드하기 위해 iOS의 Background Tasks API를 사용하는 경우 PC처럼 유니티의 적절한 SSL 지원이 없는 플랫폼에 SSL로 에셋번들을 전송해야하는 경우

4.2.3. 내장 캐쉬

유니티는 내장 에셋번들 캐쉬 시스템이 있는데, 이 시스템은 WWW.LoadFromCacheOrDownload나 UnityWebRequest API를 통해 다운로드 된 에셋번들을 캐쉬하는데에 사용될 수 있습니다.

두 API는 모두 매개변수로 에셋번들의 버전 번호를 받는 오버로드 메서드를 가지고 있습니다. 이 번호는 에셋번들의 내부에 저장되어 있지 않고, 에셋번들 시스템에 의해 생성되지도 않습니다.

캐쉬 시스템은 WWW.LoadFromCacheOrDownload나 UnityWebRequest로 전달된 가장 최근의 버전 번호를 유지하고 있습니다. 둘 중 어떤 API가 버전 번호를 넘겨받아서 호출되면, 캐쉬 시스템은 캐쉬된 에셋번들이 있는지 체크합니다. 만약 있다면, 에셋번들이 처음 캐쉬된 경우에 전달된 버전 번호와 현재 호출에 사용된 버전 번호를 비교해 봅니다. 이 번호가 같다면, 시스템은 캐쉬된 에셋번들을 로드하게 됩니다. 만약 번호가 다르다면, 캐쉬된 에셋번들이 없음을 의미하고, 유니티는 새로운 복사본을 다운로드하게 됩니다. 이 새로운 복사본은 새로운 버전 번호와 관계가 맺어집니다.

캐쉬 시스템에 있는 에셋번들은 파일명으로만 식별되고, 다운로드되는 전체 url과는 상관없습니다. 이는 같은 파일명의 에셋번들이 여러 다른 곳에 저장되어도 괜찮다는 의미입니다. 예를 들어, 하나의 에셋번들이 한 Content Delivery Network(CDN)의 여러 서버에 위치할 수 있습니다. 파일명이 같은 한, 캐쉬 시스템은 이들을 같은 에셋번들로 취급합니다.

에셋번들에 버전 번호를 할당하는 적절한 전략을 결정하고, 이 버전 번호를 WWW.LoadFromCacheOrDownload에 전달하는 것은 개별 애플리케이션에 달려있는 부분입니다. 대부분의 애플리케이션은 유니티 5의 AssetBundleManifest API를 사용할 수 있습니다. 이 API는 에셋번들의 컨텐츠에 대한 MD5 해쉬를 계산해서 각 에셋번들의 버전 번호를 생성할 수 있습니다. 에셋번들이 바뀔 때마다, 해쉬 값 또한 변하게 되고, 이는 에셋번들을 다운로드해야 함을 가리킵니다.

메모 : 유니티 내장 캐쉬 구현의 변덕(quirk)2 때문에, 오래된 에셋번들은 캐쉬가 가득 찰 때까지는 지워지지 않습니다. 유니티는 추후 릴리즈에서 이런 변덕에 대해 언급할 예정입니다.

더 자세한 내용을 알고 싶으면 에셋번들로 패치하기 섹션을 보십시오.

유니티의 내장 캐쉬는 캐쉬 객체에 있는 API를 호출함으로써 조절할 수 있습니다. 유니티 캐쉬의 동작은 Caching.expirationDelay와 Caching.maximumAvailableDiskSpace를 변경함으로써 조절할 수 있습니다.

Caching.expirationDelay는 에셋번들이 자동으로 삭제되기 위해 필요한 최소한의 시간(초 단위)입니다. 만약 에셋번들이 이 시간 동안 접근되지 않았다면, 자동으로 삭제될 것 입니다.

Caching.maximumAvailableDiskSpace 은 expirationDelay보다 덜 최근에 사용된 에셋번들을 삭제하기 시작하기 전에 캐쉬가 사용할 수 있는 로컬 저장 공간의 크기를 결정합니다. 여기에는 바이트 단위를 사용합니다. 한계 용량에 도달하게 되면, 유니티는 캐쉬 중에서 최근에 가장 적게 열었던 에셋번들(또는 Caching.MarkAsUsed를 통해 사용됐다고 표시된 에셋번들)을 삭제하게 됩니다. 유니티는 새로운 다운로드를 완료하기에 충분한 용량이 되기 전까지 캐쉬된 에셋번들을 삭제할 것 입니다.

메모 : 유니티 5.3에서, 내장 유니티 캐쉬를 조절하는 것은 매우 힘든 일 입니다. 캐쉬에서 특정 에셋번들을 삭제하는 것은 불가능합니다. 삭제는 시간적인 만기에 의해서, 디스크 사용 공간의 초과에 의해서, 또는 Caching.CleanCache(Caching.CleanCache 는 현재 캐쉬에 있는 모든 에셋번들을 삭제할 것 입니다.)에 의해서 발생합니다. 이는 개발 중이거나 라이브 동작 중일 때 문제가 될 수 있는데, 유니티가 애플리케이션에서 더 이상 사용되지 않는 에셋번들을 자동으로 지워주지 않기 때문입니다.

4.2.3.1. Cache Priming

에셋번들은 파일명으로 식별되기 때문에, 애플리케이션에 같이 포함되는 에셋번들의 캐쉬를 가장 주요한 것으로 설정하는 것이 가능합니다. 이를 위해서, /Assets/StreamingAssets/에 있는 각 에셋번들의 최초 버전을 저장하십시오. 이 과정은 프로젝트와 같이 전달 섹션에 있는 상세 내용과 같습니다.

애플리케이션이 최초에 실행될 때 Application.streamingAssetsPath로부터 에셋번들을 로딩함으로써 캐쉬를 로드할 수 있습니다. 그 때 부터, 애플리케이션은 WWW.LoadFromCacheOrDownload나 UnityWebRequest를 일반적으로 호출할 수 있습니다.

4.2.4. 커스텀 다운로더

커스텀 다운로더를 작성하는 것은 어떻게 에셋번들이 다운로드되고, 압축 해제되고 저장되는 지에 대한 모든 제어를 애플리케이션에 줄 수 있습니다. 커스텀 다운로더를 작성하는 것은 야심찬 애플리케이션을 만드는 큰 팀에 한해서만 추천드립니다. 커스텀 다운로더를 작성할 때 생각해야 할 4가지 주요 문제에 대해 이야기 해보겠습니다 :

  • 에셋번들을 어떻게 다운로드할 것인가?
  • 에셋번들을 어디에 저장할 것인가?
  • 에셋번들을 어떻게 압축할 것인가?
  • 에셋번들을 어떻게 패치할 것인가?

에셋번들의 패치에 대한 정보를 원하시면, 에셋번들로 패치하기 섹션을 살펴보십시오.

4.2.4.1 다운로드

대부분의 애플리케이션에서, HTTP가 에셋번들을 다운로드하기에 가장 간단한 방법입니다. 하지만, HTTP 기반의 다운로더를 구현하는 것은 간단한 일이 아닙니다. 커스텀 다운로더는 메모리 초과 할당, 쓰레드 초과 사용, 쓰레드 초과 깨움(excessive thread wakeups)을 피해야만 합니다. 유니티의 WWW 클래스는 여기에서 철저히 설명하는 이유에 의해 부적절 합니다. WWW의 높은 메모리 비용 때문에, 유니티의 애플리케이션에서 WWW.LoadFromCacheOrDownload를 사용하지 않는다면 WWW 클래스는 피해야 합니다.

커스텀 다운로더를 작성할 때 3개의 옵션이 있습니다 :

4.2.4.1.1. C# 클래스

애플리케이션이 HTTPS/SSL 지원을 필요로 하지 않는다면, C#의 WebClient 클래스는 에셋번들을 다운로드하는데게 가장 간단한 메커니즘을 제공합니다. 초과 메모리 할당 없이, 비동기적으로 어떤 파일을 다운로드해서 로컬 저장 공간에 바로 넣는 것이 가능합니다.

WebClient를 이용해서 에셋번들을 다운로드하려면, 클래스의 인스턴스를 할당하고, 에셋번들을 다운로드하기 위한 URL과 목적지 경로를 전달해야 합니다. 요청의 매개변수를 통한 더 많은 제어가 필요한 경우에는 C#의 HttpWebRequest를 사용해서 다운로더를 작성하는 것이 가능합니다 :

  1. HttpWebResponse.GetResponseStream으로부터 바이트 스트림을 얻습니다.
  2. 스택에 고정 크기 바이트 버퍼를 할당합니다.
  3. 응답 스트림에서 읽어서 버퍼에 넣습니다.
  4. C#의 File.IO API나 어떤 다른 스트리밍 IO 스시템을 이용해서 버퍼를 디스크에 기록합니다.

플랫폼 메모 : iOS, 안드로이드, 윈도우즈 폰의 경우에만 유니티의 C# 런타임이 C# HTTP 클래스에 대한 HTTPS/SSL 지원을 합니다. PC에서는 C# 클래스를 통해서 HTTPS 서버에 접근하려고 하면 인증서 검증 에러(certificate validation errors)가 발생합니다.

4.2.4.1.2. 에셋 스토어 패키지

일부 에셋 스토어 패키지는 HTTP, HTTPS와 다른 프로토콜을 통해서 파일을 다운로드하도록 네이티브 코드 구현을 제공하고 있습니다. 유니티를 위한 커스텀 네이티브 코드 플러그인을 제작하기 전에 사용 가능한 에셋 스토어 패키지를 평가해보는 것을 추천드립니다.

4.2.4.1.3. 커스텀 네이티브 플러그인

커스텀 네이티브 플러그인을 작성하는 것은 유니티에서 데이터를 다운로드하기 위한 가장 시간이 많이 들고 가장 유연한 방법입니다. 높은 프로그래밍 시간과 높은 기술 위험도가 있기 때문에, 이 방법은 애플리케이션의 요구 사항을 만족할 다른 방법이 없는 경우에만 추천됩니다. 예를 들어, 유니티의 C# SSL 지원이 없는 윈도우즈, OSX, 리눅스 같은 플랫폼에서 애플리케이션이 SSL 통신을 해야만 하는 경우에, 커스텀 네이티브 플러그인이 필요할 수도 있습니다.

커스텀 네이티브 플러그인은 일반적으로 타겟 플랫폼의 네이티브 다운로드 API를 감싸게 됩니다. 예로써 iOS의 NSURLConnection과 안드로이드의 java.net.HttpURLConnection이 있습니다. 이런 API를 사용하기 위한 더 상세한 내용은 각 플랫폼의 네이티브 문서의 도움을 받으십시오.

4.2.4.2. 저장 공간

모든 플랫폼에서 Application.persistentDataPath는 애플리케이션이 실행되는 동안 유지되어야 하는 데이터를 저장하는데에 사용되어야 하는 쓰기 가능한 위치를 가리킵니다. 커스텀 다운로더를 작성할 때, 다운로드된 데이터를 저장하기 위해 Application.persistentDataPath의 하위 디렉토리를 사용할 것을 강력히 추천드립니다.

Application.streamingAssetPath는 쓰기 불가능하고 에셋번들 캐쉬로써는 나쁜 선택입니다. streamingAssetsPath의 위치에 대한 예는 다음과 같습니다 :

  • OSX : .app 패키지 안 ; 쓰기 불가능
  • Windows : 설치 디렉토리 안(예를 들어 Program Files) ; 일반적으로 쓰기 불가능
  • iOS : .ipa 패키지 안 ; 쓰기 불가능
  • 안드로이드 : 압축된 .jar 파일 ; 쓰기 불가능

4.3. 에셋 할당 전략

프로젝트의 에셋을 에셋번들로 어떻게 나눌 것인가에 대한 결정을 하는 것은 간단한 일이 아닙니다. 여기에서 지나치게 단순한 전략을 채용하는 유혹에 빠지기 쉬운데요, 예를 들면 모든 오브젝트를 하나의 에셋번들에 담는 것 처럼 말이죠. 그런데 이러한 해결책은 심각한 결함이 존재합니다 :

  • 너무 적은 에셋번들을 가지는 것은…
    • 런타임 메모리 사용량을 늘립니다.
    • 로딩 시간을 늘립니다.
    • 더 많은 다운로드를 필요로 합니다.
  • 너무 많은 에셋번들을 가지는 것은…
    • 빌드 시간을 늘립니다.
    • 개발이 복잡해 질 수 있습니다.
    • 전체 다운로드 시간이 늘어납니다.

중요 결정은 어떻게 에셋번들에 오브젝트를 그룹핑 하는가 입니다. 주요한 전략은 다음과 같습니다 :

  • 논리적인 실체(Logical entities)
  • 오브젝트 타입(Object Types)
  • 동시 컨텐츠(Concurrent content)

하나의 프로젝트에서 다른 카테고리의 컨텐츠를 위해 이러한 전략들을 섞어 쓸 수 있다는 것을 알아 두십시오. 예를 들어, 다른 플랫폼에 사용될 UI 요소들을 에셋번들에 같이 그룹핑하고, 인터렉티브 컨텐츠는 레벨이나 씬에 따라 그룹핑할 수 있습니다. 어떤 전략을 채용했는지와 상관없이, 따를 수 있는 가이드 라인이 여기에 있습니다 :

  • 거의 변화가 없는 오브젝트들과 자주 업데이트 되는 오브젝트들을 다른 에셋번들로 나누세요.
  • 동시에 로드될 것 같은 오브젝트들을 같이 그룹핑하세요.

예 : 모델과 여기에 사용되는 애니메이션과 텍스쳐

  • 만약 어떤 오브젝트가 여러 개의 다른 에셋번들에 있는 오브젝트들과 의존 관계에 있다면, 그 에셋을 독립된 에셋번들로 옮기세요.
    • 이상적으로, 자식 오브젝트를 부모 오브젝트와 같이 그룹핑하세요.
  • 만약 HD와 SD 버전의 텍스쳐처럼, 두 오브젝트가 동시에 로드될 것 같지 않다면, 이들을 분리된 에셋번들로 나누세요.
  • 만약 오브젝트가 같은 오브젝트인데 다른 임포터 셋팅을 하거나 데이터를 다르게 해서 다른 버전을 가지고 있는 경우라면, AssetBundle Variants를 대신 사용하는 것을 고려해보세요.

일단 위의 가이드라인을 지키고 나면, 특정 시점에 에셋번들의 컨텐츠의 50% 미만이 사용된다면 이 에셋번들을 나누는 것을 고려해 보세요. 또한 동시에 로드되는 작은 에셋번들(5~10개 미만)을 결합하는 것도 고려해 보세요.

4.3.1. 논리적인 실체로 그룹핑(Logical entity grouping)

논리적 실체로 그룹핑하는 방법은 프로젝트의 기능적인 부분에 기반하여 오브젝트를 그룹핑하는 전략입니다. 이 전략을 채용하면, 애플리케이션의 다른 부분은 다른 에셋번들로 분리되게 됩니다.

예 :

  • 하나의 UI 화면에 있는 텍스쳐와 레이아웃 데이터를 모두 같이 묶으세요.
  • 한 셋트의 캐릭터에 사용되는 텍스쳐, 모델, 애니메이션을 모두 같이 묶으세요.
  • 여러 레벨에 거쳐서 공유되는 텍스쳐와 모델을 모두 같이 묶으세요.

논리적 실체 그룹핑은 가장 흔한 에셋번들 전략이고, 이는 특히 다음과 같은 경우에 적합합니다 :

  • DLC3
  • 애플리케이션의 생명주기 내내 많은 곳에서 등장하는 실체들

예 :

  • 공통 캐릭터나 기본 UI 요소들
  • 플랫폼이나 성능 설정에 기반해서 단독으로 변하는 실체들

논리적 실체에 의해 에셋을 그룹핑하는 것의 장점은 변하지 않은 컨텐츠를 재 다운로드할 필요없이 쉽게 개별 실체를 업데이트할 수 있다는 것 입니다. 이러한 이유 때문에 이 전략이 특히 DLC에 맞다는 것 입니다. 이 전략은 또한 가장 메모리 효율적인 경향도 있는데, 애플리케이션이 현재 사용하는 실체에 대한 에셋번들만을 로드하기 때문입니다.

하지만, 이는 구현하기에 가장 다루기 어려운 전략입니다. 왜냐하면 에셋번들에 오브젝트를 할당하는 개발자가 정확히 어떻게, 그리고 언제 각 개별 오브젝트가 프로젝트에서 사용되는지에 익숙해야하만 하기 때문입니다.

4.3.2. 타입 그룹핑(Type Grouping)

타입 그룹핑은 가장 간단한 전략입니다. 이 전략을 쓰면, 비슷하거나 같은 타입의 오브젝트들은 같은 에셋번들에 위치하게 됩니다. 예를 들어, 다른 오디오 트랙을 에셋번들에 넣거나, 다른 언어 파일을 에셋번들에 넣는 식으로 말이죠.

이 전략이 간단하기는 하지만, 빌드 시간, 로드 시간, 업데이트라는 측면에서는 종종 가장 비효율적 입니다. 이는 로컬라이지이션 파일같이 작고 동시에 업데이트되는 파일에 가장 자주 쓰입니다.

4.3.3. 동시 컨텐츠 그룹핑(Concurrent content grouping)

동시 컨텐츠 그룹핑은 동시에 로드되고 사용되는 컨텐츠를 하나의 에셋번들에 넣는 전략입니다. 이 전략은 프로젝트의 컨텐츠가 매우 지역적인 경우(컨텐츠가 특정 위치나 시간 외에 거의 나타나지 않는 경우)에 가장 일반적으로 사용됩니다. 예로는 각 레벨마다 유일한 아트, 캐릭터, 사운드 이펙트를 사용하는 레벨 기반의 게임이 있을 수 있겠네요.

동시 컨텐츠 그룹핑을 수행하는 가장 흔한 방법은 씬에 기반한 에셋번들을 만드는 것인데, 각 씬에 기반한 에셋번들은 씬의 전체 또는 대부분의 의존성을 포함하고 있습니다.

컨텐츠가 강하게 지역적이지 않은 프로젝트에서, 그리고 애플리케이션의 생명주기 동안 컨텐츠가 나오는 위치가 바뀌는 경우에는 동시 컨텐츠 그룹핑은 논리적 실체 그룹핑으로 수렴하게 됩니다. 둘 다 에셋번들의 컨텐츠의 효율을 최대화하는데에 필수적인 전략입니다.

이러한 시나리오의 예로는 오픈 월드 게임이 있을 수 있습니다. 오픈 월드 게임에서 캐릭터는 랜덤하게 생성되고 월드 공간에 분포됩니다. 이러한 경우에, 어떤 캐릭터들이 동시에 나타날지 예측하기 쉽지 않고, 따라서 캐릭터들은 일반적으로 다른 전략을 이용해서 그룹핑 되어야 합니다.

4.4. 에셋번들로 패치하기

에셋번들로 패칭하는 것은 새로운 에셋번들을 다운로드하고 기존 것을 바꿔치는 것 정도로 간단합니다. 만약 애플리케이션의 캐쉬된 에셋번들을 관리하기 위해 WWW.LoadFromCacheOrDownload 나 UnityWebRequest 를 사용한다면, 이 API에 다른 버전 인자를 넘기면 됩니다. (자세한 내용을 알고 싶으시면 위의 링크를 보세요.)

패치 시스템에서 풀어야 할 더 어려운 문제는 어떤 에셋번들을 변경해야하는지 파악하는 것 입니다. 패치 시스템에는 두 정보 리스트가 필요합니다 :

  • 현재 다운로드된 에셋번들의 리스트와 이들의 버전 정보
  • 서버에 있는 에셋번들의 리스트와 이들의 버전 정보

패쳐(patcher)는 서버에 있는 에셋번들의 리스트를 다운로드하고 이미 있는 에셋번들 리스트와 비교해야 합니다. 없는 에셋번들이나, 버전 정보가 바뀐 에셋번들은 다시 다운로드되어야 합니다.

유니티 5의 에셋번들 시스템은 빌드가 끝나면 하나의 추가적인 에셋번들을 생성합니다. 이 여분의 에셋번들은 AssetBundleManifest 오브젝트를 포함하고 있습니다. 이 매니페스트 오브젝트는 에셋번들의 리스트와 그들의 해쉬 값을 포함하고 있고, 클라이언트에 가능한 에셋번들의 리스트와 버전 정보를 전달하는 데에 사용될 수 있습니다. 에셋번들 매니페스트 번들에 대한 더 자세한 내용을 알고 싶으시면 유니티 매뉴얼을 참고하세요.

에셋번들의 변화를 탐지하기 위한 커스텀 시스템을 작성하는 것도 가능합니다. 커스텀 시스템을 만드는 대부분의 개발자는 에셋번들 파일 리스트를 위한 산업 표준(industry-standard) 데이터 포맷을 사용해야하는데, 여기에는 JSON같은 것이 있고, 체크섬을 계산하기 위해 표준 C# 클래스를 사용하는데 예를 들면 MD5가 있습니다.

4.4.1. 차이 패치 (Differential patching)

유니티 5에서, 유니티는 결정론적인 방법으로 데이터가 정렬된 에셋번들을 빌드할 수 있습니다. 이는 애플리케이션에의 커스텀 다운로더에서 차이 패치를 구현 가능케 합니다. 에셋번들을 결정론적인 레이아웃으로 빌드하기 위해, BuildAssetBundles API를 호출할 때, BuildAssetBundleOptions.DeterministicAssetBundle 플래그를 전달하십시오.

유니티는 차이 패치를 위한 내장된 매커니즘을 제공하지 않고 있고, WWW.LoadFromCacheOrDownload 나 UnityWebRequest 중 어떤 것도 내장 캐시 시스템을 쓸 때 차이 패치를 수행하지 않습니다. 만약 차이 패치가 필요하다면, 커스텀 다운로더를 작성해야 합니다.

4.4.2. iOS On-Demand Resources

On-Demand Resources 는 컨텐츠를 iOS와 TVOS 장치에 제공하기 위한 애플 API입니다. 이는 iOS 9 장치에서 쓸 수 있습니다. 이 기능은 앱스토어에 런치하기 위해 현재 요구되는 조건은 아니지만, TVOS 앱에서는 런치에 필요한 요구조건 입니다.

애플의 On-Demand Resources 시스템에 대한 일반적인 아웃라인은 애플 개발자 사이트에서 찾아 볼 수 있습니다.

유니티 5.2.1 에서, App Slicing과 On-Demand Resources 둘 다에 대한 지원은 Asset Catalogs라는 다른 애플 시스템에 만들어져 있습니다. 유니티 에디터에서 콜백을 등록한 뒤에 iOS 애플리케이션을 위한 빌드 파이프라인은 Asset Catalogs에 자동으로 위치할 파일들을 보고하고 특정 On-Demand Resources 태그를 할당합니다.

새로운 UnityEngine.iOS.OnDemandResources API는 런타임에 On-Demand Resources 파일을 가져오고 캐쉬할 수 있는 기능을 제공합니다. 일단 자원을 ODR을 통해 가져왔다면, 이 자원은 AssetBundle.LoadFromFile API를 이용해서 유니티에 로드될 수 있습니다.

더 자세한 내용과 예제 프로젝트를 보고 싶다면 이 유니티 포럼의 글을 참고 하세요.

4.5. 일반적인 실수

이 섹션에서는 에셋번들을 사용하는 프로젝트에서 일반적으로 나타나는 몇몇 문제에 대해 다룹니다.

4.5.1. 에셋 중복

유니티 5의 에셋번들 시스템은 어떤 오브젝트가 에셋번들 안에 들어 있다면, 이 오브젝트에 대한 모든 의존 관계를 가려버립니다. 이는 Asset Database를 사용할 때 발생합니다. 이러한 의존 관계 정보는 하나의 에셋번들에 포함될 오브젝트들을 결정하는 데에 사용됩니다.

하나의 에셋번들에 명시적으로 할당된 오브젝트들은 그 에셋번들에만 빌드될 것 입니다. 오브젝트가 “명시적으로 할당되었다”라는 것은 오브젝트의 에셋 임포터가 assetBundleName 속성을 비어있지 않은 스트링으로 가지고 있을 때를 말합니다. 이는 유니티 에디터의 해당 오브젝트 인스펙터에서 에셋번들을 선택하거나 에디터 스크립트에서 선택함으로써 이루어집니다.

만약 두 개의 다른 오브젝트가 각각 다른 에셋번들에 할당되었는데, 둘다 공통의 오브젝트에 대한 참조를 가지고 있다면, 그 의존 관계의 오브젝트는 두 에셋번들에 복제되게 됩니다. 복제된 의존 관계의 오브젝트는 인스턴스화 되는데, 이는 두 개의 복제된 의존 관계 오브젝트가 다른 식별자를 가진 별개의 오브젝트로 간주됨을 의미합니다. 이는 애플리케이션의 에셋번들 총 크기를 증가시킬 것 입니다. 또한 애플리케이션에서 이 두개의 오브젝트를 로드하게 되면 두개의 다른 복제본이 메모리에 로드되게 됩니다.

이를 해결하기 위한 몇 가지 방법이 있습니다 :

  1. 다른 에셋번들에 빌드될 오브젝트들은 의존 관계를 공유하지 않도록 하십시오. 의존 관계를 공유하는 어떤 오브젝트들이 있다면, 이들은 같은 에셋번들에 넣음으로써 의존 관계의 복제를 막을 수 있습니다.
    • 이러한 방법은 일반적으로 많은 의존 관계를 공유하는 프로젝트에는 적합하지 않습니다. 이런 경우에는 단일 에셋번들이 생겨서 엄청나게 자주 재빌드되고 재 다운로드되는 일이 생길 수 있습니다.
  2. 의존 관계를 공유하고 있는 어떠한 두 개의 에셋번들도 동시에 로드될 수 없도록 에셋번들을 분할 하십시오.
    • 이러한 방법은 레벨 기반의 게임같은 타입의 프로젝트에서 잘 동작할 수 있습니다. 하지만, 이는 여전히 프로젝트의 에셋번들 크기를 불필요하게 늘리고, 빌드 시간과 로딩 시간 또한 늘립니다.
  3. 모든 의존 관계 에셋들을 그들 만의 에셋번들에 빌드하십시오. 이는 전적으로 중복된 에셋에 대한 위험 부담을 제거할 수 있지만, 복잡성을 증가시킵니다. 애플리케이션은 에셋번들 간의 의존 관계를 추적해야만 하고, 어떤 AssetBundle.LoadAsset API를 호출하기 전에 적절한 에셋번들들이 로드된 상태를 보장해야만 합니다.

유니티 5에서, UnityEditor 네임스페이스에 있는 AssetDatabase API를 통해 오브젝트 간의 의존 관계를 추적할 수 있습니다. 네임스페이스가 함축하고 있듯이, 이 API는 런타임이 아닌 유니티 에디터에서만 사용 가능합니다. AssetDatabase.GetDependencies는 특정 오브젝트나 에셋의 모든 직접적인 의존 관계를 얻는 데에 사용될 수 있습니다. 이러한 의존 관계에는 스스로에 대한 의존 관계도 포함되어 있음을 기억하십시오. 추가로, AssetImporter API는 에셋번들에 어떤 특정 오브젝트가 할당됐는지 확인하기 위해 사용될 수 있습니다.

AssetDatabase와 AssetImporter API를 조합하면, 에셋번들의 모든 직접 또는 간접 의존 관계가 어떤 에셋번들에 할당되도록 하거나, 의존 관계를 공유하는 두 개의 에셋번들이 존재하지 않도록 하는 에디터 스크립트를 작성하는 것이 가능합니다. 중복되는 에셋의 메모리 비용 때문에, 모든 프로젝트에서 그러한 스크립트를 사용하기를 추천드립니다.

4.5.2. 스프라이트 아틀라스 중복

이어지는 섹션에서는 유니티 5의 에셋 의존 관계 계산 코드가 자동 생성되는 스프라이트 아틀라스과 함께 사용될 때의 변덕에 대해 다룹니다. 유니티 5.2.2p4와 유니티 5.3에서는 이런 동작을 해결하기 위한 패치가 이루어 졌습니다.

유니티 5.2.2p4과 5.3 그리고 그 이상

자동 생성된 스프라이트 아틀라스는 자신이 생성된 스프라이트 오브젝트를 포함하고 있는 에셋번들에 할당됩니다. 만약 그 스프라이트 오브젝트가 여러 개의 에셋번들에 할당되어 있다면, 스프라이트 아틀라스는 하나의 에셋번들에 할당되지 않고 중복되게 됩니다.

스프라이트 아틀라스가 중복되지 않게 하려면, 같은 아틀라스로 태그된 모든 스프라이트가 같은 에셋번들에 할당되도록 하십시오.

유니티 5.2.2p3와 그 이하

자동 생성된 스프라이트 아틀라스는 절대 어떤 한 에셋번들에 할당되지 않습니다. 이 때문에, 이 아틀라스는 이를 구성하고 있는 스프라이트가 포함된 에셋번들과, 이 스프라이트들이 참조하고 있는 에셋번들에도 포함되어 집니다.

이러한 문제때문에, 유니티의 스프라이트 패커를 사용하는 모든 유니티 5 프로젝트는 유니티 5.2.2p4나 5.3 또는 그 이상의 새로운 버전으로 업그레이드하기를 강력하게 추천하는 바입니다.

업그레이드할 수 없는 프로젝트의 경우에는 2가지 회피 방법이 있습니다 :

  1. 쉬운 방법 : 유니티의 내장 스프라이트 패커를 사용하지 마십시오. 외부 툴을 이용해서 생성하는 스프라이트 아틀라스는 일반 에셋이 되고, 에셋번들에 적절히 할당될 수 있습니다.
  2. 어려운 방법 : 자동 아틀라스 스프라이트를 사용하는 모든 오브젝트를 스프라이트와 같은 에셋번들에 할당하십시오.
  3. 이는 생성된 스프라이트 아틀라스를 다른 에셋번들의 간접 의존 관계로 보지 않기 때문에 중복되지 않을 것 입니다.
  4. 이 해결책은 유니티 스프라이트 패커를 사용하는 단순한 워크 플로우를 보존하지만, 에셋을 다른 에셋번들로 분리시킬 수 있는 능력을 감소시키고, 아틀라스가 변하지 않더라도 아틀라스를 참조하는 어떤 컴포넌트의 데이터 변화가 발생하면 전체 스프라이트 아틀라스를 다시 다운로드해야만 합니다.

4.5.3. 안드로이드 텍스쳐

안드로이드 생태계의 심각한 디바이스 파편화 때문에, 텍스쳐를 몇몇 다른 포맷으로 압축하는 것은 종종 필수적입니다. 모든 안드로이드 장치가 ETC1을 지원하지만, ETC1은 텍스쳐의 알파 채널을 지원하지 않습니다. 애플리케이션이 OpenGL ES 2 지원을 필요로하지 않는다면, 이 문제를 해결하기 위한 가장 명확한 방법은 ETC2를 사용하는 것이고, 이는 OpenGL ES 3를 지원하는 모든 안드로이드 장치에서 사용 가능합니다.

대부분의 애플리케이션은 ETC 지원이 안되는 오래된 기종에도 설치할 필요가 있습니다. 이를 해결하기 위한 하나의 방법은 유니티 5의 AssetBundle Variants를 사용하는 것 입니다. (다른 옵션에 대한 자세한 내용을 알고 싶으면, 유니티의 안드로이드 최적화 가이드를 봐주세요.)

AssetBundle Variants를 사용하기 위해서, 확실하게 ETC1으로 압축할 수 없는 모든 텍스쳐들은 텍스쳐만 있는 에셋번들로 분리해야 합니다. 다음으로, 안드로이드 생태계에서 ETC2를 수용할 수 없는 장치를 지원하기 위해 충분한 에셋번들 variants를 만들어야 합니다. 이 때 제조사 특유의 압축 포맷을 사용할 수 있는데, DXT5, PVRTC, ATITC 등이 있습니다. 각 에셋번들 variant에 대해서, 포함된 텍스쳐의 TextureImporter 셋팅의 압축 포맷을 variant에 적절하게 변경하십시오.

런타임에, 다른 텍스쳐 압축 포맷에 대한 지원은 SystemInfo.SupportsTextureFormat API를 이용해서 탐지할 수 있습니다. 이 정보는 지원되는 포맷으로 압축된 텍스쳐를 포함하고 있는 에셋번들 Variant를 선택하고 로드하는데에 사용될 수 있습니다.

안드로이드 텍스쳐 압축 포맷에 대한 더 자세한 내용은 여기에서 보실 수 있습니다.

4.5.4. iOS 파일 핸들 남용

이 섹션에 나오는 내용은 유니티 5.3.2p2에서 수정되었습니다. 현재 유니티 버전은 이 이슈에 영향을 받지 않습니다.

유니티 5.3.2p2 이전 버전에서는, 에셋번들이 로드되어 있는 전체 시간 동안 유니티가 그 에셋번들에 대한 파일 핸들을 유지하고 있었습니다. 이는 대부분의 플랫폼에서는 문제가 되지 않습니다. 하지만, iOS에서는 한 프로세스에서 동시에 열 수 있는 파일 핸들의 수가 255개로 제한되어 있습니다. 만약 에셋번들을 로드하다가 이러한 제한을 초과하게 되면, “Too Many Open File Handles” 에러를 내뿜으로 로딩 콜이 실패하게 됩니다.

이는 컨텐츠를 수백개 또는 수천개의 에셋번들로 나누려고하는 프로젝트에서 흔히 발생할 수 있는 문제였습니다.

패치된 버전의 유니티로 업그레이드할 수 없는 프로젝트에서는, 임시 방편이 있습니다 :

  • 관련된 에셋번들을 합침으로써 사용 중인 에셋번들의 수를 줄이는 방법이 있습니다.
  • 에셋번들의 파일 핸들을 닫기 위해 AssetBundle.Unload를 사용하는 것과 로드된 오브젝트의 생명주기를 수동으로 관리하는 방법이 있습니다.

4.6. 에셋번들 Variants

유니티 5의 에셋번들 시스템의 주요 특징은 AssetBundle Variants의 도입입니다. Variants의 목적은 애플리케이션을 런타임 환경에 더 잘 맞게 컨텐츠를 조절하도록 하는 것 입니다. 오브젝트를 로딩하고 Instance ID 참조를 풀 때, Variants는 다른 에셋번들 파일에 있는 다른 UnityEngine.Object를 마치 “같은” 오브젝트 인 것 처럼 보이게 해줍니다. 개념상, 이는 두 개의 UnityEngine.Object가 같은 File GUID와 Local ID를 공유하는 것처럼 보이게 해주고, 스트링 Variant ID를 이용해 로드할 실제 UnityEngine.Object를 식별합니다.

이 시스템의 2가지 주된 사용 사례가 있습니다 :

  1. Variants는 정해진 플랫폼에 대해 적절한 에셋번들을 로딩하는 것을 간단하게 해줍니다.
    • 예 : 빌드 시스템은 DirectX 11 윈도우 빌드를 위한 높은 해상도의 텍스쳐와 복잡한 셰이더를 포함하는 에셋번들과 안드로이드를 위한 더 낮은 정밀도의 컨텐츠를 포함하는 에셋번들을 빌드할 수 있습니다. 런타임에, 프로젝트의 리소스 로딩 코드는 플랫폼에 맞는 에셋번들 Variant를 로드할 수 있고, [AssetBundle.Load] API에 넘겨질 오브젝트 이름은 동일하게 사용할 수 있습니다.
  2. Variants는 같은 플랫폼이지만 하드웨어가 다른 경우, 다른 컨텐츠를 로드할 수 있도록 해줍니다.
    • 이 것이 넓은 범위의 모바일 장치들을 지원할 수 있는 핵심입니다. iPhone 4는 iPhone 6에서 보여주는 정밀도의 컨텐츠를 보여줄 수 없습니다.
    • 안드로이드에서, 에셋번들 Variants는 장치들 간의 엄청난 화면 비율과 DPI에 대한 파편화를 해결하는데에 사용될 수 있습니다.

4.6.1. 한계

에셋번들 Variant 시스템의 핵심 한계는 구별되는 에셋으로부터 Variants를 빌드할 필요가 있다는 점 입니다. 이 한계는 에셋의 임포트 셋팅에 대한 차이만으로도 적용 가능합니다. 만약 Variant A와 Variant B의 차이가 유니티 텍스쳐 임포터에서 설정한 텍스쳐 압축 알고리즘이 유일한 차이라면, Variant A와 Variant B는 완전히 다른 에셋입니다. 이는 Variant A와 Variant B는 디스크 상에서 별도 파일로 저장되어야 함을 의미합니다.

이러한 한계점은 소스 컨트롤에 의해 유지되어야 하는 특정 에셋에 대한 여러 개의 복제본을 가지는 큰 프로젝트의 관리를 복잡하게 만듭니다. 개발자가 그 에셋의 컨텐츠를 바꾸려고 하면, 해당 에셋의 모든 복제본이 업데이트 되어야 합니다.

이 문제에 대해 내장된 회피 수단은 존재하지 않습니다.

대부분의 팀은 그 팀만의 에셋번들 Variants 형태를 구현합니다. 이는 에셋번들의 파일명 뒤에 잘 정의된 접미사를 붙임으로써 할 수 있습니다. 이 때 접미사를 붙이는 이유는 그 에셋번들에 특정 variant를 식별하기 위함입니다. 커스텀 코드는 이러한 에셋번들을 빌드할 때, 포함되는 에셋의 임포터 셋티을 프로그램적으로 변경합니다. 일부 개발자들은 프리팹에 붙어있는 컴포넌트의 인자를 변경하기 위해 자신의 커스텀 시스템을 확장하기도 합니다.

4.7. 압축 또는 비압축?

에셋번들을 압축할지 말지에 대해서는 신중한 고려가 필요합니다. 중요한 질문은 다음과 같습니다 :

  • 에셋번들의 로딩 시간이 중요한 요소인가? 로컬 저장 공간이나 로컬 캐쉬에서 로딩할 때, 비압축 에셋번들이 압축 에셋번들보다 훨씬 빠르게 로드됩니다. 원격 서버에서 압축된 에셋번들을 다운로드하는 것은 일반적으로 압축되지 않는 에셋번들을 다운로드하는 것보다 빠릅니다.
  • 에셋번들의 빌드 시간이 중요한 요소인가? LZMA와 LZ4는 파일을 압축할 때 매우 느리고, 유니티 에디터는 에셋번들을 순차적으로 처리합니다. 에셋번들의 개수가 많은 프로젝트는 이들을 압축하는데에 많은 시간이 걸립니다.
  • 애플리케이션의 크기가 중요한 요소인가? 만약 애플리케이션에 에셋번들이 포함된 상태로 전달된다면, 에셋번들을 압축하는 것이 애플리케이션의 전체 사이즈를 줄일 수 있습니다. 대안으로, 에셋번들을 설치 후에 다운로드할 수도 있습니다.
  • 메모리 사용량이 중요한 요소인가? 유니티 5.3 이전에, 모든 유니티의 압축 해제 메커니즘은 압축을 해제하기 전에 압축된 에셋번들 전체를 메모리에 로드할 필요가 있었습니다. 만약 메모리 사용량이 중요하다면, 비압축이나 LZ4로 압축된 에셋번들을 사용하세요.
  • 다운로드 시간이 중요한 요소인가? 에셋번들이 크거나, 모바일에서 3G로 다운로드하는 것처럼 대역폭이 제한적인 환경에 유저가 처해져 있는 등의 경우에 압축이 필요할 지도 모릅니다. 만약 빠른 속도로 연결되어 있는 PC에 수십 MB의 데이터를 전달하는 경우라면, 압축을 생략하는 것도 가능합니다.

4.8. 에셋번들과 WebGL

유니티는 WebGL 프로젝트에서는 압축된 에셋번들을 사용하지 말 것을 강력히 추천합니다.

유니티 5.3에서, WebGL 프로젝트에서 모든 에셋번들의 압축 해제와 로딩은 메인 쓰레드에서 발생합니다. 이는 유니티 5.3의 WebGL 익스포트 옵션에서는 워커 쓰레드를 지원하지 않기 때문입니다. ( 에셋번들의 다운로드는 브라우저로 위임되어 XMLHttpRequest 자바스크립트 API를 통해 이루어지고, 유니티의 메인 쓰레드 밖에서 발생할 것 입니다.) 이는 WebGL에서 압축된 에셋번들을 매우 비쌈을 의미합니다.

대게는 에셋번들을 압축하지 않은 상태로 빌드하고, 애플리케이션의 컨텐츠를 제공하는 웹 서버가 에셋번들을 gzip 압축 형태로 제공하도록 설정하는 것이 훨씬 빠릅니다. gzip 압축이 LZMA와 같은 압축률을 낼 수는 없지만, 에셋번들은 브라우저에 의해 워커 쓰레드에서 압축 해제 되고, 유니티 애플리케이션은 에셋번들의 압축을 해제하는 동안 오랫 동안 멈춰있는 경험을 하지 않아도 됩니다.


  1. over-the-air download size limit을 무선 다운로드 크기 제한이라 번역했는데요, 아마 iOS나 안드로이드에서 통신사 네트웍으로 연결되어 있을 때 다운로드 받을 수 있는 최대 크기 제한이 아닐까 추측합니다.
  2. quirk를 변덕이라 번역하는게 맞는 것인지 잘 모르겠네요. 문맥상으로는 결함 비슷한 것 같긴 합니다.
  3. DLC – 본문에는 설명이 없지만 DownLoadable Contents를 의미하는 것 같습니다.

출처: https://code-kooc.tistory.com/entry/번역-에셋번들-사용-패턴 [코드쿡]

코루틴 결과값 리턴받기

코루틴을 사용한 리소스 비동기 로딩 구현 도중 특정 코루틴의 결과값을 리턴받아야 할 일이 생겼습니다.

public Coroutine LoadCharacterAsync(CharacterData data)
{
    return StartCoroutine(LoadCharacterAsyncRoutine(data));
}
  
IEnumerator LoadCharacterAsyncRoutine(CharacterData data)
{
    GameObject characterObject = new GameObject();
    PokoCharacter character = characterObject.AddComponent<PokoCharacter>();
     
    // 캐릭터를 구성하는 각 파츠의 SkinnedMesh를 읽어서 생성
    for(int i = 0; i < 3; ++i)
    {
        GameObject loadedParts = null;
  
        yield return StartCoroutine(LoadCharacterPartsRoutine(data.partsName[i]);
  
        // 위 코루틴의 결과값을 어떻게 받지?!
        character.AddParts(i, loadedParts);
    }
}
  
IEnumerator LoadCharacterPartsRoutine(string partsName)
{
    // Mesh, Texture, Bone, Animation 등등 로딩
}

혹시나 이런 게 되나 싶었는데

IEnumerator LoadCharacterPartsRoutine(string partsName)
{
    // Mesh, Texture, Bone, Animation 등등 로딩
    return loadedParts;
}

될 리가 없죠;; 

IEnumerator로 선언된 메소드 내에서는 ref, out 같은 키워드도 문법적으로 사용할 수 없습니다. 

해서 구글링을 해 봤더니 C#의 람다를 콜백으로 사용하는 신박한 방법이 있더라구요. 

IEnumerator LoadCharacterPartsRoutine(string partsName)
{
    // Mesh, Texture, Bone, Animation 등등 로딩
    return loadedParts;
}

NGUI <-> World 좌표계 상호 변환

1. World 좌표계 -> NGUI 좌표계

  • 사용 예시 : RPG 에서 캐릭터 머리 위에 따라다니는 HP Bar 등
  • 참고 : World 좌표계에서 Viewport 좌표계로 중간 변환하고 나서 다시 NGUI 좌표계로 변환한다.

UICamera.mainCamera.ViewportToWorldPoint (Camera.main.WorldToViewportPoint (worldPosition));

2. NGUI 좌표계 -> World 좌표계  

  • 사용 예시 : 캐릭터 아이콘 UI 에서 실제 월드상의 캐릭터 3D 모델에게 선을 긋는다.
  • 참고 : 위와 같이 Viewport 좌표계 변환이 한 번 있어야 한다.
  • 가장 중요한 포인트!
  • NGUI 좌표계에서 z값이 0 인데 특정 값을 꼭 넣어야 한다.
  • 그렇지 않으면 항상 카메라 위치가 나오게 되므로 2D->3D 가 애초에 안되니 Ray 를 쏘거나 복잡해지는데
  • z값에 값을 넣게 되면 간단히 해결!
public static Vector3 NGUIToWorld(Vector3 nguiPosition)
{
    nguiPosition.z += 5f;
    Vector3 worldPosition = Camera.main.ViewportToWorldPoint(UICamera.mainCamera.WorldToViewportPoint(nguiPosition));
    return worldPosition;
}

마우스의 현재 위치를 가져오기 위해 사용하는 Input.mousePosition은 스크린 좌표계를 사용한다.따라서 이 좌표를 월드 좌표로 변환해주기 위해 사용하는 것이 Camera.ScreenToWorldPoint()인데, 한가지 주의해야할 점이 있다.Input.mousePosition의 z값이 항상 0이라는 것이다.따라서 Camera.main.ScreenToWorldPoint(Input.mousePosition)을 하게 되면 그 결과는 카메라의 월드 좌표값이 나오게 된다.그래서 Input.mousePosition을 그대로 사용하면 안되고, z값을 변경해준 뒤에 사용해야 한다.예를들어 아래와 같이 사용할 수 있다.

Vector3 screenMouse = Input.mousePosition;screenMouse.z = Camera.main.WorldToScreenPoint(Vector3.zero).z;Vector3 worldMouse = Camera.main.ScreenToWorldPoint(screenMouse);

screenMouse의 z 값에 대입한 값의 정체를 생각해보자.월드 좌표계의 원점(0, 0, 0)을 스크린 좌표계로 변환한 값이다. 이는 카메라의 위치에 따라 다른 값이 될 수 있겠다.어쨌든 이렇게 하면 마우스 클릭이 월드 좌표계의 z값 0인 위치에서의 클릭 지점이 되겠다. 

플랫폼 쉽게 초스피드로 변경하기

유니티에서 리소스가 많아질수록 플랫폼 변경시에 시간이 많이 소요되는 문제가 있습니다.포코 메르헨의 경우에는 플랫폼 변경에만 30분정도 걸리네요.그래서 좀 찾아봤습니다.http://www.cliffordroche.ca/fast-platform-switching-in-unity/요약하자면 이런거에요.

  1. 기존에 e:/work 프로젝트 폴더가 있고, 이 프로젝트를 android 플랫폼으로 사용 중이라고 가정하겠습니다.
  2. windows 플랫폼을 하나 더 추가하고 싶다면 e:/ work_windows 폴더를 만들고,
  3. 커맨드 라인에서 e:/work_windows 폴더로 이동한 다음
  4. mklink /j Assets ..\work\Assets
  5. mklink /j ProjectSettings ..\work\ProjectSettings
  6. 유니티를 켜고 work_windows 프로젝트를 연 후 플랫폼을  pc, mac & linux standalone으로 변경합니다.

이렇게 하면 별도의 Library폴더를 가지게 되기 때문에, 코드 및 리소스를 공유를 하고 플랫폼만 다른 두개의 프로젝트를 유지할 수 있습니다.

광고 (IronSource)

IronSource

버전 히스토리

  • 6.10.1 (3.0.0 버전에서 적용)
  • 6.11.1 (3.2.0 버전에서 적용)

이슈

  • 안드로이드 환경에서 라이브러리들의 메소드갯수가 64K를 넘어가면 MultiDex를 사용해야 한다. 광고 라이브러리가 증가하면서 메소드의 갯수가 64K가 넘어가게됨 
    • Gradle시스템에서 MultiDex를 사용하여 해결
  • 안드로이드 5.0 미만의 환경에서 크래시가 나는 문제 발생
    • 안드로이드 5.0 미만의 환경에서 MultiDex를 사용하기 위해서는 추가 작업이 필요함. (메인 Application 클래스를 구현할 때 MultiDexApplication 클래스를 상속받도록 구현해야 하지만 트라이던트에서 TridentApplication을 따로 사용하고 있어 수정할 수 없는 문제가 있음)
    • 트라이던트의 TridentApplication을 상속받아 커스텀 할 수 있는 방법으로 해당 문제 수정
    • 참고 자료 : https://developer.android.com/studio/build/multidex?hl=ko#java

대시보드 주소 : https://platform.ironsrc.com/partners/dashboard

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/unity-plugin/

AdMob

버전 히스토리

  • 4.3.5
  • 4.3.6 (3.0.0 버전에서 적용)
  • 4.3.9 (3.2.0 버전에서 적용)

이슈

  • 안드로이드 환경에서 트라이던트가 사용하는 라이브러리와 충돌이 나는 문제 발생
    • 트라이던트에 있는 라이브러리를 모두 삭제한 뒤 Gradle 시스템으로 최신 라이브러리로 맞춰주도록 변경
    • 트라이던트 라이브러리 목록
      • 기존과 동일한 라이브러리
        • android.arch.core-common-1.0.0 (버전 유지)
        • android.arch.lifecycle:common:1.0.3 (버전 유지)
        • android.arch.lifecycle:runtime:1.0.3 (버전 유지)
      • androidx 용 라이브러리로 교체한 라이브러리
        • com.android.support:appcompat-v7:27.0.2 -> androidx.appcompat:appcompat:1.0.2
        • com.android.support:cardview-v7:23.2.1 -> androidx.cardview:cardview:1.0.0
        • com.android.support:customtabs:25.3.1 -> androidx.browser:browser:1.0.0
        • com.android.support:support-v4:27.0.2 -> androidx.legacy:legacy-support-v4:1.0.0
        • com.android.support:support-core-ui:27.0.2 -> androidx.legacy:legacy-support-core-ui:1.0.0
        • com.android.support:support-core-utils:27.0.2 -> androidx.legacy:legacy-support-core-utils:1.0.0
        • com.android.support:support-compat:27.0.2 -> androidx.core:core:1.0.2
        • com.android.support:support-annotations:27.0.2 -> androidx.annotation:annotation:1.0.2
        • com.android.support:support-fragment:27.0.2 -> androidx.fragment:fragment:1.0.0
        • com.android.support:support-vector-drawable:27.0.2 -> androidx.vectordrawable:vectordrawable:1.0.1
      • 버전업된 라이브러리
        • com.google.firebase:firebase-common:10.0.1 -> 18.0.0
        • com.google.firebase:firebase-iid:10.0.1 -> 18.0.0
        • com.google.firebase:firebase-messaging:10.0.1 -> 18.0.0
        • com.google.android.gms:play-services-auth:10.0.1 -> 17.0.0
        • com.google.android.gms:play-services-auth-base:10.0.1 -> 17.0.0
        • com.google.android.gms:play-services-base:10.0.1 -> 17.0.0
        • com.google.android.gms:play-services-basement:10.0.1 -> 17.0.0
        • com.google.android.gms:play-services-tasks:10.0.1 -> 17.0.0
      • 신규 추가 라이브러리
        • androidx.multidex:multidex:2.0.1 (라이브러리들의 메소드 수가 64K를 넘어가면 에러를 발생한다. 이를 위해 multidex를 사용)
        • com.google.android.gms:play-services-ads:18.2.0 (애드몹 광고를 위한 라이브러리)

대시보드 주소 : https://apps.admob.com

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/admob-mediation-guide/

AppLovin

버전 히스토리

  • 4.3.5 
  • 4.3.7 (3.0.0 버전에서 적용)
  • 4.3.8 (3.2.0 버전에서 적용)

이슈

  • 4.3.5 버전에서 아이언소스 무결성 체크 함수를 돌려보면 com.applovin.adview.AppLovinConfirmationActivity – MISSING 메시지가 발생
    • 라이브러리를 열어서 확인해 보면 4.3.4 버전까지는com.applovin.adview.AppLovinConfirmationActivity를 사용했지만 4.3.5 버전 부터는 사용하지않고, com.applovin.sdk.AppLovinWebViewActivity를 사용하도록 변경됨. 하지만 4.3.5 버전에서 무결성 체크에서 과거에 사용하는 com.applovin.adview.AppLovinConfirmationActivity를 체크하는 버그가 있어, 4.3.7 버전으로 업데이트 진행

대시보드 주소 : https://dash.applovin.com/o/analytics

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/applovin-mediation-guide/

Facebook

버전 히스토리

  • 4.3.6
  • 4.3.7
  • 4.3.8 (3.0.0 버전에서 적용)
  • 4.3.9
  • 4.3.10
  • 4.3.11 (3.2.0 버전에서 적용)

이슈

  • AOS환경에서 페이스북 광고를 시청하는 도중 앱을 백그라운드로 내린뒤, 앱 아이콘을 통해 다시 실행하면 광고가 꺼지는 문제 발생
    • 유니티는 SingleTask만 지원하기 때문에, 광고가 종료 되는 문제는 해결 할 수 없다.(페이스북 광고 개발 문서에서도 해당 문제는 스펙으로 처리되어 있다.)
    • 광고가 종료되는 것은 스펙으로 처리하는게 맞지만, 이렇게 광고가 종료되었을 경우 EndCallback이 오지 않는 문제가 있다.(EndCallbackdl 오지 않기 때문에 앱에서는 무한정 광고가 끝나기를 기다리기 때문에 입력이 먹히지 않고 앱이 멈춘것 처럼 보인다)
    • 해당 이슈를 해결하기 위해 4.3.6, 4.3.7, 4.3.8, 4.3.9, 4.3.10 다섯개의 버전으로 모두 테스트 해보았지만 해결할 수 없었다.

대시보드 주소 : https://developers.facebook.com/apps/527156608124582/dashboard/?business_id=154719308447585

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/facebook-mediation-guide/

Maio

버전 히스토리

  • 4.1.3 (3.0.0 버전에서 적용)
  • 4.1.4 (3.2.0 버전에서 적용)

대시보드 주소 : https://maio.jp/publisher/

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/maio-mediation-integration-guide/

Tapjoy

버전 히스토리

  • 4.1.9 (3.0.0 버전에서 적용)
  • 4.1.10 (3.2.0 버전에서 적용)

대시보드 주소 : https://ltv.tapjoy.com/s/partners/d294941a-e233-4d35-b5f7-321d08d21aa3#overview

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/tapjoy-mediation-guide/

TikTok

버전 히스토리

  • 4.1.1 (3.0.0 버전에서 적용)
  • 4.1.2 (3.2.0 버전에서 적용)

이슈

  • 일부 IOS 기기에서 앱 실행시 앱이 중지되는 문제 발생. 해당 이슈는 일본지역에서 그리고 일부 소수의 기기에서만 발생
    • TikTok광고를 끄고 실행하면 정상동작 하는 것을 확인
    • IronSource 6.11.1 버전과 TikTok 4.1.2 버전 업데이트 후 정상 동작하는 것을 확인

대시보드 주소 : https://ad.oceanengine.com/union/media/

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/pangle-audience-network-integration-guide/

Vungle

버전 히스토리

  • 4.1.8 (3.0.0 버전에서 적용)
  • 4.1.9
  • 4.1.10 (3.2.0 버전에서 적용)

이슈

  • AdMob 이슈 해결을 위해 안드로이드 라이브러리를 최신(AndroidX)으로 적용하면서, Vungle의 라이브러리가 AndroidX를 지원하지 않아 광고가 나오지 않는 문제가 발생
    • 벙글쪽에서 AndroidX용 라이브러리를 따로 제공받아 수정(vungle-android-sdk-6.4.11.aar 파일)
  • 4.1.8 버전에서  IOS vungle광고중 일부가 플레이 되지 않는 이슈 발생
    • 4.1.9버전에서 해결되었으나 4.1.9버전에서는 또다른 문제가 발생
  • 4.1.9 버전에서 AOS 권한요청 팝업이 뜨지 않는 문제 발생
    • vungleadapter.jar파일에 있는 manifest파일을 수정하여 해결(WRITE_EXTERNAL_STORAGE 권한 요청 구문 삭제)

업데이트시 주의사항

  • vungleadapter.jar에 있는 manifest파일에서 WRITE_EXTERNAL_STORAGE 권한 요청 구문을 삭제할 것 (하지 않으면 안드로이드 권한요청 팝업이 뜨지 않을 수 있음)
  • vungle-android-sdk-6.4.11.aar 파일은 벙글을 통해 직접 전달받은 커스텀된(AndroidX용) 파일로 버전업시 해당 버전이 AndroidX를 지원하는지 확인하고, 지원하지 않는다면 벙글쪽에 요청해야 한다.

대시보드 주소 : https://publisher.vungle.com/applications

개발 문서 주소 : https://developers.ironsrc.com/ironsource-mobile/unity/vungle-mediation-guide/

※ 붉은색 이슈는 아직 미해결 이슈 입니다.

유니티 (Unity) 2D 모바일 게임 최적화 팁 & 체크리스트

유니티2D로 제작한 모바일 게임에서 60프레임을 안정적으로 지키고, 빌드 용량을 줄이기 위한 팁들입니다.

  • 빌드전 마지막으로 체크해야 될 옵션들 위주로 나열했습니다.
  • 최적화 방법 중 디자인 패턴과 관련된 부분은경우의 수가 너무 많아 제외했습니다.
  • 유니티 5.6 이상의 2D 게임, 안드로이드를 기준으로 했습니다.
    • 다른 버전과 플랫폼에도 일반적으로 적용할 수 있는 내용들도 포함했습니다.

최적화 하기전에!

코드 최적화는 추상적이고 간결하며 범용적인 코드를 만드는 것과 반대되는 경우가 많습니다.

최적화는 알아보기 쉬운 코드를 프로젝트에 집역적으로 만듭니다. 성능은 좋으나 알아보기 힘들고 재활용 할수 없는 형태로 만듭니다.

코드 뿐만 아니라 콘텐츠 자체에도 비슷한 논리가 적용됩니다.

그렇기에 최적화는 게임 개발의 후반부에 신경써야 합니다. 릴리즈 후에는 정말 미친듯이 최적화와 싸워야 합니다.

하지만 릴리즈 전에는 코드 구조와 콘텐츠가 허구한날 새롭게 바뀝니다.

게임 출시전에는 소요되는 시간 대부분을 이미 만든것에 대한 최적화 보다는 새로운 것에 집중하는게 좋을 수도 있습니다. 😛


간단하게 할수 있는 것들

  1. Application.targetFrameRate 를 60으로 설정합니다.
    1. 모바일에서는 30이 기본으로 설정되어 있습니다.
  2. 이상이 없다면 vSyncCount 를 Quality 설정에서 켭니다. 몇몇 안드로이드 기기에서는 60fps 이상 랜더하여 기기가 과열되는 것을 막아줍니다.
  3. 비어있는 유니티 메세지 함수들은 지우세요. Update 등의 함수 내부가 비어있어도, 호출은 계속 됩니다.
  4. GameObject.Find() 같이 씬 전체를 검색하는 함수를 되도록 쓰지 마세요.
    1. Update/FixedUpdate 에서는 절대 안됩니다.
  5. 사용하지 않는 게임 오브젝트는 미리 비활성화 하세요.
  6. 오브젝트를 실시간으로 파괴하는 것보다, 비활성화 하는게 비용이 싸다는 것을 알아두세요.

글로벌 일루미네이션 제거

  • 만약 라이팅을 가지고 노는 일이 전혀 없는 게임이라면, Precomputed Realtime GI, Based GI, Fog 등을 전부 끄세요.

Quality Setting 트윅

  1. default 보다 높은 프로필은 모두 해제하세요. (모바일이라면 Simple)
  2. 사용하지 않는다면, Pixel Light Count 를 0으로 하세요.
  3. Texture Quality 를 가능한 낮게 잡으세요. Half Res 일때도 모바일에서는 눈치채기 힘든 경우가 많은데 75%나 메모리를 절약해줍니다.
  4. 텍스쳐에 확대 축소를 사용하지 않는다면, minmaps 를 해제하세요. 33% 메모리를 절약해줍니다.
  5. 비스듬한 각도에서 텍스쳐를 봐야하는 일이 없다면, Anisotropic Textures 를 해제합니다.
  6. Anti Aliasing 을 끄거나 2 passes 로 설정하세요. 그정도만 해도 지글거림을 해결해줍니다. LineRenderer 를 사용한다면 눈에 거슬릴순 있습니다.
  7. Soft Particle을 끕니다. 메쉬들을 적절히 섞어주는 역할을 하지만, 3D 상에서의 문제입니다.
  8. Shadow 를 끕니다.

여기서 가장 눈에 띄는 효과를 가진 것은, 텍스쳐 설정과 안티 얼라이언싱 입니다.

텍스쳐 퀄리티는 메모리에 직접적인 타격을 가하고 GPU 에 이미지를 보내는데 얼마나 시간을 먹을지 결정합니다.

안티얼라이언싱은 전체 화면에 적용되므로 해상도가 커짐에 따라 비용이 가파르게 증가합니다.


알아차리기 힘든 메모리 할당들

게임 엔진을 쓰면 C++ 로 직접 로우레벨을 통제할때 보다 메모리 관리에 대한 통제권이 적을 수 밖에 없습니다.

그래도 메모리가 언제 점유되는지 알고 쓰는게 최적화에 도움이 됩니다.

오브젝트 풀링 반드시 쓰기

게임 도중에 GC 가 발동된다면 게임이 실행 도중 뚝뚝 끊길수 있습니다.

GC 는 어플리케이션이 메모리를 추가로 요구할때 발동됩니다.

필요한 것들을 미리 게임 초반에 인스턴스화 하여 메모리를 점유하고, 게임 플레이 도중에는 더이상 요구하지 않는 다면 GC 문제를 겪지 않습니다.

실시간으로 많이 찍어내고 파괴하는 오브젝트는 오브젝트 풀링으로 관리하세요.

리듬게임의 노트 처럼 싱크가 중요한 경우라면 더더욱 써야 합니다.

가장 간단한 구현 방법:

  • 오브젝트를 파괴할때: 파괴대신 오브젝트를 끕니다.
  • 오브젝트를 생성할때: 꺼져있는 오브젝트를 가져와서 리셋하고 씁니다.

string 은 사용하는 만큼 생성됩니다.

string message = “Hello” + “World”; 같이 연결하는 부분을 주의하세요.

이 경우 실제로는 3개의 string 이 메모리에 할당합니다.

  • StringBuilder 나 String.Format() 을 사용하세요.

클래스와 구조체의 차이점을 고려하세요.

클래스는 Pass by Reference,  구조체는 Pass by Value 로 동작합니다.

값을 전달할때 class 는 레퍼런스가 할당되고, struct 는 복제본을 전달합니다.

struct

단순 데이터 컨테이터용으로 50개의 struct를 사용해봅시다.

struct는 메모리를 많이 사용합니다.

A->B->C 로 값을 전달하면, 총 150 개의 struct 가 생성되게 됩니다.

레퍼런스와 관련된 처리가 없으므로 더 빠를 수 있습니다.

구조체는 스택에 할당되고, 자신을 호출한 함수가 종료되면 즉시 사라집니다.
GC를 유발하지 않고 빠르게 동작합니다.

오히려 더 느려질 수도 있습니다.

단일 struct 오브젝트의 크기가 너무 커지거나, 너무 긴 리스트에 넣고 쓰면, 복사하는 시간이 길어집니다. 이때 class 형보다 처리 시간이 더 길어질 수 있습니다.

  • 변수가 2~3개 뿐인 단순한 데이터 컨테이너라면 struct 를 사용하는게 빠릅니다.
  • 일반적인 경우에는 class 를 쓰세요.

필자는 그리드형 게임에서 2차원 좌표용으로 struct를 사용합니다.

struct Coord { int x; int y;}

프로파일러를 사용합시다

  1. 유니티 프로파일러로 병목 지점을 확인하세요
  2. CPU Usage 란에서, GC Alloc 기준으로 정렬하면, 누가 할당을 언제 시도하는지 볼 수 있습니다. 할당을 최대한 줄이고 몰아서 하면, 게임이 부드러워집니다. physics.engine 할당 같은 것은 제어할 수 없지만, 그 정도는 괜찮죠.
  3. Memory Area 에서는, 현재 메모리 상태의 스냅샷을 찍을 수 있습니다. 어디에서 메모리 누수가 발생하는지 파악가능합니다.

System.GC.Collect() 로 GC를 원하는 시점에 발동시킬 수도 있습니다.

컷신이 끝난 직후나, 다음 플레이어의 조작이 활성화 되기 전이라던지, 그런 시점에서 쓸수 있습니다.

먼저 GC를 해둠으로서, 예상치 못한 시점에 발동될 확률을 낮추는 거죠.


로그와 메모리 낭비

의외로 성능을 먹는 Debug.Log

Debug.Log() 등의 호출을 가능한 지우는 것이 좋습니다.

  • 로그를 호출하는 함수는, 콘솔창이 없는 빌드된 게임에서도 동작합니다.
  • 의외로 가시적인 성능을 요구합니다.

프로파일링 할때,  Development Build 를 선택하고 프로파일링 해보면 체감이 됩니다. 로그 메세지가 스택 트레이스에 잡힐때 게임이 느려집니다.

  • 입력에 string 을 합쳐서 전달하면 메모리를 더욱더 많이 요구합니다.
  •  ToString 을 사용하면 더 심합니다.
  • 예 – Debug.Log( “Player ” + player + ” is doing something in game ” + game );

해결책

세가지 해결책이 있습니다.

Debug.Log 를 랩핑합니다.

노가다로 Debug.Log 를 전처리기로 랩핑하고 함수를 만듭니다.

public class DebugX
{
    public static void Log(string msg)
    {
        #if UNITY_EDITOR
        Debug.Log(msg);
        #endif
    }
}

하지만 이 방법은 string 입력을 받는 순간에 일어나는 메모리 할당을 막을 수는 없습니다.

Find and Replace

빌드할때만 잠시 모든 디버그 라인을 죄다 찾아가 지웁니다. 가장 무식하지만 효과적입니다.

하지만 다시 매번 디버그 코드를 되돌리는 수고를 해야 합니다.

C# 조건 속성을 사용합니다.

참고하기: https://msdn.microsoft.com/en-us/library/4xssyw96(v=vs.90).aspx

깔끔하고 우아한 방법입니다. 이 방법을 추천합니다.

using System;
using System.Diagnostics;

public class DebugX
{
    [Conditional("UnityEditor")]
    public static void Log(string msg)
    {
        Debug.Log(msg);
    }
}

// PlayerSettings > Scripting Define Symbols 에 ACTIVE_DEBUG 가 없다면 아래 코드 호출은 아예 처음부터 없었던 것으로 처리됩니다.
DebugX.Log( "Hello world" );

클래스 내부에 Conditional 속성이 지정된 static void 함수를 추가합니다.

  1. 만약 Conditional 에 따라오는 심볼이 정의됬다면 static 함수는 존재합니다.
  2. 중요한것은 static 함수 뿐만이 아니라, 그것을 호출하는 부분 까지 모조리 빌드에서 빠져나가므로 매우 간편합니다.

조건 속성으로 묶인 디버그 함수를 사용한 코드들은 빌드시에 처음부터 존재하지도 않은 것이 됩니다.

다만 유니티 콘솔창에서 로그를 더블 클릭하면, 오류가 난 지점이 아닌 디버그를 랩핑한 코드로 이동하는 사소한 불편은 있습니다.


오디오 (사운드)

오디오 기본 설정

Force To Mono 사용

모바일에서 스트레오는 큰 의미를 가지지 않습니다. Force To Mono 를 사용합시다.

Preload Audio Data

Preload Audio Data는 기본 옵션입니다. 미리 오디오 데이터를 로드하므로, 씬 로드에 조금 부담을 줍니다.

이것을 사용안하면, 오디오를 재생하는 순간에 오디오를 로드 합니다. 그러면 게임 도중에 약간의 오버헤드가 생길 수 있습니다.

Load in Background

엄격하게 재생 타이밍을 맞춰야 하는 경우가 아니라면 체크해도 됩니다.

엄격하게 로드 타이밍을 지키지 않고, 느긋하게 백그라운드에서 로드 합니다.

씬이 시작되자 마자 재생을 시도할때, 조금의 지연시간이 지난 다음에야 재생되게 됩니다. 그래도 로딩 속도를 적게나마 향상시킬 수 있습니다.

오디오 매니저 (프로젝트 세팅에 있음)

스트레오 사운드가 필요없으면 오디오 매니저에서 스피커 모드를 Mono나 Raw로 합시다.

로드 타입

메모리 많이 먹고 빠릿함 vs 메모리 적게 먹고 오버헤드

  • Decompress On Load

미리 압축을 해제해서 메모리에 적재하기에, 플레이 하는 순간에 오버헤드가 생기지 않습니다.

적은 용량의 효과음에 사용합시다. 압축을 미리 메모리에 풀기 때문에 메모리를 많이 먹기 때문입니다.

  • Compress On Memory

메모리에 로드할때는 압축되어 있다가 재생시 압축을 해제합니다.

플레이하는 순간에 압축을 푸는 과정이 생기기에, 오버헤드가 생길 수 있습니다. 메모리에 그대로 적재하기는 힘든 큰 용량의 배경 음악에 사용합시다.

오디오 압축 포맷 (Compression Format)

오디오 압축은 기본적으로 아래의 사항에서 적절한 중간 지점을 선택하는 과정입니다.

높은 품질 + 많은 메모리 사용 + 적은 오버헤드 vs
낮은 품질 + 적은 메모리 사용 + 많은 오버헤드

얻는게 있으면 잃는게 있는법이죠.

압축률이 낮을수록 품질도 높고 오버헤드도 적으나, 빌드 용량과 메모리를 많이 먹습니다.

압축률이 높으면 빌드 용량과 메모리를 적게 먹으나, 압축을 푸는 과정에서 재생이 지연될 수 있습니다.

중요: 유니티에 임포트하기전에 미리 손실 압축을 거는 것은 의미 없습니다!

PCM

품질이 제일 높습니다. 무압축 입니다.

  • 압축을 푸는 과정이 없어서 오버헤드가 없고, 재생이 지연 되지도 않습니다.
  • 빌드 용량과 메모리를 제일 많이 차지합니다.
  • 즉시 재생해야 하는 매우 짧은 효과음에 씁시다.

ADPCM

PCM 과 Vorbis/MP3 중간의 퀄리티와 압축률을 가집니다.

  • PCM 과 비교해서: 높은 압축률, 낮은 품질, 높은 오버헤드
  • Vorbis/Mp3 와 비교해서: 낮은 압축률, 높은 품질, 낮은 오버헤드
  • 어디까지나 사실상 무압축인 PCM 보다 용량이 낮고,  품질이 나쁘다는 얘기입니다.
    일반적인 손실 압축에 비해 품질 유지가 뛰어나고 CPU 오버헤드도 크지 않죠.
  • PCM 보다는 지연과 오버헤드가 존재하지만, Vorbis/MP3 와 비교할시 지연과 오버헤드 없이 즉시 재생됩니다.
  • 총격 소리와 같이 무압축에 가까운 품질 까지는 필요없지만, 지연시간 없이 자주 반복 재생 되야 하는 경우 적절합니다.

Vorbis/MP3

가장 높은 압축률을 가집니다.

  • 당연히 품질이 제일 안좋습니다. 하지만 용량을 제일 덜 먹어요.
  • 로드 혹은 재생시 압축을 풀기 때문에 지연 재생될 수 있습니다.
  • 무압축으로 할시 용량을 감당할수 없고, 지연 재생 되어도 무방한 일반적인 배경음에 씁시다.

2D 게임 텍스쳐 관리하기

중요! TinyPNG 같은거 쓰지마세요.
유니티가 알아서 손실/무손실 압축합니다.

임포트 하기전에 손실 압축 하는 것은 의미가 없어요.

이미지(텍스쳐) 설정

Generate Mil Maps 해제

카메라와의 거리를 실시간으로 조정하는 게임이 아닌 이상 필요 없습니다.

필터링

도트형 레트로 스타일의 게임에 필터를 사용하면 안됩니다!

도트형 게임은 필터를 적용하지 않는 Point 로 적용 합시다. 외곽선을 부드럽게 꺽기 때문에 비주얼상 더 난잡해 보일 수 있어요. 이외에는 무난하게 Linear 면 적당.

아틀라스 aka 스프라이트 팩킹

반드시 사용해야 하는 이유

  • 이미지는 2의 배수의 정사각형으로 메모리에 적재됩니다.
    하나의 이미지에 대해서 많은 여백이 남게되죠.
    이 여백을 낭비하지 말고 다른 이미지로 채워서 메모리를 아낍니다.
  • 자주 사용되는 이미지 끼리 하나의 파일로 뭉쳐 한번에 호출하면, 호출을 개별로 여러번 할 필요가 없어서 성능이 절약됩니다.

결론: 메모리와 성능이 절약됩니다.

사용방법

  1. Packing Tag 를 팩킹할 이미지 끼리 같은 이름으로 지정해주면 유니티가 알아서 팩킹해줍니다.
  2. 그외 옵션은 이곳을 참고할수 있습니다. http://lhh3520.tistory.com/350
  3. Resources 폴더의 텍스쳐는 엔진 구조상의 문제로 유니티 기능만으로는 팩킹 불가능합니다.

이미지 압축 (Compression) – 안드로이드 기준

상식

압축 포맷이 빌드 타켓 플랫폼과 호환되는지 확인하세요!

호환되지 않는 포맷은 해당 플랫폼에서 무압축으로 변환되어 사용됩니다. 그리고 압축된 포맷을 무압축으로 해제하는 과정에 오버헤드가 발생합니다.

=> 처음부터 무압축으로 올렸을 때 보다 오버헤드가 더 생깁니다.

알파 값이 없는 이미지라면, 알파값을 지원하지 않는 압축 포맷을 사용합니다.

알파값을 쓰지도 않는데, 알파값에 데이터를 할당하므로 RGB 컬러 퀄리티는 상대적으로 손상될수 밖에 없습니다.

특정 플랫폼에 무관하게 압축을 설정할시

Override 를 사용하지 않고 일괄 적용하는 경우입니다.

  • 압축 옵션의 포맷은 알파값이 있다면 Truecolor 아니면 Compressed 옵션을 추천 합니다.
  • 알파값이 있는 이미지는, Compressed 옵션으로 아낀 용량에 비해, 품질 손상이 좀 많이 심합니다.
  • 반대로 알파값이 없는 경우, 큰 용량 절감 효과를 보여주고 상대적으로 알파값이 있는 경우보다 품질 손상도 적습니다.
    • 알파값이 없는데 알파값이 있는 이미지로서 인식됬다면 Advanced에서 RGBA가 아닌 RGB 압축 포맷으로 지정해줍시다.

ETC2 vs ETC1

안드로이드의 메이저한 압축 포맷은 ETC2 와 ETC1 입니다.

  • ETC2 는 더 나은 성능과 품질을 제공하지만 OpenGL ES3.0 에서 지원됩니다.
    • 즉 안드로이드 4.4 버전 이하에서는 제대로 동작하지 않는 경우가 많습니다.
  • ETC1 은 OpenGL ES2.0 에서 지원되나, 알파 (투명도) 를 지원하지 않습니다.
    • ETC1 을 설정할 경우 Spilt Alpha Channel 옵션을 통해 알파를 분리한 또다른 파일을 생성하여 알파를 지원하는 꼼수가 있습니다.
      • 다만 이 옵션은 버그인지, UGUI 에서 사용할시 비정상적으로 표현됩니다. (유니티 5.6.1 기준)
  • ECT1 지원폰도 ETC2 호환은 됩니다. 다만 무압축으로 변환되서  메모리가 터집니다.

무엇을 선택할 것인가 에 대한 딱 떨어지는 답이 없어요.

  • ETC2 가 대세긴 하지만 아직도 ETC1 만 지원하는 석기시대 안드로이드 폰의 점유율이 작지 않기 때문에, 타겟을 잘 고려해야 합니다.


ES2.0 의 징그러운 생존율.

안드로이드 고유의 파편화 때문에 조금 걸러 볼 필요는 있습니다. 예를 들어, 대시보드에서 ES2.0 이라고 나와도 ES3.0 을 제한적으로 지원하는 경우도 많으니까요.

이외의 압축 포맷

RGBA32

  • 무압축 입니다. 가장 높은 품질을 가집니다. 메모리를 많이 먹지만, 품질을 높게 유지해야 하는 경우 추천 합니다.
  • 특히 유저가 매우 자주 보게 될 UI 에서는 이 옵션을 사용합시다.

RGBA16

  • 알파 품질이 조악합니다.
  • 제한된 16비트에 RGB 는 물론 A 채널까지 포함하니 품질이 나쁠 수 밖에 없죠.

RGB16

  • 쓸만한 압축률과 좋은 컬러 품질을 보여 줍니다.
  • RGBA 와 달리 16비트를 온전히 RGB에 다 쓰기 때문이죠. 물론 알파 값은 지원 못합니다.

RGB24

  • RGBA32 와 동등한 품질을 보여줍니다. 단 알파값을 지원 못합니다.

기타 참고사항

압축시 2의 제곱수를 변길이로 가진 정사각형으로 이미지를 사용합니다.

여백을 활용하기 위해 정사각형에 가깝게 이미지를 제작하는 걸 고려할 수 있겠네요.

  • 예를 들면 900×1600의 해상도를 가진 물체라면 1024×1024와 2048×2048 중에 maxSize를 고민하게 됩니다. 전자는 화질이 손상되고 후자는 낭비니까요.
  • Sprite Packing 기능으로 묶어서 여백을 활용해도 되고요.

Compress Quality 를 통해 손실 압축 포맷들은 압축의 품질을 결정할 수 있습니다.

당연히 Best 에 가까울수록 동일 품질에 대한 압축률이 좋아지겠지만, 유니티 에디터 상에서 최초 압축 및 팩킹시 매우 많은 시간을 소비합니다.

  • 즉시 빌드를 해야할때, 긴 압축시간으로 피볼수도 있습니다.

UGUI 오버헤드 관리

하나의 캔버스에 모든 UI 요소를 넣는 것이 꼭 좋은것은 아닙니다.

캔버스에서 어떤 UI 요소의 스프라이트가 동적으로 변할때, 캔버스에 있는 전체 요소들도 동시에 갱신됩니다.

즉 하나가 변하면, 같은 캔버스에 다른 친구들도 같이 갱신됩니다.

한 UI 오브젝트가 변해도, 갱신 비용은 같은 캔버스 밑에 모든 UI 오브젝트 만큼 드는 거죠.

  • 따라서 서로 같은 타이밍에 내용이 (텍스트나 이미지가) 바뀌는 오브젝트 끼리 같은 캔버스로 묶으면 성능에 좋습니다.

그러나 개발자가 직관적으로 편집하기 좋게 하이라키에 배치하는 것도 고려해야 하니, 어디까지나 케이스 바이 케이스.

폰트

  1. 완성형 한글의 모든 글자를 메모리에 집어넣는 메모리 빌런이 되고 싶지 않다면 Dynamic을 사용합시다. 폰트를 임포트할때 기본값으로 Dynamic 이 지정됩니다.
  2. 만약 해당 폰트를 꼭 써야하는게 아니라면 Incl. Font Data를 해제해도 됩니다. 해당 폰트가 시스템에 존재하면 사용하고, 없으면 시스템 폰트를 가져다 쓸것입니다. 빌드 사이즈가 줄어듭니다.
    다만 의도한 디자인을 유지하기 힘들어서, 이 방법을 추천하진 않네요.

물리 설정

  1. 물리, 레이캐스팅을 사용하지 않는 형태의 게임이라면 충돌 레이어 설정을 해제해도 됩니다. 프로젝트 세팅 중 Physics 에서 Collision 레이어 항목 체크를 해제하면 되죠.
  2. FixedUpdate는 랜더와 상관없이 물리를 위해 독립적으로 일정 간격 호출되는 함수입니다.
    리기드 바디 등을 사용하지 않는 게임이라면, 물리 체크를 자주 할 필요가 없겠죠. 필요에 따라 호출 간격을 길게 해서 성능을 아낄수 있습니다.

기타 & 빌드시 세팅

  1. 절대 Keystore 파일 잃어 먹지 마세요. (미아앱 되기 싫으면 클라우드 저장소에 백업 좀 하셈)
  2. 유니티가 빌드시 알아서 사용되지 않는 애셋은 제외합니다.
    • 하지만 스크립트 파일은 사용되든 사용되지 않든, Editor 폴더에 있지 않는한, 모두 포함되서 빌드됩니다.
  3. Resource 폴더는 안쓸수 있다면 최대한 쓰지 맙시다.
    • 여기 들어가는 요소들은 실제 사용되든 사용되지 않든 빌드에 무조건 포함되고 메모리에 적재됩니다.
  4. 안드로이드 빌드시 세팅
    • x86(데스크탑 안드로이드) 를 사용하는 기기는 거의 없습니다.
    • FAT 은 x86과 ARM 을 모두 빌드하므로 용량이 커집니다.
      • FAT을 하지 말고 ARM 대상으로만 빌드합시다.
      • 혹은 x86 과 ARM 을 따로 빌드해서 구글 플레이 콘솔에 멀티 APK 로 올려도 됩니다.
    • Stripping Level의 경우 가능한 높은 수준을 추천합니다. 다만, micro nmscorlib이나 byte code 세팅이 프로그램을 크래쉬할 수 있으니 테스트는 해봅시다.
    • 32비트의 컬러 품질이 필요한지 비교후, 그렇지 않다면 체크 해제합니다.
    • 호환에 문제가 없다면 빌드 용량을 줄이기 위해 기본값인 .NET 2.0 Subset 을 사용합니다.
  5. 클라우드 테스트와, 안드로이드 실제 기기 테스트를 둘다 애용 합시다.
    • 쓸만한 무료 클라우드 테스트로는 구글 플레이 콘솔에서 지원하는 “출시전 보고서” 기능이 있습니다.
    • 즉시 사용 가능한 유료 클라우드 테스트는, 아마존 디바이스팜과 파이어베이스 테스트 랩이 있습니다.

계속 갱신중.

모든 글보기

홈으로 돌아가기

  1. Dooseop Choi: 공감하는 글입니다. 학점은 좋았으나 코딩실력은 부족했습니다. 다만, 예화로 들려주신 분처럼 팀별 프로젝트를 할 때 파일이…Commented on: 학점은 정말 믿을게 못된다.
  2. 이제민: 1. 본래 네임스페이스와 상관없이 동작해야 하는게 맞습니다. 2. 제 생각에는 네임스페이스 문제가 아니라, VSCode로 열린…Commented on: 유니티에 비주얼 스튜디오 코드 (Visual Studio Code; VSCode) 연동방법
  3. 호나: 오류는 해결했는데 … 자동완성도 안되고 책에 있는 내용중 5장 게임 오브젝트 제어하기 파트에 p.185 ~…Commented on: 유니티에 비주얼 스튜디오 코드 (Visual Studio Code; VSCode) 연동방법
  4. 이제민: 만들고 싶은 것만 찾을 수 있다면 학점이나 시작하는 시기는 별로 상관 없습니다. 실제로 신경 쓰는…Commented on: 학점은 정말 믿을게 못된다.
  5. 뒷이야기: 딱 제 얘기네요. 학점은 4점대인데 코딩 실력은 참 형편없습니다. 후우…Commented on: 학점은 정말 믿을게 못된다.

SearchIndependent Publisher empowered by WordPress

Unity Gradle 빌드에 관하여

Unity 로 앱 및 게임을 구현하는데 있어

안드로이드 Native 기능을 사용하고자 하는 경우,

Plugin 형태(jar, aar)로 만든후 프로젝트에 import 하여 빌드해서 사용하게 된다.

그리고 최근 안드로이드 Native 기능들의 경우 대다수 모듈들이

Gradle 등의 빌드 툴을 통해 그때 그때 필요한 내용을 다운받아 사용하게 되어있다.

이러한 점 때문에 빌드시 문제가 생길 소지가 있다.

Unity가 빌드하는 동안 Native 모듈에서 사용하는 라이브러리를 다운받지 못해 

필요한 Class를 찾지 못하는 등의 문제를 발생시키기 때문입니다.

이를 해결하기 위해 Untiy에는 Gradle 빌드를 지원하고 있다.

이 Gradle 빌드를 사용하는 경우 개발자는 보통 다음의 수순을 따르게 된다.

1. Build System을 Gradle로 설정 및 Export Project 체크 하여 빌드 진행

2. Export 된 프로젝트를 안드로이드 스튜디오로 열고 빌드 관련된 사항들 체크

    (build.gradle 내용 확인 및 컴파일 이슈 확인)

3. 최종 빌드 확인 후 유니티 상에서 build.gradle 파일 수정

4. Export Project 체크 해제 후 빌드 진행.
   (이 시점부터 안드로이듯 스튜디오로 프로젝트를 열어서 빌드할 필요 없음)

하나씩 자세히 살펴보면 다음과 같다.

1. Build System을 Gradle로 설정 및 Export Project 체크 하여 빌드 진행

우선 유니티에서 빌드를 진행하며 에러나는 상황을 확인하는 것은 스트레스가 크고 시간이 오래걸린다.

그래서 우선 Build System을 Gradle로 설정하고 Export Project 를 체크하여 안드로이드용 프로젝트를 추출하는것을 우선으로 한다.

2. Export 된 프로젝트를 안드로이드 스튜디오로 열고 빌드 관련된 사항들 체크

    (build.gradle 내용 확인 및 컴파일 이슈 확인)

추출된 프로젝트를 안드로이드 스튜디오로 열고 빌드를 진행하여,

에러 상황을 체크하고 

build.gradle 파일의 repositories 및 dependencies의 내용을 추가 및 수정한다.

이 단계에서는 발생되는 에러가 있어서 (에러가 기억이 안나요…ㅠㅠ) 

보통은 아래 그림과 같이 google(), jcenter() 순서로 기입을 하고 시작하는 것이 좋다.

그 외에 빌드 후 테스트 하는 경우 

유니티가 뽑아주는 build.gradle은 Debug 빌드시에는 Key Signing 하는 부분이 생략되어 있기 때문에, apk 설치시 에러(INSTALL_FAILED_NO_MATCHING_ABIS)가 발생할 수 있다. 

이를 미연에 방지하기 위해 다음의 코드를 추가하는 것이 좋다.

여기까지 빌드 하고 확인이 되었다면 유니티로 돌아가자.

3. 최종 빌드 확인 후 유니티 상에서 build.gradle 파일 수정

이제 유니티 상에서 빌드를 할 수 있도록 build.gradle을 설정해야 한다.

PlayerSerttings -> Publishing Settings로 가면 Custom Gradle Template이라는 항목이 존재한다.

이것을 체크해야 유니티에서 빌드시 사용하는 build.gradle 파일을 수정할 수 있다.

체크하면 Assets/Plugins/Android/mainTemplate.gradle 파일이 생기며 이 파일의 내용을 수정하면

Gradle 빌드시 이 파일을 기반으로 빌드를 진행하게 된다.

mainTemplate의 내용에 추가할 것들은  

앞서 [2. Export 된 프로젝트를 안드로이드 스튜디오로 열고 빌드 관련된 사항들 체크] 과정에서 진행했던 안드로이드 스튜디오 프로젝트의 build.gradle에 추가한 것들을 옮겨주면 된다.

[ 샘플 파일을 올려두었으니 참고 할 것 ]

4. Export Project 체크 해제 후 빌드 진행. 
   (이 시점부터 안드로이듯 스튜디오로 프로젝트를 열어서 빌드할 필요 없음)

이제 Export Project 체크를 해제하고 빌드를 진행하면 안드로이드 스튜디오 프로젝트 빌드 없이도 정상적으로 빌드되는 것을 확인할 수 있다.

하지만 사용하는 라이브러리 버전에 따라서 

빌드중에 다음과 같이 Gradle 버전 에러가 나는 경우가 있다.

 CommandInvokationFailure: Gradle build failed. C:\Program Files\Java\jdk1.8.0_181\bin\java.exe -classpath “C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Tools\gradle\lib\gradle-launcher-4.2.1.jar” org.gradle.launcher.GradleMain “-Dorg.gradle.jvmargs=-Xmx2048m” “assembleDebug”

이 경우에는 유니티 내의 Gradle 버전을 올리면 해결할 수 있다.

유니티 기본 설치폴더 및 안드로이드 스튜디오 기본 설치 폴더를 기준으로 

C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Tools\gradle 내의 lib 폴더(유니티 내에서 사용중인 Gradle 버전)를 

안드로이드 스튜디오

C:\Program Files\Android\Android Studio\gradle\gradle-4.6 폴더 내의 lib 폴더(안드로이드 스튜디오에서 사용한 최신 버전)로 교체하면 된다.

이 작업시에는 유니티를 종료하고 작업해야 한다.

Script Dependencies Function for Unity

using UnityEngine;
using UnityEditor;
using System.Collections; 

public class ComponentLister : EditorWindow
{
    bool forceRefresh=false;

    private bool onlyScripts=false;
    private string filterComponent="";
    private SortedList sets = new SortedList(new myReverserClass());
    private ArrayList scripts = new ArrayList();
    private Vector2 scrollPosition;

   public class myReverserClass : IComparer  {

      // Calls CaseInsensitiveComparer.Compare with the parameters reversed.
      public int Compare( System.Object x, System.Object y )  {
          return ( (new CaseInsensitiveComparer()).Compare( ((System.Type)x).Name, ((System.Type)y).Name ) );
      }
   }

    [ MenuItem( "Component/Component lister" ) ]
    public static void Launch()
    {
        ComponentLister window = (ComponentLister)GetWindow( typeof( ComponentLister ) );
        window.forceRefresh=true;
        window.Show();
    } 


    public void UpdateList()
    {
        Object[] objects; 

        sets.Clear(); 
        scripts.Clear();

        objects = FindObjectsOfType( typeof( Component ) );
        foreach( Component component in objects )
        {
            if (component is MonoBehaviour) {
                //Debug.Log(component.GetType() + " attached to "+component.name);
                scripts.Add(component.GetType());
            }

            if( !sets.ContainsKey( component.GetType() ) )
            {
                sets.Add(component.GetType(),new ArrayList());
            } 
            int i=sets.IndexOfKey(component.GetType());
            ( ( ArrayList )sets.GetByIndex(i)).Add( (string)component.gameObject.name );
        }

        foreach( Component component in objects )
        {
            int i=sets.IndexOfKey(component.GetType());
            ( ( ArrayList )sets.GetByIndex(i)).Sort();
        }       

    } 

    public void OnHierarchyChange() {
        //Debug.Log("OnHierarchyChange");
        UpdateList();
        Repaint();
        forceRefresh = true;
    }

    public void OnGUI()
    {
        GUILayout.BeginHorizontal( GUI.skin.GetStyle( "Box" ) );
            GUILayout.Label( "Components in scene:" );

            GUILayout.FlexibleSpace();

            if( GUILayout.Button( "Refresh" ) || forceRefresh )
            {
                //if (forceRefresh) Debug.Log("forceRefresh");
                forceRefresh=false;
                UpdateList();

            }
        GUILayout.EndHorizontal();

        GUILayout.BeginHorizontal( GUI.skin.GetStyle( "Box" ) );
        filterComponent = EditorGUILayout.TextField ("Filter:", filterComponent);
        GUILayout.EndHorizontal();

        GUILayout.BeginHorizontal( GUI.skin.GetStyle( "Box" ) );
        onlyScripts = EditorGUILayout.Toggle("Show only scripts", onlyScripts);
        GUILayout.EndHorizontal();

        scrollPosition = GUILayout.BeginScrollView(scrollPosition);


            foreach( System.Type type in sets.Keys )
            {   
                if (type.Name.ToLower().Contains(filterComponent.ToLower())) {
                    bool showGameObjects=false;
                    if (scripts.Contains(type)) {
                        GUI.contentColor = Color.yellow;
                        GUILayout.Label( type.Name + " (Script):" );
                        showGameObjects=true;
                    }
                    else if (!onlyScripts){
                        GUI.contentColor = Color.white;
                        GUILayout.Label( type.Name + ":" );
                        showGameObjects=true;
                    }

                    if (showGameObjects) {
                        GUI.contentColor = Color.white;
                        foreach( string gameObject in ( ArrayList )sets[ type ] )
                        {
                            if( GUILayout.Button( gameObject ) )
                            {
                                Selection.activeObject = GameObject.Find(gameObject);
                            }
                        }
                    }
                }
            } 

        GUILayout.EndScrollView();
    }
}