Cocos2d-x游戏开发实例详解4:游戏主循环
在本篇文章中,我将结合自己的实际演练,详细解说Cocos2d-x引擎的各个模块,重点介绍游戏主循环。
我终于抽时间完成了一个功能较为完善的游戏开发。由于没有自拍神器,我将其移植到了Android平台,在我的戴妃手机上运行得很流畅。现在,我打算把开发这个游戏的经历和学习过程整理成几篇博客,权当笔记记录。
游戏引擎处理流程概述
从个人理解的角度来看,游戏逻辑可以简单抽象为一个死循环,示例代码如下:
bool game_is_running = true;
while( game_is_running ) {
update_game();
display_game();
}
我们在游戏中看到的画面、听到的音乐,以及处理的触控、输入等事件,在逻辑层面上就是这样一个不断运行的死循环。在这个循环过程中,会持续处理一系列事件,简化后主要就是上述的两个函数。
Cocos2d-x引擎同样遵循这一原理,其所有的逻辑都是在主循环的框架下实现的。接下来,我们将详细探讨Cocos2d-x在不同平台上的主循环实现方式。
各平台主循环实现
1. Windows平台
在Windows平台上,我们首先来看main.cpp
文件:
#include "main.h"
#include "../Classes/AppDelegate.h"
#include "CCEGLView.h"
USING_NS_CC;
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// 创建应用程序实例
AppDelegate app;
CCEGLView* eglView = CCEGLView::sharedOpenGLView();
eglView->setFrameSize(2048, 1536);
// iPad3的分辨率较大,一般PC分辨率较小,需调整窗口大小
eglView->setFrameZoomFactor(0.4f);
return CCApplication::sharedApplication()->run(); // 关键调用
}
在这段代码中,前面的部分主要用于传递OpenGL窗口相关信息,关键在于最后一行CCApplication::sharedApplication()->run()
。下面我们来深入分析这个run
函数:
int CCApplication::run()
{
PVRFrameEnableControlWindow(false);
// 主消息循环
MSG msg;
LARGE_INTEGER nFreq;
LARGE_INTEGER nLast;
LARGE_INTEGER nNow;
QueryPerformanceFrequency(&nFreq);
QueryPerformanceCounter(&nLast);
// 初始化实例和Cocos2d-x
if (!applicationDidFinishLaunching())
{
return 0;
}
CCEGLView* pMainWnd = CCEGLView::sharedOpenGLView();
pMainWnd->centerWindow();
ShowWindow(pMainWnd->getHWnd(), SW_SHOW);
while (1) // 主循环开始
{
if (! PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
// 获取当前时间
QueryPerformanceCounter(&nNow);
// 判断是否到了绘制下一帧的时间
if (nNow.QuadPart - nLast.QuadPart > m_nAnimationInterval.QuadPart)
{
nLast.QuadPart = nNow.QuadPart;
CCDirector::sharedDirector()->mainLoop(); // 进入Cocos2d-x主循环
}
else
{
Sleep(0);
}
continue;
}
if (WM_QUIT == msg.message)
{
// 退出消息循环
break;
}
// 处理Windows消息
if (! m_hAccelTable || ! TranslateAccelerator(msg.hwnd, m_hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
我们知道Windows是消息驱动的系统,这个死循环的主要作用就是处理Windows的消息循环,其中包含了FPS逻辑处理、消息分发等功能。需要特别注意的是CCDirector::sharedDirector()->mainLoop()
这一行代码,它标志着进入了Cocos2d-x的主循环,由导演(CCDirector
)负责维护,此后的逻辑就与Windows系统的消息处理相对独立了。
2. Android平台
在Android平台上,游戏通常从一个Activity
开始,实际上,Android的大多数应用都是以这种方式启动的。
在引擎源码的YourCocos2dxDir/cocos2dx/platform/android/java
目录下,存放着Android的Java模板代码,几乎所有基于Cocos2d-x开发的Android游戏都会使用这些代码。我们以HelloCpp
的代码为例:
package org.cocos2dx.hellocpp;
import org.cocos2dx.lib.Cocos2dxActivity;
import android.os.Bundle;
public class HelloCpp extends Cocos2dxActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
static {
System.loadLibrary("hellocpp");
}
}
这段代码虽然简单,但说明了两个重要问题:
Cocos2dxActivity
是核心的Activity
。- 游戏的C++部分(包括引擎部分)被编译成了动态链接库
hellocpp
,这里通过System.loadLibrary
方法加载该库。这个动态链接库是在使用NDK编译时生成的,路径为libs/armeabi/libhellocpp.so
。
接下来,我们深入分析Cocos2dxActivity
这个类:
public abstract class Cocos2dxActivity extends Activity implements Cocos2dxHelperListener {
// 常量定义
private static final String TAG = Cocos2dxActivity.class.getSimpleName();
// 成员变量
private Cocos2dxGLSurfaceView mGLSurfaceView; // 关键的SurfaceView
private Cocos2dxHandler mHandler;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.mHandler = new Cocos2dxHandler(this);
this.init();
Cocos2dxHelper.init(this, this);
}
public void init() {
// 帧布局
ViewGroup.LayoutParams framelayout_params =
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.FILL_PARENT);
FrameLayout framelayout = new FrameLayout(this);
framelayout.setLayoutParams(framelayout_params);
// 编辑框布局
ViewGroup.LayoutParams edittext_layout_params =
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
Cocos2dxEditText edittext = new Cocos2dxEditText(this);
edittext.setLayoutParams(edittext_layout_params);
// 添加编辑框到帧布局
framelayout.addView(edittext);
// 创建GLSurfaceView
this.mGLSurfaceView = this.onCreateView();
// 添加GLSurfaceView到帧布局
framelayout.addView(this.mGLSurfaceView);
this.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer()); // 设置渲染器
this.mGLSurfaceView.setCocos2dxEditText(edittext);
// 设置帧布局为内容视图
setContentView(framelayout);
}
public Cocos2dxGLSurfaceView onCreateView() {
return new Cocos2dxGLSurfaceView(this);
}
}
在这段代码中,核心部分是mGLSurfaceView
及其渲染器Cocos2dxRenderer
。在Android平台上,OpenGL的渲染由GLSurfaceView
和其渲染器Renderer
共同完成,GLSurfaceView
负责显示界面,Renderer
负责渲染更新,Renderer
实际上是一个由框架层维护的渲染线程。
下面我们来看Cocos2dxRenderer
类:
package org.cocos2dx.lib;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView;
public class Cocos2dxRenderer implements GLSurfaceView.Renderer {
// 常量定义
private final static long NANOSECONDSPERSECOND = 1000000000L;
private final static long NANOSECONDSPERMICROSECOND = 1000000;
private static long sAnimationInterval = (long) (1.0 / 60 * Cocos2dxRenderer.NANOSECONDSPERSECOND);
// 成员变量
private long mLastTickInNanoSeconds;
private int mScreenWidth;
private int mScreenHeight;
public static void setAnimationInterval(final double pAnimationInterval) {
Cocos2dxRenderer.sAnimationInterval = (long) (pAnimationInterval * Cocos2dxRenderer.NANOSECONDSPERSECOND);
}
public void setScreenWidthAndHeight(final int pSurfaceWidth, final int pSurfaceHeight) {
this.mScreenWidth = pSurfaceWidth;
this.mScreenHeight = pSurfaceHeight;
}
@Override // ① 窗口创建时调用
public void onSurfaceCreated(final GL10 pGL10, final EGLConfig pEGLConfig) {
Cocos2dxRenderer.nativeInit(this.mScreenWidth, this.mScreenHeight); // ② 初始化窗口
this.mLastTickInNanoSeconds = System.nanoTime();
}
@Override
public void onSurfaceChanged(final GL10 pGL10, final int pWidth, final int pHeight) {
}
@Override // ③ 渲染线程不断调用
public void onDrawFrame(final GL10 gl) {
Cocos2dxRenderer.nativeRender(); // ④ 关键的本地方法调用
}
// 本地方法声明
private static native void nativeTouchesBegin(final int pID, final float pX, final float pY);
private static native void nativeTouchesEnd(final int pID, final float pX, final float pY);
private static native void nativeTouchesMove(final int[] pIDs, final float[] pXs, final float[] pYs);
private static native void nativeTouchesCancel(final int[] pIDs, final float[] pXs, final float[] pYs);
private static native boolean nativeKeyDown(final int pKeyCode);
private static native void nativeRender();
private static native void nativeInit(final int pWidth, final int pHeight);
private static native void nativeOnPause();
private static native void nativeOnResume();
// 事件处理方法
public void handleActionDown(final int pID, final float pX, final float pY) {
Cocos2dxRenderer.nativeTouchesBegin(pID, pX, pY);
}
public void handleActionUp(final int pID, final float pX, final float pY) {
Cocos2dxRenderer.nativeTouchesEnd(pID, pX, pY);
}
public void handleActionCancel(final int[] pIDs, final float[] pXs, final float[] pYs) {
Cocos2dxRenderer.nativeTouchesCancel(pIDs, pXs, pYs);
}
public void handleActionMove(final int[] pIDs, final float[] pXs, final float[] pYs) {
Cocos2dxRenderer.nativeTouchesMove(pIDs, pXs, pYs);
}
public void handleKeyDown(final int pKeyCode) {
Cocos2dxRenderer.nativeKeyDown(pKeyCode);
}
public void handleOnPause() {
Cocos2dxRenderer.nativeOnPause();
}
public void handleOnResume() {
Cocos2dxRenderer.nativeOnResume();
}
private static native void nativeInsertText(final String pText);
private static native void nativeDeleteBackward();
private static native String nativeGetContentText();
public void handleInsertText(final String pText) {
Cocos2dxRenderer.nativeInsertText(pText);
}
public void handleDeleteBackward() {
Cocos2dxRenderer.nativeDeleteBackward();
}
public String getContentText() {
return Cocos2dxRenderer.nativeGetContentText();
}
}
虽然代码较多,但逻辑脉络清晰。GLSurfaceView
的渲染器必须实现GLSurfaceView.Renderer
接口,其中onSurfaceCreated
在窗口创建时调用,onSurfaceChanged
在窗口创建和大小变化时调用,onDrawFrame
方法类似于普通View
的onDraw
方法,在窗口初始化完成后,渲染线程会不断调用该方法。
下面我们分析代码中标记的几个关键位置:
- 标记①:窗口创建时,调用
nativeInit
方法(标记②)进行初始化,这是一个本地方法,实际上调用的是C++代码,涉及JNI调用。 - 标记③:
onDrawFrame
方法会被渲染线程不断调用,类似于主循环的死循环。 - 标记④:
nativeRender
方法同样是本地方法,用于触发C++层的渲染逻辑。
我们来看jni/hellocpp/
下的main.cpp
文件:
#include "AppDelegate.h"
#include "platform/android/jni/JniHelper.h"
#include <jni.h>
#include <android/log.h>
#include "HelloWorldScene.h"
#define LOG_TAG "main"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
using namespace cocos2d;
extern "C"
{
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
JniHelper::setJavaVM(vm);
return JNI_VERSION_1_4;
}
void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(JNIEnv* env, jobject thiz, jint w, jint h)
{
if (!CCDirector::sharedDirector()->getOpenGLView())
{
CCEGLView *view = CCEGLView::sharedOpenGLView();
view->setFrameSize(w, h);
CCLog("with %d,height %d",w,h);
AppDelegate *pAppDelegate = new AppDelegate();
CCApplication::sharedApplication()->run(); // ⑤ 关键调用
}
else
{
ccDrawInit();
ccGLInvalidateStateCache();
CCShaderCache::sharedShaderCache()->reloadDefaultShaders();
CCTextureCache::reloadAllTextures();
CCNotificationCenter::sharedNotificationCenter()->postNotification(EVNET_COME_TO_FOREGROUND, NULL);
CCDirector::sharedDirector()->setGLDefaultValues();
}
}
}
根据JNI的命名规则,标注②的nativeInit
方法对应的就是上述C++代码中的Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit
方法,用于窗口的初始化处理。需要注意的是,标注⑤的run
方法实际上并没有直接进入主循环,只是调用了applicationDidFinishLaunching
方法。
我们再来看nativeRender
方法的实现:
#include "text_input_node/CCIMEDispatcher.h"
#include "CCDirector.h"
#include "../CCApplication.h"
#include "platform/CCFileUtils.h"
#include "CCEventType.h"
#include "support/CCNotificationCenter.h"
#include "JniHelper.h"
#include <jni.h>
using namespace cocos2d;
extern "C" {
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRender(JNIEnv* env) {
cocos2d::CCDirector::sharedDirector()->mainLoop(); // 进入主循环
}
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnPause() {
CCApplication::sharedApplication()->applicationDidEnterBackground();
CCNotificationCenter::sharedNotificationCenter()->postNotification(EVENT_COME_TO_BACKGROUND, NULL);
}
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnResume() {
if (CCDirector::sharedDirector()->getOpenGLView()) {
CCApplication::sharedApplication()->applicationWillEnterForeground();
}
}
// 其他本地方法实现...
}
在这里,我们找到了导演(CCDirector
),它开始执行主循环逻辑。可以看出,Android平台上的主循环与Windows平台不同,它是由Java的渲染线程发起的,通过不断调用render
方法来驱动。
3. iOS平台
iOS平台的实现与Android平台类似。我们来看AppController.mm
文件:
#import <UIKit/UIKit.h>
#import "AppController.h"
#import "cocos2d.h"
#import "EAGLView.h"
#import "AppDelegate.h"
#import "RootViewController.h"
@implementation AppController
@synthesize window;
@synthesize viewController;
#pragma mark -
#pragma mark Application lifecycle
// Cocos2d应用程序实例
static AppDelegate s_sharedApplication;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 应用启动后的自定义处理
window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
EAGLView *__glView = [EAGLView viewWithFrame: [window bounds]
pixelFormat: kEAGLColorFormatRGBA8
depthFormat: GL_DEPTH_COMPONENT16
preserveBackbuffer: NO
sharegroup: nil
multiSampling: NO
numberOfSamples:0 ];
// 使用RootViewController管理EAGLView
viewController = [[RootViewController alloc] initWithNibName:nil bundle:nil];
viewController.wantsFullScreenLayout = YES;
viewController.view = __glView;
// 设置RootViewController到窗口
if ( [[UIDevice currentDevice].systemVersion floatValue] < 6.0)
{
[window addSubview: viewController.view];
}
else
{
[window setRootViewController:viewController];
}
[window makeKeyAndVisible];
[[UIApplication sharedApplication] setStatusBarHidden: YES];
cocos2d::CCApplication::sharedApplication()->run(); // 关键调用
return YES;
}
// 其他应用生命周期方法...
@end
我们跟进run
方法:
int CCApplication::run()
{
if (applicationDidFinishLaunching()) {
[[CCDirectorCaller sharedDirectorCaller] startMainLoop];
}
return 0;
}
再跟进startMainLoop
方法:
-(void) startMainLoop
{
// 无效化之前的显示链接
[displayLink invalidate];
displayLink = nil;
NSLog(@"run loop !");
displayLink = [NSClassFromString(@"CADisplayLink") displayLinkWithTarget:self selector:@selector(doCaller:)]; // 关键的定时器设置
[displayLink setFrameInterval: self.interval];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
这里加载的CADisplayLink
类是实现循环的关键,它实际上是一个定时器,默认每秒运行60次,通过设置其属性可以调整FPS。我们跟进回调函数doCaller
:
-(void) doCaller: (id) sender
{
cocos2d::CCDirector::sharedDirector()->mainLoop(); // 进入主循环
}
最终,我们又找到了导演(CCDirector
),它开始执行主循环逻辑。
总结
一旦进入主循环,游戏就开始执行我们自己设计的游戏逻辑。通过以上分析,我们可以看到Cocos2d-x在不同平台上的主循环实现方式各有特点,但核心都是通过不断调用CCDirector::sharedDirector()->mainLoop()
方法来驱动游戏的运行。在Windows平台上,通过Windows消息循环触发;在Android平台上,由Java渲染线程发起;在iOS平台上,则借助CADisplayLink
定时器实现。这些实现方式确保了游戏在不同平台上都能稳定、高效地运行。