Cocos2dx开发之运行与渲染流程分析

时间:2021-06-04 23:02:32

  学习Cocos2dx,我们都知道程序是由 AppDelegate 的方法 applicationDidFinishLaunching 开始,在其中做些必要的初始化,并创建运行第一个 CCScene 即可。但实际我们并不知道程序运行时,何时调用 AppDelegate 的构造函数,析构函数和程序入口函数,这是问题一。另外在实际执行的过程中,程序只调用其构造函数和入口函数,而直到程序结束运行,都没有调用其析构函数,那么程序又是在哪里结束的呢?这是问题二。

  首先,解决问题1,在windows下,可以在applicationDidFinishLaunching 方法内加断点跟踪,堆栈图如下:

  Cocos2dx开发之运行与渲染流程分析

  一个程序一般是由main函数开始,Cocos2dx也不例外,在proj.win32/main.cpp路径下,存在main.cpp文件:

USING_NS_CC;

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();
}

  CCApplication-win32.cpp:

int Application::run()
{
... ... ...
if (!applicationDidFinishLaunching())
{
return ;
} 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);
//开启Cocos的主循环
director->mainLoop();
glview->pollEvents();
}
else
{
Sleep();
}
}
}

  上面能观察到,在调用run方法之前先使用了 AppDelegate app,这样做的原因是 AppDelegate 是 CCApplication 的子类,在创建子类对象的时候,调用其构造函数的同时,父类构造函数也会执行,然后就将 AppDelegate 的对象赋给了 CCApplication 的静态变量。在 AppDelegate 中实现了 applicationDidFinishLaunching 方法,所以在 CCApplication 中 run 方法的开始处调用的就是 AppDelegate 之中的实现。

  同理,在android平台下游戏是从从一个Activity开始的,启动Activity是在文件AppActivity.java中定义的,相关代码如下:

public class __PROJECT_PACKAGE_LAST_NAME_UF__ extends Cocos2dxActivity {

    @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
} static {
System.loadLibrary("game");
}
}

  在游戏启动时,Activity首先会执行静态代码块,加载game.so库,这动态链接库是在用NDK编译的时候生成的;然后就是执行onCreate方法,这里调用了父类Cocos2dxActivity的onCreate方法。 Cocos2dxActivity在

  1. $(sourcedir)\cocos2dx\platform\android\java\src\org\cocos2dx\lib\Cocos2dxActivity.java

  其OnCreate方法代码如下:

    @Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
CocosPlayClient.init(this, false); onLoadNativeLibraries(); sContext = this;
this.mHandler = new Cocos2dxHandler(this);//处理安卓的弹窗等 Cocos2dxHelper.init(this); this.mGLContextAttrs = getGLContextAttrs();//获取OpenGL ES的相关属性
this.init(); if (mVideoHelper == null) {
mVideoHelper = new Cocos2dxVideoHelper(this, mFrameLayout);
} if(mWebViewHelper == null){
mWebViewHelper = new Cocos2dxWebViewHelper(mFrameLayout);
}
}

  一个native的函数,即在java在调用C++代码实现的函数,即需要采用JNI技术(可以看成是Java与C++交互的一个协议~)。方法nativeInit对应的C++实现是在(sourcedir)\samples\\proj.android\jni\main.cpp中:

void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(JNIEnv*  env, jobject thiz, jint w, jint h)
{
auto director = cocos2d::Director::getInstance();
auto glview = director->getOpenGLView();
if (!glview)
{
glview = cocos2d::GLViewImpl::create("Android app");
glview->setFrameSize(w, h);
director->setOpenGLView(glview);
cocos2d::Application::getInstance()->run();
} ,,, ,,,, ,,,
}

  由 Android 启动一个应用,通过各处调用,最终执行到了 Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit 函数,充当了main函数的功能,开启游戏的主循环。 到此问题一得以解决。

  有个地方要注意的,就是上面run方法里并没有循环退出条件,所以 run 方法永远不会返回。那么是怎么结束的呢?这便抛出了问题2,下面分析一下:

void Director::purgeDirector()
{
... .... ...
// OpenGL view
if (_openGLView)
{
_openGLView->end();
_openGLView = nullptr;
}// delete Director
release();
} void CCEGLView::end()
{
glfwTerminate();
delete this;
exit();
}

  游戏的运行以场景为基础,每时每刻都有一个场景正在运行,其内部有一个场景栈,遵循后进后出的原则,当我们显示的调用 end() 方法,或者弹出当前场景之时,其自动判断,如果没有场景存在,也会触发 end() 方法,以说明场景运行的结束,而游戏如果没有场景,就像演出没有了舞台,程序进入最后收尾的工作。

  程序运行时期,由 mainLoop 方法维持运行着游戏之内的各个逻辑,当在弹出最后一个场景,或者直接调用 CCDirector::end(); 方法后,触发游戏的清理工作,执行 purgeDirector 方法,从而结束了 CCEGLView(不同平台不同封装,PC使用OpenGl封装,移动终端封装的为 OpenGl ES) 的运行,调用其 end() 方法,从而直接执行 exit(0); 退出程序进程,从而结束了整个程序的运行。

  参考资料链接:

  http://blog.csdn.net/maximuszhou/article/details/39448971

  https://www.cnblogs.com/Monte/p/6735061.html

  http://www.cocoachina.com/cocos/20130607/6356.html

  Cocos2dx引擎的架构,我们可以总结成下面这个简单划分的模块系统来深入理解学习:

  Cocos2dx开发之运行与渲染流程分析

  游戏主循环

  在上面流程分析时候能看到所有的事件和渲染的内容都是实现在mainLoop的游戏主循环中,每帧做的内容如下:

  Cocos2dx开发之运行与渲染流程分析

  UI树遍历

  直接通过Cocos源码理解遍历过程,Node::Visit()方法如下:

