Android Studio+ Unity 적용 (2)
1. 코드 리펙터링 시작
저번 포스팅에서 진행했듯이, Unity 뷰를 연동하는 것 까지는 무난했다. 생각보다 술술 풀리는가 싶더니 결국 일이 터졌다.
바로, 통신 과정에서 적용이 안되는 오류였다. 이 오류를 해결하는 동안, 여러 가지 조사를 하면서 리펙터링도 진행하였고, 그 과정을 기록해보려고 한다. 인터넷 상에 AndroidStudio 내부에 Unity를 접목시키는 프로젝트에 대한 레퍼런스가 너무 없어서 너무 힘들었다ㅠㅠ
2. 수정한 내용들
1) AndroidManifest.xml (unityLibrary module)
해당 파일에서 고친 것들을 설명하면,
첫번째로, 해당 모듈은 app에 의존한다. 그러므로, 내부에 선언된 액티비티는 app 모듈의 Manifest로 옮겨주었다.
두번째로, 유니티 쪽에서 quit()를 부르거나, android쪽에 finish()를 불러 액티비티를 종료하게 되면, 앱 자체가 꺼지게 된다. 애초에 다른 프로세스를 사용하고 있어서 그렇다고 한다. 이 부분은 UnityPlayerGameView안에서 android:process=":Unity"를 추가해주면 된다.
세 번째는, heap 메모리 용량이 부족하다는 문구가 자꾸 출력되었다.
android:largeHeap="true"
이 부분을 추가해주니 해결되었다.
수정 전 AndroidManifest.xml (unityLibraryModule)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:glEsVersion="0x00030000" />
<uses-feature android:name="android.hardware.vulkan.version" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen.multitouch" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen.multitouch.distinct" android:required="false" />
<application android:enableOnBackInvokedCallback="false" android:extractNativeLibs="true">
<meta-data android:name="unity.splash-mode" android:value="0" />
<meta-data android:name="unity.splash-enable" android:value="True" />
<meta-data android:name="unity.launch-fullscreen" android:value="True" />
<meta-data android:name="unity.render-outside-safearea" android:value="True" />
<meta-data android:name="notch.config" android:value="portrait|landscape" />
<meta-data android:name="unity.auto-report-fully-drawn" android:value="true" />
<meta-data android:name="unity.auto-set-game-state" android:value="true" />
<meta-data android:name="unity.strip-engine-code" android:value="true" />
<activity android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density" android:enabled="true" android:exported="true" android:hardwareAccelerated="false" android:launchMode="singleTask" android:name="com.unity3d.player.UnityPlayerGameActivity" android:resizeableActivity="true" android:screenOrientation="portrait" android:theme="@style/BaseUnityGameActivityTheme">
<!-- <intent-filter>-->
<!--<!– <category android:name="android.intent.category.LAUNCHER" />–>-->
<!--<!– <action android:name="android.intent.action.MAIN" />–>-->
<!-- </intent-filter>-->
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
<meta-data android:name="android.app.lib_name" android:value="game" />
<meta-data android:name="WindowManagerPreference:FreeformWindowSize" android:value="@string/FreeformWindowSize_phone" />
<meta-data android:name="WindowManagerPreference:FreeformWindowOrientation" android:value="@string/FreeformWindowOrientation_portrait" />
<meta-data android:name="notch_support" android:value="true" />
<layout android:defaultHeight="1920px" android:defaultWidth="1080px" android:minHeight="1280px" android:minWidth="720px" />
</activity>
</application>
</manifest>
수정 후
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:glEsVersion="0x00030000" />
<uses-feature android:name="android.hardware.vulkan.version" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen.multitouch" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen.multitouch.distinct" android:required="false" />
<application android:enableOnBackInvokedCallback="false" android:extractNativeLibs="true">
<meta-data android:name="unity.splash-mode" android:value="0" />
<meta-data android:name="unity.splash-enable" android:value="True" />
<meta-data android:name="unity.launch-fullscreen" android:value="True" />
<meta-data android:name="unity.render-outside-safearea" android:value="True" />
<meta-data android:name="notch.config" android:value="portrait|landscape" />
<meta-data android:name="unity.auto-report-fully-drawn" android:value="true" />
<meta-data android:name="unity.auto-set-game-state" android:value="true" />
<meta-data android:name="unity.strip-engine-code" android:value="true" />
</application>
</manifest>
AndroidManifest.xml (app module)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...기존 내용들
// 여기 부터 추가
<activity
android:name="com.unity3d.player.UnityPlayerGameActivity"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density"
android:enabled="true"
android:exported="true"
android:hardwareAccelerated="false"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:process=":Unity"
android:screenOrientation="fullSensor"
android:theme="@style/BaseUnityGameActivityTheme">
<!-- <intent-filter>-->
<!-- <category android:name="android.intent.category.LAUNCHER" />-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- </intent-filter>-->
<meta-data
android:name="unityplayer.UnityActivity"
android:value="true" />
<meta-data
android:name="android.app.lib_name"
android:value="game" />
<meta-data
android:name="WindowManagerPreference:FreeformWindowSize"
android:value="@string/FreeformWindowSize_phone" />
<meta-data
android:name="WindowManagerPreference:FreeformWindowOrientation"
android:value="@string/FreeformWindowOrientation_portrait" />
<meta-data
android:name="notch_support"
android:value="true" />
<layout
android:defaultWidth="1080px"
android:defaultHeight="1920px"
android:minWidth="720px"
android:minHeight="1280px" />
</activity>
</application>
</manifest>
2) Build.gradle (unityLibrary module)
충돌이 날 수 있는 androidx 는 주석 처리 해주었다.
// implementation libs.androidx.core.v190
ndk 라이브러리 버전을 안정화 버전으로 바꿔주었다.
ndkVersion "27.2.12479018"
버전 찾은 링크 : https://docs.unity3d.com/Manual/android-supported-dependency-versions.html
3) Build.gradle (app)
유니티 모듈에서만 ndk를 사용할 것이므로, 기존 ndk 버전 명시는 지워주었다.
// ndkVersion = "25.2.9519653" // NDK 삭제
따로 유니티 산출물과 같은 버전의 ndk를 다운로드 받아 준 뒤에 경로를 써 주었다.
ndk.dir=C\:\\Users\\PC\\AppData\\Local\\Android\\Ndk
4) gradle.propertices
Export한 산출물의 프로퍼티 파일을 보고, 몇 가지의 기능을 추가하거나 삭제하였다.
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
org.gradle.parallel=true
unityTemplateVersion=18
android.enableJetifier=true
unityStreamingAssets=
나의 경우 Xmx2048m 부분을 4096으로 바꿔주고, 템플릿 버전도 수정했다.
Streaming asset은 비어있어서 똑같이 비워주었다.
5) UnityPlayerGameActivity.kt
대망의 통신 코드이다. 로그를 찍어 일일히 확인하면서 삽질을 했다.... 심지어 유니티 프로젝트 3개가 들어가서 빌드 1번에 10분을 잡아먹는다....... 한 번에 되길 기도... 안되면 유니티 개발자 분께 연락해서 다시 export 받아야 하기에..
일단 자바 언어로 된 모듈이라 자바 언어로 코딩을 해주었다.
스테이지를 인텐트로 받아온 뒤, Unity 내부의 함수를 안드로이드 쪽에서 호출하여 모듈을 실행한다. 스테이지 정보를 인자값으로 넘겨 동적으로 스테이지를 구성한다.
package com.unity3d.player;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.widget.FrameLayout;
import androidx.core.view.ViewCompat;
import com.google.androidgamesdk.GameActivity;
public class UnityPlayerGameActivity extends GameActivity implements IUnityPlayerLifecycleEvents, IUnityPermissionRequestSupport, IUnityPlayerSupport {
private String stageId = "";
class GameActivitySurfaceView extends InputEnabledSurfaceView {
GameActivity mGameActivity;
public GameActivitySurfaceView(GameActivity activity) {
super(activity);
mGameActivity = activity;
}
// Reroute motion events from captured pointer to normal events
// Otherwise when doing Cursor.lockState = CursorLockMode.Locked from C# the touch and mouse events will stop working
@Override
public boolean onCapturedPointerEvent(MotionEvent event) {
return mGameActivity.onTouchEvent(event);
}
}
protected UnityPlayerForGameActivity mUnityPlayer;
protected String updateUnityCommandLineArguments(String cmdLine) {
return cmdLine;
}
static {
System.loadLibrary("game");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// On devices with API Level >= 30 system bars are no longer accounted for and because of that window/views don't resize (see https://jira.unity3d.com/browse/UUM-18618)
// This is most likely due to deprecation of setSystemUiVisibility and changes to insets used in SystemUI.cpp
// This fix forces views to shrink to account for system bars
stageId = getIntent().getStringExtra("stage");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getWindow().setDecorFitsSystemWindows(true);
}
}
@Override
public UnityPlayerForGameActivity getUnityPlayerConnection() {
return mUnityPlayer;
}
// Soft keyboard relies on inset listener for listening to various events - keyboard opened/closed/text entered.
private void applyInsetListener(SurfaceView surfaceView) {
surfaceView.getViewTreeObserver().addOnGlobalLayoutListener(
() -> onApplyWindowInsets(surfaceView, ViewCompat.getRootWindowInsets(getWindow().getDecorView())));
}
@Override
protected InputEnabledSurfaceView createSurfaceView() {
return new GameActivitySurfaceView(this);
}
@Override
protected void onCreateSurfaceView() {
super.onCreateSurfaceView();
FrameLayout frameLayout = findViewById(contentViewId);
applyInsetListener(mSurfaceView);
mSurfaceView.setId(UnityPlayerForGameActivity.getUnityViewIdentifier(this));
String cmdLine = updateUnityCommandLineArguments(getIntent().getStringExtra("unity"));
getIntent().putExtra("unity", cmdLine);
// Unity requires access to frame layout for setting the static splash screen.
// Note: we cannot initialize in onCreate (after super.onCreate), because game activity native thread would be already started and unity runtime initialized
// we also cannot initialize before super.onCreate since frameLayout is not yet available.
mUnityPlayer = new UnityPlayerForGameActivity(this, frameLayout, mSurfaceView, this);
}
@Override
public void onUnityPlayerUnloaded() {
}
@Override
public void onUnityPlayerQuitted() {
}
// Quit Unity
@Override
protected void onDestroy() {
mUnityPlayer.destroy();
super.onDestroy();
}
@Override
protected void onStop() {
// Note: we want Java onStop callbacks to be processed before the native part processes the onStop callback
mUnityPlayer.onStop();
super.onStop();
}
@Override
protected void onStart() {
// Note: we want Java onStart callbacks to be processed before the native part processes the onStart callback
mUnityPlayer.onStart();
super.onStart();
}
// Pause Unity
@Override
protected void onPause() {
// Note: we want Java onPause callbacks to be processed before the native part processes the onPause callback
mUnityPlayer.onPause();
super.onPause();
}
// Resume Unity
@Override
protected void onResume() {
// Note: we want Java onResume callbacks to be processed before the native part processes the onResume callback
mUnityPlayer.onResume();
super.onResume();
}
// Configuration changes are used by Video playback logic in Unity
@Override
public void onConfigurationChanged(Configuration newConfig) {
mUnityPlayer.configurationChanged(newConfig);
super.onConfigurationChanged(newConfig);
}
// Notify Unity of the focus change.
@Override
public void onWindowFocusChanged(boolean hasFocus) {
mUnityPlayer.windowFocusChanged(hasFocus);
super.onWindowFocusChanged(hasFocus);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// To support deep linking, we need to make sure that the client can get access to
// the last sent intent. The clients access this through a JNI api that allows them
// to get the intent set on launch. To update that after launch we have to manually
// replace the intent with the one caught here.
setIntent(intent);
mUnityPlayer.newIntent(intent);
}
@Override
@TargetApi(Build.VERSION_CODES.M)
public void requestPermissions(PermissionRequest request) {
mUnityPlayer.addPermissionRequest(request);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
mUnityPlayer.permissionResponse(this, requestCode, permissions, grantResults);
}
public void receiveDataFromUnity(String str) {
Log.d("UnityMessage", "Received message from Unity: " + str);
// 수신된 메시지에 따라 동작 수행
switch (str) {
case "ok":
Log.d("UnityMessage", "OK command received");
break;
case "next":
Log.d("UnityMessage", "NEXT command received");
try {
Context context = UnityPlayer.currentActivity;
Intent intent = new Intent();
intent.setClassName(context, "com.example.myapplication.presentation.ui.activity.QuizClearActivity");
intent.putExtra("stage", Integer.parseInt(stageId) + 7 + "");
intent.putExtra("game2Activity", true);
context.startActivity(intent);
} catch (Exception e) {
Log.e("UnityLibrary", "Failed to start QuizClearActivity", e);
}
break;
case "quit":
Log.d("UnityMessage", "QUIT command received");
finish();
break;
default:
Log.d("UnityMessage", "Unknown command: " + str);
break;
}
// 필요하다면 Unity로 결과 반환
// sendMessageToUnity("GameObjectName", "MethodName", "Response message");
}
}
해당 클래스를 추상 클래스로 하여 상속받은 뒤, 내 입맛대로 바꾸려고 계획 중이다. 가능하다면 app모듈로 커스텀 액티비티를 통합하여 intent 코드도 정리할 수 있을 것 같다. 디자인 패턴 중 하나인 어댑터 패턴에서 영감을 얻었다.
유니티 쪽에서도 안드로이드 쪽으로 성공 여부를 반환할 수 있도록 통신해주어야 한다.
해당 코드는 이것을 가능하게 해 주는 유니티 코드이다.
using UnityEngine;
public class StageSelector : MonoBehaviour
{
void Start()
{
#if UNITY_ANDROID && !UNITY_EDITOR
AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
AndroidJavaObject intent = currentActivity.Call<AndroidJavaObject>("getIntent");
string stage = intent.Call<string>("getStringExtra", "stage_name");
Debug.Log("받은 스테이지: " + stage);
if (!string.IsNullOrEmpty(stage))
{
UnityEngine.SceneManagement.SceneManager.LoadScene(stage);
}
#endif
}
}
안드로이드 내부에서도 유니티의 함수값을 받을 수 있게 되었다.
3. 에러 핸들링
1) Task :app:processDebugMainManifest FAILED
Manifest 충돌이다. unityLibrary와 app의 Manifest의 중복 부분을 분리한다.
2) AndroidJavaException: java.lang.NoSuchMethodError: no non-static method with name='receiveDataFromUnity' signature='(Ljava/lang/String;)V' in class Ljava.lang.Object;
C++로 코딩된 유니티 쪽에서 같은 이름의 함수를 찾지 못하였다는 것이다. 안드로이드 쪽과 유니티 쪽의 메서드 명을 일치 시켜야 서로 상호작용이 가능하다.
3)Could not read script 'C:\Users\PC\Desktop\MyApplication4\shared\keepUnitySymbols.gradle' as it does not exist.
keepUnitySymbols을 찾을 수 없다는 것인데, export시 unityLibrary 외부에 있는 파일이다. 해당 파일은 사용하지 않으므로, 해당 줄을 주석처리 해주면 된다.
4) GraphicBuffer(w=4, h=4, lc=1) failed (Unknown error -7), handle=0x0
그래픽스 버퍼가 부족하다는 것이다. 위에 해결책을 기재해 놓았다.
5) :unityLibrary:buildIl2Cpp 에러
가능한 원인:
- 메모리 부족 - IL2CPP 컴파일은 많은 메모리를 사용합니다
- 코드 내 오류가 있을 수 있음
- Unity 프로젝트의 구성 문제
- 파일 경로가 너무 길거나 특수 문자가 포함된 경우
- IL2CPP 툴체인 설치 문제
나의 경우에 해당하는 에러는 내 컴퓨터의 메모 부족해서 일어났었다..
가장 핵심은... 유니티 export 파일과 최대한 같은 환경으로 만들어 주어야 하는 것 같다. 현재는 해상도가 깨지는 이슈가 있는데 openGL 쪽인지, 안드로이드 자체 밀도와 해상도 때문인지, 버전 오류인지 확인 중이다.
출처
https://docs.unity3d.com/Manual/android-supported-dependency-versions.html
Unity를 안드로이드에서 사용 - Uaal 활용(1)
출처 : https://learn-and-give.tistory.com/86위 예제 프로젝트를 설치하고 다운 받는다안드로이드 기본 프로젝트를 생성하고 유니티 앱을 실행하기 위한 버튼 생성해당 사이트에서 유니티 프로젝트 파일
velog.io
Unity 프로젝트 - Android Studio에 연동하기
연동을 하기전에 먼저 유니티에서 빌드후 export 된 안드로이드 프로젝트를 안드로이드 스튜디오에서 먼저 빌드하고 핸드폰에 실행되는지 확인을 해야한다. 2021.12.13 - [Develope/JAVA] - Unity 프로젝트
brtech.tistory.com
유니티에서 안드로이드로 데이터 전달
유니티 cs c# 소스 코드에서 안드로이드로 문자열을 전달합니다. public static void CallAndroidMethod(string methodName, string str) { using (var clsUnityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) // "com.pingtech.
zerowincoding.tistory.com