Cocos2dx源碼賞析(1)之啟動流程與主循環
我們知道Cocos2dx是一款開源的跨平台游戲引擎,而學習開源項目一個較實用的辦法就是讀源碼。所謂,“源碼之前,了無秘密”。而筆者從事的也是游戲開發工作,因此,通過梳理下源碼的脈絡,來加深對Cocos2dx游戲引擎的理解。
既然,Cocos2dx是跨平台的,那么,就有針對不同平台運行的入口以及維持引擎運轉的“死循環”。下面,就分別從Windows、Android、iOS三個平台說明下Cocos2dx從啟動到進入主循環的過程。
1、Windows
以引擎下的cpp-empty-test
項目工程為例:
Windows工程的入口函數為cpp-empty-test/win32/main.cpp中的_tWinMain
函數。
int WINAPI _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// create the application instance
AppDelegate app;
return Application::getInstance()->run();
}
這里,定義了一個AppDelegate類型的棧對象app。而AppDelegate繼承自Application,所以這里會先初始化父類Application。再看下Application的實現,注意是進到CCApplication-win32.h和CCApplication-win32.cpp里。當然,Application還繼續繼承自ApplicationProtocol(通過預處理宏來針對不同的平台執行不同的代碼)。這里,並沒有做什么特別的處理,只是作了下相應的初始化的工作。
而在CCApplication-win32.h和CCApplication-win32.cpp代碼中都有這樣的宏判斷:
#if CC_TARGET_PLATFORM == CC_PLATFORM_WIN32
繼續追蹤下去,可以發現CC_PLATFORM_WIN32
在定義了WIN32宏時定義。
接下來,便執行Application::getInstance()->run()
代碼,這里的Application為單例的實現,這也是Cocos2dx單例常用的實現方式,在2.x版本的引擎中,單例的實現為sharedApplication,這是仿照Objective-C的寫法。繼續看CCApplication-win32中的run
方法:
int Application::run()
{
PVRFrameEnableControlWindow(false);
// Main message loop:
LARGE_INTEGER nLast;
LARGE_INTEGER nNow;
QueryPerformanceCounter(&nLast);
initGLContextAttrs();
// Initialize instance and cocos2d.
if (!applicationDidFinishLaunching())
{
return 1;
}
auto director = Director::getInstance();
auto glview = director->getOpenGLView();
// Retain glview to avoid glview being released in the while loop
glview->retain();
while(!glview->windowShouldClose())
{
QueryPerformanceCounter(&nNow);
if (nNow.QuadPart - nLast.QuadPart > _animationInterval.QuadPart)
{
nLast.QuadPart = nNow.QuadPart - (nNow.QuadPart % _animationInterval.QuadPart);
director->mainLoop();
glview->pollEvents();
}
else
{
Sleep(1);
}
}
// Director should still do a cleanup if the window was closed manually.
if (glview->isOpenGLReady())
{
director->end();
director->mainLoop();
director = nullptr;
}
glview->release();
return 0;
}
這里主要先看下applicationDidFinishLaunching()的調用,applicationDidFinishLaunching是虛函數,這里會調到子類AppDelegate中的applicationDidFinishLaunching的實現:
bool AppDelegate::applicationDidFinishLaunching()
{
auto director = Director::getInstance();
auto glview = director->getOpenGLView();
if(!glview) {
glview = GLViewImpl::create("Cpp Empty Test");
director->setOpenGLView(glview);
}
director->setOpenGLView(glview);
glview->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, ResolutionPolicy::NO_BORDER);
director->setAnimationInterval(1.0f / 60);
auto scene = HelloWorld::scene();
director->runWithScene(scene);
return true;
}
這里對代碼做了適當的刪減。可以看到在AppDelegate的applicationDidFinishLaunching主要做了些跟游戲初始化相關的處理。例如,初始化導演類,設置OpenGL視圖,設置適配方式,設置幀率以及初始化場景和運行該場景等。基本這個方法,也可以當作我們游戲代碼初始化的入口。
再回到CCApplication的run方法,繼續往下看。這里,有個while循環,至此,就找到了引擎的“死循環”了。在這個循環中,調用了導演類的mainLoop主循環方法,而在mainLoop中,主要控制渲染,定時器,動畫,事件循環等處理。后續會分析這相關的部分,這里就不過多介紹了。至此,就是Cocos2dx在Windows平台從啟動到主循環,代碼執行的流程,簡單的梳理,可以知道引擎代碼是如何架構的。
2、Android
在Android平台的應用,一般由多個Activity組成,一個Activity代表一個“窗口”,Activity根據應用前后台切換有對應的聲明周期狀態。在配置清單文件中聲明了
<action android:name="android.intent.action.MAIN" />
即代表該Acitivity為應用的入口Activity。而入口Activity也一般稱為閃屏頁(Splash)或啟動頁,用來呈現公司的或運營的合作伙伴Logo,之后再切換到主Activity。在Cocos2dx游戲中,主Activity一般是繼承Cocos2dx引擎封裝的Cocos2dxActivity類。先看onCreate()方法:
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
onLoadNativeLibraries();
sContext = this;
Cocos2dxHelper.init(this);
this.init();
}
對onCreate里的代碼做了精簡,只列舉了比較重要的幾個方法。首先onLoadNativeLibraries
方法:
protected void onLoadNativeLibraries() {
try {
ApplicationInfo ai = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
Bundle bundle = ai.metaData;
String libName = bundle.getString("android.app.lib_name");
System.loadLibrary(libName);
} catch (Exception e) {
e.printStackTrace();
}
}
該方法會讀取配置在Manifest里中的meta-data
標簽的字段為android.app.lib_name
的值,來加載動態庫。即為:
<meta-data android:name="android.app.lib_name"
android:value="cpp_empty_test" />
同樣,以cpp_empty_test的項目為例,可知這里要加載名字為libcpp_empty_test.so
動態庫。由於Cocos2dx引擎核心部分是C++實現,在Android平台通過jni的方式來調用和啟動引擎。
再回到Cocos2dxActivity中的onCreate,繼續往下進行。可以看到:
sContext = this;
sContext是Cocos2dxActivity的實例,被聲明為靜態的,通過這種實現了單例的效果。在需要Context實例的地方以及需要調用Cocos2dxActivity方法的地方,可以直接用該實例。
Cocos2dxHelper.init(this);
Cocos2dxHelper的init中主要是一些對象的初始化,例如:聲音,音效,重力感應,Asset管理等。
接下來,調用了Cocos2dxActivity的init方法里:
public void init() {
ViewGroup.LayoutParams framelayout_params =
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
mFrameLayout = new ResizeLayout(this);
mFrameLayout.setLayoutParams(framelayout_params);
ViewGroup.LayoutParams edittext_layout_params =
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
Cocos2dxEditBox edittext = new Cocos2dxEditBox(this);
edittext.setLayoutParams(edittext_layout_params);
mFrameLayout.addView(edittext);
this.mGLSurfaceView = this.onCreateView();
mFrameLayout.addView(this.mGLSurfaceView);
// Switch to supported OpenGL (ARGB888) mode on emulator
if (isAndroidEmulator())
this.mGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
this.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer());
this.mGLSurfaceView.setCocos2dxEditText(edittext);
setContentView(mFrameLayout);
}
該方法主要設置要顯示的視圖界面,即mFrameLayout。重點關注這幾行代碼:
this.mGLSurfaceView = this.onCreateView();
mFrameLayout.addView(this.mGLSurfaceView);
this.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer());
在onCreateView方法中,返回了一個Cocos2dxGLSurfaceView對象,並將該對象添加到了幀布局的容器對象(mFrameLayout)中。首先,了解下Cocos2dxGLSurfaceView類的實現:
public class Cocos2dxGLSurfaceView extends GLSurfaceView {
private Cocos2dxRenderer mCocos2dxRenderer;
public void setCocos2dxRenderer(final Cocos2dxRenderer renderer) {
this.mCocos2dxRenderer = renderer;
this.setRenderer(this.mCocos2dxRenderer);
}
public void onResume() {
super.onResume();
this.setRenderMode(RENDERMODE_CONTINUOUSLY);
this.queueEvent(new Runnable() {
public void run() {
Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleOnResume();
}
});
}
public void onPause() {
this.queueEvent(new Runnable() {
public void run() {
Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleOnPause();
}
});
this.setRenderMode(RENDERMODE_WHEN_DIRTY);
//super.onPause();
}
}
(上述代碼有做刪減,只保留需要說明的地方)
Cocos2dxGLSurfaceView繼承自GLSurfaceView,通過閱讀GLSurfaceView文檔可知,GLSurfaceView又繼承自SurfaceView,而SurfaceView又進一步繼承自View。GLSurfaceView封裝了OpenGL ES所需的運行環境,同時能讓OpenGL ES渲染的內容直接生成在Android的View視圖上。繪制渲染時,用戶可以自定義渲染器(GLSurfaceView.Renderer),該渲染器運行在單獨的線程里,獨立於UI線程。GLSurfaceView還能適應於Activity的聲明周期的變化做相應的處理(例如:onPause、onResume等)。
GLSurfaceView的初始化過程中,需要設置渲染器。即調用setRenderer方法。
Cocos2dxGLSurfaceView類中的onResume和onPause方法,這兩個方法受Activity的相應的聲明周期的方法影響, Activity窗口暫停(pause)或恢復(resume)時,GLSurfaceView都會收到通知,此時它的onPause方法和 onResume方法應該被調用。這樣GLSurfaceView就會暫停或恢復它的渲染線程,以便它及時釋放或重建OpenGL的資源。其中都分別調用了queueEvent
的方法。這里,需要注意的是,Android的UI運行在主線程,而OpenGL的GLSurfaceView運行在一個單獨的線程中,因此,需要調用queueEvent
來給OpenGL線程分發調用,來達到兩個線程間通信。最后都交給Cocos2dxRenderer處理。
最后,再重點看下渲染器類Cocos2dxRenderer的實現:
public class Cocos2dxRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(final GL10 GL10, final EGLConfig EGLConfig) {
Cocos2dxRenderer.nativeInit(this.mScreenWidth, this.mScreenHeight);
this.mLastTickInNanoSeconds = System.nanoTime();
mNativeInitCompleted = true;
}
public void onSurfaceChanged(final GL10 GL10, final int width, final int height) {
Cocos2dxRenderer.nativeOnSurfaceChanged(width, height);
}
public void onDrawFrame(final GL10 gl) {
if (sAnimationInterval <= 1.0 / 60 * Cocos2dxRenderer.NANOSECONDSPERSECOND) {
Cocos2dxRenderer.nativeRender();
} else {
final long now = System.nanoTime();
final long interval = now - this.mLastTickInNanoSeconds;
if (interval < Cocos2dxRenderer.sAnimationInterval) {
try {
Thread.sleep((Cocos2dxRenderer.sAnimationInterval - interval) / Cocos2dxRenderer.NANOSECONDSPERMICROSECOND);
} catch (final Exception e) {
}
}
this.mLastTickInNanoSeconds = System.nanoTime();
Cocos2dxRenderer.nativeRender();
}
}
}
首先,Cocos2dxRenderer繼承了渲染器類GLSurfaceView.Renderer,並重寫了以下上個方法:
onSurfaceCreated:
該方法是當Surface被創建的時候,會調用,即應用程序第一次運行的時候。當設備被喚醒或用戶從其它Activity切換回來的時候,該方法也可能被調用。因此,該方法可能會被多次調用。一般會在該方法中,完成一些OpenGL ES的初始化工作。
onSurfaceChanged:
該方法是在Surface被創建以后,每次Surface尺寸發生變化時(例如:橫豎屏切換),該方法會被調用。
onDrawFrame:
繪制的每一幀,該方法都會被調用。
其實,看到onDrawFrame中的代碼,可以知道Cocos2dx引擎在Android平台的“死循環”在該方法中。最后,通過jni的方式調用nativeRender
來啟動導演類的主循環。
熟悉jni調用的可以知道,nativeRender是聲明為native的方法,Cocos2dxRenderer.nativeRender最終會調到Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp類中:
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRender(JNIEnv* env) {
cocos2d::Director::getInstance()->mainLoop();
}
可以看到,跟Windows平台的一樣,最終調用到導演類的mainLoop方法,殊途同歸。以上便是,Android平台Cocos2dx引擎從啟動到進入死循環的過程。
3、iOS
同樣,以引擎下的cpp-empty-test
項目工程為例:
iOS工程的入口函數為cpp-empty-test/proj.ios/main.cpp中的main
函數。
int main(int argc, char *argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
int retVal = UIApplicationMain(argc, argv, nil, @"AppController");
[pool release];
return retVal;
}
在iOS應用中,都必須在函數main中調用UIApplicationMain
方法來啟動應用和設置相應的事件循環。UIApplicationMain函數原型如下:
UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);
其中,argc是參數的個數,argv是可變的參數列表,principalClassName代表的是一個繼承自UIApplication類的類名,delegateClassName是應用程序的代理類名稱。在跟蹤AppController類代碼之前,有必要先了解下iOS應用的運行狀態以及相應的生命周期方法:
- Not Running(非運行狀態):應用沒有運行或被系統終止。
- Inactive(前台非活動狀態):應用正在進入前台狀態,但是還不能接受事件處理。
- Active(前台活動狀態):應用進入前台,能接受事件處理。
- Background(后台狀態):應用進入后台后,依然能夠執行代碼。如果有可執行的代碼,就會執行,如果沒有可執行的代碼或可執行的代碼執行完畢,應用會馬上進入掛起狀態。
- Suspended(掛起狀態):處於掛起的應用進入一種“冷凍”狀態,不能執行代碼。如果系統內存不夠,應用會被終止。
生命周期方法有:
application:didFinishLaunchingWithOptions::
應用程序啟動並進行初始化時會調用該方法。
applicationDidBecomeActive:
應用程序進入前台並處於活動狀態時調用該方法。
applicationWillResignActive:
應用程序從活動狀態進入非活動狀態時調用該方法。
applicationDidEnterBackground:
應用程序進入后台時調用該方法。
applicationWillEnterForeground:
應用程序進入前台,但還沒有處於活動狀態時調用該方法。
applicationWillTerminate:
應用程序被終止時調用該方法。
進入AppController類,AppController實現了UIApplicationDelegate,並重寫了相應的生命周期的方法。那么,重點看application:didFinishLaunchingWithOptions:
方法:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
cocos2d::Application *app = cocos2d::Application::getInstance();
app->initGLContextAttrs();
cocos2d::GLViewImpl::convertAttrs();
// Override point for customization after application launch.
// Add the view controller's view to the window and display.
window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
CCEAGLView *eaglView = [CCEAGLView viewWithFrame: [window bounds]
pixelFormat: (NSString*)cocos2d::GLViewImpl::_pixelFormat
depthFormat: cocos2d::GLViewImpl::_depthFormat
preserveBackbuffer: NO
sharegroup: nil
multiSampling: NO
numberOfSamples: 0];
// Use RootViewController manage CCEAGLView
viewController = [[RootViewController alloc] initWithNibName:nil bundle:nil];
viewController.wantsFullScreenLayout = YES;
viewController.view = eaglView;
// Set RootViewController to window
if ( [[UIDevice currentDevice].systemVersion floatValue] < 6.0)
{
// warning: addSubView doesn't work on iOS6
[window addSubview: viewController.view];
}
else
{
// use this method on ios6
[window setRootViewController:viewController];
}
[window makeKeyAndVisible];
[[UIApplication sharedApplication] setStatusBarHidden: YES];
// IMPORTANT: Setting the GLView should be done after creating the RootViewController
cocos2d::GLViewImpl *glview = cocos2d::GLViewImpl::createWithEAGLView(eaglView);
cocos2d::Director::getInstance()->setOpenGLView(glview);
app->run();
return YES;
}
這里主要是實例化一個UIWindow對象,每一個UIWindow對象上面都有一個根視圖,它所對應的控制器為根視圖控制器(ViewController),最后把根視圖控制器放到UIWindow上。最后,app->run()
會調用到CCApplication-ios.mm(這個也是根據項目中的預編譯宏實現)中的run方法:
int Application::run()
{
if (applicationDidFinishLaunching())
{
[[CCDirectorCaller sharedDirectorCaller] startMainLoop];
}
return 0;
}
這里有個跟生命周期方法類似的名字applicationDidFinishLaunching,這個會調到ApAppDelegate的applicationDidFinishLaunching方法,這點跟Windows平台類似,一般是在這個方法做跟游戲內容相關的初始化。run方法接下來,就是調startMainLoop方法,看這個名字,知道跟要找的目標很接近了,再繼續跟下去。這里會調到CCDirectorCaller-ios.mm中的startMainLoop方法:
-(void) startMainLoop
{
// Director::setAnimationInterval() is called, we should invalidate it first
[self stopMainLoop];
displayLink = [NSClassFromString(@"CADisplayLink") displayLinkWithTarget:self selector:@selector(doCaller:)];
[displayLink setFrameInterval: self.interval];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
首先是通過NSClassFromString動態加載CADisplayLink類,然后調用了該類的displayLinkWithTarget方法,該方法類似定時器的功能,周期的調用該selector包裝的方法(即:doCaller:方法):
-(void) doCaller: (id) sender
{
if (isAppActive) {
cocos2d::Director* director = cocos2d::Director::getInstance();
[EAGLContext setCurrentContext: [(CCEAGLView*)director->getOpenGLView()->getEAGLView() context]];
director->mainLoop();
}
}
至此,我們就找到了導演類的mainLoop方法,開啟了引擎的主循環。以上,便是Cocos2dx引擎在iOS平台從啟動到進入主循環的過程。
通過以上簡單的分析,我們知道,Cocos2dx引擎利用了相應的平台循環方式來調用導演類的主循環來進入引擎的內部工作。下一篇繼續通過代碼的方式來梳理下Cocos2dx的渲染過程。如果在本篇中,有你覺得不對的地方,也歡迎來和我討論。
技術交流QQ群:528655025
作者:AlphaGL
出處:http://www.cnblogs.com/alphagl/
版權所有,歡迎保留原文鏈接進行轉載 😃