随着公司需求变化,看来只会搞系统还不行,还需要会游戏开发。对自己挑战不小,为了生存,不得不迎难而上。
• Cocos2d-x:基于Cocos2d-iPhone的多平台二维游戏引擎,为开发者封装了功能强大的绘图代码,使开发者专注于游戏开发而不是绘图操作。
• AppDelegate:Cocos2d-x项目中的程序入口文件,提供对程序生命周期的控制事件。
• 游戏元素:任何可以呈现出来的元素,例如场景、层和精灵。
• CCNode::addChild方法:用于将一个游戏元素添加到另一个元素中。在创建一个层或者场景时,通常会初始化自己的游戏元素,定义一些特殊的效果,或是将其他的游戏元素组合到一起,而addChild方法就是用于组合游戏元素的。
游戏开发需要把控的关键点:
• 基本原理
• 元素组成
典型的流程控制如下图所示:
CCDirector的主要职责如下:
1) 负责初始化OpenGL ES环境
2) 以Stack方式管理场景(CCScene), 并负责场景切换、暂停或恢复游戏场景的运行
3) 游戏呈现方面的设定,包括设定游戏呈现的窗口、FPS显示、默认帧率上限、纹理颜色位宽等。
CCDirector扮演着全局导演的角色,因而很自然地采用了单实例的设计模式。在程序的任何地方,都可以通过下面的简单代码访问到:
CCDirector *pDirector = CCDirector::sharedDirector();
CCDirector中用于管理场景的方法如下所示:
1) runWithScene(CCScene* scene):
启动游戏,并运行scene场景。这个方法在主程序启动时第一次启动主场景时调用。
2) replaceScene(CCScene* scene):
直接使用传入的scene替换当前场景来切换画面,当前场景将被释放。这是切换场景时最常用的方法。
3) pushScene(CCScene* scene):
将当前运行中的场景暂停并压入到执行场景栈中,再将传入的scene设置为当前运行场景。
4) popScene(void):
释放当前场景,再从执行场景栈中弹出栈顶的场景,并将其设置为当前运行场景。如果栈为空,则直接结束应用。与pushScene成对使用,可以达到形如由主界面进入设置界面,然后回到主界面的效果。
5) pause(void):暂停当前运行场景中的所有计时器和动作,场景仍然会显示在屏幕上。
6) resume(void):恢复当前运行场景中被暂停的计时器和动作。它与pause配合使用。
7) end(void):结束场景,同时退出应用。
注:以上三种切换场景的方法(replaceScene、pushScene、popScene)均是先将待切换的场景完全加载完毕后,才将当前运行的场景释放掉。所以,在新场景恰好完全加载完毕的瞬间,系统中同时存在着两个场景,这将是对内存的一个考验,若不注意的话,切换场景可能会造成内存不足。
一个场景相当于电影的一个镜头,一个游戏可由很多场景组成,但在任意时刻,有且仅有一个场景被显示。一个游戏典型的场景有:介绍、菜单、第一关、第n关、闯关成功、闯关失败、排行榜。
一个场景可以由一个或多个孩子CCNode(如CCLayer、CCSprite等)组成。
通过其子类CCTransitionScene可以实现场景间切换的特技效果(如淡入淡出、滑动等)。
层是隶属于场景之下的游戏元素。通常,一个复杂场景会拥有多个层,一个层会显示一部分视觉元素,空白部分为透明或半透明,以实现多个层的重叠显示。层与层之间按照顺序叠放在一起,就组成了一个复杂的场景。
常用的层有:菜单层、触摸层、动画层、背景层。
CCLayer也是一具CCNode,其主要功能如下:
1) 处理Touch事件(ccTouchBegan, ccTouchMoved, ccTouchEnded, 或ccTouchCancelled),一个CCLayer可与玩家交互,Touch事件从前向后依次传递给CCScene中的所有层,直到有一层处理了此事件为止。
2) 渲染它们自己且可能半透明,以让游戏玩家看到其后面的层
3) 定义游戏画面和行为
Cocos2d-x提供了几个预定义的层:
1) CCMenu(一个简单的菜单层)
2) CCColorLayer(画指定颜色的层)
3) CCLayerMultiplex(有多个孩子,同一时刻只激活一个孩子)
CCLayer可包含任何的CCNode作为孩子,如 CCSprite、CCLabel或其它CCLayer对象。
CCLayer的一个十分重要的功能是可以接受用户输入事件,包括:触摸、加速度计和键盘输入等。其相关成员如下表所示:
成员类型 | 名称 | 描述 |
属性 | m_bTouchEnabled | 标识是否接收触摸事件 |
m_bAccelerometerEnabled | 标识是否接收触摸事件 | |
m_bKeypadEnabled | 标识是否接收触摸事件 | |
方法 | registerWithTouchDispatcher | 向Touch Dispatcher注册接收器,设置需要注册的触摸类型 |
回调函数 | didAccelerate | 接收G-Sensor事件 |
cTouchBegan | 单位点触摸 | |
ccTouchMoved | ||
ccTouchEnded | ||
ccTouchCancelled | ||
ccTouchesBegan | 多点触摸 | |
ccTouchesMoved | ||
ccTouchesEnded | ||
ccTouchesCancelled |
CCSprite是一个精灵,它是一个2D图像且可被平移、旋转、拉伸、动画或其它变换。它可以把其它的CCSprite作为其孩子,当进行变换时,它的所有孩子进行同样的变换。
层和场景是其他游戏元素的容器,如果没有向它们添加可见的游戏元素,它们看起来就一直是透明的。精灵则与层或场景不同,它隶属于层,是场景中出现的可见图形。玩家控制的主角、AI控制的NPC,以及地图上的宝箱、石块,甚至游戏主菜单的背景图片都是精灵。因此,可以这样认为,玩家看到的一切几乎都是由精灵构成的。
精灵不一定是静态的。通常,一个精灵可以不断变化,变化的方式包括:移动、旋转、缩放、变形、显现消失、动画效果(类似GIF动画)等。精灵按照层次结构组合起来,并与玩家互动,构成了一个完整的游戏。
创建精灵的方法:
1) 使用图片文件:
static CCSprite* create(const char *pszFileName);
static CCSprite* create(const char *pszFileName, const CCRect& rect);
2) 使用CCTexture2D:(纹理坐标的原点:在左上角,与OpenGL坐标<原点在左下角>不一样)
static CCSprite* create(CCTexture2D *pTexture);
static CCSprite* create(CCTexture2D *pTexture, const CCRect& rect);
3) 使用CCSpriteFrame:
static CCSprite* create(CCSpriteFrame *pSpriteFrame);
为了绘制场景,需要绘制场景中的层,为了绘制层,需要绘制层中的精灵。因此,关系图实质上安排了图元的绘图方式,关系图中的每一个元素称作节点(node),关系图则称作渲染树(rendering tree)。渲染场景的过程就是遍历渲染树的过程。
每个节点有一系列属性,包括节点相对于父节点的位置、旋转角度、缩放比例和变形参数等。渲染树的优势在于,我们只需要考虑节点相对于父节点的属性,就可以逐层创建复杂的对象或动作。
Cocos2d-x也采用了渲染树架构。任何可见的游戏元素都派生自Cocos2d-x节点(CCNode),常见的游戏元素有场景(CCScene)、层(CCLayer)和精灵(CCSprite)等。通常游戏按照场景、层、精灵的层次顺序组织,每种节点都有各自的特点。然而在实际开发中,为了实现一些特殊的效果,也不必拘泥于这个层次顺序。层或精灵都是普通的节点,因此,即使向精灵中添加精灵,向场景中添加精灵,甚至向精灵中添加层,这些操作也都没有被禁止
CCAction是发送给CCNode对象的命令,它经常修改CCNode对象的属性(如:位置、旋转、拉伸等)。如果这些属性在一段时间内修改(有开始时间和结束时间),则它是CCActionInterval;否则它就是一个CCActionInstant。
CCAction家族图谱如下图所示:
CCAction分类如下表所示:
类名 | 相关类名 |
Position |
CCMoveBy、CCMoveTo、CCJumpBy、CCJumpTo、CCBezierBy、CCBezierTo、CCPlace |
Scale |
CCScaleBy、CCScaleTo |
Rotation |
CCRotateBy、CCRotateTo |
Visibility |
CCShow、CCHide、CCBlink、CCToggleVisibility |
Opacity |
CCFadeIn、CCFadeOut、CCFadeTo |
Color |
CCTintBy、CCTintTo |
动画(Animation)是一种特殊的持续性动作,它只能应用于精灵上,用于实现帧动画效果。如同电影胶片一样,一个帧动画由多张静止的图片不停地切换形成。静止的图片叫做帧(frame),帧的序列代表一个动画效果。CCAnimation是CCObject的派生类,其例子代码如下:
CCAnimation *animation = CCAnimation::create(); // load image file from local file system to CCSpriteFrame, then add into CCAnimation for (int i = 1; i < 15; i++) { char szImageFileName[128] = {0}; sprintf(szImageFileName, "Images/grossini_dance_%02d.png", i); animation->addSpriteFrameWithFileName(szImageFileName); } animation->setDelayPerUnit(2.8f / 14.0f); // This animation contains 14 frames, will continuous 2.8 seconds. animation->setRestoreOriginalFrame(true); // Return to the 1st frame after the 14th frame is played. CCAnimate *action = CCAnimate::create(animation); sprite->runAction(action); // run action on sprite object从上面的代码可知,CCAnimation是由精灵的帧序列组成,CCAnimate是一个Action,它使用CCAnimation来创建。
它通过以下两个元素进行创建:
1).png:包含所有精灵帧的一个png文件
2).plist:定义了每一帧在png文件中的起始坐标和矩形大小,以及每一帧的名字,如:child1.gif、 father.gif、 sister1.gif、 sister2.gif。
其示例如下所示:
//装载精灵帧文件和列表,通过解析,把每个精灵帧保存在Cache中,以便后面使用 CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("animations/ghosts.plist", "animations/ghosts.png"); CCNode *aParent; CCSprite *l1, *l2a, *l2b, *l3a1, *l3a2, *l3b1, *l3b2; // // SpriteBatchNode: 3 levels of children // 必须被增加到Scene,它是rendering pipeline的一部分 aParent = CCSpriteBatchNode::create("animations/ghosts.png"); addChild(aParent, 0, kTagSprite1); // parent 根据Cache中的内容和给定的名字创建精灵帧 l1 = CCSprite::createWithSpriteFrameName("father.gif"); l1->setPosition(ccp( s.width/2, s.height/2)); aParent->addChild(l1, 0, kTagSprite2); CCSize l1Size = l1->getContentSize(); // child left根据Cache中的内容和给定的名字创建精灵帧 l2a = CCSprite::createWithSpriteFrameName("sister1.gif"); l2a->setPosition(ccp( -25 + l1Size.width/2, 0 + l1Size.height/2)); l1->addChild(l2a, -1, kTagSpriteLeft); CCSize l2aSize = l2a->getContentSize(); // child right根据Cache中的内容和给定的名字创建精灵帧 l2b = CCSprite::createWithSpriteFrameName("sister2.gif"); l2b->setPosition(ccp( +25 + l1Size.width/2, 0 + l1Size.height/2)); l1->addChild(l2b, 1, kTagSpriteRight); CCSize l2bSize = l2a->getContentSize(); // child left bottom根据Cache中的内容和给定的名字创建精灵帧 l3a1 = CCSprite::createWithSpriteFrameName("child1.gif"); l3a1->setScale(0.65f); l3a1->setPosition(ccp(0+l2aSize.width/2,-50+l2aSize.height/2)); l2a->addChild(l3a1, -1);
CCAnimationCache可以加载xml/plist文件,xml/plist文件描述了描述批量节点、精灵帧和他们的矩形区域。示例如下:
CCAnimationCache *cache = CCAnimationCache::sharedAnimationCache(); // "caches" are always singletons in cocos2d cache->addAnimationsWithFile("animations/animations-2.plist"); CCAnimation animation = cache->animationByName("dance_1"); CCAnimate animate = CCAnimate::create(animation); // Don't confused between CCAnimation and CCAnimate :) sprite->runAction(animate);
与精灵表动画相比,它占用更少的内存和加载时间。可使用CocosBuilder创建骨骼动画。在骨骼动画中, 一个精灵由两部分组成:
1)用于绘制精灵(称为皮肤或网格)的表面表示
2)一套分层的相互连接的骨头(称为骨架)用于动态(姿势和关键帧)的网格
UI坐标系的原点位于:左上角。
OpenGL和Cocos2d坐标系的原点位于:左下角。
纹理坐标系以左上角为原点,向右为x轴正方向,向下为y轴正方向,如下图所示。在Cocos2d-x中,只有从纹理中截取部分矩形时才使用这个坐标系,如CCSprite的TextureRect属性。
• Anchor Point(锚点):
CCNode有一个m_obAnchorPoint属性,可通过CCNode::setAnchorPoint进行设置。锚点同时用于定位和旋转对象。锚点坐标是一个相对坐标,通常对应Node对象中的一个点,如:ccp(0,0)位于Node对象左下角,ccp(0.5,0.5)位于Node对象中心,ccp(1,1)位于Node对象右上角。
AnchorPoint用于设置一个锚点,以便精确地控制节点的位置和变换。AnchorPoint的两个参量x和y的取值通常都是0到1之间的实数,表示锚点相对于节点长宽的位置。例如,把节点左下角作为锚点,值为(0,0);把节点的中心作为锚点,值为(0.5,0.5);把节点右下角作为锚点,值为(1,0)。精灵的AnchorPoint默认值为(0.5,0.5),其他节点的默认值为(0,0)。
• 设置对象位置(Position)
m_obPosition用于设置本Node对象的锚点处在父亲节点中的位置。
Position用于设置节点的位置。由于Position指的是锚点在父节点中的坐标值,节点显示的位置通常与锚点有关。因此,如果层与场景保持默认的位置,只需把层中精灵位置设为窗口长宽的一半即可让它显示在屏幕中央。
当多个节点嵌套时,每个节点的坐标系原点位于其内容的左下角。因此,锚点在父节点中的坐标实际上是它相对于父节点左下角的坐标值。
• 旋转对象
对象围绕Anchor Point(锚点)进行旋转。
• getVisibleSize, getVisibleOrigin vs getWinSize
其关系如下图及代码所示:
m_fScaleX = (float)m_obScreenSize.width / m_obDesignResolutionSize.width; //值为2.0 m_fScaleY = (float)m_obScreenSize.height / m_obDesignResolutionSize.height; //值为1.5 if (resolutionPolicy == kResolutionNoBorder) { m_fScaleX = m_fScaleY = MAX(m_fScaleX, m_fScaleY); //值为2.0,为了保持不变形,有一部分看不见 } CCSize CCEGLViewProtocol::getVisibleSize() const { if (m_eResolutionPolicy == kResolutionNoBorder) { //值为 (640,360) return CCSizeMake(m_obScreenSize.width/m_fScaleX, m_obScreenSize.height/m_fScaleY); } else { return m_obDesignResolutionSize; } } CCPoint CCEGLViewProtocol::getVisibleOrigin() const { if (m_eResolutionPolicy == kResolutionNoBorder) { //值为(0,60) return CCPointMake((m_obDesignResolutionSize.width - m_obScreenSize.width/m_fScaleX)/2, (m_obDesignResolutionSize.height - m_obScreenSize.height/m_fScaleY)/2); } else { return CCPointZero; } }
• 返回FrameBuffer分辨率
CCEGLView::sharedOpenGLView()->getFrameSize()
• designResolutionSize
所有的游戏坐标都是基于设计分辨率(designResolutionSize),而不关心设备屏幕(framebuffer)分辨率。
1) 如果游戏的UI设计资源只有一种,则通过以下函数来设置Design Resolution:
CCEGLView::sharedOpenGLView()->setDesignResolutionSize(width, height, policy)
2) 如果游戏的UI设计资源有多种,以更好地适合设备显示,则可以通过以下函数实现:
searchPath.push_back(largeResource.directory);
CCDirector::setContentScaleFactor(float scaleFactor);
//scaleFactor为:(资源的高度/设计分辨率的高度)与(资源的宽度/设计分辨率的宽度)中较小的一个。
• contentScaleFactor
在上面的状态下,为了让画面可以全屏显示,有以下2中解决方案:
1) 使用背景图片更宽
2) 基于宽度计算contentScaleFactor
本要看的代码是Cocos2d-x v2.2.1。
1) 初始化导演、EGLView和场景,由应用实现
2) 导演管理EGLView和场景
从以上分析可知,应用程序的主要工作就是根据当前用户的操作,创建对应的CCScene并进行显示。
游戏主循环为:CCDirector::mainLoop()方法,当然它只是一个被调用者,真正的主循环为GLThread->run。
这个方法负责调用定时器、绘图、发送全局通知、并处理内存回收池。该方法按帧调用,每帧调用一次,而帧间间隔取决于两个因素,一个是预设的帧率,默认为60帧每秒;另一个是每帧的计算量大小。当逻辑处理与绘图计算量过大时,设备无法完成每秒60次绘制,此时帧率就会降低。
void CCDisplayLinkDirector::mainLoop(void) { if (m_bPurgeDirecotorInNextLoop) { m_bPurgeDirecotorInNextLoop = false; purgeDirector(); } else if (! m_bInvalid) { // 绘制当前场景并进行其他必要的处理 drawScene(); // 弹出自动回收池,使得这一帧被放入自动回收池的对象全部释放。 CCPoolManager::sharedPoolManager()->pop(); } }
OpenGL维护了一个当前绘图矩阵,用于表示当前的绘图坐标系。这个矩阵被初始化为单位矩阵,此时绘图坐标系与世界坐标系相同,当我们不断地在绘图矩阵后乘上新的矩阵时,会相应地改变绘图坐标系。
在OpenGL ES 1.0中有对应的操作函数,而在Cocos2d-x 2.0采用的OpenGL ES 2.0中,这些函数已经不可使用了。OpenGL ES 2.0已经放弃了固定的渲染流水线,取而代之的是自定义的各种着色器,在这种情况下变换操作通常需要由开发者来维护。所幸引擎也引入了一套第三方库Kazmath,它使得我们几乎可以按照原来OpenGL ES 1.0所采用的方式进行开发。其对应关系如下表所示: