글쓴이 보관물: Toughman

Unreal Android aab 파일 업로드 시 경고 해결

개요

Unreal 로 개발을 마치고 플레이 스토어에 aab 파일을 올렸는데 경고 메시지가 나타났습니다. 무시해도 당분간은 문제가 없습니다. 하지만 업데이트는 계속해야 하고 결국 해결해야 할 문제라는 생각이 들어 조치하기로 했습니다.

첫 번째 경고 메시지

첫 번째 경고 메시지는 다음과 같습니다.

androidx.fragment:fragment (androidx.fragment:fragment) 개발자가 1.0.0 버전이 오래되었다고 신고했습니다. 신작을 게시하기 전에 다음 버전 중 하나로 업그레이드하는 것이 좋습니다.1.1.0+ 사용 중인 SDK에 대해 자세히 알아보고 Google Play SDK 색인 정보를 바탕으로 SDK를 선택하세요.

내용 그대로 오래된 버전으로 지정되어 발생한 경고입니다. Engine\Source\ThirdParty\AndroidPermission\permission_library\additions.gradle 파일을 열어서 다음의 내용을 추가합니다.

dependencies {
    ...
    constraints.implementation 'androidx.fragment:fragment:1.3.6'
}

Unreal Engine 5.3.2 기준으로 fragment 버전을 최신(1.6.2)으로 하면 빌드 오류가 발생했습니다. 1.3.6 으로 지정하면 정상적으로 빌드됩니다.

나머지 경고 메시지

앱 화면 하단에 배너 광고가 나타나도록 했습니다. 다른 경고 메시지는 애드몹(Admob)에 관련된 것 이었습니다.

Google Mobile Ads (GMA) SDK (com.google.android.gms:play-services-ads) 개발자가 18.1.0 버전이 오래되었다고 신고했습니다. 이 버전의 앱을 출시한 지 90일이 지나면 새 버전(20.0.0+)으로 업그레이드할 때까지 이 SDK를 포함한 새 버전을 출시할 수 없습니다. 사용 중인 SDK에 대해 자세히 알아보고 Google Play SDK 색인 정보를 바탕으로 SDK를 선택하세요.

Google Mobile Ads (GMA) SDK (com.google.android.gms:play-services-ads) 개발자가 SDK 버전 18.1.0에 다음 메모를 추가했습니다. As of June 30th 2023, this version is sunset. For more information, please visit https://developers.google.com/admob/android/deprecation. 이 버전의 앱을 출시한 지 90일이 지나면 새 버전으로 업그레이드할 때까지 이 SDK를 포함한 새 버전을 출시할 수 없습니다. 사용 중인 SDK에 대해 자세히 알아보고 Google Play SDK 색인 정보를 바탕으로 SDK를 선택하세요.

Google Mobile Ads (GMA) SDK (com.google.android.gms:play-services-ads-lite) 개발자가 18.1.0 버전이 오래되었다고 신고했습니다. 이 버전의 앱을 출시한 지 90일이 지나면 새 버전(20.0.0+)으로 업그레이드할 때까지 이 SDK를 포함한 새 버전을 출시할 수 없습니다. 사용 중인 SDK에 대해 자세히 알아보고 Google Play SDK 색인 정보를 바탕으로 SDK를 선택하세요.

나머지 경고 메시지 해결 방법

이 글에서 안내하는 해결 방법은 배너 광고에만 해당합니다. Engine\Source\Runtime\Advertising\Android\AndroidAdvertising\AndroidAdvertising_APL.xml 파일을 열어 implementation(‘com.google.android.gms:play-services-ads:18.0.1’) 을 implementation(‘com.google.android.gms:play-services-ads:22.6.0’) 로 변경합니다.

이 상태에서 빌드하면 InterstitialAd 관련 오류가 발생합니다. 원인은 com.google.android.gms.ads.InterstitialAd 이 20.0.0+ 에서 deprecated 되어 AndroidAdvertising_APL.xml 파일내의 삽입광고 구현부분과 라이브러리 버전 20.0.0+ 과의 불일치 때문입니다.

삽입광고를 사용하시는 분들은 새로 변경된 라이브러리 버전에 맞추어 수정하셔야 합니다. <![CDATA[ , ]]>로 감싸진 부분이 애드몹 관련 자바 코드 부분입니다. 필자는 삽입 광고 부분을 모두 주석 처리 했습니다. 추가로 더 이상 지원되지 않는 메소드의 @Override 부분도 다음과 같이 주석 처리 했습니다.

/*
@Override
public void onAdFailedToLoad(int errorCode)
{
    adIsAvailable = false;
    adIsRequested = false;

    // don't immediately request a new ad on failure, wait until the next show
    updateAdVisibility(false);
}
*/

