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 번을 지정했는데, 만약 통합된 내장 그래픽 카드가 있는 상태라면 다른 번호를 지정해야 할 수도 있습니다.



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

개요



이 문서에서는 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.

 

어느 정도 깊이로 분석할지는 모르겠지만, 최선을 다해 볼 생각입니다.

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 을 처리해야 할 수도 있습니다.


수정 : 그리고 공중회전같은 것을 하면 현재 스크립트의 결과물은 이상해질 것입니다. 이럴 경우에는 다른 방법을 찾아야 합니다.


기회가 되면 루트 모션을 만드는 여러 가지 방법들에 대해서 연구해 봐야할 것 같습니다.

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


개요



언리얼 엔진에서 캐릭터를 다루기 위해서 알아야 할 가장 중요한 컴포넌트가 무엇이냐고 질문을 하신다면, 저는 "가장 중요한 것은 UCharacterMovementComponent 입니다" 라고 대답을 할 것입니다.


대부분의 사람들이 이 컴포넌트를 아무 생각없이 사용하고 있지만 이 컴포넌트의 동작을 제대로 이해하지 못한다면 많은 문제에 부딪히게 됩니다. 


튜토리얼같은 것들을 따라 할때는 제한된 조건에서 캐릭터를 움직이기 때문에 이것을 이해해야 할 필요성을 별로 느끼지 못합니다. 하지만 복합적인 상황을 고려하면 반드시 이해해야만 합니다.


몇 가지 간단한 예를 들어 보도록 하겠습니다.

    • RootMotion 을 실행하고 있을 경우에 ACharacter::LaunchCharacter() 류의 메서드 호출이 제대로 동작하지 않습니다.
    • AIController 를 연결했을 경우에 AController::SetControlRotation() 류의 메서드 호출이 제대로 동작하지 않습니다.


결국 조금만 복잡한 상황을 처리하거나 깊이 들어 가면 CharacterMovement 컴포넌트의 동작을 제대로 이해하지 않고서는 작업을 진행하기 어려운 상태가 된다는 것입니다. 그리고 이는 작업의 방향에도 큰 영향을 끼칩니다.


위에서 예로 들었던 RootMotion 을 생각해 봅시다. 만약 idle 이나 walk 를 RootMotion 으로 만들었다면, 플레이어 캐릭터가 몹을 때렸을 때 밀려나게 만들 수 없을 것입니다. 그러므로 그런 상황을 가정했다면 idel 이나 walk 를 RootMotion 이 아니도록 만들어야만 합니다. 아니면 자신만의 CharacterMovement 컴포넌트를 구현해야합니다.


이러한 문제점들을 해결하기 위한 목적 이외에도, CharacterMovement 컴포넌트는 RVO Avoidance System, Navigation System, AI system, Animation System, Physics System, Input System 등과 연결되어 있기 때문에 잘 이해해야 할 시스템입니다.


저도 이 컴포넌트에 대한 이해없이 그냥 예제 보면서 따라하다가 무의미하게 시간만 낭비했습니다. 그래서 이 컴포넌트에 대한 이해도를 높여 보려는 목적으로 이 문서를 작성하게 되었습니다. 부족함이 많은 글이겠지만, 모쪼록 처음 접하는 사람들에게 도움이 되었으면 합니다.


이 문서에서는 모든 요소들에 대해서 세부적으로 다루지는 않습니다. 코드를 분석하는데 도움이 될 수 있는 가이드를 제공할 뿐입니다. 그리고 여기에서는 standalone 으로 플레이한다는 가정을 깔고 있습니다. Client-Server 모델에서는 약간 동작이 다른데 그 부분은 알아서 분석하시기 바랍니다.


다음과 같은 참고 문서들을 확인하시기 바랍니다.


기본 : 속도와 가속도



플레이어가 1초 후에 X 축으로 1 미터 앞에 있기를 원한다고 합시다. 그러면 이 목표를 달성하기 위한 메서드를 만들어야 합니다. 간단하게 생각하면 UCharacterMovement::SetLocation( const FVector& NewLocation, const float Time ) 과 같은 메서드를 만들 수 있습니다. 이것을 기획자가 사용한다면 매우 멋진 인터페이스가 될 것입니다.


하지만 이것을 실제 구현하고 있다고 생각해 봅시다. 순간이동이 아니기 때문에 0 초와 1 초 사이의 무수히( ? ) 많은 프레임에서의 위치를 계산해야 합니다. 등속도 운동을 해야 할까요? 아니면 등가속도 운동을 해야 할까요?


혹은 중간에 뭔가 장애물이 있다고 합시다. 이것을 어떻게 처리해야 할까요?


결국 월드와의 상호작용을 고려한다면 일관된 법칙을 가질 필요가 있습니다. 이미 PhysicsX 라는 물리 엔진을 기반으로 깔고 가고 있기 때문에 이에 맞추는 것이 여러모로 편할 것입니다. 


CharaterMovement 컴포넌트의 기본은 velocity( 속도 ) 와 acceleration( 가속도 ) 을 제어하는데 있습니다. 최종적으로 이를 통해서 캐릭터의 Location, Rotation 이 결정됩니다. 그리고 수많은 시스템들이 속도와 가속도를 결정하는데 영향을 주게 됩니다.


기억이 잘 안 나는 분들이 있을 것이기 때문에 기본적인 뉴턴 운동법칙에 대해서 복습해 보도록 하겠습니다. 뉴턴 운동법칙은 크게 3 가지 법칙으로 나뉩니다.

  • 1법칙 : 관성의 법칙.
  • 2법칙 : 가속도의 법칙.
  • 3법칙 : 작용과 반작용의 법칙.


우리가 주목할 것은 가속도의 법칙입니다. 그 정의는 다음과 같습니다.


물체의 운동량의 시간에 따른 변화율은 그 물체에 작용하는 알짜힘과 ( 크기와 방향에 있어서 ) 같다.


출처 : 뉴턴 운동 법칙, 위키피디아.


이를 수식으로 쓰면 다음과 같습니다. 여기에서 F 는 알짜힘, P( = mv ) 는 운동량, m 은 질량, v 는 속도, a 는 가속도입니다.




일반적으로 질량이라는 것이 시간에 따라 변하지 않으므로 다음과 같이 식을 작성할 수 있습니다.




이제 우리가 어떤 물체를 움직이는 함수를 작성한다고 해 봅시다. 그러면 하나의 값을 사용해서 물체를 움직일 수 있습니다. 그것은 "힘"입니다.



UCharacterMovementComponent::AddForce() 메서드의 7 라인에서 Force 를 Mass 로 나누는 것을 볼 수 있습니다. 결국 PendingForceToApply 라는 것은 가속도를 의미하게 됩니다.


이제 실제 이 가속도는 어디에서 사용되는 것일까요? 다음과 같은 호출순서에 의해 가속도가 처리됩니다.



그림 1


3 번 호출의 내용을 보면 다음과 같습니다.



12 라인의 PendingForceToApply * DeltaSeconds 를 통해 Velocity 를 얻어 내고 있습니다. 가속도라는 것은 초당 속도의 변화량이기 때문에 DeltaSeconds 를 곱하는 것이죠. 만약 가속도가 100 이라면 1 초 후에는 속도가 100 이 증가되기를 원한다는 의미가 됩니다. 그러므로 0.1 초 동안 증가한 속도는 10 입니다( 이 설명은 왜 하고 있는지 ㅡㅡ;; ).


어쨌든 이런 식으로 캐릭터의 이동을 처리하게 됩니다. 보통 입력시스템과 관련한 플루프린트에서는 AddForce() 를 호출하게 됩니다.


본 주제로 돌아 가서 1초 동안 1 미터를 이동하려면 어떻게 해야 할까요? 이 경우에는 복잡한 계산이 필요합니다. 보통 캐릭터 입력은 등가속도 운동과 등속도 운동이 모두 포함되기 때문에 단순하게 계산하기 어렵습니다( 미적분이 필요합니다 ). 그러므로 이는 기획자가 사용하기에는 적절하지 않습니다. 프로그래머나 물리를 좀 아는 기획자가 미리 테이블을 만들어서 전달해 줄 필요가 있습니다. 아니면 기획자가 감으로 해 보는 수밖에 없습니다.


여기에서는 설명을 단순화하기 위해서 마치 속도가 한 메서드 내부에서만 계산되는 것처럼 설명했습니다. 하지만 실제로는 매우 많은 메서드 내에서 이 속도를 제어하게 됩니다. 앞에서도 언급했듯이 많은 다양한 시스템들의 영향을 받게 됩니다.


UpdatedComponent



CharaterMovement 컴포넌트가 실제로 제어하는 것은 어떤 요소일까요?


Character 의 경우에는 자신의 RootComponent 를 CharacterMovement 컴포넌트가 제어해야 하는 요소로 지정하고 있습니다.



위의 생성자의 13 라인을 보면 UpdatedComponent 에 CapsuleComponent 를 할당하고 있는 것을 볼 수 있습니다. 즉 CharacterMovement 컴포넌트는 루트 컴포넌트를 제어하게 된다는 의미입니다.


이제 CharacterMovement 컴포넌트 내에서는 UpdatedComponent 에 접근해서 이런 저런 일들을 수행하게 됩니다. 예를 들어 다음과 같은 메서드가 있습니다( UCharacterMovementComponent 는 궁극적으로 UMovementComponent 를 상속하고 있습니다 ).



PerformMovement



캐릭터의 최종 위치를 결정하는 핵심 메서드는 UCharacterMovementComponent::PerformMovement() 입니다. 여기에서는 여러 가지 입력을 평가해서 UpdatedComponent 에 그 결과를 반영합니다.


그림 2


전체적으로 크게 이동 처리와 회전 처리로 구분됩니다. 그림 2 의 3, 4, 5, 6 은 상황에 맞게 여러 가지 입력으로부터 속도를 계산합니다. 그리고 7 은 현재 상황( walking, jumping )에 맞게 이동 계산을 수행합니다. 그리고 8, 9 에서는 회전 계산을 수행합니다. 그리고 나서 10, 11 에서 이벤트를 처리합니다.


