Gradle 3.X Migration


UE4 4.20 으로 마이그레이션하면 "arr-imports.gradle" 파일 같은 곳에서 gradle build 에러가 날 수 있습니다. 원래 UE4 가 gradle 2.x 를 사용하고 있었는데, 4.20 에서 3.x 버전으로 변경하면서 "Build" 디렉토리에 있는 템플릿 "build.gradle" 파일과 Android 용 build-tool 코드에 변경이 있었습니다.


이 때문에 매우 다양한 에러가 발생할 수 있는데요, 대표적인 것이 implementation() 과 관련한 컴파일 에러입니다. 2.x 에서는 implementation() 이 없습니다. 3.x 에서 compile() 이 implementation() 으로 대체되었죠. 그래서 implementation() 을 찾을 수 없다고 에러가 납니다. 만약 그 에러가 뜬다면 gradle 이 3.x 로 갱신되지 않은 것입니다. 그러므로 Setup.bat 을 실행해서 새로운 build.gradle 파일을 받아야 합니다.


그 외에 UPL 을 통해 직접 gradle 코드를 삽입한 경우 compile() 을 implementation() 으로 변경하셔야 합니다. 3.x 버전에서는 compile() 이 에러까지는 아니지만 경고를 뱉습니다.


그 외에도 여러 가지 에러가 있을 수 있는데요, [ Gradle용 Android 플러그인 3.0.0으로 마이그레이션 ] 을 참고하시기 바랍니다.


Crashlytics Symbols 폴더 변경


아마 Crashlytics 를 사용하시는 분들은 당황하셨으리라 생각하는데, UE4 가 기존의 "jni" 와 "obj" 폴더를 제거하고 "jniLibs" 라는 폴더로 바이너리 출력 폴더를 통합했습니다. 아마도 4.20 에서 Android Studio 를 통한 디버깅 기능 개선을 하면서 정리를 한 것이 아닌가 싶습니다.


"jniLibs" 폴더는 debug symbols 를 포함하고 있으니, 반드시 폴더를 바꿔줘야 합니다. 그리고 "Libs" 에는 심볼이 없기 때문에 debug 든 release 든 모두 "jniLibs" 를 사용해야 합니다.



개요

 

Gradle 을 사용하다가 보면 처음에 가장 이해하기 어려운 것이 dependency 입니다. 

 

특히 UE4 를 사용해서 cross-platform 작업을 하시는 분들은 AutomationTool 을 사용하고 있기 때문에 내부가 어떻게 돌아가고 있는지 잘 모르는 경향이 있습니다. 사실 저도 XBox 와 PS4 쪽 작업을 할 때는 그런 고민을 별로 안 했습니다.

 

그 플랫폼들은 자체 포맷으로 packaging 하는 것을 제외하고는 Neighborhood 와 같은 도구를 통해 쉽게 deploy 할 수 있도록 해 줍니다. 뭔가 추가적인 모듈을 사용하고 싶다면 이미 배포된 SDK/XDK 의 것을 가져다 쓰면 됩니다. 즉 C++ 코드 내에서 작업이 수행됩니다.

 

문제는 Android 입니다( IOS 는 제가 잘 모르겠습니다 ). 일단 Android 에서는 프로그램 실행을 위한 기본 언어가 Java 입니다. Native C++ 을 사용할 수 있도록 NDK( Native Development Kit ) 를 제공하고 있지만, 이건 그냥 라이브러리라고 생각하시면 됩니다. 물론 몇 가지 디버깅 툴이 있기는 하지만 이 문서의 주제와는 관련이 없으므로 그냥 넘어 가도록 하겠습니다. 어쨌든 UE4 에서 작업해서 나온 "libUE4.so" 파일을 사용하기 위해서는 JNI( Java Native Interface )를 통해서 Java 코드와 연관시켜야 합니다.

 

사실 이런 모든 작업은 AutomationTool 을 통해서 수행됩니다. 하지만! BUT! 본인이 UE4 에서 제공하는 모듈 이외의 것을 사용하려고 한다면, 그때부터는 지옥이 열립니다.

 

저 스스로에 대해서 한탄스럽게 생각하는 거긴 하지만, 일반적으로 Windwos 개발자들은 MAKE, CMake, Ant, Gradle 과 같은 도구 사용에 익숙하지 못합니다. 그런 게 존재하는지도 모르는 경우가 많죠.

 

우리는 Microsoft 의 Visual Studio 는 Windows 플랫폼에서 개발하기 위한 기본 설정들을 모두 해 줍니다. 하지만 외부 라이브러리 소스들을 받으면 조금 어렵습니다. "*.sln" 이나 ".vcxproj" 같은 구성 파일들이 없으면 힘들죠. 이 상황에서 매우 쉬운 길을 택하면 그냥 자기가 가지고 있던 프로젝트에 전부 포함시켜 버립니다. 그나마 좀 깔끔하게 라이브러리로 사용하겠다는 분들은 직접 프로젝트를 만들어서 넣습니다.

 

그런데 수작업하는 것도 규모가 작은 소스를 사용할 때의 이야기지 엄청난 규모의 소스를 사용하고 있고, 그 소스들이 내부적으로 서로를 DLL 로 참조해야 하는 제약이 있다거나 하면 '멘붕'에 빠지게 됩니다.

 

이를 좀 더 쉽게 할 수 있도록 해 주는 것이 CMake 입니다. 하지만 "알아야 면장이라도 한다"는 속담이 있듯이, 내가 원하는 대로 설정하려면 최소한의 기본지식은 있어야 하는데 지금까지 경험해 보지 못한 용어와 구조때문에 머리가 아픕니다. Visual Studio 에서는 NMAKE 프로젝트라는 것이 나오기도 했습니다. 현재 UE4 빌드시스템이 NMAKE 를 사용하고 있죠.

 

그나마 Windows 플랫폼에서 C++ 작업을 좀 해 보신 분들이라면 NMake 에는 익숙할 겁니다. 하지만 Ant, Gradle 은 신세계입니다. 기본적으로 Java 를 위한 프로젝트를 구성하고 컴파일, 배포까지 수행합니다. 뭔가 하는 일이 어마어마하게 많죠.

 

Windows 가 개발자에게 매력있게 다가올 수 있는 이유는 엄청난 자동화와 매우 편리한 IDE 일 것입니다. 하지만 오픈소스 프로젝트들은 누가 본격적으로 관리하지 않는 오픈소스답게 초심자들에게는 불편한 점이 많습니다. 그래서 Ant, Gradle 같은 자동화 도구들을 만드는 것 같은데, Visual Studio 를 사용하는 입장에서는 매우 불편하게 느껴지는 것이 현실입니다. 그리고 Windows 플랫폼에서 응용프로그램 개발을 하는 분들이라면 C# 이나 C++ 를 사용하지, Java 는 거의 사용하지 않을 겁니다( 물론 사용하는 분들이 있기는 하겠지만 적어도 게임업계에서는 그렇습니다 ).

 

서론이 길었는데요, 어쨌든 UE4 에서 Android 개발을 하는데, 뭔가 커스텀한 일을 수행하고자 한다면, Java 와 Gradle 은 필수라는 겁니다. 예전에는 Ant 가 기본 빌드 시스템이었는데, 최근에 Gradle 로 갈아탔다고 하더군요.

 

최근에 회사에서 Gradle 에 대해서 분석하고 해 봤는데, 제가 형식에만 집중하고 알맹이는 쏙 빠뜨렸다는 생각이 들어서 공부를 더 하고 있습니다. Gradle build script, Groovy, UPL, 이 세 가지 부분에 집중했는데, 정말 중요한 부분을 빠뜨리고 공부했습니다. 사실 앞에서 언급한 것들은 기본으로 알아야 하는 것이고, dependency 야 말로 가장 중요한 부분입니다. 제가 겪은 대부분의 에러는 이쪽에서 발생했습니다( 물론 제가 경험이 엄청 많다는 이야기는 아닙니다 ).

 

이 문서에서는 Gradle 의 dependency 개념에 대해 소개하고, UPL 과 dependency 가 어떻게 연관되는 지 설명할 것입니다. 아직 Gradle 의 기본 개념과 Groovy 문법에 대해서 모르신다면, 공부를 하고 보시는 것이 좋을 것 같습니다. 제가 그런 부분에 대해서까지 설명하기에는 좀 부족함이 많네요. UPL 에 대해서는 제 블로그의 [ UnrealPluginLanguage ( UPL ) 이란 ] 에 정리해 뒀습니다.

 

 

Gradle Dependency

 

Dependency 를 매우 저렴하게 표현하자면, Visual C++ 에서 볼 수 있는 헤더 경로와 라이브러리 경로입니다. 예를 들어 다음과 같은 호출을 생각할 수 있겠죠.

 

 

내가 사용하고자 하는 모듈의 헤더와 라이브러리를 포함하지 않고서는 해당 모듈이 제공하는 기능을 이용할 수 없습니다. 컴파일 에러나 링크 에러가 나겠죠. 그러면 이런 헤더와 라이브러리는 어디에서 구해야 하는 걸까요? 보통 VS 로 개발할 때는 소스코드를 아예 프로젝트에 포함시키거나, 소스코드의 라이브러리를 받아서 import 합니다. 혹은 라이브러리를 그대로 받아서 import 하죠.

 

Linux 에 관심이 있어서 깔아 보신 분들이라면, 아래와 같은 형식으로 패키지를 설치하는 걸 보신 적이 있을 겁니다.

 

 

특정 플랫폼을 대상으로 하는 특정 버전의 gzip 을 설치하는 거죠. 이렇게 하면 자동으로 http 를 통해 설치하고자 하는 패키지의 소스코드를 다운로드해서 빌드해서 설치해 줍니다. 참 쉽죠?

 

어쨌든 Linux 같은 오픈소스 진영에서는 외부에 저장소( repository )를 두고 거기에서 소스를 다운로드 받는 것이 일반화되어 있는 것 같습니다. Linux 는 매우 변종이 많기 때문에 정확한 플랫폼과 버전을 지정하지 않으면 안 되겠죠. Windows 야 자기들이 OS 를 만드니까 알아서 패치할 수 있지만, Linux 에서는 쉽지 않습니다. Ubuntu 같은 OS 들도 자기들이 원하는 건 설치하겠지만, 남들이 만들어 놓은 것은 어떻게 할 수 없겠죠.

 

Gradle 은 아래 그림에서 보이듯이 gradle 이 dependency 를 받는 방법은 3 가지 입니다. Network 를 통해 외부 repository 에서 받는 경우가 있고, local repository 에서 받는 경우가 있습니다. 마지막으로 이미 netowrk 를 통해 받은 repository 를 caching 해 놓고 거기에서 가지고 오는 경우가 있습니다.

 

 

 

머리가 아프실테니, 이야기를 더 진행하기 전에 기본 용어부터 정리하도록 하겠습니다. 아래 인용부분은 [ 2 ] 의 번역입니다.

 

Configuration

 

특정 목적을 위해 그룹화된 dependency 들의 named set 을 의미합니다: 예를 들어 "implementation" configuration 은 프로젝트를 컴파일하는 데 요구되는 dependency 들입니다. Configuration 은 module 과 artifact 에 대한 접근을 제공합니다. Configuration 이라는 말은 dependency management 문맥 바깥에서는 다른 의미를 가질 수 있으니 주의하세요.

 

Dependency

 

Module 을 빌드하고 테스트하고 실행하는 데 요구되는 소프트웨어의 일부에 대한 pointer 입니다.

 

Dependency constraint

 

Dependency constraint 는 어떤 dependency 에 대해서 module 이 유효한 결과를 산출하도록 하기 위해 필요한 요구사항을 의미합니다. 예를 들어 dependency constraint 는 지원되는 module version 집합을 한정할 수 있습니다. Dependency constraint 는 transitive dependency 에 대한 요청과 같은 것을 표현하는 데 사용될 수도 있습니다. 더 많은 정보를 원하신다면, "Managing versions of transitive dependencies with dependency constraints" 항목을 참조하세요.

 

Module

 

Google Guava 와 같은 소프트웨어 조각을 의미합니다. 이것은 시간이 지남에 따라서 바뀌고 있고 모든 버전을 포함한 개념입니다. 모든 module 은 이름을 가지고 있습니다. Module 의 각 배포본( release )은 module version 에 의해 표현됩니다. 편리하게 사용될 수 있도록 하기 위해, module 은 repository 에서 호스팅될 수 있습니다.

 

Module metadata

 

Module 배포본은 metadata 를 제공할 수 있습니다. Metadata는 module 에 대해 자세히 기술하는 데이터입니다. 예를 들어 repository 의 좌표( coordinate )라든가, project 나 요구되는 transitive dependency 에 대한 정보같은 것들이 있습니다. Maven 에서 metadata 파일은 ".pom" 이라 불리며, Ivy 에서는 "ivy.xml" 이라 불립니다.

 

Module version

 

Module version 은 배포된 module 이 변화할 때마다 만들어진 독립된 집합을 의미합니다. 예를 들어 18.0 은 com.google:gujava:18.0 이라는 좌표를 사용하는 module version 임을 의미합니다. 경험적으로 볼 때, module version 의 스키마에 대한 제약은 없습니다. 시간, 숫자, "-GA" 같은 특별한 접미어들이 식별자로 허용되고 있습니다. 가장 널리 사용되고 있는 versioning 전략은 "semantic versioning" 입니다.

 

Repository

 

Repository( 저장소 ) 는 module 집합을 호스팅합니다. 각각은 module version 에 의해 식별되는 하나 이상의 배포본들을 제공합니다. Repository 는 binary repository product( 예를 들어 Artifactory 나 Nexus ) 나 파일 시스템의 directory sturcture 에 기반하고 있습니다. 더 많은 정보를 원한다면 "Declaring repository" 를 참조하세요.

 

Resolution rule

 

Resolution rule 은 dependency 가 풀리는( resolved ) 방식에 영향을 줍니다. Resolution rule 은 빌드 로직의 일부로 정의됩니다. 더 많은 정보를 원하신다면 "Customizing Dependency Resolution Behavior" 를 참조하세요.

 

정리하자면 어떤 module 은 module version 과 함께 배포( release )되며, 이것은 repository 좌표를 찍어서 얻어 올 수 있는 겁니다.

 

Declaring Repository

 

그러면 여기에서 "어떤 repository 들이 있을가?" 라는 의문이 생기겠죠. Repository 의 종류는 "Repository Types" 문서에서 찾아 볼 수 있습니다. 일반적으로는 세 가지 종류의 repository 를 사용하고 있습니다.

 

출처 : [ 3 ]

 

예를 들어 JCenter 를 사용한다고 하면, Gradle 빌드 스크립트에 다음과 같이 적습니다.

 

 

하지만 Crashlytics 와 같은 특정 서비스들은 자신만의 repository 를 제공하기도 합니다. 그런 경우에는 URL 을 통해 접근해야 하죠.

 

 

URL 위쪽에 "maven" 이라는 closure 로 묶여 있는 것을 볼 수 있는데요, 이것은 RepositoryHandler 라고 부릅니다. "Repository Types" 에서 지정하고 있는 repository 에 대해 구체적인 정보를 설정할 수 있게 해 주는 것이죠.

 

Declaring Dependency

 

이제 사용할 dependency 를 선언해야 합니다. 일반적으로는 module 제공하는 사람들이 repository 와 version 을 알려줍니다. 예를 들어 Fabric Crashlytics 같은 경우에는 documentation 에서 이런 예제를 제공하죠.

 

 

 

첫 번째 코드는 gradle 프로젝트의 "base/build.gradle" 에 입력할 내용이고 두 번째 코드는 "library/build.grale" 에 입력할 코드입니다.

 

보시면 알겠지만 하나는 "dependencies" closure 에 "classpath" 라는 식별자를 사용하고 있고, 다른 하나는 "compile" 이라는 식별자를 사용하고 있습니다. 이 두 가지는 차이가 있습니다. "classpath" 는 플러그인에 접근하는 데 사용하는 경로입니다. 하지만 "compile" 은 라이브러리에 접근하는 데 사용합니다.

 

여기에서 구체적으로 언급하기에는 너무 내용이 많습니다. 그러므로 "Gradle User Guide" 에 가서 링크를 찾아 다니면서 내용을 숙지하시는 것이 좋습니다. 여기에서는 dependency 가 뭐고 UPL 과 어떤 관계를 가지는 지에 대해서만 정리할 것입니다.

 

UPL 과 Dependency

 

 

UnrealPluginLanguage 는 크게 "AndroidManifest.xml", "buildAddition.gradle", "*Activiy.java" 파일들을 수정할 수 있도록 해주는 일종의 스크립트입니다. 가장 쉬운 예제는 "Engine/Source/Runtime/Advertising/AndroidAdvertising" 에 있는 "AndroidAdvertising" 모듈에서 찾아 볼 수 있습니다.

 

이 모듈에서 핵심적인 파일은 두 개 입니다.

 

 

"AndroidAdvertising.Build.cs" 에서는 "AndroidAdvertising_APL.xml" 을 등록합니다. 여기에서 "UPL" 이 아니라 "APL" 이라는 이름을 사용해서 헷갈리는 분들이 계실텐데요, "APL" 은 "Android Plugin Language" 의 약자로 UPL 의 전신입니다. Android 전용으로만 사용되었을 때 사용하던 용어입니다.

 

어쨌든 다음과 같이 UPL 을 등록합니다.

 

 

그러면 AutomationTool 에서 Android BuildTool 을 실행하면서 이 UPL 파일을 평가하게 되는거죠. 이 UPL 파일 안쪽에 보면, "<buildGradleAdditions>" 엘리먼트에서 다음과 같이 "buildAddtions.gradle" 파일에 코드를 삽입하는 것을 볼 수 있습니다.

 

 

이건 또 "implementation" 이라는 키워드를 사용하는데요, 프로젝트 컴파일에 사용하는 구성이므로 그렇습니다.

 

UPL 은 정말 많은 커스터마이징 기능을 가지고 있습니다. 그 목록은 [ UnrealPluginLanguage ( UPL ) 이란 ] 에서 찾아 보세요.

 

참고자료

 

[ 1 ] Introduction to Dependency Management, Gradle Docs.

[ 2 ] Dependency Management Terminology, Gradle Docs.

[ 3 ] Declaring Repositories, Gradle Docs.

UE4 에서 Android 를 위해서 Crashlystics 를 붙여 보려고 하는데, 이를 위해서는 "AndroidMenifest.xml" 과 "build.gradle" 파일이 수정되어야 합니다. 하지만 이 파일들은 packaging 시에 생성되므로, 어떻게 손을 댈 방법이 없습니다.


어떤 사람들은 apk 가 나온 뒤에 Android Studio 같은 도구를 사용해서 바꾸라고 하는데, 저는 이렇게 추가적인 프로세스가 들어 가는 것은 불합리하다고 생각을 했습니다. 그래서 검색을 좀 해 보니, UE4 4.17 에 gradle 지원을 하면서, UnrealPluginLanguage 에 대한 언급을 한게 있더군요.


UE4 4.17 을 릴리스하면서, Gradle 에 대한 지원이 실험 빌드 옵션으로 제공됩니다. 이는 Android Project Settings 의 프로젝트당 설정으로 활성화되는데요, "Enable instead of Ant [Experimental]" 체크박스를 사용합니다. 만약 여러분이 Android SDK 라이선스 동의를 하지 않았다면, "동의" 라는 표시와 함께 다이얼로그가 활성화됩니다. "동의" 를 누르면 적절한 라이선스 파일이 생성될 것이며, Gradle 이 종속성을 다운로드할 수 있게 됩니다.


aar-imports.txt 나 Unreal Plugin Language( UPL )<AARImports/> 를 사용해 등록되는 AAR 파일들이 Gradle 의 종속성으로서 자동으로 추가됩니다. build.gradle 에 대한 추가사항은 UPL<buildGradleAdditions/> 노드를 사용해 만들어질 수 있습니다. build.gradle 을 추가하는 다른 방법은 "additions.gradle" 이라는 파일을 <prebuildCopies/> 노드를 사용해 JavaLibs 에 복사되는 디렉토리 중 하나에 저장하는 것입니다( Engine/Source/ThirdParty/AndroidPermission/permission_library 를 예제로 참고하세요 ).


새로운 Gradle build path 의 유용한 이점은 Android Studio 에서 launch 하거나 packging 한 후에 결과로 나오는 build.gradle 을 로드할 수 있다는 겁니다. 이 파일은 여러분의 프로젝트의 "Intermediate/Android/APK/gradle" 디렉토리에 존재할 겁니다. 이 구현을 사용해서 native debugging 이 가능하지는 않지만, 배치 파일을 먼저 실행해 packaging 으로부터 실행되거나 설치된 OBB 를 가지고 있다면, Run 이나 Debug 버튼을 누를 수 있습니다. Native 코드를 변경하지만 않았다면, Java 코드를 변경하거나 Java 를 디버깅하기 위해 중단점을 설정하거나, 이터레이션을 수행할 수 있습니다. 경고: 이 위치에서는 Java 코드의 복사본을 가지고 작업하고 있기 때문에, UE4 에디터에서 launch 나 package 를 사용하기 전에 정상 위치에 변경사항을 반영하시기 바랍니다. 그렇지 않으면 덮어써질 겁니다!


이 릴리스에서 Gradle 을 사용하다가 문제가 생기면 알려 주세요; Google 이 더 이상 Ant 를 지원하지 않으므로, 그것을 제거하려고 합니다.


출처 : Gradle support in UE4 4.17


여기에서 알 수 있는 건 단지 Gradle 로 빌드 체인을 변경하면서, UnrealPluginLanguage 라는 것을 사용해서 종속성을 추가해야 한다는 것입니다. 어떻게 사용하는 지는 모르겠구요.


찾아 보니, 이것에 대한 공식 문서는 존재하지 않고, 그냥 "UnrealPluginLanguage.cs" 파일에 주석으로 설명해 놨더군요.


UnrealPluginLanguage ( UPL ) 은 간단한 XML 기반 언어로 XML 을 조작하고 문자열을 반환하는데 사용됩니다. 이것은 <init> 섹션을 포함하고 있는데, 이 섹션은 다른 섹션들이 평가되기 전에 아키텍쳐 당 한 번만 평가됩니다. 상태가 보존되고, 다음 섹션들이 평가될 때 이전됩니다. 그러므로 섹션이 실행되는 순서는 중요합니다.


UPL 은 XML 을 수정하고 질의하는 데 있어 일반적인 시스템이지만, 그것은 특별히 플러그인들이 그것이 구성하고 있는 package 의 전역 설정에 영향을 줄 수 있도록 하는데 사용합니다. 예를 들어, 이것은 플러그인이 Android 의 APK AndroidManifest.xml 파일을 수정하거나 IOS IPA 의 plist 파일을 수정할 수 있도록 해 줍니다. UBT 는 플러그인의 UPL xml 파일을 질의해서 Android 의 .java 파일들과 같은 패키지에 있어 일반적으로 포함되어야 하는 파일들에 문자들을 포함시킬 수 있도록 합니다.


만약 여러분이 플러그인 칸텍스트( context )에서 실행될 명령들을 보기 원한다면 추적( tracing )을 활성화하십시오 :


<trace enabled="true"/>


이 명령 이후에는 <trace enable="false"/> 가 실행될 때까지 여러분의 칸텍스트에서 실행되는 모든 노드들이 로그에 기록될 겁니다. 다음 명령을 사용해서 여러분의 칸텍스트에 있는 모든 변수들을 출력할 수 있습니다 :


<dumpvars/>


Bool, Int, String 변수 유형들이 지원됩니다. 모든 애트리뷰트( attribute )들은 대부분 변수를 참조할 것이며, 다음 구문을 사용해 평가되기 전에 문자열로 치환될 겁니다.


$B(name) = boolean 변수 "name" 의 값.

$I(name) = integer 변수 "name" 의 값.

$S(name) = string 변수 "name" 의 값.

$E(name_ = element 변수 "name" 의 값.


다음 변수들은 자동으로 초기화됩니다:


$S(Output) = 이 섹션을 평가해서 반환된 출력( Input 에 대해 초기화됨 ).

$S(Architecture) = 대상 아키텍쳐( armeabi-armv7a, arm64-v8a, x86, x86_64 ).

$S(PluginDir) = XML 파일이 로드된 디렉토리.

$S(EngineDir) = 엔진 디렉토리.

$S(BuildDir) = 프로젝트의 플랫폼 관련 빌드 디렉토리( Intermediate 폴너 내부 ).

$S(Configuration) = 구성 유형( Debug, DebugGame, Development, Test, Shipping ).

$B(Distribution) = Distribution 빌드이면 true.


주의 : 위에 있는 변수들에서 예외가 발생하면, 플러그인의 칸텍스트에 있는 모든 것들은 namespace 충돌을 막습니다; 새로운 값을 위의 변수들에 설정하려고 시도하는데, Output 에서 예외가 발생하면, 현재 칸텍스트에만 영향을 줄 것입니다.


다음 노드들은 변수를 조작하는 데 사용됩니다:


<setBool result="" value=""/>

<setInt result="" value=""/>

<setString result="" value=""/>

<setElement result="" value=""/>

<setElement result="" value="" text=""/>

<setElement result="" xml=""/>


value 와 함께 사용되는 <setElement> 는 태그( tag )가 value 로 설정된 비어있는 XML 엘리먼트를 생성합니다.

value 및 text 와 함께 사용되는 <setElement> 는 태그가 value 로 설정된 파싱되지 않은 text 에 대한 XML 엘리먼트를 생성합니다.

xml 과 함께 사용되는 <setElement> 는 제공된 XML 을 파싱할 겁니다. escape character 를 넣으면 안 됩니다.


변수들은 ini 파일에 있는 속성( property )들로부터 설정될 수도 있습니다:


<setBoolFromProperty result="" ini="" section="" property="" default=""/>

<setBoolFromPropertyContains result="" ini="" section="" property="" default="" contains=""/>

<setIntFromProperty result="" ini="" section="" property="" default=""/>

<setStringFromProperty result="" ini="" section="" property="" default=""/>


문자열들은 <setStringFromEnvVar> 노드를 사용해 환경 변수로부터 설정될 수도 있습니다.

환경 변수들은 반드시 '%' 문자의 쌍으로 묶여 있어야 하며 'value' 애트리뷰트로서 지정되어야만 합니다.


<setStringFromEnvVar result="" value=""/>


특정 환경 변수가 정의되어 있는지를 검사하는 것도 가능합니다( 역시, '%' 문자로 묶여야 합니다 ):


<setBoolEnvVarDefined result="" value=""/>


환경 변수 노드를 사용하는 일반적인 예제는 다음과 같습니다:


<setBoolEnvVarDefined result="bHasNDK" value="%NDKROOT%"/>

<setStringFromEnvVar result="NDKPath" value="%NDKROOT%"/>


Boolean 변수들은 연산자가 적용된 결과로 설정될 수도 있습니다:


<setBoolNot result="" source=""/>

<setBoolAnd result="" arg1="" arg2=""/>

<setBoolOr result="" arg1="" arg2=""/>

<setBoolIsEqual result="" arg1="" arg2=""/>

<setBoolIsLess result="" arg1="" arg2=""/>

<setBoolIsLessEqual result="" arg1="" arg2=""/>

<setBoolIsGreater result="" arg1="" arg2=""/>

 <setBoolIsGreaterEqual result="" arg1="" arg2=""/>

<setBoolFromPropertyContains result="" ini="" section="" property="" contains=""/>


Integer 변수들은 수학 연산자들을 사용할 수 있습니다:


<setIntAdd result="" arg1="" arg2=""/>

<setIntSubtract result="" arg1="" arg2=""/>

<setIntMultiply result="" arg1="" arg2=""/>

<setIntDivide result="" arg1="" arg2=""/>


String 변수들은 다음과 같이 조작될 수 있습니다:


<setStringAdd result="" arg1="" arg2=""/>

<setStringSubstring result="" source="" start="" length=""/>

<setStringReplace result="" source="" find="" with=""/>


String 의 길이를 획득할 수도 있습니다:


<setIntLength result="" source=""/>


소스로부터 문자열을 검색해 인덱스를 획득할 수도 있습니다 :


<setIntFindString result="" source="" find=""/>


다음 예제는 문자열을 비교하는데, <setIntFindString> 을 사용하는 대신에 result 를 검사합니다:


<setBoolStartsWith result="" source="" find=""/>

<setBoolEndsWith result="" source="" find=""/>

<setBoolContains result="" source="" find=""/>


다음 노드를 사용하면 로그에 메시지를 기록할 수 있습니다:


<log text=""/>


다음 형식을 사용하면 조건 실행이 가능합니다:


<if condition="">

<true>

  <!-- condition 이 true 이면 실행 -->

  </true>

  <false>

  <!-- condition 이 false 이면 실행 -->

  </false>

 </if>


<true> 와 <false> 블락은 선택적입니다. 조건은 반드시 boolean 변수여야 합니다.

Boolean 연산자 노드를 결합해서 더욱 복잡한 조건을 위한 최종 상태를 만들 수 있습니다 :


<setBoolNot result="notDistribution" source="$B(Distribution)/>

<setBoolIsEqual result="isX86" arg1="$S(Architecture)" arg2="x86"/>

<setBoolIsEqual result="isX86_64" arg2="$S(Architecture)" arg2="x86_64/">

<setBoolOr result="isIntel" arg1="$B(isX86)" arg2="$B(isX86_64)"/>

<setBoolAnd result="intelAndNotDistribution" arg1="$B(isIntel)" arg2="$B(notDistribution)"/>

<if condition="intelAndNotDistribution">

<true>

<!-- do something for Intel if not a distribution build -->

</true>

</if>


"isIntel" 은 다음과 같이 수행될 수도 있음에 주목하세요:


<setStringSubstring result="subarch" source="$S(Architecture)" start="0" length="3"/>

<setBoolEquals result="isIntel" arg1="$S(subarch)" arg2="x86"/>


조건 실행을 위해서 아래 두 노드를 이용할 수 있습니다:


<isArch arch="armeabi-armv7">

<!-- do stuff -->

</isArch>


이것은 다음과 동일합니다:


<setBoolEquals result="temp" arg1="$S(Architecture)" arg2="armeabi-armv7">

<if condition="temp">

<true>

<!-- do stuff -->

</true>

</if>


그리고


<isDistribution>

<!-- do stuff -->

</isDistribution>


는 다음과 동일합니다 :


<if condition="Distribution">

<!-- do stuff -->

</if>


다음 노드들을 사용해 루프를 돌릴 수도 있습니다:


<while condition="">

<!-- do stuff -->

</while>


<break/>

<continue/>


<while> 바디는 condition 이 false 이거나 <break/> 를 만날 때까지 실행될 겁니다. <continue/> 는 condition 이 여전히 true 인 동안에는 루프 실행을 재개할 것이며 그렇지 않으면 루프를 나갈 겁니다.


주의 : <while> 바디 바깥쪽의 <break/> 는 <return/> 과 동일하게 동작합니다.


여기에 1 에서 5 까지 로그를 출력하는데, 3 은 건너 뛰는 예제가 있습니다. while 조건의 갱신은 continue 전에 수행되어야 한다는 것에 주목하세요. 그렇지 않으면 루프를 나가버립니다.


<setInt result="index" value="0"/>

<setBool result="loopRun" value="true"/>

<while condition="loopRun">

<setIntAdd result="index" arg1="$I(index)" arg2="1"/>

<setBoolIsLess result="loopRun" arg1="$I(index)" arg2="5"/>

<setBoolIsEqual result="indexIs3" arg1="$I(index)" arg2="3"/>

<if condition="indexIs3">

<true>

<continue/>

</true>

</if>

<log text="$I(index)"/>

</while>


result 와 name 을 생성할 때 변수 치환을 하는 것도 가능합니다. 이것은 루프 안에서 배열을 생성하는 것을 가능하게 해 줍니다:


<setString result="array_$I(index)" value="element $I(index) in array"/>


다음 노드를 사용해서 그것을 획득할 수 있습니다( 값이 변수 이름으로 취급됩니다 ):


<setStringFrom result="out" value="array_$I(index)"/>


Boolean 및 integer 유형에 대해 여러분은 <setBoolFrom/> 과 <setIntFrom/> 을 사용할 수 있습니다.


섹션에 텍스트를 삽입하기 위한 노드는 다음과 같습니다:


<insert> body </insert>

<insertNewline/>

<insertValue value=""/>

<loadLibrary name="" failmsg=""/>


첫 번째 것은 텍스트나 노드를 반환되는 섹션 문자열에 삽입합니다. escape character 를 사용하려면 다음과 같이 해야 한다는 것에 주의하세요 :


< = &lt;

> = &gt;

& = &amp;


<insertValue value=""/> 는 삽입 전에 value 의 변수를 평가합니다. 만약 value 가 쌍따옴표( " ) 를 포함하고 있다면, 그것을 &quot; 로 작성해야 합니다:


<loadLibrary name="" failmsg=""/> 는 system.LoadLibrary try/catch 블락을 삽입하기 위한 단축명령이며, 로딩에 실패했을 때 사용할 선택적인 로그 메시지를 포함합니다.


Output 을 검색해서 치환할 수도 있습니다:


<replace find="" with=""/>


실제 $S(Output) 을 직접 조작할 수도 있다는 것을 기억하세요. 위의 것이 훨씬 효율적입니다:


<setStringAdd result="Input" arg1="$S(Output)" arg2="sample\n"/>

<setStringReplace result="Input" source="$S(Output)" find=".LAUNCH" with=".INFO"/>


XML 조작은 다음 노드를 사용합니다:


<addElement tag="" name=""/>

<addElements tag=""> body </addElements>

<removeElement tag=""/>

<setStringFromTag result="" tag=""/>

<setStringFromAttribute result="" tag="" name=""/>

<setStringFromTagText result="" tag=""/>

<addAttribute tag="" name="" value=""/>

<removeAttribute tag="" name=""/>

<loopElements tag=""> instructions </loopElements>


현재 엘리먼트는 tag="$" 에 의해 참조됩니다. 엘리먼트 변수들은 $varname 을 사용해 참조되는데, $E(varname) 을 사용하면 XML 의 string 으로 확장되 버리기 때문입니다.


addElement, addElements, removeElement 는 기본적으로 일치하는 태그들에 대해 모두 적용됩니다. 선택적인 once="true" 애트리뷰트가 지정되면 처음으로 일치하는 태그에 대해서만 적용되도록 할 수 있습니다.


<uses-permission>, <uses-feature>, <uses-library> 는 다음과 같이 갱신되었습니다:


addPermission android:name="" .. />

<addFeature android:name="" .. />

<addLibrary android:name="" .. />


위의 명령들에서 모든 애트리뷰트들은 매니페이스에 추가되는 엘리먼트들에 복사되므로, 여러분은 다음과 같이 할 수 있습니다:


<addFeature android:name="android.hardware.usb.host" android:required="true"/>


마지막으로, 다음 노드들은 jar 이나 파일들을 스테이징하기 위해서 파일을을 복사하는 것을 허용합니다:


<copyFile src="" dst="" force=""/>

<copyDir src="" dst="" force=""/>


만약 force 가 false 라면, 길이나 수정시간이 다를 때만 파일들이 대체될 것입니다. 기본값은 true 입니다.


다음은 src 와 dst 경로를 위해 base 로 사용되어야 합니다:


$S(PluginDir) = XML 파일이 로드된 디렉토리.

$S(EngineDir) = 엔진 디렉토리.

$S(BuildDir) = 프로젝트의 플랫폼 관련 빌드 디렉토리.


APK 디렉토리 외부에 파일을 쓰는 것도 가능하지만, 추천하지는 않습니다.


만약 파일을 제거해야 한다면( 예를 들어 development-only 파일을 distribution 빌드에서 제거하려면 ), 다음 노드를 사용할 수 있습니다:


<deleteFiles filespec=""/>


이건 BuildDir 에서 파일을 제거하는 용도로만 사용될 수 있습니다. Occlus Signature( soig ) 파일들을 assets 디렉토리에서 제거하는 예제가 있습니다:


<deleteFiles filespec="assets/oculussig_*"/>


다음 섹션은 패키징과 디플로잉( deploy ) 스테이지에서 평가됩니다 :


** For all platforms **

<!-- init section is always evaluated once per architecture -->

<init> </init>

 

 ** Android Specific sections **

<!-- optional updates applied to AndroidManifest.xml -->

<androidManifestUpdates> </androidManifestUpdates>

<!-- optional additions to proguard -->

<proguardAdditions> </proguardAdditions>

<!-- optional AAR imports additions -->

<AARImports> </AARImports>

<!-- optional base build.gradle additions -->

<baseBuildGradleAdditions>  </baseBuildGradleAdditions>


<!-- optional base build.gradle buildscript additions -->

<buildscriptGradleAdditions>  </buildscriptGradleAdditions>

<!-- optional app build.gradle additions -->

<buildGradleAdditions>  </buildGradleAdditions>

<!-- optional additions to generated build.xml before ${sdk.dir}/tools/ant/build.xml import -->

<buildXmlPropertyAdditions> </buildXmlPropertyAdditions>


<!-- optional files or directories to copy or delete from Intermediate/Android/APK before ndk-build -->

<prebuildCopies> </prebuildCopies>

<!-- optional files or directories to copy or delete from Intermediate/Android/APK after ndk-build -->

<resourceCopies> </resourceCopies>

<!-- optional files or directories to copy or delete from Intermediate/Android/APK before Gradle -->

<gradleCopies> </gradleCopies>

<!-- optional properties to add to gradle.properties -->

<gradleProperties> </gradleProperties>


<!-- optional parameters to add to Gradle commandline (prefix with a space or will run into previous parameter(s)) -->

<gradleParameters> </gradleParameters>


  <!-- optional minimum SDK API level required -->

  <minimumSDKAPI> </minimumSDKAPI>

<!-- optional additions to the GameActivity imports in GameActivity.java -->

<gameActivityImportAdditions> </gameActivityImportAdditions>


<!-- optional additions to the GameActivity after imports in GameActivity.java -->

  <gameActivityPostImportAdditions> </gameActivityPostImportAdditions>

  

<!-- optional additions to the GameActivity class in GameActivity.java -->

<gameActivityClassAdditions> </gameActivityOnClassAdditions>

<!-- optional additions to GameActivity onCreate metadata reading in GameActivity.java -->

<gameActivityReadMetadata> </gameActivityReadMetadata>

 

<!-- optional additions to GameActivity onCreate in GameActivity.java -->

<gameActivityOnCreateAdditions> </gameActivityOnCreateAdditions>

<!-- optional additions to GameActivity onDestroy in GameActivity.java -->

<gameActivityOnDestroyAdditions> </gameActivityOnDestroyAdditions>

<!-- optional additions to GameActivity onStart in GameActivity.java -->

<gameActivityOnStartAdditions> </gameActivityOnStartAdditions>

<!-- optional additions to GameActivity onStop in GameActivity.java -->

<gameActivityOnStopAdditions> </gameActivityOnStopAdditions>

<!-- optional additions to GameActivity onPause in GameActivity.java -->

<gameActivityOnPauseAdditions> </gameActivityOnPauseAdditions>

<!-- optional additions to GameActivity onResume in GameActivity.java -->

<gameActivityOnResumeAdditions> </gameActivityOnResumeAdditions>

<!-- optional additions to GameActivity onNewIntent in GameActivity.java -->

<gameActivityOnNewIntentAdditions> </gameActivityOnNewIntentAdditions>

<!-- optional additions to GameActivity onActivityResult in GameActivity.java -->

<gameActivityOnActivityResultAdditions> </gameActivityOnActivityResultAdditions>

<!-- optional libraries to load in GameActivity.java before libUE4.so -->

<soLoadLibrary> </soLoadLibrary>


아래에는 지원되는 전체 노드 목록이 있습니다.


<isArch arch="">

<isDistribution>

<if> => <true> / <false>

<while condition="">

<return/>

<break/>

<continue/>

<log text=""/>

<insert> </insert>

<insertValue value=""/>

<replace find="" with""/>

<copyFile src="" dst=""/>

<copyDir src="" dst=""/>

<loadLibrary name="" failmsg=""/>

<setBool result="" value=""/>

<setBoolEnvVarDefined result="" value=""/>

<setBoolFrom result="" value=""/>

<setBoolFromProperty result="" ini="" section="" property="" default=""/>

<setBoolFromPropertyContains result="" ini="" section="" property="" contains=""/>

<setBoolNot result="" source=""/>

<setBoolAnd result="" arg1="" arg2=""/>

<setBoolOr result="" arg1="" arg2=""/>

<setBoolIsEqual result="" arg1="" arg2=""/>

<setBoolIsLess result="" arg1="" arg2=""/>

<setBoolIsLessEqual result="" arg1="" arg2=""/>

<setBoolIsGreater result="" arg1="" arg2=""/>

<setBoolIsGreaterEqual result="" arg1="" arg2=""/>

<setInt result="" value=""/>

<setIntFrom result="" value=""/>

<setIntFromProperty result="" ini="" section="" property="" default=""/>

<setIntAdd result="" arg1="" arg2=""/>

<setIntSubtract result="" arg1="" arg2=""/>

<setIntMultiply result="" arg1="" arg2=""/>

<setIntDivide result="" arg1="" arg2=""/>

<setIntLength result="" source=""/>

<setIntFindString result="" source="" find=""/>

<setString result="" value=""/>

<setStringFrom result="" value=""/>

<setStringFromEnvVar result="" value=""/>

<setStringFromProperty result="" ini="" section="" property="" default=""/>

<setStringAdd result="" arg1="" arg2=""/>

<setStringSubstring result="" source="" index="" length=""/>

<setStringReplace result="" source="" find="" with=""/>


뭔가 상당히 복잡해 보이는데, 과정에 익숙해지면 플랫폼별 빌드 구성을 하는데 있어서 매우 편리한 도구가 될 수 있을거라는 생각이 드네요.



일단 이 문서는 UPL 이 뭔지 정리하는 데 목적을 두고 있기 때문에 여기에서 마무리할 계획입니다. 다음에는 실제로 UPL 을 사용해 Crashlystics 와 연동하는 작업을 해 볼 계획입니다.

주의 : 공부하면서 정리한 것이라 잘못된 내용이 있을 수 있습니다.

주의 : BSP 알고리즘에 대해 구체적으로 설명을 하지 않습니다. 어느 정도 개념을 알고 있다고 가정합니다.



UE4 Brush, BSP, PhysX Convex 에 대해


BSP( Binary Space Partitioning ) 는 둠라는 게임에서 사용된 이후로 매우 유명해졌습니다. 말 그대로 씬을 두 개의 노드로 분할한다는 의미로 이와 관련된 자료는 인터넷에 많이 있으니 검색해 보시기 바랍니다. 이 문서에서는 UE4 에서 어떤 방식으로 BSP 를 이용하는지에 대해서 중점적으로 다룹니다.


기존의 1인칭 슈팅 게임에서는 한정된 자원으로 게임을 만들어야 했기 때문에 메쉬의 사용을 최소화하고 빠르게 위치 및 충돌을 검출하기 위해서 BSP 를 많이 사용했습니다. 특히 얼리얼에서는 CSG( Constructive Solid Geometry ) 라는 개념을 통해 씬을 쉽게 구성할 수 있는 도구를 제공했습니다.


하지만 이제는 예전과는 다르게 1인칭 슈팅 게임들도 복잡한 지오메트리를 사용하게 되고 그에 걸맞게 하드웨어 성능과 공간분할 알고리즘들이 발전하게 되면서 BSP 를 사용하지 않는 추세입니다. 그러나 언리얼에서 여전히 BSP 를 중요하게 사용하는 부분이 있습니다. 바로 Geometry 라 표현되는 개체입니다.


게임을 만들어 보신 분이라면 한 번쯤은 "Trigger Volume" 이라는 액터를 배치해 보신적이 있을 것입니다. 이 트리거 볼륨 액터는 사용자가 어떤 영역에 들어 오고 나가는 것을 알려줍니다.


그림1


하지만 저 계단 위쪽에 들어 왔는지 나갔는지를 판단하려면 매우 복잡한 모양의 트리거 볼륨이 필요합니다. 왼쪽에 있는 "Modes" 툴바의 "Geometry Editing" 항목을 선택하면 이것을 복잡하게 편집할 수 있습니다. "Extrude" 를 기능을 사용해서 멋지게 편집해 봤습니다. 절대 여러 개의 볼륨을 사용한 것이 아닙니다. 하나만으로 만들었습니다.



그림2


이 경우에도 저 볼륨의 영역에 캐릭터가 들어갔다 나갔다 하면 앞에서와 같은 이벤트가 발생하게 됩니다. 


예전에는 BSP 노드에 대한 충돌검사를 수행했습니다. 하지만 UE4 에서는 PhysicsX 를 도입하면서 BSP 노드를 convex 집합으로 바꾸게 됩니다. 이것을 언리얼에서는 "Aggregate Geometry" 라 부르게 됩니다. 



언리얼에서 Volume 은 AVolume 이라는 개체로 표현되며, 이는 ABrush 를 상속합니다.



그림 3


    • ABrush 는 기존의 UE3 의 CSG 브러쉬를 대체합니다. "Placement Mode" 에서 "Geometry" 카테고리에 포함됩니다. 여러 개의 액터가 존재하는데 이것은 브러쉬 빌더의 타입을 보여 줍니다. FBspModeModules::StartupModule() 메서드에서는 "Geometry" 카테고리에 이 빌더들을 등록하고 있습니다. 관심이 있으신 분들은 찾아 보시기 바랍니다.
    • AVolume 은 ABrush 중에서 볼륨 개체를 추상화하기 위해서 사용됩니다. 기본적으로는 ABrush 의 기능과 동일합니다.
    • ABrushShape 은 geometry editing mode 에서 "Lathe" 를 위해 사용된다고 하는데 뭔지 잘 모르겠습니다.


그림 4


어쨌든 ABrush 가 여기에서 다루고자 하는 핵심입니다. ABrush 에서는 폴리곤 데이터를 UModel 개체에 물리 및 렌더링 데이터를 UBrushComponent 를 개체에 저장합니다.


이 문서에서 이야기하고자 하는 핵심 클래스들은 다음과 같습니다. 물론 훨씬 더 많은 자료구조를 포함하고 있지만 주제와 밀접한 클래스들만 정리했습니다.


그림 5


사설이 길었는데요 이제 조금 구체적으로 파 보도록 하겠습니다.


Brush : Polygons & BSP Nodes



언리얼에서는 브러쉬는 어떤 메쉬를 생성하기 위한 틀을 의미합니다. 액터로서는 ABrush 로 표현되지만, 실제 폴리곤( 혹은 지오메트리 ) 데이터는 UModel 개체에 포함되어 있습니다. UModel* ABrush::Brush 라고 정의되어 있습니다.


우리가 에디터에서 geometry editing mode 를 켜 놓고 편집을 하게 되면 실제로는 UModel 개체를 수정하게 되는 것입니다. 이것은 브러쉬의 지오메트리를 표현하기 위한 많은 정보들을 포함합니다. 그 중의 핵심이라고 할 수 있는 것은 폴리곤 데이터( FPoly )와 BSP 노드 데이터( FBspNode )입니다.


폴리곤 데이터는 하나의 면( FPlane )을 표현합니다. 물론 범위가 정해져 있으니 점 데이터도 포함하게 됩니다. 예를 들어 큐브는 6 개의 폴리곤과 24 개( 각 노드당 4 개 )의 점으로 구성됩니다. 점을 공유하고 있지는 않더군요. 그런데 점을 하나 수직으로 움직이게 되면 면 하나가 두 개의 삼각형으로 전환됩니다. 이 경우에는 7 개의 폴리곤과 26 개의 점으로 구성됩니다; 5 * 4 + 3 * 2 = 26.


그림 6


여기서 퀴즈입니다. 그림 2 의 폴리곤은 몇 개일까요? 볼륨을 둘러 싸는 가장 바깥쪽 면 개수만 세야 합니다. 답은 댓글에...


편집을 통해 폴리곤 정보가 변경되면 FBSPOps::bspBuild() 라는 메서드가 호출됩니다. 


UE4 에서 BSP 를 구성하는 데 있어 핵심이 되는 파라미터는 "Balance" 입니다. 이는 균형잡힌 이진트리를 만드느냐 아니면 가장 분할( 다른 폴리곤을 자르는 것 )이 적은 이진트리를 만드냐를 결정하게 됩니다. 그것의 pseudo code 는 다음과 같습니다.



여기에서 관계라는 것은 front, coplanar, back, split 입니다. 각각의 카운트를 세는거죠. 그리고 점수는 다음과 같이 계산됩니다.



이 Balance 값은 현재 15 로 하드코딩되어 있습니다( 수정 : 코드 검색하다가 보니 0, 15, 70 인 경우가 있었습니다 ). 예전에는 수치를 받았던거 같은데 UE4 로 오면서 경험적으로 좋은 수치를 설정한 것 같습니다. 


내용을 곱씹어 보면 다른 폴리곤을 많이 쪼갤수록 그리고 Front 와 Back 의 개수 차이가 클수록 점수가 높아집니다. 그런데 Lerp( linear interpolation ) 처럼 Balance 값을 통해 가중치를 조절합니다.


만약 Balance 가 크다면 어떻게 될까요? 100.0 - float(Balance) 의 값이 작아지므로 Front 와 Back 의 차이가 클수록 좋은 점수를 받겠죠. 반대로 Balance 가 작다면 어떻게 될까요? 100 - float(Balance) 의 값이 커지므로 쪼개는 개수가 많아질수록 좋은 점수를 받겠죠. 헷갈리니 예를 들어 보도록 하겠습니다.


그림 7 은 3 이 splitter 로 선택되었을 때의 상황을 가정하고 그린 것입니다. 2 를 2f 와 2b 로 나누지 않은 이유에 대해서는 나중에 알게 될 것입니다. 그림 7 에서 각 폴리곤들의 노멀은 바깥쪽이라고 가정하시고 보시면 됩니다.


그림 7


 Balance

 1 ( f : 0, b : 4, s : 0 )

 2 ( f : 0, b : 4, s : 0 )

 3 ( f : 1, b : 2, s : 1 )

 4 ( f : 1, b : 2, s : 1 )

 5 ( f : 0, b : 4, s : 0 )

 Best Splitter Order

 15

 60

 60

 90

 90

 60

 1, 2, 5, 3, 4

 75

 300

 300

 90

 90

 300

 3, 4, 1, 2, 5


Balance 가 75 인 경우에는 3 이 splitter 로 사용됩니다. 3 이 가장 먼저 splitter 로 사용되며 5f, 5b 가 생성됩니다. 여기에서 의문이 발생할 것입니다. 왜 2 는 쪼개지지 않을까요. 4 가 splitter 로 사용되는 시점에는 3 을 기준으로 front-end 로 분할된 상황이기 때문에 2 는 4 가 쪼갤 수 있는 대상이 아닙니다. 4 는 front 에 있고 2 는 back 에 있습니다. 그러므로 분할은 3 을 splitter 로 사용했을 때 단 한 번만 발생합니다. 최종적으로 리스트에는 1, 2, 3, 4, 5f, 5b 가 존재하게 됩니다.


결국 Balance 가 15 인 경우와 75 인 경우에는 다음과 같은 이진 트리가 나오게 됩니다.




그림 8


그림 8 에서 보면 leaf 노드에 색이 칠해져 있는 것을 볼 수 있습니다. leaf 노드에 도달했다는 것은 면으로 이루어진 도형이 닫힌거라 생각하시면 됩니다. 


예를 들어 오른쪽 그림의 경우를 봅시다. 평면 3, 1, 2, 5b 에 대해서 어떤 점과의 위치 관계를 구했을 때 모두 backside 라고 하면 녹색 도형 안에 점이 존재한다고 할 수 있습니다. 이것을 BSP Tree Traverse 의 관점에서 보자면 back-leaf 노드에 도달한 것입니다. 


왼쪽 그림의 경우를 봅시다. 만약 front node 가 존재한다면 그쪽으로 concave 가 형성되는 구멍이 뚫려 있다고 보시면 됩니다. 만약 front-leaf 노드에 도달했을 때 폴리곤의 normal 과 점의 관계가 backside 라고 한다면 닫히게 되는 것입니다. 만약 frontside 라고 하면 열린 폴리곤이라는 의미가 됩니다.


PhysX Convex



이제 이것을 실제로 사용하기 위해서 PhysX 에 넘겨 줄 필요가 있습니다. 그런데 많은 분들이 알고 계시다시피 물리 엔진들은 concave 를 사용하지 않습니다. 사용한다고 하더라도 내부적으로 이것을 convex 집합으로 바꾸게 됩니다. 이 convex 집합이 바로 Aggregate Geometry 입니다.


BSP 노드들을 Convex 로 바꾸는 작업은 ModelToHullsWorker() 에 의해서 수행됩니다. 여기에서 하는 작업은 별거 없습니다. 그림 8 처럼 concave 를 convex 로 쪼개는 작업을 합니다.


노드를 순회하면서 폴리곤을 수집하다가 leaf 를 만나면 그때까지 수집했던 폴리곤들을 사용해 convex 를 하나 생성합니다. 그런데 여기서 알아야 할 점은 front node 를 만나면 새로운 convex 를 만들기 위해 폴리곤들을 수집하기 시작한다는 것입니다. 아까도 언급했듯이 front node 가 있다는 것은 그쪽으로 뚫려 있다는 이야기이므로 leaf 를 만날 때까지 새로운 폴리곤을 수집해야 합니다.


닫힌 convex 가 완성되면 AddConvexPrim() 이라는 메서드를 호출하게 되는데요, 여기에 위에서 언급했던 폴리곤의 리스트( 즉 plane list )를 넘겨 줍니다. 그 메서드에서는 정확성을 높이기 위해서 vertex snapping 과 같은 작업을 수행하고 FKAggregateGeom 에다가 FKConvexElem 를 추가해 줍니다. 여기까지 하면 PhysX Convex 를 생성할 준비를 마친 것입니다.


실제 PhysX Convex 는 UBodySetup::CreatePhysicsMeshes() 호출을 통해 생성됩니다.


생성된 PhysX Convex 와 Brush Helper 의 모양을 비교해서 보여줬으면 좋겠지만 제가 PhysX Mesh 를 출력할 수 있는 방법을 찾지 못해서 안타깝네요. 


PhysX Capture



PhysicsX Visual Debugger ( PVD ) 를 사용해서 씬을 캡쳐할 수 있다는 제보를 받고 시도를 해 봤습니다. 이미 API integration 은 되어 있는 상황이더군요.


UE4 에서는 PvdConnect() 라는 메서드를 통해서 이를 지원합니다. 


에디터 콘솔 명령창에서 "pvd connect""pvd disconnect" 를 사용해서 활성화/비활성화할 수 있습니다. 실제 적용되는건 play 를 눌렀을 때입니다. 그러므로 play 버튼을 누른 후에 connect 했다가 disconnect 하면 됩니다. 계속 캡쳐되고 있으므로 프레임 디버깅할 것이 아니라면 빨리 disconnect 하세요.


그런데 저는 pvd 연결하면 프로그램이 멈춰버리더군요.... 나중에 방법을 찾으면 캡쳐해서 올리도록 하겠습니다.


추가 : 결국 해결( ? )했습니다. pvd3.exe 가 너무 많이 떠 있어서 그랬던 것 같습니다. 다 지우고 하나만 띄우니 되네요.


좌표계를 UE4 와 일치하는 화면을 보시려면 "Left-Handed", "Z+" 설정하시고 Bounding box 를 "All" 로 하시면 쉽게 확인할 수 있습니다. Convex 별로 physx collision 이 할당된 것을 확인하실 수 있습니다.



주의 : 공부하면서 정리한 것이라 잘못된 내용이 포함될 수 있습니다.


 

1. 들어가며

 

UE4 와 관련한 프로그래밍을 하면서 처음에 가장 헷갈리는 부분이 패키지와 애셋이라는 개념입니다. 그리고 이것은 나중에 C++ 코드를 통해 패키지나 애셋을 로드하다가 보면 머리가 아파집니다. 경로를 집어 넣으라고 하는데 뭐가 뭔지 알수가 없습니다.

 

사실 언리얼 공식 문서인 [ 애셋과 패키지 ] 에 그 개념들에 대해서 설명을 하고 있기는 하지만 좀 부족합니다. 아니 부족하다기 보다는 우리가 생각하는 애셋과 패키지가 아닙니다. 뭐랄까 아티스트나 디자이너 위주의 추상화된 개념이라 할 수 있습니다. 실제 API 들에서 명명하는 개념과는 다릅니다.

 

특히나 커맨드렛 같은 도구를 통해 애셋관리를 자동화할 때 이 문제는 심각해집니다. 애셋을 복사하고 싶은데 애셋을 복사하는 방법을 모릅니다. 그래서 어찌어찌 내가 애셋을 복사한다고 하는게 패키지를 복사하는 것이라는 것을 깨닫게 됩니다. 그런데 패키지를 복사한 후에 경로 문제 때문에 애셋을 로드하지 못하는 사태가 발생합니다.

 

많은 분들이 이런 문제들 때문에 골치가 아팠을 것이라 생각합니다. 그래서 이 문서에서는 그런 개념들에 대해 정리해 볼 계획입니다. 모쪼록 도움이 됐으면 좋겠네요.

 

2. 패키지와 애셋

 

여러분은 패키지라고 하면 무엇이 생각나십니까? 여러 개의 애셋을 하나로 묶어 놓은 ( Unity3D 에서와 같은 ) 번들을 생각하시는 분도 있을테고 배포하기 위해서 묶어 놓은 패키지를 생각하실 수 있을 것입니다. UE4 에디터에서 공식적으로 패키지라는 이름이 등장하는 것은 후자의 경우입니다. 쿠킹 패키징할 때의 패키지입니다.

 

 

하지만 코딩 문맥에서는 패키지라는 것은 그냥 "*.uasset" 파일입니다. UE3 에서는 어땠는지 모르겠지만 UE4 에서는 패키지와 애셋은 일반적으로 1:1 로 대응됩니다. 그럼 ".uasset" 파일을 칸텐츠 브라우저에서는 애셋이라 부르고 코드에서는 패키지라 부르는 것일까요? 여러분은 어떻게 생각하시나요?

 

아래의 칸텐츠 브라우저에 있는 "FirstPersonCharacter" 는 코딩문맥에서 보면 패키지인가요 애셋인가요?

 

 

답은 애셋입니다. 좀 더 명확하게 하기 위해서 익스플로러에서 이 파일을 복제해 보겠습니다.

 

 

보이시나요? 동일한 "*.uasset" 파일을 복사하게 되면 파일의 이름은 다른데 칸텐츠 브라우저에서의 이름은 동일합니다. 그러므로 제가 위에서 "*.uasset" 은 패키지이고 칸텐츠 브라우저에서는 애셋으로 표현된다고 말씀드린 것입니다. 절대 "FirstPersonCharacter2" 라는 이름으로 애셋이름이 변경되지 않습니다.

 

"에이~ 내가 해봤는데 칸텐츠 브라우저에서 애셋 복사하면 이름 바뀌던데?" 라고 반문하시는 분 있을 겁니다. 맞습니다. 칸텐츠 브라우저에서 애셋을 복사하면 패키지 복사와 함께 이름 변경까지 수행해 줍니다. 그러나 이는 칸텐츠 브라우저의 동작이지 패키지의 동작은 아니라는 겁니다. 그러므로 나중에 패키지를 코드를 통해 복사하게 되면 문제가 발생하는 겁니다.

 

정리하자면 패키지라는 것은 "*.uasset" 을 이야기하는 것이고 애셋이라는 것은 "*.uasset" 에 포함되어 있는 대표 오브젝트를 의미합니다. 편의상 이를 애셋이라는 이름으로 개념화한거죠. 여러분이 코드를 보실 때 주의하실 점은 "class UAsset" 과 같은 검색어를 날릴 필요는 없다는 것입니다. 존재하지 않습니다. UObject 를 상속한 어떠한 오브젝트라도 패키지의 대표 오브젝트가 될 수 있습니다.

 

3. Paths

 

이제 패키지와 애셋이 다르다는 것을 인지했으니 코드 상에서는 이에 어떻게 접근을 하는지 알아 보도록 하겠습니다. 위에서 한 것과 똑같은 동작을 코드에서 수행해 보도록 하겠습니다.

 

3.1. LongPackageName

 

일단 패키지를 로드하기 위해서는 어떤 경로를 사용해야 할까요? 여러 분이 칸텐츠 브라우저에서 애셋 위에 마우스를 올리시면 다음과 같은 툴팁을 보실 수 있습니다.

 

 

저기에서 "/Game" 이라는 이름으로 "Path" 가 시작되는 것이 보이실 겁니다. UE4 에서 사용하는 모든 칸텐츠의 경로는 저 "/Game" 이라는 접두어로 시작됩니다. 그것은 "/Content" 라는 이름에 대한 키처럼 사용됩니다. 아래 그림을 보시면 붉은색 박스가 게임 디렉토리이고 녹색 박스가 위에서 표현하고 있는 Path 입니다. 단지 "Content" 라는 이름이 "Game" 이라는 이름으로 변경되었을 뿐이죠. 

 

 

 

어쨌든 이 경로와 확장자를 제거한 패지키 이름을 합치면 그것을 LongPackageName 이라 부릅니다. 이 LongPackageName 에서는 "\" 가 아니라 "/" 를 디렉토리 분리자로 사용합니다.

 

 

우리의 경우에는 "/Game/FirstPersonCPP/Blueprints/FirstPersonCharacter" 가 되겠죠. 

 

 

3.2. Filename

 

앞에서 로드한 패키지를 "/Game/FirstPersonCPP/Blueprints/FirstPersonCharacter2" 이라는 이름으로 저장해 보도록 하겠습니다. 그런데 패키지를 저장하는 메서드는 Filename 이라는 인자를 요구하는군요.

 

 

Filename 이라는 인자를 요구하는 경우에는 절대 경로를 요구하는 것이라 보시면 됩니다. 확장자까지 확실히 붙어 있어야 합니다. 우리는 LongPackaeName 을 알고 있기 때문에 이를 절대 경로로 바꾸는 과정을 거치게 됩니다. 아래 코드를 살펴 보십시오. 어렵지 않을 것입니다.

 

 

FPackagePath::LongPackageNameToFilename() 은 LongPackageName 을 상대 경로인 파일경로로 바꿔줍니다. DestRelFilename 은 "../../../../Users/lifeisforu/Documents/Unreal Projects/PackageTest/Content/FirstPersonCPP/Blueprints/FirstPersonCharacter2.uasset" 이 나왔습니다. 이 메서드의 두 번째 인자로 공급해 주면 DestLongPackageName 에 확장자를 포함시켜주지 않아도 되고 임시 변수를 만들지 않아도 됩니다.

 

FPaths::ConvertRelativePathToFull() 은 상대경로를 절대경로로 바꿔줍니다. 이 시점에서 DestAbsFilename 은 "C:/Users/lifeisforu/Documents/Unreal Projects/PackageTest/Content/FirstPersonCPP/Blueprints/FirstPersonCharacter2.uasset" 이 나옵니다.

 

하지만 이것은 Windows 플랫폼에 맞는 이름이 아니죠. 그래서 플랫폼에 맞게 분리자 문자( '/' )를 교정해 줍니다. FPaths::MakePlaformFilename() 이 그 역할을 합니다. 그러면 DestAbsFilename 은 "C:\Users\lifeisforu\Documents\Unreal Projects\PackageTest\Content\FirstPersonCPP\Blueprints\FirstPersonCharacter2.uasset" 이 됩니다.

 

이제 패키지를 저장하고 나서 보면... 짠!

 

 

위에서 나온 것과 같은 상황이 발생합니다. 애셋 이름과 패키지 이름이 다릅니다. 그냥 패키지만 복사하면 이게 정상입니다.

 

3.3. ObjectPath

 

이제 애셋의 이름을 정상적으로 고쳐줄 필요가 있겠군요. 그러나 package 관련 메서드들에는 절대 RenameAsset 과 관련한 이름이 존재하지 않습니다. 정말 우울한 일이죠. 애셋을 로드해야만 합니다.

 

여기에서 ObjectPath 라는 개념이 나옵니다. StaticLoadObject 는 "Name" 이라는 인자를 받습니다. "LongPackageName" 이나 "Filename" 이 아닌 그냥 "Name" 이라는 인자는, 그것이 오브젝트와 관련되어 있다면, 십중팔구 ObjectPath 라 생각하시면 됩니다.

 

ObjectPath 라는 것은 "{LongPackageName}.{AssetName}" 입니다. 애셋의 이름이 마치 확장자인 것처럼 사용됩니다. 그러나 프로그래머 관점에서는 클래스의 필드 이름 정도로 생각하시는 것이 좋을 것입니다. A 오브젝트의 B 필드에는 A.B 라는 식으로 접근하니 이해하기 편할 것입니다.

 

패키지 파일은 복사했지만 애셋 이름은 고친적이 없으니 이 애셋을 로드하기 위해서는 그 경로는 "/Game/FirstPersonCPP/Blueprints/FirstPersonCharacter2.FirstPersonCharacter" 입니다. 이를 "/Game/FirstPersonCPP/Blueprints/FirstPersonCharacter2.FirstPersonCharacter2" 로 고쳐야겠죠.

 

 

이제 애셋이름이 패키지 이름과 동일하게 생성되는 것을 확인할 수 있습니다.

 

 

4. 결론

 

정리하자면 다음과 같습니다.

 

    • 패키지 : uasset 파일.
    • 애셋 : 패키지를 대표하는 오브젝트.
    • LongPackageName : "/Game" 으로 시작하는 확장자를 배제한 패키지 이름.
    • Filename : 시스템에서의 절대 경로. FPackageName 과 FPaths 라는 유틸리티를 사용해서 LongPackageName 으로부터 변환할 수 있음.
    • ObjectPath : 패키지 이름과 애셋이름을 합친 경로. 애셋이름이 확장자처럼 사용됨.
    • 코드에서 패키지를 복사하려면 애셋 이름도 반드시 따로 변경해야 함.

 

UE4 에서 Visual Studio 2015 Graphcis Diagnostics 이용하기


예전에 DXSDK 에는 PIX for Windows 라는 디버깅 유틸리티가 포함되어 있었습니다. 하지만 Windows 10 에서 DXSDK 가 Windows SDK 에 통합된 이후로는 이를 이용할 수 없게 되었습니다. 이제는 Visual Studio Graphics Diagnostics 를 사용하게 됩니다.


이 Graphics Diagnostics( 그래픽 진단 ) 기능은 Direct3D SDK Layer 를 통해 Direct3D debug device 생성합니다. 그리고 Graphics Debugging, Frame Analysis, GPU Usage 를 이용할 수 있게 해 줍니다.


Visual Studio 의 툴바의 빈칸에서 마우스 오른쪽을 클릭하면 아래 그림과 같이 칸텍스트 메뉴가 뜹니다( 당연히 Visual Studio 를 설치할 때 Windows 10 SDK 가 설치되어 있어야겠죠! ). 거기에서 "Graphics" 항목에 체크를 해 주시면 됩니다.



그러면 다음과 같은 도구가 뜹니다.



저 카메라 버튼을 누르게 되면 그래픽스 진단 모드가 실행됩니다.


사실 그냥 메인 메뉴에서 실행을 하는 것도 가능합니다. "Alt + F5" 라는 단축키도 지정되어 있군요.



자 이제 실행해 봅시다. 잘 되나요?



안타깝게도 "DX11 feature level 10.0 is required to run the engine" 이라는 에러가 발생하는군요. 필자의 경우에는 NVidia Geforce GTX 980 을 사용하고 있는데 한탄할 노릇입니다. 분명히 프로젝트 세팅에서도 DirectX 11( SM5 ) 을 사용하도록 설정했는데 말이죠...


문제의 메시지를 내 뱉는 코드의 위치는 다음과 같습니다( WindowsDynamicRHI.cpp 의 PlatformCreateDynamicRHI() ).



FD3D11DynamicRHIModule::IsSupported() 메서드 내부에서는 FD3D11DynamicRHIModule::FindAdapter() 라는 메서드를 호출합니다. 문제는 그래픽스 진단을 사용해 에디터를 띄울 때 이 메서드가 제대로 된 어댑터를 찾지 못한다는 것입니다.


그 이유는 FindAdapter() 내부에서 bSkipHmdGraphicsAdapter 가 true 값을 가지기 때문입니다. 결국 이것은 bSkipAdapter 값을 true 로 만들게 되죠.



그래픽스 진단 모드를 실행하게 되면 "Capture Adapter" 라는 이름을 가진 어댑터가 생성되는데, 이 때 bIsMicrosoft 가 true 가 됩니다. CVarExplicitAdapterValue 는 -1 이고, bUseHmdGraphicsAdapter 는 false 이므로 bSkipHmdGraphicsAdapter 가 true 입니다. 결국 이 어댑터를 무시하게 되는 것이죠.


이 문제를 해결하기 위해서 엔진 코드를 수정하는 방법이 있는데, 별로 좋은 방법은 아니라고 봅니다. 그래서 여기에서는 옵션을 사용해서 이를 우회하는 방법을 소개하고자 합니다.


필자는 현재 RenderingTest 라는 프로젝트를 만들었는데, 거기 있는 "Config/DefaultEngine.ini" 파일을 수정함으로써 원래 설정을 덮어 쓰고자 합니다. CVarExplicitAdapterValue 를 강제로 0 으로 만들어 준다면 bSkipHmdGraphicsAdapter 를 false 로 만들 수 있습니다.



이와 관련된 콘솔 변수는 다음과 같습니다.



그러므로 "DefaultEngien.ini""r.GraphicsAdapter" 변수를 지정해 주면 됩니다. 저는 한 개의 그래픽 카드만 가지고 있기 때문에 0 번을 지정했는데, 만약 통합된 내장 그래픽 카드가 있는 상태라면 다른 번호를 지정해야 할 수도 있습니다.



이제 즐겁게 디버깅을 하시면 됩니다.

원문 : Other UI Optimization Techniques And Tips

주의 : 번역이 개판이므로 이상하면 원문을 참조하십시오.

주의 : 허락받고 번역한 것이 아니므로 언제든지 내려갈 수 있습니다.



Other UI Optimization Techniques And Tips


확인 완료한 버전: 5.3 - 난이도: 고급


종종 UI 를 최적화할 명백한 방법이 없을 때도 있습니다. 이 섹션은 UI 성능을 개선하는데 도움을 줄 수도 있지만 구조적으로 "명백"하지는 않은, 유지보수하기는 어려울 수도 있거나 이상한 사이드 이펙트를 가질 수도 있는 몇 가지 제안을 하고자 합니다. 다른 것들은 초기 개발을 단순화하기 위한 UI 에서의 동작들을 위한 우회책들인데, 이는 상대적으로 성능 문제를 쉽게 발생시킬 수 있도록 해 줍니다.


RectTransform-Based Layouts


Layout 컴포넌트는 상대적으로 비쌉니다. 왜냐하면 그것들은 그것이 갱신될 때마다 자식 요소들의 크기와 위치를 재계산해야만 하기 때문입니다( 세부 사항을 원하면 Fundamentals 챕터의 Graphic rebuild 섹션을 참고하십시오 ). 만약 주어진 Layout 에 상대적으로 적고 고정된 개수의 요소들이 존재한다면, Layout 은 상대적으로 단순한 구조를 가지며, RectTransform 기반 레이아웃으로 대체하는 것이 가능할 것입니다.


RectTransform 에 대한 앵커를 할당함으로써, RectTrasnform 의 위치와 크기는 그것의 부모에 기반해 스케일링되도록 만들어질 수 있습니다. 예를 들어 단순한 두 개의 컬럼으로 구성된 레이아웃은 두 개의 RectTransform 들로 만들어질 수 있습니다:


    • 왼쪽 컬럼의 앵커는 X:(0, 0.5), Y(0,1) 이어야 합니다.
    • 오른쪽 컬럼의 앵커는 X(0.5,1), Y(0,1) 이어야 합니다.


RectTransform 의 크기와 위치에 대한 계산은 Transform 시스템 자체에 의해 네이티브 코드에서 수행될 것입니다. 이는 일반적으로 Layout 시스템에 의존하는 것 보다 더 좋은 성능을 냅니다. 또한 RectTransform 기반 레이아웃을 설정한 MonoBehaviour 를 작성하는 것도 가능합니다. 그러나 이는 상대적으로 복잡한 작업이며, 이 가이드가 다루는 영역에서 벗어납니다.


Disabling Canvas Renderers


UI 의 서로 다른 부분을 보여주거나 가릴 때, 보통 UI 의 루트에 있는 게임오브젝트를 enable 시키거나 disable 시킵니다. 이는 disable 된 UI 내의 컴포넌트가 input 이나 유니티 콜백을 받지 않도록 만듭니다.


그러나, 이는 Canvas 로 하여금 자신의 VBO 데이터를 버리도록 만듭니다. Canvas 를 다시 enabling 하는 것은 Canvas( 및 그것의 모든 Sub-Canvas ) 로 하여금 리빌드 절차와 리배칭 절차를 실행하도록 만듭니다. 만약 이러한 일이 자주 발생한다면, 증가된 CPU 사용량이 애플리에이션의 프레임 율을 뚝 떨어뜨릴 수 있습니다.


가능하다면, 이상하기는 하지만, UI 를 자신의 Canvas 나 Sub-canvas 상에 shown/hidden 되도록 배치하고 Canvas 나 Sub-canvas 에 붙어 있는 CanvasRenderer 컴포넌트를 enable/disable 시키는 것은 거의 하지 않는 우회책이 있습니다.


이는 UI 의 메쉬들이 그려지지 않도록 만들지만, 그것들은 메모리 상에 남아 있게 되며 원래의 배칭이 보존됩니다. 더우기 OnEnable 이나 OnDisable 콜백이 UI 계층상에서 호출되지 않을 것입니다.


그러나 이는 UI 의 Graphic 들을 GraphicRegistry 에서 제거하지 않으므로 그것들은 여전히 Graphic Raycast 를 검사하기 위해 컴포넌트의 리스트 상에 제출될 거라는 것에 주의하십시오. 이는 감춰진 UI 내의 어떠한 MonoBehaviour 들도 disable 시키지 않을 것이며, 이러한 MonoBehaviour 들은 여전히 Update 와 같은 유니티 생명주기 콜백들을 받을 것입니다.


이 문제를 피하기 위해서는, 이러한 방식으로 disable 될 UI 상의 MonoBehaviour 들이 유니티 생명주기 콜백들을 직접적으로 구현하지 않지만 UI 의 루트 게임오브젝트 상의 "Callback Manager" MonoBehaviour 로부터 자신들의 콜백을 받도록 해야 합니다. 이 "Callback Manager" 는 UI 가 shown/hidden 될 때마다 통지를 받아서 생명주기 이벤트들이 필요에 따라 전파되거나 전파되지 않도록 보장할 수 있습니다. 이 "Callback Manager" 패턴에 대한 더 많은 설명은 이 가이드의 영역을 벗어납니다.


Assigning Event Cameras


만약 가 월드 공간 카메라 모드나 스크린 공간 카메라 모드로 렌더링하도록 설정되어 있는 Canvas 들과 함께 유니티의 내장 Input Manager 를 사용하고 있다면, 항상 각각에 대해 Event Camera 나 Renderer Camera 속성을 설정하는 것이 중요합니다. 스크립트에서, 이는 항상 worldCamera 속성으로 노출됩니다.


만약 이 속성이 설정되지 않는다면, 유니티 UI 는 Main Camera 태그를 가진 게임 오브젝트에 붙어 있는 Camera 컴포넌트들을 검색함으로써 메인 카메라를 검색할 것입니다. 이 검색은 월드 공간 캔버스나 로컬 공간 캔버스당 적어도 한 번은 발생할 것입니다. GameObject.FindWithTag 는 느린 것으로 알려져 있기 때문에, 모든 월드 공간 캔버스와 카메라 공간 캔버스들은 디자인시나 초기화시에 할당된 Camera 속성들을 가질 것을 강력히 권합니다.


이 문제는 Overlay 캔바스에서는 발생하지 않습니다.

원문 : Optimizing UI Controls

주의 : 번역이 개판이므로 이상하면 원문을 참조하십시오.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.



Optimizing UI Controls


확인 완료한 버전: 5.3 - 난이도: 고급


이 섹션은 특정 유형의 UI 컨트롤들을 제출하는 것에 초점을 맞춘 최적화 가이드의 섹션입니다. 대부분의 컨트롤들은 성능 관점에서 상대적으로 유사하지만, 두 가지는 게임이 출시 가능한 상태에 가까워졌을 때 많은 성능 이슈를 일으키므로 특별합니다.


UI Text


유니티의 내장 Text 컴포넌트는 래스터화된 텍스트 글리프( glyph )를 UI 내부에 디스플레이하기 위한 편한 방법입니다. 그러나 일반적으로 알지 못하는 여러 가지 동작이 존재하며, 아직까지 주로 성능 핫스팟이 됩니다. UI 에 텍스트를 추가할 때, 텍스트 글리프는 실제로는 글자 하나당 개별적인 쿼드( quad )로 렌더링된다는 점을 기억해야 합니다. 이러한 쿼드들은 글리프를 둘러싼 빈 공간의 많은 부분을 차지하는 경향이 있습니다. 이는 그것의 모양에 의존하며, 원치않게 다른 UI 요소들의 배칭을 저해하는 텍스트가 배치되는 경우가 많습니다.


Text mesh rebuilds


한 가지 주요 이슈는 UI 텍스트 메쉬를 리빌드하는 것입니다. UI 텍스트 컴포넌트가 변경될 때마다, 그 텍스트 컴포넌트는 실제 텍스트를 디스플레이하기 위해서 사용되는 폴리곤들을 재계산해야 합니다. 이 재계산은 텍스트가 변하지 않더라도 텍스트 컴포넌트나 그것의 부모 게임 오브젝트들이 단순히 disable 되거나 다시 enable 될 때도 발생합니다.


이러한 동작은 점수판이나 통계를 위해 많은 개수의 레이블을 디스플레이하는 UI 에서 문제가 됩니다. 유니티 UI 를 가리거나 보여주는 가장 일반적인 방법은 UI 를 포함하는 게임오브젝트를 enable/disable 시키는 것이며, 많은 개수의 텍스트 컴포넌트를 가지고 있는 UI 는 그것들이 디스플레이될 때마다 원치않는 프레임율 스파이크를 일으키게 될 것입니다.


이 이슈에 대한 잠재적인 우회책을 원한다면, 다음 챕터의 Disabling Canvas Renderes 를 참고하시기 바랍니다.


Dynamic fonts and font atlases


동적 폰트는 전체 문자들의 집합이 매우 많거나 런타임에 앞서 알려지지 았을 때 텍스트를 렌더링하기 편한 방법을 제공합니다. 유니티 구현에서는, 이러한 폰트들은 UI Text 컴포넌트 내에 문자들에 기반해 런타임에 글리프 아틀라스를 빌드합니다.


로드된 각각의 개별 폰트 오브젝트는 자신만의 텍스쳐 아틀라스를 유지할 것입니다. 심지어는 그것이 다른 폰트들과 동일한 폰트 패밀리를 사용한다고 해도 말이죠. 예를 들어, 한 컨트롤 내에서 굵은( bold ) 텍스트를 가진 가진 Arial 을 사용하고, 다른 컨트롤에서 Arial Bold 를 사용하면 동일한 결과를 산출하지만, 유니티는 두 개를 개별적인 텍스쳐 아틀라스에 유지할 것입니다 - 하나는 Arial 을 위해 다른 하나는 Arial Bold 를 위해( 역주 : 하나는 Arial 폰트를 사용하고 텍스트에 굵기 옵션을 준 것이고, 다른 하나는 Arial Bold 폰트를 사용한 것을 의미하는 듯 ).


성능 관점에서 볼 때, 유니티 UI 의 동적 폰트가 개별 크기, 스타일, 문자를 위해 폰트 텍스쳐 아틀라스에 하나의 글리프를 유지한다는 것을 이해하는 것이 중요합니다. 즉, 만약 UI 가 두 개의 텍스트 컴포넌트를 가지고 있고, 둘다 'A' 라는 문자를 디스플레이한다고 하면:


    • 만약 두 Text 컴포넌트가 같은 크기를 공유한다면, 폰트 아틀라스는 그것 안에 하나의 글리프를 가지게 될 것입니다.
    • 만약 두 Text 컴포넌트가 같은 크기를 공유하지 않는다면( 예를 들어 하나는 16 포인트이고 다른 하나는 24 포인트라면 ), 폰트 아틀라스는 서로 다른 크기의 문자 'A' 를 위해 두 개의 복사본을 가지게 될 것입니다.
    • 만약 하나의 Text 컴포넌트는 bold 이고 다른 하나는 그렇지 않다면, 폰트 아틀라스는 굵은 'A' 와 보통 'A' 를 포함하게 될 것입니다.


동적 폰트를 가진 UI Text 오브젝트에 아직 폰트 텍스쳐로 래스터화되지 않은 글리프가 있다면, 폰트 텍스쳐 아틀라스는 반드시 리빌드되어야만 합니다. 새로운 글리프가 현재 아틀라스에 맞다면, 그것이 추가되고 그 아틀라스는 그래픽 디바이스에 다시 업로드될 것입니다. 그러나 현재 아틀라스가 너무 작다면, 시스템은 아틀라스를 리빌드할 것입니다. 이는 두 개의 단계로 수행됩니다.


첫째, 아틀라스는 같은 크기로 리빌드됩니다. 이 때 현재 활성화된 UI Text 컴포넌트에 의해 보이고 있는 글리프만이 사용됩니다. 만약 시스템이 현재 사용중인 글리프들을 새로운 아틀라스에 끼워 맞추는 것에 성공한다면, 그것은 그 아틀라스를 래스터화하고 다음 단계로 진행하지 않습니다.


둘째, 만약 현재 사용중인 글리프 집합이 현재 아틀라스와 같은 크기의 아틀라스에 끼워 맞춰질 수 없다면, 해상도가 더 낮은 쪽을 두 배로 늘린 더 큰 아틀라스가 생성됩니다. 예를 들어 512x512 아틀라스는 512x1024 아틀라스로 확장됩니다.


위의 알고리즘 때문에, 동적 폰트 아틀라스는 한 번 생성되고 나면 크기가 늘어나기만 합니다. Given the cost of rebuilding the texture atlases, 리빌드 동안 반드시 최소화해야 합니다. 이는 두 가지 방식으로 수행될 수 있습니다:


가능할 때마다, 원하는 글리프 셋을 위해서 비-동적 폰트들과 preconfigure support 를 사용하십시오. 이는 일반적으로 제약이 잘 된 Latin/ASCII 문자들과 같은 적은 범위의 크기를 가지는 문자 집합을 사용하는 UI 들을 위해 잘 동작합니다.


만약 전체 유니코드 셋과 같은 극단적으로 큰 범위를 가진 문자들이 지원되어야만 한다면, 폰트는 Dynamic 으로 설정되어야만 합니다. 예측할 수 있는 성능 문제들을 피하기 위해서는, 시작시에 폰트 글리프 아틀라스를 Font.RequestCharactersInTexture 를 통해 적절한 문자들의 집합을 가지도록 미리 준비시키기 바랍니다.


폰트 아틀라스 리빌드는 변경되는 각 UI Text 컴포넌트들을 위해서 개별적으로 발동된다는 것에 주의하십시오. 극단적으로 많은 개수의 Text 컴포넌트들을 띄울 때는, Text 컴포넌트의 내용에서 유일한 문자들을 수집하고 폰트 아틀라스를 미리 준비해야 이득입니다. 이는 글리프 아틀라스가 새로운 글리프가 나타날 때마다 한 번씩 리빌드되지 않고 한 번만 리빌드될 수 있도록 보장해 줄 것입니다.


폰트 아틀라스가 리빌드될 때, 활성화된 UI Text 컴포넌트에 현재 포함되어 있지 않은 모든 문자들은, 그것들이 Font.RequestCharactersInTexture 호출의 결과로서 원래부터 아틀라스에 추가되어 있는 경우라 할지라도, 새로운 아틀라스에 제출되지 않는다는 것에도 주의하시기 바랍니다. 이러한 제한을 우회하기 위해서는, 모든 원하는 문자들이 준비되어 있는 상태로 남아 있도록 하기 위해 Font.textureRebuilt 델리케이트를 구독하고 Font.characterInfo 를 질의하십시오.


Font.textureRebuilt 델리게이트는 현재 문서화되어 있지 않습니다. 그것은 단일 인자를 가진 유니티 이벤트입니다. 이 인자는 그것의 텍스쳐가 리빌드되어 있는 폰트입니다. 이 이벤트의 구독자는 다음 구문을 따라야 합니다:



Specialized glyph renderes


각 글리프 사이에 상대적으로 고정된 위치를 가지고 있는 잘 알려져 있는 글리프의 경우에는, 그들의 글리프를 디스플레이하는 커스텀 컴포넌트를 작성하는 것이 이득입니다. 이것의 예는 스코어 디스플레이가 있습니다.


점수를 위해, 디스플레이 가능한 문자들은 잘 알려진 글리프 셋( 숫자 0 ~ 9 )으로부터 그려지며, 지역에 따라 변하지 않고, 서로 고정된 거리로 보여집니다. 정수를 그것의 숫자로 분리하고 적절한 숫자 스프라이트를 렌더링하는 것은 상대적으로 단순합니다. This sort of specialized digit-display system can be built in a manner that is both allocationless and considerably faster to calculate, animate and display than the Canvas-driven UI Text component.


Fallback fonts and memory usage


큰 문자 셋을 지원해야만 하는 애플리케이션을 위해서는, 폰트 임포터의 "Font Names" 필드에서 많은 폰트들을 리스팅하고자 하는 마음이 듭니다. "Font Names" 필드에 리스팅된 모든 폰트들은 글리프가 주요 폰트 내에 배치될 수 없는 상황에서 대안으로 사용될 수 있을 것입니다. 폴백의 순서는 "Font Names" 필드에서 리스팅된 순서에 의해서 결정됩니다.


그러나 이러한 동작을 지원하기 위해서는 유니티는 "Font Names" 필드에서 리스팅된 모든 폰트들을 메모리에 로드해서 유지할 것입니다. 만약 폰트 문자 셋이 매우 크다면, 폴백 폰트에 의해 소비되는 메모리의 양이 매우 커질 것입니다. 이는 일본어 한자나 중국 문자들과 같은 그림 문자( pictographical ) 폰트를 포함할 때 자주 보이는 현상입니다.


Best Fit and performance


일반적으로, UI Text 컴포넌트의 Best Fit 설정은 절대 사용되어서는 안 됩니다.


"Best Fit" 는 동적으로, 이는 Text 컴포넌트의 바운딩 박스 내에 오우버플로우 없이 디스플레이될 수 있으며 구성 가능한 최소/최대 포인트 크기로 잘린, 가장 큰 정수 포인트 크기로 폰트의 크기를 조정합니다. 그러나 유니티는 디스플레이되고 있는 개별 문자의 크기별로 개별 글리프를 폰트 아틀라스에 렌더링하기 때문에, Best Fit 를 사용하면 다양한 크기의 글리프를 사용하는 아틀라스의 크기를 급격히 넘어서게 될 것입니다.


유니티 5.3 에서는, Best Fit 에 의해 사용되는 크기 검출이 선택적이지 않습니다. 그것은 It generates glyphs in the font atlas for each size increment tested, which further increases the amount of time required to generate font atlases. 이는 아틀라스 오우버플로우를 발생시키는 경향이 있는데, 이는 오래된 글리프가 아틀라스에서 제거되도록 만듭니다. Best Fit 계산을 위한 많은 횟수의 테스트 때문에, 이는 종종 다른 Text 컴포넌트에 의해 사용중인 글리프들을 퇴거시킬 것이고, 적절한 폰트 사이즈가 계산된 후에 적어도 한 번 더 폰트 아틀라스를 리빌드시킬 것입니다. 이러한 특정 이슈는 유니티 5.4 에서 교정되었습니다. 그리고 Best Fit 는 폰트 텍스쳐 아틀라스를 불필요하게 확장하지 않을 것이지만, 여전히 정적으로 크기가 정해진 텍스트보다는 훨씬 더 느립니다.


자주 사용하는 폰트 아틀라스의 리빌드는 런타임 성능을 급격히 떨어뜨릴 것이고 메모리 단편화를 일으킬 것입니다. Best Fit 로 설정된 Text 컴포넌트의 양이 많아질 수록, 이 문제가 악화될 것입니다.


Scroll Views


Fill-rate 문제 다음으로, 유니티 UI 의 스크롤 뷰는 일반적으로 두 번째 가는 런타임 성능 이슈입니다. 스크롤 뷰는 일반적으로 매우 많은 개수의 UI 요소들을 사용해 자신의 칸텐츠를 표현할 것을 요구합니다. 스크롤 뷰를 띄우는 기본적인 두 가지 접근법이 있습니다:


    • 모든 스크롤 뷰 칸텐츠를 표현하기 위해서 충분한 모든 요소들을 채웁니다.
    • 요소들을 풀링하여, 보여지는 칸텐츠를 표현하는 데 필요한 만큼 위치를 재조정합니다.


둘 다 문제를 가진 해결책들입니다.


첫 번째 해결책은 모든 UI 요소들을 인스턴스화기 위해서 필요한 시간을 증가시킵니다. 왜냐하면 표현해야 할 아이템이 늘어날 수록 스크롤 뷰를 리빌드하는 시간이 늘어나기 때문입니다. 한줌의 Text 컴포넌트만을 디스플레이할 필요가 있는 스크롤 뷰처럼, 스크롤 뷰 내에 요구되는 요소의 개수가 적다면, 단순함을 위해 이 기법이 선호됩니다.


두 번째 해결책은 현재 UI 와 레이아웃 시스템 하에서 올바르게 구현하기 위한 코드의 양이 너무 많습니다. 아래에서 나중에 두 가지 가능한 기법에 대해서 논의하도록 하겠습니다. 매우 복잡한 스크롤링 UI 에 대해서, 성능 문제를 피하기 위해서는 몇 가지 종류의 풀링 접근법이 필요합니다.


이러한 이슈들에도 불구하고, 모든 접근법들은 RectMask2D 컴포넌트를 스크롤 뷰에 추가함으로써 개선될 수 있습니다. 이 컴포넌트는 스크롤 뷰 뷰포트 외부에 존재하는 스크롤 뷰 요소들이, Canvas 가 리빌딩될 때 지오메트리가 생성되고 정렬되고 분석되어야 하는, drawable 요소의 리스트에 포함되지 않도록 보장해 줍니다.


Simple Scroll View element pooling


유니티 내장 Scroll View 컴포넌트를 사용하는 것의 자연스러운 이점의 대부분을 보존하면서 스크롤 뷰를 사용하는 오브젝트 풀링을 구현하기 위한 가장 단순한 방법은 하이브리드 접근법을 사용하는 것입니다:


UI 에 요소들을 놓기 위해서, 보이는 UI 요소들을 위한 "placeholder" 로서 Layout Element 컴포넌트를 게임오브젝트와 함께 사용하십시오. 이는 레이아웃 시스템이 스크롤 뷰의 칸텐트를 적절히 계산하도록 해 주고 스크롤바가 적절히 기능하게 해 줄 것입니다.


그리고 나서, 스크롤 뷰의 가시 영역의 보이는 부분에 충분히 맞게 보이는 UI 요소들에 대한 풀을 인스턴스화하고, 이것들을 위치 placeholder 들의 부모로 붙이십시오. 스크롤 뷰가 스크롤링되면, UI 요소들을 재사용해서 뷰에 스크롤된 칸텐트들을 디스플레이하십시오.


이는 배칭되어야 하는 UI 요소들의 개수를 지속적으로 줄이게 될 것입니다. 왜냐하면 배칭 비용은 RectTransform 의 개수가 아니라 Canvas 내의 CanvasRendere 의 개수에 기반해서만 증가할 것이기 때문입니다.


Problems with the simple approach


현재는, UI 요소의 부모가 변경되거나 이웃의 순서가 변경될 때마다, 그 요소와 그것의 하위 요소들은 갱신된 것으로 표시되며 그것들의 Canvas 가 강제로 리빌드됩니다.


그 이유는 유니티가 transform 의 부모를 변경하는 콜백과 이웃의 순서를 수정하기 위한 콜백을 분리하지 않았기 때문입니다. 이 이벤트들은 둘 다 OnTransformParentChanged 콜백을 부릅니다. 유니티 UI 의 Graphic 클래스의 소스( Graphics.cs )에서, 그 콜백이 구현되며 SetAllDirty 메서드를 호출합니다. Graphic 을 갱신함으로써, 시스템은 Graphic 이 다음 프레임에 렌더링되기 전에 그것의 레이아웃과 버텍스들을 리빌드할 수 있도록 만듭니다.


캔버스들에 스크롤 뷰 내의 각 요소에 대한 루트 RectTransform 을 할당하는 것이 가능합니다. 그러면 스크롤 뷰의 전체 칸텐츠가 아니라 부모가 변경된 요소들에 국한해서 리빌드를 할 것입니다. 그러나 이것은 스크롤 뷰를 렌더링하는 데 필요한 드로콜의 개수를 증가시키는 경향이 있습니다. 더우기, 스크롤 뷰 내부의 개별 요소들이 복잡하고 수십개의 Graphic 컴포넌트로 구성되어 있고, 특히 각 요소에 Layout 컴포넌트들이 매우 많다면, 그것들을 리빌드하는 비용은 저사양 디바이스에서 프레임율을 급격하게 떨어뜨리게 될 것입니다.


만약 스크롤 뷰 요소가 가변 크기를 가지지 않는다면, 레이아웃과 버텍스들에 대한 완전한 재계산은 불필요합니다. 그러나 이 동작을 피하는 것은 부모 변화나 이웃 순서 변화 대신에 위치 변화에 기반한 오브젝트 풀링 솔루션을 구현할 것을 요구합니다.


Position-based Scroll View Pools


위에서 언급한 문제들을 피하기 위해서, 오브젝트가 포함된 UI 요소들의 RectTransform 들을 단순하게 이동시킴으로써 오브젝트를 풀링하는 스크롤 뷰를 생성하는 것이 가능합니다. 움직여진 RectTransform 의 크기가 변경되지 않았다면 내용은 다시 빌드될 필요가 없으며, 이는 스크롤 뷰의 성능을 매우 개선하게 됩니다.


일반적으로는 이를 위해 ScrollView 의 커스텀 서브클래스를 작성하거나 커스텀 LayoutGroup 컴포넌트를 작성하는 것이 최선입니다. 후자는 일반적으로 더 단순한 솔루션이며, 유니티 UI 의 LayoutGroup 추상 기저 클래스의 서브클래스를 구현함으로써 성취할 수 있습니다.


커스텀 LayoutGroup 은 밑에 있는 소스 데이터들을 분석해 얼마나 많은 데이터 요소들이 디스플레이되어야 하고 스크롤 뷰의 Content RectTransform 이 적절히 리사이징될 수 있는지 판단합니다. 그리고 나서 그것은 ScrollView change event 들을 구독하고 이를 사용해 그것의 가시적 요소를 적절하게 재배치할 수 있습니다.

원문 : Fill-Rate, Canvases and Input

주의 : 번역이 개판이므로 이상하면 원문을 참조하십시오.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.



Fill-Rate, Canvases and Input


확인 완료한 버전: 5.3 - 난이도: 고급


이 챕터는 유니티 UI 를 구조화하는 것과 관련한 여러 이슈들에 대해 논의합니다.


Remediating Fill-Rate Issues


GPU 프래그먼트 파이프라인 상에서 부하를 제거하기 위해서 취해질 수 있는 두 가지 행동이 있습니다:


    • 프래그먼트 쉐이더의 복잡도를 줄이는 것입니다.
      • 더 많은 세부사항을 원한다면 "UI Shaders and low-spec devices" 섹션을 참고하십시오.
    • 샘플링되어야 하는 픽셀의 개수를 줄이는 것입니다.


UI 쉐이더는 일반적으로 표준화되어 있기 때문에, 가장 일반적인 문제는 단순히 fill-rate 용량을 초과하는 것입니다. 이는 대부분 UI 요소들이 매우 많이 겹쳐 그려지거나 화면의 많은 부분을 차지하는 UI 요소들이 많기 때문에 발생합니다. 둘다 높은 수준의 overdraw 를 유발할 수 있습니다.


fill-rate 에 대한 overutilization 을 줄이고 overdraw 를 줄이려면, 다음과 같은 해결책들을 고려해 보십시오.


Eliminating invisible UI


이 기법은, 플레이어에게 보이지 않는 비활성화된 요소들을 단순화하기 위해, 현존하는 UI 요소들을 최소로 재설계하는 것을 요구합니다. 가장 일반적인 경우는 불투명한 백그라운드를 가진 전체화면 UI 를 여는 것입니다. 이 경우 전체화면 UI 아래에 배치된 모든 UI 요소들은 비활성화될 수 있습니다.


이를 위한 가장 단순한 방법은 루트 게임오브젝트나 안 보이는 UI 요소를 포함하는 게임오브젝트들을 비활성화하는 것입니다. 대안적인 해결책을 원한다면 Disabling Canvas Renderer 섹션을 참고하십시오.


Disabling invisible camera output


불투명한 배경을 가진 전체 화면 UI 가 유니티 UI 에서 열려 있다면, 월드 공간 카메라는 여전히 표준 3D 씬을 UI 뒤에다가 렌더링하고 있을 것입니다. 이 렌더러는 전체화면 유니티 UI 가 전체 3D 씬을 가리고 있다는 사실을 인지하지 못합니다.


그러므로, 만약 완전한 전체화면 UI 가 열렸다면, 차폐된 모든 월드 공간 카메라를 비활성화하는 것은 GPU 부하를 줄여주며, 이는 단순히 불필요한 3D 공간 렌더링을 제거함으로써 수행됩니다.


노트: 만약 Canvas 가 "Screen Space - Overlay" 로 설정되어 있다면, 씬에서 활성하된 카메라의 개수를 무시하고 그려질 것입니다.


Majority-obscured cameras


많은 "전체화면" UI 들은 실제로는 전체 3D 월드를 차폐하지 않습니다. 그러나 월드의 일부분만을 보이게합니다. 이 경우에는, 보이는 월드 부분을 렌더 텍스쳐에 캡쳐하는 것이 더 효율적일 수 있습니다. 만약 월드의 보이는 부분이 렌더 텍스쳐에 "캐싱"되면, 실제 월드 공간 카메라가 비활성화될 수 있으며, 캐싱된 렌더 텍스쳐는 3D 월드에 대한 임포스터 버전을 제공하기 위해서 UI 스크린 뒤에 그려질 수 있습니다.


Composition-based UIs


컴포지션을 통해서 UI 를 생성하는 것은 매우 일반적입니다 - 최종 UI 를 생성하기 위해서 표준 백그라운드와 요소들을 합치고 레이어화하는 것입니다. 이 작업은 상대적으로 단순하고 반복적으로 수행하기 쉽지만, 그것은 유니티 UI 가 transparent 렌더링 큐를 사용하므로 성능에 좋지 않습니다.


백그라운드, 버튼, 그리고 버튼 위의 텍스트로 구성된 단순한 UI 를 생각해 봅시다. 텍스트 글리프( glyph ) 내에 포함된 픽셀의 경우, GPU 는 백그라운드 텍스쳐와 버튼 텍스쳐를 샘플링한 후에 최종적으로 텍스트 아틀라스 텍스쳐를 처리합니다. 결국 세 번의 샘플링이 필요합니다. UI 의 복잡도가 커질수록, 백그라운드 위쪽에 레이어화되어야 하는 요소들이 많아지며, 샘플링 개수는 급격하게 증가할 것입니다.


만약 큰 UI 가 fill-rate 한계( bound )에 도달했다는 것이 밝혀지면, UI 의 장식된( decorative )/불변하는( invariant ) 요소들을 그것의 백그라운드 텍스쳐와 머지하는 특별한 UI 스프라이트를 생성하는 것이 최선입니다. 이는 다른 요소 위에 레이어화되어야 하는 요소의 개수를 줄여주지만, 노가다가 필요하며 프로젝트의 텍스쳐 아틀라스의 크기가 증가합니다.


주어진 UI 를 특별한 UI 스프라이트 위에 생성하는 데 필요한 레이어화된 요소들을 압축하는데 있어서의 원칙은 sub-element 들을 위해서 사용될 수도 있습니다. 제품에 대한 스크롤링 팬을 가진 스토어 UI 를 생각해 봅시다. 각 제품의 UI 요소는 경계선, 배경, 가격을 표시하는 아이콘, 이름, 그리고 다른 정보들을 포함합니다.


스토어 UI 는 백그라운드를 필요로 하지만, 제품은 백그라운드 위에서 스크롤링되기 때문에 제품 요소는 스토어 UI 의 백그라운드 텍스쳐에 머지될 수 없습니다. 그러나 경계선, 가격, 이름, 그리고 다른 요소들은 제품의 백그라운드에 머지될 수 있습니다. 아이콘의 크기와 개수에 따라, fill-rate 절약을 고려해 볼 수 있습니다.


레이어화된 요소들을 합치는 것은 몇 가지 단점을 가지고 있습니다. 특별한 요소들을 더 이상 재사용할 수 없으며, 그것을 생성하기 위해서는 추가적인 아티스트 자원을 요구합니다. 새로운 큰 텍스쳐의 추가는 UI 텍스쳐를 저장하는데 필요한 메모리 총량을 증가시킬 것이며, 특히 UI 텍스쳐가 on-demand 로 로드되거나 언로드되지 않는다면 문제는 심각해질 것입니다.


UI Shaders and low-spec devices


유니티 UI 에 의해 사용되는 내장 쉐이더는 마스킹, 클리핑, 그리고 여러 가지 복잡한 연산에 대한 지원을 포함합니다. 이는 복잡도를 증가시키므로, UI 쉐이더는 iPhone 4 와 같은 low-end 장치 상에서의 더 단순한 유니티 2D 쉐이더와 비교했을 때 매우 느립니다.


만약 마스킹, 클리핑, 그리고 다른 "화려한" 기능들이 low-end 장치에서 필요하지 않다면, 최소 UI 쉐이더와 같은 불필요한 연산을 제거하는 커스텀 쉐이더를 만드는 것이 가능합니다.



UI Canvas Rebuilds


어떤 UI 를 디스플레이하기 위해서, UI 시스템은 스크린상에 표현된 각 UI 컴포넌트를 위한 지오메트리를 생성해야 합니다. 이는 동적 레이아웃 코드를 실행하거나 글자를 UI 텍스트 문자열로 표현하기 위해서 폴리곤을 생성한다거나 드로 콜을 최소화하기 위해서 가능한한 많은 지오메트리를 하나의 메쉬로 통합한다거나 하는 일들을 포함합니다. 이것은 여러 단계의 절차로 구성되며, 이 가이드의 시작부분의 Fandamentals 섹션에서 세부적으로 기술하고 있습니다.


두 가지 주요 원인 때문에 Canvas 를 리빌드하는 것은 성능 문제를 일으킬 수 있습니다:


    • 만약 Canvas 상에서 그려질 수 있는 UI 요소의 개수가 너무 많다면, 자체적으로 배치를 계산하는 비용이 매우 비싸집니다. 이는 요소들을 정렬하고 분석하는 비용 때문이며, 이 비용은 요소의 개수에 따라 비선형적으로 증가합니다.
    • 만약 Canvas 가 자주 갱신되면, 상대적으로 적은 변화를 가지는 Canvas 보다 리프레쉬하는데 더 많은 비용을 소비해야 합니다.


이러한 문제들은 모두 Canvas 상의 요소 개수가 증가하기 때문에 심해지는 경향이 있습니다.


중요하게 기억할 점: 그릴수 있는 UI 요소가 변경될 때마다, Canvas 는 배치 빌딩 절차를 재수행해야만 합니다. 이 절차는 그릴 수 있는 UI 요소 전체를 그것이 변경되었는지 여부와 관계없이 다시 분석합니다. UI 오브젝트의 외형에 영향을 주는 모든 변경을 "변경"이라고 하며, 이는 스프라이트 렌더러에 할당된 스프라이트, transform 위치와 스케일, 텍스트 메쉬에 포함된 텍스트 등을 포함합니다.


Child order


유니티 UI 들은 뒤에서 앞으로 생성되는데, 계층 구조 상에서 오브젝트의 순서가 그것의 정렬 순서를 결정합니다. 계층에서 먼저 등장하는 오브젝트는 나중에 등장하는 오브젝트보다 뒤에 있다고 간주됩니다. 배치는 계층을 위에서 아래로 돌면서 빌드되며, 같은 머티리얼과 텍스쳐를 사용하지만 중간 레이어( intermediate layer, 서로 다른 머티리얼을 가진 그래피컬 오브젝트인데, 그것의 바운딩 박스는 배치 가능한 다른 오브젝트와 겹치며 배치 가능한 두 오브젝트 사이의 계층에 배치되어 있습니다 )를 가지지 않는 모든 오브젝트들이 수집됩니다. 중간 레이어들은 배치를 깨게 됩니다.


Unity Frame Debugger 섹션에서 언급했듯이, 프레임 디버거는 중간 레이어들을 위한 UI 를 확인하기 위해 사용될 수 있습니다. 이는 한 오브젝트가 배치 가능한 다른 두 오브젝트 사이에 끼어들고 있는 상황입니다.


이 문제는 텍스트나 스프라이트가 다른 것들 근처에 위치할 때 자주 발생합니다: 텍스트의 바운딩 박스는 인접한 스프라이트와 보이지는 않지만 겹치게 됩니다. 왜냐하면 텍스트 글리프의 대부분의 폴리곤이 투명하기 때문입니다. 이는 두 가지 방법으로 해결될 수 있습니다:


    • 배칭될 수 없는 오브젝트에 의해서 배칭될 수 있는 오브젝트가 방해받지 않도록 요소를 재정렬합니다: 즉, 배칭될 수 없는 오브젝트를 배칭될 수 있는 오브젝트의 위쪽이나 아래쪽으로 움직입니다.
    • 오브젝트의 위치를 수정해서 보이지 않는 겹침을 제거합니다.


이 두 가지 방법은 모두 유니티 프레임 디버거를 활성화시켜서 수행될 수 있습니다. 유니티 프레임 디버거에서 보이는 드로콜의 개수를 관찰함으로써, UI 요소가 겹침으로써 낭비되는 드로콜의 개수를 최소화하는 순서와 위치를 찾는 것이 가능합니다.


Splitting Canvases


사소하기는 하지만, Canvas 를 나누는 것도 일반적으로 좋은 생각입니다. 요소들을 sub-canvas 나 인접한 Canvas 로 옮기는 것입니다.


( 예를 들어, 튜토리얼 화살표처럼 ) UI 의 특정 부분이 그 UI 의 나머지와는 다르게 깊이가 제어되어야 하는 경우에는 인접한 Canvas 로 옮기는 전략을 사용하는 것이 최선입니다.


다른 경우에는, sub-canvas 가 더욱 편리합니다. 왜냐하면 그것은 부모 Canvas 의 디스플레이 세팅을 상속하기 때문입니다.


일견하기에는 UI 를 sub-canvas 들에 하위분할하는 것이 최상이라 생각할 수 있지만, Canvas 시스템은 분리된 Canvas 들을 배치에 합치지 않는다는 점에 주의하십시오. 성능을 고려한 UI 설계는 리빌드 비용을 최소화하는 것과 드로콜을 낭비하는 것을 최소화하는 것 사이의 균형을 요구합니다.


General guidelines


Canvas  는 Canvas 내부의 변경된 요소들을 아무때나 리배칭하기 때문에, 단순하지 않은 Canvas 는 적어도 두 부분으로 분리하는 것이 일반적으로 최상입니다. 더우기 만약 요소들이 동시에 변경될 것을 기대하고 있다면 같은 Canvas 상에 요소들을 같이 배치하려고 시도하는 것이 최상입니다. 그 예로는 프로그레스 바와 카운트다운 타이머를 들 수 있습니다. 이것들은 모두 같은 데이터에 기반하고 있으므로, 동시에 갱신되는 것을 요구할 것이며, 그것들은 같은 Canvas 에 배치되어야 합니다.


정적이거나 변하지 않는 백그라운드나 레이블과 같은 모든 요소들을 한 Canvas 에 배치하십시오. 이것들은 Canvas 가 처음 디스플레이될 때 한 번에 배칭됩니다. 그리고 앞으로는 더 이상 리배칭이 필요하지 않을 것입니다.


"동적인" 요소들을 다른 캔버스에 배치하십시오 - 그것들은 자주 변경되는 것들입니다. 이는 이 Canvas 가 자주 갱신되는 요소들을 리배칭하는 것을 보장할 것입니다. 만약 동적 요소의 개수가 매우 많아진다면, ( 예를 들어 프로그레스 바, 타이머 표시, 움직이는 요소와 같은 ) 지속적으로 변화하는 요소 집합과 가끔 변화하는 요소 집합으로 분리하십시오.


경험적으로 볼 때 이는 좀 더 어렵습니다. 특히 UI 컨트롤들을 프리팹으로 캡슐화할 때 어렵습니다. 대신에 많은 UI 들은 비용이 많이 드는 컨트롤들을 Sub-Canvas 로 옮김으로써 Canvas 를 하위분할하는 정책을 씁니다.


Unity 5.2 and Optimized Bathcing


유니티 5.2 에서는 배칭 코드가 지속적으로 재작성되었으며, 4.6, 5.0, 5.1 과 비교했을 때 더 많은 성능 개선이 있었습니다. 더우기 1 개 이상의 코어를 가진 장치에서, 유니티 UI 시스템은 대부분의 처리를 워커 스레드에 넘길 것입니다. 일반적으로, 유니티 5.2 는 UI 를 수십개의 Sub-canvas 로 공격적으로 분할할 필요성을 제거했습니다. 모빌 장치 상의 많은 UI 들은 이제 2 ~ 3 개의 Canvas 들만 가지고도 좋은 성능을 낼 수 있습니다.


유니티 5.2 에서의 최적화에 대한 더 많은 정보를 원하신다면, 이 블로그 포스트를 참고하십시오.


Input and Raycasting In Unity UI


기본적으로, 유니티 UI 는 Graphic Raycast 컴포넌트를 사용해서 터치나 마우스 이벤트와 같은 입력 이벤트들을 다룹니다. 이는 일반적으로 Standalone Input Manager 컴포넌트에 의해 제어됩니다. 이름 대문에 Standalone Input Manager 가 "universal" 입력 관리 시스템으로 보이는데, 이는 마우스와 터치를 모두 다룰 것입니다.


Erroneous mouse input detection on mobile (5.3)


5.4 전에는 Graphic Raycaster 가 붙은 각각의 활성화된 Canvas 가 마치 터치 입력이 비활성화 되어 있는 것처럼 마우스의 위치를 검사하기 위해 매프레임 레이캐스트를 수행했습니다. 이는 플랫폼을 구분하지 않고 발생했습니다; iOS 와 안드로이드 장치에는 마우스가 없는데 여전히 마우스 위치를 질의하고 UI 요소들이 마우스 포인터 아래에 있는지 확인하려고 시도했습니다.


이는 CPU 시간을 낭비했으며, 유니티 애플리케이션 CPU 프레임 시간의 5% 이상을 소비하는 것이 밝혀졌습니다.


이 이슈는 5.4 에서 해결되었습니다: 5.4 부터는 마우스를 가지지 않은 장치는 마우스 위치를 질의하지 않으면 불필요한 레이케이트를 수행하지 않을 것입니다.


5.4 전 버전을 사용하고 있다면 모빌 개발자들은 자신만의 Input Manager 클래스를 작성할 것을 강력히 권합니다. 이는 유니티의 Standard Input Manager 를 유니티 UI 소스로부터 복사함으로써 단순화될 수 있으며, ProcessMouseEvent 메서드를 그 메서드에 대한 호출과 함께 주석처리해 버리면 됩니다.


Raycast optimization


Graphic Raycast 는 상대적으로 직관적인 구현이며, 이는 "Raycast Target" 설정이 true 인 모든 Graphic 컴포넌트를 돕니다. 각 Raycast Target 에 대해, Raycaster 는 몇 개의 테스트를 수행합니다. 만약 Raycast Target 이 모든 테스트를 통과하면, 히트 리스트에 그것이 추가됩니다.


Raycast implementation details


테스트는 다음과 같은 시점에 수행됩니다:


    • Raycast Target 이 활성화되어 있고, 그려질 때( 예를 들어 지오메트리 )
    • 입력 포인트가 Raycast Target 이 붙어 있는 RectTransform 내에 존재할 때
    • Raycast Target 이 ICanvasRaycastFilter 컴포넌트를 가지고 있거나 ( 깊이 상관없이 ) 그것의 자식일 때, 그리고 그 Raycast Filter 컴포넌트가 레이캐스트를 허용할 때


히트된 Raycast Target 의 리스트는 깊이에 의해 정렬되며, 순서를 바꾸기 위해서 필터링되며, 카메라 뒤에 렌더링되는 ( 예를 들어 스크린에 보이지 않는 ) 요소들을 제거하기 위해서 필터링됩니다.


Graphic Raycaster 는 Graphic Raycast 의 "Blocking Objects" 속성에 각각의 플래그가 설정되어 있으면 3D 나 2D 물리 시스템으로 레이를 캐스트할 수도 있습니다( 스크립트에서 수행하려면, blockingObjects 라는 속성임 ).


만약 2D 혹은 3D 블락킹 오브젝트가 활성화되어 있다면, raycast-blocking 물리 레이어 상에서 2D 혹은 3D 오브젝트 아래에서 그려지는 모든 Raycast Target 은 히트 리스트로부터 제거될 것입니다.


그리고 나서 최종 리스트가 반환됩니다.


Raycasting optimization tips


모든 Raycast Target 들은 Graphic Raycaster 에 의해서 테스트되어야만 한다면, 포인터 이벤트를 받아야만 하는 IU 컴포넌트 상의 "Raycast Target" 만 활성화하는 것이 최상입니다. Raycast Target 의 리스트를 더 작게 만들고 더 얕은 계층이 순회될 수록, Raycast 테스트의 속도가 빨라질 것입니다.


백그라운드와 텍스트가 모두 색상을 변경할 필요가 있는 버튼과 같이, 포인터 이벤트에 응답해야만 하는 다중의 UI 오브젝트들을 포함하는 복합 UI 컨트롤의 경우에는, 복합 UI 컨트롤의 루트에 하나의 Raycast Target 만을 배치하는 것이 더 좋습니다. 그 단일 Raycast Target 이 포인터 이벤트를 받게 되면, 그 이벤트를 관심있는 컴포넌트들에게 전달할 수 있습니다.


Hierarchy depth and raycast filters


각각의 Graphic Raycast 는 raycast 필터를 찾을 때 루트까지 모든 Transform 계층을 순회합니다. 이 연산의 비용은 계층 내의 깊이에 따라 선형적으로 증가합니다. 계층 내의 각 Transform 에 붙어 있는 모든 컴포넌트들이 ICanvasRaycastFilter 를 구현했는지 확인하기 위해 테스트되어야만 하므로, 이는 싼 연산이 아닙니다.


ICanvasRaycastFilter 를 사용하는 CanvasGroup, Image, Mask, RectMask2D 와 같은 표준 유니티 UI 컴포넌트들이 몇 개 있습니다. 그러므로 이 순회 비용은 쉽게 제거되기 힘듭니다.


Sub-canvas and the OverridedSorting property


Sub-canvas 에 있는 overrideSorting 속성은 Graphic Raycast 테스트가 Transform 계층을 올라가는 것을 막습니다. 만약 정렬이나 레이캐스트 검출 이슈없이 이것이 활성화될 수 있다면, 레이캐스트 계층 순회의 비용을 감소시키기 위해서 이것을 사용하십시오.

원문 : Funadamentals of Unity UI

주의 : 번역이 개판이므로 이상하면 원문을 참조하십시오.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.



Fundametals of Unity UI


확인 완료한 버전: 5.3 - 난이도: 고급


유니티 UI 시스템을 구성하는 여러 부분을 이해하는 것은 중요합니다. 그 시스템을 구성하는 여러 개의 기본 클래스들과 컴포넌트들이 있습니다. 이 챕터는 먼저 이 기사의 시리즈 전반에서 사용될 몇 개의 개념들을 정의하고, 유니티 UI 의 몇 가지 핵심 시스템의 저수준 동작에 대해 논의할 것입니다.


Teminology


Canvas 네이티브 코드 유니티 컴포넌트인데, 이는 게임의 월드 공간의 상위에 그려질 레이어화된 지오메트리( geometry )를 제공하기 위해 유니티 렌더링 시스템에 의해 사용됩니다.


Canvas 들은 자신의 내부 지오메트리를 배칭( batching )할 책임이 있으며, 적절한 렌더링 커맨드들을 생성하고 이것들을 유니티 그래픽스 시스템에 전달합니다. 이것은 전부 네이티브 C++ 코드에서 수행되며, 이는 rebatch 라든가 batch build 라고 불립니다. 캔버스가 리배칭을 요구하는 지오메트리로 구성되어 있다고 표시되면, 그 캔버스는 갱신되었다( dirty )고 판단됩니다.


지오메트리는 Canvas Renderer 컴포넌트에 의해 Canvas 에 제공됩니다.


Sub-canvas 는 그냥 Canvas 컴포넌트인데, 이는 다른 Canvas  컴포넌트에 내포됩니다. Sub-canvas 들은 그들의 부모의 다른 자식들과는 독립적입니다: 한 자식이 갱신되었다고 해서 부모로 하여금 그것의 지오메트리를 리빌드하도록 강제하지는 않을 것이라는 것입니다.


Graphic 클래스는 유니티 UI C# 라이브러리에 의해 제공되는 기반 클래스입니다. 이것은 Canvas 시스템 상에서 그려질 수 있는 지오메트리를 제공하는 모든 유니티 UI C# 클래스들의 기반 클래스입니다. 대부분의 내장 유니티 UI Graphics 들은 MaskableGrphic 서브 클래스를 통해서 구현됩니다. 이는 IMaskable 인터페이스를 통해 그것들이 마스킹될 수 있도록 해 줍니다. Drawable 의 주요 서브클래스들은 Image 와 Text 입니다. 그것들은 자신의 이름과 동일한 컴포넌트들을 제공합니다.


Layout 컴포넌트는 RectTransform 의 위치와 크기를 제어하며, 일반적으로 자신의 칸텐츠에 대한 상대적인 크기와 위치를 요구하는 복잡한 레이아웃을 생성하기 위해서 사용됩니다. Layout 컴포넌트는 RectTransform 에 의존하며, 이는 그것들에 연관된 RectTransform 의 속성에 영향을 미칩니다. 그것들은 Graphic 클래스에 대해서 종속적이지 않으며, 유니티 UI 의 Graphic 컴포넌트들과는 독립적으로 사용될 수 있습니다.


Graphic 컴포넌트와 Layout 컴포넌트는 모두 CanvasUpdateRegistry 클래스에 의존하는데, 이는 유니티 에디터 인터페이스에는 노출되지 않습니다. 이 클래스는 갱신되어야만 하는 Layout 컴포넌트와 Graphic 컴포넌트의 집합을 추적하는 클래스이며, 그것과 연관된 Canvas 가 willRenderCanvases 이벤트를 호출할 때 필요에 의해 트리거들이 갱신됩니다.


Layout 컴포넌트와 Graphic 컴포넌트에 대한 갱신을 rebuild 라 부릅니다. 이 리빌드 절차에 대해서는 이 문서의 나중에 더 세부적으로 다루도록 하겠습니다.


Rendering Details


유저 인터페이스를 유니티 UI 로 만들 때, Canvas 에 의해 그려지는 모든 지오메트리들은 Transparent 큐에 그려질 것이라는 점을 기억하십시오. 즉, 유니티 UI 에 의해서 생성된 지오메트리는 항상 알파블렌딩을 사용해 뒤에서 앞 순서로 그려질 것입니다. 성능 관점에서 기억해야 할 중요한 것은, 폴리곤으로부터 래스터화된 각 픽셀은 마치 그것이 다른 불투명한 폴리곤에 의해 의해 완전히 가려져 있는 것처럼 샘플링될 것이라는 것입니다. 모바일 장치에서는, 고수준의 overdraw 는 GPU 의  fill-rate 수용량( capacity )를 급격하게 초과할 수 있습니다.


The Batch Building Process (Canvases)


배치 빌딩 절차는 Canvas 가 그것의 UI 요소들을 표현하는 메시들을 결합하고 유니티의 그래픽스 파이프라인에 보내기 위한 렌더링 커맨드들을 생성하는 절차입니다. 이 절차의 결과는 Canvas 가 갱신( dirty )되었다고 표시될 때까지 캐싱되고 재사용됩니다. 이러한 갱신은 그것을 구성하는 메시들 중의 하나가 변경될 때마다 발생합니다.


Canvas 에 의해 사용되는 메시는 Canvas 에 붙어 있는 Canvas Renderer 컴포넌트로부터 획득됩니다. 하지만 Sub-canvas 에 포함된 Canvas Renderer 에서 획득하지는 않습니다.


배치를 계산하는 것은 메시를 깊이값에 의해 정렬하고 겹치는지 혹은 머티리얼을 공유하는지 등을 검사하는 작업들을 요구합니다. 이 연산은 멀티-스레드에서 수행되기 때문에 그것의 성능은 일반적으로 CPU 아키텍쳐가 달라질 때마다 달라지며, 특히 모바일 ( 일반적으로 몇 안 되는 CPU 코어를 가진 ) SoC 와 현대 데스크탑의 ( 보통 4 개 이상의 코어를 가지고 있는 ) CPU 사이에서 큰 차이가 납니다.


The Rebuild Process ( Graphics )


리빌드 절차는 유니티 UI 의C# Graphic 컴포넌트의 레이아웃과 메시가 재계산되는 곳입니다. 이는 CanvasUpdateRegistry 클래스에 의해 수행됩니다. 이것은 C# 클래스이며, 그것의 소스는 Unity's Bitbucket 에서 찾아볼 수 있음을 기억하십시오.


CanvasUpdateRegistry 내에서, 흥미로운 메서드는 PerformUpdate 입니다. 이 메서드는 Canvas 컴포넌트가 WillRenderCanvases 이벤트를 호출할 때마다 호출됩니다. 이 이벤트는 프레임당 한 번 불립니다.


PerformUpdate 는 세 단계의 절차로 실행됩니다:


    • 레이아웃을 리빌드하기 위해서 갱신된 Layout 컴포넌트들이 요청됩니다. 이는 ICanvasElement.Rebuild 메서드를 통해 수행됩니다.
    • 컬링과 클리핑을 수행하기 위해서 등록된 모든 ( Mask 같은 ) Clipping 컴포넌트들이 요청됩니다. 이는 ClippingRegistry.Cull 을 통해 수행됩니다.
    • 그래피컬 요소들을 리빌드하기 위해서 갱신된 Graphic 컴포넌트들이 요청됩니다.


Layout 과 Graphic 을 리빌드하기 위해, 이 절차는 여러 개의 부분으로 나뉩니다. Layout 리빌드는 세 가지 부분으로 실행됩니다( PreLayout, Layout, PostLayout ). 그리고 Graphic 리빌드는 두 가지 부분으로 실행됩니다( PreRender, LatePreRender ).


Layout rebuilds


하나 이상의 Layout 컴포넌트에 포함된 컴포넌트의 적절한 위치를 계산하기 위해서는, 그것들의 적절한 계층적 순서를 적용할 필요가 있습니다. 게임 오브젝트 계층에서 루트와 가까운 Layout 들은 잠재적으로 그것들에 내포되어 있는 다른 Layout 들의 위치와 크기를 수정할 수 있습니다. 그러므로 가장 먼저 계산되어야만 합니다.


이를 위해, 유니티 UI 는 계층 내에서의 깊이를 중심으로 갱신된 Layout 컴포넌트들을 정렬합니다. 계층 구조에서 높은 곳에 있는 아이템( 예를 들어 부모 Transform 이 거의 없는 아이템 )들은 리스트의 앞쪽으로 이동됩니다.


그리고 나서 레이아웃을 리빌드하기 위해서 Layout 컴포넌트들의 정렬된 리스트가 요청됩니다: 여기에서 Layout 컴포넌트에 의해서 제어되는 UI 요소들의 위치와 크기가 실제로 수정됩니다. 개별 요소들의 위치가 Layout 의 영향을 받는 방식에 대한 더 세부적인 내용을 원한다면, 유니티 매뉴얼의 UI Auto Layout 섹션을 참고하십시오.


Graphics rebuilds


Graphic 컴포넌트들이 리빌드될 때, 유니티 UI 는 ICanvasElement 인터페이스의 Rebuild 메서드에 대한 제어를 넘깁니다. Graphic 은 이를 구현하며, 리빌드 절차의 PreRender 스테이지 동안 두 개의 리빌드 단계를 실행합니다.


    • 만약 버텍스 데이터가 갱신되었다고 표시되었다면( 예를 들어, 컴포넌트의 RectTransform 의 크기가 변했다면 ), 메시가 리빌드됩니다.
    • 만약 머티리얼 데이터가 갱신되었다고 표시되었다면( 예를 들어, 컴포넌트의 머티리얼이나 텍스쳐가 변경되었다면 ), 동봉된 Canvas Renderer 의 머티리얼이 갱신됩니다.


Graphic 리빌드는 특정 순서로 처리되지는 않습니다. 그리고 어떠한 정렬 연산도 요구하지 않습니다.

원문 : AssetBundle Usage Patterns

주의 : 번역이 개판이므로 이상하면 원문을 참조하십시오.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.

주의 : 아래쪽의 일부 섹션들은 현재 제 관심사와 관계가 없어 보여서 번역하지 않았습니다.



AssetBundle Usage Patterns


확인 완료한 버전: 5.3 - 난이도: 고급


이 문서는 유니티 5 의 애셋, 리소스, 리소스 관리를 다루는 기사 시리즈의 다섯 번째 챕터입니다.


이 시리즈의 이전 챕터에서는 애셋번들의 기초에 대해 다뤘는데, 특히 다양한 로딩 API 의 저수준 동작에 대해 다뤘습니다. 이 챕터는 애셋번들을 실무에서 사용하는 것의 다양한 관점에 대한 문제들과 잠재적 해결책들에 대해 논의합니다.


4.1. Managing Loaded Assets


메모리에 민감한 환경에서 로드된 오브젝트의 크기와 개수를 조심스럽게 제어하는 것은 매우 중요합니다. 유니티는 활성화된 씬에서 오브제트가 제거될 때 자동으로 그것들을 언로드하지 않습니다. 애셋 정리는 특정 시점에 발동되며, 이것은 수동으로 발동될 수도 있습니다.


애셋번들 자체는 매우 주의깊게 관리되어야만 합니다. ( 유니티 캐시에 있거나 AssetBundle.LoadFromFile 에 의해 로드된 ) 로컬 저장소로부터 로드된 애셋번들은 최소한의 메모리 부하를 가지는데, 약 10 ~ 40 KB 를 잘 넘지 않습니다. 이 부하는 매우 많은 개수의 애셋번들이 제출될 때 여전히 문제가 될 수 있습니다.


대부분의 프로젝트들은 유저에게 ( 레벨을 리플레이하는 것과 같은 ) 재경험 칸텐츠를 허용하며, 애셋번들을 로드하거나 언로드해야 하는 시점을 아는 것은 중욯바니다. 만약 애셋번들이 부적절하게 언로드되면, 그것은 메모리상에서 오브젝트가 중복되도록 만들 수 있습니다. 애셋번들을 부적절하게 언로드하는 것은 특정 환경에서 원치않는 동작을 유발할 수도 있는데, 이는 텍스쳐가 없어진다던가 하는 상황을 만듭니다. 이런 일이 왜 일어나는지 이해하려면, Asset, Objects, and Serialization 챕터의 Inter-Object references 섹션을 참고하십시오.


애셋과 애셋번들을 관리할 때 가장 중요한 것은 AssetBundle.Unload 의 인자를 true 로 공급하느냐 false 로 공급하느냐에 따라 다른 동작을 한다는 것을 이해하는 것입니다.


이 API 는 호출되고 있는 애셋번들의 헤더 정보를 언로드합니다. 인자는 이 애셋번들로부터 인스턴스화된 모든 오브젝트를 언로드할지 여부를 지정합니다. 만약 true 라면, 애셋번들로부터 생성된 모든 오브젝트들은 즉시 언로드됩니다 - 심지어 활성화된 씬에서 현재 사용중일지라도 말이죠.


예를 들어, 머티리얼 M 이 애셋번들 AB 로부터 로드되고, M 이 현재 활성화된 씬에서 사용주이라고 가정해 보겠습니다.


만약 AB.Unload(true) 가 호출되면, M 은 씬에서 제거되며 파괴되고 언로드될 것입니다. 그러나 AB.Unload(false) 가 호출되면 AB 의 헤더 정보가 언로드되지만, M 은 씬에 남아 있게 되고 여전히 자신의 기능을 수행할 것입니다. AssetBundle.Unload(false) 호출은 M 과 AB 간의 링크를 끊습니다. 만약 AB 가 나중에 다시 로드된다면, AB 에 포함된 오브젝트들에 대한 새로운 복사본이 메모리에 로드될 것입니다.


만약 AB 가 나중에 다시 로드된다면, 애셋번들의 헤더 정보의 새로운 복사본이 다시 로드될 것입니다. 하지만 M 은 AB 의 새 복사본으로부터 로드되지 않습니다. 유니티는 AB 와 M 의 새로운 복사본 사이의 어떠한 링크도 만들어 주지 않습니다.


만약 AB.LoadAsset() 을 호출해 M 을 다시 로드한다면, 유니티는 M 의 예전 복사본을 AB 안의 데이터의 인스턴스인 것처럼 해석하지 않을 것입니다. 그러므로 유니티는 M 의 새로운 복사본을 로드하고 씬에는 M 에 대한 동일한 두 개의 복사본이 존재하게 될 것입니다.


대부분의 프로젝트의 경우에, 이러한 동작은 원치않는 동작입니다. 대부분의 프로젝트들은 AssetBundle.Unload(true)를 사용해야 하며 오브젝트들이 중복되지 않는다고 확신하기 위한 기법을 써야 합니다. 두 개의 기법이 있습니다:


    1. 애플리케이션 생명주기 동안에 일시적인 애셋번들이 언로드되는 잘 정의된 지점을 가지고 있어야 합니다. 이러한 예로는 레벨이 전환되는 시점이나 로딩 스크린이 나오고 있는 동안이 있습니다. 이는 단순하고 가장 흔한 선택입니다.
    2. 개별 오브젝트들에 대한 참조 카운트를 유지하고, 애셋번들에서 생성된 오브젝트가 더 이상 사용되지 않을 때 애셋번들을 언로드하는 것입니다.


먄약 애플리케이션이 AssetBundle.Unload(false) 를 사용해야만 한다면, 개별 오브젝트들은 두 가지 방식으로 언로드될 수 있습니다:


    1. 원치않는 오브젝트에 대한 모든 참조를 제거하는데, 이는 씬과 코드에서 모두 이루어져야 합니다. 이 작업이 끝나면 Resources.UnloadUnusedAllsets 를 호출합니다.
    2. 씬을 non-additively 로 로드합니다. 이는 현재 씬에 있는 모든 오브젝트들을 파괴하고 Resources.UnloadUnusedAssets 를 자동으로 호출할 것입니다.


만약 프로젝트가 오브젝트가 로드되거나 언로드되는 동안 사용자로 하여금 대기하게 만들 수 있는( 게임 모드나 레벨이 전환되는 사이와 같은 ) 잘 정의된 지점을 가지고 있다면, 이 지점들은 필요한 만큼 많은 오브젝트들을 언로드하고 새로운 오브젝트들을 로드하는데 사용되어야 합니다.


이를 위한 가장 단순한 방법은 분리된 청크의 프로젝트를 씬에 패키징하는 것입니다. 그리고 나서 씬의 모든 종속성과 함께 씬을 애셋번들로 빌드합니다. 그리고 나서 애플리케이션은 씬 "로딩"에 들어 가는데, 이전 씬에 포함된 모든 애셋번들을 완전히 언로드하고, 새로운 씬을 포함하는 애셋번들을 로드합니다.


이것이 가장 단순한 플로우이기는 하지만, 어떤 프로젝트들은 좀 더 복잡한 애셋번들 관리를 요구합니다. 보편적인 애셋번들 설계 패턴이라는 것은 존재하지 않습니다. 각 프로즈게트의 데이터는 서로 다릅니다. 오브젝트들을 애셋번들로 그룹화하는 방법을 결정할 때, 일반적으로 가장 좋은 것은 동시에 로드되거나 업데이트되어야만 하는 오브젝트들을 애셋번들로 묶는 것입니다.


예를 들어, 롤플레잉 게임을 생각해 봅시다. 개별 맵과 컷씬들은 씬에 의해 애셋번들로 그룹화될 수 있습니다. 하지만 어떤 오븢게트들은 대부분의 씬에서 필요할 것입니다. 애셋번들은 초상화, 인게임 UI, 서로 다른 캐릭터 모델 및 텍스쳐를 제공하기 위해서 빌드도리 수 있습니다. 나중에 언급한 오브젝트들과 애셋들은 애플리케이션 시작시에 로드되어 애플리케이션 생명주기 동안 로드된 상태를 유지하는 애셋번들의 2차 집합으로 그룹화될 수 있습니다.


애셋번들이 언로드된 다음에 유니티가 애셋번들로부터 오브젝트를 다시 로드해야 하는 경우에 다른 문제가 발생할 수 있습니다. 이 경우에, 리로드는 실패할 것이며, 유니티 에디터의 계층에서는 (Missing) 오브젝트로 나타날 것입니다.


이 상황은, 모바일 앱이 중단되었을 때나 사용자가 PC 를 lock 했을 때 같이, 유니티가 그것의 그래픽스 칸텍스트에 대한 제어를 잃거나 다시 획득할 때 주로 발생합니다. 이 경우, 유니티는 GPU 에 텍스쳐와 쉐이더를 다시 업로드해야 합니다. 만약 이 애셋들을 위한 소스 애셋번들이 이용할 수 없는 상태라면, 애플리케이션은 "missing shader" 마젠타색으로 씬의 오븢게트를 렌더링하게 될 것입니다.


4.2. Distribution


프로젝트의 애셋번들을 클라이언트에 배포하기 위한 두 가지 기본 방식이 있습니다: 프로젝트와 함께 설치하는 것과 설치 후에 다운로드하는 것. 어떤 방식을 선택하느냐는 프로젝트가 실행되어야 하는 플랫폼의 기능과 제약에 의해 결정됩니다. 모바일 프로젝트들은 보통 초기 설치 크기를 줄이고 무선 다운로드 크기 제한 아래로 파일 크기를 유지하기 위해서 post-install 옵션을 선택합니다. 콘솔이나 PC 프로젝트들은 일반적으로 애셋번들을 초기 설치에 포함시킵니다.


적절한 구조를 만들면 애셋번들이 처음에 어떤 방식으로 전달되었느냐의 여부와는 관계없이 프로젝트의 post-install 로 새로운 혹은 수정된 칸텐트를 패치할 수 있습니다. 이와 관련한 더 많은 정보를 원한다면 이 기사의 Patching with AssetBundles 섹션을 참고하십시오.


4.2.1. Shipped with project


프로젝트에 애셋번들을 포함시키는 것은 애셋번들을 배포하는 가장 단순한 방법입니다. 왜냐하면 그것은 추가적인 다운로드 관리 코드를 필요로하지 않기 때문입니다. 프로젝트가 애셋번들을 설치 파일에 포함시켜야 하는 이유는 크게 두 가지가 있습니다:


    • 프로젝트 빌드 시간을 줄이고 반복적인 개발을 더 단순하게 만들기 위해서입니다. 만약 이 애셋번들이 애플리케이션 자체로부터 개별적으로 갱신될 필요가 없다면, 애셋번들은, 스트리밍 애셋에 애셋번들을 저장함으로써, 애플리케이션에 포함될 수 있습니다. 아래의 Streaming Assets 섹션을 참고하십시오.
    • 업데이트가 가능한 칸텐트의 초기 리비전을 포함시키기 위해서입니다. 이는 보통 초기 설치 이후에 엔드유저의 시간을 절약하기 위함이거나 나중의 패칭을 위한 기반의 역할을 하기 위해서입니다. Streaming Assets 는 이 경우에 이상적입니다. 그러나 커스텀 다운로딩 및 캐싱 시스템을 선택할 수 없다면, 업데이트가능한 칸텐트의 초기 리비전은 스트리밍 애셋에서 유니티 캐시로 로드될 수 있습니다.


4.2.1.1. Streaming Assets


설치시에 유니티 애플리케이션에 모든 유형의 칸텐트를 포함하는 가장 쉬운 방법은 프로젝트를 빌드하기 전에 칸텐트를 /Assets/StreamingAssets/ 폴더에 빌드하는 것입니다. 빌드시에 StreamingAssets 폴더 내에 포함된 모든 애셋은 최종 애플리케이션으로 복사됩니다. 이 폴더는 애셋번들 뿐만 아니라 최종 애플리케이션 내의 모든 유형의 칸텐트를 저장하기 위해 사용될 수 있습니다.


로컬 저장소 상에서 StreamingAssets 폴더의 전체 경로는 런타임에 Application.streamingAssetsPath 프라퍼티를 통해서 접근할 수 있습니다. 대부분의 플랫폼에서 애셋번들들은 AssetBundle.LoadFromFile 을 통해 로드될수 있습니다.


안드로이드 개발자: 안드로이드에서 Application.streamingAssetsPath 는 압축된 .jar 파일을 가리킬 것입니다. 마치 애셋번들이 압축된 것처럼 말이죠. 이 경우, WWW.LoadFromCacheOrDownload 을 호출해서 개별 애셋번들을 로드해야 합니다. 또한 .jar 파일을 압축해제하기 위한 커스텀 코드를 작성하고 애셋번들을 로컬 저장소의 읽기가능 위치로 추출하는 것도 가능합니다.


노트: 스트리밍 애셋은 특정 플랫폼에서는 쓰기가능한 위치가 아닙니다. 만약 프로젝트의 애셋번들이 설치 후에 갱신될 필요가 있다면, WWW.LoadFromCacheOrDownload 를 사용하거나 커스텀 다운로더를 작성하십시오. 세부사항을 원한다면 Custom downloaders - storage 섹션을 참고하십시오.


4.2.2. Downloaded post-install


애셋번들을 모바일 디바이스로 전송하기 위해 선호되는 기법은 번들을 앱 설치 후에 다운로드하는 것입니다. 이는 사용자가 전체 애플리케이션을 다시 다운로드하게 하지 않고도 설치 후에 새로운 혹은 변경된 칸텐트들을 사용해 칸텐트들이 업데이트될 수 있도록 해 줍니다. 모바일 플랫폼에서, 애플리케이션 바이너리들은 비싸고 복잡한 재인증 절차를 밟아야만 합니다. 그러므로 post-install 다운로드를 위한 좋은 시스템을 개발하는 것이 중요합니다.


애셋번들을 전송하기 위한 가장 단순한 방법은 웹 서버에 그것을 배치하고 WWW.LoadFromCacheOrDownload 혹은 UnityWebRequest 를 통해 전송하는 것입니다. 유니티는 자동으로 다운로드된 애셋번들을 로컬 저장소에 캐싱합니다. 만약 다운로드된 애셋번들이 LZMA 로 압축이 되어 있다면, 그 애셋번들은 추후의 빠른 로딩을 위해 압축이 안 된 캐시로 저장될 것입니다. 만약 다운로드된 번들이 LZ4 로 압축이 되어 있다면, 애셋번들은 압축된 상태로 저장될 것입니다.


만약 캐시가 꽉 차면, 유니티는 가장 최근에 사용되지 않은 애셋번들을 캐시에서 제거합니다. 세부사항을 원하면 Built-in caching 을 참고하십시오.


WWW.LoadFromCacheOrDownload 는 결점을 가지고 있음에 주의하십시오. Loading AssetBundles 섹션에서 기술했듯이, WWW 오브젝트는 다운로드를 하는 동안 애셋번들의 데이터와 같은 크기의 메모리를 소비합니다. 이는 원치않는 메모리 스파이크를 유발할 수 있습니다. 이를 피하기 위한 세 가지 방법이 있습니다:


    • 애셋번들을 작은 크기로 만드십시오. 번들이 다운로드되고 있을 때 애셋번들의 최대 크기는 프로젝트의 메모리 예산에 의해 결정될 것입니다. 다운로딩 스크린을 가진 애플리케이션은 애셋번들을 백그라운드에서 스트리밍하는 애플리케이션보다 더 많은 메모리를 할당할 수 있습니다.
    • 만약 유니티 5.3 이상 버전을 사용하고 있다면, 새로운 UnityWebRequest API 의 DownloadHandlerAssetBundle 로 전환하시기 바랍니다. 이는 다운로드 동안 메모리 스파이크를 일으키지 않습니다.
    • 커스텀 다운로더를 작성하십시오. 더 많은 정보를 원한다면 Custom downloaders 섹션을 참고하십시오.


가능하면 UnityWebRequest 를 사용해서 시작하는 것을 권합니다. 만약 5.2 이전 버전을 사용한다면 WWW.LoadFromCacheOrDownload 를 사용하십시오. 만약 built-in API 의 메모리 소비, 캐싱, 동작, 성능이 특정 프로젝트에서 원치 않는 결과를 보여 준다면, 혹은 프로젝트가 플랫폼-특정 코드를 실행해서 그것의 요구를 만족시켜야만 한다면, 커스텀 다운로드 시스템에 투자하십시오.


UnityWebRequest 나 WWW.LoadFromCacheOrDownload 의 사용을 막아야 하는 경우는 다음과 같습니다:


    • 애셋번들 캐시에 대한 fine-grained 제어가 요구될 때
    • 프로젝트가 커스텀 압축 전략을 구현할 필요가 있을 때
    • 프로젝트가 플랫폼-특정 API 를 사용해서, 비활성화되어 있는 동안 데이터를 스트림하고자 하는 요구같은, 특정 요구를 만족시키고자 할 때 
      • 예: iOS 백그라운드 태스크 API 를 사용해 백그라운드에서 데이터를 다운로드하고자 할 때
    • ( PC 처럼 ) 적절한 SSL 지원을 가지고 있지 않은 플랫폼 상에서, SSL 을 통해 전송되어야만 할때


4.2.3. Built-in caching


유니티는 빌트인 애셋번들 캐싱 시스템을 가지고 있는데, 이는 WWW.LoadFromCacheOrDownload 나 UnityWebRequest API 를 통해 다운로드된 애셋번들을 캐싱하기 위해서 사용될 수 있습니다.


두 API 는 모두 애셋번들 버전 번호를 인자로 받는 오우버로드 메서드를 가지고 있습니다. 이 번호는 애셋번들 내에 저장되는 것이 아니며, 애셋번들 시스템에 의해서 생성되지도 않습니다.


캐싱 시스템은 WWW.LoadFromCacheOrDownload 나 UnityWebRequest 에 넘겨진 마지막 버전 번호의 기록을 유지합니다. 다른 API 가 버전 번호와 함께 호출될 때, 캐싱 시스템은 캐싱된 애셋번들이 존재하는지를 확인합니다. 만약 존재한다면, 그것은 애셋번들이 처음 캐싱되었을 때 넘겨진 버전 번호와 현재 호출에 넘겨진 버전 번호를 비교합니다. 만약 이 번호가 일치하면, 시스템은 캐싱된 애셋번들을 로드하게 됩니다. 만약 번호가 일치하지 않거나 캐싱된 애셋번들이 존재하지 않는다면, 유니티는 새로운 복사본을 다운로드합니다. 이 새로운 복사본은 새로운 버전 번호와 연관됩니다.


캐싱 시스템 내의 애셋번들은 그것들이 다운로드되었던 전체 URL 에 의해서가 아니라 파일 이름에 의해서만 식별됩니다. 이는 같은 이름을 가진 애셋번들이 서로 다른 위치에 저장될 수도 있다는 것을 의미합니다. 예를 들면 애셋번들은 CDN( Content Delivery Network )의 다중 서버 상에 배치될 수 있습니다. 파일 이름이 동일한 이상, 캐싱 시스템은 그것들을 같은 애셋번들로 인지합니다.


애셋번들에 버전 번호를 할당하고 이 번호를 WWW.LoadFromCacheOrDownload 에 넘기기 위한 적절한 전략을 세우는 것은 개별 애플리케이션에 달려 있습니다. 대부분의 애플리케이션들은 유니티 5 의 AssetBundleManifest API 를 사용할 수 있습니다. 이 API 는 애셋번들 칸텐트의 MD5 해시를 계산함으로써 각 애셋번들의 버전 번호를 생성합니다. 애셋번들이 변경될 때마다 그것의 해시는 변경되며, 이는 그 애셋번들이 다운로드되어야만 한다는 것을 지시합니다.


노트: 유니티 빌트인 캐시 구현의 특이점( quirk ) 때문에, 예전 애셋번들은 캐시가 꽉 차기 전까지는 삭제되지 않을 것입니다. 유니티는 앞으로의 릴리스에서 이 특이점에 대해 고심할 의도를 가지고 있습니다.


세부사항을 원한다면 Patching with AssetBundles 섹션을 참고하십시오.


유니티의 빌트인 캐싱은 Caching 오브젝트 상의 API 를 호출함으로써 제어될 수 있습니다. 유니티 캐시의 동작은 Caching.expirationDelay 와 Caching.maximumAvailableDiskSpace 를 변경함으로써 제어될 수 있습니다.


Caching.expirationDelay 는 애셋번들이 자동으로 제거되기 전에 기다려야 할 최소한의 시간( 초 )입니다. 만약 이 시간 동안 애셋번들에 대한 접근이 없으면, 그것은 자동으로 지워집니다.


Caching.maximumAvailableDiskSpace determines the amout of space on local storage that the cache may use before it begings deleting AssetBundles that have been used less recently than the expirationDelay. 그것은 바이트 단위로 카운팅됩니다. 제한에 도달하게 되면, 유니티는 가장 옛날에 열린( 혹은 Caching.MarkedAsUsed 로 마킹된 ) 캐시에서 애셋번들을 제거합니다. 유니티는 새로운 다운로드를 완료하기 위해 충분한 공간을 확보할 때까지 캐싱된 애셋번들을 제거할 것입니다.


노트: 유니티 5.3 에서는 빌트인 캐시에 대한 제어가 매우 거칩니다. 특정 애셋번들을 캐시에서 제거하는 것은 불가능합니다. 그것들은 expiration 이나 디스크 공간 부족, 혹은 Caching.CleanCache 호출을 통해서만 제거될 수 있습니다( Caching.CleanCache 는 현재 캐시에 있는 모든 애셋번들을 제거하게 됩니다 ). 이는 개발이나 라이브 연산 동안에는 문제가 될 수 있습니다. 왜냐하면 유니티가 애플리케이션에 의해서 더 이상 사용되지 않는 애셋번들을 자동으로 지워주지 않기 때문입니다.


4.2.3.1. Cache Priming


애셋번들은 파일 이름으로 식별되기 때문에, 애플리케이션에 포함된 애셋번들을 사용하는 캐시를 "미리 지정하는( prime )" 것이 불가능합니다. 이를 위해서는, 각 애셋번들의 초기 혹은 기본 버전을 /Assets/StreamingAssets/ 에 저장하십시오. 그 절차는 Shipped with project 섹션에서 세부적으로 설명한 것과 동일합니다.


애플리케이션이 처음 실행될 때 Application.streamingAssetsPath 로부터 애셋번들을 로딩함으로써 캐시가 생성될 수 있습니다. 그 때부터, 애플리케이션은 WWW.LoadFromCacheOrDownloads 나 UnityWebRequest 를 정상적으로 호출할 수 있습니다.


4.2.4. Custom downloaders


커스텀 다운로더를 작성하는 것은 애플리케이션으로 하여금 애셋번들을 다운로드하고, 압축해제하고, 저장하는 것과 관련한 모든 제어를 할 수 있게 해 줍니다. 커스텀 다운로더를 작성하는 것은 야심찬 애플리케이션을 작성하고 있는 큰 팀들에 대해서만 권장됩니다. 커스텀 다운로더를 작성하는 동안 생각해야 하는 네 가지 정도의 문제가 있습니다:


    • 애셋번들을 다운로드하는 방법.
    • 애셋번들을 저장할 위치.
    • 애셋번들을 압축하는 방법이나 압축할지 여부.
    • 애셋번들을 패치하는 방법.


애셋번들을 패치하는 것과 관련한 더 많은 정보를 원한다면, Patching with AssetBundles 섹션을 참고하십시오.


4.2.4.1. Downloading


대부분의 애플리케이션에서, HTTP 는 애셋번들을 다운로드하는 가장 단순한 방법입니다. 그러나 HTTP 기반 다운로더를 구현하는 것은 단순한 작업이 아닙니다. 커스텀 다운로더는 너무 많은 메모리를 할당하는 행위, 너무 많은 스레드를 사용하는 행위 등을 피해야만 합니다. 유니티의 WWW 클래스는 여기에서 속속들이 설명한 이유 때문에 부적절합니다. WWW 는 높은 메모리 비용을 가지고 있으므로, WWW.LoadFromCacheOrDownload 클래스를 사용하는 경우가 아니라면 유니티의 WWW 클래스를 사용하는 것은 피하는 것이 좋습니다.


커스텀 다운로더를 작성할 때, 세 가지 옵션이 있습니다:


    • C# 의 HttpWebRequest 와 WebClient 클래스.
    • 커스텀 네이티브 플러그인.
    • 애셋 스토어 패키지.


4.2.4.1.1. C# classes


애플리케이션이 HTTPS/SSL 지원을 요구하지 않는다면, C# 의 WebClient 클래스는 애셋번들을 다운로드하기 위한 가장 단순한 가능성있는 메커니즘을 제공합니다. 그것은 관리되는 메모리 할당을 초과하는 일이 없이 로컬 저장소에 파일을 바로 다운로드하는 기능을 제공합니다.


애셋번들을 WebClient 를 통해 다운로드하기 위해서는, 클래스의 인스턴스를 할당하고 그것에 다운로드할 애셋번들의 URL 과 대상 경로를 넘깁니다. 만약 요청 파라미터에 대한 더 많은 제어를 원한다면, HttpWebRequest 클래스를 사용해서 다운로더를 작성하는 것이 가능합니다.


    1. HttpWebResponse.GetResponseStream 으로부터 바이트 스트림을 획득합니다.
    2. 고정 크기 바이트 버퍼를 스택에 할당합니다.
    3. 응답 스트림으로부터 읽어들여 버퍼에 씁니다.
    4. C# File.IO API 를 사용하거나 다른 스트리밍 IO 시스템을 사용해서 버퍼를 디스크에 씁니다.


플랫폼 노트: iOS, 안드로이드, 윈도우즈 폰은 유니티 C# 런타임이 C# HTTP 클래스들을 위한 HTTPS/SSL 지원을 포함하고 있는 유일한 플랫폼들입니다. PC 에서는, C# 클래스를 통해 HTTPS 서버에 접근하는 시도가 인증서 검증 에러( certificate validation error )를 발생시킬 것입니다.


4.2.4.1.2. Asset Store Packages


몇 개의 애셋 스토어 패키지들은 HTTP, HTTPS, 그리고 다른 프로토콜들을 통해서 파일을 다운로드하기 위한 네이티브 코드 구현을 제공하고 있습니다. 커스텀 네이티브 코드 플러그인을 작성하기 전에, 이용가능한 애셋 스토어 패키지들을 평가해 보실 것을 권합니다.


4.2.4.1.3. Custom Native Plugins


커스텀 네이티브 플러그인을 작성하는 것은 매우 시간이 많이 걸리는 작업이며 유니티에서 데이터를 다운로딩하기 위한 더욱 유연한 기법입니다. 프로그래밍 시간이 오래 걸리고 기술적 위험도가 높으므로, 이 기법은 다른 기법들이 애플리케이션의 요구사항을 만족시켜주지 못했을 때만 사용할 것을 권합니다. 예를 들어, 커스텀 네이티브 플러그인은 유니티의 C# SSL 지원이 없는 플랫폼( 윈도우즈, OSX, Linux )에서 애플리케이션이 SSL 통신을 해야만 하는 경우가 있습니다.


커스텀 네이티브 플러그인은 일반적으로 대상 플랫폼의 네이티브 다운로딩 API 에 대한 래퍼일 것입니다. iOS 의 NSURLConnection 과 안드로이드의 java.net.HttpURLConnection 이라는 예제가 있습니다. 이러한 API 를 사용하는 데 있어서의 세부사항을 원한다면 각 플랫폼의 네이티브 문서를 참고하십시오.


4.2.4.2. Storage


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


Application.streamingAssetPath 는 쓰기가능한 위치가 아니며, 애셋번들 캐시를 위해서는 좋지 않은 선택입니다. StreamingAssetsPath 가 포함하는 위치의 예는 다음과 같습니다:


    • OSX: .app 패키지 내; 쓰기 불가능.
    • Windows: 설치 디렉토리 내( 예를 들어 program files ); 보통 쓰기 불가능.
    • iOS: ipa 패키지 내; 쓰기 불가능.
    • Android: 압축된 .jar 파일 내; 쓰기 불가능.


4.3. Asset Assignment Strategies


프로젝트의 애셋을 애셋번들로 어떻게 나눌지를 결정하는 것은 쉽지 않은 문제입니다. 모든 오브젝트에 대해 각각의 애셋번들을 만들거나 하나의 애셋번들만 사용하는 것과 같은 단순한 전략을 취하고 싶은 충동을 느끼게 될 것입니다. 하지만 이러한 해결책은 심각한 단점들을 가지고 있습니다:


    • 너무 적은 애셋번들을 가지고 있습니다...
      • 런타임 메모리 사용량이 증가합니다.
      • 로딩 시간이 증가합니다.
      • Requires larger downloads

    • 너무 많은 애셋번들을 가지고 있습니다...
      • 빌드 시간이 증가합니다.
      • 개발이 복잡해집니다.
      • 전체 다운로드 시간이 증가합니다.


핵심 결정 사항은 오브젝트를 애셋번들에 어떻게 그룹화하느냐입니다. 주요 전략은 다음과 같습니다:


    • 논리적 요소들.
    • 오브젝트 유형들.
    • 동시성 칸텐트.


단일 프로젝트는 서로 다른 칸텐트 카테고리들을 위해 이들 전략을 섞을 수 있고 섞어야만 합니다. 예를 들어, 프로젝트는 UI 요소들을 서로 다른 플랫폼을 위한 애셋번들로 그룹화할 수 있습니다. 그러나 그것의 인터랙티브 칸텐트는 레벨이나 씬 단위로 그룹화합니다. 적용한 전략과 관계없이, 다음은 좋은 가이드라인이 됩니다:


    • 자주 갱신되는 오브젝트는 보통 변경되지 않는 오브젝트와는 다른 애셋번들로 나눕니다.
    • 동시에 로드될 것 같은 오븢게트는 함께 그룹화합니다.


예: 모델, 그것의 애니메이션, 텍스쳐.


    • 만약 오브젝트가 서로 다른 애셋번들 내에 있는 다수 개의 오브젝트들에 대한 종속성을 가지고 있다면, 그 애셋을 개별 애셋번들로 이동시키십시오.
      • 이상적으로 볼 때, 자식 오브젝트들을 그것의 부모 오브젝트들과 함께 그룹화하는 것이 좋습니다.
    • 만약 ( 텍스쳐의 HD 및 SD 버전 처럼 ) 두 개의 오브젝트들이 동시에 로드되는 것을 원하지 않는다면, 그것들을 개별 애셋번들로 나누십시오.
    • 만약 오브젝트들이 서로 다른 임포터 세팅이나 데이터를 가지고 있기 때문에 같은 오브젝트에 대한 서로 다른 버전이 된 것이라면, 애셋번들 Variants 를 대신 사용하십시오.


Once the above guideline are followed, 애셋번들 칸텐트의 50% 보다 적은 부분이 주어진 시간에 로드된다면 애셋번들을 분리하는 것을 고려하시기 바랍니다. 또한 동시에 로드되는 작은 애셋번들( 5 ~ 10 개 보다 적은 애셋 )들을 합치는 것을 고려하시기 바랍니다.


4.3.1. Logical entity grouping


논리적 엔터니 그룹화 전략에서는 오브젝트들이 그것들이 제공하는 프로젝트의 기능적 부분에 기반해서 그룹화됩니다. 이 전략에 따르면, 애플리케이션의 서로 다른 부분들은 서로 다른 애셋번들로 분리됩니다.


예:


    • UI 스크린을 위한 모든 텍스쳐와 레이아웃 데이터를 함께 번들로 묶습니다.
    • 캐릭터 셋을 위한 텍스쳐, 모델, 애니메이션을 함께 번들로 묶습니다.
    • 많은 레벨에서 공유되는 배경 조각들을 위한 텍스쳐와 모델들을 함께 번들로 묶습니다.


논리적 엔터티 그룹화는 가장 일반적인 애셋번들 전략입니다. 그리고 이는 다음과 같은 상황에 특히 적합합니다:


    • DLC( 역주 : Downloadable Content ).
    • 애플리케이션 생명주기 전반에 걸쳐 많은 곳에서 나타나는 엔터티들.


예:


    • 공통 캐릭터나 기본 UI 요소들.
    • 플랫폼이나 성능 세팅에 기반해서 다양해지는 엔터티들.


애셋을 논리적 엔터티에 의해서 그룹화하는 것의 장점은 변경되지 않은 칸텐트들을 다시 다운로드하지 않고도 개별 엔터티들을 쉽게 갱신할 수 있게 한다는 것입니다. 이것이 이 전략이 DLC 를 위해 특히 적합한 이유입니다. 이 전략은 대부분 메모리에 대해 효율적입니다. 왜냐하면 애플리케이션은 현재 사용중인 엔터티를 제출하는 애셋번들만을 로드할 필요가 있기 때문입니다.


그러나 이 전략은 구현하기 애매한 전략입니다. 왜냐하면 개발자들이 개별 오브젝트가 프로젝트에 의해서 언제 어떻게 사용되는지를 정확하게 알아야만 오브젝트들을 애셋번들에 할당할 수 있기 때문입니다.


4.3.2. Type Grouping


타입 그룹화는 가장 단순한 전략입니다. 이 전략에서, 비슷하거나 동일한 타입을 가진 오브젝트들이 같은 애셋번들에 묶입니다. 예를 들어, 여러 개의 서로 다른 오디오 트랙들이 애셋번들에 배치되거나, 여러 개의 서로 다른 언어 파일들이 애셋번들에 배치될 수 있습니다.


이 전략은 단순하지만, 빌드 시간, 로딩 시간, 업데이트 시간의 관점에서 봤을 때는 가장 비효율적입니다. 이 전략은 지역화 파일들처럼 작고 동시에 갱신되는 파일들을 위해서 자주 사용됩니다.


4.3.3. Concurrent content grouping


동시 칸텐트 그룹화는 오브젝트들이 동시에 로드되고 사용될 때 하나의 애셋번들로 묶는 전략입니다. 이 전략은 칸텐트가 매우 지역적인 프로젝트에서 보통 사용됩니다: 여기에서 칸텐트는 애플리케이션의 특정 위치나 시점에서 벗어나서 나타나지 않습니다. 예를 들어, 개별 레벨마다 유일한 아트, 캐릭터, 사운드 이펙트가 나오는 레벨 기반 게임을 들 수 있습니다.


동시-칸텐트 그룹화를 수행하기 위한 가장 일반적인 기법은 씬에 기반해 애셋번들을 생성하는 것입니다. 이 때 씬의 종속성을 거의 혹은 모두 포함하는 씬 기반 애셋번들을 사용합니다.


칸텐트가 매우 지역적이지 않거나 칸텐트가 애플리케이션 생명주기 동안에 다양한 위치에서 나타나는 프로젝트의 경우에는, 동시 칸텐트 그룹화는 논리적 엔터티 그룹화와 결합됩니다. 둘다 주어진 애셋번들의 칸텐트의 유용성을 최대화하기 위해서 필수적인 전략들입니다.


이 시나리오의 예는 오픈월드 게임입니다. 여기에서는 캐릭터들이 랜덤하게 스폰되며 월드 공간에 펴져 있습니다. 이 경우, 캐릭터가 동시에 어디에서 나타날지 예측하는 것은 쉽지 않습니다. 그래서 그것들은 일반적으로 서로 다른 전략을 사용해서 그룹화되어야 합니다.


4.4. Patching with AssetBundles


애셋번들을 패치하는 것은 새로운 애셋번들을 다운로드하고 그것을 현존하는 것과 교체하면 되기 때문에 매우 단순합니다. 만약 애플리케이션의 캐싱된 애셋번들을 관리하기 위해서 WWW.LoadCacheOrDownloadUnityWebRequest 가 사용되었다면, 이는 선택된 API 에 다른 version 매개변수를 넘기기만하므로 단순합니다( 더 많은 세부사항을 원한다면 스크립팅 레퍼런에 대한 위의 링크를 참고하세요 ). 


패칭 시스템에 있어서 해결하기 어려운 문제는 어떠한 애셋번들이 대체되어야 하는지를 찾는 것입니다. 패칭 시스템은 두 개의 리스트를 요구합니다:


    • 현재 다운로드된 애셋번들과 그것들의 버전 정보 리스트.
    • 서버 상의 애셋번들과 그것들의 버전 정보 리스트.


패쳐는 서버측의 애셋번들 리스트를 다운로드하여 애셋번들 리스트와 비교해야 합니다. Missing 애셋번들이나 버전 정보가 변한 애셋번들은 다시 다운로드되어야 합니다.


유니티 5 의 애셋번들 시스템은 빌드가 완료되었을 때 새로운 부가적인 애셋번들을 생성합니다. 이 부가적인 애셋번들은 AssetBundleManifest 오브젝트를 포함합니다. 이 매니페스트 오브젝트는 애셋번들의 리스트와 그것의 해시를 포함하고 있으며, 그것은 이용가능한 애셋번들의 리스트와 버전 정보를 클라이언트에 전송하기 위해서 사용됩니다. 애셋번들 매니페스트 번들에 대한 더 많은 정보를 원한다면 유니티 매뉴얼을 참고하세요.


애셋번들의 변화를 검색하는 커스텀 시스템을 작성하는 것도 가능합니다. 자신만의 시스템을 작성하는 대부분의 개발자들은 애셋번들 파일 리스트를 위해 JSON 같은 업계-표준 데이터 포맷을 사용하며, MD5 와 같은 체크섬을 계산하기 위해서 표준 C# 클래스를 사용합니다.


4.4.1. Differential patching


유니티 5 에서, 유니티는 결정론적인 방식으로 순서화된 데이터를 사용해 애셋번들을 빌드할 수 있습니다. 이는 커스텀 다운로더를 가진 애플리케이션이 differential patching( 역주 : 미분 패칭? ) 을 구현할 수 있도록 해 줍니다. 결정론적 레이아웃을 사용해서 애셋번들을 빌드하기 위해서는, BuildAssetBundleOptions.DeterministicAssetBundle 플래그를 BuildAssetBundles API 를 호출할 때 넘깁니다( 더 많은 세부 사항을 원한다면 스크립팅 레퍼런스 링크를 참고하세요 ).


유니티는 differential patching 을 위한 내장 메커니즘을 제공하지 않습니다. 그리고 WWW.LoadFromCachedOrDownload 나 UnityWebRequest 는 내장 캐싱 시스템을 사용할 때 differential patching 을 수행하지 않습니다. 만약 differntial pathcing 이 요구되면, 커스텀 다운로더를 작성해야 합니다.


4.4.2. iOS On-Demand Resources


온디맨드 리소스는 iOS 와 TVOS 장치에 칸텐트를 제공하기 위한 애플 API 입니다. 이는 iOS 9 장치에서 이용할 수 있습니다. 이것은 앱스토어 상에 런칭하는 데는 요구되지 않습니다만, TVOS 앱을 위해서는 요구됩니다.


애플의 온디맨드 리소스 시스템의 일반적인 개요는 애플 개발자 사이트에서 찾아볼 수 있습니다.


As of Unity 5.2.1, support for App Slicing and On-Demand Resources are both built upon another Apple system, Asset Catalogs. After registering a callback in the Unity Editor, the build pipeline for an iOS applications can report a set of files which will be automatically placed into Asset Catalogs and assigned specified On-Demand Resources tags.


A new UnityEngine.iOS.OnDemandResources API provides runtime support for retrieving and caching On-Demand Resources files. Once resources have been retrieved via ODR, they can then be loaded into Unity via the standard AssetBundle.LoadFromFile API.


For more details and an example project, see this Unity forum post.


4.5. Common Pitfalls


이 섹션은 애셋번들을 사용할 때 프로젝트에서 일반적으로 나타날 수 있는 몇 가지 문제들에 대해서 기술합니다.


4.5.1. Asset duplication


유니티 5 의 애셋번들 시스템은 오브젝트가 애셋번들에서 빌드될 때 오브젝트의 모든 종속성을 고려하지 않습니다. 이는 애셋 데이터베이스를 사용해서 수행됩니다. 이 종속성 정보는 애셋번들에 포함될 오브젝트 집합을 결정하는데 사용하는 정보입니다.


애셋번들에 명시적으로 할당된 오브젝트들은 그 애셋번들로 빌드되어야 할것입니다. 오브젝트의 AssetImporter 가 그것의 assetBundleName 속성을 비어있지 않은 문자열로 설정할 때 오브젝트는 "명시적으로" 할당됩니다. 이는 유니티 에디터에서 수행되는데, 오브젝트의 인스펙터나 에디터 스크립트로부터 애셋번들을 선택함으로써 수행될 수 있습니다.


애셋번들에 명시적으로 할당되지 않은 모든 오브젝트는 표시되지 않은 오브젝트를 참조하는 하나 이상의 오브젝트를 포함하는 모든 애셋번들들에 포함될 것입니다.


만약 두 개의 오브젝트가 서로 다른 애셋번들에 할당되었지만 둘다 공통 종속성 오브젝트를 참조하고 있다면, 그 종속성 오브젝트는 두 애셋번들에 모두 복사됩니다. 중복된 종속성이 인스턴스화될 것입니다. 이는 종속성 오브젝트에 대한 두 개의 복사본이 서로 다른 식별자를 가진 서로 다른 오브젝트로 고려된다는 것을 의미합니다. 이는 애플리케이션의 애셋번들의 전체 크기를 증가시킵니다. 또한 애플리케이션이 그것의 부모를 모두 로드한다면, 오브젝트의 서로 다른 복사본이 메모리에 로드될 것입니다.


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


  1. 서로 다른 애셋번들에 빌드된 오브젝트가 같은 종속성을 가지지 않도록 합니다. 종속성을 공유하는 모든 오브젝트들은 자신의 종속성들을 복사하지 않고 같은 애셋번들에 배치될 수 있습니다.
    • 이 기법은 많은 공유 종속성을 가진 프로젝트에 대해서는 항상 실행가능한 것이 아닙니다. 이는 모놀리식 애셋번들을 생성하며, 그 애셋번들은 편의성과 효율성을 위해서 너무 자주 리빌드되거나 다시 다운로드되어야만 합니다.
  2. 애셋번들을 세그먼트화하면 종속성을 공유하는 두 개의 애셋번들이 동시에 로드되지 않을 것입니다.
    • 이 기법은 레벨 기반 게임과 같은 특정 유형의 프로젝트에 대해서 작동할 것입니다. 그러나 여전히 프로젝트의 애셋번들의 크기, 빌드 시간, 로딩 시간이 불필요하게 늘어납니다.
  3. 종속성 애셋이 자신만의 애셋번들에 빌드되도록 합니다. 이는 전체적으로 중복 애셋의 위험성을 제거합니다만, 복잡도를 증가시킵니다. 애플리케이션은 애셋번들 간의 종속성을 추적해서 AssetBundle.LoadAsset API 호출 전에 올바른 애셋번들이 로드되어 있도록 해야만 합니다.


유니티 5 에서, 오브젝트 종속성은 AssetDatabase API 를 통해 추적될 수 있습니다. 이는 UnityEditor 네임스페이스에 존재합니다. 네임스페이스가 내포하고 있듯이, 이 API 는 유니티 에디터에서만 사용할 수 있으며 런타임에는 사용할 수 없습니다. AssetDatabase.GetDependencies 는 특정 오브젝트나 애셋의 즉각적인 종속성을 모두 찾아내기 위해서 사용됩니다. 이 종속성들은 자신만의 종속성들을 가질 수도 있다는 점에 주의하십시오. 부가적으로, AssetImporter API 는 특정 오브젝트가 할당된 애셋번들을 질의하는데 사용될 수 있습니다.


AssestDatabase 와 AssetImporter API 를 혼용함으로써, 애셋번들의 직접적인 혹은 간접적인 종속성들을 모두 애셋번들에 할당하거나 애셋번들에 할당되지 않은 종속성을 애셋번들이 공유하고 있는지를 확인하는 에디터 스크립트를 작성하는 것이 가능합니다. 애셋이 중복되는 것의 메모리 비용 때문에, 모든 프로젝트에서 그런 스크립트를 제작하기를 권장합니다.


4.5.2. Sprite atlas duplication


다음 섹션은 자동으로 생성된 스프라이트 아틀라스를 결합했을 때 유니티 5 애셋 종속성 계산 코드의 특이함에 대해 기술합니다. 유니티 5.2.2p4 와 유니티 5.3 은 이 동작을 해결하기 위해서 패치되었습니다.


Unity 5.2.2p4, 5.3 and newer


자동으로 생성된 모든 스프라이트 아틀라스들은 스프라이트 아틀라스가 생성된 스프라이트 오브젝트를 포함하는 애셋번들에 할당될 것입니다. 만약 스프라이트 오브젝트가 다수개의 애셋번들에 할당되었다면, 스프라이트 아틀라스는 애셋번들에 할당되지 않고 중복되게 될 것입니다. 또한 만약 스프라이트 오브젝트가 애셋번들에 할당되지 않았다면, 스프라이트 아틀라스는 애셋번들에 할당되지 않을 것입니다.


스프라이트 아틀라스가 중복되지 않도록 하기 위해서는 같은 스프라이트 아틀라스라고 표기된 모든 스프라이트들이 같은 애셋번들에 할당되었는지 확인하십시오.


Unity 5.2.2p3 and older


Automatically-generated sprite atlases will never be assigned to an AssetBundle. Because of this, they will be included in any AssetBundles containing their constituent sprites and also any AssetBundles referencing their constituent sprites.


Because of this problem, it is strongly recommended that all Unity 5 projects using Unity's sprite packer upgrade to Unity 5.2.2p4, 5.3 or any newer version of Unity.


For projects that cannot upgrade, there are two workarounds for this problem:


    1. Easy: Avoid using Unity's built-in sprite packer. Sprite atlases generated by external tools will be normal Assets, and can be properly assigned to an AssetBundle.
    2. Hard: Assign all Objects that use automatically atlased sprites to the same AssetBundle as the sprites.


    • This will ensure that the generated sprite atlas is not seen as the indirect dependency of any other AssetBundles and will not be duplicated.
    • This solution preserves the simple workflow of using Unity's sprite packer, but it degrades developers' ability to separate Assets into different AssetBundles, and forces the re-download of an entire sprite atlas when any data changes on any component referencing the atlas, even if the atlas itself is unchanged.


4.5.3. Android textures


Due to heavy device fragmentation in the Android ecosystem, it is often necessary to compress textures into several different formats. While all Android devices support ETC1, ETC1 does not support textures with alpha channels. Should an application not require OpenGL ES 2 support, the cleanest way to solve the problem is to use ETC2, which is supported by all Android OpenGL ES 3 devices.


Most applications need to ship on older devices where ETC2 support is unavailable. One way to solve this problem is with Unity 5's AssetBundle Variants. (Please see Unity's Android optimization guide for details on other options.)


To use AssetBundle Variants, all textures that cannot be cleanly compressed using ETC1 must be isolated into texture-only AssetBundles. Next, create sufficient variants of these AssetBundles to support the non-ETC2-capable slices of the Android ecosystem, using vendor-specific texture compression formats such as DXT5, PVRTC and ATITC. For each AssetBundle Variant, change the included textures' TextureImporter settings to the compression format appropriate to the Variant.


At runtime, support for the different texture compression formats can be detected using the SystemInfo.SupportsTextureFormat API. This information should be used to select and load the AssetBundle Variant containing textures compressed in a supported format.


More information on Android texture compression formats can be found here.


4.5.4. iOS file handle overuse


이 이슈는 5.3.2p2 에서 해결되었습니다. 현재 버전의 유니티는 이 이슈의 영향을 받지 않습니다.


In versions prior to Unity 5.3.2p2, Unity would hold an open file handle to an AssetBundle the entire time that the AssetBundle is loaded. This is not a problem on most platforms. However, iOS limits the number of file handles a process may simultaneously have open to 255. If loading an AssetBundle causes this limit to be exceeded, the loading call will fail with a "Too Many Open File Handles" error.


This was a common problem for projects trying to divide their content across many hundreds or thousands of AssetBundles.


For projects unable to upgrade to a patched version of Unity, temporary solutions are:


    • Reducing the number of AssetBundles in use by merging related AssetBundles
    • Using AssetBundle.Unload(false) to close an AssetBundle's file handle, and managing the loaded Objects' lifecycles manually


4.6. AssetBundle Variant


A key feature of Unity 5's AssetBundle system is the introduction of AssetBundle Variants. The purpose of Variants is to allow an application to adjust its content to better suit its runtime environment. Variants permit different UnityEngine.Objects in different AssetBundle files to appear as being the "same" Object when loading Objects and resolving Instance ID references. Conceptually, it permits two UnityEngine.Objects to appear to share the same File GUID & Local ID, and identifies the actual UnityEngine.Object to load by a string Variant ID.


There are two primary use cases for this system:


  1. Variants simplify the loading of AssetBundles appropriate for a given platform.
    • Example: A build system might create an AssetBundle containing high-resolution textures and complex shaders suitable for a standalone DirectX11 Windows build, and a second AssetBundle with lower-fidelity content intended for Android. At runtime, the project's resource loading code can then load the appropriate AssetBundle Variant for its platform, and the Object names passed into the AssetBundle.Load API do not need to change.
  2. Variants allow an application to load different content on the same platform, but with different hardware.
    • This is key for supporting a wide range of mobile devices. An iPhone 4 is incapable of displaying the same fidelity of content as an iPhone 6 in any real-world application.
    • On Android, AssetBundle Variants can be used to tackle the immense fragmentation of screen aspect ratios and DPIs between devices.


4.6.1. Limitations


A key limitation of the AssetBundle Variant system is that it requires Variants to be built from distinct Assets. This limitation applies even if the only variations between those Assets is their import settings. If the only distinction between a texture built into Variant A and Variant B is the specific texture compression algorithm selected in the Unity texture importer, Variant A and Variant B must still be entirely different Assets. This means that Variant A and Variant B must be separate files on disk.


This limitation complicates the management of large projects as multiple copies of a specific Asset must be kept in source control. All copies of an Asset must be updated when developers wish to change the content of the Asset.


There are no built-in workarounds for this problem.


Most teams implement their own form of AssetBundle Variants. This is done by building AssetBundles with well-defined suffixes appended to their filenames, in order to identify the specific variant a given AssetBundle represents. Custom code programmatically alters the importer settings of the included Assets when building these AssetBundles. Some developers have extended their custom systems to also be able to alter parameters on components attached to prefabs.


4.7. Compressed or Uncompressed?


애셋번들을 압축할 것이냐 말 것이냐는 주의깊게 생각해 볼 주제입니다. 중요한 질문들은 다음과 같습니다:


    • 애셋번들의 로딩 타임이 중요한 요소입니까? 압축 안 된 애셋번들이 압축된 애셋번들보다 로컬 저장소나 로컬 캐시에서 로드하는데 훨씬 빠릅니다. 압축된 애셋번들을 원격 서버에서 다운로드할 대는 압축 안 된 애셋번들을 다운로드할 때보다 빠릅니다.
    • 애셋번들의 빌드 타임이 중요한 요소입니까? LZMA 와 LZ4 는 파일을 압축할 때 매우 느립니다. 그리고 유니티 에디터는 애셋번들을 직렬적으로 처리합니다. 많은 개수의 애셋번들을 포함하는 프로젝트는 그것들을 압축하는데 많은 시간을 소비하게 될 것입니다.
    • 애플리케이션 크기가 중요한 요소입니까? 만약 애셋번들이 애플리케이션에 포함된다면, 그것들을 압축하는 것이 전체 애플리케이션의 크기를 줄이는데 도움이 될 것입니다. 대안적으로, 애셋번들을 post-install 에 다운로드할 수도 있습니다.
    • 메모리 사용량이 중요한 요소입니까? 5.3 전에는 유니티의 압축해제 메커니즘이 압축해제를 하기 전에 전체 압축된 애셋번들을 메모리에 로드할 것을 요구했습니다. 만약 메모리 사용량이 중요하다면, 압축하지 말든가 LZ4 로 압축된 애셋번들을 사용하십시오.
    • 다운로드 시간이 중요한 요소입니까? 애셋번들이 크거나 사용자가 모바일 3G 나 low-speed meterred connections 와 같은 대역폭이 제한된 환경에 있다면 압축이 필요할 것입니다. 만약 몇 십 메가바이트의 데이터를 high-speed connections 를 사용하는 PC 로 전송하고 있다면, 압축을 배제해도 될 것입니다.


4.8. AssetBundles and WebGL


유니티는 WebGL 프로젝트에서는 개발자들이 압축된 애셋번들을 사용하지 말도록 강력히 권하고 있습니다.


As of Unity 5.3, all AssetBundle decompression and loading in a WebGL project must occur on the main thread. This is because Unity 5.3's WebGL export option does not currently support worker threads. (The downloading of AssetBundles is delegated to the browser via the XMLHttpRequest Javascript API, and will occur off of Unity's main thread.) This means that compressed AssetBundles are extremely expensive to load on WebGL.


With this in mind, you may want to avoid using the default LZMA Format for your AssetBundles and compress using LZ4 instead, which is decompressed very efficiently on-demand. If you need smaller compression sizes then LZ4 delivers, you can configure your web server to gzip-compress the files on the http protocol level (on top of LZ4 compression).

원문 : AssetBundle Fundamentals

주의 : 번역이 개판이므로 이상하면 원문을 참조하세요.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.



AssetBundle Fundamentals


확인 완료한 버전: 5.3 - 난이도: 고급


이것은 유니티 5 의 애샛, 리소스, 그리고 리소스 관리를 다루는 기사 시리즈의 네 번째 챕터입니다.


이 챕터에서는 애셋번들에 대해서 이야기합니다. 여기에서는 애셋번들이 빌드될 때까지의 기본적인 이스템과 함께 애셋번들과 상호작용하기 위해서 사용하는 핵심 API 들에 대해서 소개합니다. 특히, 애셋번들 자체를 로드하거나 언로드하는 것과 애셋번들로부터 특정 애셋과 오브젝트를 언로드하는 것에 대해서도 논의합니다.


애셋번들을 사용함에 있어서, 더 많은 패턴 및 best practices 를 원한다면 이 시리즈의 다음 챕터를 참조하십시오.


3.1. Overview


애셋번들 시스템은 하나 이상의 파일들을 유니티가 인덱싱할 수 있는 압축( archival ) 포맷으로 저장하는 기법을 제공합니다. 이 시스템의 목적은 유니티의 직렬화 시스템과 호환되는 데이터 전송 기법을 제공하는데 있습니다. 애셋번들은 설치 후에 non-code content 를 전송하고 갱신하기 위한 유니티의 주요 도구입니다. 이는 개발자들로 하여금 shipped asset size 를 줄이고, 런타임 메모리 압력을 최소화하고, 엔드 유저의 디바이스를 위해서 최적화된 칸텐트를 선택적으로 로드할 수 있도록 해 줍니다.


애셋번들이 동작하는 방식에 대해서 이해하는 것은 모바일 디바이스를 위해 성공적인 유니티 프로젝트를 빌드하기 위한 기초입니다.


3.2. What's in an AssetBundle?


애셋번들은 두 개의 부분으로 구성됩니다: 헤더와 데이터 세그먼트.


헤더는 애셋번들이 빌드될 때 유니티에 의해서 생성됩니다. 그것은 애셋 번들에 대한 정보를 포함하는데요, 그 정보로는 애셋번들의 식별자, 압축 여부, 매니페이스( manifest ) 등이 있습니다.


매니페스트는 오브젝트의 이름을 키로 사용하는 검색 테이블입니다. 각 엔트리는 주어진 오브젝트를 애셋번들의 데이터 세그먼트의 어느 부분에서 찾을 수 있는지를 가르쳐 주는 바이트 인덱스를 제공합니다. 대부분의 플랫폼에서, 이 검색 테이블은 STL std::multimap 으로 구현됩니다. 주어진 플랫폼에서 STL 구현에 의해 사용되는 특정 알고리즘은 다양하지만, 대부분은 균형잡힌 검색 트리( balanced search tree )를 사용합니다. Windows 와 OSX 에서 파생된 ( iOS 를 포함한 ) 플랫폼들은 red-black tree 를 사용합니다. 그러므로 매니페스트를 생성하기 위해 필요한 시간은 애셋번들 내부의 애셋들이 많아질 수록 비선형적으로 증가합니다.


데이터 세그먼트는 애셋번들 내의 애셋들을 직렬화함으로써 생성된 raw data 를 포함합니다. 만약 데이터 세그먼트가 압축되어 있다면, LZMA 알고리즘이 collective sequence of serialized bytes 에 적용된 것입니다 - 즉, 모든 애셋들은 직렬화되어 있으며, 전체 바이트 배열이 압축된 것입니다.


유니티 5.3 전에는 오브젝트들이 애셋번들 내부에 개별적으로 압축될 수 없었습니다. 결과적으로 5.3 전 버전에서는 압축된 애셋번들로부터 하나 이상의 오브젝트를 읽을 때, 유니티는 전체 애셋번들을 압축해제해야 했습니다. 일반적으로 유니티는 같은 애셋번들에서 발생하는 연속적인 로딩 요청을 최적화하기 위해서 애셋번들에 대한 압축해제된 복사본을 캐싱합니다.


유니티 5.3 은 LZ4 압축 옵션을 추가했습니다. LZ4 압축 옵션으로 빌드된 애셋번들은 애셋번들 내의 오브젝트를 개별적으로 압축하며, 이는 유니티가 압축된 애셋번들을 디스크에 저장할 수 있도록 허용합니다. 이는 유니티가 전체 애셋번들을 압축해제하지 않고도 개별 오브젝트를 압축해제할 수 있도록 해 줍니다.


3.3. The AssetBundle Manager


유니티는 Bitbucket 에 애셋번들 관리자의 구현에 대한 레퍼런스를 개발하고 유지합니다. 이 관리자는 이 챕터에서 세부적으로 설명하는 많은 개념들과 API 들을 사용하며, 애셋번들을 리소스 관리 워크플로우에 통합해야 하는 프로젝트들을 위한 유용한 시작점을 제공합니다.


주목할 만한 특징은 "simulation mode" 를 포함합니다. 유니티 에디터가 활성화되면, 이 모드는 애셋번들로 표시된( tagged ) 애셋들에 대한 요청을 프로젝트의 /Assets/ 폴더 내에 있는 원래 애셋으로 리디렉트( redirect )합니다. 이는 개발자로 하여금 애셋번들을 리빌드하지 않고도 프로젝트에서 작업할 수 있도록 해 줍니다.


애셋번들 관리자는 오픈소스 프로젝트이며, 여기에서 찾아볼 수 있습니다.


3.4. Loading AssetBundles


유니티 5 에서, 애셋번들은 네 개의 API 를 통해서 로드될 수 있습니다. 이 네 개의 API 들의 동작은 두 개의 조건에 의존해 달라집니다:


    1. 애셋번들이 LZMA 압축인가, LZ4 압축인가.
    2. 애셋번들이 로드되고 있는 플랫폼이 어디인가.


다음과 같은 API 들이 있습니다:




3.4.1. AssetBundle.LoadFromMemroyAsync


유니티는 이 API 를 사용하는 것을 추천하지 않습니다.


유니티 5.3.3 업데이트: 이 API 는 유니티 5.3.3 에서 이름이 변경되었습니다. 유니티 5.3.2( 및 이전 )에는, 이 API 가 AssetBundle.CreateFromMemory 로 알려져 있었습니다. 그것의 기능이 변경되지는 않았습니다.


AssetBundle.LoadFromMemoryAsync 는 관리되는 코드의 바이트 배열( byte[] in C# )에서 애셋번들을 로드합니다. 그것은 항상 소스 데이터를 관리되는 코드 바이트 배열에서 새롭게 할당된 연속된 네이티브 메모리 블록의 배열로 복사합니다. 만약 애셋번들이 LZMA 로 압축되었다면, 그것은 복사하는 동안 애셋번들을 압축해제합니다. 압축이 안 되어 있거나 LZ4 로 압축된 애셋번들은 말 그대로 복사됩니다.


이 API 에 의해 소비되는 메모리의 최대량은 적어도 애셋번들의 크기의 두 배입니다: 한 복사본은 API 에 의해 생성된 네이티브 메모리에 있고, 한 복사본은 API 에 넘겨진 관리되는 바이트 배열에 있습니다. 그러므로 이 API 를 통해 애셋번들로부터 로드된 애셋들은 메모리에 세 번 복사됩니다: 하나는 관리되는 코드 바이트 배열, 하나는 네이티브 메모리 복사본, 하나는 애셋 자체를 위해서 GPU 나 시스템 메모리에.


3.4.2. AssetBundle.LoadFromFile


유니티 5.3 업데이트: 이 API 는 유니티 5.3 에서 이름이 변경되었습니다. 유니티 5.2( 및 이전 )에서는 AssetBundle.CreateFromFile 로 알려져 있었습니다. 그것의 기능은 변경되지 않았습니다.


AssetBundle.LoadFromFile 은 매우 효율적인 API 이며, SD 카드나 하드 디스크와 같은 로컬 저장소에서 압축되지 않은 애셋번들을 로드하기 위한 의도로 만들어졌습니다. 만약 애셋번들이 압축되지 않았거나 LZ4 로 압축되었다면, 이 API 의 동작은 다음과 같습니다:


모바일 디바이스: 이 API 는 애셋번들의 헤더만을 로드하며, 데이터는 디스크에 남겨 둡니다. 애셋번들의 오브젝트는 로딩 메서드들이 호출되거나 그것들의 InstanceID 가 역참조될 때 요청에 의해( on-demand ) 로드될 것입니다. 이 시나리오에서는 과도하게 사용되는 메모리가 없습니다.


유니티 에디터: 이 API 는 마치 AssetBundle.LoadFromMemoryAsync 가 사용되거나 바이트들이 디스크에서 일어들여 지는 것처럼 전체 애셋번들을 메모리로 로드할 것입니다. 이 API 는 프로젝트가 유니티 에디터에서 프로우파일링되고 있는 동안에는 메모리 스파이크를 발생시킬 수 있습니다. 이는 디바이스 상에서의 성능에 영향을 미치지 않으며, 개선책을 취하기 전에 이 스파이크를 디바이스에서 다시 테스트해 보아야 합니다.


노트: 유니티 5.3 이전의 안드로이드 디바이스에서, 이 API 는 Streaming Assets 경로에서 애셋번들을 로드하려고 시도할 때 실패할 것입니다. 이는 그 경로의 내용이 압축된 .jar 파일 내부에 존재하기 때문입니다. 더 많은 세부사항에 대해서 알고자 한다면, AssetBundle usage patterns 챕터의 Distribution - shipped with project 섹션을 참고하십시오. 이 이슈는 유니티 5.4 에서 해결되었습니다. 유니티 5.4 이상에서 빌드된 게임에서는 이제 이 API 를 사용해 Streaming Asset 으로부터 애셋번들을 로드할 수 있습니다.


노트: AssetBundle.LoadFromFile 에 대한 호출은 LZMA 압축 애셋번들에 대해서는 항상 실패합니다.


3.4.3. WWW.LoadFromCacheOrDownload


WWW.LoadFromCacheOrDownload 는 원격 서버와 로컬 저장소에서 오브젝트를 로딩하기 위해서 유용한 API 입니다. 파일들은 file:// URL 을 사용해서 로컬 저장소로부터 로드될 수 있습니다. 만약 애셋번들이 유니티 캐시에 제출되었다면, 이 API 는 AssetBundle.LoadFromFile 과 정확히 같은 동작을 수행하게 됩니다.


만약 애셋번들이 아직 캐싱되지 않았다면, WWW.LoadFromCacheOrDownload 는 애셋번들을 소스로부터 읽어들일 것입니다. 만약 애셋번들이 압축되어 있다면, 그것은 워커 스레드( worker thread )를 사용하여 압축을 해제하고 그것을 캐시에 쓰게 됩니다. 그렇지 않다면, 그것은 워커 스레드를 통해 캐시로 바로 쓰여집니다.


일단 애셋번들이 캐싱되면, WWW.LoadFromCacheOrDownload 는 헤더 정보를 캐싱되고 압축해제된 애셋번들에서 로드합니다. 그리고 나서 이 API 는 AssetBundle.LoadFromFile 을 사용해 애셋번들을 로드하는 것과 동일한 동작을 수행합니다.


노트: 고정크기 버퍼를 통해 데이터가 압축해제되고 캐시에 쓰이는 동안, WWW 오브젝트는 애셋번들의 바이트의 전체 복사본을 네이티브 메모리에 유지합니다. 이 부가적인 복사본은 WWW.bytes 속성에 의해서 지속적으로 지원됩니다.


WWW 오브젝트 내에 애셋번들의 바이트를 캐싱하기 위한 메모리 오우버헤드 때문에, WWW.LoadFromCacheOrDownload 를 사용하는 개발자들은 자신들의 애셋번들을 작게 유지할 필요가 있습니다 - 거의 몇 메가 바이트 정도로. 또한 모바일 디바이스와 같은 제한된 메모리를 가진 플랫폼에서 작업하는 개발자들은 메모리 스파이크를 피하기 위해서 자신들의 코드가 한 번에 하나의 애셋번들만을 다운로드하도록 해야 합니다. 애셋번들 크기와 관련한 더 많은 논의를 원한다면, AssetBundle usage Patterns 챕터의 Asset assignment strategies 섹션을 참고하십시오.


노트: 이 API 에 대한 각각의 호출은 새로운 워커 스레드를 생성할 것입니다. 이 API 를 여러 번 호출할 때는 스레드가 너무 많이 생성되지 않도록 주의해야만 합니다. 만약 5 ~ 10 개 이상의 애셋번들이 다운로드되어야 한다면, 적은 개수의 애셋번들이 동시에 다운로드되도록 코드를 작성하는 것을 권장합니다.


3.4.4. AssetBundleDownloadHandler


유니티 5.3 에서 모바일 플랫폼을 위해 소개된 UnityWebRequest API 는 WWW API 를 대체할 더욱 유연한 대안을 제공합니다. UnityWebRequest 는 개발자로 하여금 유니티가 다운로드된 데이터를 어떻게 다룰지 정확하게 지정할 수 있게 합니다. 또한 개발자로 하여금 불필요한 메모리 사용량을 줄일 수 있도록 해 줍니다. UnityWebRequest 를 통해 애셋번들을 다운로드하는 가장 단순한 방법은 UnityWebRequest.GetAssetBundle API 를 호출하는 것입니다.


이 가이드의 목적을 볼 때 흥미로운 클래스는 DownloadHandlerAssetBundle 입니다. 다운로드 핸들러는 WWW.LoadFromCacheOrDownload 와 유사한 동작을 수행합니다. 그것은 워커 스레드를 사용해서 다운로드된 데이터를 고정 크기 버퍼에 스트리밍하고, 다운로드 핸들러의 구성에 따라 버퍼링된 데이터를 임시 저장소나 애셋번들 캐시에 스풀링합니다. LZMA 압축된 애셋번들은 다운로드 동안 압축해제되고 압축되지 않은 캐시로 저장됩니다.


이러한 모든 연산들은 네이티브 코드 상에서 수행되며, 관리되는 힙이 확장될 위험성을 제거해 줍니다. 더우기 이 다운로드 핸들러는 다운로드된 모드 바이트들에 대한 네이티브 코드 복사본을 유지하지 않습니다. 게다가 애셋번들을 다운로드하는 데 필요한 메모리 오우버헤드를 줄여줍니다.


다운로드가 완료되면, 다운로드 핸들러의 assetBundle 프라퍼티는 다운로드된 애셋번들에 대한 접근을 제공합니다. 마치 AssetBundle.LoadFromFile 이 다운로드된 애셋번들상에서 호출되는 것처럼 말이죠.


UnityWebRequest API 는 WWW.LoadFromCacheOrDownload 와 동일한 방식으로 캐싱을 지원하기도 합니다. 만약 캐싱 정보가 UnityWebRequest 오브젝트에 제공되고 요청된 애셋번들이 이미 유니티 캐시에 존재한다면, 애셋번들은 즉시 이용가능해지며 이 API 는 AssetBundle.LoadFromFile 과 동일하게 동작합니다.


노트: 유니티 애셋번들 캐시는 WWW.LoadFromCacheOrDownload 와 UnityWebRequest 사이에서 공유됩니다. 한 API 에 의해서 다운로드된 모든 애셋번들은 다른 API 를 통해서 사용할 수 있습니다.


노트: WWW 와는 다르게, UnityWebRequest 시스템은 내부 워커 스레드 풀과 내부 잡( job ) 시스템을 가지고 있으며, 이는 개발자가 동시에 너무 많은 번들을 다운로드할 수 없게 해 줍니다. 스레드 풀의 크기는 현재 사용자가 설정할 수 없습니다.


3.4.5. Recommendations


일반적으로, 가능하다면 AssetBundle.LoadFromFile 을 사용하십시오. 이 API 는 속도, 디스크 사용량, 런타임 메모리 사용량의 관점에서 가장 효율적입니다.


애셋번들을 다운로드하고 패치해야 하는 프로젝트를 위해서는, 유니티 5.3 이상의 버전을 사용하고 있다면 UnityWebRequest 를 사용하고 5.2 이하의 버전을 사용하고 있다면 WWW.LoadFromCacheOrDownload 를 사용하는 것을 강력히 권합니다. 다음 챕터의 Distribution 섹션에서 설명하듯이, it is possible to prime the AssetBundle Cache with Bundles include within a project's installer.


WWW.LoadFromCacheOrDownload 를 사용할 때는 프로젝트의 애셋번들이 프로젝트의 최대 메모리 예산보다 2 ~ 3% 정도 작은 상태로 유지되도록 하는 것을 추천합니다. 그래야 메모리 사용량 스파이크 때문에 애플리케이션이 종료되는 것을 막을 수 있습니다. 대부분의 프로젝트에서 애셋번들은 5 MB 를 넘어서서는 안되며, 1 ~ 2 개 이상의 애셋번들이 동시에 다운로드되어서는 안 됩니다.


WWW.LoadFromCacheOrDownload 나 UnityWebRequest 를 사용할 때는 애셋번들을 로드한 후에 다운로드 코드가 적절히 Dispose 를 호출하도록 해야 합니다. C# 의 using 문은 WWW 와 UnityWebRequest 를 안전하게 dispose 할 수 있는 가장 편리한 방법입니다.


규모가 큰 엔지니어링 팀을 위한 프로젝트와 단일 캐싱 이나 다운로딩 요구들을 위해서는 커스텀 다운로드가 필요합니다. 커스텀 다운로더를 작성하는 것은 사소한 엔지니어링 작업이 아닙니다. 그리고 모든 커스텀 다운로더는 AssetBundle.LoadFromFile 과 호환되도록 만들어져야만 합니다. 세부사항을 원한다면 다음 챕터의 Distribution 섹션을 참고하십시오.


3.5. Loading Assets From AssetBundles


UnityEngine.Object 는 AssetBundle 오브젝트에 있는 여러 API 를 사용해 애셋번들로부터 로드될 수 있습니다 : LoadAsset, LoadAllAssets, LoadAssetWithSubAssets. 이러한 모든 API 들은 비동기 버전을 가지고 있습니다 - Async 라는 접미어가 있습니다: LoadAssetAsync, LoadAllAssetsAsync, LoadAssetWithSubAssetsAsync.


동기 API 는 항상 비동기 API 보다 적어도 한 프레임 단위에서는 빠릅니다. 이는 특히 유니티 5.1 이전 버전에서는 사실입니다. 유니티 5.2 전에는 모든 비동기 API 들이 한 프레임에 거의 하나의 오브젝트만을 로드했스니다. 이는 LoadAllAssetsAsync 와 LoadAssetWithSubAssetAsync 가 관련 동기 API 들보다 심각하게 느렸음을 의미합니다. 이 동작은 유니티 5.2 에서 교정되었습니다. 비동기 로딩은 이제 자신의 시간 분할( time-slice ) 제한이 걸릴 때까지 한 프레임에 여러 개의 오브젝트를 로드합니다. 그것의 기반이 되는 기술적 요인과 시간 분할과 관련한 세부사항을 살펴보고자 한다면 아래의 Low-level loading details 를 참고하시기 바랍니다.


LoadAllAssets 는 독립적인 다수개의 UnityEngine.Object 들을 로드할 때 사용됩니다. 그것은 애셋번들 내의 대부분의( 혹은 모든 ) 오브젝트들이 로드될 필요가 있을 경우에 사용되야 합니다. 다른 두 API 와 비교해 봤을 때, LoadAllAssets 는 LoadAssets 를 개별적으로 호출했을 때 보다는 약간 빠릅니다. 그러므로 로드될 애셋의 개수가 많지만 한 번에 로드될 필요가 있는 개수가 애셋번들의 내용은 2/3 보다 적을 때는 애셋번들을 다수 개의 작은 번들로 분리하고 LoadAllAssets 를 호출할 것을 권합니다.


LoadAssetWithSubAssets 는 여러 개의 내포된 오브젝트를 포함하고 있는 복합 애셋을 로드해야 할 때 사용해야 합니다. 그러한 예로는 FBX 모델이 있는데, 거기에는 내포된 애니메이션이나 내부에 여러 개의 내포된 스프라이트를 포함하는 스프라이트 아틀라스를 포함하고 있을 수 있습니다. 만약 오브젝트들이 같은 애셋으로부터 로드될 필요가 있지만 애셋번들 내에 관련없는 다른 오브젝트들이 존재한다면, 이 API 를 사용하십시오.


다른 경우에는 LoadAsset 이나 LoadAssetAsync 를 사용하십시오.


3.5.1. Low-loevle loading details


UnityEngine.Object 로딩은 메인 스레드에서 수행됩니다: 오브젝트의 데이터는 워커 스레드에서 저장소로부터 읽혀들여집니다. 유니티 시스템의 스레드에 민감하지 않은 부분들( 스크립팅, 그래픽스 )을 건드리는 것들은 워커 스레드에서 변환됩니다. 예를 들어 VBO 는 메쉬로부터 생성되며, 텍스쳐들은 압축해제됩니다.


5.3 버전 전에는 오브젝트 로딩이 직렬적이며 오브젝트 로딩의 일부분은 메인 스레드에서만 수행되었습니다. 이는 "Integration" 이라 불렸습니다. 워커 스레드가 오브젝트 데이터를 로딩하는 것을 마친 후에, 그것은 새롭게 로드된 오브젝트를 메인 스레드에서 통합하기 위해 중지되었으며, 메인 스레드 통합이 완료될 때까지 ( 다음 오브젝트를 로딩하지 않고 ) 중지된 상태로 남아 있었습니다.


5.3 버전부터는 오브젝트 로딩은 병행처리됩니다. 여러 개의 오브젝트들이 역직렬화되며, 워커 스레드 상에서 처리되고 통합됩니다. 오브젝트가 로딩을 마쳤을 때, 그것의 Awake 콜백이 호출되며 오브젝트는 다음 프레임 동안에 유니티 엔진의 나머지 부분에 대해 이용 가능하게 됩니다.


동기화된 AssetBundle.Load 메서드는 오브젝트 로딩이 완료될 때까지 메인 스레드를 중단시킬 것입니다. 5.3 버전 전에는 비동기 AssetBundle.LoadAsync 메서드는 오브젝트를 메인 스레드에서 통합하기 전까지 메인 스레드를 중단시키지 않을 것입니다. They will also time-slice Object loading so that Object integration does not occupy more than a certain number of milliseconds of frame time. 얼마만큼의 시간이 설정되느냐는 Application.backgroundLoadingProperty 라는 프라퍼티에 의해 설정됩니다.


    • ThreadProirity.High: 프레임당 최대 50 밀리세컨드.
    • ThreadPriority.Normal: 프레임당 최대 10 밀리세컨드.
    • ThreadPriotity.BelowNormal: 프레임당 최대 4 밀리세컨드.
    • ThreadPriority.Low: 프레임당 최대 2 밀리세컨드.


유니티 5.1 전 버전에서, 비동기 API 들은 프레임당 한 오브젝트만 통합했을 것입니다. 이는 버그로 간주되었으며, 유니티 5.2 에서 해결되었습니다. 유니티 5.2 부터는 오브젝트 로딩을 위한 프레임 타임 제한에 도달할 때까지 여러 개의 오브젝트가 로드될 것입니다. AssetBundle.LoadAsync 는 ( 모든 요소가 동일하다는 가정하에서 볼 때 ) 동기 API 보다는 완료되는 시간이 길 것입니다. 왜냐하면 LoadAsync 호출을 제출하는 것과 오브젝트가 엔진에 대해 이용가능한 상태가 되는 것 사이에 최소 한 프레임의 지연이 존재하기 때문입니다.


실제 오브젝트들과 애셋들을 가지고 테스트해 보면 다른 결과가 나옵니다. 5.2 전에는 특정한 큰 텍스쳐를 로우엔드 디바이스에서 로드하면, 동기 메서드를 사용했을 때 7 ms 가 나오고 비동기 메서드를 사용했을 때 70 ms 가 나왔습니다. 5.2 이후에는 그 차이가 0 에 가까워졌습니다.


3.5.2. AssetBundle dependencies


유니티 5 의 애셋번들 시스테멩서 애셋번들 사이의 종속성은 두 개의 API 를 통해 자동적으로 트래킹되는데, 이는 런타임 환경과 관련이 있습니다. 유니티 에디터에서는 AssetBundle 종속성이 AssetDatabase API 를 통해 질의될 수 있습니다. 애셋번들 할당과 종속성은 AssetImporter API 를 통해서 접근되거나 변경될 수 있습니다. 런타임에는 유니티가 애셋번들 빌드 동안에 생성된 종속성 정보를 로드할 수 있는 선택적인 API 를 제공합니다. 이는 ScriptableObject 기반의 AssetBundleManifest API 입니다.


부모 애셋번들의 UnityEngine.Object 들이 다른 애셋번들의 UnityEngine.Object 들에 대한 하나 이상의 참조를 가질 때, 한 애셋 번들은 다른 애셋번들에 대해 "종속적"입니다. 오브젝트간 참조에 대한 더 많은 정보를 원한다면, Assets, Objects and Serialization 기사의 Inter-Object references 섹션을 참고하십시오.


그 기사의 Serialization and instances 섹션에서 기술햇듯이, 애셋번들은 애셋번들에 포함되어 있는 개별 오브젝트의 파일 GUID 와 로컬 ID 에 의해 식별되는 소스 데이터를 위한 소스로서 기능합니다


오브젝느는 인스턴스 ID 가 처음 역참조될 때 로드되기 때문에, 그리고 그것의 애셋번들이 로드될 때 오브젝트는 유효한 인스턴스 ID 를 할당받기 때문에, 애셋번들이 로드되는 순서는 중요치 않습니다. 대신에 오브젝트 자체를 로드하기 전에 오브젝트의 종속성을 포함하는 모든 애셋번들을 로드하는 것은 중요합니다. 유니티는 부모 애셋번들이 로드될 때 자식 애셋번들을 자동으로 로드해 주지 않을 것입니다.


예:


머티리얼 A 가 텍스쳐 B 를 참조하고 있다고 가정합시다. 머티리얼 A 는 AssetBundle 1 에 패키징되어 있고, 텍스쳐 B 는 AssetBundle 2 에 패키징되어 있습니다.




이 경우, Asset Bundle 2 는 Asset Bundle 1 으로부터 머티리얼 A 를 로딩하기 전에 로드되어야만 합니다.


이는 Asset Bundle 2 가 Asset Bundle 1  전에 로드되어야만 한다든가 그 텍스쳐 B 가 Asset Bundle 2 로부터 명시적으로 로드되어야만 한다든가 하는 것을 암시하지는 않습니다. Asset Bundle 1 로부터 머티리얼 A 를 로딩하는 것보다 Asset Bundle 2 를 먼저 로드하기만 하면 충분합니다.


유니티는 Asset Bundle 1 이 로드될 때 자동으로 Asset Bundle 2 를 로드하지 않을 것입니다. 이는 스크립트를 통해 명시적으로 수행되어야만 합니다. Asset Bundle 1 과 2 를 로드하기 위해서 사용되는 특정 애셋번들 API 는 중요하지 않습니다. WWW.LoadFromCacheOrDownload 를 통해 로드된 애셋번들은 AssetBundle.LoadFromFile 이나 AssetBundle.LoadFromMemoryAsync 를 통해 로드된 애셋번들과 자유롭게 혼용될 수 있습니다.


3.5.3. AssetBundle manifest


BuildPipeline.BuildAssetBundles API 를 통해 애셋번들 빌드 파이프라인을 실행할 때, 유니티는 애셋번들의 종속성 정보를 포함하는 오브젝트를 직렬화합니다. 이 데이터는 개별 애셋번들에 저장되며, 이는 AssetBundleManifest 타입의 오브젝트를 포함합니다.


이 애셋은 애셋번들이 빌드된 부모 디렉토리와 같은 이름을 가지고 애셋번들 내에 저장될 것입니다. 만약 프로젝트가 그것의 애셋번들을 (projectroot)/build/Client/ 라는 폴더에 빌드한다면, 매니페이스를 포함하는 애셋번들은 (projectroot)/build/Client/Client.manifest 로 저장될 것입니다.


매니페스트를 포함하는 애셋번들은 다른 애셋번들들처럼 로드되거나 캐싱되거나 언로드될 수 있습니다.


AssetBundleManifest 오브젝트는 그 자체로 GetAllAssetBundles 라는 API 를 제공해 매니페스트와 함께 빌드된 모든 애셋번들들을 리스팅할 수 있도록 합니다. 그리고 특정 애셋번들의 종속성을 질의하기 위한 두 개의 메서드를 제공합니다.


AssetBundleManifest.GetAllDependencies 는 애셋번들의 종속성을 모두 반환합니다. 이는 애셋번들의 계층구조상의 모든 자손들의 종속성을 포함합니다.


AssetBundleManifest.GetDirectDependencies 는 애셋번들의 바로 아래 자손들의 종속성만을 반환합니다.


이 API 들은 모두 문자열 배열을 할당한다는 점에 주의하시기 바랍니다. 자주 사용하지는 마십시오. 그리고 애플리케이션 생명주기의 성능이 중요한 시점에는 사용하지 마십시오.


3.5.4. Recommandations


애플리케이션에서 성능이 중요한 부분에 도달하기 전에 필요한 오브젝트를 최대한 많이 로드하는 것이 좋습니다. 예를 들어 메인 게임 레벨이나 월드에 들어 가기 전에 로드하는 것이 좋습니다. 이는 모바일 플랫폼에서 특히 중요합니다. 이 환경에서 로컬 저장소에 대한 접근은 느리며 플레이를 하는 동안에 오브젝트를 마구 로드하고 언로드하는 것은 가비지 콜렉팅을 발생시킬 수 있습니다.


애플리케이션이 상호작용을 하는 동안에 오브젝트들을 로드하고 언로드해야만 하는 프로젝트의 경우, 오브젝트와 애셋번들을 언로드하는 것에 대한 더 많은 정보를 원한다면 AssetBundle usgae pattern 기사의 Managing loaded assets 섹션을 참고하십시오.

원문 : Assets, Objects and Serialization.

주의 : 번역이 개판이므로 원문을 참조하세요.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.



Assets, Objects and Serialization


확인 완료한 버전 : 5.4 - 난이도 : 고급


이 기사는 유니티 5 의 애셋, 리소스, 그리고 리소스 관리를 다루는 기사 시리즈의 두 번째 챕터입니다.


이 챕터는 유니티의 직렬화 시스템의 내부 구현과 유니티 에디터와 런타임에서 서로 다른 오브젝트 간의 빠른 참조를 유지하는 방법에 대해서 다룹니다. 오브젝트와 애셋의 기술적인 차이에 대해서도 논의합니다. 여기에서 다루는 주제들은 유니티에서 애셋들을 효율적으로 로드하고 언로드하는 방법을 이해하기 위해서 필수적입니다. 적절한 애셋 관리는 로딩 시간을 짧게 유지하고 메모리 사용을 적게 하기 위해서 중요합니다.


1.1. Inside Assets and Objects


유니티에서 데이터를 적절하게 관리하기 위한 방법을 이해하기 위해서는, 유니티가 데이터를 식별하고 직렬화하는 방법을 이해하는 것이 중요합니다. 첫 번째 중요 지점은 애셋과 UnityEngine.Object 의 차이점입니다.


애셋은 디스크 상의 파일이며, 유니티 프로젝트의 Assets 폴더에 저장됩니다. 예를 들어, 텍스쳐 파일들, 머티리얼 파일들, FBX 파일들은 모두 애셋입니다. 어떤 애셋들은 머티리얼들처럼 유니티 포맷( formats native to Unity )들로 된 데이터를 포함합니다. FBX 파일과 같은 다른 애셋들은 자체 포맷( native formats )들로 처리될 필요가 있습니다.


대문자 'O' 로 시작하는 UnityEngine.Object( 혹은 Object ) 들은 리소스의 특정 인스턴스를 집합적으로 기술하는 직렬화된( serialized ) 데이터의 집합입니다. 이는 유니티 엔진이 사용하는 어떤 리소스 타입이든 될 수 있습니다. 그 예로는 메쉬( mesh ), 스프라이트( sprite ), 오디오 클립( AudioClip ), 애니메이션 클립( AnimationClip ) 등이 있습니다. 모든 오브젝트들은 UnityEngine.Object 기저 클래스의 서브클래스입니다.


대부분의 오브젝트 타입들은 내장 타입( built-in type )인데, 두 개의 특별한 타입들이 있습니다.


    1. ScriptableObject 는 개발자가 자신만의 데이터 타입을 정의할 수 있도록 해 주는 편리한 시스템을 제공합니다. 이 타입들은 유니티에 의해 자연스럽게 직렬화되고 역직렬화( deserialized )되며, 유니티 에디터의 인스펙터 윈도우에서 다뤄집니다.
    2. MonoBehaviourMonoScript 를 링크하는 래퍼( wrapper )를 제공합니다. MonoScript 는 유니티가 특정 어셈블리( assembly )나 네임 스페이스( namespace ) 내의 특정 스크립팅 클래스에 대한 참조를 유지하기 위해서 사용하는 내부 데이터 타입입니다. MonoScript 는 어떠한 실제 실행 코드도 포함하지 않습니다.


애셋과 오브젝트 사이에는 one-to-many 관계가 성립합니다; 즉 주어진 애셋 파일은 하나 이상의 오브젝트를 포함합니다.


1.2. Inter-Object References


모든 UnityEngine.Object 는 다른 UnityEngine.Object 들에 대한 참조를 가질 수 있습니다. 이 다른 오브젝트들은 같은 애셋 파일에 있을 수도 있고 다른 애셋 파일에 있을 수도 있습니다. 예를 들어 머티리얼 오브젝트는 항상 하나 이상의 텍스쳐 오브젝트에 대한 참조를 가집니다. 이 텍스쳐 오브젝트들은 일반적으로 ( PNG 나 JPG 같은 ) 하나 이상의 텍스쳐 애셋 파일들로부터 임포트( import )됩니다.


직렬화 시에, 이 참조들은 두 개의 개별 데이터 조각으로 구성됩니다: File GUIDLocal ID.  파일 GUID 는 대상 리소스가 저장되어 있는 애셋 파일을 식별합니다. 지역적으로 유일한 로컬 ID 는 애셋 파일 내의 각 오브젝트들을 식별합니다. 왜냐하면 애셋 파일은 여러 개의 오브젝트들을 포함할 수 있기 때문입니다.


파일 GUID 는 .meta 파일에 저장됩니다. 이 .meta 파일들은 애셋이 처음 임포트될 때 생성되며 애셋과 같은 디렉토리에 저장됩니다.


위의 식별자와 참조 시스템은 텍스트 에디터에서 확인할 수 있습니다: 새 유니티 프로젝트를 생성하고 그것의 에디터 셋팅을 Visible Meta Files 를 노출하고 애셋을 텍스트로 직렬화하도록 합니다. 그 프로젝트에서 머티리얼을 생성하고 텍스쳐를 임포트합니다. 그리고 씬에 있는 큐브에 머티리얼을 할당하고 씬을 저장합니다.


텍스트 에디터를 사용해, 위의 머티리얼과 연관된 .meta 파일을 엽니다. "guid" 라 표시된 라인은 파일의 거의 최상단에 보일 것입니다. 이 라인은 머티리얼 애셋의 파일 GUID 를 정의합니다. 로컬 ID 를 찾으려면, 텍스트 에디터에서 머티리얼 파일을 엽니다. 머티리얼 오브젝트의 정의는 다음과 같이 보일 것입니다:


--- !u!21 &2100000

Material:

 serializedVersion: 3

 ... more data ...


위의 예제에서, & 뒤에 나오는 숫자가 머티리얼의 로컬 ID 입니다. 만약 머티리얼 오브젝트가 파일 GUID "abcdefg" 에 의해서 식별되는 애셋 내부에 존재하면, 그 머티리얼 오브젝트는 파일 GUID "abcedfg" 와 로컬 ID "2100000" 의 조합에 의해 유일하게 식별될 것입니다.


1.3. Why file GUIDs and local IDs?



왜 유니티에서는 파일 GUID 시스템과 로컬 ID 시스템이 필요할까요? 그 대답은 신뢰성( robustness ) 및 유연하고 플랫폼 독립적인 워크플로우를 제공하기 위함입니다.


파일 GUID 는 파일의 특정 위치에 대한 추상화를 제공합니다. 특정 파일 GUID 가 특정 파일과 연관되어 있는 한, 그 파일의 디스크 상의 위치는 중요하지 않습니다. 그 파일은 파일을 참조하는 모든 오브젝트들을 갱신하지 않고도 자유롭게 이동될 수 있습니다.


애셋 파일은 다수 개의 UnityEngine.Object 리소스들을 포함할 수 있기 때문에( 혹은 임포트를 통해 생성할 수 있기 때문에 ), 로컬 ID 는 다른 오브젝트들과 명확하게 구분될 수 있을 것을 요구받습니다.


만약 애셋 파일과 연관된 파일 GUID 가 사라진다면, 그 애셋 파일 내부의 모든 오브젝트에 대한 참조도 사라질 것입니다. 이것이 .meta 파일이 그것이 연관된 애셋 파일과 같은 파일 이름을 가지고 같은 폴더에 저장되어야만 하는 이유입니다. 유니티는 제거되거나 위치가 잘못된 .meta 파일들을 자동으로 재생성한다는 점에 주의하십시오.


유니티 에디터는 알려진 파일 GUID 와 파일 경로에 대한 맵을 가지고 있습니다. 맵 엔트리는 애셋이 로드되거나 임포트될 때마다 기록됩니다. 맵 엔트리는 애셋의 특정 경로를 애셋의 파일 GUID 에 링크합니다. 만약 유니티 에디터가 .meta 파일이 사라졌지만 애셋의 경로가 변경되지 않았을 때 열리면, 에디터는 애셋이 같은 파일 GUID 를 유지하도록 해 줍니다.


만약 .meta 파일이 유니티 에디터가 닫히고 있을 때 사라지거나 .meta 파일은 그대로 두고 애셋 파일만 옮기면, 그 애셋 안의 모든 오브젝트에 대한 참조가 깨집니다.


1.4. Composite Assets and Importers


[ Inside Assets and Objects ] 에서 언급했듯이, non-native 애셋 타입들은 유니티에 임포트되어야만 합니다. 이는 애셋 임포터를 통해서 수행됩니다. 이 임포터들은 보통 자동으로 실행되지만, 그것들은 AssetImporter API 와 그것의 서브클래스를 통해 스크립트에 노출될 수도 있습니다. 예를 들어 TextureImporter API 는 PNG 나 JPG 와 같은 개별 텍스쳐 애셋을 임포트할 때 사용할 설정들에 대한 접근을 제공합니다.


임포트 절차의 결과는 하나 이상의 UnityEngine.Object 입니다. 이것들은 유니티 에디터에서 부모 애셋 내부의 다중 서브 애셋 형태로 가시화됩니다. 예를 들어 스프라이트 아틀라스로서 임포트된 텍스쳐 애셋 내부에 포함된 다중 스프라이트들이 있습니다. 각 오브젝트들은 같은 애셋 파일 내에 저장된 소스 데이터와 같은 파일 GUID 를 공유할 것입니다. 그것들은 임포트된 텍스쳐 애셋 내에서 로컬 ID 를 통해 구분될 것입니다.


임포트 절차는 소스 애셋을 유니티 에디터에서 선택한 대상 플랫폼에 맞는 포맷들로 변환합니다. 임포트 절차는 텍스쳐 압축과 같은 무거운 연산을 포함할 수 있습니다. 유니티 에디터가 열릴 때마다 임포트 절차가 수행되는 것은 매우 비효율적입니다.


대신에, 애셋 임포팅의 결과들은 Library 폴더로 캐싱됩니다. 특히, 임포트 절차의 결과는 폴더에 저장되는데, 폴더의 이름은 애셋 파일 GUID 의 처음 두 숫자입니다. 이 폴더는 LIbrary/metadata/ 폴더 내부에 저장됩니다. 개별 오브젝트들은 하나의 바이너리 파일로 직렬화되는데, 그것은 애셋 파일 GUID 와 동일한 이름을 가지게 됩니다.


이는 실제적으로 non-native 애셋들뿐만 아니라 모든 애셋에 대해 사실입니다. 그러나 native 애셋들은 장황한 변환 절차나 재직렬화를 요구하지 않습니다.


1.5. Serialization and Instances


파일 GUID 와 로컬 ID 가 신뢰성있기는 하지만, GUID 비교는 느리며 실시간에 좀 더 성능이 좋은 시스템이 필요합니다. 유니티는 내부적으로 파일 GUID 와 로컬 ID 를 단순한 정수로 변환하는 캐시를 유지합니다. 이 캐시는 싱글 세션에서만 유일합니다. 이들은 인스턴스 ID 라 불립니다. 그리고 새로운 오브젝트가 캐쉬에 등록될 때 단순한 점진적으로 증가하는 순서로 할당됩니다.


그 캐쉬는 주어진 인스턴스 ID( 오브젝트의 소스 데이터의 위치를 정의하는 파일 GUID 와 로컬 ID ) 와 메모리 상의 오브젝트 인스턴스 사이의 매핑을 유지합니다. 이는 UnityEngine.Object 들이 서로에 대한 참조를 신뢰성있게 유지할 수 있도록 해 줍니다. 인스턴스 ID 참조에 대한 resolving 은 인스턴스 ID 에 의해 표현되는 로드된 오브젝트를 빠르게 반환할 수 있습니다. 만약 대상 오브젝트가 아직 로드되지 않았다면, 파일 GUID 와 로컬 ID 는 오브젝트의 소스 데이터에 대해 resolve 될 수 있으며, 그 후에 유니티는 오브젝트를 제때( just-in-time ) 로드할 수 있습니다.


시작시에, 프로젝트에 내장된 모든 오브젝트( 예를 들어 씬에서 참조되는 )와 Resources 폴더에 포함된 모든 오브젝트를 위한 데이터를 사용해 인스턴스 ID 캐시가 초기화됩니다. 런타임에 새로운 애셋이 임포트되거나 애셋 번들로부터 오브젝트가 로드될 때 새로운 엔트리가 캐쉬에 추가됩니다. 인스턴스 ID 엔트리는 오브젝트가 무효화될( stale ) 때만 캐쉬에서 제거될 것입니다. 이는 특정 파일 GUID 와 로컬 ID 에 대한 애셋번들이 언로드될 때 발생합니다.


애셋 번들을 언로딩이 인스턴스 ID 를 무효화할 때, 메모리 보존을 위해 인스턴스 ID 와 그것의 파일 GUID 및 로컬 ID 사이의 매핑이 제거됩니다. 만약 애셋 번들이 다시 로드되면, 새로운 인스턴스 ID 가 다시 로드된 애셋 번들로부터 로드된 오브젝트를 위해서 생성될 것입니다.


애셋 번들을 언로드하는 것의 영향에 대한 더 깊은 논의를 원한다면, AssetBundle Usage Patterns 기사의 Managing Loaded Assets 섹션을 참조하십시오.


특정 플랫폼에서의 특정 이벤트는 오브젝트를 out of memory 상태로 이끌 수 있습니다. 예를 들어 앱이 중단되면( suspended ), iOS 상의 그래픽스 메모리로부터 그래피컬 애셋들이 언로드될 수 있습니다. 만약 이 오브젝트들이 언로드된 애셋 번들로부터 만들어진 것이라면, 유니티는 그 오브젝트를 위해 소스 데이터를 다시 로드하는 것이 불가능할 것입니다. 이러한 오브젝트들에 대한 현존하는 참조들도 무효화될 것입니다. 앞의 예에서, 안 보이는( 없어진 ) 메쉬나 마젠타( 없어진 ) 텍스쳐 & 머티리얼을 가진 것처럼 오브젝트가 렌더링 될 것입니다.


구현 노트 : 런타임에 위의 컨트롤 플로우는 문자 그대로 정확한 것은 아닙니다. 파일 GUID 와 Local ID 를 런타임에 비교하는 것은 무거운 로딩 연산 동안에는 충분히 효율적이지 못할 것입니다. 유니티 프로젝트에서 빌드를 할 때, 파일 GUID 와 Local ID 는 결정론적으로( deterministically ) 더 단순한 포맷으로 매핑됩니다. 그러나 그 개념은 동일하게 남아 있으며, 파일 GUID 와 로컬 ID 항에 대한 생각은 런타임 동안에 유용한 비유( analogy )로 남아 있게 됩니다.


이것은 애셋 파일 GUID 가 런타임에 질의될( queried ) 수 없는 이유이기도 합니다.


1.6. MonoScript


MonoBehaviour 가 MonoScript 에 대한 참조를 가지고 있고, MonoScript 는 단순히 특정 스크립트 클래스를 배치하기 위해서 필요한 정보만을 유지한다는 것을 이해하는 것은 중요합니다. 어떠한 유형의 오브젝트라도 스크립트 클래스의 실행 코드를 포함하지는 않습니다.


MonoScript 는 세 가지 문자열을 포함합니다: 어셈블리 이름, 클래스 이름, 네임스페이스.


프로젝트를 빌드할 때, 유니티는 모든 loose script file 을 Assets 폴더에서 수집해서 그것들을 모노 어셈블리로 컴파일합니다. 특히, 유니티는 Assets 폴더 내에서 사용된 각각의 다른 언어들을 위한 어셈블리를 빌드합니다. 또한 Assets/Plugins 폴더에 포함된 스크립트를 위한 독립된 어셈블리를 빌드합니다. Plugins 서브 폴더 외부의 C# 스크립트들은 Assembly-CSharp.dll 에 배치됩니다. Plugins 서브 폴더 내부의 스크립트들은 Assembly-CSharp-firstPass.dll 에 배치되는 식입니다.


( 미리 빌드된 어셈블리 DLL 들과 함께 ) 이 어셈블리들은 유니티 애플리케이션의 최종 빌드에 포함됩니다. 그것들은 MonoScript 가 참조하는 어셈블리이기도 합니다. 다른 리소스들과는 다르게, 유니티 애플리케이션에 포함된 모든 어셈블리들은 애플리케이션이 처음 시작될 때 로드됩니다.


이 MonoScript 오브젝트는 애셋번들( 혹은 씬 혹은 프리팹 )이 내부에 있는 MonoBehaviour 컴포넌트들 내의 어떠한 실행 코드도 포함하고 있지 않은 이유입니다. 이는 서로 다른 MonoBehaviour 들이 특정 공유 클래스나 심지어는 다른 애셋 번들에 존재하는 MonoBehaviour 들을 참조할 수 있도록 해 줍니다.


1.7. Resource LifeCycle


UnityEngine.Object 들은 특정 시점에 메모리에 로드되거나 메모리로부터 언로드됩니다. 로딩 시간을 줄이고 애플리케이션의 메모리 사용량을 줄이기 위해서는, UnityEngine.Object 들의 리소스 생명 주기에 대해서 이해하는 것이 중요합니다.


UnityEngine.Object 를 로드하는 두 가지 방법이 있습니다: 오브젝트는 자동으로 혹은 명시적으로. 역참조되고( dereferenced ) 있으며 현재 메모리에 로드되지 않았으며 오브젝트 소스 데이터가 정확한 위치에 있는 오브젝트에 인스턴스 ID 가 매핑되었을 때 자동으로 로드됩니다. 오브젝트는 그것들을 생성하거나 리소스 로딩 API( 예를 들어 AssetBundle.LoadAsset ) 를 호출함을써 스크립트를 통해 명시적으로 로드될 수 있습니다.


오브젝트가 로드될 때, 유니티는 각 참조의 파일 GUID 와 로컬 ID 를 인스턴스 ID 로 변환함으로써 모든 참조를 resolve 하려 시도합니다.


두 가지 조건만 만족한다면, 오브젝트는 자신의 인스턴스 ID 가 처음으로 역참조될 때 요구에 의해( on-demand ) 로드될 것입니다:


    1. 인스턴스 ID 가 현재 로드되지 않은 오브젝트를 참조할 때.
    2. 인스턴스 ID 가 캐시에 등록된 올바른 파일 GUID 와 로컬 ID 를 가질 때.


이는 일반적으로 참조가 스스로 로드되고 resolve 된 후에 매우 짧은 시간에 발생합니다.


만약 파일 GUID 와 로컬 ID 가 인스턴스 ID 를 가지고 있지 않거나, 언로드된 오브젝트를 가진 인스턴스 ID 가 유효하지 않은 파일 GUID 와 로컬ID 를 참조한다면, 그 참조는 보존되고 실제 오브젝트는 로드되지 않을 것입니다. 이는 유니티에서 "(Missing)" 참조라고 나옵니다. 실행중인 애플리케이션이나 씬뷰에서는 "(Missing)" 참조가 그것들의 유형에 따라 다른 방식으로 가시화될 것입니다: 메쉬들은 비가시화 상태가 되고, 텍스쳐들은 마젠타로 나타나는 식입니다.


오브젝트들은 세 가지 특정 시나리오에서 언로드됩니다:


    1. 오브젝트들은 사용되지 않는 애셋들을 클린업할 때 자동으로 언로드됩니다. 이 절차는 씬들이 파괴될 때( Application.LoadLevel API 를 non-additive 방식으로 호출할 때 )나 스크립트에서 Resources.UnloadUnusedAssets API 를 호출할 때 자동으로 발동합니다. 이 절차는 참조가 없는( unreferenced ) 오브젝트들만을 언로드합니다: Mono 변수가 그 오브젝트에 대한 참조를 가지고 있지 않거나 그 오브젝트에 대한 참조를 가진 살아 있는 오브젝트가 하나도 없을 때만 오브젝트가 언로드될 것입니다.
    2. Resources 폴더로부터 생성된 오브젝트들은 Resources.UnloadAsset API 를 호출함으로써 명시적으로 언로드될 수 있습니다. 이러한 오브젝트들의 인스턴스 ID 는 유효한 상태로 유지되며, 여전히 유효한 파일 GUID 와 로컬 ID 엔트리를 포함할 것입니다. 어떤 Mono 변수나 다른 오브젝트가 Resources.UnloadAsset 을 사용해 언로드된 오브젝트에 대한 참조를 가지고 있다면, 그 오브젝트는 살아 있는 참조가 역참조되자 마자 다시 로드될 것입니다.
    3. 애셋번들로부터 생성된 오브젝트들은 AssetBundle.Unload(true) API 를 호출할 때 자동으로 즉시 언로드됩니다. 이는 오븢게트의 InstanceID 의 파일 GUID 와 로컬 ID 참조를 무효화하며, 언로드된 오브젝트에 대한 살아 있는 모든 참조들은 "(Missing)" 참조가 될 것입니다. C# 스크립트에서, 언로드된 오브젝트에 대한 메서드나 프라퍼티에 접근하는 것은 NullReferenceException 을 생성하게 될 것입니다.


AssetBundle.Unload(false) 가 호출되면, 언로드된 애셋번들로부터 생성된 살아 있는 오브젝트들은 파괴되지 않을 것입니다. 하지만 유니티는 그것들의 인스턴스 ID 의 파일 GUID 와 로컬 ID 참조를 무효화시킬 것입니다. 만약 나중에 오브젝트가 메모리에서 언로드되고 언로드된 오브젝트에 대한 살아 있는 참조들이 유지되고 있다면, 이 오브젝트들을 유니티가 다시 로드하는 것은 불가능할 것입니다.


1.8. Loading Large Hierarchies


유니티 GameObject 의 계층을 직렬화할 때( 예를 들어 프리팹을 직렬화할 때 ), 전체 계층이 완전히 직렬화된다는 것을 기억하는 것이 중요합니다. 즉, 계층 내부의 모든 GameObject 와 Component 들이 개별적으로 직렬화된 데이터 내에 표현된다는 것입니다. 이는 GameObject 의 계층을 로드하고 인스턴스화하는데 필요한 시간에 흥미로운 영향을 미칩니다.


GameObject 계층을 생성할 때, CPU 시간이 몇 가지 방식으로 소비됩니다:


    1. 소스 데이터를 읽는 시간( 저장소로부터, 다른 GameObject 로부터 등 ).
    2. 새로운 Transform 사이에서 부모 자식 관계를 설정하는 시간.
    3. GameObject 와 Component 를 인스턴스화하는 시간.
    4. GameObject 와 Component 를 깨우는( awaken ) 시간.


나머지 세 비용은 일반적으로 계층이 현존하는 계층으로부터 복사되느냐 저장소( 예를 들어 애셋번들 )에서 로드되느냐와 관계없이 일정합니다. 그러나 소스 데이터를 읽는 비용은 계층에 직렬화된 Component 및 GameObject 의 개수에 따라 선형적으로 증가하며, 이는 데이터 소스의 속도와 곱해집니다.


현재 모든 플랫폼에서, 저장소 디바이스에서 로드하는 것보다는 메모리 어딘가에서 데이터를 읽어 들이는 것이 훨씬 빠릅니다. 더우기, 이용가능한 저장소 매체의 성능 특성은 각 플랫폼에서 매우 다양합니다 -- 데스크탑 PC 는 모바일 디바이스보다 훨씬 빠르게 디스크에서 로드합니다.


그러므로 느린 저장소를 가진 플랫폼에서 프리팹을 로드할 때, 저장소로부터 프리팹의 직렬화된 데이터를 읽는 비용은 프리팹을 인스턴스화하는 비용을 급격하게 넘어 갈 수 있습니다. 즉, 로딩 연산의 비용은 스토리지 I/O 시간에 의해 결정됩니다.


이전에 언급했듯이, monolithic prefab 을 직렬화할 때, 각 GameObject 와 Component 의 데이터는 개별적으로 직렬화됩니다 - 심지어 데이터가 중복되어 있더라도 그렇습니다. 30 개의 동일한 요소를 가진 UI 스크린은 동일한 요소를 30 번 직렬화할 것입니다. 이는 많은 바이너리 데이터 덩어리를 생성합니다. At load time, the data for all of the GameObjects and Components on each one of those thirty duplicate elements must be read from disk before being transferred to the newly-instantiated Object. 이는 파일 읽기 시간이며, 큰 프리팹을 인스턴싱하는 비용의 대부분을 차지합니다.


유니티가 내포된 프리팹( nested prefabs )을 지원하기 전까지는, 극단적으로 큰 GameObject 계층을 인스턴스화하는 프로젝트들은유니티의 직렬화 및 프리팹 시스템에 완전히 의존하기보다는,  재사용되는 요소들을 개별 프리팹으로 쪼개고 런타임에 그것을 인스턴스화함으로써 그들의 큰 프리팹의 로딩 비용을 많이 감소시킬 수 있었습니다.


더우기, 프리팹이나 GameObject 계층이 일단 생성되면, 새로운 복사본을 저장소에서 로드하는 것보다 현존하는 계층을 복사하는 것이 더 빠릅니다.


유니티 5.4 노트: 유니티 5.4 는 메모리에서 transform 에 대한 표현을 수정했습니다. 각 root transform 의 전체 자식 계층은 작고 연속적인 메모리 영역에 저장됩니다. 다른 계층으로 reparent 되는 새로운 GameObject 를 인스턴스화할 때는, parent 인자를 허용하는 GameObject.Instantiate 오우버로드( overloads ) 메서드를 사용하는 것을 골려하십시오.


이 오우버로드 메서드를 사용하면 새로운 GameObject 를 위한 root transform 계층을 할당하는 것을 피할 수 있습니다. 테스트를 해 봤을 때, 이것은 인스턴스화 연산을 위해 요구되는 시간을 5 ~ 10 % 정도 빠르게 만들어 줬습니다.


-- 각주 생략

개요



이 문서에서는 1인칭 캐릭터에서의 이동 및 애니메이션에 대해서 다룹니다.


1인칭 애니메이션 애셋은 "Content/RestrictedAssets/Animations/Universal/1stPerson/UT4_Base_1stP_AnimBP.uasset" 입니다. 이 블루프린트 안에는 여러 가지 애니메이션 블렌딩 루틴들이 있는데요, 이 중에서 이동과 관련한 부분만 살펴 볼 것입니다.


1인칭 이동 애니메이션은 FSM( Finite State Machines )로 표현됩니다. 다음과 같은 순서로 이동 애니메이션을 분석해 볼 것입니다.


    • 먼저 FSM 이 어떤 식으로 구성되어 있는지를 알아 볼 것입니다.
    • 다음으로 FSM 에 값을 공급하기 위해서 이벤트 그래프가 어떻게 구성되는지 살펴 볼 것입니다.
    • 다음으로 각 State 에서 어떠한 애니메이션들이 블렌딩되는지를 살펴 볼 것입니다.
    • 마지막으로 각각의 애니메이션 애셋들이 어떻게 구성되어 있는지를 살펴 볼 것입니다.


Main_Motion FSM



1인칭 이동 애니메이션에서 핵심은 "Main_Motion" FSM 입니다. AnimGraph 를 보면, "Main_Motion_Cache" 라는 것을 만들기 위해서 "Main_Motion" FSM 을 사용합니다. "Main_Motion_Cache" 가 뭐냐라는 질문에 대한 답은 다른 문서에서 하도록 하겠습니다. 여기에서는 "Main_Motion" 에 대해서만 집중합시다.


그림1. Main_Motion_Cache.


이 "Main_Motion" 노드를 더블클릭하면, 아래와 같은 FSM 을 볼 수 있습니다.



너무 그림도 작은데다가 Hierarchical FSM 이기 때문에 State 안쪽의 내용을 볼 수 없습니다. 게다가 Transition 조건도 마우스를 올리거나 클릭해야 나오기 때문에 한 눈에 볼 수 없습니다. 그래서 따로 UML State Diagram 을 준비했습니다.



Transition 까지 전부 표현하다 보니 그림이 좀 지저분한 감이 있긴 하지만, 보시는데 큰 문제는 없을 것입니다.


Movement 를 중심으로 Jump, Fall, Slide 라는 동작을 수행하게 됩니다. 물론 각각은 여러 개의 state 로 나뉘어 있거나 sub-state 를 가지게 됩니다.


 State

Animation & Desc

 Transition Desc

 Target State

 Movement

  • 이동 관련 하위 상태를 포함함.
    • Idle.
    • Start_Run.
    • Run.
    • Stop_Run.
    • Change_Direction.
  • 11 번 transition.

 Falling

  • 18 번 transition.
 Slide

 Idle

  • 1stP_Idle_Ready_Rif( 대기 동작 ).
  • 대기 상태.
  • 1 번 transition.

 Start_Run

  • 5 번 transition.

 Run

 Start_Run

  • 1stP_Run_Fwd_Rif( 전방 달리기 동작 ).
  • BS_1stP_Run_Transition_Direction_Change_Rif( 방향에 따른 팔 블렌딩 ).
    • 1stP_Run_Transition_Fwd_to_Bwd_Rif.
    • 1stP_Run_Transition_Right_to_Left_Rif.
    • 1stP_Run_Transition_Bwd_to_Fwd_Rif.
    • 1stP_Run_Transition_Left_to_Right_Rif.
  • 달리기 시작함.
  • 원하는 이동방향에 따라서 팔을 흔드는 모양이 달라짐.
  • 2 번 transition.

 Run

  • 8 번 transition.

 Stop_Run

 Run

  • 1stP_Run_Fwd_Rif( 전방 달리기 동작 ).
  • 앞으로 달려감.
  • 3 번 transition.

 Stop_Run

  • 5 번 transition.

 Idle

  • 9 번 transition.

 Change_Direction

 Stop_Run

  • 1stP_Idle_Ready_Rif( 대기 동작 ).
  • BS_1stP_Run_Transition_Direction_Change_Rif( 방향에 따른 팔 블렌딩 ).
    • 1stP_Run_Transition_Fwd_to_Bwd_Rif.
    • 1stP_Run_Transition_Right_to_Left_Rif.
    • 1stP_Run_Transition_Bwd_to_Fwd_Rif.
    • 1stP_Run_Transition_Left_to_Right_Rif.
  • 멈추기 시작하거나 달리기 시작함.
  • 원하는 이동방향에 따라서 팔을 흔드는 모양이 달라짐.
  • 4 번 transition.

 Idle

  • 7 번 transition.
 Start_Run

 Change_Direction

  • BS_1stP_Run_Transition_Direction_Change_Rif( 방향에 따른 팔 블렌딩 ).
    • 1stP_Run_Transition_Fwd_to_Bwd_Rif.
    • 1stP_Run_Transition_Right_to_Left_Rif.
    • 1stP_Run_Transition_Bwd_to_Fwd_Rif.
    • 1stP_Run_Transition_Left_to_Right_Rif.
  • 원하는 이동방향에 따라서 팔을 흔드는 모양이 달라짐.
  • 10 번 transition.

 Run

 Falling
  • 점프 관련 하위 상태를 포함함.
  • Jump_Start.
    • 1stP_Jump_Start_Rif( 점프 시작 동작 ).
  • Jump_Loop.
    • 1stP_Jump_Loop_Rif( 점프 활강 동작 ).
  • Jump_Start 애니 끝나면 바로 Jump_Loop 로 전이됨.
  • 12 번 transition.

 Jump_Land

  • 14 번 transition.

 Slide

 Jump_Land
  • 1stP_Jump_Land_Rif( 착지 동작 ).
  • 바닥에 착지함. 
  • 13 번 transition.
 Movement
  • 15 번 transition.
 Slide

 Slide

  • 1stP_KneeSlide_Fwd_Rif( 미끌어지는 동작 ).
  • 경사면에서 미끌어짐.
  • 왼손의 총에 대한 IK 가 해제됨.
  • 16 번 transition.

 Slide_End

  • 19 번 transition.
 Movement
 Slide_End
  • 1stP_KneeSlide_Fwd_End_Rif( 미끌어지는 동작 종료 ).
  • 왼손의 총에 대한 IK 가 복구됨.
  • 17 번 transition.

 Movement

 

개요



[ 5.3. 플레이어 입력 ] 에서 어떤 플레이어 입력이 있는지 살펴 보았습니다. 이 섹션에서는 이동과 관련한 로직 및 애니메이션에 대해서 살펴 보도록 하겠습니다.


이동( locomotion, 보행, 운동 )과 관련된 로직에 대해서 다루도록 하겠습니다. 간단하게 요약하자면 UT 의 이동 로직은 UUTCharacterMovment 컴포넌트에 의존합니다. Character component 의 처리 결과에 따라서 어떠한 애니메이션들을 블렌딩할지를 결정하게 됩니다.


1인칭 캐릭터에서는 "Content/RestrictedAssets/Universal/1stPerson/UT4_Base_1stP_AnimBP.uasset" "Main_Motion" FSM( finite state machines ) 을 사용하며, 3인칭 캐릭터에서는 "Content/RestrictedAssets/Animations/Universal/UT4_Base_AnimBP.uasset""Locomotion" FSM 과 "Landing" FSM 을 사용합니다.


일단 Character Movement 컴포넌트의 주요 역할은 물리와 애니메이션에서 사용할 자료( 변수 )를 준비하는 것입니다. 만약 Character Movement 컴포넌트가 없고 Player Controller 만 존재한다면, Player Controller 나 Character 에서 물리 및 애니메이션을 처리해야만 할 것입니다.


Character Movement 컴포넌트



UT 에서 기본 Character Movement 컴포넌트의 클래스는 UUTCharacterMovement 입니다. "DefaultCharacter" 블루 프린트를 열어서 "Components" 뷰를 보면 아래쪽에 "UTCharacterMovment(Inherit)" 라는 항목을 볼 수 있습니다. 오른쪽의 "Details" 뷰를 보면 매우 많은 속성 카테고리들이 존재함을 알 수 있습니다.



보시면 알겠지만 게임에서 사용할 만한 상황들은 거의 다 들어 가 있습니다. 물론 상황에 맞춰 FSM 을 구성하는 수고는 해야 합니다.


이동 모드 



Character Movement 컴포넌트에서 이동 모드의 전환은 FSM 으로 표현될 수 있습니다만 여기에서 다루지는 않겠습니다. 애니메이션 블루프린트마다 애니메이션 FSM 을 따로 구성하기 때문에 코드 상에서의 상태 전환에 대해서 언급하는 것은 크게 의미가 없을 것 같습니다. 애니메이션 FSM 에 대해서는 다른 문서에서 다루도록 하겠습니다. 단지 다음과 같은 이동 모드들이 있다는 것만 알아 두시면 될 것 같습니다.


    • Walking : 땅바닥에 붙어서 걸어다니는 이동 모드.
    • NavWalking : AI 등이 네비게이션 메쉬와 길찾기를 통해서 걸어다니는 모드.
    • Swimming : 물에 들어 갔을 때의 이동 모드.
    • Falling : 점프를 하거나 바닥에 닿지 않아 떨어지고 있을 때의 이동 모드.
    • Custom : 사용자의 커스텀한 이동 모드.
    • Flying : 날아다니는 이동 모드.


이제 각 이동 모드와 관련한 중요한 속성들을 살펴 보도록 하겠습니다. 대부분 어렵지 않은 속성들을 가지고 있기 때문에 이해하는데 큰 어려움은 없을 것입니다. 일단 어떤 기능들을 가지고 있는지 이해하고 있어야지 이미 존재하는 기능을 또 구현하는 불상사를 막을 수 있기 때문에, 지루하더라도 한 번씩은 읽어 보시기 바랍니다. 


제가 밥상은 차려줄 수 있지만, 떠 먹는건 알아서 하시기 바랍니다.


Character Movement




각 이동 모드의 가속도 및 감속도를 설정하는 곳입니다.


 이름

 설명

 MaxFallingAcceleration

  • Falling 시의 최대 가속도( 이는 AirControl 속성에 의해 스케일링 됨 ).

 MaxSwimmingAcceleration

  • Swimming 시의 최대 상수 가속도.

 MaxRelativeSwimmingAccelNumerator

  • Swimming 시의 부가적 가속도인데, 속도 크기에 의해 나눠짐. Swimming 가속도는 MaxSwimmingAcceleration + MaxRelativeSwimmingAccelNumerator / ( Speed + MaxRelativeSwimmingAccelDenominator ) 임.

 MaxRelativeSwimmingAccelDenomitor

  • Swimming 가속도 공식의 일부.  Swimming 가속도는 MaxSwimmingAcceleration + MaxRelativeSwimmingAccelNumerator / ( Speed + MaxRelativeSwimmingAccelDenominator ) 임.

 BrakingDecelerationSliding

  • Sliding 시의 정지 감속도.

 DefaultBrakingDecelerationWalking

  • Walking 시의 정지 감속도 - BrakingDecelerationWalking 과 같은 값으로 설정하라.

 IgnoreClientMovementErrorChecksAndCorrection

  • true 이면, 이 이동 컴포넌트 상에서 클라이언트 에러를 위한 서버 위치 차이 검사를 무시한다.
  • 이는 캐릭터가 잠시 동안 극단적인 속력으로 움직이는데 클라이언트에서 부드럽게 보이도록 만들고 싶을 때 유용하다. 사용하고 나면 비활성했는지 확인해야 한다. 왜냐하면 이것은 캐릭터의 서버-클라이언트 이동 보정을 오동작하게 만들기 때문이다.


Character Movement(General Settings)




일반적인 설정을 하는 곳입니다.


 이름

 설명

 GravityScale

  • 커스텀 중력 스케일. 캐릭터를 위한 중력에 이 값이 곱해진다.

 MaxAccelleration

  • 최대 가속도( 속도가 변하는 비율 ).

 BrakingFrictionFactor

  • 정지할 때 사용되는 실제 마찰력 값에 곱해지기 위한 요소이다.
  • 이는 현재 사용되는 모든 마찰력 값에 적용된다. UseSeperateBrakingFriction 에 의존한다.
  • @note : 이는 경험적 이유로 2 값이 기본값이다. 1 값은 실제 drag equation 이다.

 BrakingFriction

  • ( Acceleration = 0 이거나 캐릭터가 최대 속력을 초과하고 있을 때마다 ) 정지시에 적용될 마찰력 ( drag ) 상수; 실제 사용되는 값은 BrakingFrictionFactor 와 곱해진다.
  • 정지중일 때, 이 속성은 바닥을 통해 움직이고 있을 때 얼마나 많은 마찰력이 적용될지를 제어할 수 있도록 해 준다. 이는 현재 속도를 스케일링하는 반대쪽 힘을 적용하게 된다.
  • 정지는 마찰력( 속도 의존적인 drag )와 상수 감속( deceleration )으로 구성된다.
  • 이는 모든 이동 모드에서 사용되는 현재값이다; 만약 이것을 원하지 않는다면, 이동 모드가 바뀔 때 이 값이나 bUseSeperatedBrakingFriction 을 재정의하라.
  • bUseSerperatedBrakingFriction 이 true 일 때만 사용된다. 그렇지 않으면 GroundFriction 같은 현재 마찰력이 사용된다.

 UseSeperateBrakingFriction

  • 만약 true 이면, ( 가속이 없을 때 ) 캐릭터를 천천히 멈추게 하기 위해서 BrakingFriction 이 사용될 것이다.
  • 만약 false 이면, CalcVelocity() 에 넘겨지는 것과 같은 마찰력을 사용할 것이다( 예를 들어 걸어다닐 때의 GroundFriction ). 이는 BrakingFrictionFactor 와 곱해진다.
  • 이 설정은 모든 이동 모드에 적용된다; 만약 특정 모드에서 이를 원하지 않는다면, 이동 모드가 변경될 때 토글링하는 것을 고려해 보라.

 CrouchedHalfHeight

  • Crouching( 앉기 )시에 사용할 충돌 절반 높이( 컴포넌트 스케일이 각각 적용됨 ).

 RotationRate

  • 초당 회전률. UseControllerDesiredRotation 이나 OrientationToMovement 가 true 일 때 사용된다. Infinite rotation rate 와 instant trun 을 위해서는 음수값을 설정한다.

 OrientRotationToMovement

  • true 이면, 가속 방향을 향해 캐릭터를 회전시킨다. 이 때 회전률은 RotationRate 를 통해 지정된다. UsecontrollerDesiredRotation 을 덮어 쓴다.
  • 보통 캐릭터의 UseControllerRotationYaw 같은 다른 설정들이 클리어되어 있기를 기대할 것이다.

 Mass

  • 폰( pawn )의 질량( 운동량( momentum )이 주어졌을 때를 위해 ).
 DefaultLandMovementMode
  • 물 속에 있지 않을 때의 기본 이동 모드. 플레이어 스타트업이나 텔레포트시에 사용됨.
  • Walking
  • NavWalking
  • Falling,
  • Swimming
  • Flying
  • Custom
 DefaultWaterMovementMode
  • 물 속에 있을 때의 기본 이동 모드. 플레이어 스타트업이나 텔레포트시에 사용됨.

 JustTeleported

  • 위치 변화가 일반 이동에 의한 것인지 텔레포트에 의한 것인지 결정하기 위해서 이동 코드에서 사용됨. 만약 텔레포트가 아니면, 위치 변화 기반해 속도가 재계산될 수 있음.

 WantstoCrouch

  • true 이면, 다음 업데이트시에 crouch( 혹은 crouch 유지 )를 시도함. false 이면 다음 업데이트시에 crouching 종료를 시도함.
 UseControllerDesiredRotation
  • true 이면, 컨트롤러의 회전을 향해서 캐릭터를 부드럽게 회전시킨다. 이 때 회전률은 RotationRate 를 통해 지정된다. OrientRotationToMovement 에 의해 덮어 써 진다.
 EnableScopedMovementUpdates
  • If true, high-level movement updates will be wrapped in a movement scope that accumulates updates and defers a bulk of the work until the end.
  • When enbled, touch and hit events will not be triggered until the end of multiple moves within n update, which can improve performance.
  • 역주 : 이동을 누적시키는 것과 관련한 업데이트들을 한 번에 묶어서 처리하고, 나머지 업데이트들은 지연시켜서 나중에 처리하는 기능을 의미하는 듯하다.

 RunPhysicsWithNoController

  • true 이면, 캐릭터 소유자를 위한 컨트롤러가 존재하지 않더라도 이동을 실행할 것이다.
  • 일반적으로 컨트롤러가 없으면, 이동은 무시되며, 캐릭터가 걷고 있다면 속도와 가속도가 0 이 될 것이다.
  • 컨트롤러 없이 생성된 캐릭터에서 이 플래그가 활성화되면, 이동 모드를 DefultLandMovementMode 나 DefultWaterMovementMode 로 적절히 초기화할 것이다.

 MaxSimulationTimeStep

  • 각각의 분절된( discrete ) 시뮬레이션 스텝의 최대 타임 델타이다.
  • 주로 큰 타임 스텝을 쪼개는 진보된 이동 모드에서 사용된다( usually those applying gravity such as falling and walking ).
  • 이 값을 작게 하면, 빠르게 움직이는 오브젝트 시나리오나 복잡한 충돌 시나리오를 해결할 수 있는데, 성능 비용을 지불해야 한다.
  • @경고 : 만약 ( MaxSimulationTimeStep * MaxSimulationIterations ) 가 최소 프레임율보다 너무 작다면, 마지막 시뮬레이션 스텝은 시뮬레이션을 완료하기 위해서 MaxSimulationTimeStep 을 초과할 수도 있다.

 MaxSimulationIterations

  • 각각의 분절된 시뮬레이션 스텝들을 위해 사용되는 반복( iteration ) 횟수.
  • 주로 큰 타임 스텝을 쪼개는 진보된 이동 모드에서 사용된다( usually those applying gravity such as falling and walking ).
  • 이 값을 증가시키면, 빠르게 움직이는 오브젝트 시나리오나 복잡한 충돌 시나리오를 해결할 수 있는데, 성능 비용을 지불해야 한다.
 CrouchMaintainsBaseLocation
  • true 이면, crouching 은 쪼그라든 캡슐의 중심을 낮춤으로써 캡슐의 바닥을 그대로 유지한다. false 이면, 캡슐의 바닥이 위로 올라가고 중심은 그대로 남는다.
  • 같은 행위가 uncrouch 시에도 적용된다: true 이면 바닥은 같은 위치를 유지하고, 중심이 올라 간다. false 이면, 캡슐이 커지고 바닥이 무엇인가와 충돌할 때만 올라간다. 
  • 기본적으로 이 변수는 이동 모드가 변경될 때만 설정된다: walking 일 때 true 로 설정하고 그렇지 않으면 false 로 설정한다. 이동 모드가 변경될 때 이 행위를 재정의하는데 부담가질 필요가 없다.
 RequestedMoveUseAcceleration
  • path following 을 위해 가속도를 사용할지 여부를 결정한다.
  • true 이면, path following 시에 목표 속도에 도달하기 위해서 가속도를 적용한다.
  • false 이면, path following 속도가 직접 설정되며, 가속도를 고려하지 않는다.


CharacterMovement : Walking




걷기와 관련한 설정을 하는 곳입니다. 


 이름

 설명

 MaxStepHeight

  • 캐릭터가 걸어 올라 갈 수 있는 ( 계단의 ) 최대 높이. 

 WalkableFloorAngle

  • 걸어다닐 수 있는 표면의 최대 각도. 이보다 큰 각도이면 걷기에는 너무 뾰족한 것임. 

 WalkableFloorZ

  • 바닥의 법선( normal )에 대한 최소 z 값. 이것보다 크면 걸을 수 없음. WalkableFloorAngle 로부터 계산됨.

 GroundFriction

  • 이동 제어에 영향을 주는 설정. 값이 높을수록 방향이 빠르게 변함.
  • 만약 bUseSeperateBrakingFriction 이 false 라면,  ( Acceleration 이 0 일 때마다 ) 멈출 때 더 빠르게 정지하는 능력에 영향을 주기도 함. 이는 BrakingFrictionFactor 와 곱해짐.
  • 이 속성은 땅바닥 위를 이동하다가 정지할 때 마찰력이 얼마나 적용되어야 하는지를 사용자가 제어할 수 있도록 하는 속성임. 이는 현재 속도를 스케일링하는 반대방향의 힘임.
  • 이 값을 변경함으로써 이는 눈이나 기름과 같은 미끄러운 표면을 시뮬레이션하기 위해서 사용될 수도 있음( 아마도 폰이 서 있는 재질에 기반해서 설정해야 할 것임 ).

 MaxWalkSpeed

  •  걸어다닐 때의 최대 속력. 떨어지고 있을 때의 최대 측면( lateral ) 속도를 결정하기도 함.

 MaxWalkSpeedCrouched

  •  앉아서 걸어다닐 때의 최대 속력.

 BrakingDecelerationWalking

  • 가속도 없이 걸어 다닐 대의 감속도. 이는 상수값에 의해서 속도를 직접적으로 감소시키는 지속적인 반대방향의 힘이다.

 CanWalkOffLedges

  • true 이면 캐릭터가 절벽 가장자리( ledge )에서 걸어서 떨어질 수 있음.

 CanWalkOffLedgesWhenCrouching

  • true 이면 캐릭터가 앉아서 걸어다닐 때 절벽에서 걸어서 떨어질 수 있음.

 CurrentFloor

  • 캐릭터가 서 있는 바닥에 대한 정보( 걷기 이동시에만 갱신됨 ).

 MaintainHorizontalGroundVelocity

  • true 이면, 걷기 이동시에 경사로를 올라 가더라도 항상 수평 속도를 유지하는데, 이는 경사로가 아닌 평면에서 이동했을 때보다 더 빠르게 이동하게 만든다. 
  • false 이면, 경사로가 아닌 평면에서 이동했을 때와 같은 속도로 이동하게 된다.

 IgnoreBaseRotation

  • 캐릭터가 그것이 서 있는 바닥의 회전을 무시할지 여부.
  • true 이면, 캐릭터가 현재 world 회전값을 유지한다.
  • false 이면, 캐릭터가 움직이는 바닥의 회전값만큼 회전한다.

 PerchRadiusThreshold

  • 캐릭터의 캡슐의 가장자리와 표면의 가장자리가 가까울 때 캐릭터가 표면의 가장자리에 걸쳐 있지 못하게 하는 문턱값.
  • 걸어다닐 수 있는 표면보다 낮은 값의 MaxStepHeight 를 가지는 캐릭터는 떨어지지 않는다는 것에 주의할 것.

 PerchAdditionalHeight

  • 절벽 가장자리에 걸쳐 있을 때, MaxStepHeight 에 이 값을 더해서 걸어다닐 수 있는 바닥에서 얼마나 떨어져 있는지를 검사한다.
  • 걸어 올라가도록 하기 위해서 MaxStepHeight 강제로 바꿈; 이는 캐릭터를 가장자리에서 떨어지게 하거나 바닥에서 약간 높게 떠서 올라가도록 함.

 ForceNextFloorCheck

  • 캐릭터가 걷기 이동중 상태일 때 그것이 실제로 이동하지 못했더라도 강제로 유효한 바닥을 검사하도록 함. 다음 바닥 검사 이후에 클리어됨.
  • 보통 AlwaysCheckFloor 가 false 일 때는 특정 상황이 발생하지 않는다면 바닥 검사를 피하려고 시도할 필요가 없다. 하지만 이는 다음 검사를 항상 실행하도록 강제하기 위해서 사용됨.

 LedgeCheckThreshold

  • 폰이 절벽 가장자리로 가서 떨어지는지 여부를 검사하기 위해서 사용됨. 만약 이 값보다 가장자리의 길이가 짧다면 폰은 절벽에서 떨어질 수 있다.

 AlwaysCheckFloor

  • 캐릭터가 걷고 있는 동안에 stationary character 를 위한 바닥 검사를 항상 강제할 것인지 여부.
  • 보통 움직이고 있지 않을 때는 바닥 검사를 피하는 것이 좋다. 하지만 ( 오브젝트들이 캐릭터 위로 타고 올라 가는 상황과 같이 ) 깡총깡총 뛰다가 잘못되는 상황이 존재한다면 바닥 검사를 강제하기 위해서 사용될 수 있다.

 UseFlatBaseforFloorChecks

  • 캐릭터가 평평한 바닥을 가진 도형을 사용하고 있는 것인양 바닥 검사를 수행함.
  • 이는 캐릭터가 절벽 가장자리에서 ( 캡슐이 가장자리에서 균형을 이루는 것처럼 ) 천천히 떨어지는 상황을 방지한다.


CharacterMovement : Jumping / Falling



캐릭터가 점프하거나 떨어지고 있는 상황을 제어하는 곳입니다.



 이름

 설명

 JumpZVelocity

  • 점프할 때 초기 속도( 즉각적인 수직 가속도 ).

 BrakingDecelerationFalling

  • 떨어질 때 측면 감속도. 가속도에 영향을 주지 않음. 

 AirControl

  • 떨어질 때, 캐릭터에 대해 가능한 측면 이동 제어의 양.
  • 0 = 제어 없음. 1 = MaxWalkSpeed 의 최대 값에서 완전한 제어.

 AirControlBoostMultiplier

  • 떨어질 때, AircontrolBootVelocityThreshold 보다 작은 측면 속도일 때 Aircontrol 에 곱해질 값. 
  • 이 값을 0 으로 설정하면 air control boosting 이 비활성화됨. 최종 결과는 1 로 잘림.

 AirControlBoostVelocityThreshold

  • 떨어질 때, 측면 속도의 크기가 이 값보다 작으면, AirControl 에 AirControlBoostMultiplier 가 곱해짐.
  • 이 값을 0 으로 설정하면 air control boosting 이 비활성화됨.

 FallingLateralFriction

  • 떨어질 때, 측면 공중 이동에 적용될 마찰력.
  • bUseSeperateBrakingFriction 이 false 이면, ( 가속도가 0 일 때마다 ) 멈출 때 더욱 빠르게 멈추는 능력에 영향을 줌.

 ImpartBaseVelocityX

  • true 이면, ( 점프를 포함해 ) 떨어지는 것이 끝났을 때 기저 액터의 X 축 속도에 전달됨. 

 ImpartBaseVelocityY

  • true 이면, ( 점프를 포함해 ) 떨어지는 것이 끝났을 때 기저 액터의 Y 축 속도에 전달됨.

 ImpartBaseVelocityZ

  • true 이면, ( 점프를 포함해 ) 떨어지는 것이 끝났을 때 기저 액터의 Z 축 속도에 전달됨.

 ImpartBaseAngularVelocity

  • true 이면, 점프나 떨어지는 것이 끝났을 때 기저 컴포넌트의 각속도의 접선( tangential ) 성분에 전달됨.

 NotifyApex

  • true 이면, 꼭대기( apex )에서 점프할 때 CharacterOwner 의 컨트롤러에 NotifyJumpApex() 이벤트를 전달함. 이는 이벤트가 트리거되면 클리어됨.

 JumpOffJumpZFactor

  • 캐릭터 밑에 있도록 허용되지 않은 액터로 점프해서 올라갔을 때 사용될 JumpZVelocity 의 분수값( 예를 들어, 여러분이 다른 플레이어의 위에 서 있도록 허용되지 않았다면 ).


Character Movement : Swimming



캐릭터가 물에 빠졌을 때의 이동을 제어하는 곳입니다.



 이름

 설명

 MaxSwimSpeed

  • 최대 수영 속력.

 BrakingDecelerationSwimming

  • 수영시의 감속도이며 가속도에는 영향을 주지 않음.

 Buoyancy

  • 물의 부력. 비율( 1.0 = 자연스런 부력, 0.0 = 부력 없음 ).

 MaxOutOfWaterStepHeight

  • 물에서 나가기 위한 최대 높이.

 OutofWaterZ

  • 폰이 물에서 나가려 할 때 적용되는 Z 축 속도.
 JumpOutofWaterPitch
  • 물 속에 있을 때, 이 값 이상의 pitch 각을 올리면 jump 함.


Character Movement : Flying



날아 다닐 때의 이동을 제어하는 곳입니다.



 이름

 설명

 MaxFlySpeed

  • 최대 비행 속력.

 BrakingDecelerationFlying

  • 비행시의 감속도이며 가속도에는 영향을 주지 않음.


Character Movement : Custom Movement



사용자가 커스텀하게 설정하는 이동을 제어하는 곳입니다.


 이름

 설명

 MaxCustomMovementSpeed

 최대 속력.


Character Movement : Physics Interaction


캐릭터 이동시 물리의 영향을 어떻게 받을 것인지 설정하는 곳입니다.



 이름

 설명

 EnablePhysicsInteraction

  • 만약 활성화되면, 걸어다닐 때 플레이어는 물리 개체들과 상호작용함.

 TouchForceScaledtoMass

  • 만약 활성화되면, TouchForceFactor 가 영향받는 오브젝트의 kg 질량마다 적용됨.

 PushForceScaledtoMass

  • 만약 활성화되면, PushForceFactor 가 영향받는 오브젝트의 kg 질량마다 적용됨.

 PushForceUsingZOffset

  • 만약 활성화되면, PushForce 위치가 PushForcePointZOffsetFactor 를 사용해 이동됨. 그렇지 않으면 단순하게 충돌 지점을 사용함.

 ScalePushForcetoVelocity

  • 만약 활성화되면, 적용된 push force 는 물리개체의 속도를 플레이어의 속도와 같게 만들려고 시도함. 이는 힘을 감소시킬 것이며, PushForceFactor 에 의해 정의된 것보다 더 많은 힘을 적용하지는 않을 것이다.

 StandingDownwardForceScale

  • 플레이어가 서 있는 개체에 적용되는 힘이 ( 질량과 중력 때문에 ) 이 만큼 스케일링 됨.

 InitialPushForceFactor

  • 플레이어가 blocking 하는 물리 개체로 튕겨졌을 때 적용하는 초기 충격력( impulse force ).

 PushForceFactor

  • 플레이어가 blocking 하는 물리 개체와 충돌했을 때 적용할 힘.

 PushForcePointZOffsetFactor

  • 힘이 적용되는 위치에 대한 Z축 오프셋. 0.0 은 물리 개체의 중심이며, 1.0 은 천장. -1.0 은 바닥.

 TouchForceFctor

  • 플레이어가 터치했을 때 물리 개체에 적용되는 힘.

 MinTouchForce

  • 플레이어가 터치한 물리 개체에 적용되는 최소 힘. 만약 0.0 보다 작으면, 최소값 없음.

 MaxTouchForce

  • 플레이어가 터치한 물리 개체에 적용되는 최대 힘. 만약 0.0 보다 작으면 최대값 없음.

 RepulsionForce

  • 모든 겹쳐있는 요소들에게 지속적으로 적용되는 kg 당 힘.


개요



UT 에서 캐릭터 관련 애셋들이 실제로 어떻게 사용되는지 구체적으로 알아 보기 전에 캐릭터의 로직에 대해서 살펴 볼 필요가 있습니다.


플레이어가 게임 도중에 취할 수 있는 행동은 Input 매핑과 관련이 있습니다. "Edit >> Project Settings >> Input" 항목에는 두 종류의 Input Mapping 이 존재합니다; Action Mapping 과 Axis Mapping.




이 문서에서는 어떠한 입력 매핑이 존재하고, 그런 것들이 어떻게 캐릭터와 연관되는지에 대해서 살펴 보도록 하겠습니다.


입력 매핑



다들 알고 계실거라 생각하지만 노파심에 설명하자면 Action Mapping 은 단일 이벤트이며, Axis Mapping 은 지속 이벤트입니다. 이러한 Input Mapping 과 관련한 자세한 내용이 궁금하시다면, 언리얼 공식 문서의 [ 입력 (Input) ] 항목을 참조하십시오. 참고로 매핑을 할 때는 특정 키가 연관되어 있다는 것만 지정하지, down, up, double click 등의 정보를 명시적으로 지정하지는 않습니다.


어쨌든 UT 의 Action Mapping 은 다음 표와 같습니다( 여기에서는 PC 관련 매핑만 다루도록 하겠습니다 ).


 매핑 키

  매핑 입력 키

 설명 

 AUTPlayerController 메서드

 Jump

 Space Bar

 점프.

 IE_Pressed : Jump()

 Crouch

 C

 Left Ctrl

 앉기.

 IE_Pressed : Crouch()
 IE_Released : UnCrouch()

 Slide

 Left Shift

 슬라이딩.

 IE_Pressed : Slide()
 IE_Released : StopSlide()

 PrevWeapon

 Mouse Wheel Up

 이전 무기를 선택.

 IE_Pressed : PrevWeapon()

 NextWeapon

 Mouse Wheel Down

 다음 무기를 선택.

 IE_Released : NextWeapon()

 ThrowWeapon

 M

 무기를 버림.

 IE_Released : ThrowWeapon()

 StartFire

 Left Mouse Button

 Right Ctrl

 총질을 시작.

 IE_Pressed : OnFire()

 StopFire

 Left Mouse Button

 Right Ctrl

 총질을 멈춤.

 IE_Released : StopFire()

 StartAltFire

 Right Mouse Button

 아마도 alternative fire( secondary fire ).

 Alternative fire 를 시작.

 IE_Pressed : OnAltFire()

 StopFire

 Right Mouse Button

 Alternative fire 를 멈춤.

 IE_Released : OnStopAltFire()

 StartActivatePowerup

 Q

 Powerup( U Damage, Invisibility, Berserk, Jump Boots ) 을 활성화함.

 IE_Pressed : OnActivatePowerupPress()

 SlowerEmote

 Mouse Wheel Down

 감정 표현 애니메이션을 느리게 만듦.

 IE_Pressed : FasterEmote()

 FasterEmote

 Mouse Whee Up

 감정 표현 애니메이션을 빠르게 만듦.

 IE_Pressed : SlowerEmote()

 Play Taunt

 J

 첫 번째 도발 애니메이션 재생.

 IE_Pressed : PlayTaunt()

 Play Taunt2

 K

 두 번째 도발 애니메이션 재생.

 IE_Pressed : PlayTaunt2()

 TapRight

 D

 오른쪽으로 회피.

 IE_Pressed : OnTabRight()

 IE_Released : OnTapRightRelease()

 TapLeft

 A

 왼쪽으로 회피.

 IE_Pressed : OnTabLeft()
 IE_Released : OnTapLeftRelease()

 TapForward

 W

 앞쪽으로 회피.

 IE_Pressed : OnTabForward()
 IE_Released : OnTabForwardRelease()

 TapBack

 S

 뒤쪽으로 회피.

 IE_Pressed : OnTabBack()
 IE_Released : OnTabBackRelease()

 SingleTapDodge

 V

 앞의 TabXXX 시리즈가 더블 클릭을 요구하는데 단일 클릭만으로 회피.

 IE_Pressed : OnSingleTabDodge()

 ShowScores

 Tab

 점수 현황판을 보여 줌.

 IE_Pressed : OnShowScores()
 IE_Released : OnHideScore()

 ShowMenu

 Escape

 메뉴를 보여 줌.

 ShowMenu()

 Talk

 T

 전체 채팅창을 보여 줌.

 IE_Pressed : Talk()

 TeamTalk

 Y

 팀 채팅창을 보여 줌.

 IE_Pressed : TeamTalk()


Axis Mapping 은 다음 표와 같습니다.


 매핑 키

 매핑 입력 키

 스케일

 설명 

 AUTPlayerController 메서드

 MoveForward

 W

 Up

 1.0

 1.0 

 앞쪽으로 이동. 

 MoveForward()

 MoveBackward

 S

 Down

 1.0

 1.0 

 뒤쪽으로 이동.

 MoveBackward()

 MoveLeft

 A

 1.0

 왼쪽으로 이동.

 MoveLeft()

 MoveRight

 D 

 1.0 

 뒤쪽으로 이동.

 MoveRight()

 TurnRate

 Left

 Right

 -1.0

 1.0

 비율로 Yaw 회전.

 TurnAtRate()

 Turn

 Mouse X

 1.0

 Yaw 회전.

 AddYawInput()

 LookUp

 Mouse Y

 -1.0

 Pitch 회전.

 AddPitchInput()

 MoveUp

 C

 Space Bar

 1.0

 -1.0

 수직으로 이동. 

 MoveUp()


이러한 입력 매핑들은 AUTPlayerController::SetupInputComponent() 에서 수행됩니다.



이동 입력



이동을 위한 이벤트 처리 로직은 아래의 sequence diagram 에 나와 있습니다. 간단한 실행 흐름만을 보여 주려고 했기 때문에, 내부적으로 설정되는 필드같은 것은 생략했습니다. 여기에서 전부 표현해 주기에는 너무 많습니다. 


반복되는 호출에 대해서는 하위 호출을 생략했으니 보시는데 주의하시기 바랍니다.




위의 다이어그램은 매우 복잡해 보이지만 사실 매우 단순합니다. 입력은 Player Controller( AUTPlayerController ) 에서 처리합니다. 그리고 Character( AUTCharacter ) 로 전달하죠. 그러면 Character 는 최종 TM 을 결정하기 위한 플래그나 정보를 설정합니다. 그런데 애니메이션 피드백이나 물리 피드백을 위해서 Movement Component( UUTCharacterMovement ) 에 상태 정보를 전달합니다. 


이벤트 처리 단계에서는 대부분 현재 상태나 플래그를 설정하는 것으로 끝납니다. 애니메이션 피드백이나 물리 피드백을 위한 실제 처리는 UUTCharacterMovement::PerformMovement() 에서 처리됩니다.


PerformMovement() 의 내부가 어떻게 구성되는지 좀 더 자세히 알고자 하신다면 [ UE4 캐릭터 이동 시스템 가이드 ]를 참조하시기 바랍니다.


전투 입력



FPS 게임이다보니 전투와 관련한 입력은 그리 많지 않습니다; fire, alt-fire, select weapon, drop weapon.


전투 입력이나 이동 입력이나 전반적으로 그 처리의 흐름이 크게 다르지 않습니다. 단지 Movement Component 와의 연관성이 더 적습니다. 특이한 것은 FDeferredFireInput 의 인스턴스는 UUTCharacterMovement::TickComponent() 호출 시점에서 소비된다는 것입니다.


나가며



여기에서는 플레이어 입력과 관련한 로직들을 간단하게 살펴 보았습니다.


그런데 근본적으로 우리가 관심을 가지고 있는 것은 이런 로직들이 최종적으로 다른 애셋들( 메쉬, 애니메이션 등 )과 어떤 식으로 연관되는지 알아 내는 것이므로 아직 성에 차지 않을 수 있습니다. 하지만 내용이 너무 복잡해져서 여기에서 모두 담기 어려운 면이 있어서 나눠서 분석할 계획입니다.

개요



1인칭 캐릭터는 말 그대로 1인칭 카메라 모드에서 동작합니다.


UT 에서는 하나의 캐릭터 내에 1인칭 캐릭터 메쉬와 3인칭 캐릭터 메쉬를 모두 포함하고 있습니다. 일단 플레이를 시작하면 플레이어를 위해서 DefaultCharacter 블루프린트의 인스턴스를 생성하게 됩니다.


그림1. DefaultCharacter 블루프린트.


노란 엣지를 가진 메쉬를 확인할 수 있습니다. 이 블루프린트의 상속구조는 다음과 같습니다.


그림2. DefaultCharacter 상속 구조.



[ 그림1 ]의 블루프린트에서 "(Inherited)" 라고 표시되어 있는 컴포넌트들은 모두 AUTCharacter 클래스에서 정의한 컴포넌트들입니다. 그래서 위치를 바꾸거나 이름을 변경하는 것이 불가능합니다. 만약 변경하고 싶다면 소스 코드를 수정해야 합니다.


어쨌든 이 문서에서는 1인칭 캐릭터와 관련한 컴포넌트에 집중하도록 하겠습니다. [ 그림1 ]의 "Components" 뷰에서 선택한 두 개의 컴포넌트( CharacterCameraComonent 와 FirstPersonMesh )들이 1인칭 캐릭터 전용 컴포넌트입니다. UTChracterMovement 는 1인칭 캐릭터와 3인칭 캐릭터에서 공용으로 사용합니다. 이 CharacterMovement 컴포넌트에 대한 간략한 설명은 [ UE4 캐릭터 이동 시스템 가이드 ] 에서 찾아볼 수 있습니다.


FirstPersonMesh



[ 그림1 ]의 FirstPersonMesh 컴포넌트는 USkeletalMeshComponent 의 인스턴스입니다. 이것은 UStaticMeshComponent 와 대응되는 것으로서 본애니메이션이 가능한 메쉬 컴포넌트를 의미합니다.


[ 그림3 ] 은 이 컴포넌트의 가장 중요한 속성들을 보여 줍니다.


그림3. FirstPersonMesh 컴포넌트의 주요 속성들.


속성들은 다음과 같은 의미를 가집니다. 여기에서는 간략하게 설명하도록 하겠습니다.

    • Animation 카테고리에서 "Content/RestrictedAssets/Animations/Universal/1stPerson/UT4_Base_1stP_AnimBP_C.uasset" 이라는 애니메이션 블루프린트를 사용할 것임을 지정합니다. 
    • Mesh 카테고리에서 "Content/RestrictedAssets/Character/Human/Male/malcolm_ut4_1stp_SKELMESH.uasset" 이라는 스켈레탈 메쉬를 사용할 것임을 지정합니다.
    • Materials 카테고리에서 "Content/RestrictedAssets/Character/Malcom_New/Materials/M_Malcom_Body_Panini.uasset" 이라는 머티리얼을 사용할 것임을 지정합니다.
    • Collision 카테고리에서 "NoCollision" 프리셋을 지정합니다. 즉, 부모 컴포넌트인 CapsuleComponent 의 콜리전 설정에 의존하겠다는 것입니다. 참고로 CapsuleComponent 의 콜리전 프리셋은 "Pawn" 으로 지정되어 있으며, [ 그림 4 ] 에서 확인할 수 있습니다.


그림4. CapsuleComponent 컴포넌트의 Collision Preset.


CharacterCameraComponent



CharacterCameraComponent 는 1인칭 카메라를 의미합니다. CharacterCameraComponent 가 FirstPersonMesh 를 자식으로 가지고 있기 때문에 카메라를 회전시키거나 이동시키면 자동으로 FirstPersonMesh 도 카메라의 영향을 받게 됩니다.


반대로 캐릭터 컨트롤러를 회전시키게 되면 자동으로 카메라도 회전합니다. 이는 CameraSettings 카테고리의 "UsePawnControlRotation" 속성을 true 로 만듦으로써 가능합니다.




"UsePawnControlRotation" 는 카메라가 플레이어 컨트롤러의 forward 방향을 바라보도록 강제하겠다는 것입니다. 다시 말하면 카메라가 항상 캐릭터의 등짝을 바라보게 된다는 의미입니다. 1인칭에 딱 맞는 카메라라 할 수 있겠죠. 세부적인 구현은 다음과 같습니다. 



정리



여러 가지 컴포넌트들이 존재하지만, 1인칭 캐릭터와 관련된 핵심 컴포넌트는 FirstPersonMesh 스켈레탈 메쉬 컴포넌트와 CharacterCameraComonent 카메라 컴포넌트입니다.


중요한 것은 FirstPersonMesh 가 CharacerCameraComponent 의 자식이어서 캐릭터의 TM 이 카메라에 종속되며, 카메라가 항상 등쪽을 바라보도록 하기 위해서 "UsePawnControlRotation" 을 사용한다는 것입니다.


이 문서에서는 간략하게 구조에 대해서만 언급하고 다른 문서들에서 1인칭 캐릭터의 세부 사항에 대해서 다루도록 하겠습니다.

개요



UT 에는 여러 종류의 캐릭터들이 있습니다. 그런데 N 개의 캐릭터에 대해 M 개의 애니메이션을 가지고 있다면, N X M 개의 애니메이션이 필요할 것입니다. 게다가 1인칭과 3 인칭을 모두 표현해야 한다는 것을 감안한다면 몇 개의 애니메이션을 만들어야 할지 감도 잡기 어렵습니다. 제가 대충 살펴 보니 3rdPerson 폴더에만 327 개의 애니메이션 애셋이 존재하더군요. 그리고 애니메이션의 개수도 문제지만 용량도 무시할 수 없습니다.


그런데 다행히도 UT 의 캐릭터들은 모두 인간형( Humanoid )이라는 공통점을 가집니다. 그래서 UE4 의 애니메이션 리타게팅( Animation Retargeting ) 기능을 사용하면 큰 힘을 들이지 않고도 하나의 스켈레톤( Skeleton )과 애니메이션을 공유할 수 있게 됩니다. 하지만 UT 에서는 리타게팅 기법을 사용하지 않고, 그냥 모든 스켈레탈 메쉬( Skeletal Mesh )의 스켈레톤을 공유하고 있습니다. 아마도 신체 비율이나 메쉬의 모양이 큰 차이가 없다고 판단한 것 같습니다.


공유하고 있는 스켈레톤 애셋의 경로는 "/Content/RestrictedAssets/Character/Human/Male/ut4_base_Skeleton" 입니다.


이 스켈레톤을 중심으로 크게 2 부류의 애니메이션으로 나뉩니다; 1인칭3인칭. 대부분의 애니메이션 애셋들은 "Content/RestrictedAssets/Animations/Universal" 에 저장되어 있습니다. "Content/RestrictedAssets/Animations""Human", "Necris", "Skaarj" 같은 하위 폴더들이 존재하지만, 거기에는 특정 캐릭터에 국한된 애니메이션들만 저장되어 있습니다.


1인칭의 경우에는 "Content/RestrictedAssets/Animations/Universal/IstPerson/UT4_Base_1stP_AnimBP" 라는 애니메이션 블루프린트를 사용하고, 3인칭의 경우에는 "Content/RestrictedAssets/Animations/Universal/UT4_Base_AnimBP" 라는 애니메이션 블루프린트를 사용합니다.


Animation Blueprint



애니메이션 블루프린트는 UAnimInstance 를 부모로 하는 블루프린트입니다. 가끔 애니메이션 블루프린트를 캐릭터 블루프린트같은 곳에서 어떻게 찾느냐는 질문을 하시는 분들이 있는데, 다음과 같이 찾으시면 됩니다. 아래 그림은 BaseUTCharacter 블루프린트에서의 예제입니다.



GetAnimInstance 를 통해 얻은 인스턴스를 UT4_Base_AnimBP 로 변환하는 것을 보실 수 있습니다. Target 으로 Mesh 가 설정되어 있는데, 그것은 3인칭 캐릭터에 대한 USkeletalMeshComponent 인스턴스입니다.


어쨌든 중요한 것은 애니메이션 블루프린트는 애니메이션 인스턴스라는 것입니다.


애니메이션 블루프린트를 열게 되면 크게 3 종류의 그래프가 존재한다는 것을 알 수 있습니다.


    • EventGraph
    • AnimGraph
    • StateMachine


UE4 에서는 이러한 그래프들을 적절하게 다룸으로써 최종 애니메이션 포즈를 결정하게 됩니다.


EventGraph



EventGraph 는 AnimGraph 와 StateMachine 에서 사용할 변수들을 공급하거나 AnimMontage 같은 애니메이션을 실행하기 위한 그래프입니다. 여기에서는 보통 Player Controller 나 Character 에 접근해서 필요한 정보를 뽑아 옵니다.



EventGraph 는, 그 이름이 의미하는 것처럼, 특정 이벤트가 발생했을 때 특별한 처리를 하게 됩니다. 이것은 일반적인 블루프린트의 EventGraph 와 같은 방식으로 동작합니다. 여기에서는 Animation 이나 State Machine 에서 발생하는 이벤트( notify )도 받을 수 있기 때문에 매우 활용도가 높습니다.




AnimGraph



AnimGraph 는 애니메이션 블렌딩을 위한 그래프입니다. 여러 종류의 애니메이션을 블렌딩할 수 있습니다. 



여기에서 특이한 점은 최종 포즈에서 사용되지 않는 블렌딩 노드들은 평가되지 않는다는 것입니다. 위의 그림을 보면 "Blend Poses by bool" 노드의 "True Pose" 슬롯에 바인딩되어 있는 라인은 평가되고 있지 않은 것을 볼 수 있습니다.


블렌딩 대상은 개별적으로 플레이될 수 있는 대부분의 애니메이션 요소이며, State Machine, 개별 애니메이션, Animation Montage 등을 블렌딩할 수 있습니다.


StateMachine



다들 잘 아시겠지만 StateMachine 은 특정 조건을 통해 노드를 전환할 수 있는 그래프입니다. 이것은 AnimGraph 의 하위요소입니다. AnimGraph 가 블렌딩을 통해 최종 포즈를 결정하는 것이기 때문에, 이러한 구조는 당연한 것이라 할 수 있습니다. UE4 의 StateMachine 은 HFSM( Hierarchical Finite State Machine ) 입니다.



UE4 의 StateMachine 에서 가장 좋은 점을 꼽으라고 한다면, transition 의 유연성이라고 할 수 있을 것 같습니다.


Unity3D 의 Mechanim StateMachine 같은 경우에는 파라미터를 넘겨서 transition 조건을 판단하는데, 이것은 복잡한 조건을 판단해야 하는 경우에는 매우 불편합니다. 그러므로 조건이 복잡해지면 하드코딩을 해야 합니다.


하지만 UE4 StateMachine 은 EventGraph 를 통해 복잡한 조건을 검사해서 StateMachine 에 변수를 공급할 수도 있고, transition 자체에서 조건을 만들 수도 있습니다.



게다가 transition 에 다양한 옵션들이 존재하고 있으며, blueprint 로 Notify 를 보낼 수 있다는 점이 매우 좋습니다.



나가며



정리하다가 보니 UE4 광고가 되 버린 것 같습니다. 어쨌든 UT 는 UE4 의 기본 애니메이션 메커니즘을 사용하고 있습니다.


애니메이션 애셋들은 1인칭 용과 3인칭 용으로 나뉘어 있으며, 모든 캐릭터가 스켈레톤을 공유하고 있습니다.


사실은 이 문서에서 애니메이션 애셋을 용도별( Animation Offset, Animation Montage, Animation Blend )로 구분해 놓으려고 했으나 좀 복잡해서 나중으로 미루도록 하겠습니다.


1인칭과 3인칭에 대한 애니메이션 애셋들의 종류나 그것이 사용되는 방식들에 대해서는 다른 문서를 통해 다루기로 하겠습니다.

개요



UE4 에서는 어떤 인스턴스의 근본 구조와 로직은 C++ 클래스를 통해 정의하고, 인스턴스당 특성들은 블루프린트 및 그것의 상속을 통해서 정의합니다.


UT 에서의 캐릭터도 마찬가지의 방식으로 정의되어 있으며, 그 구조는 다음과 같습니다.



GhostCharacter 라는 것은 튜토리얼에서 사용하는 특별한 캐릭터입니다. 그런데 이 구조를 보면서 뭔가 미진함을 느끼시는 분들이 있을 겁니다. DefaultCharacter BaseUTCharacter 에는 메시를 바꿀만한 이벤트같은 것이 존재하지 않습니다. 그러면 어떠한 경로를 통해서 다양한 메시가 나올 수 있는 것일까요? 


결론적으로 이야기하자면 UT 에서 모든 캐릭터는 DefaultCharacter 만 사용해서 생성됩니다. 다음 섹션에서는 이러한 일들이 어떻게 가능한지에 대해 구체적으로 설명하도록 하겠습니다.


Character Content



UT 에서 DefaultCharacter 의 Mesh 컴포넌트는 AUTCharacterContent 라는 액터를 참조하여 대체될 수 있습니다. Mesh 컴포넌트는 이전에도 이야기했듯이 3인칭 관점에서의 캐릭터를 표현하기 위한 컴포넌트입니다.


이 Mesh 컴포넌트를 어떻게 채워야 하는지에 대한 정보를 담고 있는 것이 바로 character content 입니다. UT 에서 AUTCharacterContent 를 상속하여 만든 블루프린트들은 "Content/RestrictedAssets/Character" 폴더에 있습니다.



그중에 NecrisFemaleBase 라는 블루프린트를 열어 보도록 하겠습니다.



열어 보시면 매우 실망스러울 것입니다. Mesh 라는 컴포넌트가 달랑 하나 있는데, 내용이 전혀 채워져 있지 않습니다. 그것은 Base 클래스이기 때문에 그렇습니다. 그렇다면 실제로 뭔가가 할당된 것이 있을까요?


네 있습니다. 단지 하나밖에 없어서 문제지요. "Content/RestrictedAssets/Character/Malcom_New" 폴더에 가면 Malcolm_New 라는 블루프린트 애셋이 있습니다( 폴더 이름과 애셋 이름이 다른 것은 오타가 아닙니다. 실제로 그렇게 되어 있습니다 ). 이것은 HumanMaleBase 를 상속하고 있으며, SkeletalMesh'/Game/RestrictedAssets/Character/Human/Male/malcolm_ut4_SKELMESH.malcolm_ut4_SKELMESH' 를 사용합니다.



여기까지 보시고 나서 "그래서 뭐 어쩌라고?" 라는 질문을 던지시는 분이 계실 겁니다. 당연한 질문입니다.


AUTCharacter 에는 ApplyCharacterData() 라는 메서드가 있습니다. 여기에서 AUTCharacterContent 를 인자로 받아 AUTCharacter 가 가지고 있는 Mesh 컴포넌트를 변경하게 됩니다.



코드를 조금만 읽어 봐도 금방 이해하시겠지만, 요약하자면 팀 재질과 메쉬를 설정합니다.


용례 : Bot



실제로 DefaultCharacterAUTCharacterContent 가 어떤 식으로 연결되는지에 대한 예를 들어 보도록 하겠습니다.


UE4 에서 플레이어의 상태를 저장하기 위해서는 PlayerState 라는 것을 사용합니다. Character 나 Pawn 에 정보를 직접 저장할 수도 있지만, 서버와의 연동을 생각하면 PlayerState 를 사용하는 것이 좋습니다. 언리얼 공식 문서의 [ 게임플레이 프로그래밍 ] 에서는 PlayerState 에 대해 다음과 같이 정의합니다.


인간 플레이어 또는 플레이어인 척 하는 봇과 같은 게임 참여자의 상태를 말합니다. 게임의 일부로서 존재하는 플레이어가 아닌 AI 에는 PlayerState 가 없습니다.

PlayerState 에 적합한 예제 데이터라면, 플레이어 이름, 점수, MOBA 게임류에서의 대전상대 레벨, CTF 게임에서 플레이어가 현재 깃발을 운반중인지 여부 등입니다. 모든 플레이어에 대한 PlayerState는 ( PlayerController 와는 달리 ) 모든 머신에 존재하며, 동기화 상태 유지를 위해 자유로이 리플리케이트 가능합니다.


어쨌든 정리하자면 이 PlayerState 는 플레이어의 정보를 유지하는데 사용됩니다. UT 의 경우에는 AUTCharacterContent  AUTPlayerState::SelectedCharacter 라는 속성을 통해 현재 플레이 대상의 CharacterContent 를 공급합니다.


자 원래 주제로 돌아 와서, UT 에서 Bot 은 UUTBotCharacter 라는 데이터 애셋으로 정의됩니다. "Content/RestrictedAssets/Character/Bots" 폴더에 보면 봇 캐릭터를 정의하는 애셋들이 있습니다. 이것은 실제 캐릭터가 아니라 UDataAsset 을 상속하고 있는 데이터입니다. "Content/RestrictedAssets/Character/Bots/ThunderCrash/Taye" 라는 봇의 데이터를 한 번 살펴 보도록 하겠습니다.



봇의 성격이라든가 목소리라든가 하는 여러 가지 특징들을 정의할 수 있군요. 하지만 우리가 집중할 것은 노란 박스로 강조해 둔 Character 속성입니다. 이것은 다음과 같이 정의되어 있죠.



앞에서 언급한 AUTCharacterComponent 를 사용하도록 메타 클래스를 지정하고 있음을 알 수 있습니다. 그런데 지금은 "None" 으로 비어 있죠? 이 경우에는 위에서 언급한 Malcolm_New 라는 기본 CharacterContent 를 사용하게 됩니다. 이것을 바꾸는 방법에 대해서는 "실습" 섹션에서 논의하도록 하겠습니다.


자 이제 이것이 전체적으로 어떤 관계를 가지는지 클래스 다이어그램을 통해 살펴 보도록 하겠습니다.



우리가 Character 속성에 CharacterContent 애셋을 지정하면, 그것은 AUTPlayerState.SelectedCharacter 에 설정되며, 그것은 다시 ( DefaultCharacter 의 부모인 ) AUTCharacter ApplyCharacterData() 의 인자로 넘겨집니다. 이를 통해서 DefaultCharacter 의 ( 3인칭 메쉬의 ) 외형이 결정되는 것입니다.


여기에서는 봇의 경우만 예로 들었지만, 다른 플레이어 캐릭터의 경우에도 PlayerState 를 가지기 때문에 다를 것이 없습니다.


실습 : Taye



이제 실제로 우리가 원하는 CharacterContent 의 봇에 지정해 보도록 하겠습니다. Taye 는 왠지 여자 이름인 것 같으므로 여자 캐릭터를 지정해 보겠습니다. 일단 "Content/RestrictedAssets/Character/Necris_Female" 폴더로 이동합니다.


1. 먼저 CharacterContent 부터 생성해야겠죠. 다음과 같이 블루프린트 부모로 NecrisFemaleBase 를 지정한 후에, 블루프린트가 생성되면 이름을 "NecrisFemale" 로 변경합니다.



그리고 나서 Mesh 의 "Skeletal Mesh" 항목에 "necris_female_ut4_SKELMESH" 를 할당해 줍니다. 이것으로서 CharacterContent 애셋 생성은 끝입니다. 굳이 애니메이션 돌아 가는 것을 확인하고 싶으시다면 "Anim Class" 항목에 애니메이션을 할당해 봐도 좋겠죠.



다음으로는 봇에다가 이 CharacterData 를 할당할 차례입니다. Taye 봇 데이터 애셋을 열어 "Character" 항목에 "NecrisFemale" 을 할당합니다.



"ExmapleMap" 의 시작부에는 다음 스샷과 같이 봇을 생성하는 곳이 있습니다. 게임에 들어 가서 그 위치에서 "Enter" 키를 누르면 봇이 생성되었다가, 다시 "Enter" 키를 누르면 봇이 죽습니다. 이렇게 몇 번 하다가 보면 Taye 가 생성됩니다. 기본으로는 Malcolm_New 가 생성되므로 구분하기는 어렵지 않을 것입니다. 랜덤으로 생성되기 때문에 인내심이 필요할 것입니다( Blue Team Spawn 에 서서 생성하면 초반에 잘 나오더군요 ). 




아래 그림은 possession 을 해제하고 찍은 스샷입니다. 이상하게도 빙의를 해제하면, 1인칭 메시도 같이 나오더군요. 이 부분은 왜 그런지 나중에 확인해 봐야겠습니다.




원하는 봇만 생성되기를 원한다면 AUTGameMode::AddBot() 메서드를 수정하십시오.

개요



Example_Map 을 플레이하게 되면 게임모드와 캐릭터가 생성되는 것을 볼 수 있습니다. 여기에서, 경험자라면 프로젝트 세팅을 떠 올리게 될 테지만, 초보자나 저같이 간만에 UE 를 다시 본 사람들은 혼란에 빠지게 됩니다.


"Edit -> Project Settings..." 메뉴를 클릭하게 되면 "Project Settings" 라는 다이얼로그가 뜹니다. 여기에서 "Maps & Modes" 카테고리를 선택하게 되면 다음과 같은 화면을 볼 수 있습니다.



프로젝트 세팅과 관련한 전반적인 내용은 다음 링크에서 확인하시기 바랍니다.


  • Default Maps
    • Editor Startup Map 은 에디터를 처음 실행할 때 열어 줄 맵을 지정합니다.
    • Game Default Map 은 게임을 실행할 때 열어 줄 맵을 지정합니다. 그러나 PIE( Play In Editor ) 에서 게임을 실행하면 현재 맵에서 게임을 실행합니다.
    • Local Map OptionUEngine::LoadMap() 호출시 넘어 갈 부가인자를 설정합니다. 현재는 "?listen" 을 붙여 listen server 기능을 강제하는 기능이 있다는 것만 알고 있습니다. 자세한 내용은 더 연구를 해 봐야 알 수 있을 것 같습니다.
    • Transition MapSeamless Travel 이 활성화된 상태에서 맵을 전환할 때 중간에 사용되는 맵입니다.
    • Server Default Map 은 서버를 실행할 때 열어 줄 맵을 지정합니다.
  • Default Modes
    • Default GameMode 는 맵의 "World Settings" 에서 "GameMode Override" 를 지정하지 않았을 때, 기본적으로 실행해 줄 게임모드를 지정합니다.
    • Selected GameMode 는 "Default GameMode" 에 선택된 게임모드의 내용을 간략하게 보여 줍니다.
    • Global Default Server Game Mode 는 서버의 게임모드를 지정합니다.
  • Local Multiplayer
    • Use Splitscreen 은 로컬에서 멀티플레이를 실행할 때 화면을 분할할 것인지 여부를 결정합니다.
    • Two Player Splitscreen Layout 은 로컬에서 2 마리에 대한 멀티플레이를 실행할 때 화면을 분할할 레이아웃을 결정합니다.
    • Three Player Splitscree Layout 은 로컬에서 3 마리에 대한 멀티플레이를 실행할 때 화면을 분할할 레이아웃을 결정합니다.
  • Game Instance
    • Game Instance Class 는 게임 인스턴스의 유형을 결정합니다.

UUTGameInstance



UE 4.4 버전부터 GameInstance 라는 개체가 생겼습니다. UE 문서에 따르면 게임인스턴스는 다음과 같은 역할을 합니다.


GameInstance: 실행중인 게임의 인스턴스를 위한 고수준 관리자 개체. 게임 생성시 생성되지만 게임 인스턴스가 닫힐때까지 파괴되지 않습니다. Standalone 게임으로서 실행중일 때, GameInstance 는 하나만 생성될 것입니다. PIE( Play-in-editor ) 에서 실행하면 PIE 인스턴스당 하나씩 생성될 것입니다.


뭔가 설명을 어렵게 해 놨는데, 결국은 게임 그 자체를 의미하게 됩니다. 레벨이 전환되더라도 공유할 수 있는 데이터를 저장할 수 있는 전역 개체입니다.


UE 위키에서 열심히 활동하는 Rama 는 [ Game Instance, Custom Game Instance For Inter-Level Persistent Data Storage ] 라는 아티클에서 다음과 같이 활용예를 들었습니다.


현실적인 예로써, Solus 에는 다른 레벨로 들어 갔을 때도 같은 상태를 유지할 필요가 있는 통신탑( Comms Tower )이 존재합니다. 마치 그것이 엄청나게 먼 거리에서 보이는 것처럼 말이죠.


플레이어는 통신탑의 상태를 발전시키는 행동을 취할 수 있습니다. 그리고 그런 변화는 플레이어가 입장하는 다른 레벨에서도 반영되어야만 합니다.


GameInstance 클래스를 사용하면, 나는 플레이어가 통신탑의 상태에 변화를 줄 때마다 그것을 기록해서, 그 정보를 플레이어가 새로운 레벨에 진입할 때 넘겨줄 수 있습니다. 


UT 의 경우에는 UUTGameInstance 라는 클래스를 기본 게임 인스턴스 클래스로 지정합니다( asset-field 에 클래스가 들어 갈 때는 접두어를 배제한 이름이 들어갑니다. 그래서 실제 클래스는 UTGameInstance 가 아니라 UUTGameInstance 입니다 ).


이 게임 인스턴스 클래스는 다음과 같은 정보를 담고 있습니다:

    • 마지막 플레이한 데모 정보.
    • 매칭 정보.
    • 파티 정보.
    • 세션 정보.


AUTDMGameMode



게임 인스턴스가 게임 자체를 의미한다면, 게임 플레이를 의미하는 것은 게임 모드입니다. 일단 "A" 는 "Actor" 의 머리글자이며, "UT" 는 "Unreal Tournament" 의 머리글자이고, "DM" 은 "Death Match" 의 머리글자입니다. 데스 매치를 위한 게임 모드라 할 수 있습니다. 왜 게임 모드가 액터냐는 질문을 던지신다면야, 실제로 씬에 배치되기 때문이라고 대답하겠습니다. 왜 씬에 배치되야 하느냐고 질문하신다면, 그건 언리얼을 만든 사람의 마음이라 잘 모르겠습니다. 아마도, Unity 3D 처럼, 게임 인스턴스를 제외한 모든 개체는 월드( 씬 ) 단위로 유지되기를 원한 것 같습니다.


어쨌든 게임 모드는 게임에서 사용하게 될 핵심 액터들을 정의하게 됩니다. 각각은 다음과 같은 역할을 수행합니다.


    • DefaultPawnClass AGameMode.DefaultPawnClass 프라퍼티입니다. 아래에서 리스팅된 이름들도 다 마찬가지입니다. 다들 잘 아시겠지만 노파심에 설명하자면, 프라퍼티는 C# 프라퍼티가 아니라 UPROPERTY() 특성 매크로를 지정한 필드를 의미합니다. 이것은 일종의 메타 데이터( metadata )로, UnrealHeaderTool 은 이것을 파싱해서 C++ 클래스로 만듭니다. 어쨌든 DefaultPawnClass 는 게임 모드를 생성했을 때 자동으로 생성할 플레이어를 위한 을 의미하는 클래스입니다.
    • HUDClass 는 게임에서 사용할 HUD 를 지정하는 프라퍼티입니다.
    • PlayerControllerClass 는 플레이어를 위한 컨트롤러를 지정하는 프라퍼티입니다. 그것은 폰과 그것을 제어하는 사람간의 인터페이스를 제공합니다. 플레이어 컨트롤러는 플레이어의 의지( 조작 )를 반영합니다.
    • GameStateClass 는 게임 상태를 모니터링 할 게임 스테이트를 지정하는 프라퍼티입니다.
    • PlayerStateClass 는 플레이어 컨트롤러가 사용하는 플레이어에 대한 리플리케이트 정보를 포함하는 플레이어 스테이트를 지정하는 프라퍼티입니다.
    • SpectatorClass 는 관전모드에 들어 갔을 때 사용할 관찰자를 지정하는 프라퍼티입니다.


이러한 정보들에 대한 더 자세히 알고자 한다면, 공식 문서의 [ 게임플레이 프로그래밍 ] 항목을 참조하시기 바랍니다.


게임 모드들



여기까지 읽으시면 게임 모드가 하나밖에 없다고 착각하실 수 있으실텐데요, UT 에는 수 많은 게임 모드들이 있습니다.



개요


 

UT 는 1인칭 슈팅 게임입니다. 그런데 내 입장에서는 1인칭이지만 적이나 아군 입장에서는 3인칭입니다. 또한 ( 관전모드와 같이 ) 자신을 관찰해야 하는 모드에서는 3인칭으로 보여야 합니다. 그래서 UT 에서 캐릭터는 2 개의 skeletal mesh 와 각각의 mesh 에 할당된 animation blueprint 를 소유하게 됩니다.

 

UT 를 컴파일하고 에디터를 열면 Exmaple_Map 이라는 레벨을 볼 수 있습니다다. 이 레벨에서 Play 를 실행하면, PIE( Play In Editor ) 모드로 게임이 실행되는데, DefaultCharacter 라는 것이 생성되어 있는 것을 볼 수 있습니다.

 

 

플레이중에 화면 바깥으로 나가려면 "ALT + ENTER" 를 눌러야 합니다. "ESC" 를 누르면 플레이가 중지됩니다.

 

어쨌든 월드 아웃라이너( World Outliner )에서 흰색 액터( actor )는 원래 씬에 배치되어 있던 것이고, 노란색( ? ) 액터는 플레이 이후에 생성된 것입니다. 오른쪽에 있는 "Edit DefaultCharacter" 라는 하늘색 링크를 누르면 블루프린트가 뜹니다.

 

 

처음에는 그냥 property-grid( Class Default 탭 )만 나오는데, "Open Full Blueprint Editor" 라고 써진 하늘색 링크를 누르면 전체 블루프린트를 볼 수 있습니다. 그리고 "Parent class" 항목은 이 블루프린트의 부모 블루프린트나 부모 클래스가 무엇인지 알려 줍니다. 이름이 하늘색으로 써져 있으면 블루프린트 애셋이며 흰색으로 써져 있으면 C++ 클래스입니다. DefaultCharacter 의 경우에는 "BaseUTCharacter" 라는 블루프린트를 부모로 하고 있습니다.

 

이제 Viewport 항목으로 이동하면 두 개의 mesh 가 존재한다는 것을 알 수 있습니다; Mesh, FirstPersonMesh. 컴포넌트 뷰( Components )에서 (Inherited)라 되어 있는 것은 C++ 클래스에서 정의하고 있는 컴포넌트들이며 계층구조나 이름을 변경할 수 없습니다.

 

 

BaseUTCharacter 는 AUTCharacter 클래스를 상속합니다. 아래 그림에서는 "Parent class" 항목에 "UTCharacter" 라고만 써져 있는데, 이 클래스는 액터 클래스이므로 "A" 라는 접두어가 붙습니다. 만약 헷갈린다고 생각하시면, 그냥 헤더 파일 이름이라 생각하셔도 좋을 것 같습니다. 클릭하면 실제로 Visual Studio 에서 헤더 파일이 열립니다.

 

 

앞에서 언급하지는 않았지만 DefaultCharacter 블루프린트의 EventGraph 는 텅 비어 있습니다. 하지만 BaseUTCharacter 의 EventGraph 에서는 뭔가 열심히 하고 있습니다. 즉, 현재 DefaultCharacter 는 mesh 와 animation 을 재정의하기 위한 용도로 BaseUTCharacter 를 상속하고 있는 것입니다.

 

일반적으로 언리얼 엔진을 사용할 때는 액터의 구조 및 핵심 기능은 C++ 클래스에서 구현하고, 기본 동작은 블루프린트에서 구현합니다. 그리고 variation 은 블루프린트 상속을 통해 구현합니다.

 

다른 요소들 : Reference Viewer


 

지금까지는 mesh 와 animation 에 대해서만 언급했지만, 이것과 연관된 모든 요소들을 확인하고 싶을 경우가 있습니다. 이 때 사용하는 것이 reference viewer 입니다.

 

애셋 브라우저에서 애셋을 선택한 다음에 마우스 오른쪽 버튼을 클릭하면 context menu 가 나옵니다. 이 중에서 "Reference Viewer..." 라는 항목을 선택하면 참조 그래프가 나옵니다. 열어 놓은 블루 프린트에서 "Asset > Reference Viewer..." 를 선택해도 동작은 같습니다.

 

 

이제 DefaultCharacter 의 참조 그래프를 살펴 보도록 하겠습니다.

 

 

중심에 있는 노드가 "DefaultCharacter" 입니다. 그리고 왼쪽의 노드들이 DefaultCharacter 를 참조하고 있는 애셋들이며, 오른쪽에 있는 노드들이 DefaultCharacter 에 의해 참조되고 있는 애셋들입니다.

 

"Search Depth Limit" 와 "Search Breadth Limit" 를 조절하면 그래프를 모두 볼 수 있습니다. 노드의 가장 아래쪽을 보면 "Collapsed nodes" 라는 노드가 있습니다. 이는 "Search Breadth Limit" 를 늘리면 펼쳐집니다.

 

 

 

다른 노드를 클릭해서 다른 애셋의 참조 그래프를 볼 수 있습니다. 익스플로러에서처럼 좌상단의 좌우 버튼을 눌러 뒤로 돌아 가는 것도 가능합니다.


각 요소들의 세부사항에 대해서는 다른 섹션에서 다루도록 하겠습니다.

개요



언리얼 엔진에서 모듈이라는 개념을 이해하는 것은 매우 중요합니다. 그래서 이 문서에서는 다음과 같은 주제에 대해서 다루고자 합니다.


  • 모듈의 개념.
  • 모듈의 빌드 방식.
  • 모듈 룰 파일.


모듈의 정의


 

언리얼에서의 "모듈"이라는 것은 특정 함수성( functionality )를 제공하는 기능 집합을 의미합니다. 예를 들어 UI 모듈이면 UI 관련 기능 집합을 담고 있는 dll 이라 할 수 있습니다. 아래 그림은 언리얼 엔진이 제공하는 모듈의 예를 보여 줍니다.

 

 

이런 모듈을 만들기 위해서 기존에는 프로젝트를 따로 생성했었습니다. 그런데 언리얼의 경우에는 하나의 프로젝트 내에서 여러 개의 모듈을 생성할 수 있도록 하고 있습니다. 아마도 dll 을 만들기 위해서 프로젝트를 여러 개 생성해야 하고 설정도 해야 하는 불편함을 없애려는 목적으로 그렇게 한 것 같습니다.

 

언리얼에서 한 프로젝트에서 모듈을 여러 개 생성할 수 있도록 해 주는 메커니즘은 NMAKE 입니다. UnrealBuildTool.exe 을 이용해서 모듈 단위로 묶어서 컴파일하고 링크하는 것이 가능하기 때문에, 한 프로젝트에서 여러 개의 모듈 파일( dll )이 나올 수 있는 것입니다.


그런데 도대체 모듈은 왜 구분하는 것일까요? 대부분의 모듈들은 정적으로 임포트되지 않습니다. 왜냐하면 특정 기능집합을 특정 응용프로그램에서는 사용하지 않을 수도 있기 때문입니다. 예를 들어 네트워킹과 관련한 기능이 전혀 없는 응용프로그램에서 그것과 관련한 모듈을 반드시 임포트해야 할 필요는 없을 것입니다.


이러한 모듈 개념은 동적인 로드/언로드/리로드를 가능하게 해 주며, 이런 특성은 플러그인 시스템을 구축하기 위해서 사용되기도 합니다.

 

모듈의 빌드 방식


 

언리얼 엔진에서 모듈은 두 가지 방식으로 빌드됩니다. Modular( 조립식 ) 모드와 Monolithic( 통합식 ) 모드가 있습니다.

 

Modular 모드는 모든 모듈이 개별 dll 로 쪼개진다는 것을 의미하고, Monolithic 모드는 모든 모듈이 정적 라이브러리처럼 하나의 실행파일에 임포트되는 것을 의미합니다. 어떤 모드로 빌드되느냐는 UnrealBuildTool 세팅, 플랫폼, 빌드 환경 설정에 따라 다릅니다.

 

언리얼에서는 빌드를 하기 위해서 타겟 룰이라는 것을 사용합니다. 그러한 타겟 룰의 유형에는 Game, Editor, Client, Server, Program 이 있는데, Game, Client, Server 의 경우에는 Monolithic 을 권장하고, Editor 에는 Modular 를 권장합니다. 그리고 Program 의 경우에는 프로그램의 종류에 따라 다릅니다.

 

그렇다고 Game 타겟 룰은 무조건 Monolithic 이어야 한다는 것은 아닙니다. 이것은 Target.cs 파일을 작성하는 사람의 마음대로 정할 수 있는 것입니다. 예를 들어 TargetRules 클래스는 다음과 같은 가상 메서드를 포함합니다. 이를 재정의하게 되면 Monolithic 모드 실행 여부를 결정할 수 있습니다.

 


기본적인 구현은 이렇습니다. Editor 가 아닌 경우에는 "-modular" 라는 인자를 강제로 지정하지 않으면 Monolithic 모드로 빌드하게 됩니다. Editor 인 경우에는 "-monolithic" 을 지정해야만 Monolithic 모드로 빌드하게 됩니다. 앞의 조건에 해당하지 않으면 무조건 Modular 모드로 빌드합니다.


그런데 어떤 경우에 Modular 모드를 사용하고 어떤 경우에 Monolithic 모드를 사용할까요? Modular 모드를 사용하면 빌드( 컴파일 + 링크 ) 단위를 구분할 수 있다는 장점이 있습니다. 이는 빌드 시간에 영향을 줍니다. 일부 모듈만 수정된 상황이라면 Modular 모드는 그 모듈만을 빌드합니다.

 

모듈 룰 파일


 

Modular 모드로 모듈을 빌드하면 dll 이 생성됩니다. 이 모듈을 컴파일하기 위해 dllexport 지정자가 사용되면, 이 모듈에 접근하기 위해서는 dllimport 지정자가 필요합니다. 만약 한 번이라도 dll 을 만들어 보신 분이라면, 쉽게 이해하실 수 있을 것입니다. 이러한 지정자를 모듈 API 지정자라 부릅니다.


언리얼 엔진 솔루션을 열어 솔루션 익스플로러를 확인하면, 여러 개의 폴더가 있는데, 그 중에 아무 거나 펼쳐 봅시다. 여기에서는 EditorStyle 폴더를 열어 보았습니다.


 

그러면 그 폴더에 EditorStyle.Build.cs 라는 파일이 있는 것을 확인할 수 있습니다. 이것이 모듈 룰 파일입니다. 이 모듈 룰 파일이 있는 디렉토리는 하나의 모듈이라 할 수 있습니다. 모듈 룰 파일에서는 모듈 이름을 지정하고, import 할 다른 모듈을 지정하고, 자신의 프로젝트 내에서 include 할 header 의 경로를 지정합니다. EditorStyle.Build.cs 파일을 살펴 봅시다.



5 라인에서 모듈의 이름을 "EditorStyle" 로 지정해 줍니다. 그리고 PublicDependencyModuleNames, PrivateDependencyModuleNames, PrivateIncludePathModuleNames 등을 지정합니다. 위에서 언급했듯이 외부 모듈 등을 설정해 줍니다. 여러 종류의 다른 리스트들이 존재하지만, 아직까지 모든 리스트의 의미에 대해서 파악하지는 못했습니다. 이게 제대로 정리되어 있는 문서가 없네요. 단지 경험적으로 역할에 대해 대충 추정할 뿐입니다. 이 부분에 대해서는 기회가 되면 따로 정리하거나 문서를 갱신하도록 하겠습니다.


어쨌든 이렇게 모듈 이름이 지정되면 UnrealHeaderTool 이라는 녀석이 자동으로 EDITORSTYLE_API 라는 정의를 생성해 줍니다.


그래서 "#define EDITORSTYLE_API" 라는 것은 아무리 검색해도 찾을 수 없지만, 빌드할 때 자동으로 생성되는 *.generated.h 파일에는 포함됩니다.






개요



UT 를 설치하고 나면 게임을 어떻게 실행해야 하는지 몰라서 답답함을 느끼게 될 것입니다. 이 때 도움을 줄 수 있는 것이 "ServerLaunch.exe" 라는 프로그램입니다.


이 파일의 경로는 "$(SolutionDir)/UnrealTournament/Binaries/ServerLaunch.exe" 이며, 이것을 디버깅하거나 분석할 수 있는 소스 파일 패키지( ServerLaunchSource.zip )도 같은 디렉토리에 들어 있습니다.




이 파일을 실행하게 되면 다음과 같은 윈도우가 뜹니다.



로고 버튼 혹은 LAUNCHER 버튼 : Settings



가장 위쪽에 있는 로고나 LAUNCHER 라는 글자를 클릭하면 Settings 다이얼로그가 나옵니다.



  • "Location of the UE4Editor.exe" 항목은 "UE4Editor.exe" 의 경로입니다. 즉 게임을 실행할 때 "UE4Editor.exe" 를 통해서 실행한다는 의미입니다. 물론 "UE4Editor.exe" 도 "UnrealTournament" 모듈을 포함하기 때문에 게임을 실행하는 데는 문제가 없습니다.
  • "Client IP" 는 클라이언트 프로그램의 IP 인데요, 로컬에서 띄울 것이기 때문에 "127.0.0.1" 로 설정되어 있습니다.
  • "Add Custom Server Command Line" 항목은 서버 실행시에 넘길 매개변수입니다. 어떠한 매개변수가 추가될 수 있는지는 아직까지는 잘 모르겠습니다. 거기까지는 분석을 안 했거든요...
  • "Game Types" 의 의미는 다음과 같습니다.
    • DM : Death Match.
    • TDM : Team Death Match.
    • CTF : Capture The Flags.
  • "Client Resolutions" 는 클라이언트 해상도입니다. 원하는 해상도를 추가하시면 됩니다. 게임 내에서 해상도를 바꿔도 다시 실행했을 때 런처의 설정으로 덮어 써 집니다. 해상도 말고 다른 설정들은 유지됩니다.


참고로 왜인지 모르겠지만 다이얼로그의 크기가 맞지 않아서 "Save" 버튼이 안 나오고 있는데, 현재 그 버튼에 포커스가 가 있으므로 엔터를 치면 "Save" 가 눌립니다. 맘에 들지 않으면 소스를 수정하시면 됩니다. 소스의 윈폼 디자이너에서 다이얼로그 크기를 늘렸다가 다시 줄이면 제대로 나옵니다.


"Single Player" 버튼



"Single Player" 버튼을 누르면 하나의 싱글 모드 클라이언트가 실행됩니다. 다음과 같은 식으로 인자가 넘어 갑니다.


UE4Editor.exe UnrealTournament ?Game=DM?GoalScore=5?TimeLimit=6 -game -log -winx=20 -winy=40 -resx=1600 -resy=900 -consolex=20 -consoley=960


의미를 이해하는 데는 큰 어려움이 없을 거라 생각합니다.


이 모드에서는 "Map To Play" 드롭다운박스에서 선택한 맵을 플레이를 시작하게 됩니다.


클라이언트를 띄우면서 콘솔이 같이 뜨는데, 그건 서버가 아닙니다. 서버는 따로 "Launch Server" 버튼을 통해 띄우게 되어 있습니다.



이 모드에서는 튜토리얼같은 싱글플레이만 즐길 수 있습니다. 멀티 플레이에 입장이 안 되는 건 아니지만, 입장해 봐야 다른 클라이언트가 들어 올 수 없으므로 의미가 없습니다.


현재 초기 맵이 "ShockAttaches" 라는 DM 맵이므로, 초기 화면으로 가려면 ESC 버튼을 누르고 Back 버튼을 누르면 됩니다.



클라이언트가 하나 뜨면 "DESKTOP-XXX" 와 같은 플레이어 아이디가 생성됩니다. 확인해 본 결과 싱글 플레이를 띄우면 항상 같은 아이디를 할당하는 것을 알 수 있었습니다.


이제 "BASIC TRAINING" 과 같은 싱글 모드 contents 를 즐길 수 있습니다. 아까도 말했듯이 멀티 플레이는 들어 가 봐야 의미가 없습니다. 트레이닝 맵에 처음 입장할 때는 텍스쳐 압축같은 것을 하느라 시간이 걸릴 수 있으므로 로그 창을 잘 보고 인내심을 기르시기 바랍니다.


"Launch Server" 버튼



"Launch Server" 버튼을 누르면 서버를 띄우게 됩니다. "Listen Server" 가 선택되지 않았을 때 다음과 같은 인자가 넘어 갑니다.


UE4Editor.exe UnrealTournament ?Game=DM?GoalScore=5?TimeLimit=6 -SERVER -log


그리고 나서 다음과 같이 서버 콘솔이 하나 뜹니다.



하지만 "Listen Server" 에 체크를 하게 되면, 게임을 띄웁니다. 제가 listen server 의 의미는 잘 몰라서 더 이상은 설명드리기 어려울 것 같습니다.


UE4Editor.exe UnrealTournament ?Game=DM?GoalScore=5?TimeLimit=6?Listen=1 -GAME -winx=20 -winy=40 -resx=1280 -resy=720 -consolex=20 -consoley=780 -log


"Connect Local Client" 버튼



"Connect Local Client" 는 "127.0.0.1" 에 접근하는 클라이언트를 "Number of Clients" 에 설정된 개수만큼 띄워 줍니다. 그 서버는 "Launch Server" 버튼을 통해 미리 띄워 둔 서버입니다. 물론 "Listen Server" 에 체크를 해서는 안 됩니다.



Single Player 의 인자와 다른 점이 있다면, "127.0.0.1" 이라는 주소가 들어 가 있다는 것과 클라이언트마다 창 위치를 다르게 한다는 것입니다. 만약 본인이 서버를 따로 구축했다면, 소스 코드에서 이 주소 부분을 수정하면 됩니다. 그렇게 되면 더 이상 "Local" 은 아니겠지만 말이죠.


이 클라이언트들도 싱글모드에서와 마찬가지로 각자 ID 를 할당받게 됩니다.


개요


 

UE4 는 모듈이라는 개념을 사용해서 프로젝트를 관리합니다. 각 프로젝트에는 "*.Target.cs" 라는 파일이 존재합니다. 이는 빌드타겟을 지정하고 있으며, 내부의 SetupBinaries() 메서드에서는 사용하고자 하는 모듈을 지정하게 됩니다.

 

UT 의 UnrealTournament 는 세 개의 빌드 타겟을 가집니다.

 

    • UnrealTournament
    • UnrealTournamentEditor
    • UnrealTournamentServer

 

그 중에서 UnrealTournamentEditor 빌드 타겟은 다음과 같은 모듈들을 포함합니다.

 

 

아래의 섹션들중에서 클라이언트 및 에디터와 관련한 두 모듈에 대해서 설명하도록 하겠습니다.

 

UnrelTournament 모듈


 

이 모듈의 정의파일은 UnrealTournament.Build.cs 입니다.

 

다음과 같은 기능들을 포함합니다. 모든 기능에 대해서 언급하기는 어렵고 관심이 있으신 분들은 관련 모듈 파일들을 살펴 보시기 바랍니다.

 

 

대부분 게임을 구동하는 데 사용하는 모듈들입니다. NetworkReplayStreaming, Json, JsonUtilities 등은 json replay data streaming 을 위해 사용됩니다.

 

 

UnrealTournamentEditor 모듈


 

이 모듈의 정의파일은 UnreaTournamentEditor.Build.cs 입니다.

 

당연한 이야기지만 UnrealTornament 의 기능을 포함하며, 편집을 위한 기능들을 추가적으로 가집니다.

 


이 문서는 https://github.com/EpicGames/UnrealTournament 의 How to get started (Windows) 항목에 대한 번역입니다. 해당 github 에 접근하는 것에 실패했다면, [ GITHUB 계정 연동 ] 문서를 참조하시기 바랍니다.

 


 

현재 언리얼 토너먼트 프로젝트는 언리얼 엔진 4.12 프리뷰 버전에 기반합니다( 역주 : 나중에는 버전이 더 올라갈 수 있습니다 ). 편의를 위해서 이 엔진 소스는 이제 우리 저장소에 포함됩니다. 윈도우즈에서의 사용을 위해서는 비주얼 스튜디오 2015 가 필요합니다.

 

언리얼 토너먼트 프로젝트 다운로드하기:

    • 이 페이지에 있는 Download ZIP 파일을 클릭해서 프로젝트를 다운로드하십시오.
    • 여러분의 컴퓨터에 있는 폴더에서 파일의 압축을 해제하십시오( 역주 : zip 파일의 크기는 작지만, 설치과정을 마치고 에디터를 실행하게 되면 최종적으로는 60 GB 이상의 용량이 필요합니다 ).
    • 여러분이 생성했던 디렉토리 내의 Setup.bat 파일을 실행하십시오. 이는 GitHub 에 저장되어 있지 않은 필요한 바이너리들을 다운로드하게 됩니다.
    • Engine\Extras\Redist\en-us 폴더에 있는 UE4PrereqSetup_x64 를 실행하십시오. 이는 누락되어 있을 수도 있는 다른 종속성 파일들을 설치할 것입니다( 역주 : Setup.bat 파일을 실행하는 과정에서 이것이 같이 실행되었다면, 굳이 설치할 필요가 없습니다 ).

 

언리얼 토너먼트 프로젝트 컴파일하기:

    • Engine\Source\Programs\UnrealSwarm 에 있는 UnrealSwarm.sln 을 열기 위해서 비주얼 스튜디오를 사용하십시오. 솔루션 구성( Solution Configuration ) 드롭다운 메뉴에서 Development 모드를 선택하십시오.
    • Agent 프로젝트에서 마우스 오른쪽을 클릭해서 속성( Properties )를 선택하십시오. 서명( Singing ) 탭으로 이동하여, "Sign the ClickOnce manifests" 에 대한 선택을 해제하십시오( 역주 : 소스를 받으면 기본값으로 해제되어 있습니다 ).
    • 마우스 오른쪽을 클릭해서 Agent 프로젝트를 빌드하십시오. 빌드가 끝나면 UnrealSwarm 솔루션을 닫아도 됩니다.
    • 프로젝트 압축을 해제한 디렉토리에서 GenerateProjectFiles.bat 를 실행하십시오. 이는 UE4.sln 을 생성할 것입니다.
    • UE4.sln 을 비주얼 스튜디오로 열어서, 솔루션 구성을 Development Editor 로 변경하십시오.
    • Programs 폴더에 있는 ShaderCompileWroker 와 UnrealLightMmass 를 빌드하십시오.
    • UnrealTournament 프로젝트를 빌드하십시오. UE4Editor.exe 가 Engine\Bindaries\Win64 에 생성될 것입니다.
    • 명령줄에서 "UE4Editor.exe UnrealTournament" 를 실행하거나, 비주얼 스튜디오에서 UnrealTournament 프로젝트에서 마우스 오른쪽을 클릭해서 디버그로 실행하십시오.
가끔 GenerateProjectFiles.bat 을 호출하면 Social 모듈 파일이 없다고 나오는데, 그건 zip 파일이 잘못 올라 가 있는 것입니다. 다른 버전의 파일을 받으시든지 [ 이 페이지 ] 의 Common issues 항목을 참조하시든지 하십시오. 


Unreal Tournament( UT ) 란 Epic Games 와 Digital Extremes 에서 공동개발한 1인칭 슈팅( First Person Shooting ) 게임입니다. UE4 로 넘어 오면서는 그냥 Epic Games 에서만 개발하고 있는 것으로 보입니다. UT 사이트에 가 봤더니 다른 공동 개발사에 대한 언급은 없더군요.

 

어쨌든 UT 는 Counter Strike 와 같은 전통적인 FPS 게임과는 다르게 총을 쏘는 즉시 반응하는 것이 아니라 나가는 궤적을 볼 수 있으며 곡사도 가능합니다. 그리고 매우 다양한 패턴의 무기가 존재합니다. 이런 부분들이 우리나라 게이머들에게는 별로 취향에 맞지 않는지 우리나라에서는 인기가 없습니다.

 

그럼에도 불구하고 제가 UT 를 분석하려고 하는 이유는 다음과 같기 때문입니다.

    • 최신버전에 맞게 갱신된 소스가 공개되어 있습니다.
    • 매우 오랜 기간동안 개발해 와서 안정성이 상당히 높습니다.
    • Epic Games 에서 개발하고 있기 때문에 UE4 사용법에 대한 훌륭한 레퍼런스가 될 것으로 보입니다.

 

UT 분석을 통해서 다음과 같은 부분에 대한 이해도를 높이려고 합니다.

    • Character.
    • Animation.
    • Cosmetic.
    • Physics.
    • Networking.
    • Gameplay.
    • Slate.
    • Optimization.

 

어느 정도 깊이로 분석할지는 모르겠지만, 최선을 다해 볼 생각입니다.

모티브


 

Unity 3D 는 다수 개의 게임 인스턴스를 용납하지 않기 때문에 시뮬레이션이 필요한 에디팅을 할 때는 정말 짜증나는 경우가 많다. 예를 들어 캐릭터 애니메이션 에디터를 만든다고 하자.

 

보통의 상용 엔진이라고 하면 새로운 씬이나 게임 인스턴스를 생성하도록 한다. 예를 들어 UE4 같은 경우에는 editor game instance, play-in-editor( pie ) game instance, 일반적인 game instance 들이 따로 존재할 수 있는 구조이다. 그리고 각 게임 인스턴스들의 입력, 씬 구성 등은 모두 다르다. 그러므로 에디터 메인 뷰를 띄워 놓은 상태에서 페르소나 애니메이션 툴을 따로 띄워 놓고 내부적으로 시뮬레이션을 수행할 수 있다.

 

그러나 유니티는 그런거 절대 없다. 시뮬레이션이 필요하다면 반드시 플레이 버튼을 눌러야 하며 씬도 한 번에 하나밖에 못 띄운다( 콜백을 받아서 편집 상태에서 시뮬레이션하는 방법이 있다고 하는데, 아직까지 해 본 적이 없어서 잘 모르겠다 ). 일단 게임 인스턴스라는 개념조차 없는 것 같으니 대충 비슷하게라도 처리할 수 있는 방법을 찾고 있었다. 그러던 차에 5.3.X 버전부터 멀티 씬 편집을 지원한다는 사실을 알게 되었다( 유니티도 UE 와 같이 스트리밍을 하고 싶었나보다 ).

 

유니티는 현재 시점에는 한글 도움말에는 이것에 대한 정보 자체를 공개하지 않고 있다. 하지만 영문 도움말에는 나와 있다. 이상하게 영문 도움말과 한글 도움말의 카테고리 구성조차 다르다. 뭐하자는 건지 잘 모르겠다. 어쨌든 이 멀티 씬 개념을 사용하면 적어도 오브젝트 관리라도 따로 할 수 있지 않을까라는 생각이 들어서 정확하게 어떤 개념인지 알아 보기로 했다. 목표는 아래와 같다.

 

1. 에디터에서 원하는 시점에 다중 씬을 구성할 수 있는지 확인.

2. 특정 씬의 개체를 획득할 수 있는지 확인.

3. 특정 씬을 편집할 수 없도록 잠글 수 있는지 확인.

4. 특정 윈도우를 띄웠을 때 원하는 뷰에서 특정씬만을 볼 수 있는지 확인.

5. 특정 씬만 플레이할 수 있는지 확인.

 

씬 구성


 

테스트 프로젝트를 하나 만들어서 "Main" 과 "Customizer" 라는 씬을 만들었다. Main 에는 카메라와 빈 게임 오브젝트가 들어 있고, Customizer 에는 도형들이 들어 가 있다. 서로 위치가 겹치지 않게 하기 위해서 ( 원하는 뷰가 나오는지 확인하기 위해서 ), 조금 다른 위치에 배치했다.

 

툴의 "Open Scene Additively" 메뉴를 사용해서 화면을 열면 다음과 같은 결과를 얻을 수 있다.

 

 

두 개의 씬이 올라 와 있고 서로 다른 계층 구조를 가지고 있는 것을 확인할 수 있다. 현재는 Main 씬이 활성화되어 있다.

 

새 윈도우에 씬 렌더링하기


 

위와 동일한 결과를 획득하면서 새로운 윈도우에 Customizer 씬을 렌더링하는 테스트를 수행했다. 이를 위해서 Customizer 라는 EditorWindow 를 제작했다. 주석을 열심히 달아 놓았으니 의미를 파악하기는 어렵지 않을 것이라 본다.

 

 

이렇게 했을 때 다음과 같은 결과를 얻을 수 있었다. Customizer 씬이 활성화되어 있는 것을 볼 수 있다. 씬이 활성화되어 있다는 의미는 다음과 같다. 새로운 게임 오브젝트들은 기본적으로 활성화된 씬에 포함된다.

 

 

 

즉 활성화 여부만으로는 편집을 막거나 렌더링을 막을 수 없다. 실제로 위의 상태에서 메인씬에 3D 텍스트를 추가해 보았다. 다음과 같은 결과를 얻을 수 있었다.

 

 

결론


 

위의 테스트를 통해서 다음과 같은 결론을 얻을 수 있었다.

 

1. 에디터에서 원하는 시점에 다중 씬을 구성할 수 있는지 확인 : 원하는 시점에 다중 씬을 구성할 수 있다.

 

2. 특정 씬의 개체를 획득할 수 있는지 확인 : 현재( 5.3.1f ) 특정 씬의 개체를 획득할 수 인터페이스는 없었다Workaround 가 있다GameObject 가 scene 이라는 멤버 필드를 가지고 있으므로, 모든 게임 오브젝트에 대해 루프를 돌면서 확인할 수는 있다. 5.3.2 버전 부터는 Scene.GetRootGameObjects() 라는 메서드를 지원한다고 하니 그것을 이용할 수도 있다.

 

3. 씬을 편집할 수 없도록 잠글 수 있는지 확인 : 잠글 수 없다. Workaround 가 있다. 만약 모달 윈도우를 띄울 수 있다면 비슷하게 할 수는 있을 것 같다. 그러면 추가/삭제가 활성화된 씬에서 이루어질 것이기 때문읻. 유니티에서 모달 시스템 구현을 하는 방법에 대한 아티클들이 있으니 참고해 볼 수 있을 것 같다.

 

4. 특정 윈도우를 띄웠을 때 원하는 뷰에서 특정씬만을 볼 수 있는지 확인 : 원하는 뷰에서 볼 수는 있지만 특정 씬만을 볼 수는 없다. Workaround 가 있다. 이쪽은 아직 자세하게 몰라서 그런데, tag 나 layer 같은 것을 써서 렌더링 요소를 filtering 할 수 있다고 들은 것 같다. 막 나갈라면 그냥 모든 요소 돌면서 Customizer 씬의 요소가 아니면 렌더링을 꺼 버리는 수가 있다. 물론 복구를 잘 해야 하지만...

 

5. 특정 씬만 플레이할 수 있는지 확인 : 이것은 4 번 항목과 유사한 문제라 할 수 있다.

 

결과적으로 볼 때, ( 설계를 매우 잘 해야 하겠지만 ) 여러 가지 꼼수들을 사용하면 다중 게임 인터페이스가 있는 것과 유사한 행동을 하도록 구성할 수는 있을 것 같다.

 

참고로 버그를 하나 발견했는데, Customizer 윈도우를 띄우고 나서 플레이를 누른다음에 창을 닫으면 Customizer 씬이 없어지지 않는다. 아마도 플레이 동작에서 씬에 대한 레퍼런스를 하나 들고 있는 것이 아닌가 싶다.

UE4 용 VS 2015 프로젝트를 생성하면 아래와 같은 에러가 발생한 후에 build 가 제대로 안 되는 경우가 있습니다.

 

 

위에서 10.0.10586.0 버전의 ucrt 를 사용하는 것을 알 수 있습니다. 하지만 실제 해당 디렉토리에는 ucrt 가 생성되어 있지 않습니다.

 

그래서 Windows Software Development Kit - Windows 10.0.10586.0 을 설치해 봤습니다. 하지만 여전히 ucrt 는 생성되지 않습니다. Visual Studio 설치 프로세스를 통해 설치하면 위의 에러는 나지 않는데, 이상하게도 VS 2015 가 솔루션을 로드한 다음에 조금 있다가 크래쉬가 납니다. 결국 VS 를 다시 설치했습니다. 이건 순서가 어땠는지 헷갈리네요.

 

그래서 실제 VS 의 lib-path 는 어떻게 되어 있는지 확인해 봤습니다

 

 

그것은 10.0.10240.0 버전이었습니다. 그래서 [ [ 번역 ] Universal CRT 소개 ] 에서 언급한 것처럼 VC++ props 에 $(UniversalCRT_IncludePath) 등을 추가해 봤으나 아무런 소용이 없었습니다.

 

결국에는 UnrealBuildTool 에서 잘못된 lib-path 를 가지고 프로젝트를 생성한다는 결론을 내릴 수밖에 없었습니다. 아마도 10586 과 관련한 path 가 최신의 path 이기 때문에 그것을 지정하는 것이 아닌가 생각합니다. 이것을 수정하고 싶지만 귀찮아서 그냥 꽁수로 해결하기로 했습니다.

 

그래서 10240 버전이 아닌 path 들이 지정되는 것을 막기 위해서 다음과 같은 프로그램들을 제거했습니다.

 

 

이제 제대로 빌드가 됩니다. 회사에서는 잘 되었었는데, 집에서는 안 된 것은 WDK 를 최신버전으로 설치했느냐 그렇지 않느냐의 차이인 것 같네요.

 

어쨌든 결론은 Windows Kits 디렉토리에 설치되는 Windows Software Development Kit 나 Windows Driver Kit 를 같은 버전으로 유지해야 한다는 것입니다. 아니면 UnrealBuildTool 이 제대로 설치된 ucrt 경로를 찾아 주든가요... 아마 후자를 기대하기는 힘들 것 같습니다.

 

주의 : 공부하면서 정리한 것이라 잘못된 내용이 있을 수 있습니다.


이 글은 [ UE4 캐릭터 이동 시스템 가이드 ]의 보충 문서입니다. 


UCharacterMovementComponent 의 속성은 여러 개의 카테고리로 나뉘는데, 이 문서에서는 "MovementMode" 카테고리에 대해서 다룹니다. 말 그대로 걷거나 달리는 것과 관련한 행위를 제어합니다.


이것과 관련한 속성으로는 다음과 같은 것들이 있습니다( Desc 항목은 주석을 그대로 옮겨 놓은 것입니다 ).


 Property

 Specifier

 Desc

 MovementMode

 BlueprintReadOnly

 액터의 현재 이동 모드( walking, falling 등 ).

  - walking: 표면 위를 걷는데, 마찰력 효과를 가지며, 방해물을 걸어 올라갈 수 있음. 수직 속력은 0 임.

  - falling: 중력 효과를 받아 떨어지는데, 점프하거나 표면의 가장자리에서 발이 떨어지는 상황임.

 - flying: 중력 효과를 무시하고 날아감.

 - swimming: 유체 덩어리를 통해서 수영하고 있는데, 중력 효과와 부심( bouyancy ) 효과의 영향을 받음.

 - custom: 사용자 정의 커스텀 이동 모드. 매우 많은 서브 모드를 포함할 수 있음.

 이는 Character owner 를 통해 그리고 client-server 이동 함수들을 위해 자동으로 복제된다.

 CustomMovementMode

 BlueprintReadOnly

 MovementMode 가 Custom 일 때 현재의 커스텀 서브 모드를 지정함.

 이는 Character owner 를 통해 그리고 client-server 이동 함수들을 위해 자동으로 복제된다.


주석 자체가 매우 많은 설명을 담고 있기 때문에 뭐라 더 설명할 것이 없을 것 같습니다.


요는 정해진 4 개의 이동 모드가 있고, 사용자가 원한다면 자신만의 커스텀 모드를 설정할 수 있다는 것입니다. 예를 들어 벽을 기어 오르는 모드라든가 매달리는 모드같은 것을 생각해 볼 수 있습니다. 매달리는 상황에서는 매달리는 손들을 기준으로 진자운동을 하며 ( 허리 이하의 ) 일부 본들은 랙돌 상태의 영향을 받는 것이 좋겠죠. 중력을 계산할 필요는 없을 것이구요. 이는 분명히 기존의 4 개의 이동 모드의 동작과는 다릅니다.


각각의 모드는 제약( constraint )를 가집니다. 만약 여러분이 RootMotion 을 만들었는데, 그 중에 z 축 방향으로 점프하는 동작이 포함되었다고 합시다. 그리고 이를 그냥 재생해 봅니다. 그러면 이상하게도 위로는 올라가지 않고 제자리에서 깔짝대기만 합니다. 이는 walking 모드에서는 z 축 성분을 무시하기 때문입니다( 정확하게는 애니메이션의 z 축 성분을 무시하는 것이 아니라 컨트롤러의 z 축 성분을 무시하는 것입니다 ).


RootMotion 이 아닐 때는 위로 점프하는 것이 제대로 동작할 것입니다. 이는 RootMotion 이 아닌 경우에는 컨트롤러가 바닥에 붙어 있기 때문입니다. 위의 표에서 walking 의 설명을 보십시오. "수직 속력이 0" 이라고 되어 있고, "표면위를 걷는다"고 되어 있습니다. 이러한 이동 모드의 특징을 제대로 이해해야 원하는 동작을 제대로 구현할 수 있을 것입니다.


이동 모드를 변경하는 것은 간단합니다. UCharacterMovementComponent 에는 SetMovementMode() 라는 메서드가 있습니다. 캐릭터에서 Jump() 같은 메서드를 호출하는 것은 내부적으로 이동모드의 변경을 내포하고 있습니다.



MovementMode 속성과 CustomMovementMode 속성을 설정할 수 있는 메서드입니다.

주의 : 다른 방법으로도 만들 수 있으니 이 자료를 맹신하지 마시기 바랍니다.

주의 : 공부하면서 정리한 것이기 때문에 잘못된 내용이 있을 수 있습니다.

주의 : Max 2013 과 2015 에서 테스트했습니다.

주의 : 맥스 스크립트는 이번에 처음 만들어 봅니다. 이상한 부분이 있을 수 있으니 양해 부탁드리고 문제점이 있으면 지적해 주십시오.


개요



언리얼의 [ 루트 모션 ]은 "Root" 라는 이름을 가진 본이 스켈레톤 구조의 최상위에 위치할 것을 요구합니다. 


그런데 우리 나라의 경우에는 Biped 만을 사용하기 때문에 최상위 본은 항상 "Bip01" 입니다. 애니메이터에게 물어 봤더니 Biped layer 같은 것을 사용하려면 Bip01 본 상위에 뭔가 붙여서는 안 된다고 하더군요. 


외국물 좀 먹은 다른 애니메이터에게 물어 봤더니 쉐도우 리깅이나 스크립트를 사용해서 루트 모션을 만들어 낸다고 합니다. 그리고 그 애니메이터의 경우에는 Biped 를 사용하지 않아도 애니메이션 만들 방법은 많기 때문에 굳이 그런 고민을 안 한다고 합니다( 그리고 마야를 많이 쓴다고 하더군요 ).


애니메이터가 Biped 에서 루트 모션을 만드는 과정을 관찰했더니 이건 완전히 생노가다였습니다. 그런데 우리나라 애니메이터들은 Biped 이외에는 사용할 생각이 없기 때문에, 이를 맥스스크립트로 자동화시켜야겠다는 생각을 했습니다.


맥스 스크립트



루트 모션을 만드는 것을 관찰했더니 순서는 [ 다음 ] 과 같았습니다( 링크의 문서는 우리 회사 캐릭터 팀장이 작성했습니다. 물론 위에서 언급했듯이 이게 루트 모션을 만드는 유일한 방법은 아닙니다  ).


저는 그 과정을 보고 최종적으로 "Root 본이 Bip01 의 TM 을 따라 가고, 반대로 Bip01 은 Root 본에 종속되어 상대적인 TM 을 가지면 된다" 라는 결론을 내렸습니다. 그래서 다음과 같은 알고리즘을 세웠습니다.


  1. 제거할 축 입력받음.
  2. "Root" 본 생성.
  3. "Bip01" 본 획득.
  4. Root 본에 Position_XYZ 컨트롤러 바인딩하고 프레임 개수만큼 키를 생성.
  5. Bip01 을 따라 가는 Position_Constraint 컨트롤러 "tempController" 생성.
  6. 전체 프레임을 돌면서 "tempController" 값을 Root 본에 복사함( Bip01 을 복사하는 것과 동일함 ).
  7. Biped 에 "RootMotionLayer" 레이어를 생성하고 현재 레이어로 설정함.
  8. 새로운 레이어의 position 컨트롤러를 획득해서 전체 프레임 개수만큼 키를 생성.
  9. 새로운 레이어의 키의 position 에 "tempController" 컨트롤러에서 특정 축값을 제거한 값을 삽입.
  10. Root 본을 Bip01 의 부모로 설정.
  11. FBX 로 익스포트( 수동 ).
  12. 익스포트 후에 "RootMotionLayer" 레이어 및 "Root" 본 제거( 수동 ).


5 ~ 10 작업은 Bip01 이 Root 에 대해 상대적인 TM 을 가지도록 블렌딩해 주는 작업입니다. 제가 Biped 에 대해서 잘 모르기 때문에 자세한 것은 설명해 드리기 힘들고, 궁금한 것은 자기 팀 애니메이터에게 문의하시면 될 것 같습니다. 



스크립트는 다음과 같습니다. 수정 : Biped Root 의 이름을 입력받을 수 있게 했습니다. 수정 : minZ 와 maxZ 를 입력받을 수 있게 했습니다. 수정 : 바닥이 될 본을 설정할 수 있게 했습니다. 이는 RemoveZ 가 꺼졌을 때만 활성화됩니다.



한계 및 할 일들



애니메이터의 이야기를 들어 보니 점프를 하거나 비대칭적인 캐릭터( 예를 들어 기린? )를 작업할 때는 직접 Dummy 를 만들어서 컨트롤러의 위치를 지정한다고 합니다. 그러므로 그런 경우에는 Root 본이 Bip01 이 아니라 Dummy 를 따라 가도록 만들어야 할 것입니다. 다이얼로그에 그런 옵션들을 추가로 넣고 코드를 수정할 수 있을 것 같습니다. 


그리고 알고리즘의 11 번이나 12 번을 자동화할 필요도 있을 것 같습니다. 아니면 차라리 새로운 파일을 자동으로 만들어서 거기에서 작업이 수행되도록 할 수도 있겠죠.


그리고 현재 스크립트는 항상 position 만을 처리하고 있는데, rotation 이나 scaling 을 처리해야 할 수도 있습니다.


수정 : 그리고 공중회전같은 것을 하면 현재 스크립트의 결과물은 이상해질 것입니다. 이럴 경우에는 다른 방법을 찾아야 합니다.


기회가 되면 루트 모션을 만드는 여러 가지 방법들에 대해서 연구해 봐야할 것 같습니다.

+ Recent posts