游戏开发教程cocos2d-x移植之三(上)
示例说明
上一节我们对cocos2d-x的基本概念有了初步了解,下面我们将通过一个实例对前面的概念进行实战演练。
假设我们要开发一款飞行射击类游戏,其中有3个主要画面。
1)主菜单画面
2)游戏画面
3)设置画面
在游戏场景中,我们使用一副太空的图片作为背景,游戏的主角是一架喷着火焰的飞行器,状态栏用来显示生命条数和游戏时间,当然少不了一个"Main Menu"以便返回主菜单画面。
为了让游戏看起来更加精致,我们在场景切换时添加一些效果。
幻灯片切换效果:
缩放显示切换效果:
预备知识
场景切换
实例中包含3个典型场景,分别由3个不同的层来实现。场景的切换主要使用CCDirector的replaceScene函数实现,具体代码如下:
1 void SysMenu::onNewGame(CCObject* pSender) 2 { 3 CCScene *scene = CCScene::node(); 4 scene->addChild(GameLayer::node()); 5 CCDirector::sharedDirector()->replaceScene(scene); 6 }
首先创建一个新的CCScene实例,接着创建一个目标层的新实例(我们这里就是GameLayer),并附加给场景实例。然后调用CCDirector的replaceScene函数,用新场景取代当前场景。
需要为画面切换增加效果时,可以使用画面切换效果类,以CCTransitionSlideInR类为例,上面的代码要修改成:
1 CCDirector::sharedDirector()->replaceScene(CCTransitionSlideInR::transitionWithDuration(1.2f, scene));
从本章开始,我们将逐步深入讲解基于cocos2d-x的代码编写。网上很多示例代码的作者没有深入研究cocos2d-x自带的例子和分析该引擎的源代码,因此写出来的代码比较随意,不规范。笔者建议大家仔细分析官方的源代码,并尽量使用标准的编程方法。
坐标系
再进一步细讲每个层的实现之前,先明确一下坐标系的概念。
一般意义上的坐标系为笛卡尔坐标系(应该是初中平面几何开始讲的吧,高中立体几何扩展到三维空间。):
不同的图形库采用不同的坐标系。iPhone平台提供了两种绘图库:Quartz 2D和OpenGL ES。其中Quartz 2D是Core Graphics绘图库的子集,OpenGL ES是跨平台图形库OpenGL的嵌入设备版。这两者的坐标系原点不一样。
Quartz 2D的原点在左上角:
大多数图形窗口应用程序都采用类似的坐标系。这是一种基于虚拟“画布”绘图模型的图形库,绘图指令按次序向“画布”上画下不同的内容,后画的内容会覆盖先画的内容(透明的除外)。这比较容易理解。
OpenGL ES的原点在左下角:
OpenGL ES相对比较复杂,这实际上是一个3D的绘图库,按照“状态机”模型设计的绘图库。他不是简单地让后者叠加在前者上面,而是记录各个绘制内容的三维位置关系,再按照系统设定的投影关系,将绘制的所有内容投影在某个特定的虚拟窗口上。
cocos2d-x是基于OpenGL ES的,所以请大家牢记我们使用笛卡尔坐标系,坐标系的原点在左下角。
cocos2d-x引擎的大多数可显示对象都是从CCNode类派生而来的,理解该类对于使用该图形引擎至关重要。今天我们先来说明与位置相关的属性:
1)anchorPoint(锚点)
为了将一个矩形图像精准地放置在屏幕某一位置上,需要设置该矩形的位置参考点。通常人们习惯于将该参考点设置在矩形的左上角上,而在cocos2d-x中我们称其为锚点,默认位置在矩形的中心。
2)Position(位置)
Position就是CCNode对象实际的OpenGL ES坐标。
下图说明了如何利用这两个属性来设置CCNode对象的显示位置:
图中红色矩形框的Position为(5, 5),anchorPoint为(0.3, 0.5)。若要选择紫色大圆点A为锚点,则设置anchorPoint为(0, 0),如要选择粉红色大圆点B为锚点,则设置anchorPoint为(1, 1)。显然设置为(0.5, 0.5)时,锚点位于矩形对象的几何中心点C,这是CCSprite类对象的默认anchorPoint值。
代码分析
上文说明了几个场景之间的切换。下面,我们重点介绍每个场景内部的实现。
SysMenu
作为游戏的主菜单,SysMenu的实现并不复杂。
1)主菜单的构建
1 bool SysMenu::init(void) 2 { 3 bool bRet = false; 4 do 5 { 6 CC_BREAK_IF(! CCLayer::init()); 7 8 // 用一个图片做画面的背景 9 CCSprite *sp = CCSprite::spriteWithFile("bg.png"); 10 CC_BREAK_IF(! sp); 11 sp->setAnchorPoint(CCPointZero); 12 // 既然是背景,zOrder值尽量小。 13 this->addChild(sp, 0, 1); 14 15 // 设置菜单项字体 16 CCMenuItemFont::setFontName("Marker Felt"); 17 CCMenuItemFont::setFontSize(25); 18 19 // 逐一创建5个菜单项 20 CCMenuItemFont *newGame = CCMenuItemFont::itemFromString("New Game", this, menu_selector(SysMenu::onNewGame)); 21 CC_BREAK_IF(! newGame); 22 CCMenuItemFont *loadGame = CCMenuItemFont::itemFromString("Load"); 23 CC_BREAK_IF(! loadGame); 24 CCMenuItemFont *gameSettings = CCMenuItemFont::itemFromString("Option", this, menu_selector(SysMenu::onSettings)); 25 CC_BREAK_IF(! gameSettings); 26 CCMenuItemFont *help = CCMenuItemFont::itemFromString("Help"); 27 CC_BREAK_IF(! help); 28 CCMenuItemFont *quit = CCMenuItemFont::itemFromString("Quit", this, menu_selector(SysMenu::onQuit)); 29 CC_BREAK_IF(! quit); 30 31 // 将这些菜单项都加入到菜单对象中 32 CCMenu *menu = CCMenu::menuWithItems(newGame, loadGame, gameSettings, help, quit, NULL); 33 CC_BREAK_IF(! menu); 34 // 设置菜单以纵向布局显示 35 menu->alignItemsVertically(); 36 // 最重要的是不要忘记把菜单加入主层对象,否则不会显示 37 this->addChild(menu, 1, 2); 38 39 bRet = true; 40 } while (0); 41 42 return bRet; 43 }
需要讲解的核心函数是itemFromString,他是CCMenuItemFont的一个静态成员函数,能够帮助我们创建CCMenuItemFont菜单项。
第一个参数比较直接,就是菜单项显示的文字。
第二个参数是响应菜单事件的对象,通常是菜单所属的层对象。
第三个参数是菜单事件对应的回调函数。
2)切换到其他场景
以下两个函数onNewGame和onSettings被作为参数传递给菜单构建函数,他们的作用都是进行场景切换。
1 void SysMenu::onNewGame(CCObject* pSender) 2 { 3 CCScene *scene = CCScene::node(); 4 // 特别说明,我们将游戏层和控制层在此分开,由scene作为他们统一的父节点。 5 scene->addChild(GameLayer::node()); 6 scene->addChild(GameControlMenu::node()); 7 // 从右至左滚动画面切换到游戏场景。 8 CCDirector::sharedDirector()->replaceScene(CCTransitionSlideInR::transitionWithDuration(1.2f, scene)); 9 } 10 11 void SysMenu::onSettings(CCObject* pSender) 12 { 13 CCScene *scene = CCScene::node(); 14 scene->addChild(SettingsLayer::node()); 15 // 缩放变换切换到游戏设置画面。 16 CCDirector::sharedDirector()->replaceScene(CCTransitionShrinkGrow::transitionWithDuration(1.2f, scene)); 17 }
GameLayer
作为游戏的主画面,主要包含以下几个关键要素:
1)状态栏:显示主角的生命数,得分,游戏时间。
2)控制栏:游戏需要的控制菜单,动作按钮等。
3)场景画面:游戏场景、NPC、助教角色动作(是主要角色动作吗?)。
下面我们详细介绍GameLayer的三个重点:
主画面的创建
1 bool GameLayer::init(void) 2 { 3 bool bRet = false; 4 do 5 { 6 CC_BREAK_IF(! CCLayer::init()); 7 8 // 游戏场景背景图 9 CCSprite *bg = CCSprite::spriteWithFile("Space.png"); 10 CC_BREAK_IF(! bg); 11 bg->setAnchorPoint(CCPointZero); 12 // 为了突出游戏场景中的精灵,将背景色彩变淡 13 bg->setOpacity(100); 14 this->addChild(bg, 0, 1); 15 16 // 使用位图字体显示游戏时间 17 CCLabelBMFont *lbScore = CCLabelBMFont::labelWithString("Time: 0", "font09.fnt"); 18 CC_BREAK_IF(! lbScore); 19 lbScore->setAnchorPoint(ccp(1, 1)); 20 lbScore->setScale(0.6f); 21 this->addChild(lbScore, 1, 3); 22 lbScore->setPosition(ccp(310, 450)); 23 24 // 载入飞船图像集。整个图像集仅载入一次,可以被使用多次。 25 CCSpriteBatchNode *mgr = CCSpriteBatchNode::batchNodeWithFile("flight.png", 5); 26 CC_BREAK_IF(! mgr); 27 this->addChild(mgr, 0, 4); 28 29 // 在状态栏显示一个飞船的图标 30 CCSprite *sprite = CCSprite::spriteWithTexture(mgr->getTexture(), CCRectMake(0, 0, 31, 30)); 31 CC_BREAK_IF(! sprite); 32 mgr->addChild(sprite, 1, 5); 33 sprite->setScale(1.1f); 34 sprite->setAnchorPoint(ccp(0, 1)); 35 sprite->setPosition(ccp(10, 460)); 36 37 // 显示当前飞船生命条数 38 CCLabelBMFont *lbLife = CCLabelBMFont::labelWithString("3", "font09.fnt"); 39 CC_BREAK_IF(! lbLife); 40 lbLife->setAnchorPoint(ccp(0, 1)); 41 lbLife->setScale(0.6f); 42 this->addChild(lbLife, 1, 6); 43 lbLife->setPosition(ccp(50, 450)); 44 45 // 设定时间回调函数,修改游戏用时显示 46 this->schedule(schedule_selector(GameLayer::step), 1.0f); 47 48 // 显示飞船。为了让飞船有不断闪烁的火焰喷射效果,这是一个简单的重复性动作。 49 // 在后面的章节中我们会专门讲解动作。 50 flight = CCSprite::spriteWithTexture(mgr->getTexture(), CCRectMake(0, 0, 31, 30)); 51 CC_BREAK_IF(! flight); 52 flight->setPosition(ccp(160, 30)); 53 flight->setScale(1.6f); 54 mgr->addChild(flight, 1, 99); 55 56 // 设定动画每一帧的内容 57 CCAnimation *animation = CCAnimation::animation(); 58 CC_BREAK_IF(! animation); 59 animation->setName("flight"); 60 animation->setDelay(0.2f); 61 for (int i = 0; i < 3; ++i) 62 { 63 int x = i % 3; 64 animation->addFrameWithTexture(mgr->getTexture(), CCRectMake(x * 32, 0, 31, 30)); 65 } 66 67 // 基于动画创建动作 68 CCAnimate *action = CCAnimate::actionWithAnimation(animation); 69 CC_BREAK_IF(! action); 70 // 主角精灵不断重复动作,实现动态飞行效果 71 flight->runAction(CCRepeatForever::actionWithAction(action)); 72 73 // accept touch now! 74 this->setIsTouchEnabled(true); 75 76 bRet = true; 77 } while (0); 78 79 return bRet; 80 }
计时器
在游戏设计时,我们需要不断的改变屏幕显示来反映游戏操作的效果,最简单的就是提示用户已经进行的游戏时间。为此,我们需要使用cocos2d-x内置的任务调度机制,即CCNode的schedule成员函数。
1 // 设定时间回调函数,修改游戏用时显示 2 this->schedule(schedule_selector(GameLayer::step), 1.0f);
schedule的作用类似计时器,按照指定的时间间隔不断调用某个指定的回调函数。在我们的演示程序中,这个回调函数是这样的:
1 void GameLayer::step(ccTime dt) 2 { 3 time += dt; 4 char time_str[20]; 5 sprintf(time_str, "Time: %d", (int)time); 6 CCLabelBMFont *label1 = (CCLabelBMFont *)this->getChildByTag(3); 7 label1->setString(time_str); 8 }
这段代码的作用十分简单,每隔1秒记录一下新的时间值,并修改游戏用时的画面显示。需要特别说明的是本函数的参数dt,这是系统决定的,是上一次调用到本次调用的时间间隔。因此为了计算游戏的用时,我们只需将它与累积时间相加即可。
按照时间间隔检查内部对象之前的相互作用,及时更新画面。这是游戏编程中经常要用到的逻辑。
对于这样的处理,我们是统一设定一个处理所有检查反馈逻辑的计时器呢,还是使用多个专用的计时器,这两种方法哪一个效率更高?cocos2d-iphone的作者指出:小于20个的专用计时器和一个复杂时间计时器的效率基本相同。
可以通过CCNode的unschedule成员函数来取消计时器。
简单的动作
让游戏中的精灵执行一次或者重复执行某个动画动作是游戏中经常遇到的,因此cocos2d-x引擎支持一个重要的概念Action(动作)。本例中我们只是给出了一个简单的“喷火”动画效果来模拟飞船的飞行。后面我们将有专门的章节来详细说明动作。
为了允许飞船在用户的操作下移动,我们增加了Touch事件的响应:
1 void GameLayer::ccTouchesEnded(CCSet *pTouches, CCEvent *pEvent) 2 { 3 CCTouch *touch = (CCTouch *)pTouches->anyObject(); 4 // 获得触摸点坐标 5 CCPoint location = touch->locationInView(touch->view()); 6 CCPoint convertedLocation = CCDirector::sharedDirector()->convertToGL(location); 7 // 让飞船在1秒钟内移动过去 8 flight->runAction(CCMoveTo::actionWithDuration(1.0f, ccp(convertedLocation.x, convertedLocation.y))); 9 }
这里又用到了一个简单的动作:CCMoveTo。他的作用像他的名字一样直接,就是移动到某个点。第一个参数是动作执行的时间,第二参数是目标点的坐标。
GameControlMenu
完全可以将菜单按钮放置在GameLayer层实现,但考虑到将来画面滚动等问题,我们把游戏界面上的控制画面分离出来,由一个独立的层实现。
目前来说,这上面只有唯一的一个菜单项。
1 bool GameControlMenu::init(void) 2 { 3 bool bRet = false; 4 do 5 { 6 CC_BREAK_IF(! CCLayer::init()); 7 8 // 控制栏部分出于简单考虑,只有一个切换到主菜单的菜单项 9 CCMenuItemFont::setFontSize(22); 10 CCMenuItemFont *systemMenu = CCMenuItemFont::itemFromString("Main Menu", 11 this, menu_selector(GameControlMenu::sysMenu)); 12 CC_BREAK_IF(! systemMenu); 13 CCMenu *menu = CCMenu::menuWithItems(systemMenu, NULL); 14 CC_BREAK_IF(! menu); 15 menu->setPosition(ccp(0, 0)); 16 // 手工设置菜单位置在左下角 17 systemMenu->setAnchorPoint(ccp(0, 0)); 18 systemMenu->setPosition(ccp(0, 0)); 19 this->addChild(menu, 1, 2); 20 21 bRet = true; 22 } while (0); 23 24 return bRet; 25 }
下面的函数实现切换到主菜单画面的功能:
1 void GameControlMenu::sysMenu(CCObject* pSender) 2 { 3 CCScene *scene = CCScene::node(); 4 scene->addChild(SysMenu::node()); 5 CCDirector::sharedDirector()->replaceScene(CCTransitionSlideInL::transitionWithDuration(1.2f, scene)); 6 }
SettingsLayer
这个画面的实现我们主要参考了cocos2d-x自带的MenuTest例子。
1 bool SettingsLayer::init(void) 2 { 3 bool bRet = false; 4 do 5 { 6 CC_BREAK_IF(! CCLayer::init()); 7 8 // 设置标题字体 9 CCMenuItemFont::setFontName("American Typewriter"); 10 CCMenuItemFont::setFontSize(18); 11 // 用一个禁用状态的菜单项作为标题 12 CCMenuItemFont *title1 = CCMenuItemFont::itemFromString("Sound"); 13 CC_BREAK_IF(! title1); 14 title1->setIsEnabled(false); 15 16 // 设置选项字体(设置不同的字体以示与标题的区别) 17 CCMenuItemFont::setFontName("Marker Felt"); 18 CCMenuItemFont::setFontSize(26); 19 // 设置可切换的菜单项,菜单状态:开、关。 20 CCMenuItemToggle *item1 = CCMenuItemToggle::itemWithTarget(NULL, NULL, 21 CCMenuItemFont::itemFromString("On"), CCMenuItemFont::itemFromString("Off"), NULL); 22 CC_BREAK_IF(! item1); 23 24 // 设置标题字体 25 CCMenuItemFont::setFontName("American Typewriter"); 26 CCMenuItemFont::setFontSize(18); 27 CCMenuItemFont *title2 = CCMenuItemFont::itemFromString("Music"); 28 CC_BREAK_IF(! title2); 29 title2->setIsEnabled(false); 30 31 // 设置选项字体 32 CCMenuItemFont::setFontName("Marker Felt"); 33 CCMenuItemFont::setFontSize(26); 34 CCMenuItemToggle *item2 = CCMenuItemToggle::itemWithTarget(NULL, NULL, 35 CCMenuItemFont::itemFromString("On"), CCMenuItemFont::itemFromString("Off"), NULL); 36 CC_BREAK_IF(! item2); 37 38 CCMenuItemFont::setFontName("American Typewriter"); 39 CCMenuItemFont::setFontSize(18); 40 CCMenuItemFont *title3 = CCMenuItemFont::itemFromString("AI"); 41 CC_BREAK_IF(! title3); 42 title3->setIsEnabled(false); 43 44 CCMenuItemFont::setFontName("Marker Felt"); 45 CCMenuItemFont::setFontSize(26); 46 CCMenuItemToggle *item3 = CCMenuItemToggle::itemWithTarget(NULL, NULL, 47 CCMenuItemFont::itemFromString("Attack"), CCMenuItemFont::itemFromString("Defense"), NULL); 48 CC_BREAK_IF(! item3); 49 50 CCMenuItemFont::setFontName("American Typewriter"); 51 CCMenuItemFont::setFontSize(18); 52 CCMenuItemFont *title4 = CCMenuItemFont::itemFromString("Mode"); 53 CC_BREAK_IF(! title4); 54 title4->setIsEnabled(false); 55 56 // 设置多选项效果。首先加入一个子选项(subItems),再加入一个包含了多个子菜单的数组。 57 CCMenuItemFont::setFontName("Marker Felt"); 58 CCMenuItemFont::setFontSize(26); 59 CCMenuItemToggle *item4 = CCMenuItemToggle::itemWithTarget(NULL, NULL, 60 CCMenuItemFont::itemFromString("Easy"), NULL); 61 CC_BREAK_IF(! item4); 62 CCMutableArray<CCMenuItem *> *more_items = 63 CCMutableArray<CCMenuItem *>::arrayWithObjects(CCMenuItemFont::itemFromString("Normal"), 64 CCMenuItemFont::itemFromString("Hard"), CCMenuItemFont::itemFromString("Nightmare"), NULL); 65 CC_BREAK_IF(! more_items); 66 // TIP: you can manipulate the items like any other CCMutableArray 67 item4->getSubItems()->addObjectsFromArray(more_items); 68 // you can change the one of the items by doing this 69 item4->setSelectedIndex(0); 70 71 CCMenuItemFont::setFontName("Marker Felt"); 72 CCMenuItemFont::setFontSize(26); 73 CCLabelBMFont *label = CCLabelBMFont::labelWithString("Go back", "font01.fnt"); 74 CC_BREAK_IF(! label); 75 CCMenuItemLabel *back = CCMenuItemLabel::itemWithLabel(label, this, menu_selector(SettingsLayer::backCallback)); 76 CC_BREAK_IF(! back); 77 back->setScale(0.8f); 78 79 // 组合创建菜单层 80 CCMenu *menu = CCMenu::menuWithItems(title1, title2, item1, item2, 81 title3, title4, item3, item4, back, NULL); 82 CC_BREAK_IF(! menu); 83 // 设置多列的菜单项布局 84 menu->alignItemsInColumns(2, 2, 2, 2, 1, NULL); 85 this->addChild(menu); 86 87 // 手工微调一下最后一个菜单项的位置 88 cocos2d::CCPoint cp_back = back->getPosition(); 89 cp_back.y -= 50.0f; 90 back->setPosition(cp_back); 91 92 bRet = true; 93 } while (0); 94 95 return bRet; 96 }
演示实例源代码可以从下面的链接获取
http://files.cnblogs.com/cocos2d-x/ZYG002.rar