Cocos2dx源碼賞析(1)之啓動流程與主循環

Cocos2dx源碼賞析(1)之啓動流程與主循環

咱們知道Cocos2dx是一款開源的跨平臺遊戲引擎,而學習開源項目一個較實用的辦法就是讀源碼。所謂,「源碼以前,了無祕密」。而筆者從事的也是遊戲開發工做,所以,經過梳理下源碼的脈絡,來加深對Cocos2dx遊戲引擎的理解。html

既然,Cocos2dx是跨平臺的,那麼,就有針對不一樣平臺運行的入口以及維持引擎運轉的「死循環」。下面,就分別從Windows、Android、iOS三個平臺說明下Cocos2dx從啓動到進入主循環的過程。java

一、Windows

以引擎下的cpp-empty-test項目工程爲例:
Windows工程的入口函數爲cpp-empty-test/win32/main.cpp中的_tWinMain函數。android

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(經過預處理宏來針對不一樣的平臺執行不一樣的代碼)。這裏,並無作什麼特別的處理,只是做了下相應的初始化的工做。ios

而在CCApplication-win32.h和CCApplication-win32.cpp代碼中都有這樣的宏判斷:windows

#if CC_TARGET_PLATFORM == CC_PLATFORM_WIN32

繼續追蹤下去,能夠發現CC_PLATFORM_WIN32在定義了WIN32宏時定義。架構

接下來,便執行Application::getInstance()->run()代碼,這裏的Application爲單例的實現,這也是Cocos2dx單例經常使用的實現方式,在2.x版本的引擎中,單例的實現爲sharedApplication,這是仿照Objective-C的寫法。繼續看CCApplication-win32中的run方法:app

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的實現:ide

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平臺從啓動到主循環,代碼執行的流程,簡單的梳理,能夠知道引擎代碼是如何架構的。oop

二、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引擎從啓動到進入死循環的過程。

三、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/ 版權全部,歡迎保留原文連接進行轉載 :)

相關文章
相關標籤/搜索