Cocos2dx源码赏析(1)之启动流程与主循环
Cocos2dx源码赏析(1)之启动流程与主循环
我们知道Cocos2dx是一款开源的跨平台游戏引擎,而学习开源项目一个较实用的办法就是读源码。所谓,“源码之前,了无秘密”。而笔者从事的也是游戏开发工作,因此,通过梳理下源码的脉络,来加深对Cocos2dx游戏引擎的理解。
既然,Cocos2dx是跨平台的,那么,就有针对不同平台运行的入口以及维持引擎运转的“死循环”。下面,就分别从Windows、Android、iOS三个平台说明下Cocos2dx从启动到进入主循环的过程。
1、Windows
以引擎下的cpp-empty-test
项目工程为例:
Windows工程的入口函数为cpp-empty-test/win32/main.cpp中的_tWinMain
函数。
int WINAPI _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。而AppDelegate继承自Application,所以这里会先初始化父类Application。再看下Application的实现,注意是进到CCApplication-win32.h和CCApplication-win32.cpp里。当然,Application还继续继承自ApplicationProtocol(通过预处理宏来针对不同的平台执行不同的代码)。这里,并没有做什么特别的处理,只是作了下相应的初始化的工作。
而在CCApplication-win32.h和CCApplication-win32.cpp代码中都有这样的宏判断:
#if CC_TARGET_PLATFORM == CC_PLATFORM_WIN32
继续追踪下去,可以发现CC_PLATFORM_WIN32
在定义了WIN32宏时定义。
接下来,便执行Application::getInstance()->run()
代码,这里的Application为单例的实现,这也是Cocos2dx单例常用的实现方式,在2.x版本的引擎中,单例的实现为sharedApplication,这是仿照Objective-C的写法。继续看CCApplication-win32中的run
方法:
int Application::run()
{
PVRFrameEnableControlWindow(false);
// Main message loop:
LARGE_INTEGER nLast;
LARGE_INTEGER nNow;
QueryPerformanceCounter(&nLast);
initGLContextAttrs();
// Initialize instance and cocos2d.
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);
director->mainLoop();
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;
}
这里主要先看下applicationDidFinishLaunching()的调用,applicationDidFinishLaunching是虚函数,这里会调到子类AppDelegate中的applicationDidFinishLaunching的实现:
bool AppDelegate::applicationDidFinishLaunching()
{
auto director = Director::getInstance();
auto glview = director->getOpenGLView();
if(!glview) {
glview = GLViewImpl::create("Cpp Empty Test");
director->setOpenGLView(glview);
}
director->setOpenGLView(glview);
glview->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, ResolutionPolicy::NO_BORDER);
director->setAnimationInterval(1.0f / 60);
auto scene = HelloWorld::scene();
director->runWithScene(scene);
return true;
}
这里对代码做了适当的删减。可以看到在AppDelegate的applicationDidFinishLaunching主要做了些跟游戏初始化相关的处理。例如,初始化导演类,设置OpenGL视图,设置适配方式,设置帧率以及初始化场景和运行该场景等。基本这个方法,也可以当作我们游戏代码初始化的入口。
再回到CCApplication的run方法,继续往下看。这里,有个while循环,至此,就找到了引擎的“死循环”了。在这个循环中,调用了导演类的mainLoop主循环方法,而在mainLoop中,主要控制渲染,定时器,动画,事件循环等处理。后续会分析这相关的部分,这里就不过多介绍了。至此,就是Cocos2dx在Windows平台从启动到主循环,代码执行的流程,简单的梳理,可以知道引擎代码是如何架构的。
2、Android
在Android平台的应用,一般由多个Activity组成,一个Activity代表一个“窗口”,Activity根据应用前后台切换有对应的声明周期状态。在配置清单文件中声明了
<action android:name="android.intent.action.MAIN" />
即代表该Acitivity为应用的入口Activity。而入口Activity也一般称为闪屏页(Splash)或启动页,用来呈现公司的或运营的合作伙伴Logo,之后再切换到主Activity。在Cocos2dx游戏中,主Activity一般是继承Cocos2dx引擎封装的Cocos2dxActivity类。先看onCreate()方法:
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
onLoadNativeLibraries();
sContext = this;
Cocos2dxHelper.init(this);
this.init();
}
对onCreate里的代码做了精简,只列举了比较重要的几个方法。首先onLoadNativeLibraries
方法:
protected void onLoadNativeLibraries() {
try {
ApplicationInfo ai = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
Bundle bundle = ai.metaData;
String libName = bundle.getString("android.app.lib_name");
System.loadLibrary(libName);
} catch (Exception e) {
e.printStackTrace();
}
}
该方法会读取配置在Manifest里中的meta-data
标签的字段为android.app.lib_name
的值,来加载动态库。即为:
<meta-data android:name="android.app.lib_name"
android:value="cpp_empty_test" />
同样,以cpp_empty_test的项目为例,可知这里要加载名字为libcpp_empty_test.so
动态库。由于Cocos2dx引擎核心部分是C++实现,在Android平台通过jni的方式来调用和启动引擎。
再回到Cocos2dxActivity中的onCreate,继续往下进行。可以看到:
sContext = this;
sContext是Cocos2dxActivity的实例,被声明为静态的,通过这种实现了单例的效果。在需要Context实例的地方以及需要调用Cocos2dxActivity方法的地方,可以直接用该实例。
Cocos2dxHelper.init(this);
Cocos2dxHelper的init中主要是一些对象的初始化,例如:声音,音效,重力感应,Asset管理等。
接下来,调用了Cocos2dxActivity的init方法里:
public void init() {
ViewGroup.LayoutParams framelayout_params =
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
mFrameLayout = new ResizeLayout(this);
mFrameLayout.setLayoutParams(framelayout_params);
ViewGroup.LayoutParams edittext_layout_params =
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
Cocos2dxEditBox edittext = new Cocos2dxEditBox(this);
edittext.setLayoutParams(edittext_layout_params);
mFrameLayout.addView(edittext);
this.mGLSurfaceView = this.onCreateView();
mFrameLayout.addView(this.mGLSurfaceView);
// Switch to supported OpenGL (ARGB888) mode on emulator
if (isAndroidEmulator())
this.mGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
this.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer());
this.mGLSurfaceView.setCocos2dxEditText(edittext);
setContentView(mFrameLayout);
}
该方法主要设置要显示的视图界面,即mFrameLayout。重点关注这几行代码:
this.mGLSurfaceView = this.onCreateView();
mFrameLayout.addView(this.mGLSurfaceView);
this.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer());
在onCreateView方法中,返回了一个Cocos2dxGLSurfaceView对象,并将该对象添加到了帧布局的容器对象(mFrameLayout)中。首先,了解下Cocos2dxGLSurfaceView类的实现:
public class Cocos2dxGLSurfaceView extends GLSurfaceView {
private Cocos2dxRenderer mCocos2dxRenderer;
public void setCocos2dxRenderer(final Cocos2dxRenderer renderer) {
this.mCocos2dxRenderer = renderer;
this.setRenderer(this.mCocos2dxRenderer);
}
public void onResume() {
super.onResume();
this.setRenderMode(RENDERMODE_CONTINUOUSLY);
this.queueEvent(new Runnable() {
public void run() {
Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleOnResume();
}
});
}
public void onPause() {
this.queueEvent(new Runnable() {
public void run() {
Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleOnPause();
}
});
this.setRenderMode(RENDERMODE_WHEN_DIRTY);
//super.onPause();
}
}
(上述代码有做删减,只保留需要说明的地方)
Cocos2dxGLSurfaceView继承自GLSurfaceView,通过阅读GLSurfaceView文档可知,GLSurfaceView又继承自SurfaceView,而SurfaceView又进一步继承自View。GLSurfaceView封装了OpenGL ES所需的运行环境,同时能让OpenGL ES渲染的内容直接生成在Android的View视图上。绘制渲染时,用户可以自定义渲染器(GLSurfaceView.Renderer),该渲染器运行在单独的线程里,独立于UI线程。GLSurfaceView还能适应于Activity的声明周期的变化做相应的处理(例如:onPause、onResume等)。
GLSurfaceView的初始化过程中,需要设置渲染器。即调用setRenderer方法。
Cocos2dxGLSurfaceView类中的onResume和onPause方法,这两个方法受Activity的相应的声明周期的方法影响, Activity窗口暂停(pause)或恢复(resume)时,GLSurfaceView都会收到通知,此时它的onPause方法和 onResume方法应该被调用。这样GLSurfaceView就会暂停或恢复它的渲染线程,以便它及时释放或重建OpenGL的资源。其中都分别调用了queueEvent
的方法。这里,需要注意的是,Android的UI运行在主线程,而OpenGL的GLSurfaceView运行在一个单独的线程中,因此,需要调用queueEvent
来给OpenGL线程分发调用,来达到两个线程间通信。最后都交给Cocos2dxRenderer处理。
最后,再重点看下渲染器类Cocos2dxRenderer的实现:
public class Cocos2dxRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(final GL10 GL10, final EGLConfig EGLConfig) {
Cocos2dxRenderer.nativeInit(this.mScreenWidth, this.mScreenHeight);
this.mLastTickInNanoSeconds = System.nanoTime();
mNativeInitCompleted = true;
}
public void onSurfaceChanged(final GL10 GL10, final int width, final int height) {
Cocos2dxRenderer.nativeOnSurfaceChanged(width, height);
}
public void onDrawFrame(final GL10 gl) {
if (sAnimationInterval <= 1.0 / 60 * Cocos2dxRenderer.NANOSECONDSPERSECOND) {
Cocos2dxRenderer.nativeRender();
} else {
final long now = System.nanoTime();
final long interval = now - this.mLastTickInNanoSeconds;
if (interval < Cocos2dxRenderer.sAnimationInterval) {
try {
Thread.sleep((Cocos2dxRenderer.sAnimationInterval - interval) / Cocos2dxRenderer.NANOSECONDSPERMICROSECOND);
} catch (final Exception e) {
}
}
this.mLastTickInNanoSeconds = System.nanoTime();
Cocos2dxRenderer.nativeRender();
}
}
}
首先,Cocos2dxRenderer继承了渲染器类GLSurfaceView.Renderer,并重写了以下上个方法:
onSurfaceCreated:
该方法是当Surface被创建的时候,会调用,即应用程序第一次运行的时候。当设备被唤醒或用户从其它Activity切换回来的时候,该方法也可能被调用。因此,该方法可能会被多次调用。一般会在该方法中,完成一些OpenGL ES的初始化工作。
onSurfaceChanged:
该方法是在Surface被创建以后,每次Surface尺寸发生变化时(例如:横竖屏切换),该方法会被调用。
onDrawFrame:
绘制的每一帧,该方法都会被调用。
其实,看到onDrawFrame中的代码,可以知道Cocos2dx引擎在Android平台的“死循环”在该方法中。最后,通过jni的方式调用nativeRender
来启动导演类的主循环。
熟悉jni调用的可以知道,nativeRender是声明为native的方法,Cocos2dxRenderer.nativeRender最终会调到Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp类中:
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRender(JNIEnv* env) {
cocos2d::Director::getInstance()->mainLoop();
}
可以看到,跟Windows平台的一样,最终调用到导演类的mainLoop方法,殊途同归。以上便是,Android平台Cocos2dx引擎从启动到进入死循环的过程。
3、iOS
同样,以引擎下的cpp-empty-test
项目工程为例:
iOS工程的入口函数为cpp-empty-test/proj.ios/main.cpp中的main
函数。
int main(int argc, char *argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
int retVal = UIApplicationMain(argc, argv, nil, @"AppController");
[pool release];
return retVal;
}
在iOS应用中,都必须在函数main中调用UIApplicationMain
方法来启动应用和设置相应的事件循环。UIApplicationMain函数原型如下:
UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);
其中,argc是参数的个数,argv是可变的参数列表,principalClassName代表的是一个继承自UIApplication类的类名,delegateClassName是应用程序的代理类名称。在跟踪AppController类代码之前,有必要先了解下iOS应用的运行状态以及相应的生命周期方法:
- Not Running(非运行状态):应用没有运行或被系统终止。
- Inactive(前台非活动状态):应用正在进入前台状态,但是还不能接受事件处理。
- Active(前台活动状态):应用进入前台,能接受事件处理。
- Background(后台状态):应用进入后台后,依然能够执行代码。如果有可执行的代码,就会执行,如果没有可执行的代码或可执行的代码执行完毕,应用会马上进入挂起状态。
- Suspended(挂起状态):处于挂起的应用进入一种“冷冻”状态,不能执行代码。如果系统内存不够,应用会被终止。
生命周期方法有:
application:didFinishLaunchingWithOptions::
应用程序启动并进行初始化时会调用该方法。
applicationDidBecomeActive:
应用程序进入前台并处于活动状态时调用该方法。
applicationWillResignActive:
应用程序从活动状态进入非活动状态时调用该方法。
applicationDidEnterBackground:
应用程序进入后台时调用该方法。
applicationWillEnterForeground:
应用程序进入前台,但还没有处于活动状态时调用该方法。
applicationWillTerminate:
应用程序被终止时调用该方法。
进入AppController类,AppController实现了UIApplicationDelegate,并重写了相应的生命周期的方法。那么,重点看application:didFinishLaunchingWithOptions:
方法:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
cocos2d::Application *app = cocos2d::Application::getInstance();
app->initGLContextAttrs();
cocos2d::GLViewImpl::convertAttrs();
// Override point for customization after application launch.
// Add the view controller's view to the window and display.
window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
CCEAGLView *eaglView = [CCEAGLView viewWithFrame: [window bounds]
pixelFormat: (NSString*)cocos2d::GLViewImpl::_pixelFormat
depthFormat: cocos2d::GLViewImpl::_depthFormat
preserveBackbuffer: NO
sharegroup: nil
multiSampling: NO
numberOfSamples: 0];
// Use RootViewController manage CCEAGLView
viewController = [[RootViewController alloc] initWithNibName:nil bundle:nil];
viewController.wantsFullScreenLayout = YES;
viewController.view = eaglView;
// Set RootViewController to window
if ( [[UIDevice currentDevice].systemVersion floatValue] < 6.0)
{
// warning: addSubView doesn't work on iOS6
[window addSubview: viewController.view];
}
else
{
// use this method on ios6
[window setRootViewController:viewController];
}
[window makeKeyAndVisible];
[[UIApplication sharedApplication] setStatusBarHidden: YES];
// IMPORTANT: Setting the GLView should be done after creating the RootViewController
cocos2d::GLViewImpl *glview = cocos2d::GLViewImpl::createWithEAGLView(eaglView);
cocos2d::Director::getInstance()->setOpenGLView(glview);
app->run();
return YES;
}
这里主要是实例化一个UIWindow对象,每一个UIWindow对象上面都有一个根视图,它所对应的控制器为根视图控制器(ViewController),最后把根视图控制器放到UIWindow上。最后,app->run()
会调用到CCApplication-ios.mm(这个也是根据项目中的预编译宏实现)中的run方法:
int Application::run()
{
if (applicationDidFinishLaunching())
{
[[CCDirectorCaller sharedDirectorCaller] startMainLoop];
}
return 0;
}
这里有个跟生命周期方法类似的名字applicationDidFinishLaunching,这个会调到ApAppDelegate的applicationDidFinishLaunching方法,这点跟Windows平台类似,一般是在这个方法做跟游戏内容相关的初始化。run方法接下来,就是调startMainLoop方法,看这个名字,知道跟要找的目标很接近了,再继续跟下去。这里会调到CCDirectorCaller-ios.mm中的startMainLoop方法:
-(void) startMainLoop
{
// Director::setAnimationInterval() is called, we should invalidate it first
[self stopMainLoop];
displayLink = [NSClassFromString(@"CADisplayLink") displayLinkWithTarget:self selector:@selector(doCaller:)];
[displayLink setFrameInterval: self.interval];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
首先是通过NSClassFromString动态加载CADisplayLink类,然后调用了该类的displayLinkWithTarget方法,该方法类似定时器的功能,周期的调用该selector包装的方法(即:doCaller:方法):
-(void) doCaller: (id) sender
{
if (isAppActive) {
cocos2d::Director* director = cocos2d::Director::getInstance();
[EAGLContext setCurrentContext: [(CCEAGLView*)director->getOpenGLView()->getEAGLView() context]];
director->mainLoop();
}
}
至此,我们就找到了导演类的mainLoop方法,开启了引擎的主循环。以上,便是Cocos2dx引擎在iOS平台从启动到进入主循环的过程。
通过以上简单的分析,我们知道,Cocos2dx引擎利用了相应的平台循环方式来调用导演类的主循环来进入引擎的内部工作。下一篇继续通过代码的方式来梳理下Cocos2dx的渲染过程。如果在本篇中,有你觉得不对的地方,也欢迎来和我讨论。