StartNewPhysics() 에서는 MovementMode 에 따라 다음과 같이 호출을 분기시켜 줍니다.


 MovementMode

 관련 Method

 비고

 MOVE_None

 

 

 MOVE_Walking

 PhysWalking()

 

 MOVE_NavWaling

 PhysNavWalking()

 

 MOVE_Falling

 PhysFalling()

 Jump 및 Launch 에 의해 실행됨

 MOVE_Flying

 PhysFlying()

 

 MOVE_Swimming

 PhysSwimming()

 

 MOVE_Custom

 PhysCustom()

 


RootMotion



RootMotion 과 CharacterMovement 컴포넌트의 관계를 잘 이해할 필요가 있습니다.


RootMotion 이라는 것은 애니메이션의 Root 본을 기준으로 캐릭터를 제어하는 모드를 의미합니다. 자세한 내용은 언리얼의 [ Root Motion ] 을 참고하시면 됩니다( 원래는 한국어 번역도 있었던 것 같은데, 지금은 한국어만 안 되네요 ).


RootMotion 모드일 때 캐릭터의 위치을 애니메이션 기준으로 제어해야 하기 때문에, 그림 2 의 5, 6 을 통해서 속도를 계산합니다. 


SkeletalMesh 의 pose 는 USkeletalMeshComponent::TickPose() 에 의해 결정됩니다. 그런데 일반적으로는 이것이 USkinnedMeshComponent::TickComponent() 에 의해 호출되지만, RootMotion 일 때는 UCharacterMovementComponent::TickComponent() 에 의해 호출된다는 차이가 있습니다. 이는 원하는 시점에 정확하게 애니메이션을 시뮬레이션하기 위함이 아닌가 싶습니다.


RootMotion 의 경우에 5, 6 을 처리한다는 것은, 캐릭터에 대한 입력이 애니메이션인 경우라고 생각하시면 될 것 같습니다.


AI



아래 블루프린트는 캐릭터를 초당 한 번씩 회전시키려는 의도로 만들어졌습니다.



만약 AIController 를 사용하는 것이 아니라 일반 PlayerController 를 사용하고 있다면 위의 코드는 정상적으로 동작합니다. 그런데 AIController 만 붙이면 해당 호출이 무시됩니다.


그 이유를 구현 측면에서 좀더 파 보자면, AAIController::UpdateControlRotation() 에서 AAIController::SetControlRotation() 를 호출하고 있기 때문입니다. 그래서 블루프린트 내의 호출이 무시되는 것입니다.



AIController 는 FocalPoint 나 FocusActor 로부터 회전을 산출하도록 설계되어 있기 때문에, 이런 문제가 발생하는 것입니다. 결국 사용자가 각 컨트롤러의 구현 방식을 이해하지 못했다면 CharacterMovement 컴포넌트의 제어에 어려움이 발생합니다.


결론



CharacterMovementController 는 기본적으로 속도를 계산해 UpdateComponent 에 반영해 주는 일을 수행합니다. 그 과정에서 여러 요소들이 간섭을 하게 됩니다.


이 문서는 그러한 모든 요소들에 대해 설명하고 있지는 않습니다. 하지만 캐릭터 이동에 대해 제대로 이해하기 위해서는 이 컴포넌트와 다른 요소들이 어떻게 상호작용하는지 이해할 필요가 있습니다.


이 문서를 통해 캐릭터 이동이나 회전이 우리의 의도와는 다르게 동작할 수 있다는 점을 이해하고 그러한 경우에 어떻게 대처해야 하는지를 이해할 수 있다면, 이 문서를 작성한 의도의 대부분은 달성한 것이 아닐까 싶습니다.


시간이 되면( ? ), 다음에는 CharacterMovement 컴포넌트가 다른 요소들과 어떤 식으로 상호작용하는지에 대한 내용을 다뤄 볼 계획입니다.

개요



언리얼 엔진을 커스터마이징하거나 플러그인같은 것들을 제작하기 위해, 가장 먼저 이해해야 할 것은 언리얼의 빌드 시스템입니다.


물론 언리얼 엔진의 공식 문서에는 [ 언리얼 빌드 시스템 ] 이라는 항목이 존재하지만, 각 주제들이 개별적으로 설명되고 있기 때문에, 처음 접하는 입장에서는 종합적으로 이해하기 어렵습니다.


저도 이런 부분때문에 고생을 많이 했고, 이번 기회에 제대로 정리를 해 볼 생각입니다. 모쪼록 이 문서가 많은 사람들에게 도움이 되었으면 합니다.


이 문서에서는 빌드시스템의 각 항목에 대한 세부적인 내용을 다루기 보다는, 전체적인 윤곽을 보여 주고 문제를 해결하기 위해서 어떤 문서를 찾아야 하는지에 대한 가이드를 제공하려고 합니다.


여러분이 이 문서를 읽고 나면, 다음과 같은 것들을 할 수 있어야 합니다. 만약 그렇지 못하다면 제가 글을 잘 못쓰거나 누락한 부분이 있는 거겠죠.


  • 커스텀 빌드를 배포하기 위해서 소스 제어에 어떠한 것들을 등록해야 하는지 알 수 있습니다.
  • GenerateProjectFiles.bat 를 어떠한 상황에서 사용하는지 알 수 있습니다.
  • 언리얼 엔진의 프로젝트 구성에 대해 이해할 수 있습니다.
  • 모듈의 의미를 이해하고 사용할 수 있습니다.
  • 언리얼 엔진을 수정하고 배포할 수 있습니다.


이 문서는 "UnrealEngine 4.10.0-release" 와 윈도우즈 시스템을 기반으로 작성되었습니다.


참고로 이 문서에서는 자동화와 배포( automation & depolyment )에 대해서는 다루지 않습니다. 이건 빌드시스템과는 좀 다른 영역이라 나중에 기회가 되면 다루도록 하겠습니다.


공부하면서 정리한 거라서 잘못된 내용이 있을 수 있으므로, 이를 감안하고 보시기 바랍니다. 잘못된 내용들은 수정해 가도록 하겠습니다.


1. 소스 제어 - 기본 데이터 등록



언리얼 엔진의 소스를 받는 방법은 [ 언리얼 엔진 소스 코드 내려받기 ]에 나와 있습니다. 그런데 언리얼 엔진의 소스를 받고 나면 어떠한 것들을 소스 컨트롤에 등록해야 하는지 알기 어렵습니다. 어떤 파일들이 공유되어야 하고 어떤 파일들이 개인용 파일들인지 모르기 때문입니다. 뭘 올려야 하는지에 대한 설명도 제대로 없습니다.


언리얼 엔진의 소스를 받으면 다음과 같은 파일들이 생성됩니다.각각의 항목에 대한 자세한 내용은 [ 디렉토리 구조 ] 문서에 잘 나와 있습니다.



일단 소스를 받은 상황에서 아무 것도 하지 마시고, 1 번 항목을 제외하고는 통째로 소스 제어에 등록하시면 됩니다. 저는 git 을 안 쓰기 때문에 1번 항목을 지워버렸습니다.


2. Setup.bat



이제 Setup.bat 를 실행해서 의존성 파일들을 설치합니다. 1 번을 통해 소스 제어에 등록된 파일들을 다운로드받은 ( 프로그래머가 아닌 ) 작업자들도 반드시 이를 실행해야만 합니다.


나중에 자동화와 배포에 대해서 정리할 기회가 있으면 그 때 다시 언급하겠지만, 만약 작업자 머신에서 이 배치파일이 실행된 적이 없다면, 프로그래머가 열심히 바이너리를 배포해 놓아도 에디터에서 안드로이드같은 디바이스로 프로그램을 런칭시킬 수 없습니다( 물론 "Engine/Extras/AndroidWorks/Win64/AndroidWorks-1R1-windows.exe" 도 깔려 있어야겠죠. 여기에서는 자동화와 배포에 대한 주제를 다루지 않기 때문에 구체적인 언급은 피하겠습니다 ).


3. 소스 제어 - 의존성 파일 등록



이 과정을 통해서 생성된 파일들을 소스 제어에 등록하는 것은 취향( ? )의 문제로 보입니다. 만약 언리얼 빌드 시스템에 손을 댈 계획이라면 소스제어에 등록해야 하지만, 그렇지 않다면 등록할 필요가 없습니다.


저같은 경우에는, 언리얼 빌드 시스템에 손을 댈 계획이 없지만, 그래도 두 가지 이유 때문에 의존성 파일들을 소스 제어에 등록해 둡니다.

  • Setup.bat 는 의존성 파일들을 먼저 검색하고 없는 파일만 추가적으로 다운로드합니다. 일반적으로 외부에서 데이터를 받는 것보다는 내부에서 받는 것이 빠르기 때문에 미리 올려 놓는 것이 좋습니다. Setup.bat 를 실행했을 때 다운로드받는 양은 적어지고 필요한 작업만 수행합니다.
  • 나중에 엔진을 빌드하고 나면 의존성 파일들이 있는 디렉토리와 엔진 바이너리 파일들이 있는 디렉토리가 동일하기 때문에, 파일을 개별적으로 관리하기가 귀찮아집니다. 그냥 통째로 "Engine/Binaries" 폴더를 등록하는 것이 편합니다( 의존성 파일들이 해당 디렉토리에만 있는 의미는 아니니 헷갈리지 않길 바랍니다 ).


참고로 다른 자리에서 Setup.exe 를 실행하면 콘솔 프롬프트에 일부 파일을 덮어 쓸 것이냐는 질문을 하는데, 이 때 No 를 선택하시면 됩니다.


만약 의존성 파일을 등록해야겠다고 판단하셨다면, 새롭게 추가된 파일들을 모두 등록하시면 됩니다( 여기까지는 그냥 엔진 루트 디렉토리를 통째로 등록하면 됩니다 ).


4. GenerateProjectFiles.bat



이제 프로젝트 관련 파일들을 생성할 차례입니다. 이 파일은 프로그래머만 실행하시면 됩니다. 이 파일을 실행하게 되면 언리얼 엔진 소스 폴더를 열심히 돌면서 자동으로 프로젝트 파일을 생성합니다. 이와 관련한 도움말은 [ 자동 프로젝트 파일 생성 ] 이 있습니다.


요즘은 멀티 플랫폼에서 개발하는 시대이기 때문에, 특정 버전의 프로젝트 파일을 제공하는 것은 불합리합니다. 물론 make 파일을 사용하면 되지만, 이를 구성별로 사용자가 따로 관리하는 것은 어렵습니다. 그래서 언리얼은 이러한 과정을 모두 자동화했습니다. 안 그랬으면 UE42013.sln, UE42015.sln 같은 파일들을 만들어서 배포해야겠죠. 좀 더 나가면 UE4Editor2015.sln 같은 솔루션을 배포해야 할 수도 있었습니다.


