Cocos2d-x游戏开发实例详解4:游戏主循环

2015年03月25日 13:44 0 点赞 0 评论 更新于 2017-05-09 05:43

在本篇文章中,我将结合自己的实际演练,详细解说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");
}
}

这段代码虽然简单,但说明了两个重要问题:

  1. Cocos2dxActivity是核心的Activity
  2. 游戏的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方法类似于普通ViewonDraw方法,在窗口初始化完成后,渲染线程会不断调用该方法。

下面我们分析代码中标记的几个关键位置:

  • 标记①:窗口创建时,调用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定时器实现。这些实现方式确保了游戏在不同平台上都能稳定、高效地运行。

作者信息

boke

boke

共发布了 1025 篇文章