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 와 연동하는 작업을 해 볼 계획입니다.

+ Recent posts