Cocos2d-x 3.x启动过程

时间:2023-02-09 00:02:15
目标:
- 理解cocos2d-x启动过程
- 对整个框架有个初步认识

程序入口

我们在学习C/C++的时候,知道每个C/C++程序都有一个且只有一个入口点(main函数),同样我们通过Cocos引擎生成的初始项目代码也有入口点,由于此时我们所写的代码是区别于控制台的代码,入口点函数便不再是main(这是在学习控制台程序时的入口点)。有过Win32应用开发经验的人知道,每个程序的入口点函数是_tWinMain(这个在win32筛选器下的main.cpp可以找到),它的函数原型如下:

int APIENTRY _tWinMain(HINSTANCE hInstance,
                       HINSTANCE hPrevInstance,
                       LPTSTR    lpCmdLine,
                       int       nCmdShow)

知道了入口点,我们就可以从入口点函数一条一条代码的往下读

int APIENTRY _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的类。实例化之后,通过调用getInstance()方法获得实例指针(单例模式),然后调用run()方法,整个_tWinMain函数到此就结束了。众所周知,在C/C++中,main函数执行完了,那么整个程序也就退出了,但是为什么在这里,程序能够一直运行呢?

真正的开始—run()

接着上面的疑问,我们试着调试程序,顺表看看run()的实现。
int Application::run()
{
    //在注册表中写入对于PVRFrame图像文件帧的显示和隐藏的设置 
    PVRFrameEnableControlWindow(false);

    // Main message loop:
    LARGE_INTEGER nLast;
    LARGE_INTEGER nNow;

    //这是个WIN API,使用QueryPerformanceCounter来查询定时器的计数值,如果硬件里有定时器,它就会启动这个定时器,并且不断获取定时器的值,这样的定时器精度,就跟硬件时钟的晶振一样精确的。
    QueryPerformanceCounter(&nLast);

     //设置GL上下文环境,需要重载实现,否则使用默认配置</span>
    initGLContextAttrs();

    // Initialize instance and cocos2d.
    // 完成应用程序启动前的一些工作,主要完成导演实例化和场景布置等等工作。
    if (!applicationDidFinishLaunching())
    {
        return 1;
    }
    auto director = Director::getInstance();

    //cocos2d::GLViewImp1,获得整个GL视图的管理
    auto glview = director->getOpenGLView();

    // Retain glview to avoid glview being released in the while loop
    glview->retain();//引用计数+1

    while(!glview->windowShouldClose()) //默认返回false
    {
        //针对 每帧渲染时间消耗 与 fps之间的矛盾着的“异常”处理
        //nNow : 本次渲染开始时刻
        //nLast : 上一次渲染结束时刻
        QueryPerformanceCounter(&nNow);
        //如果上一次渲染花费的时间 > 设定的fps 
        if (nNow.QuadPart - nLast.QuadPart > _animationInterval.QuadPart)
        {
            nLast.QuadPart = nNow.QuadPart - (nNow.QuadPart % _animationInterval.QuadPart);

            director->mainLoop(); //如果没有设置nextScene,那么将一直调用对当前场景渲染
            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;
}

void AppDelegate::initGLContextAttrs()
{
    GLContextAttrs glContextAttrs = {8, 8, 8, 8, 24, 8};
    GLView::setGLContextAttrs(glContextAttrs);
}
  • 在调试的过程中我们发现,程序将一直“徘徊”在while(!glview->windowShouldClose())循环处。其实,这个循环就是类似Win32开发当中的消息循环,也就是说,在关闭应用程序之前,整个程序将一直在处于上述这个循环当中。
  • 其实,整个程序的开始是由调用run()开始,run()方法中,前半部分负责初始化应用程序环境,中间部分while循环负责消息循环及场景渲染,后半部份负责清理。

以下函数负责初始化OpenGL视图上下文环境。

void AppDelegate::initGLContextAttrs()
{
    //结构体
    GLContextAttrs glContextAttrs = {8, 8, 8, 8, 24, 8};
    //通过GLView的一些函数可以操作GL视图的界面信息
    GLView::setGLContextAttrs(glContextAttrs);
}
struct GLContextAttrs 
{ //设置渲染所用的调色风格,如:redBits = 8,表示R通道用8bit来表示
    int redBits;
    int greenBits;
    int blueBits;
    int alphaBits;
    int depthBits;
    int stencilBits;
};

准备工作

首先先看下AppDelegate这个类:
class  AppDelegate : private cocos2d::Application
{
public:
    AppDelegate();
    virtual ~AppDelegate();

    virtual void initGLContextAttrs();

    virtual bool applicationDidFinishLaunching();

    virtual void applicationDidEnterBackground();

    virtual void applicationWillEnterForeground();
};

AppDelegate继承于Application,主要实现了6个方法,其中initGLContextAttrs()用于初始化GL视图环境;applicationDidFinishLaunching()是在程序位于前台时候会调用;applicationDidEnterBackground()是程序转向后台时调用,一般实现是暂停游戏;applicationWillEnterForeground()是程序由后台转向前台时调用,一般实现是恢复游戏。

在上述run()方法中,我们发现依次调用了initGLContextAttrs()用于初始化GL视图 和 applicationDidFinishLaunching()用于负责应用程序生成前的一些准备工作,比如实例化导演对象和场景布置:
bool AppDelegate::applicationDidFinishLaunching() {
    auto director = Director::getInstance();
    auto glview = director->getOpenGLView();
    if(!glview) {
     //因为cocos2d底层采用opengl进行渲染,首先需要一张opengl的画布
     //有了画布之后,整个给app才能显示
        glview = GLViewImpl::create("Cpp Empty Test");
        director->setOpenGLView(glview);
    }
    director->setOpenGLView(glview);
    //设置分辨率,因为director->setOpenGLView(glview)传的是指针,所以此处修改,能直接director所指对象的状态。
    glview->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, ResolutionPolicy::NO_BORDER);
    //获得app框架大小,这个值区别于分辨率。screen size
    Size frameSize = glview->getFrameSize();

    vector<string> searchPath;

    // 下面这段代码是cocos2d关于屏幕适配,省略部分代码,不影响讲解
    if (frameSize.height > mediumResource.size.height)
        ...

    //得出屏幕适配方案,设置资源路径
    FileUtils::getInstance()->setSearchPaths(searchPath);

    // 用于调试用的
    director->setDisplayStats(true);

    // FPS 默认是1/60.0
    director->setAnimationInterval(1.0 / 60);

    // 创建场景:通常需要我们写的就是HelloWorld等等具体的游戏场景
    auto scene = HelloWorld::scene();

    // 导演负责将场景布置到opengl视图之上,opengl负责渲染场景
    director->runWithScene(scene);
    return true;
}

上述代码,负责将导演实例化之后,并且为导演分配工具(GL视图)和场景,导演将根据场景布置现场。runWithScene将场景Scene压入场景栈,在Application::run()的循环中,将把该被渲染的场景(被设置为_runningScene的场景)解析成绘图命令(draw command),形成一个queue(绘图队列)。

void Director::runWithScene(Scene *scene)
{
    CCASSERT(scene != nullptr, "This command can only be used to start the Director. There is already a scene present.");
    CCASSERT(_runningScene == nullptr, "_runningScene should be null");
    pushScene(scene);
    startAnimation();
}
void Director::pushScene(Scene *scene)
{
    CCASSERT(scene, "the scene should not null");
    _sendCleanupToScene = false;
    //一个导演并不只是负责一个场景,导演类维护了一个场景栈(保存了所有要渲染的场景)
    _scenesStack.pushBack(scene); 
    //当将一个场景压入场景栈中时,也顺便指定下一个要渲染的场景为压入的场景
    _nextScene = scene;
}
void DisplayLinkDirector::startAnimation()
{
    if (gettimeofday(_lastUpdate, nullptr) != 0)
    {
        CCLOG("cocos2d: DisplayLinkDirector: Error on gettimeofday");
    }
    //这个函数中并没有真正开始播放动画,设置场景无效标志。
    _invalid = false;
    _cocos2d_thread_id = std::this_thread::get_id();
    //设置FPS :帧间隔时间(单位:秒),默认是1.0/60
    Application::getInstance()->setAnimationInterval(_animationInterval);
    // fix issue #3509, skip one fps to avoid incorrect time calculation.
    setNextDeltaTimeZero(true);
}
所谓的“渲染”在mainLoop中进行,但是drawScene并不进行真正的渲染(v3.x版本之后,cocos2d-x改变了他的渲染方式):
void DisplayLinkDirector::mainLoop()
{   //检测Director对象的标志,根据标志选择接下来的动作
    if (_purgeDirectorInNextLoop)
    {
        _purgeDirectorInNextLoop = false;
        purgeDirector(); //清理
    }
    else if (_restartDirectorInNextLoop)
    {
        _restartDirectorInNextLoop = false;
        restartDirector();
    }
    else if (! _invalid)
    {   //检测到场景无效标志,则渲染场景
        drawScene();

        // release the objects
        //因为场景中有各种Sprite/Layer等等对象,这些对象可能采用静态工厂方法创建(如:create),那么这些对象都将在创建的时候被加入到自动释放池,由引擎管理的自动释放池来维护这些对象
        //当“渲染”完场景之后,自动释放池将被调用来清理这些对象。清理的
        //上述glview也是采用静态工厂方法生成,所以为免被自动释放池清理,需要在创建之后retain一次。
        PoolManager::getInstance()->getCurrentPool()->clear();
    }
}
drawScene只是生成一些渲染命令,并将命令压入一个队列:
// Draw the Scene
void Director::drawScene()
{
    // calculate "global" dt
    calculateDeltaTime();

    if (_openGLView)
    { //GL视图轮询事情,这是个空函数
        _openGLView->pollEvents();
    }
    //tick before glClear: issue #533
    if (! _paused)
    {
        _eventDispatcher->dispatchEvent(_eventBeforeUpdate);
        _scheduler->update(_deltaTime);
        _eventDispatcher->dispatchEvent(_eventAfterUpdate);
    }
    _renderer->clear();
    experimental::FrameBuffer::clearAllFBOs();
    /* to avoid flickr, nextScene MUST be here: after tick and before draw. * FIXME: Which bug is this one. It seems that it can't be reproduced with v0.9 */
    if (_nextScene)
    {
        setNextScene();
    }
    pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);

    if (_runningScene)
    {
#if (CC_USE_PHYSICS || (CC_USE_3D_PHYSICS && CC_ENABLE_BULLET_INTEGRATION) || CC_USE_NAVMESH)
        _runningScene->stepPhysicsAndNavigation(_deltaTime);
#endif
        //clear draw stats
        _renderer->clearDrawStats();

        //render the scene//render负责节点树中的各节点渲染需要</span>
        _runningScene->render(_renderer);

        _eventDispatcher->dispatchEvent(_eventAfterVisit);
    }
    // draw the notifications node
    if (_notificationNode)
    {//visit函数负责处理由事情触发的渲染请求</span>
        _notificationNode->visit(_renderer, Mat4::IDENTITY, 0);
    }
    if (_displayStats)
    {
        showStats();
    }
    _renderer->render();
    _eventDispatcher->dispatchEvent(_eventAfterDraw);
    popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
    _totalFrames++;
    // swap buffers
    if (_openGLView)
    {
        _openGLView->swapBuffers();
    }
    if (_displayStats)
    {
        calculateMPF();
    }
}

小结

  1. 程序的路口点是_tWinMain
  2. 程序真的开始是由调用AppDelegate::run()开始,run()中实现了消息循环和场景渲染。
  3. applicationDidFinishLaunching()负责程序开始前的一些准备工作,如场景大小、导演等等的初始化。