void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
// quick return if not visible. children won't be drawn.
if (!_visible)
{
return;
} uint32_t flags = processParentFlags(parentTransform, parentFlags); // IMPORTANT:
// To ease the migration to v3.0, we still support the Mat4 stack,
// but it is deprecated and your code should not rely on it
_director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
_director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW, _modelViewTransform); //(mask 2) bool visibleByCamera = isVisitableByVisitingCamera(); int i = ; if(!_children.empty())
{
sortAllChildren();
// draw children zOrder < 0 位于父节点之后
for( ; i < _children.size(); i++ )
{
auto node = _children.at(i); if (node && node->_localZOrder < )
node->visit(renderer, _modelViewTransform, flags); //(mask 1
else
break;
}
// self draw
if (visibleByCamera)
this->draw(renderer, _modelViewTransform, flags);
    
for(auto it=_children.cbegin()+i; it != _children.cend(); ++it)
(*it)->visit(renderer, _modelViewTransform, flags);
}
else if (visibleByCamera)
{
this->draw(renderer, _modelViewTransform, flags);
}
_director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
}

  模型视图变换矩阵

  Node维护有一模型视图变换矩阵,其由父亲的模型视图变换矩阵右乘当前节点在本地坐标系中的变换矩阵得到(如上 mask 2)。在遍历时,根节点的变换矩阵为单位矩阵,一次向下级传递自身的模型视图变换矩阵来计算子元素的模型视图变换矩阵(如上 mask 1),最后这个变换矩阵连同元素相关信息被传入OpenGL ES渲染管线。 通过传递并右乘的方式,有利于确保父节点下面的子节点都跟父亲做相同的模型视图变换,如根节点缩放并位移,其儿子也进行相同的矩阵变换。

  新绘制系统

  在Cocos2dx 2.x旧引擎版本里,每个UI元素的绘制逻辑(即渲染的GL命令)都分布在对应的内部draw函数中,紧密跟随UI树的遍历,换句话说就是遍历某个父节点得到的每个儿子马上执行绘制逻辑。这样的设计明显会带来两个问题,是多个层级之间(即不同父节点下的子节点)无法调整绘制的顺序;是不容易扩展对绘制性能的优化(如自动批处理)。为解决这些问题,3.x版本改进有以下的特点:

  1)将绘制逻辑从主循环的UI树遍历中分离

  Cocos2dx开发之运行与渲染流程分析

  2)运用应用程序级别视口裁剪。若一个UI元素在场景中的坐标位于视窗区域以外,它不会将任何绘制命令发送到绘制栈。这将减少绘制栈上绘制命令的数量,也将减少绘制命令的排序时间,还减少对GPU的浪费(不同于OpenGL ES层在图元装配阶段将位于视口之外的图元丢弃或者裁剪,因为这阶段的作用说明已经发送了绘制指令,处于渲染管线工序中的某一步了,比较耗性能)。

void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
#if CC_USE_CULLING
// Don't do calculate the culling if the transform was not updated
_insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds; if(_insideBounds)
#endif
{
_quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, , transform, flags);
renderer->addCommand(&_quadCommand);
}
}

  3)采用自动批绘制技术。不需要像2.x以前要把每个元素添加到一个spriteBatchNode上,在3.x引擎下当不同类型的UI元素的对应相关的绘制指令(即QuadCommond)在执行顺序上相邻,并且使用相同的纹理,着色器等绘制属性时,这些QuadCommond会自动组合到一起,形成一次绘制,即只会调用一次的OpenGL绘制指令。

  Cocos2dx开发之运行与渲染流程分析

  以一Sprite实际渲染分析下这个渲染流程:

        _quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, , transform, flags);
renderer->addCommand(&_quadCommand);

  绘制流程可以分为3个阶段:生成绘制指令->对绘制指令进行排序->执行绘制指令

  生成绘制指令,指向renderer发送一RendererCommond(如QuadCommond)绘制指令,该指令不执行任何GL绘制命令,renderer会将RenderCommond放到绘制栈中。等UI元素全部遍历完毕就开始执行栈中的所有RendererCommond。这样抽离出来的好处是方便统一处理全部的绘制命令,一方面可以针对绘制做一些优化,如相邻且使用相同纹理的QuadCommond执行自动批;另一方面可以灵活调整不同UI层级之间元素的绘制顺序

  绘制排序,指绘制命令不一定是UI元素被遍历的顺序,3.x引擎可以使用globalZOrder变量直接设置元素的绘制顺序。绘制先后首先由globalZOrder决定,然后才是遍历顺序,如下图:

  Cocos2dx开发之运行与渲染流程分析