Cocos2dx学习笔记(10)——帧循环

学完了动画,让我们来学习学习新的知识。

本文仅供个人记录和复习,不用于其他用途

什么是帧循环

我们之前对于帧已经有了一个基本的概念,事实上,游戏乃至图形界面都是不断的绘图,然而这些绘图必须遵守一定的逻辑,也就是所谓的游戏逻辑。游戏逻辑控制游戏的内容,会根据用户的输入和时间的流逝而改变。因此,游戏可以抽象为不断地重复以下动作:

  • 处理用户输入
  • 处理定时事件
  • 绘图

游戏的主循环基本上就是如此,它会反复执行以上的动作,保持游戏的进程,直到玩家退出游戏。

mainLoop方法

CCDirector::mainLoop()方法是定义在CCDirector中的抽象方法,它的具体实现在CCDisplayLinkDirector类(3.0找不到的话就把CC去掉)。这个方法负责调用定时器、绘图、发送全局通知、并处理内存回收池。该方法按帧调用,每帧调用一次,而帧间间隔取决于两个因素:预设频率(默认为60帧每秒)和每帧计算量的大小。我们在玩游戏的时候都知道,如果电脑的配置较低,哪怕你设置的帧率为60,电脑的计算力也无法达到60帧每秒。因此,就会出现游戏画面不流畅。下面是从DisplayLinkDirector类中找到的关于mainLoop()方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void DisplayLinkDirector::mainLoop()
{
if (_purgeDirectorInNextLoop)
{
_purgeDirectorInNextLoop = false;
purgeDirector();
}
else if (_restartDirectorInNextLoop)
{
_restartDirectorInNextLoop = false;
restartDirector();
}
else if (! _invalid)
{
drawScene();
// release the objects
PoolManager::getInstance()->getCurrentPool()->clear();
}
}

mainLoop()主要是先查看是否需要释放Director,一般是游戏结束时执行。第三个有一个drawScene()方法,从字面意义上来看就是绘制场景,遍历每一个节点进行绘图。最后将这一帧弹出内存回收池,删除这一帧的内容。具体代码如下:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
void Director::drawScene()
{
// calculate "global" dt
calculateDeltaTime();
if (_openGLView)
{
_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
_runningScene->render(_renderer);
_eventDispatcher->dispatchEvent(_eventAfterVisit);
}
// draw the notifications node
if (_notificationNode)
{
_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();
}
}

可以发现drawScene主要用于处理OpenGL和一些细节,如计算FPS、帧间时间差等,这里我们主要进行了以下操作:

  • 计算时间差,2帧之前的时间差,calculateDeltaTime()
  • 定时任务调用_scheduler->update(_deltaTime)
  • 事件处理EventAfterUpdate
  • 设置当前的下一个场景,主要就是把当前场景释放掉,之后把_nextScene设置为当前的场景
  • 调用当前场景的渲染方法_runningScene->render(_renderer)
  • 事件处理意思是Visit调用完了,_eventAfterVisit
  • 调用渲染引擎进行渲染_renderer->render()
  • 事件处理_eventAfterDraw
  • 调用openGLView平台提供的屏幕显示方法,_openGLView->swapBuffers()

游戏的开始

说起来,我们介绍了那么多概念,貌似还没有讲游戏是如何开始的。事实上,游戏的开始文件是main.h,也就是游戏的入口函数,位于win32目录下。查看源文件,能够发现如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
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对象,AppDelegate继承自CCApplication,在创建AppDelegate对象的时候就会隐式调用CCApplication构造函数,在这个构造函数里边会将AppDelegatethis指针传递给全局共享对象sm_pSharedApplication,如下:

1
2
3
4
5
6
7
8
9
10
Application::Application() :
// 初始化win32应用程序对象
m_openURLDelegate(nullptr)
{
// 用于控制帧数的计数值
m_nAnimationInterval.QuadPart = 0;
CC_ASSERT(! sm_pSharedApplication);
// 全局共享对象
sm_pSharedApplication = this;
}

接下来调用Application::getInstance()->run()启动游戏,如下:

1
2
3
4
5
6
7
8
9
10
11
int Application::run()
{
// Initialize instance and cocos2d.
if (!applicationDidFinishLaunching())
{
return 0;
}
GLViewImpl::sharedOpenGLView()->Run();
return 0;
}

这里可以看到,run()启动了applicationDidFinishLaunching()方法。我们之前介绍过,这个方法用于设置游戏启动相关参数,同时也执行我们自定义的代码。

1
director->setOpenGLView(glview);

另外呢,在applicationDidFinishLaunching()中可以找到这一行代码。它设置了glview对象之后就开始运行场景,GLView主要就是就是负责窗口管理的工作。

1
2
3
4
5
6
int GLViewImpl::Run()
{
// XAML version does not have a run loop
m_running = true;
return 0;
};

至于Run()方法就是将m_running设置为true,用于启动。我们再来看看使用了m_running的方法:

1
2
3
4
5
6
7
8
9
10
11
12
void GLViewImpl::Render()
{
OnRendering();
}
void GLViewImpl::OnRendering()
{
if(m_running && m_initialized)
{
Director::getInstance()->mainLoop();
}
}

可以看到,Rander()渲染方法执行了OnRendering()。而当m_runningm_initialized全为true时,便调用我们的mainLoop()方法。至此,便是游戏的主循环。至于具体的渲染过程,之后再讲。