AndroidAdvertising_APL.xml 파일을 수정한 경우 Unreal Engine이 실행 중이면 바로 반영되지 않습니다. 다시 엔진을 시작하고 빌드한 후 aab 파일을 올리면 위의 경고 메시지가 나타나지 않는 것을 알 수 있습니다.

삽입 광고 관련한 부분을 변경된 라이브러리에 맞추어 수정된 파일이 있지 않을까 해서 검색을 좀 해보았는데 발견하지 못하였습니다. 엔진이 업데이트 되면 이 불일치 부분도 해결되었으면 좋겠습니다.

참고로 AndroidAdvertising_APL.xml 파일의 수정된 부분 전체의 내용입니다.

/* 위쪽 import 부분 */
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.ads.MobileAds;
import com.google.android.gms.ads.initialization.InitializationStatus;
import com.google.android.gms.ads.initialization.OnInitializationCompleteListener;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.AdView;
import com.google.android.gms.ads.AdSize;
import com.google.android.gms.ads.AdListener;
//import com.google.android.gms.ads.InterstitialAd;
import com.google.android.gms.ads.interstitial.InterstitialAd;
import com.google.android.gms.ads.RequestConfiguration;
import com.google.android.gms.ads.identifier.AdvertisingIdClient;
import com.google.android.gms.ads.identifier.AdvertisingIdClient.Info;

    /** AdMob support */
    private PopupWindow adPopupWindow;
    private AdView adView;
    private boolean adInit = false;
    private LinearLayout adLayout;
    private int adGravity = Gravity.TOP;
    //private InterstitialAd interstitialAd;
    //private boolean isInterstitialAdLoaded = false;
    //private boolean isInterstitialAdRequested = false;
    //private AdRequest interstitialAdRequest;
    private String advertisingID = null;

	/** true when the application has requested that an ad be displayed */
	private boolean adWantsToBeShown = false;

	/** true when an ad is available to be displayed */
	private boolean adIsAvailable = false;

	/** true when an ad request is in flight */
	private boolean adIsRequested = false;

	// handle ad popup visibility and requests
	private void updateAdVisibility(boolean loadIfNeeded)
	{
		if (!adInit || (adPopupWindow == null))
		{
			return;
		}

		// request an ad if we don't have one available or requested, but would like one
		if (adWantsToBeShown && !adIsAvailable && !adIsRequested && loadIfNeeded)
		{
			AdRequest adRequest = new AdRequest.Builder().build();		// add test devices here
			_activity.adView.loadAd(adRequest);

			adIsRequested = true;
		}

		if (adIsAvailable && adWantsToBeShown)
		{
			if (adPopupWindow.isShowing())
			{
				return;
			}

			adPopupWindow.showAtLocation(activityLayout, adGravity, 0, 0);
			// don't call update on 7.0 to work around this issue: https://code.google.com/p/android/issues/detail?id=221001
			if (ANDROID_BUILD_VERSION != 24) {
				adPopupWindow.update();
			}
		}
		else
		{
			if (!adPopupWindow.isShowing())
			{
				return;
			}

			adPopupWindow.dismiss();
			adPopupWindow.update();
		}
	}

	public void AndroidThunkJava_ShowAdBanner(String AdMobAdUnitID, boolean bShowOnBottonOfScreen)
	{
		Log.debug("In AndroidThunkJava_ShowAdBanner");
		Log.debug("AdID: " + AdMobAdUnitID);

		adGravity = bShowOnBottonOfScreen ? Gravity.BOTTOM : Gravity.TOP;

		if (adInit)
		{
			// already created, make it visible
			_activity.runOnUiThread(new Runnable()
			{
				@Override
				public void run()
				{
					if ((adPopupWindow == null) || adPopupWindow.isShowing())
					{
						return;
					}

					adWantsToBeShown = true;
					updateAdVisibility(true);
				}
			});

			return;
		}

		// init our AdMob window
		adView = new AdView(this);
		adView.setAdUnitId(AdMobAdUnitID);
		adView.setAdSize(AdSize.BANNER);

		if (adView != null)
		{
			_activity.runOnUiThread(new Runnable()
			{
				@Override
				public void run()
				{
					adInit = true;

					final DisplayMetrics dm = getResources().getDisplayMetrics();
					final float scale = dm.density;
					adPopupWindow = new PopupWindow(_activity);
					adPopupWindow.setWidth((int)(320*scale));
					adPopupWindow.setHeight((int)(50*scale));
					adPopupWindow.setClippingEnabled(false);

					adLayout = new LinearLayout(_activity);

					final int padding = (int)(-5*scale);
					adLayout.setPadding(padding,padding,padding,padding);

					MarginLayoutParams params = new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);;

					params.setMargins(0,0,0,0);

					adLayout.setOrientation(LinearLayout.VERTICAL);
					adLayout.addView(adView, params);
					adPopupWindow.setContentView(adLayout);

					// set up our ad callbacks
					_activity.adView.setAdListener(new AdListener()
					{
						 @Override
						public void onAdLoaded()
						{
							adIsAvailable = true;
							adIsRequested = false;

							updateAdVisibility(true);
						}

						/*
						 @Override
						public void onAdFailedToLoad(int errorCode)
						{
							adIsAvailable = false;
							adIsRequested = false;

							// don't immediately request a new ad on failure, wait until the next show
							updateAdVisibility(false);
						}
						*/
					});

					adWantsToBeShown = true;
					updateAdVisibility(true);
				}
			});
		}
	}

	public void AndroidThunkJava_HideAdBanner()
	{
		Log.debug("In AndroidThunkJava_HideAdBanner");

		if (!adInit)
		{
			return;
		}

		_activity.runOnUiThread(new Runnable()
		{
			@Override
			public void run()
			{
				adWantsToBeShown = false;
				updateAdVisibility(true);
			}
		});
	}

	public void AndroidThunkJava_CloseAdBanner()
	{
		Log.debug("In AndroidThunkJava_CloseAdBanner");

		if (!adInit)
		{
			return;
		}

		// currently the same as hide.  should we do a full teardown?
		_activity.runOnUiThread(new Runnable()
		{
			@Override
			public void run()
			{
				adWantsToBeShown = false;
				updateAdVisibility(true);
			}
		});
	}

	public void AndroidThunkJava_LoadInterstitialAd(String AdMobAdUnitID)
	{
		/*
		interstitialAdRequest = new AdRequest.Builder().build();

		interstitialAd = new InterstitialAd(this);
		isInterstitialAdLoaded = false;
		isInterstitialAdRequested = true;
		interstitialAd.setAdUnitId(AdMobAdUnitID);

		_activity.runOnUiThread(new Runnable()
		{
			@Override
			public void run()
			{
				interstitialAd.loadAd(interstitialAdRequest);
			}
		});

		interstitialAd.setAdListener(new AdListener()
		{
			@Override
			public void onAdFailedToLoad(int errorCode)
			{
				Log.debug("Interstitial Ad failed to load, errocode: " + errorCode);
				isInterstitialAdLoaded = false;
				isInterstitialAdRequested = false;
			}
			@Override
			public void onAdLoaded()
			{
				//track if the ad is loaded since we can only called interstitialAd.isLoaded() from the uiThread
				isInterstitialAdLoaded = true;
				isInterstitialAdRequested = false;
			}
		});
		*/
	}

	public boolean AndroidThunkJava_IsInterstitialAdAvailable()
	{
		//return interstitialAd != null && isInterstitialAdLoaded;
		return false;
	}

	public boolean AndroidThunkJava_IsInterstitialAdRequested()
	{
		//return interstitialAd != null && isInterstitialAdRequested;
		return false;
	}

	public void AndroidThunkJava_ShowInterstitialAd()
	{
		/*
		if(isInterstitialAdLoaded)
		{
			_activity.runOnUiThread(new Runnable()
			{
				@Override
				public void run()
				{
					interstitialAd.show();
				}
			});
		}
		else
		{
			Log.debug("Interstitial Ad is not available to show - call LoadInterstitialAd or wait for it to finish loading");
		}
		*/
	}

	private GetAdvertisingIdTask AdTask = null;

	private class GetAdvertisingIdTask extends android.os.AsyncTask<String, Integer, String>
	{
		@Override
		protected String doInBackground(String... values)
		{
			AdvertisingIdClient.Info adInfo = null;
			try
			{
				adInfo = AdvertisingIdClient.getAdvertisingIdInfo(GameActivity.Get().getApplicationContext());
				if (adInfo.isLimitAdTrackingEnabled())
				{
					Log.debug("GetAdvertisingId: User opted out of ad tracking");
					adInfo = null;
				}
				Log.debug("GetAdvertisingID: success");
			}
			catch (Exception e) {
				Log.debug("GetAdvertisingId failed: " + e.getMessage());
			}
			return (adInfo == null) ? "" : adInfo.getId();
		}

		@Override
		protected void onPostExecute(String s)
		{
			advertisingID = s;
		}
	}

	public String AndroidThunkJava_GetAdvertisingId()
	{
		try
		{
			AdTask.get();
		}
		catch (Exception e)
		{
			advertisingID = null;
		}

		return advertisingID;
	}
]]>