제가 이전에 프로젝트를 진행하면서 그런 짓을 좀 해 봤는데, 정말 할 짓이 아닙니다. 파일 하나를 추가하면 솔루션이나 프로젝트를 열어서 일일이 추가해 줘야 하고, 삭제나 이동도 마찬가지입니다.


언리얼은 이 과정을 자동화시켜 사용하기 편하게 만들었습니다. 어떤 파일이 추가/삭제/이동되면 그냥 GenerateProjectFiles.bat 을 한 번 실행해 주기만 하면 됩니다. 프로젝트 관련 파일들은 Intermediate 디렉토리에 생성되며, 이러한 것들은 소스 제어에 등록될 필요가 없습니다. VS 에서 파일 구조를 filter 같은 것을 사용해서 관리하기 보다는, 실제 디렉토리를 중심으로 filter 가 생성되도록 하는 것이 더 합리적이고 파일을 찾기도 편합니다.


그런데 가끔 이 파일을 실행하면 아주 오랫 동안 아무 반응이 없을 때가 있습니다( 요즘 머신에서 15 초 넘어 가면 문제가 있습니다 ). 반응이 없어서 콘솔을 닫고 파일을 재실행하면 UnrealBuildTool.exe 가 사용중이라서 어쩌고 하면서 에러가 뜹니다. 이 경우에는 컴퓨터를 재시작하시고 Avast 같은 안티 바이러스 도구를 일시적으로 정지시키십시오. 엔진 폴더를 검사에서 배제하는 방법을 써 봤지만 소용이 없더군요. 그리고 나서 실행하시면 됩니다( 예전에는 디렉토리나 UnrealBuildTool.exe 파일을 배제하는 것만으로 됐던 것 같은데, 왜 그런지 모르겠네요. Windows 10 64bits 입니다 ). 추가 : Avast 에서 DeepScreen 기능을 아예 꺼버리니 제대로 동작합니다.


추가 : GenerateProjectFiles 를 실행하면 노란색으로 ucrt 경로 관련 경고가 출력될 경우가 있습니다. 이 경우에는 빌드도 제대로 안 되므로 반드시 해결해야 합니다. 그 방법은 [ UE4 빌드시에 ucrt 경로 충돌 문제 해결방법 ]에 정리해 뒀습니다.

 

이제 엔진 루트에 UE4.sln 이라는 솔루션 파일이 생성되어 있는 것을 확인하실 수 있습니다. 프로젝트 관련 ( 임시 ) 파일들은 "Engine/Intermediate" 디렉토리 안에 생성됩니다. 이 디렉토리는 소스제어에 등록할 필요가 없습니다. 참고로 솔루션 파일도 재생성할 수 있으므로 소스 제어에 등록할 필요가 없습니다.


5. 엔진 빌드하기



이제 엔진을 빌드해 줄 차례입니다. 언리얼 엔진을 빌드하려고 하면서 가장 어려웠던 점은 솔루션 구성을 이해하는 것입니다. 엄청나게 많은 구성들이 존재하기 때문에 머리가 엄청나게 복잡해집니다. 이와 관련한 문서는 [ 게임 프로젝트 컴파일하기 ] 와 [ 소스에서 언리얼 엔진 빌드하기 ] 등이 있습니다.



구성은 크게 5 종류의 카테고리로 나뉩니다. 각 항목은 다음과 같은 의미를 가집니다.


 항목

 설명

 Debug

 모든 프로젝트의 바이너리가 디버깅 심볼을 가지도록 합니다.

 DebugGame

 게임 프로젝트의 바이너리만 디버깅 심볼을 가지도록 합니다.

 Development

 일반적인 프로젝트에서의 Release 환경설정과 같습니다.

 Test

 Shipping 에서 콘솔, 통계, 프로우파일링을 추가한 것입니다.

 Shipping

 최상의 성능을 가진 설정입니다.


자 이제 우리는 어떠한 구성을 빌드해서 나오는 바이너리를 사용자에게 제공해야 하는걸까요? 도움말에서는 "DevelopmentEditor + Win64 + UE4" 를 빌드할 것을 권장하고 있습니다( 빌드하는데 상당히 오랜 시간이 걸리므로, 빌드를 걸어 놓고 그냥 딴 일을 하시기 바랍니다 ).


빌드할 때도 "Performing full C++ include scan (no include cache file)" 이라는 로그 이후에 오랫동안 아무런 반응이 없으면, Avast 같은 안티 바이러스 도구를 일시적으로 정지시켜야 합니다.


빌드가 끝나면 에디터를 실행하는 데 전혀 문제가 없습니다( 커스텀 혹은 인하우스 빌드는 런처를 통해서 실행할 수 없습니다, 엔진 루트의 "Engine/Binaries/Win64/UE4Editor.exe" 를 실행해야 합니다 ). 원래 에디터를 처음 실행할 때는 45% 정도에서 오래 멈춰 있으니 성급하게 끄지 말아 주세요.


