一般游戏引擎都是使用c++实现逻辑,再通过一个虚拟机实现用脚本语言维护业务逻辑。因此在安卓下进行一层包装是十分必要的,使得引擎能够和安卓接口交互,也能正确地管理生命周期。
Unreal 启动 初始化流程如下图所示: 在Android侧,使用SplashActivity
作为入口。主要处理一些机型的适配,权限处理。创建GameActivity
之后会调用finish
自行销毁。GameActivity
是一个NativeActivity
,NativeActivity
通过loadNativeCode
方法实现了一套自有looper事件处理逻辑。在调用了loadNativeCode
之后,会在c++侧创建一个子线程,即UE的GameThread
。目前是在onCreate
中通过调用super的onCreate
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void onCreate (Bundle savedInstanceState) { _activity = this ; InternalFilesDir = getFilesDir().getAbsolutePath() + "/" ; ExternalFilesDir = getExternalFilesDir(null ).getAbsolutePath() + "/" ; Logger.RegisterCallback(this ); AssetManagerReference = this .getAssets(); super .onCreate(savedInstanceState); ... }
接下来的engine初始化流程来到GameThread内,实际的入口为LaunchAndroid::android_main
。
此实际上engine的初始化流程过长也不会影响到主线程,这样可以避免出现ANR的情况。
android_main
主要做了三件事:
记录主线程ID
创建AndroidEventThread,用于处理cmd和input事件
engine从cmd和input事件获取到来自主线程的回调
执行AndroidMain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void android_main (struct android_app* state) { FTaskTagScope Scope (ETaskTag::EGameThread) ; GGameThreadId = FPlatformTLS::GetCurrentThreadId (); GNativeAndroidApp = state; check (GNativeAndroidApp); pthread_attr_t otherAttr; pthread_attr_init (&otherAttr); pthread_attr_setdetachstate (&otherAttr, PTHREAD_CREATE_DETACHED); pthread_create (&G_AndroidEventThread, &otherAttr, AndroidEventThreadWorker, state); AndroidMain (state); }
AndroidMain
是实际上的engine的主循环,主要做了以下几件事情:
等待GResumeMainInit
初始化,这个flag会在GameActivity
调用完onResume
之后设置。说明此时GameActivity
已经准备完毕了,可以开始交互。
初始化文件系统权限
engine的初始化流程
游戏tick循环
游戏退出
代码太长不贴了,在LaunchAndroid.cpp里
事件交互 如前所述,android_main
新建了一个新的线程用于处理Android的事件。线程的处理函数在LaunchAndroid::AndroidEventThreadWorker
中。这个函数主要做了几件事情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static void * AndroidEventThreadWorker ( void * param ) { ALooper* looper = ALooper_prepare (ALOOPER_PREPARE_ALLOW_NON_CALLBACKS); ALooper_addFd (looper, state->msgread, LOOPER_ID_MAIN, ALOOPER_EVENT_INPUT, NULL , &state->cmdPollSource); state->looper = looper; state->onAppCmd = OnAppCommandCB; state->onInputEvent = HandleInputCB; while (!IsEngineExitRequested ()) { AndroidProcessEvents (state); sleep (EventRefreshRate); } return NULL ; }
这里用新的线程中绑定Alooper
的作用是避免事件的回调在Game Thread上触发。而是在engine的tick中集中处理掉这些事件。AndroidProcessEvents
基本上是一个类似epoll的处理函数,取出时间并且触发处理,没太多值得看的东西:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static void AndroidProcessEvents (struct android_app* state) { int ident; int fdesc; int events; struct android_poll_source * source; while ((ident = ALooper_pollAll (-1 , &fdesc, &events, (void **)&source)) >= 0 ) { if (source) { source->process (state, source); } } }
在回调中,根据消息id会分派到对应逻辑去处理。基本上都是塞进一个消息队列中,等Game Thread主循环的tick中处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 static void OnAppCommandCB (struct android_app* app, int32_t cmd) { switch (cmd) { case APP_CMD_SAVE_STATE: UE_LOG (LogAndroid, Log, TEXT ("Case APP_CMD_SAVE_STATE" )); FAppEventManager::GetInstance ()->EnqueueAppEvent (APP_EVENT_STATE_SAVE_STATE); break ; case APP_CMD_DESTROY: FAppEventManager::GetInstance ()->EnqueueAppEvent (APP_EVENT_RUN_CALLBACK, FAppEventData ([]() { FGraphEventRef WillTerminateTask = FFunctionGraphTask::CreateAndDispatchWhenReady ([]() { PRAGMA_DISABLE_DEPRECATION_WARNINGS FCoreDelegates::ApplicationWillTerminateDelegate.Broadcast (); PRAGMA_ENABLE_DEPRECATION_WARNINGS FCoreDelegates::GetApplicationWillTerminateDelegate ().Broadcast (); }, TStatId (), NULL , ENamedThreads::GameThread); FTaskGraphInterface::Get ().WaitUntilTaskCompletes (WillTerminateTask); FAndroidMisc::NonReentrantRequestExit (); })); FAppEventManager::GetInstance ()->EnqueueAppEvent (APP_EVENT_STATE_ON_DESTROY); break ; } }
其它交互 如之前所述,目前所有的事件都会从GameActivity
转发到c++层处理。这里就不再赘述。
Cocos 启动 和Unreal选择使用NativeActivity不同,cocos选择了使用安卓原生(jetpack)提供的GameActivity
作为应用的入口:(https://developer.android.com/games/agdk/game-activity )
按照官方的说法:我们强烈建议您将 GameActivity
用于新游戏和其他 C/C++ 密集型应用。如果您已有 NativeActivity
应用,我们建议您迁移到 GameActivity
。
具体的启动流程如下图: 从上图中我们可以很轻易发现除了使用了原生的GameActivity
之外,没有再额外创建一个新的事件交互线程去处理安卓事件。实际上这是官方推荐的使用方式 除此之外,两边的共同点都是
在native线程而非java线程中处理game loop
游戏初始化在native线程中
事件交互 相比之下,对于事件的处理在cocos中就简单很多,以下是cocos主循环的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 int32_t AndroidPlatform::loop () { IXRInterface *xr = CC_GET_XR_INTERFACE (); while (true ) { int events; struct android_poll_source *source; while ((ALooper_pollAll (_loopTimeOut, nullptr , &events, reinterpret_cast <void **>(&source))) >= 0 ) { if (source != nullptr ) { source->process (_app, source); } if (_app->destroyRequested) { break ; } } if (_app->destroyRequested) { break ; } if (xr && !xr->platformLoopStart ()) continue ; if (xr) xr->platformLoopEnd (); } onDestroy (); }
相比于Unreal,cocos通过利用安卓原生的能力,相比之下就简单直白很多。唯一的问题是占用了一部分主循环的时间处理。不过处理switch-case省下的时间未必就比跨线程操作队列来得更迅速。
Unity 在unity6之前,gameloop是实现在java线程的,但是在unity6上,已经将GameActivity
作为了可选的安卓入口(在设置中搜索Application Entry Point)。因此交互流程大同小异,不再赘述(最主要是看不到代码XD)。
分析 可以发现,支持安卓的引擎在实现安卓的入口时,几乎都倾向于使用GameActivity
的方式,unreal的实现中也几乎是生造了一个类似的实现。对比一下,我们可以发现有这两点优势:
减少大量jni的接口,基本管理frameloop需要做的jni接口都已经由native_app_glue
实现了
更好地控制app和安卓层的交互,消息交互部分变得更清晰了,在主线程处理即可
生命周期更好管理,native线程天然和GameActivity
生命周期绑定,尤其是“唯一性”