笑看天下无敌手 发表于 2025-3-30 16:34:36

UE5安卓AAR打包

1.什么是AAR打包

这里引用deepseek的尺度回复:Android开发中用于封装和复用代码及资源的一种尺度格式,全称为Android Archive(文件扩展名为.aar)。在现实项目中,特别是车机应用中,把UE5作为第三方库,通过JNI函数的方式提供给安卓引擎函数,使安卓端不通过NDK的方式,更加灵活的使用UE5进行渲染。说得普通点,就是安卓开发也可以自界说UI、其他业务逻辑等,仅把UE作为3D渲染引擎,可以实现安卓端点击某些按钮控制UE场景结果等
2. 安卓和c++交互简介

安卓端和c++使用JNI函数进行交互:尺度格式是
JNI_METHOD void java_com_包名_类名_函数名(函数参数)
实例:
JNI_METHOD jint Java_com_epicgames_unreal_nativeMain(JNIEnv* jenv, jobject thiz, jstring projectModule, jobjcet activity)
JAVA端使用界说:
留意JNI函数前两个参数是安卓调用的时候自动通报的,第一个代表JAVA虚拟机对象,第二个体现当前调用类对象
public native void nativeMain(String projectModule, Activity activity);
3.UE5安卓端apk引擎初始化流程

安卓NDK源代码中会创建线程调用c++的全局函数android_main(struct android_app* state),所以对于UE来说,在LauchAndroid.cpp中实现了android_main函数。
https://i-blog.csdnimg.cn/direct/1507155ea9ee4220b1d6db1dd36cb2cd.png
这个函数重要调用AndroidMain函数,AndroidMain函数才是重要调用LaunchEngineLoop初始化引擎的函数
int32 AndroidMain(struct android_app* state)
{
        ......
        //加载安卓库
        void* Lib = dlopen("libandroid.so",0);
        if (Lib != NULL)
        {
                GetAxes = (GetAxesType)dlsym(Lib, "AMotionEvent_getAxisValue");
        }
        //调用LaunchEngineLoop.cpp中PreInit函数       
        int32 PreInitResult = GEngineLoop.PreInit(0, NULL, FCommandLine::Get());
        ........
        //调用LaunchEngineLoop.cpp中Init函数
        ErrorLevel = GEngineLoop.Init();
        ........
        // 启动引擎tick
        while (!IsEngineExitRequested())
        {
                FAndroidStats::UpdateAndroidStats();
                FAppEventManager::GetInstance()->Tick();
                if (!FAppEventManager::GetInstance()->IsGamePaused())
                {
                        GEngineLoop.Tick();
                }
                else
                {
                        // use less CPU when paused
                        FPlatformProcess::Sleep(0.10f);
                }
                }
}
上述代码片段,描述了初始化UE引擎的重要过程,这个过程中PreInit和Init之间还会死循环卡死,等待安卓端调用对应的JNI函数响应初始化流程。
EngineLoop::PreInit具体实现
https://i-blog.csdnimg.cn/direct/b68d7676a223400da369420aa6d460f3.png
简单描述下:PreInitPreStartupScreen里面干了很多事情
1.加载核心CoreUobject模块,初始化反射体系基础类对象UClass,UFunction等
2.初始化全局GGameThreadId游戏线程ID
3.创建TaskGraph线程池
4.加载其他的核心模块,Engine、Renderer、SlateRHIRenderer、RenderCore、RHICore等
5.调用不同平台层preInit函数
6.调用InitGamePhys初始化物理引擎
7.创建不同平台层FSlateApplication对象,进而调用不同平台创建窗口函数
8.调用不同平台层的PlatformCreateDynamicRHI函数创建RHI对象,有的平台支持Vulkan,有的支持OpenGL,windows平台下使用DX,都是在这里处置惩罚的
9.加载插件等等
PreInitPostStartupScreen
1.调用显示开机加载动画
2.加载资源管理模块AssetRegistry
3.调用ProcessNewlyLoadedUObjects再次确保CoreUObejct核心反射模块已加载
4.InitRenderingThread()初始化rendering渲染线程
EngineLoop::Init重要作用是创建:
1.创建GEngine引擎对象,编辑器对应UEditorEngine,游戏模式对应UGameEngine,然后在GEngine初始化的时候创建GameInstance
2.初始化MoviePlayer体系等
3.FCoreDelegates::OnFEngineLoopInitComplete通知引擎初始化完
EngineLoop::Tick重要实现:
1.调用BeginFrameRenderThread(RHICmdList, CurrentFrameCounter)
2.CalculateFPSTimings()计算帧率
3.调用GEngine->Tick,这里才是游戏逻辑Tick入口,重要作用是:
1)调用TickWorldTravel检测是否是要无缝加载舆图,
2)调用UWorld::Tick( ELevelTick TickType, float DeltaSeconds ),每帧开始收网络同步包,异步加载资源,调用TickGroup(本质是Actor和ActorComponent等的Tick函数被Level::AddTickFunction函数参加Tick队列),每帧末了发网络包,GC回收检测等
3)调用FTickableGameObject::TickObjects,实现继续自FTickableGameObject类对象的Tick函数调用
apk输入事故绑定
android_main里面给事故获取单独开了个线程
https://i-blog.csdnimg.cn/direct/5f8e6a394786483f9bc161dd8bc0c3ca.png
现实输入事故绑定是通过给NDK注册输入回调HandleInputCB
https://i-blog.csdnimg.cn/direct/ce0c14f752824d5c818421e3bd58b7ea.png
HandleInputCB中具体代码实现,直接通过NDK接口获取
if ((EventSource & AINPUT_SOURCE_MOUSE) == AINPUT_SOURCE_MOUSE)
{
        static int32 previousButtonState = 0;
        const int32 device = AInputEvent_getDeviceId(event);
        const int32 action = AMotionEvent_getAction(event);
        const int32 actionType = action & AMOTION_EVENT_ACTION_MASK;
        int32 buttonState = AMotionEvent_getButtonState(event);
        .....
}
至于UE的渲染结果是怎样绑定渲染到安卓界面窗口的?涉及到AndroidEGL等类,后续有空再先容,简而言之是安卓端会通报一个surfaceview UI对象到UE,然后转换成ANativeWindow对象进行渲染。
4.APK打包流程简介