그런데 에디터를 잘 가지고 놀다가 플레이( Play In Editor, PIE )를 하는 것은 상관없는데, 실행( Launch )를 하면 에러가 발생합니다.



이런 류의 에러는 앞으로 안드로이드용 개발을 하다가도 자주 만나게 되는 에러입니다. 


XXXEditor 구성을 사용하면, "UE4Editor-$(Platform)-$(Configuration).exe" 라는 바이너리를 생성합니다. 하지만 DevelopmentEditor 구성에서는 그냥 "UE4Editor.exe" 를 생성합니다.


그리고 Debug, DebugGame, Development, Test, Shipping 은 각각의 구성에 맞게 "UE4Game-$(Platform)-$(Configuration).exe" 라는 바이너리를 생성합니다. 이것이 하나의 에디터에서 다양한 플랫폼의 실행 파일을 런칭할 수 있는 비법( ? )입니다.


DevelopmentEditor 구성으로 UE4Editor.exe 를 빌드하면 기본적으로 UE4Game-Win64-Development.exe 를 런칭합니다. 다른 구성으로 빌드된 바이너리를 런칭하기 위해서는 "실행 > 프로젝트 런처" 를 클릭합니다.



그리고 나서 나오는 다이얼로그에서 오른쪽 상단의 "고급" 버튼을 클릭합니다. 그러면 각 구성과 플랫폼별로 런칭할 수 있는 구성이 나옵니다.



만약 해당 구성으로 빌드한 적이 없다면 또 "UE4Game 바이너리가 없습니다" 라는 에러 메시지를 만나게 되겠죠.


어쨌든 정리하자면 그냥 개발하는 동안에는 "DevelopmentEditor + Win64 + UE4" 와 "Development + Win64 + UE4" 구성만 빌드하면 됩니다. 만약 에디터에서 안드로이드 플랫폼에 프로젝트를 런칭하기를 원한다면 "Development + Andoroid + UE4" 를 빌드하면 됩니다.


프로젝트 런처를 통해서 실행한 프로그램을 프로세스에 연결해서 디버깅해 보고 싶다면 "Debug + Win64 + UE4" 같은 구성도 미리 빌드해 놓는 것도 좋겠죠. 하지만 이런 구성은 다른 개발자들에게 배포할 필요는 없으므로 소스 제어에는 등록할 필요가 없을 것 같습니다.


6. 소스 제어 - 바이너리 등록



"DevelopmentEditor + Win64 + UE4", "Development + Win64 + UE4", "Development + Android + UE4" 를 빌드했다면, "Engine/Binaries" 디렉토리를 통째로 소스 제어에 등록하시면 됩니다. 물론 3 번에서 의존성 파일을 등록하지 않았다면 그것을 배제하는 작업을 좀 해야겠죠. 추가 : 플러그인들도 빌드되기 때문에 Plugins 디렉토리도 같이 등록해야 합니다. 이 때 각 하위 디렉토리의 Intermediate 디렉토리에 있는 것들은 등록에서 배제해야 합니다.


추가적으로 다른 구성을 등록해야겠다고 생각하시면 그렇게 하셔도 상관은 없습니다. 아래와 같이 플랫폼별로 디렉토리가 세분화되어 있기 때문에 관리하기가 어렵지는 않을 겁니다.



7. 모듈 이해하기



언리얼에서 CPP 프로그래밍을 하다가 보면 모듈( Module )이라는 용어가 자주 등장합니다. 이와 관련해서는 [ 언리얼 아키텍처 ], [ 모듈 API 지정자 ], [ 게임플레이 모듈 ] 등의 도움말이 있지만, 그리 친절하지는 않습니다.

 

제가 대충 정리한 [ 언리얼 엔진 모듈 ] 항목을 참조하십시오. 해당 문서는 새로운 정보를 습득할 때마다 계속 업데이트 됩니다.

이러한 모듈 개념을 잘 이해하고 있어야, 나중에 다른 모듈을 사용하는데 어려움을 겪지 않게 됩니다.


8. CPP 프로젝트 만들기



이제 CPP 프로젝트를 하나 만들어 보겠습니다. 



자 이제 솔루션에는 BlankCpp 라는 프로젝트가 포함됩니다. 어떻게 포함되었을까요? 당연히 내부적으로 GenerateProjectFiles.bat 을 실행시켜서 포함시킨 것입니다


제가 언리얼을 처음 만질 때, 가장 두려웠던 것이 GenerateProjectFiles.bat 을 실행하는 것이었습니다. 혹시 뭔가 데이터가 날라가지 않을까라는 의심이 들었는데, 디렉토리에 파일이 남아 있으면 제대로 프로젝트가 생성되기 때문에 걱정하지 않으셔도 됩니다.


어쨌든 솔루션 익스플로러를 확인해 봅시다.



역시 여기에도 BlankCpp.Build.cs 라는 녀석이 생성되어 있는 것을 볼 수 있습니다. 즉 BlankCpp 모듈인 것입니다.


우리는 UE4 를 이미 빌드했기 때문에, BlankCpp 만 빌드하면 됩니다.



9. 소스제어 - CPP 프로젝트 데이터 등록



BlankCpp 프로젝트를 빌드했으면 이제 소스 제어에 올릴 차례입니다. 프로젝트 관련 파일들은 재생성하면 된다고 했기 때문에 올릴 필요가 없습니다. 그래서 아래 그림과 같이 선택된 디렉토리와 파일만 등록하면 됩니다.


그런데 다른 자리에서는 어떻게 프로젝트 파일을 생성하냐구요? "GenerateProjectFiles.bat BlankCpp.uproject -Game" 이라고 콘솔 창에서 입력하시면 됩니다. 아니면 uproject 를 열어서 에디터에서 다음과 같은 메뉴를 사용하셔도 됩니다.



또는 BlankCpp.uproject 파일의 컨텍스트 메뉴에서 생성할 수 있습니다.



어쨌든 전부다 본질은 GenerateProjectFiles.bat 에 있습니다.



10. 결론



언리얼 엔진의 빌드 시스템에 대해서 이해하면 개발이 편해지지만, 그렇지 못하면 지옥을 겪게 됩니다.


아직 제가 모르는 부분도 많고 자세히 다루지 않은 부분도 있습니다. 단지 이 문서에서는 가이드만 제시하는 것이기 때문에 다른 문서들을 참조하면서 해 보시기 바랍니다.


이 문서에서는 모듈에 대해서만 잠깐 다루고 타깃에 대해서는 다루지 않았는데, "*.Build.cs" 와 마찬가지로 "*.Target.cs" 라는 파일들이 존재합니다. 이건 dll 단위로 존재하게 됩니다. 자세한 내용은 [ 언리얼 빌드 시스템 타겟 파일 ] 도움말에서 찾아 보시기 바랍니다.


추가 : setup.bat 실행 후에 AndroidWorks 를 설치하셨다면, setup.bat 를 다시 실행해 주셔야 합니다.

 

추가 : 혹시 엔진을 커스터마이징해서 배포하는 팀( 혹은 사람 )과 엔진을 받아서 사용하는 팀이 구분되어 있고 서로의 영역을 건드리지 않는다면, [ 빌드 그래프 ] 와 [ Installed Build 참고서 ] 를 참고해서 배포하시기 바랍니다. 그냥 빌드해서 서밋하시면 코드가 변경되었기 때문에 엔진 코드를 수정하면 게임 칸텐츠를 작업하는 측에서 엔진까지 빌드해야 하는 경우가 생깁니다.

언리얼 엔진의 [ 데이터 주도형 게임플레이 요소 ] 항목을 따라 하다가 보면 문제가 발생합니다. 


UE4 에서는 UTF-8 포맷으로 CSV 를 읽고 있기 때문에, 일반적인 방식으로 XLSX 에서 CSV 를 뽑게 되면 한글같은 것은 다 깨져서 나옵니다.


이를 해결하기 위한 일반적인 방법은 다음과 같습니다.


  1. XLS 를 편집.
  2. CSV 로 저장( 이 과정에서 메시지 박스가 자꾸 떠서 불편함 ).
  3. CSV 를 메모장에서 열어 UTF-8 포맷으로 재저장.
  4. 다시 편집할 때는 1 ~ 3 을 반복.


이런 과정은 너무 불편한 데다가 실수로 CSV 만 손대면 XLS 와 내용이 틀려져서 관리가 어렵습니다.


그래서 이를 자동화하는 방법을 고민했고, 이런 저런 자료들을 참고하여 매크로를 만들었습니다: XLSM( 매크로 XLS )사용해서 CSV 를 익스포트할 수 있도록 했습니다.


앞으로 UE4 에서 사용할 엑셀 원본 파일은 아래의 파일을 기반으로 만들면 됩니다.


XlsToCsv.xlsm


여기에서 사용하는 VBAScript 는 다음과 같습니다.



그런데 "Export" 라는 sheet 에만 연결을 해 두었기 때문에, 다른 sheet 에서 작업을 하려면 따로 스크립트를 연결해야 합니다.




무기를 위해서 UStaticMeshComponent 를 상속하는 UStaticWeaponComponent 를 만들었다고 가정합시다.



다음으로 나서 내가 정의한 커스텀 캐릭터의 멤버로 넣습니다.



다음으로 생성자에서 Weapon 컴포넌트를 생성해 줍니다.



그리고 나서 ACustomCharacter 를 가지고 플루프린트 클래스를 만듭니다. 그 다음 컴포넌트 리스트에서 Weapon 컴포넌트를 선택하면 Details 패널에 다음과 같이 나옵니다.


그림1. EditAnywhere 를 지정했을 때 Details 패널.


뭔가 평소에 보던 것과는 다릅니다. 이는 EditAnywhere 라는 속성 지정자 때문입니다. 보통 VisibleAnywhere 와 EditAnywhere 를 사용하는데, 이는 서로 배타적입니다. 만약 이것을 VisibleAnywhere 로 변경한다면 다음과 같은 Details 패널을 볼 수 있습니다.


그림2. VisibleAnywhere 를 지정했을 때 Details 패널.


그렇다면 EditAnywhere 와 VisibleAnywhere 의 차이는 무엇일까요? 언리얼 엔진 공식 문서에서는 다음과 같이 설명합니다.


EditAnywhere


이 속성이 아키타입( archetype )이나 인스턴스의 속성창에서 편집될 수 있음을 의미합니다.


