學習Cocos2dx,我們都知道程序是由 AppDelegate 的方法 applicationDidFinishLaunching 開始,在其中做些必要的初始化,並創建運行第一個 CCScene 即可。但實際我們並不知道程序運行時,何時調用 AppDelegate 的構造函數,析構函數和程序入口函數,這是問題一。另外在實際執行的過程中,程序只調用其構造函數和入口函數,而直到程序結束運行,都沒有調用其析構函數,那么程序又是在哪里結束的呢?這是問題二。
首先,解決問題1,在windows下,可以在applicationDidFinishLaunching 方法內加斷點跟蹤,堆棧圖如下:
一個程序一般是由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 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); //開啟Cocos的主循環 director->mainLoop(); glview->pollEvents(); } else { Sleep(1); } } }
上面能觀察到,在調用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在
- $(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(0); }
游戲的運行以場景為基礎,每時每刻都有一個場景正在運行,其內部有一個場景棧,遵循后進后出的原則,當我們顯示的調用 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引擎的架構,我們可以總結成下面這個簡單划分的模塊系統來深入理解學習:
游戲主循環
在上面流程分析時候能看到所有的事件和渲染的內容都是實現在mainLoop的游戲主循環中,每幀做的內容如下:
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 = 0; if(!_children.empty()) { sortAllChildren(); // draw children zOrder < 0 位於父節點之后 for( ; i < _children.size(); i++ ) { auto node = _children.at(i); if (node && node->_localZOrder < 0) 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樹遍歷中分離。
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, 1, transform, flags); renderer->addCommand(&_quadCommand); } }
3)采用自動批繪制技術。不需要像2.x以前要把每個元素添加到一個spriteBatchNode上,在3.x引擎下當不同類型的UI元素的對應相關的繪制指令(即QuadCommond)在執行順序上相鄰,並且使用相同的紋理,着色器等繪制屬性時,這些QuadCommond會自動組合到一起,形成一次繪制,即只會調用一次的OpenGL繪制指令。
以一Sprite實際渲染分析下這個渲染流程:
_quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, 1, transform, flags); renderer->addCommand(&_quadCommand);
繪制流程可以分為3個階段:生成繪制指令->對繪制指令進行排序->執行繪制指令。
生成繪制指令,指向renderer發送一RendererCommond(如QuadCommond)繪制指令,該指令不執行任何GL繪制命令,renderer會將RenderCommond放到繪制棧中。等UI元素全部遍歷完畢就開始執行棧中的所有RendererCommond。這樣抽離出來的好處是方便統一處理全部的繪制命令,一方面可以針對繪制做一些優化,如相鄰且使用相同紋理的QuadCommond執行自動批;另一方面可以靈活調整不同UI層級之間元素的繪制順序。
繪制排序,指繪制命令不一定是UI元素被遍歷的順序,3.x引擎可以使用globalZOrder變量直接設置元素的繪制順序。繪制先后首先由globalZOrder決定,然后才是遍歷順序,如下圖: