目标:
- 理解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();
}
}
小结
- 程序的路口点是_tWinMain
- 程序真的开始是由调用AppDelegate::run()开始,run()中实现了消息循环和场景渲染。
- applicationDidFinishLaunching()负责程序开始前的一些准备工作,如场景大小、导演等等的初始化。