별 내용이 없어 보이는데 생각보다 해결에 시간이 많이 소요되었습니다. 필자와 같은 문제를 겪고 계신 분들께 도움이 되었으면 좋겠습니다.

Unreal 안드로이드 apk 파일 크기 줄이기

개요

Unreal 에서 간단한 게임을 제작하고 실제 기기에서 테스트 하기 위해 apk 파일로 빌드해야 합니다. 게임의 내용에 따라 다르지만 아무런 설정 변경 없이 생성하면 파일의 용량이 크다는 것을 알 수 있습니다(기본 100MB 이상).

간단한 게임인데 생각 이상으로 용량이 큽니다. Unity 와 비교하면 상대적으로 더 용량이 큰 것 같습니다. 개인적으로 Unreal 의 안드로이드 지원 부분의 최적화 작업이 더 필요한 것 같습니다.

설정 변경

Unreal 버전은 5.3을 기준으로 했습니다. 글을 쓰는 현재 기준으로 아주 극적인 용량 축소는 없었습니다. 그래도 조금이라도 크기를 줄여야 하므로 몇 가지 설정과 방법을 알아보도록 하겠습니다.

크게 Project Settings 변경, 불필요한 플러그인 삭제가 있습니다.

Project Settings

Project, Platform 섹션의 항목을 변경합니다.