省略前面cook资源流程,着重先容下cook完后与安卓端干系的交互
1)生成安卓gradle工程
https://i-blog.csdnimg.cn/direct/e2b3d31681614c13b428571fb03a95e9.png
一般打包后会在Intermediate目录下生成这些内容
https://i-blog.csdnimg.cn/direct/376ba7bf1a544104ad8da77e6cd9c08f.png
assets: 包含了一个main.obb.png文件,所有的蓝图,贴图,关卡资源都会被打包到这里面,末了提供给安卓端解析
https://i-blog.csdnimg.cn/direct/5b75e24306594124b40926ccbfec24ad.png
gradle: 这是安卓工程,可以直接用AndroidStudio打开,里面包含了安卓端源代码,比力核心的就是GameActivity.java,里面包含了安卓端唤醒和挂起APP后,控制UE怎样响应的代码。
安卓端源代码是通过拷贝引擎准备好的模板,即引擎目录下UE_5.2\Engine\Build\Android\Java\src\com\epicgames\unreal,每次构建的时候都会拷贝模板重新生成,所以书写安卓代码实在并不方便。也就衍生了自建安卓gradle工程,以及不依靠NDK接口,安卓端控制UE拉起,即后面要先容的AAR打包调用
jni: 包含UE5的c++代码就会被打包成libUnreal.so库(注UE4叫libUE4.so)
5.AAR安卓端调用UE方式实现