출처 : https://docs.unrealengine.com/latest/INT/Programming/UnrealArchitecture/Reference/Properties/Specifiers/EditAnywhere/index.html


VisibleAnywhere


이 속성이 속성창에서 보이지만, 편집할 수 없음을 의미합니다. 이 연산은 Edit* 지정자들과 호환되지 않습니다.


출처 : https://docs.unrealengine.com/latest/INT/Programming/UnrealArchitecture/Reference/Properties/Specifiers/VisibleAnywhere/index.html


일단 편집이 된다와 안 된다가 가장 큰 차이인 것 같지만, 실제로는 둘 다 편집이 가능합니다. 설명이 참 이상한 것 같습니다.


추가 : 언리얼 까페의 나가놀자 님에 따르면 두 옵션의 차이는 다음과 같다고 합니다.


Edit은 포인터 자체를 수정하겠다는 의미가 됩니다.

반면 Visible은 포인터의 멤버를 노출하겠다는 의미가 됩니다.


출처: http://cafe.naver.com/unrealenginekr/7259


어쨌든 UI 상에 있어 핵심적인 차이는 아키타입이나 인스턴스 속성창에서 편집될 수 있다는 것입니다. 언리얼에서 아키타입은 다른 엔진들의 프리팹 정도라고 생각하시면 될 것 같습니다. 어쨌든 중요한 것은 인스턴스별로 편집이 가능하다는 것입니다.


예를 들어 EditAnywhere 를 지정하고 만든 클래스의 인스턴스를 생성하면 그 인스턴스의 Details 패널에서 다음과 같은 항목을 볼 수 있습니다. VisibleAnywhere 를 지정하면 나오지 않습니다.



그림3. EditAnywhere 를 지정했을 때 인스턴스의 Details 패널.


정리하자면, EditAnywhere 는 속성을 인스턴스별 속성으로 만들 때 필요하며, VisibleAnywhere 는 속성을 블루프린트 자체의 속성으로만 사용할 때 필요합니다. EditAnywhere 를 지정했을 때 그림1 처럼 접혀 있는 이유는 인스턴스의 Details 패널이 너무 위아래로 길어지지 않도록 하기 위함으로 보입니다( 그래도 저같으면 블루프린트 안에서는 펼쳐져 있도록 구현했을 것 같은데요 ㅡㅡ;; ).


인스턴스별 속성이 뭐냐고 질문하는 분도 계실 겁니다. 그냥 인스턴스별로 속성을 재정의( overriding )할 수 있다는 의미입니다. 만약 EditAnywhere 를 지정한 경우라면 블루프린트 클래스를 여러 개 만들지 않고 하나만 만든 다음에 씬에 배치한 후에 속성을 편집할 수 있겠죠. 만약 VisibleAnywhere 를 지정한 경우라면 생성할 인스턴스 개수마다 블루프린트 클래스를 만들어야만 합니다.


물론 프로그래밍이나 스크립팅을 통해서 이를 제어한다면 상관없지만, 아티스트나 디자이너가 뭔가 하나씩 배치하고 속성을 바꾸기를 원한다면 EditAnywhere 를 사용하는 것을 고려해야 합니다.

언리얼 엔진을 커스터마이징하다가 보면 액터의 컴포넌트를 직접 정의해야 하는 경우가 있습니다. 예를 들어 무기 컴포넌트를 만든다고 합시다.


이 경우에 스태틱 메쉬 애셋을 사용할 것이므로 UStaticMeshComonent 를 상속하는 UStaticWeaponComponent 를 생성합니다. 혹시 채찍같은 것을 만들면 USkeletalMeshComponent 를 상속하는 USkeletalWeaponComonent 를 만들어야겠죠.



그런데 이러한 컴포넌트를 만들고 나서 액터에 추가하려고 하면 "Add Component" 드롭다운 메뉴에 내가 만든 컴포넌트가 리스팅되지 않습니다.




이는 UStaticWeaponComponent 을 생성할 수 있도록 지정하지 않았기 때문입니다. 아래와 같이 클래스 지정자를 설정하면 됩니다. 



이제 "Static Weapon" 항목이 추가되어 있는 것을 확인할 수 있습니다. 참고로 ClassGroup 이라는 것은 이 컴포넌트의 카테고리를 의미합니다.




언리얼 엔진을 빌드할 때 "Ctrl + Break" 를 누르면 그 다음부터 빌드가 진행되지 않는 경우가 있습니다. 


이 때 에디터나 VS 를 다시 닫았다가 열어도 빌드를 진행하지 못하고 아무리 기다려도 소용이 없습니다. 심지어 몇 시간을 기다려도 빌드는 끝나지 않습니다. 실제로 졸려서 자고 왔는데도 그 상태더군요.


이것의 원인은 UnrealBuildTool.exe 프로세스입니다. 이 상태가 되면 빌드를 누를 때마다 프로세스가 계속 누적이 됩니다.


그냥 UnrealBuildTool.exe 프로세스를 종료시켜 주시면 됩니다. 만약 종료가 안 된다면 VS 를 끄고 종료시키면 됩니다.


저같은 경우에는 작업관리자를 열었다가 닫는 것이 귀찮아서 바탕화면에 "KillUEBuildProc.bat" 파일을 만들어 놓고 종료시킵니다.



+ Recent posts