Project > Packaging

– Create compressed cooked package 체크

– Exclude editor content when cooking 항목 체크

– Directories to never cook 항목에 엔진 폴더 추가(/Engine/StarterContent …)

Engine 폴더 전체를 추가했을 때 10MB 정도 용량이 줄었으나 실기기에서 정상적으로 실행되지 않았습니다. 폴더를 각각 추가했을 때 약간의 용량 감소가 있었습니다. 결과적으로는 같은 것인데 왜 이런 차이가 나는지 정확하게 알 수 없었습니다.

– Project > Build Configuraion 항목 Shipping 선택, For Distribution 체크(Platforms > Android > Distribution Signing 정보가 있어야 오류 발생하지 않습니다).

Platforms > Android

– Advanced Build > Build with hidden symbol visibility in shipping config 항목 체크

사용하지 않는 플러그인 제거

Edit > Plugins 메뉴 선택해서 사용하지 않는 플러그인 제거합니다. 우측 상단의 Settings 버튼을 클릭해서 Show Only Enabled 를 체크 후 제거하시면 편리합니다.

Show Only Enabled 체크

주의하실 것은 플러그인 중에 Unreal 에디터 자체의 기능과 관련된 것이 있는데 이것을 제거하지 않도록 합니다(Ex: Plugin Browser, Plugin Utilities, EditorDebugTools, Engine Asset Definitions…).

만일 Edit > Plugins 메뉴가 나타나지 않는다면 프로젝트를 닫고 uproject 파일을 텍스트 에디터로 엽니다. Plugins 항목 아래에 Plugin Browser, Plugin Utilities 항목의 Enabled 가 false 로 되어 있는 것을 true로 변경하고 프로젝트를 다시 열면 메뉴가 나타납니다.

명백하게 사용하지 않은 플러그인 몇 가지(Ex: Paper2D…)는 구분할 수 있는데 판단이 되지 않는 것들이 있습니다. 설명이 있는 것은 그나마 삭제 여부를 판단할 수 있습니다. 그러나 제목만 있는 경우 어쩔 수 없이 제거해 보고 에디터에서 실행이 되는지 보고 빌드까지 하고 확인하는 방법밖에는 없었습니다.

남겨 놓은 플러그인 목록입니다. 완벽히 모두 제거한 것은 아니므로 참고만 하시기 바랍니다. 이 상태에서 apk파일이 빌드 되고 실기기에서 실행 되었습니다.

Android Device Profile Selector
Android Runtime Permission
Asset Tags
Blueprint C++ Header Preview
Content Browser – Asset Data Source
Content Browser – Class Data Source
Data Validation
EditorDebugTools
Engine Asset Definitions
Enhanced Input
Oodle Texture
Plugin Browser
Plugin Utilities
Property Access Editor
TraceUtilities
Visual Studio Integration

정리

대상은 간단한 3D 게임이고 단순 Mesh를 불러와서 그것을 움직이는 형태였습니다. 가장 용량이 크게 줄었던 것은 Project > Build Configuraion 항목 Shipping 선택이었습니다. 그 다음으로는 사용하지 않은 플러그인을 제거한 후 용량이 많이 줄었습니다. 그 이외에는 극적으로 줄지는 않았으나 모두 합쳐보면 무시할 수 없는 용량이었습니다.

처음에는 100MB 이상이었던 것을 53.4MB 까지 줄인 상태입니다. 완벽한 방법이라고 할 수는 없겠지만 apk파일 용량 문제로 고민하셨던 분들께 도움이 되었으면 좋겠습니다.