这里工作量实在重要都在安卓端,UE只是提供控制的JNI接口。
1.提供初始化引擎重要入口nativeMain,不依靠安卓NDK
JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeMain(JNIEnv* jenv, jobject thiz, jstring projectModule)
{
        dummy_state.activity = nullptr;
       
        pthread_attr_t otherAttr;
        pthread_attr_init(&otherAttr);
        pthread_attr_setdetachstate(&otherAttr, PTHREAD_CREATE_DETACHED);
        pthread_create(&G_AndroidMainThread, &otherAttr, AndroidMain, &dummy_state);
}
2.不依靠NDK接口,直接从安卓端把输入事故通报过来,再通过FAndroidInputInterface::QueueTouchInput绑定到UE里通报给slate UI
JNI_METHOD jint Java_com_epicgames_ue4_GameActivity_nativeInputTouch(JNIEnv* jenv, jobject thiz, jint device, jint inType, jint inPointerId, jint inX, jint inY)
{
        TArray<TouchInput> TouchesArray;

        TouchType type = TouchEnded;
        switch (inType)
        {
                case 0: type = TouchBegan; break;
                case 1: type = TouchMoved; break;
                case 2: type = TouchEnded; break;
        }

        ANativeWindow* Window = (ANativeWindow*)FAndroidWindow::GetHardwareWindow_EventThread();
        if (!Window)
        {
                return 0;
        }

        int32_t Width = 0;
        int32_t Height = 0;

        if (Window)
        {
                extern void* GAndroidWindowOverride;

                if (GAndroidWindowOverride != nullptr)
                {
                        Width = ANativeWindow_getWidth((ANativeWindow*)GAndroidWindowOverride);
                        Height = ANativeWindow_getHeight((ANativeWindow*)GAndroidWindowOverride);
                }
                else
                {
                        // we are on the event thread. true here indicates we will retrieve dimensions from the current window.
                        FAndroidWindow::CalculateSurfaceSize(Width, Height, true);
                }
        }

        // make sure OpenGL context created before accepting touch events.. FAndroidWindow::GetScreenRect() may try to create it early from wrong thread if this is the first call
        if (!GAndroidGPUInfoReady)
        {
                return 1;
        }
        FPlatformRect ScreenRect = FAndroidWindow::GetScreenRect(true);
        int32_t ScreenWidth = ScreenRect.Right - ScreenRect.Left;
        int32_t ScreenHeight = ScreenRect.Bottom - ScreenRect.Top;

        int32 eventX = inX - ScreenRect.Left;
        int32 eventY = inY - ScreenRect.Top;
        if (eventX < 0 || eventY < 0 || eventX >= ScreenWidth || eventY >= ScreenHeight)
        {
                return 0;
        }

        int pointerId = inPointerId;
        float x = FMath::Max<float>(FMath::Min<float>((float)eventX / Width, 1.f), 0.f) * (ScreenWidth - 1);
        float y = FMath::Max<float>(FMath::Min<float>((float)eventY / Height, 1.f), 0.f) * (ScreenHeight - 1);

        //FPlatformMisc::LowLevelOutputDebugStringf(TEXT("TOUCH: at (%d,%d) windowPos = (%d,%d), windowSize = (%d,%d), screenSize = (%d,%d), final pos: (%f,%f)"),
        //        eventX, eventY, ScreenRect.Left, ScreenRect.Top, ScreenWidth, ScreenHeight, Width, Height, x, y);
        UE_LOG(LogAndroid, Verbose, TEXT("Received targeted motion event from pointer %u (id %d) action %d: (%.2f, %.2f)"), 0, pointerId, type, x, y);

        TouchInput TouchMessage;
        TouchMessage.DeviceId = device;
        TouchMessage.Handle = pointerId;
        TouchMessage.Type = type;
        TouchMessage.Position = FVector2D(x, y);
        TouchMessage.LastPosition = FVector2D(x, y);
        TouchesArray.Add(TouchMessage);

        FAndroidInputInterface::QueueTouchInput(TouchesArray);

        return 1;
}
3.命令控制app窗口生命周期并通知UE
JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeAppCommand(JNIEnv* jenv, jobject thiz, jint cmd)
{
        extern void* GAndroidWindowOverride;
        dummy_state.window = (ANativeWindow*)GAndroidWindowOverride;
        dummy_state.pendingWindow = (ANativeWindow*)GAndroidWindowOverride;

        FPlatformMisc::LowLevelOutputDebugStringf(TEXT("OnAppCommandCB cmd: %u, tid = %d"), cmd, gettid());
        OnAppCommandCB(&dummy_state, cmd);
}
比方cmd通报APP_CMD_RESUME,唤醒UE窗口。
APP_CMD_PAUSE,暂停窗口渲染,制止音效等
https://i-blog.csdnimg.cn/direct/657f523f277044578f79b9867000a2d2.png
4.通报自界说的surfaceview对象给UE进行渲染
//This function is declared in the Java-defined class, GameActivity.java: "public native void nativeSetGlobalActivity();"
JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeSetGlobalActivity(JNIEnv* jenv, jobject thiz, jboolean bUseExternalFilesDir, jboolean bPublicLogFiles, jstring internalFilePath, jstring externalFilePath, jboolean bOBBinAPK, jstring APKFilename /*, jobject googleServices*/)
{
        if (!FJavaWrapper::GameActivityThis)
        {
                GGameActivityThis = FJavaWrapper::GameActivityThis = jenv->NewGlobalRef(thiz);
                if (!FJavaWrapper::GameActivityThis)
                {
                        FPlatformMisc::LowLevelOutputDebugString(TEXT("Error setting the global GameActivity activity"));
                        check(false);
                }

                // This call is only to set the correct GameActivityThis
                FAndroidApplication::InitializeJavaEnv(GJavaVM, JNI_CURRENT_VERSION, FJavaWrapper::GameActivityThis);
        .....
}
5.安卓重要调用片段
初始化调用nativeMain
https://i-blog.csdnimg.cn/direct/6abb7bac424d4aff933de0114c7e7fdb.png
唤醒安卓APP的时候,通过调用前面界说的JNI函数,nativeAppCommand通知UE
https://i-blog.csdnimg.cn/direct/24555094409348e1ad76130c7ce85b57.png
总结

末了总结一下,安卓与UE的交互是通过JNI函数的方式,所以安卓端可以更加灵活的控制,不依靠NDK限制,直接拉起UE,并控制UE的生命周期。比方车机端使用UE,想安卓和UE能更加深度的交互,并以安卓为主逻辑驱动,就需像AAR打包方式一样,自界说控制流程。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: UE5安卓AAR打包