为什么我们要在一个实例方法中初始化类,而不在构造函数中初始化呢?在C++中,一般习惯在构造函数中初始化类,然而由于Cocos2d-x的来源特殊,所以才没有采用C++的编程风格
—————————————————————————————————————————————————————————
一个复杂场景会拥有多个层,一个层会显示一部分视觉元素,空白部分为透明或半透明,以实现多个层的重叠显示。层与层之间按照顺序叠放在一起,就组成了一个复杂的场景。
————clorlayer 设置透明
—————————————————————————————————————————————————————————
4条腿相对整个海龟在一定角度内旋转;躯干相对于整个海龟静止不动;整个海龟在鱼层中游动,位置和方向在不断改变。
如果没有树型结构,组织一个稍微复杂的游动都会成为一个巨大的工程。
—————————————————————————————————————————————————————————
构造函数与初始化
在C++中,我们只需要调用类的构造函数即可创建一个对象,既可直接创建一个栈上的值对象,也可以使用new操作符创建一个指针,指向堆上的对象。
Cocos2d-x不使用传统的值类型(由类型的实际值表示的数据类型指针类型指向数据类型),所有的对象都创建在堆上,然后通过指针引用。
在Objective-C中并没有构造函数,创建一个对象需要先为对象分配内存,然后调用初始化方法来初始化对象,这个过程就等价于C++中的构造函数。与Objective-C一样,Cocos2d-x也采用了这个步骤。Cocos2d-x类的构造函数通常没有参数,创建对象所需的参数通过init开头的一系列初始化方法传递给对象。
—————————————————————————————————————————————————————————
? bool init()
? {
? if(CCLayer::init()) //规范。 父类的init调用 否则会出问题的
? {
? //在此处写入初始化这个类所需的代码
? return true;
? }
? return false;
? }
—————————————————————————————————————————————————————————
现有的智能内存管理技术 ? 目前,主要有两种实现智能管理内存的技术,一是引用计数,一是垃圾回收。引用计数:它是一种很有效的机制,通过给每个对象维护一个引用计数器,记录该对象当前被引用的次数。当对象增加一次引用时,计数器加1;而对象失去一次引用时,计数器减1;当引用计数为0时,标志着该对象的生命周期结束,自动触发对象的回收释放。引用计数的重要规则是每一个程序片段必须负责任地维护引用计数,在需要维持对象生存的程序段的开始和结束分别增加和减少一次引用计数,这样我们就可以实现十分灵活的智能内存管理了。实际上,这与new和delete的配对使用十分类似,但是很巧妙地将生成和回收的事件转换成了使用和使用结束的事件。对于程序员来说,维护引用计数比维护生命周期信息轻松了许多。引用计数解决了对象的生命周期管理问题,但堆碎片化和管理烦琐的问题仍然存在。垃圾回收:它通过引入一种自动的内存回收器,试图将程序员从复杂的内存管理任务中完全解放出来。它会自动跟踪每一个对象的所有引用,以便找到所有正在使用的对象,然后释放其余不再需要的对象。垃圾回收器还可以压缩使用中的内存,以缩小堆所需要的工作空间。垃圾回收可以防止内存泄露,有效地使用可用内存。但是,垃圾回收器通常是作为一个单独的低级别的线程运行的,在不可预知的情况下对内存堆中已经死亡的或者长时间没有使用过的对象进行清除和回收,程序员不能手动指派垃圾回收器回收某个对象。回收机制包括分代复制垃圾回收、标记垃圾回收和增量垃圾回收。
Cocos2d-x的内存管理机制
为了实现对象的引用计数记录,Cocos2d-x实现了自己的根类CCObject,引擎中的所有类都派生自CCObject。在"CCObject.h"头文件中我们可以看到CCObject的定义
class CC_DLL CCObject : public CCCopying
? {
? public:
//对象id,在脚本引擎中使用
? unsigned int m_uID;
? //Lua中的引用ID,同样被脚本引擎使用
? int m_nLuaID;
? protected:
? //引用数量
? unsigned int m_uReference;
? //标识此对象是否已设置为autorelease
? bool m_bManaged;
? public:
? CCObject(void);
? virtual ~CCObject(void);
? void release(void);
? void retain(void);
? CCObject* autorelease(void);
? CCObject* copy(void);
? bool isSingleRefrence(void);
? unsigned int retainCount(void);
? virtual bool isEqual(const CCObject* pObject);
? virtual void update(ccTime dt) {CC_UNUSED_PARAM(dt);};
? friend class CCAutoreleasePool;
? };
每个对象包含一个用来控制生命周期的引用计数器,它就是CCObject的成员变量m_u- Reference。我们可以通过retainCount()方法获得对象当前的引用计数值。在对象通过构造函数创建的时候,该引用值被赋为1,表示对象由创建者所引用。在其他地方需要引用对象时,我们会调用retain()方法,令其引用计数增1,表示获取该对象的引用权;在引用结束的时候调用release()方法,令其引用计数值减1,表示释放该对象的引用权。
另一个很有趣的方法是autorelease(),其作用是将对象放入自动回收池(CCAutore- leasePool)。当回收池自身被释放的时候,它就会对池中的所有对象执行一次release()方法,实现灵活的垃圾回收。回收池可以手动创建和释放。除此之外,引擎在每次游戏循环开始之前也会创建一个【新的】回收池,在循环结束后释放回收池。因此,即使我们没有手工创建和释放回收池,每一帧结束的时候,自动回收池中的对象也都会被执行一次release()方法。我们马上就会领略到autorelease()的方便之处。
下面是一个简单的例子。可以看到,对象创建后,引用计数为1;执行一次retain()后,引用计数为2;执行一次release()后,引用计数回到1;执行一次autorelease()后,对象的引用计数值并没有立即减1,但是在下一帧开始前,对象会被释放掉。
虽然,Cocos2d-x已经保证每一帧结束后释放一次回收池,并在下一帧开始前创建一个新的回收池,但是我们也应该考虑到回收池本身维护着一个将要执行释放操作的对象列表,如果在一帧之内生成了大量的autorelease对象,将会导致回收池性能下降。因此,在生成autorelease对象密集的区域(通常是循环中)的前后,我们最好可以手动创建并释放一个回收池。
我们可以通过回收池管理器CCPoolManager的push()或pop()方法来创建或释放回收池,其中的CCPoolManager也是一个单例对象
CCPoolManager::sharedPoolManager()->push();
? for(int i=0; i
? CCString* dataItem = CCString::createWithFormat("%d", Data[i]);
? stringArray->addObject(dataItem);
? }
? CCPoolManager::sharedPoolManager()->pop();
通常,引擎维护着一个回收池,所有的autorelease对象都添加到了这个池中。多个自动回收池排列成栈结构,当我们手动创建了回收池后,回收池会压入栈的顶端,autorelease对象仅添加到顶端的池中。当顶层的回收池被弹出释放时,它内部所有的对象都会被释放一次,此后出现的autorelease对象则会添加到下一个池中
(还是蛮好用的)
————————————
工厂方法
? CCObject* factoryMethod() {
? CCObject* ret = new CCObject();
? //这里对ret对象进行必要的初始化操作
? ret->autorelease(); ——————创建后加入垃圾池 所以不addChild在下一帧就没了!!!!!!!
? return ret;
? }
///用new+init+autorelease也可以 的 吧
如果需要继续使用 调用者有足够的时间来对它进行retain操作以便接管ret对象的引用权
因此,我们建议在开发过程中应该避免滥用autorelease(),只在工厂方法等不得不用的情况下使用,尽量以release()来释放对象引用。
————————————
关于对象传值 ?
将一个对象赋值给某一指针作为引用的时候,为了遵循内存管理的原则,我们需要获得新对象的引用权,释放旧对象的引用权。此时,release()和retain()的顺序是尤为重要的首先来看下面一段代码:
void SomeClass::setObject(CCObject* other)
{
? this->object->release();
? other->retain();
? this->object = other;
} ?
这里存在的隐患是,当other和object实际上指向同一个对象时,第一个release()可能会触发该对象的回收,这显然不是我们想看到的局面,所以应该先执行retain()来保证other对象有效,然后再释放旧对象:
void SomeClass::setObject(CCObject* other) {
? other->retain();
? this->object->release();
this->object = other;
}
——————————
容器
使用Cocos2d-x容器的一个重要原因在于Cocos2d-x的内存管理。一般来说,被存入容器的对象在移除之前都应该保证是有效的,回顾一下引用计数的管理原则,对象的存入和移除必须对应一组retain()和release()或者对应autorelease()。直接使用STL容器,开发者势必进行烦琐重复的内存管理操作,而Cocos2d-x容器对这一过程进行了封装,保证了容器对对象的存取过程总是符合引用计数的内存管理原则。
存入容器的对象必须是CCObject或其派生类。同时,Cocos2d-x的容器本身也是CCObject的派生类,当容器被释放时,它保存的所有元素都会被释放一次引用
容器存在的意义不仅仅局限于内存管理方面,因此我们应该尽量采用Cocos2d-x提供的容器类。(跨语言移植游戏 所以不用stl)
内存管理原则总结——————
程序段必须成对执行retain()和release()或者执行autorelease()来声明开始和结束对象的引用;工厂方法返回前,应通过autorelease()结束对该对象的引用;对象传值时,应考虑到新旧对象相同的特殊情况;尽量使用release()而不是autorelease()来释放对象引用,以确保性能最优;保存CCObject的子类对象时,应严格使用Cocos2d-x提供的容器,避免使用STL容器,对象必须以指针形式存入。
如果希望自定义的类也拥有Cocos2d-x的内存管理功能,可以把CCObject作为自定义类的基类,并在实现类时严格遵守Cocos2d-x的内存管理原则
—————————————————————————————————————————————————————————
导演
popScene:释放当前场景,再从代执行场景栈中弹出栈顶的场景,并将其设置为当前运行场景。如果栈为空,则直接结束应用。与pushScene成对使用,可以达到形如由主界面进入设置界面,然后回到主界面的效果。
值得注意的一点是,以上三种切换场景的方法(replaceScene、pushScene、popScene)均是先将待切换的场景完全加载完毕后,才将当前运行的场景释放掉。所以,在新场景恰好完全加载完毕的瞬间,系统中同时存在着两个场景
—————————————————————————————————————————————————————————
层
void addChild(CCNode* child);
void addChild(CCNode* child, int zOrder);
void addChild(CCNode* child, int zOrder, int tag);
如果为子节点设置了tag值,就可以在它的父节点中利用tag值找到它了
—————————————————————————————————————————————————————————
精灵
bool isFrameDisplayed(CCSpriteFrame *pFrame):返回一个值,表示pFrame是否是正在显示中的纹理框帧。
颜色相关的属性 ?
CCSprite提供了以下与颜色相关的属性。 ?
ccColor3 Color:获取或设置叠加在精灵上的颜色。
ccColor3由三个颜色分量(红色、绿色和蓝色分量)组成。默认为纯白色,表示不改变精灵的颜色,如果设置为其他值,则会改变精灵的颜色。 ?
GLubyte Opacity:获取或设置精灵的不透明度。GLubyte为OpenGL的内置类型,表示一个无符号8位整数,取值范围从最小值0到最大值255。 ?
bool OpacityModifyRGB:获取或设置精灵所使用的纹理数据是否已经预乘Alpha通道。当包含Alpha通道的图片显示错误时,可以尝试修改这个属性。
对于精灵来说,ContentSize是它的纹理显示部分的大小;对于层或场景等全屏的大型节点来说,ContentSize则是屏幕大小。
对于场景或层等大型节点,它们的IgnoreAnchorPointForPosition属性为true,此时引擎会认为AnchorPoint永远为(0,0);而其他节点的该属性为flase,它们的锚点不会被忽略。
Tag可以用于定位子节点,因此添加到同一节点的所有CCNode之中,不能有两个节点的Tag相同,否则就给定位带来了麻烦。与Tag相关的方法有getChildByTag、removeChildByTag等。
void* UserData:获取或设置与节点相关的额外信息。UserData为void*类型,我们可以利用这个属性来保存任何数据。
—————————————————————————————————————————————————————————
定时器
很显然,定时器就是使游戏动态变化所需的工具。Cocos2d-x为我们提供了两种方式实现定时机制--使用update方法以及使用schedule方法,下面简要介绍这两种方式。
update定时器 ? 第一种定时机制是CCNode的刷新事件update方法,该方法在每帧绘制之前都会被触发一次
schedule定时器 ? 另一种定时机制是CCNode提供的schedule方法,可以实现以一定的时间间隔连续调用某个函数。由于引擎的调度机制,这里的时间间隔必须大于两帧的间隔,否则两帧期间的多次调用会被合并成一次调用。
实际开发中,许多定时操作都通过schedule定时器实现,例如鱼群的定时生成、免费金币的定时刷新等(因为update的调用间隔是不可控的)
。我们把处理鱼群移动和碰撞监测的代码放置在主游戏场景GameScene的updateGame方法中。在GameScene的init初始化方法中添加以下代码来启用定时器:
? this->schedule(schedule_selector(GameScene::updateGame)
一般是 不用update的 直接自定义一个update函数
scheduleUpdateWithPriority(int priority) 启用update定时器,并设定定时器的优先级
schedules the "update" selector with a custom priority. This selector will be called every frame.
Scheduled selectors with a lower priority will be called before the ones that have a higher value.
Only one "update" selector could be scheduled per node (You can't have 2 'update' selectors)
unscheduleUpdate 取消update定时器
scheduleOnce(SEL_SCHEDULE selector, float delay)CONTROL 添加一个schedule定时器,但定时器只触发一次
unschedule(SEL_SCHEDULE selector)ESC 取消selector所对应函数的定时器
unscheduleAllSelectors 取消此节点所关联的全部定时器
pauseSchedulerAndActions 暂停此节点所关联的全部定时器与动作
resumeSchedulerAndActions 继续执行此节点所关联的定时器与动作
定时器机制是Cocos2d-x调度机制的基础,第4章将介绍的动作机制实际上也依赖定时器实现。由于Cocos2d-x的调度是纯粹的串行机制,因此所有函数都运行在同一个线程,不会存在并行程序的种种麻烦,这大大简化了编程的复杂性。
其他与流程控制相关的事件————
onEnter()——
当此节点所在场景即将呈现时,会调用此方法
onEnterTransitionDidFinish()——
当此节点所在场景的入场动作结束后,会调用此方法。如果所在场景没有入场动作,则此方法会紧接着onEnter()后被调用
onExit()——
当此节点所在场景即将退出时,会调用此方法
onExitTransitionDidStart()——
当此节点所在场景的出场动作结束后,会调用此方法。如果所在场景没有出场动作,则此方法会紧接着onExit()后被调用
这些事件的默认实现通常负责处理定时器和动作的启用与暂停,因此必须在重载方法中调用父类的方法
—————————————————————————————————————————————————————————
Cocos2d-x内置的常用层(1)
CCLayerColor:一个单纯的实心色块。 ?
CCLayerGradient:一个色块,但可以设置两种颜色的渐变效果。 ?
CCMenu:十分常用的游戏菜单。
static CCLayerColor * create(const ccColor4B& color);
static CCLayerColor * create(const ccColor4B& color, GLfloat width, GLfloat height);
static CCLayerGradient* create(const ccColor4B& start, const ccColor4B& end,const CCPoint& v);
在色块创建后,也可以通过下面列举的方法来修改色块大小:
void changeWidth(GLfloat w);
void changeHeight(GLfloat h);
void changeWidthAndHeight(GLfloat w ,GLfloat h);
—————————————————————————————————————————————————————————
Cocos2d-x调度原理 ?
Cocos2d的一大特色就是提供了事件驱动的游戏框架,引擎会在合适的时候调用事件处理函数,我们只需要在函数中添加对各种游戏事件的处理,就可以完成一个完整的游戏了。例如,为了实现游戏的动态变化,Cocos2d提供了两种定时器事件;为了响应用户输入,Cocos2d提供了触摸事件和传感器事件;此外,Cocos2d还提供了一系列控制程序生命周期的事件。
游戏主循环(1)
游戏乃至图形界面的本质是不断地绘图,然而绘图并不是随意的,任何游戏都需要遵循一定的规则来呈现出来,这些规则就体现为游戏逻辑。游戏逻辑会控制游戏内容,使其根据用户输入和时间流逝而改变。因此,游戏可以抽象为不断地重复以下动作————? 处理用户输入 ?
处理定时事件 ?
绘图 ?
1、游戏主循环就是这样的一个循环,它会反复执行以上动作,保持游戏进行下去,直到玩家退出游戏
在Cocos2d中,以上的动作包含在CCDirector的某个方法之中,而引擎会根据不同的平台设法使系统不断地调用这个方法,从而完成了游戏主循环。 ?
2、现在我们回到Cocos2d-x游戏主循环的话题上来。上面介绍了CCDirector包含一个管理引擎逻辑的方法,它就是CCDirector::mainLoop()方法,这个方法负责调用定时器,绘图,发送全局通知,并处理内存回收池。该方法按帧调用,每帧调用一次,而帧间间隔取决于两个因素,一个是预设的帧率,默认为60帧每秒;另一个是每帧的计算量大小。当逻辑处理与绘图计算量过大时,设备无法完成每秒60次绘制,此时帧率就会降低。
3、mainLoop()方法会被定时调用,然而在不同的平台下它的调用者不同。通常CCApplication类负责处理平台相关的任务,其中就包含了对mainLoop()的调用。有兴趣的读者可以对比Android、iOS与Windows Phone三个平台下不同的实现,平台相关的代码位于引擎的"platform"目录。
mainLoop()方法是定义在CCDirector中的抽象方法,它的实现位于同一个文件中的CCDisplayLinkDirector类。现在我们来看一下它的代码:
void CCDisplayLinkDirector::mainLoop()
? {
? if (m_bPurgeDirecotorInNextLoop)
? {
? m_bPurgeDirecotorInNextLoop = false;
? purgeDirector();
? }
? else if (! m_bInvalid)
? {
? drawScene();
? //释放对象
? CCPoolManager::sharedPoolManager()->pop();
? }
? }
上述代码主要包含如下3个步骤。 ?
1、判断是否需要释放CCDirector,如果需要,则删除CCDirector占用的资源。通常,游戏结束时才会执行这个步骤。 ?
2、调用drawScene()方法,绘制当前场景并进行其他必要的处理。 ?
3、弹出自动回收池,使得这一帧被放入自动回收池的对象全部释放。
由此可见,mainLoop()把内存管理以外的操作都交给了drawScene()方法,因此关键的步骤都在drawScene()方法之中。下面是drawScene()方法的实现:
void CCDirector::drawScene()
? {
? //计算全局帧间时间差dt
? calculateDeltaTime();
?
? if (! m_bPaused)
? {
? m_pScheduler->update(m_fDeltaTime);
? }
?
? glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
if (m_pNextScene)
? {
? setNextScene();
? }
?
? kmGLPushMatrix();
//绘制场景
? if (m_pRunningScene)
? {
? m_pRunningScene->visit();
? }
?
? //处理通知节点
? if (m_pNotificationNode)
? {
? m_pNotificationNode->visit();
? }
if (m_bDisplayStats)
? {
? showStats();
? }
?
? if (m_pWatcherFun && m_pWatcherSender)
? {
? (*m_pWatcherFun)(m_pWatcherSender);
? }
kmGLPopMatrix();
?
? m_uTotalFrames++;
?
? //交换缓冲区
? if (m_pobOpenGLView)
? {
? m_pobOpenGLView->swapBuffers();
? }
if (m_bDisplayStats)
? {
? calculateMPF();
? }
? }
—————————————————————————————————————————————————————————
游戏主循环(2)
我们看到drawScene()方法内进行了许多操作,甚至包含了少量OpenGL函数。这是由于Cocos2d-x在游戏主循环中对引擎的细节进行了许多处理,我们并不关心这些细节,因此我们首先剔除掉细枝末节,整理出一个精简版本的drawScene()方法:
void CCDirector::drawSceneSimplified()
{
? _calculate_time();
?
? if (! m_bPaused)
? m_pScheduler->update(m_fDeltaTime);
?
? if (m_pNextScene)
? setNextScene();
?
? _deal_with_opengl();
if (m_pRunningScene)
? m_pRunningScene->visit();
?
? _do_other_things();
? }
对比一下drawSceneSimplified()与drawScene()的代码,可以发现我们省略掉的代码主要用于处理OpenGL和一些细节,如计算FPS、帧间时间差等。在主循环中,我们主要进行了以下3个操作。 ?
1、调用了定时调度器的update方法,引发定时器事件。 ?
2、如果场景需要被切换,则调用setNextStage方法,在显示场景前切换场景。 ?
3、调用当前场景的visit方法,绘制当前场景。
—————————————————————————————————————————————————————————
CCScheduler成员 ?
经过上面的分析,我们已经知道CCNode提供的定时器不是由它本身而是由CCScheduler管理的。因此,我们把注意力转移到定时调度器上。显而易见,定时调度器应该对每一个节点维护一个定时器列表,在恰当的时候就会触发其定时事件。
打开CCScheduler类的头文件,可以看到它的成员。 ?
scheduleSelector 为指定目标设置一个定时器
unscheduleSelector 取消指定目标的定时器
unscheduleAllSelectorsForTarget 取消指定目标的所有定时器(包含普通定时器与update定时器)
unscheduleAllSelectors 取消所有被CCScheduler管理的定时器,包括update定时器
scheduleUpdateForTarget 启用指定目标的update定时器
unscheduleUpdateForTarget 取消指定目标的update定时器
pauseTarge 暂停指定目标的全部定时器
resumeTarget 恢复指定目标的全部定时器
isTargetPaused 返回一个值,表示目标是否被暂停
pauseAllTargets 暂停所有被CCScheduler管理的目标 私有字段
m_pUpdatesNegList 一个链表,记录优先值小于0的update定时器
m_pUpdates0List 一个链表,记录优先值为0的update定时器
m_pUpdatesPosList 一个链表,记录优先值大于0的update定时器
m_pHashForUpdates 记录全部update定时器的散列表,便于调度器检索定时器
m_pHashForSelectors 记录普通定时器的散列表
调度器可以随时增删或修改被注册的定时器。具体来看,调度器将update定时器与普通定时器分别处理:当某个节点注册update定时器时,调度器就会把节点添加到Updates容器中,为了提高调度器效率,Cocos2d-x使用了散列表与链表结合的方式来保存定时器信息;当某个节点注册普通定时器时,调度器会把回调函数和其他信息保存到Selectors散列表中。
update方法
?
在游戏主循环中,我们已经见到了update方法。可以看到,游戏主循环会不停地调用update方法。该方法包含一个实型参数,表示两次调用的时间间隔。在该方法中,引擎会利用两次调用的间隔来计算何时触发定时器。 ?
update方法的实现看起来较为复杂,而实际上它的内部多是重复的代码片段,逻辑并不复杂。我们可以利用Cocos2d-x中精心编写的注释来帮助理解update方法的工作流程,相关代码如下:
void CCScheduler::update(float dt)
? {
? m_bUpdateHashLocked = true;
?
? //a.预处理
? if (m_fTimeScale != 1.0f)
? dt *= m_fTimeScale;
?
? //b.枚举所有的update定时器
? tListEntry *pEntry, *pTmp;
//优先级小于0的定时器
? DL_FOREACH_SAFE(m_pUpdatesNegList, pEntry, pTmp)
? if ((! pEntry->paused) && (! pEntry->markedForDeletion))
? pEntry->target->update(dt);
?
? //优先级等于0的定时器
? DL_FOREACH_SAFE(m_pUpdates0List, pEntry, pTmp)
if ((! pEntry->paused) && (! pEntry->markedForDeletion))
? pEntry->target->update(dt);
//优先级大于0的定时器
? DL_FOREACH_SAFE(m_pUpdatesPosList, pEntry, pTmp)
? if ((! pEntry->paused) && (! pEntry->markedForDeletion))
? pEntry->target->update(dt);
?
? //c.枚举所有的普通定时器
for (tHashSelectorEntry *elt = m_pHashForSelectors; elt != NULL; )
? {
? m_pCurrentTarget = elt;
? m_bCurrentTargetSalvaged = false;
?
? if (! m_pCurrentTarget->paused)
? {
? //枚举此节点中的所有定时器
? //timers数组可能在循环中改变,因此在此处需要小心处理
? for (elt->timerIndex = 0; elt->timerIndex < elt->timers->num;
++(elt->timerIndex))
? {
elt->currentTimer = (CCTimer*)(elt->timers->arr[elt->timerIndex]);
? elt->currentTimerSalvaged = false;
?
? elt->currentTimer->update(dt);
?
? if (elt->currentTimerSalvaged)
? {
? elt->currentTimer->release();
? }
?
? elt->currentTimer = NULL;
? }
? }
?
elt = (tHashSelectorEntry *)elt->hh.next;
?
? if (m_bCurrentTargetSalvaged && m_pCurrentTarget->timers->num == 0)
? {
? removeHashElement(m_pCurrentTarget);
? }
? }
//d.处理脚本引擎相关的事件
? if (m_pScriptHandlerEntries)
? {
? for (int i = m_pScriptHandlerEntries->count() - 1; i >= 0; i--)
? {
? CCSchedulerScriptHandlerEntry* pEntry =
? static_cast
? ->objectAtIndex(i));
? if (pEntry->isMarkedForDeletion())
? {
? m_pScriptHandlerEntries->removeObjectAtIndex(i);
}
? else if (!pEntry->isPaused())
? {
? pEntry->getTimer()->update(dt);
? }
? }
? }
//e.清理所有被标记了删除记号的update方法
? //优先级小于0的定时器
? DL_FOREACH_SAFE(m_pUpdatesNegList, pEntry, pTmp)
? {
? if (pEntry->markedForDeletion)
? {
? this->removeUpdateFromHash(pEntry);
? }
? }
//优先级等于0的定时器
? DL_FOREACH_SAFE(m_pUpdates0List, pEntry, pTmp)
? {
? if (pEntry->markedForDeletion)
? {
? this->removeUpdateFromHash(pEntry);
? }
? }
//优先级大于0的定时器
? DL_FOREACH_SAFE(m_pUpdatesPosList, pEntry, pTmp)
? {
? if (pEntry->markedForDeletion)
{
? this->removeUpdateFromHash(pEntry);
? }
? }
?
? m_bUpdateHashLocked = false;
?
? m_pCurrentTarget = NULL;
}
—————————————————————————————————————————————————————————
定时调度器(3)
借助注释,能够看出update方法的流程大致如下所示。
?
1、参数dt乘以一个缩放系数,以改变游戏全局的速度,其中缩放系数可以由CCScheduler的TimeScale属性设置。 ?
2、分别枚举优先级小于0、等于0、大于0的update定时器。如果定时器没有暂停,也没有被标记为即将删除,则触发定时器。 ?
3、枚举所有注册过普通定时器的节点,再枚举该节点的定时器,调用定时器的更新方法,从而决定是否触发该定时器。 ?
4、我们暂不关心脚本引擎相关的处理。 ?
5、再次枚举优先级小于0、等于0、大于0的update定时器,移除前几个步骤中被标记了删除记号的定时器。
6、对于update定时器来说,每一节点只可能注册一个定时器,因此调度器中存储定时器数据的结构体_listEntry主要保存了注册者与优先级。对于普通定时器来说,每一个节点可以注册多个定时器,引擎使用回调函数(选择器)来区分同一节点下注册的不同定时器。调度器为每一个定时器创建了一个CCTimer对象,它记录了定时器的目标、回调函数、触发周期、重复触发还是仅触发一次等属性。 ?
7、CCTimer也提供了update方法,它的名字和参数都与CCScheduler的update方法一样,而且它们也都需要被定时调用。不同的是,CCTimer的update方法会把每一次调用时接收的时间间隔dt积累下来,如果经历的时间达到了周期,就会引发定时器的定时事件。第一次引发了定时事件后,如果是仅触发一次的定时器,则update方法会中止,否则定时器会重新计时,从而反复地触发定时事件。
8、回到CCScheduler的update方法上来。在步骤c中,程序首先枚举了每一个注册过定时器的对象,然后再枚举对象中定时器对应的CCTimer对象,调用CCTimer对象的update方法来更新定时器状态,以便触发定时事件。 ?
9、至此,我们可以看到事件驱动的普通定时器调用顺序为:系统的时间事件驱动游戏主循环,游戏主循环调用CCScheduler的update方法,CCScheduler调用普通定时器对应的CCTimer对象的update方法,CCTimer类的update方法调用定时器对应的回调函数。对于update定时器,调用顺序更为简单,因此前面仅列出了普通定时器的调用顺序。 ?
10、同时,我们也可以看到,在定时器被触发的时刻,CCScheduler类的update方法正在迭代之中,开发者完全可能在定时器事件中启用或停止其他定时器(如图3-8所示)。不过,这么做会导致update方法中的迭代被破坏。Cocos2d-x的设计已经考虑到了这个问题,采用了一些技巧避免迭代被破坏。例如,update定时器被删除时,不会直接删除,而是标记为将要删除,在定时器迭代完毕后再清理被标记的定时器,这样即可保证迭代的正确性。
CCSchedule::update(float dt)
--------
for each CCTimer t in |timers|<__________
-------- |
{ |
CCTime::update(float dt) |
_____________________________ |
|GameScene::tick(float dt) | |
|self->unschedule(selector1)|___|
-----------------------------
在迭代中修改容器
Cocos2d-x的设计使得很多离散在各处的代码通过事件联系起来,在每一帧中起作用。基于事件驱动的游戏框架易于掌握,使用灵活,而且所有事件串行地在同一线程中执行,不会出现线程同步的问题。在后面更深入的讨论中,读者将会看到更多有趣的现象。
定时器:
分为update定时器与schedule定时器,前者每一帧触发一次,而后者可以指定触发间隔。定时器由定时调度器(CCScheduler)控制,每个定时器互不干扰,串行执行。
动作———————————————————————————————————————————————————————
CCEaseBounceOut
http://blog.csdn.net/dayuqi/article/details/8121813贝塞尔曲线动作
值得注意的是,一个CCAction只能使用一次,这是因为动作对象不仅描述了动作,还保存了这个动作持续过程中不断改变的一些中间参数。对于需要反复使用的动作对象,可以通过copy方法复制使用。
由CCFiniteTimeAction派生出的两个主要类分别是瞬时动作(CCActionInstant)和持续性动作(CCActionInterval)
瞬时动作
更准确地说,这类动作是在下一帧会立刻执行并完成的动作,如设定位置、设定缩放等。这些动作原本可以通过简单地对CCNode赋值完成,但是把它们包装为动作后,可以方便地与其他动作类组合为复杂动作。(如回调函数动作)
持续动作—————————————————————
贝塞尔曲线CCBezierTo和CCBezierBy:
使节点进行曲线运动,运动的轨迹由贝塞尔曲线描述。贝塞尔曲线是描述任意曲线的有力工具,在许多软件(如Adobe Photoshop)中,钢笔工具就是贝塞尔曲线的应用。实际上,在《捕鱼达人》游戏中,为了控制鱼的游动,我们就用到了贝塞尔曲线。
每一条贝塞尔曲线都包含一个起点和一个终点。在一条曲线中,起点和终点都各自包含一个控制点,而控制点到端点的连线称作控制线。控制线决定了从端点发出的曲线的形状,包含角度和长度两个参数:角度决定了它所控制的曲线的方向,即这段曲线在这一控制点的切线方向;长度控制曲线的曲率。控制线越长,它所控制的曲线离控制线越近。
任意一段曲线都可以由一段或几段相连的贝塞尔曲线组成,因此我们只需考虑一段贝塞尔曲线应该如何描述即可
使用时我们要先创建ccBezierConfig结构体,设置好终点endPosition以及两个控制点controlPoint_1和controlPoint_2后,再把结构体传入CCBezierTo或CCBezierBy的初始化方法中:
ccBezierConfig bezier;
bezier.controlPoint_1 = ccp(20, 150);
bezier.controlPoint_2 = ccp(200, 30);
bezier.endPosition = ccp(160, 30);
CFiniteTimeAction * beizerAction = CCBezierTo::create(actualDuration / 4, bezier);
CCTintTo和CCTintBy:设置色调变化。这个动作较为少用
CCSequence提供了一个动作队列,它会顺序执行一系列动作,例如鱼游出屏幕外后需要调用回调函数,捕到鱼后显示金币数量,经过一段时间再让金币数量消失,等等。
在实现CCSequence和CCSpawn两个组合动作类时,有一个非常有趣的细节:成员变量中并没有定义一个可变长的容器来容纳每一个动作系列,而是定义了m_pOne和m_pTwo两个动作成员变量。采用这种递归的方式,而不是直接使用容器来定义组合动作
延时(CCDelayTime) ?
CCDelayTime是一个"什么都不做"的动作,类似于音乐中的休止符,用来表示动作序列里一段空白期,通过占位的方式将不同的动作段串接在一起。实际上,这与一个定时期实现的延迟没有区别,但相比之下,使用CCDelayTime动作来延时就可以方便地利用动作序列把一套动作连接在一起。
CCSpeed ?
CCSpeed用于线性地改变某个动作的速度,因此,可以实现成倍地快放或慢放功能。为了改变一个动作的速度,首先需要将目标动作包装到CCSpeed动作中
使用repeat动作创建了一个CCSpeed变速动作。create初始化方法中的两个参数分别为目标动作与变速比率。设置变速比率为1,目标动作的速度将不会改变。
下面的代码将会把上面设置的动画速度变为原来的两倍:
speed->setSpeed(2.0f);
CCActionEase ?
虽然使用CCSpeed能够改变动作的速度,然而它只能按比例改变目标动作的速度。如果我们要实现动作由快到慢、速度随时间改变的变速运动,需要不停地修改它的speed属性才能实现,显然这是一个很烦琐的方法。下面将要介绍的CCActionEase系列动作通过使用内置的多种自动速度变化来解决这一问题。
CCActionEase系列包含15个动作,它们可以被概括为5类动作:指数缓冲、Sine缓冲、弹性缓冲、跳跃缓冲和回震缓冲。每一类动作都有3个不同时期的变换:In、Out和InOut
对于曲线运动来说,鱼的方向并没有精确地吻合游动轨迹。
如果物体沿着一条曲线移动,那么物体在某一点的瞬时速度方向一定是该点切线的正方向
因此,我们只需要根据两帧中鱼的位置差计算出鱼前进的方向
自定义动作————————————————————————————————————————————————————
CCAction包含两个重要的方法:step与update。step方法会在每一帧动作更新时触发,该方法接受一个表示调用时间间隔的参数dt,dt的积累即为动作运行的总时间。引擎利用积累时间来计算动作运行的进度(一个从0到1的实数),并调用update方法更新动作。update方法是CCAction的核心,它由step方法调用,接受一个表示动作进度的参数,每一个动作都需要利用进度值改变目标节点的属性或执行其他指令。自定义动作只需要从这两个方法入手即可,我们通常只需要修改update方法就可以实现简单的动作。
下面我们编写一个继承于CCAction的CCRotateAction动作。如同复合动作与变速动作一样,它会把另一个动作包装起来,在执行被包装动作的同时,设置精灵的方向。为此,我们需要在每一帧?记录上一帧精灵的位置,然后再根据精灵两帧的位移确定精灵的方向。由于我们必须在CCRotateAction执行的同时运行被包含的目标动作,所以我们需要在step方法中调用目标动作的step方法。下面我们来看CCRotateAction的实现。
"RotateWithAction.h"中的定义如下:
?
class RotateWithAction : public CCActionInterval
{
? public:
? CCObject* copyWithZone(CCZone* pZone);
? ~RotateWithAction();
? static RotateWithAction* create(CCActionInterval * action);
? virtual void startWithTarget(CCNode* pTarget);
? bool initWithAction(CCActionInterval* pAction);
? bool isDone();
? void step(ccTime dt);
protected:
? void RotateWithAction::setInnerAction(CCActionInterval* pAction);
?
? CCNode* pInnerTarget;
? CCActionInterval* pInnerAction;
?
};
"RotateWithAction.cpp"中的实现如下:
RotateWithAction::~RotateWithAction()
{
CC_SAFE_RELEASE(pInnerAction);
? }
?
? RotateWithAction* RotateWithAction::create(CCActionInterval* pAction)
? {
RotateWithAction* action = new RotateWithAction();
? if (action && action->initWithAction(pAction))
? {
? action->autorelease();
? return action;
? }
? CC_SAFE_DELETE(action);
? return NULL;
? }
bool RotateWithAction::initWithAction(CCActionInterval* pAction)
? {
? pAction->retain();
? pInnerAction = pAction;
? return true;
}
?
? void RotateWithAction::startWithTarget(CCNode* pTarget)
? {
? pInnerTarget = pTarget;
? CCAction::startWithTarget(pTarget);
? pInnerAction->startWithTarget(pTarget);
? }
bool RotateWithAction::isDone()
? {
? return pInnerAction->isDone();
? }
?
? void RotateWithAction::step(ccTime dt)
? {
? CCPoint prePos = pInnerTarget->getPosition();
? pInnerAction->step(dt);
CCPoint curPos = pInnerTarget->getPosition();
?
? float tan = -(curPos.y - prePos.y) / (curPos.x - prePos.x);
float degree = atan(tan);
? degreedegree = degree / 3.14159f * 180;
?
? pInnerTarget->setRotation(degree);
? }
?
? void RotateWithAction::setInnerAction(CCActionInterval* pAction)
{
? if (pInnerAction != pAction)
? {
? CC_SAFE_RELEASE(pInnerAction);
? pInnerAction = pAction;
? CC_SAFE_RETAIN(pInnerAction);
? }
? }
CCObject* RotateWithAction::copyWithZone(CCZone* pZone)
? {
? CCZone* pNewZone = NULL;
? RotateWithAction* pCopy = NULL;
? if(pZone && pZone->m_pCopyObject)
{
? pCopy = (RotateWithAction*)(pZone->m_pCopyObject);
? }
? else
? {
? pCopy = new RotateWithAction();
? pZone = pNewZone = new CCZone(pCopy);
? }
CCActionInterval::copyWithZone(pZone);
?
? pCopy->initWithAction(dynamic_cast
? (pInnerAction->copy()->autorelease()));
?
? CC_SAFE_DELETE(pNewZone);
? return pCopy;
? }
也许有的读者已经有了疑问,step方法与update方法都可以做到每一帧判断一次方向,为什么选择重载step方法而不是update方法呢?这是因为引擎在step方法中对动作对象的内部成员进行了更新,更新后才会由此方法调用update方法来更新目标节点。在方向追踪的动作中,我们除了在每一帧判断方向,还必须同步执行被包装的动作。这就需要我们调用被包装动作的step方法,以保证对象能够被完整地更新。
现在,我们已经不需要使用4.6节介绍的CCSpawn来实现蹩脚的方向追踪效果了,只要把需要追踪方向的动作传递给CCRotateAction,即可得到一个自动改变鱼方向的智能动作。
我们的菜单收放效果就很好地印证了这个结论。菜单的全部收放动作效果形成了一个比较长且单一的运行轨迹,所以我们不妨为动作添加一些变速效果,将玩家有限的注意力集中到我们希望玩家关注的效果上。 ?
进场动作:由快到慢,快速进入后缓慢停下,在停止前给玩家足够的视觉时间分辨清楚进入的图像。 ?
出场动作:先慢后快,展示了出场趋势和方向后快速移出屏幕,不拖泥带水。 ?
这个变速效果就很自然地交给前面提到的CCEase系列动作实现了。针对具体的需求,我们选择了CCEaseExponential动作来实现变速效果。
Cocos2d-x动作原理——————————
继承自CCAction的CCFiniteTimeAction主要新增了一个用于保存该动作总的完成时间的成员变量:ccTime m_fDuration。 ? 对于CCFiniteTimeAction的两个子类CCActionInstant和CCActionInterval,前者没有新增任何函数和变量,而后者增加了两个成员变量--ccTime m_elapsed和bool m_bFirstTick,其中m_elapsed是从动作开始起逝去的时间,而m_bFirstTick是一个控制变量,在后面的分析中,我们将看到它的作用。
动作的更新————
1、CCNode调用runAction —— CCActionManager会将新的CCAction和对应的目标节点添加到其管理的动作表中
(startWithTarget(CCNode* pTarget)来绑定
如 CCActionInterval CCFiniteTimeAction::startWithTarget(pTarget);
? m_elapsed = 0.0f;
? m_bFirstTick = true;
)
2、每一帧刷新屏幕时,系统都会在CCActionManager中遍历其动作表中的每一个动作,并调用该动作的step(ccTimedt)方法。step方法主要负责计算m_elapsed的值,并调用update(float time)方法
void CCActionInterval::step(float dt)
? {
? if (m_bFirstTick)
? {
? m_bFirstTick = false;
? m_elapsed = 0;
? }
? else
? {
? m_elapsed += dt;
? }
?
? this->update(MAX (0,MIN(1, m_elapsed / MAX(m_fDuration, FLT_EPSILON))));
? }
传入update方法的time参数表示逝去的时间与动作完成需要的时间的比值,是介于0和1之间的一个数,即动作完成的百分比。 ?
CCActionInterval并没有进一步实现update方法。下面我们继续以继承自CCAction- Interval的CCRotateTo动作的update方法为例,分析update函数是如何实现的,其实现代码如下:
void CCRotateTo::update(float time)
? {
? if (m_pTarget)
? {
? m_pTarget->setRotation(m_fStartAngle + m_fDiffAngle * time);
? }
? }
看到这里,我们已经能看出Cocos2d-x的动作机制的整个工作流程了。在CCRotateTo中,最终完成的操作是修改目标节点的Rotation属性值,更新该目标节点的旋转属性值。
3、最后,在每一帧刷新结束后,在CCActionManager类的update方法中都会检查动作队列中每一个动作的isDone函数是否返回true。如果返回true,则动作已完成,将其从队列中删除。isDone函数的代码如下:
? bool CCActionInterval::isDone(void)
? {
? return m_elapsed >= m_fDuration;
? }
对于不同的动作类,虽然整体流程大致都是先调用step方法,然后按照各个动作的具体定义来更新目标节点的属性,但是不同动作的具体实现会有所不同。例如,CCRepeatForever动作的isDone函数始终返回false,因为它是永远在执行的动作;又如CCActionInstant及其子类的step函数中,向update传递的参数值始终是1,因为瞬时动作会在下一帧刷新后完成,不需要多次执行update。
CCActionManager的工作原理————
下面的代码是CCDirector::init()方法中的一部分:
? //动作管理器
? m_pActionManager = new CCActionManager();
m_pScheduler->scheduleUpdateForTarget(m_pActionManager, kCCPrioritySystem, false);
CCScheduler在每一帧更新时,都会触发CCActionManager注册的update方法。与调度器CCScheduler类似的一点是,为了防止动作调度过程中所遍历的表被修改,Cocos2d-x对动作的删除进行了仔细地处理,保证任何情况下都可以安全地删除动作:
void CCActionManager::update(float dt)
? {
? //枚举动作表中的每一个目标节点
? for (tHashElement *elt = m_pTargets; elt != NULL; )
? {
? m_pCurrentTarget = elt;
? m_bCurrentTargetSalvaged = false;
if (! m_pCurrentTarget->paused)
? {
? //枚举目标节点对应的每一个动作
? //actions数组可能会在循环中被修改,因此需要谨慎处理
? for (m_pCurrentTarget->actionIndex = 0;
? m_pCurrentTarget->actionIndex < m_pCurrentTarget->actions->num;
m_pCurrentTarget->actionIndex++)
? {
? m_pCurrentTarget->currentAction =
? (CCAction*)m_pCurrentTarget->actions
? ->arr[m_pCurrentTarget->actionIndex];
if (m_pCurrentTarget->currentAction == NULL)
? {
? continue;
? }
m_pCurrentTarget->currentActionSalvaged = false;
?
? m_pCurrentTarget->currentAction->step(dt); //触发动作更新
?
? if (m_pCurrentTarget->currentActionSalvaged)
{
? m_pCurrentTarget->currentAction->release();
? }
? else if (m_pCurrentTarget->currentAction->isDone())
{
? m_pCurrentTarget->currentAction->stop();
?
? CCAction *pAction = m_pCurrentTarget->currentAction;
m_pCurrentTarget->currentAction = NULL;
removeAction(pAction);
? }
? m_pCurrentTarget->currentAction = NULL;
? }
? }
elt = (tHashElement*)(elt->hh.next);
?
? if (m_bCurrentTargetSalvaged && m_pCurrentTarget->actions->num == 0)
? {
? deleteHashElement(m_pCurrentTarget);
? }?
}
?
? m_pCurrentTarget = NULL;
? }
动画——————
通常,较为简单的动画可以利用Flash工具制作出来,而更为复杂与细腻的动画可以利用三维建模软件逐帧渲染,或完全手动绘制。
考虑到制作成本以及回放成本,如果没有必要,我们一般不在游戏中大规模使用动画。
动画帧类CCAni- mationFrame同样包含两个属性,其一是对一个框帧的引用,其二是帧的延时。一个Cocos2d-x的动画CCAnimation是对一个动画的描述,它包含显示动画所需要的动画帧。对于匀速播放的帧动画,只需设置所有帧的延时相同即可。 ?
我们使用CCAnimation描述一个动画,而精灵显示动画的动作则是一个CCAnimate对象
动画与动画动作的关系就如同CD光盘与CD播放机的关系一样--前者记录了动画的内容,而后者是播放动画的工具
触摸———————————————————————————————————————————————————————
利用层来实现触摸十分简便,然而只要玩家触摸了屏幕,所有响应触摸事件的层都会被触发。当层的数量很多时,维护多个层的触摸事件就成了一件复杂的事情。因此,在实际开发中,我们通常单独建立一个触摸层。用触摸层来接收用户输入事件,并根据需要通知游戏中的其他部件来响应触摸事件。
两种Cocos2d-x触摸事件(1)——————————
CCTouchDelegate 触摸事件委托,就是系统捕捉到触摸事件后交由它或者它的子类处理,所以我们在处理触屏事件时,必须得继承它。它封装了下面这些处理触屏事件的函数:
子类:
CCStandardTouchDelegate用于处理多点触摸;CCTargetedTouchDelegate用于处理单点触摸。
CCTouchDelegate可以处理多触摸 和 单触摸
CCStandardTouchDelegate
virtual void ccTouchesBegan(CCSet *pTouches, CCEvent *pEvent) //多
CCTargetedTouchDelegate
virtual bool ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent) //单
CCTouchDispatcher——触摸调度器(分发器),
该类用单例模式处理所有的触摸事件(此单例可以通过导演获得)
class CC_DLL CCTouchDispatcher 类内函数:
//熟悉 布尔类型,用于获取或设置事件分发器是否工作
DispatchEvents
?
bool isDispatchEvents(void);//dispatch 调度
void setDispatchEvents(bool bDispatchEvents);
//Adds a standard touch delegate to the dispatcher's list.
//对应地存入m_pStandardHandlers或m_pTargetedHandlers容器中
void addStandardDelegate(CCTouchDelegate *pDelegate, int nPriority);//delegate 协议
void addTargetedDelegate(CCTouchDelegate *pDelegate, int nPriority, bool bSwallowsTouches);//优先级
Swallow 吞 只要前面一层touchbegan 返回true 则截取 否则不截取
优先级数值越低,越先响应
//CCMenu的默认优先级是-128,CCScrollView的默认优先级是0
void removeDelegate(CCTouchDelegate *pDelegate);
void removeAllDelegates(void);
void setPriority(int nPriority, CCTouchDelegate *pDelegate);
virtual void touchesBegan(CCSet* touches, CCEvent* pEvent);
virtual void touchesMoved(CCSet* touches, CCEvent* pEvent);
virtual void touchesEnded(CCSet* touches, CCEvent* pEvent);
virtual void touchesCancelled(CCSet* touches, CCEvent* pEvent);
1、该调度器将事件(通过4个touch函数获得)分发给注册过的TouchHandlers
2、首先,调度器发送接收到的触摸给指定的目标触摸句柄,这些触摸事件可以被该目标触摸句柄截获。如果有剩余的触摸事件,这些触摸事件将被发送给标准触摸句柄
使用方法1 :
1、通过导演得到分发器
2、加入分调度方法 addStandardDelegate addTargetedDelegate 加入触摸协议
层封装了 可以用以下方法2:
1、注册触摸句柄
void registerScriptTouchHandler(int nHandler, bool bIsMultiTouches = false, int nPriority = INT_MIN, bool bSwallowsTouches = false); //id 是否多触电(默认单) 优先级(默认0)是否吞噬(默认否)
2、 void setTouchEnabled(bool value);方法
——其实也没变得简单 可以一直用方法一
两种Cocos2d-x触摸事件(2) ?——————
只要事件分发器接收到用户的触摸事件,就会分发给所有的订阅者
触摸分发器原理————
触摸分发器会利用系统的API获取触摸事件,然后把事件分发给游戏中接收触摸事件的对象。
事件分发器从系统接收到了触摸事件之后还需要逐一分发。分发事件的相关代码主要集中在touches方法之中。
分发过程遵循以下的规则——————
对于触摸集合中的每个触摸点,按照优先级询问每一个注册到分发器的对象。
被吞噬的点将立即移出触摸集合,不再分发给后续目标(包括注册了标准触摸事件的目标)
触摸中的陷阱——————
1、触摸分发器和引擎中的绘图是相互独立的,所以并不关心触摸代理是否处于屏幕上。而CCLayer也仅仅会在切换场景时将自己从分发器中移除,所以同场景内手动切换CCLayer的时候,也需要注意禁用触摸来从分发器移除自己。
2、另一个陷阱出自CCTargetedTouchDelegate。尽管每次只传入一个触摸点,也只有在开始阶段被声明过的触摸点后续才会传入,但是这并不意味着只会接收一个触摸点:只要被声明过的触摸点都会传入,而且可能是乱序的。因此,一个良好的习惯是,如果使用CCTargeted- TouchDelegate,那么只声明一个触摸,针对一个触摸作处理即可。(没懂)
CCMenuItemToggle
可以将任意的
CCMenuItem
封装进去,作为一个按钮式的开关
文本输入框———————————————————————————————————————————————————
Cocos2d-x中的粒子系统(1)———————————————————————————————————————————
暂时抛开粒子效果文件Plist不谈,如果我们已经拥有一个粒子效果文件,就可以利用CCParticleSystem的初始化方法直接从文件中导入一个粒子效果,相关代码如下:
bool initWithFile(const char *plistFile)
static CCParticleSystem* create(const char *plistFile)
Plist文件实质上是一个XML文件,我们可以利用任何文本编辑器来创建或修改。
Cocos2d-x中的粒子系统看似简单,实际上却是一个十分强大的特效工具。使用得当的粒子系统可以实现许多梦幻般的特效(漫天雪花,雨天 用编辑器尝试做出以上两种或者更多效果)。
瓦片地图———————————————————————————————————————————
大型地图——————————————
超过屏幕大小的地图,玩家可以像在即时战略游戏(如《魔兽争霸》)中一样在地图中滚动游戏画面
无论是即时战略、角色扮演,还是模拟经营,通常都需要一张非常大的地图来展现一个灵活多变的世界。
TileMap要求每个瓦片占据地图上一个四边形或六边形的区域。
TileMap地图支持3种不同的视图:正交视图(orthogonal view,瓦片水平垂直排列)、六边形视图(hexagonal view,六边形瓦片紧密连接)和等轴视图(isometric view,45度斜视排列)。
TileMap中的层级关系和Cocos2d-x中的是类似的,地图可以包含多个不同的图层,每个图层内都放置瓦片,同层内的瓦片间平铺排列,而高一层的瓦片可以遮盖低一层的瓦片。与Cocos2d-x不同的是,TileMap的坐标系的原点位于左上角,以一个瓦片为单位,
导入游戏——————————————
Cocos2d-x为我们提供了CCTMXTileMap和CCTMXLayer两个类来处理瓦片地图。其中,CCTMXTileMap代表一个完整的瓦片地图,它负责地图文件的载入、管理以及呈现。与其他游戏元素相同,CCTMXTileMap也继承自CCNode,因此可像层一样把它添加到游戏场景中。CCTMXLayer代表一个瓦片地图中的图层,可以从图层对象获取图层信息,如某一点是否存在对象组或属性。CCTMXLayer隶属于CCTMXTileMap,因此通常不需要我们手动管理图层。
此外,CCTMXTileMap还提供了一些操作地图的图层或地图对象的方法,可以通过关键字获取层、对象组或属性,这也为我们操作地图提供了便利。
鱼在水草间穿梭的层次效果——————————————
我们把靠近屏幕的水草置于glass1图层中,而远离屏幕的水草置于glass0图层中,它们的Z轴顺序值分别设置为3与4。此时,当鱼的Z轴顺序值为2时,它会被所有水草遮挡;为3时,会被部分水草遮挡;为4时,不会被任何水草遮挡。通过改变鱼群的Z轴顺序值,我们实现了鱼在水草中层次感地穿梭的效果,因此我们可以实时根据鱼的位置来设置鱼的Z轴顺序值。
OPENGL——————————————————————————————————————————————————————
状态机————————————
OpenGL是一个基于状态的绘图模型,我们把这种模型称为状态机
在此模型下,OpenGL时刻维护着一组状态,这组状态涵盖了一切绘图参数,如即将绘制的多边形、填充颜色、纹理、混合模式和当前的坐标系等。为了正确地绘制图形,我们需要把OpenGL设置到合适的状态,然后调用绘图指令。
状态机优势:
1、其中许多参数并不频繁改变,因此也没有必要每次都重新设置。OpenGL把所有的参数作为状态来保存,如果没有设置新的参数,则会一直采用当前的状态来绘图
2、另一个优势在于,我们可以把绘图设备人为地分为两个部分:"服务器端",负责具体的绘制渲染;"客户端",负责向服务器端发送绘图指令。(cpu gpu 游戏服务器 游戏客户端)因此我们也需要尽力避免在客户端与服务器端传递不必要的数据。 ?
OpenGL提供了许多改变绘图状态的函数
GLenum类型用来表示OpenGL的状态量。后面我们将会看到,全部状态的列表定义在"gl2.h"头文件中。不同的绘图效果需要不同的支持状态,默认情况下,Cocos2d-x只会开启固定的几种状态,必要的时候必须自己主动开启所需状态,使用完毕后主动禁止
为了裁剪渲染区域,就需要设置GL_SCISSOR_TEST状态
void Spin888::visit()
{
glEnable(GL_SCISSOR_TEST);
glScissor(x,y,w,h);//x, y, w, h 左下0 0点
CCLayer::visit();
glDisable(GL_SCISSOR_TEST);
}
实际上,从"gl2.h"头文件中就可以看出,OpenGL是一个非常接近底层的接口标准,核心部分只包括了约170个函数和约300个常量
可编程着色器——————————
利用可编程着色器,开发者可以在渲染过程中自由控制顶点和片段处理采用的算法,以便实现更加炫丽的渲染效果。可编程着色器主要包含顶点着色器和片段着色器,其中前者负责对顶点进行几何变换以及光照计算,后者负责处理光栅化得到的像素以及纹理。
绘图————————————
void HelloWorld::draw()
? {
? //顶点数据
? static GLfloat vertex[] = { //顶点坐标:x,y,z
? 0.0f, 0.0f, 0.0f, //左下
? 200.0f, 0.0f, 0.0f, //右下
? 0.0f, 200.0f, 0.0f, //左上
? 200.0f, 200.0f, 0.0f, //右上
? };
? static GLfloat coord[] = { //纹理坐标:s,t
? 0.0f, 1.0f,
? 1.0f, 1.0f,
? 0.0f, 0.0f,
? 1.0f, 0.0f,
? };
static GLfloat color[] = { //颜色:红色、蓝色、绿色、不透明度
? 1.0f, 1.0f, 1.0f, 1.0f,
? 1.0f, 1.0f, 1.0f, 1.0f,
? 1.0f, 1.0f, 1.0f, 1.0f,
? 1.0f, 1.0f, 1.0f, 1.0f,
? };
?
? //初始化纹理
? static CCTexture2D* texture2d = NULL;
? if(!texture2d) {
? texture2d = CCTextureCache::sharedTextureCache()->addImage("HelloWorld.png");
? coord[2] = coord[6] = texture2d->getMaxS();
? coord[1] = coord[3] = texture2d->getMaxT();
? }
?
? //设置着色器
? ccGLEnableVertexAttribs(kCCVertexAttribFlag_PosColorTex);
? texture2d->getShaderProgram()->use();
? texture2d->getShaderProgram()->setUniformForModelViewProjectionMatrix();
//绑定纹理
? glBindTexture(GL_TEXTURE_2D, texture2d->getName());
?
? //设置顶点数组
? glVertexAttribPointer(kCCVertexAttrib_Position, 3, GL_FLOAT, GL_FALSE, 0, vertex);
? glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, coord);
? glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_FLOAT, GL_FALSE, 0, color);
?
? //绘图
? glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
draw大致上可以分为3个部分--数据部分、初始化纹理和绘图,它绘制了一个带纹理的矩形。事实上,我们也可以通过绘制一个"三角形带(triangle stripe)"来绘制。
代码的第一部分是数据部分,在这一部分中我们声明了3个静态数组,它们分别是vertex、coord和color,对应了三角形带中共4个顶点的顶点坐标、纹理坐标和顶点颜色。每个数组均按照左下、右下、左上、右上的顺序来存储。
vertex:共4个顶点,每个顶点包含x、y和z三个分量,因此顶点坐标数组共有12个值。在本例中,矩形位于屏幕左下角,大小为200×200。
coord:包含s和t(横坐标和纵坐标)两个分量,因此共有8个值,每个分量的取值范围是0到1,需要根据纹理的属性确定取值。
color:包含r、g、b和a(红色、绿色、蓝色和不透明度)4个分量,因此共有16个值,每个分量的取值范围是0~1。把颜色值设为纯白(1, 1, 1, 1),则会显示纹理原来的颜色。
第二部分是初始化纹理。利用CCTextureCache类可以方便地从文件中载入一个纹理,获取纹理尺寸,以及获取纹理在OpenGL中的编号。在纹理没有被初始化时,我们首先使用CCTextureCache::addImage方法载入一个图片,把返回的CCTexture2D对象保存下来,并使用纹理的属性设置4个顶点的纹理坐标。对于单个纹理的图片,只需要按照上面代码中的方法设置纹理坐标即可
最后一部分是绘制图片。绘制图片的步骤可以简述为:绑定纹理、设置顶点数组和绘图。绑定纹理是指把一个曾经载入的纹理当做当前纹理,从此绘制出来的多边形都使用此纹理。设置顶点数组是指为OpenGL指定第一步准备好的顶点坐标数组、纹理坐标数组以及顶点颜色数组。绘图则是最终通知OpenGL如何利用刚才提供的信息进行绘图,并实际把图形绘制出来。在这个过程中,我们可以看到最重要的一个函数为glDrawArrays(GLenum mode, GLint first, GLsizei count),其中mode指定将要绘制何种图形,first表示前面数组中起始顶点的下标,count表示即将绘制的图形顶点数量。
矩阵与变换————————
在计算机中,坐标变换是通过矩阵乘法实现的,用向量表示坐标,矩阵表示变换形式,则变换后的顶点坐标可以用向量与矩阵的乘法来表示。使用矩阵乘法的优点在于,计算机(包括移动设备)的图形硬件通常对矩阵乘法进行了大量优化,从而大大提高了运算效率。
点、向量与矩阵 ?
在计算机中,通常不直接使用与点维度数量一样的向量来表示一个点,因为这样就无法利用矩阵乘法来对点进行平移等操作了。因此,在计算机图形学中,通常采用齐次坐标来表示一个顶点。具体地说,齐次坐标系中每一个点的维度比顶点维度多1,多出的一个维度值为1。对于任何三维中的顶点(x, y, z),它在齐次坐标系中的向量为[x, y, z, 1],例如,空间中的(1.2, 5, 10)对应的向量为[1.2, 5, 10, 1]。
具体使用 283p
//Cocos2d-x 2.0(OpenGL ES 2.0)
kmGLScalef(0.8f, 0.8f, 0.8f); //乘上缩放矩阵
kmGLTranslatef(1.0f, 2.0f, 3.0f); //乘上平移矩阵
kmGLScalef(2.5f, 2.5f, 2.5f); //乘上缩放矩阵
DrawObject(); //绘制任意图形
Cocos2d-x 2.0中矩阵函数的替代函数
OpenGL ES 1.0函数 替代函数 描述
glPushMatrix kmGLPushMatrix 把矩阵压栈
glPopMatrix kmGLPopMatrix 从矩阵栈中弹出
glMatrixMode kmGLMatrixMode 设置当前矩阵模式
glLoadIdentity kmGLLoadIdentity 把当前矩阵置为单位矩阵
glLoadMatrix kmGLLoadMatrix 设置当前矩阵的值
glMultMatrix kmGLMultMatrix 右乘一个矩阵
glTranslatef kmGLTranslatef 右乘一个平移矩阵
glRotatef kmGLRotatef 右乘一个旋转矩阵
glScalef kmGLScalef 右乘一个缩放矩阵
重写精灵的draw 中画边框部分
//画边框
void MyImage::drawBorder(){
ccDrawColor4B(0,255,0,255);
CCSize s = this->getTextureRect().size;
CCPoint offsetPix = this->getOffsetPosition();
CCPoint vertices[4] = {
ccp(offsetPix.x,offsetPix.y), ccp(offsetPix.x+s.width,offsetPix.y),
ccp(offsetPix.x+s.width,offsetPix.y+s.height), ccp(offsetPix.x,offsetPix.y+s.height)
};
ccDrawPoly(vertices, 4, true);
}
void CCSprite::draw(void)
? {
? //1. 初始准备
?
? CC_PROFILER_START_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw");
? CCAssert(!m_pobBatchNode, "If CCSprite is being rendered by CCSpriteBatchNode,
? CCSprite#draw SHOULD NOT be called");
? CC_NODE_DRAW_SETUP();//CC_NODE_DRAW_SETUP宏函数用于准备绘制相关环境;
?
? //2. 颜色混合函数
?
? ccGLBlendFunc(m_sBlendFunc.src, m_sBlendFunc.dst);
/***************************************************
OpenGL 会把源颜色和目标颜色各自取出,并乘以一个系数(源颜色乘以的系数称为“源因子”,目标颜色乘以的系数称为“目标因子”),然后相加,这样就得到了新的颜 色。glBlendFunc有两个参数,前者表示源因子,后者表示目标因子。
GL_ZERO: 表示使用0.0作为因子,实际上相当于不使用这种颜色参与混合运算。
GL_ONE: 表示使用1.0作为因子,实际上相当于完全的使用了这种颜色参与混合运算。
GL_SRC_ALPHA:表示使用源颜色的alpha值来作为因子。
GL_DST_ALPHA:表示使用目标颜色的alpha值来作为因子。
GL_ONE_MINUS_SRC_ALPHA:表示用1.0减去源颜色的alpha值来作为因子。
GL_ONE_MINUS_DST_ALPHA 表示用1.0减去目标颜色的alpha值来作为因子。
除此以外,还有GL_SRC_COLOR(把源颜色的四个分量分别作为因子的四个分量)、GL_ONE_MINUS_SRC_COLOR、 GL_DST_COLOR、GL_ONE_MINUS_DST_COLOR等,前两个在OpenGL旧版本中只能用于设置目标因子,后两个在OpenGL 旧版本中只能用于设置源因子。新版本的OpenGL则没有这个限制,并且支持新的GL_CONST_COLOR(设定一种常数颜色,将其四个分量分别作为 因子的四个分量)、GL_ONE_MINUS_CONST_COLOR、GL_CONST_ALPHA、 GL_ONE_MINUS_CONST_ALPHA。另外还有GL_SRC_ALPHA_SATURATE
这些宏cocos2d 兼容的
***************************************************/
?
? //3. 绑定纹理
if (m_pobTexture != NULL)
? {
? ccGLBindTexture2D(m_pobTexture->getName());
? }
? else
? {
? ccGLBindTexture2D(0);
? }
?
? //4. 绘图
?
? ccGLEnableVertexAttribs(kCCVertexAttribFlag_PosColorTex);设置使用相应的顶点格式为:位置+颜色+纹理坐标
?
? #define kQuadSize sizeof(m_sQuad.bl)
? long offset = (long)&m_sQuad;
?
? //顶点坐标
? int diff = offsetof(ccV3F_C4B_T2F, vertices);
? glVertexAttribPointer(kCCVertexAttrib_Position, 3, GL_FLOAT, GL_FALSE,
? kQuadSize, (void*) (offset + diff));//设置顶点缓冲中纹理坐标数据的描述
//纹理坐标
? diff = offsetof(ccV3F_C4B_T2F, texCoords);
? glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE,
? kQuadSize, (void*)(offset + diff));//设置顶点缓冲中纹理坐标数据的描述
?
? //顶点颜色
? diff = offsetof(ccV3F_C4B_T2F, colors);
? glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_UNSIGNED_BYTE, GL_TRUE,
? kQuadSize, (void*)(offset + diff));//设置顶点缓冲中颜色数据的描述
?
? //绘制图形
? glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
?
? CHECK_GL_ERROR_DEBUG();
/***************************************************
//绘制边框 可以自定义绘制边框 应用:显示boundingBox
?CCSize s = this->getTextureRect().size;
CCPoint offsetPix = this->getOffsetPosition();
CCPoint vertices[4] =
{
ccp(offsetPix.x,offsetPix.y), ccp(offsetPix.x+s.width,offsetPix.y),
ccp(offsetPix.x+s.width,offsetPix.y+s.height), ccp(offsetPix.x,offsetPix.y+s.height)
};
ccDrawPoly(vertices, 4, true);
***********************************************/
? //5. 调试相关的处理
?
? #if CC_SPRITE_DEBUG_DRAW == 1 //通过修改这个宏值 执行下面相应代码
? //调试模式1:绘制边框
? CCPoint vertices[4]={
ccp(m_sQuad.tl.vertices.x,m_sQuad.tl.vertices.y),
? ccp(m_sQuad.bl.vertices.x,m_sQuad.bl.vertices.y),
? ccp(m_sQuad.br.vertices.x,m_sQuad.br.vertices.y),
? ccp(m_sQuad.tr.vertices.x,m_sQuad.tr.vertices.y),
? };
? ccDrawPoly(vertices, 4, true);
? #elif CC_SPRITE_DEBUG_DRAW == 2
? //调试模式2:绘制纹理边缘
? CCSize s = this->getTextureRect().size;
? CCPoint offsetPix = this->getOffsetPosition();
? CCPoint vertices[4] = {
? ccp(offsetPix.x,offsetPix.y), ccp(offsetPix.x+s.width,offsetPix.y),
? ccp(offsetPix.x+s.width,offsetPix.y+s.height), ccp(offsetPix.x,offsetPix.y+
? s.height)
? };
? ccDrawPoly(vertices, 4, true);
? #endif
? CC_INCREMENT_GL_DRAWS(1);
?
? CC_PROFILER_STOP_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw");
? }
渲染树的绘制(1)————————————————
void CCNode::visit()
{
? //1. 先行处理
? if (!m_bIsVisible)//当此节点被设置为不可见时,则直接返回不进行绘制
? {
? return;
? }
? kmGLPushMatrix(); //矩阵压栈
/***********************************************
保存当前的绘图矩阵
绘图矩阵保存好之后,就可以根据需要对矩阵进行任意的操作了,直到操作结束后再通过"矩阵出栈"来恢复保存的矩阵。由于所有对绘图矩阵的操作都在恢复矩阵之前进行,因此我们的改动不会影响到以后的绘制。
***********************************************/
?
? //处理Grid特效
? if (m_pGrid && m_pGrid->isActive())
? {
? m_pGrid->beforeDraw();
? }
?
? //2. 应用变换
? this->transform();
/***********************************************
transform方法进行一系列变换,以便把自己以及子节点绘制到正确的位置上
draw方法负责把图形绘制出来,但是从上一节的学习可知,draw方法并不关心纹理绘制的位置,实际上它仅把纹理绘制到当前坐标系中的原点
***********************************************/?
? //3. 递归绘图
? CCNode* pNode = NULL;
? unsigned int i = 0;
?
? if(m_pChildren && m_pChildren->count() > 0)
//存在子节点
? sortAllChildren();
? //绘制zOrder < 0的子节点
? ccArray *arrayData = m_pChildren->data;
? for( ; i < arrayData->num; i++ )
? {
? pNode = (CCNode*) arrayData->arr[i];
?
? if ( pNode && pNode->m_nZOrder < 0 )
? {
? pNode->visit();
? }
? else
? {
? break;
? }
? }
? //绘制自身
? this->draw();
?
? //绘制剩余的子节点
? for( ; i < arrayData->num; i++ )
? {
pNode = (CCNode*) arrayData->arr[i];
? if (pNode)
? {
? pNode->visit();
? }
? }
? }
? else
? {
? //没有子节点:直接绘制自身
? this->draw();
? }
?
? //4. 恢复工作
? m_nOrderOfArrival = 0;
?
? if (m_pGrid && m_pGrid->isActive())
? {
? m_pGrid->afterDraw(this);
? }
?
? kmGLPopMatrix(); //矩阵出栈
}
绘图瓶颈———————————————————————————————————————————
影响游戏性能的瓶颈——————
1、纹理过小:
OpenGL在显存中保存的纹理的长宽像素数一定是2的幂,对于大小不足的纹理,则在其余部分填充空白,这无疑是对显存极大的浪费;
同一个纹理可以容纳多个精灵,把内容相近的精灵拼合到一起是一个很好的选择。(小图拼大图)
2、纹理切换次数过多当我们连续使用两个不同的纹理绘图时,GPU不得不进行一次纹理切换,这是开销很大的操作,然而当我们不断地使用同一个纹理进行绘图时,GPU工作在同一个状态,额外开销就小了很多,因此,如果我们需要批量绘制一些内容相近的精灵,就可以考虑利用这个特点来减少纹理切换的次数。
3、纹理过大:显存是有限的,如果在游戏中不加节制地使用很大的纹理,则必然会导致显存紧张,因此要尽可能减少纹理的尺寸以及色深。
优化方式——————————————————————————————————
1、碎图压缩与精灵框帧:——TexturePacker
到目前为止,我们都是使用各自的纹理来创建精灵,由此导致的纹理过小和纹理切换次数过多是产生瓶颈的根源。针对这个问题,一个简单的解决方案是碎图合并与精灵框帧。碎图合并可以将许多零碎的小图片合并到一张大图里,并且这张大图的大小恰好符合OpenGL的纹理规范,从空间上减少无谓的浪费。框帧是纹理中的一部分,当我们把小纹理合并好之后就可以利用精灵框帧来创建精灵了。
2、批量渲染:————————
有了足够大的纹理图后,就可以考虑从渲染次数上进一步优化了。如果不需要切换绑定纹理,那么几个OpenGL的渲染请求是可以批量提交的,也就是说,在同一纹理下的绘制都可以一次提交完成。在Cocos2d-x中,我们提供了CCSpriteBatchNode来实现这一优化。
CCSpriteBatchNode可以一次批量提交所有子节点的绘图请求,以减少提交次数,提高绘图性能。这个优化要求每个子节点都使用同一张纹理,例如我们可以把所有的鱼的图片放在一个纹理之中,每个精灵显示出自己存在于纹理中的那一部分。CCSpriteBatchNode的使用方法也很简单,它是一个特殊的节点,我们只要把需要绘制的精灵添加为它的子节点,然后再把CCSpriteBatchNode添加到层或场景之中即可。当然,这些精灵必须使用统一个纹理。可以认为CCSpriteBatchNode所扮演的角色是精灵与绘图层间的一个中间层,只需要把需要绘制的精灵加入为它的子节点,就可以提高绘制效率。
自定义绘图————————
void CCNode::draw()
? {
? //CCAssert(0);
? //可以重载此方法
? //最好仅在这个方法中绘制自定义的内容
? }
Cocos2d-x提供了一些简单的快捷绘图接口实现最简单的功能,我们可以使用这些接口,并从中找到很好的OpenGL编程规范。这些接口由"CCDrawingPrimitives.h"和对应的cpp文件提供,包括了点、线、多边形、圆形和贝塞尔曲线等最基本的几何图形的绘制,还包括了一些基本的设置,如设置点的大小、绘制的颜色等。
我们不妨给炮台加上瞄准线功能。考虑到针对炮台的功能已经足够多、足够复杂了,我们将炮台抽象为一个类,集中封装相关操作。根据引擎的接口规范,应该在draw函数中绘制我们的自定义效果
——————不要滥用类 只有足够复杂时在做类
void CannonSprite::draw()
? {
? CCSprite::draw(); //调用CCSprite的绘图,保证纹理被正确绘制
CCPoint origin = CCPointZero;
? CCPoint direction = ccp(0, 1);
? direction = ccpMult(direction, 1024);
? CCPoint target = ccpAdd(origin, direction)
?
? ccDrawColor4B(255, 225, 255, 255); //设置绘图颜色为白色
? ccDrawLine(origin, target); //绘制线段
? }
炮台旋转时引起的整个坐标系的变换能保证我们的瞄准线也随着炮台一起旋转相应的角度
数据交流——————
p325 - p332 截图实现 —— 小地图
可编程着色器—————————————————————————————————————————
在渲染流水线上,存在着两个对开发者可见的可编程着色器,具体如下所示。
1、顶点着色器(vertex shader)。对每个顶点调用一次,完成顶点变换(投影变换和视图模型变换)、法线变换与规格化、纹理坐标生成、纹理坐标变换、光照、颜色材质应用等操作,并最终确定渲染区域。在Cocos2d-x的世界中,精灵和层等都是矩形,它们的一次渲染会调用4次顶点着色器。
2、段着色器(fragment shader,又称片段着色器)。这个着色器会在每个像素被渲染的时候调用,也就是说,如果我们在屏幕上显示一张320×480的图片,那么像素着色器就会被调用153 600次。所幸,在显卡中通常存在不止一个图形处理单元,渲染的过程是并行化的,其渲染效率会比用串行的CPU执行高得多。
这两个着色器不能单独使用,必须成对出现,这是因为顶点着色器会首先确定每一个显示到屏幕上的顶点的属性,然后这些顶点组成的区域被化分成一系列像素,这些像素的每一个都会调用一次段着色器,最后这些经过处理的像素显示在屏幕上,二者是协同工作的。
我们可以找到足够多的开源的着色器,能够提供各种丰富的效果。
如何在Cocos2d-x游戏中导入自定义的着色器效果————————
p334 - p353
着色器 可以实现水纹等效果
CCGrid3D 可以代替着色器实现水纹
与自定义着色器相比,CCActionGrid3D局限于表现一些使画面变形的效果,其本质是将目标节点所在区域划分为网格,对每一个小网格进行坐标变换从而形成画面的特殊扭曲。
正因为此,它无法改变 光照 与 颜色 的渲染方式。
————————————————————
小结:
纹理图片:CCImage与CCTexture分别代表一张图片和一个可载入到显存的纹理。CCTexture可由CCImage创建,而CCImage可以载入或保存PNG、TIFF、JPG等格式的文件。
着色器:着色器是用于代替渲染流水线的一段程序,可以用来实现对顶点和像素的操作。在这一章中,我们使用着色器实现了水纹效果。 ?
CCGrid3D:Cocos2d-x提供一套网格变换的功能,通过CCActionGrid3D动作类可以实现一些简单画面的3D变换,例如水纹效果。 ?
物理引擎—————————————————————————————————————————————————————
p358 - 385
数据处理 存储 xml————————————————————————————————————————————————
下面将由浅入深地介绍几种数据持久化的方法:
CCUserDefault————
CCUserDefault是Cocos2d-x引擎提供的持久化方案,其作用是存储所有游戏通用的用户配置信息,例如音乐和音效配置等。为了方便起见,有时我们也可以用CCUserDefault来存储金币数目这种简单的数据项。 ? CCUserDefault可以看做一个永久存储的字典,本质是一个XML文件,将每个键及其对应的值以节点的形式存储到外存中。值只支持int和float等基本类型。使用接口非常简单,只需要一行代码:
CCUserDefault::sharedUserDefault()->setIntegerForKey("coin", coin - 1);
由于每次设置和读取都会遍历整棵XML树,效率不高,且值类型具有局限性,因此CCUserDefault只适合小规模使用,对于复杂的持久化场景就会显得很无力。
格式化存储————
对于稍微复杂的持久化情景,还是可以借助CCUserDefault来满足我们的需求的。由于CCUserDefault是允许存储字符串值的,所以只要将需要保存的数据类型先转化为字符串,就可以写入外存中。
我们先将用户记录封装为类,由用户ID标识,在一个ID下存放金币、经验值和音乐3个值,这样游戏中就允许存在多个用户的记录了。我们创建了一个UserRecord类来读写用户记录,其定义如下:
class UserRecord : public CCObject
? {
? CC_SYNTHESIZE_PASS_BY_REF(string, m_userID, UserID);
? CC_SYNTHESIZE_PASS_BY_REF(int, m_coin, Coin);
? CC_SYNTHESIZE_PASS_BY_REF(int, m_exp, Exp);
? CC_SYNTHESIZE_PASS_BY_REF(bool, m_isMusicOn, IsMusicOn);
?
? public:
UserRecord(const string& userID);
? void saveToCCUserDefault();
? void readFromCCUserDefault();
? };
我们把需要存档的数据通过sprintf函数格式化成一个字符串,并把字符串保存到CCUserDefault之中。注意,这里我们做了一点小小的处理,存储的关键字除了用户ID之外,还添加了一个前缀"UserRecord",这样可以保证,即使在存储时其他类型对象用了同样的用户ID,也可以被区分开。具体代码如下:
void UserRecord::saveToCCUserDefault()
? {
? char buff[100];
? sprintf(buff, "%d %d %d",
? this->getCoin(),
? this->getExp(),
? this->getIsMusicOn() ? 1 : 0
? );
const char* key = ("UserRecord." + this->getUserID()).c_str();
? CCUserDefault::sharedUserDefault()->setStringForKey(key, buff);
? }
有了写入存档的功能,我们还需要一个逆向的从存档读取的过程。读取过程与此过程刚好相反。我们从CCUserDefault来获取保存的字符串,再使用sscanf函数来得到每个数据的值,相关代码如下:
void UserRecord::readFromCCUserDefault()
? {
? string buff = CCUserDefault::sharedUserDef;
? ault()->getStringForKey(("UserRecord." + this->getUserID()).c_str());
?
? int coin = 0;
? int experience = 0;
? int music = 0;
?
? sscanf(buff.c_str(), "%d %d %d", &coin, &experience, &music);
? this->setCoin(coin);
? this->setExp(experience);
? this->setIsMusicOn(music!=0);
这一写一读的过程可以称为序列化与反序列化,是立体的内存数据与一维的字符串间的相互转换。实际上,我们只完成了从数据到CCUserDefault的标准化存储间的转换,从标准化存储到实际存储在文件中的字符串间的转换是交由引擎封装完成的。
本地文件存储————
现在我们已经可以将复杂的数据类型存储到配置文件中了,但把全部数据集中在一个文件中显然不是一个明智的做法。如果将不同类别的数据(例如,NPC的状态和玩家完成的成就)存储到不同的文件中,既可以提高效率,也方便我们查找。下面我们来看看如何实现它。
不同平台间的文件系统不尽相同,为了简化操作、方便开发,Cocos2d-x引擎为我们提供了CCFileUtil类,用于实现获取路径和读取内容等功能,其中两个最重要的接口如下
static unsigned char* getFileData(const char* pszFileName,
const char* pszMode, unsigned long * pSize);//装载文件内容
static std::string getWriteablePath(); //获得可读写路径
借助这两个接口,我们可以获得一个路径,然后对文件进行相应的读写。文件读写在实际开发中应用得比较直接,一般是批量集中写入和读出,在此不再赘述。对于稍微灵活的场景,尤其是需要在大量数据中随机读写一小部分的时候,直接的文件存储由于缺少寻址支持,会变得非常麻烦。我们可以借助XML和SQL这两种方式,来更好地解决这个问题。
XML与JSON————————————————————————————————
XML与JSON(1)
XML和JSON都是当下流行的数据存储格式,它们的共同特点就是数据明文,十分易于阅读。XML源自于SGML,是一种标记性数据描述语言,而JSON则是一种轻量级数据交换格式,比XML更为简洁。鉴于C++对XML的支持更为完善,Cocos2d-x选择了XML作为主要的文件存储格式。
下面我们看看用户记录是如何存储到XML文件中的,相关代码如下所示:
?
与直接的无格式存储相比,这样的文件虽然会耗费稍大的空间,但可读性更强,程序解析起来也更方便一些。
XML文档的语法非常简洁。文档由节点组成,节点的定义是递归的,节点内可以是一个字符串,也可以是由一组
随Cocos2d-x一起分发的还有一个处理XML的开源库LibXML2,它用纯C语言的接口封装了对XML的创建、寻址、读和写等操作,极大地方便了开发。这里我们可以仿照CCUserDefault的做法,将对象存储到指定的XML文件中。
和XML语言的规范相对应,LibXML2库同样十分简洁,只有两个核心的概念,如表13-1所示。
LibXML2
核心类类名 含义 涵盖功能
xmlDocPtr 指向XML文档的指针 XML文档的创建、保存、文档基本信息存取、根节点存取等 xmlNodePrt 指向XML文档中一个节点的指针 节点内容存取、子节点的增、删、改等 ?
下面我们开始以外部XML文件的方式存储UserRecord对象,并从中看到XML文档的操作和LibXML的具体用法。 ?
在UserRecord类中,我们添加如下两个接口,分别负责将对象从XML文件中读出和写入:
void saveToXMLFile(const char* filename="default.xml");
void readFromXMLFile(const char* filename="default.xml");
在开始之前,我们可以进一步抽象出两个函数,完成对象和字符串间的序列化和反序列化,以便在XML的读写接口和CCUserDefault的读写接口间共享,相关代码如下:
void UserRecord::readFromString(const string& str)
? {
? int coin = 0;
? int experience = 0;
? int music = 0;
?
? sscanf(str.c_str(), "%d %d %d", &coin, &experience, &music);
? this->setCoin(coin);
? this->setExp(experience);
? this->setIsMusicOn(music != 0);
void UserRecord::writeToString(string& str)
? {
? char buff[100] = "";
? sprintf(buff,"%d %d %d",
? this->getCoin(),
this->getExp(),
? this->getIsMusicOn() ? 1 : 0
? );
? str = buff;
XML与JSON(2)————
完成了序列化与反序列化的功能后,通过CCUserDefault读写UserRecord的实现就十分简洁了。下面是相关的代码:
void UserRecord::readFromCCUserDefault()
? {
? string key("UserRecord.");
? key += this->getUserID();
?
? string buff = CCUserDefault::sharedUserDefault()->getStringForKey(key.c_str());
? this->readFromString(buff);
xmlFreeDoc(node->doc);
? }
? void UserRecord::saveToCCUserDefault()
? {
string buff;
? this->writeToString(buff);
?
? string key("UserRecord.");
? key += this->getUserID();
?
? CCUserDefault::sharedUserDefault()->setStringForKey(key.c_str(),buff);
? xmlFreeDoc(node->doc);
? }
有了对字符的序列化和反序列化,实际上我们只需要关心如何正确地在XML文档中读写键值对。我们暂且将对象都写到文档的根节点下,不考虑存储数组等复合数据结构的情景,尽管这些情景在操作上是类似的。首先,我们在一个指定的文档的根节点下找到一个键值,如果根节点下不存在指定的键值,将根据参数指定来创建,相关代码如下:
?
xmlNodePtr getXMLNodeForKey(const char* pKey, const char* filename,
? bool creatIfNotExists = true)
? {
? xmlNodePtr curNode = NULL,rootNode = NULL;
? if (! pKey) {
? return NULL;
}
? do {
? //得到根节点
?
? xmlDocPtr doc = getXMLDocument(filename);
? rootNode = xmlDocGetRootElement(doc);
? if (NULL == rootNode) {
? CCLOG("read root node error");
? break;
? }
? //在根节点下找到目标节点
? curNode = (rootNode)->xmlChildrenNode;
? while (NULL != curNode) {
? if (!xmlStrcmp(curNode->name, BAD_CAST pKey)){
? break;
? }
? curNodecurNode = curNode->next;
? }
? //如果没找到且需要创建,则创建该节点
? if(NULL == curNode && creatIfNotExists) {
? curNode = xmlNewNode(NULL, BAD_CAST pKey);
xmlAddChild(rootNode, curNode);
?
? }
? } while (0);
?
? return curNode;
? }
在上述代码中,我们首先根据文件名获得了对应的XML文档指针,然后通过xmlDocGet- ?
RootElement函数获得了该文档的根节点rootNode。一个节点的子节点是以链表形式存储的,通过xmlChildrenNode获得第一个子节点指针,再通过next函数迭代整个子节点列表。如果没有找到指定节点,且函数参数指定了必须创建对应键值的子节点,则函数会根据给定的键值key创建并添加到根节点中。 ?
接下来,则是根据文件名获得XML文档指针的方法,相关代码如下:
bool createXMLFile(const char* filename, const char* rootNodeName = "root")
? {
? bool bRet = false;
? xmlDocPtr doc = NULL;
? do {
? //创建XML文档
doc = xmlNewDoc(BAD_CAST"1.0");
? if (doc == NULL) {
? CCLOG("can not create xml doc");
? break;
? }
?
? //创建根节点
? xmlNodePtr rootNode = xmlNewNode(NULL, BAD_CAST rootNodeName);
? if (rootNode == NULL) {
? CCLOG("can not create root node");
? break;
? }
?
? xmlDocSetRootElement(doc, rootNode);
?
? //保存文档
? xmlSaveFile(filename, doc);
? bRet = true;
? } while (0);
?
? //释放文档
? if (doc) {
xmlFreeDoc(doc);
? }
?
? return bRet;
? }
? xmlDocPtr getXMLDocument(const char* filename)
? {
? if(!isFileExists(filename) && !createXMLFile(filename)) {
? return NULL;
? }
? return xmlReadFile(filename, "utf-8", XML_PARSE_RECOVER);
? }
? bool isFileExists(const char *filename)
? {
? FILE *fp = fopen(filename, "r");
? bool bRet = false;
? if (fp) {
? bRet = true;
? fclose(fp);
? }
? return bRet;
}
这3段代码分别做了3件事情:创建一个具有特定根节点的XML文档,获取一个特定文件名的XML文件,测试文件是否存在。 ?
集成以上的代码,我们再次保存UserRecord对象,可以成功地将其存入一个指定的XML文档中,相关代码如下:
?
?
?
加密与解密————
p399 - 411
网络传输架构———————————————————————————————————————————————————
p412-430 (第一轮不看网络)
移动设备昂贵的CPU与内存————————————————————————————————————————————
\*************************************************************************
许多人认为,“缓存”是内存的一部分 许多技术文章都是这样教授的
但是还是有很多人不知道缓存在什么地方,缓存是做什么用的
其实,缓存是CPU的一部分,它存在于CPU中 CPU存取数据的速度非常的快,一秒钟能够存取、处理十亿条指令和数据(术语:CPU主频1G),而内存就慢很多,快的内存能够达到几十兆就不错了,可见两者的速度差异是多么的大
缓存是为了解决CPU速度和内存速度的速度差异问题
内存中被CPU访问最频繁的数据和指令被复制入CPU中的缓存,这样CPU就可以不经常到象“蜗牛”一样慢的内存中去取数据了,CPU只要到缓存中去取就行了,而缓存的速度要比内存快很多
这里要特别指出的是:
1.因为缓存只是内存中少部分数据的复制品,所以CPU到缓存中寻找数据时,也会出现找不到的情况(因为这些数据没有从内存复制到缓存中去),这时CPU还是会到内存中去找数据,这样系统的速度就慢下来了,不过CPU会把这些数据复制到缓存中去,以便下一次不要再到内存中去取。
2.因为随着时间的变化,被访问得最频繁的数据不是一成不变的,也就是说,刚才还不频繁的数据,此时已经需要被频繁的访问,刚才还是最频繁的数据,现在又不频繁了,所以说缓存中的数据要经常按照一定的算法来更换,这样才能保证缓存中的数据是被访问最频繁的
3.关于一级缓存和二级缓存 为了分清这两个概念,我们先了解一下RAM ram和ROM相对的,RAM是掉电以后,其中才信息就消失那一种,ROM在掉电以后信息也不会消失那一种 RAM又分两种, 一种是静态RAM,SRAM;一种是动态RAM,DRAM。前者的存储速度要比后者快得多,我们现在使用的内存一般都是动态RAM。
有的菜鸟就说了,为了增加系统的速度,把缓存扩大不就行了吗,扩大的越大,缓存的数据越多,系统不就越快了吗 缓存通常都是静态RAM,速度是非常的快, 但是静态RAM集成度低(存储相同的数据,静态RAM的体积是动态RAM的6倍), 价格高(同容量的静态RAM是动态RAM的四倍), 由此可见,扩大静态RAM作为缓存是一个非常愚蠢的行为, 但是为了提高系统的性能和速度,我们必须要扩大缓存, 这样就有了一个折中的方法,不扩大原来的静态RAM缓存,而是增加一些高速动态RAM做为缓存, 这些高速动态RAM速度要比常规动态RAM快,但比原来的静态RAM缓存慢,
我们把原来的静态ram缓存叫一级缓存,而把后来增加的动态RAM叫二级缓存。 一级缓存和二级缓存中的内容都是内存中访问频率高的数据的复制品(映射),它们的存在都是为了减少高速CPU对慢速内存的访问。 通常CPU找数据或指令的顺序是:先到一级缓存中找,找不到再到二级缓存中找,如果还找不到就只有到内存中找了
(千万不能把缓存理解成一个东西,它是一种处理方式的统称! 缓存只是一种策略)
*******************************************************************/
CCTextureCache—————————————
Cocos2d-x中的缓存 ?
幸运的是,我们不需要自己实现缓存,因为Cocos2d-x已经为我们提供了足够强大的实现。引擎中存在3个缓存类,都是全局单例模式。
1、CCTextureCache
其原理是对加入缓存的纹理资源进行一次引用,使其引用计数加一,保持不被清除,而Cocos2d-x的渲染机制是可以重复使用同一份纹理在不同的场合进行绘制,从而达到重复使用,降低内存和GPU运算资源的开销的目的
接口:
static CCTextureCache* sharedTextureCache(); //返回纹理缓存的全局单例
CCTexture2D* addImage(const char* fileimage); //添加一张纹理图片到缓存中
void removeUnusedTextures(); //清除不使用的纹理 ————释放当前所有引用计数为1的纹理,即目前没有被使用的纹理
引擎会在设备出现内存警告时自动清理缓存,但是这显然在很多情况下已经为时过晚了。一般情况下,我们应该在切换场景时清理缓存中的无用纹理,因为不同场景间使用的纹理是不同的。
如果确实存在着共享的纹理,将其加入一个标记数组来保持其引用计数,以避免被清理了
2、CCSpriteFrameCache
第二个则是精灵框帧缓存。顾名思义,这里缓存的是精灵框帧CCSpriteFrame,它主要服务于多张碎图合并出来的纹理图片
static CCSpriteFrameCache* sharedSpriteFrameCache(void); //全局共享的缓存单例
void addSpriteFramesWithFile(const char *pszPlist); //通过plist配置文件添加一组精灵帧
void removeUnusedSpriteFrames(void); //清理无用缓存
3、CCAnimationCache
对于一个精灵动画,每次创建时都需要加载精灵帧,按顺序添加到数组,再创建对应动作类,这是一个非常烦琐的计算过程。对于使用频率高的动画,比如鱼的游动,将其加入缓存可以有效降低每次创建的巨大消耗
static CCAnimationCache* sharedAnimationCache(void);//全局共享的缓存单例
void addAnimation(CCAnimation *animation, const char * name);//添加一个动画到缓存
void removeAnimationByName(const char* name);//移除一个指定的动画
CCAnimation* animationByName(const char* name);//获得事先存入的动画
实际上,如果考虑到两个场景间使用的动画基本不会重复,可以直接清理整个动画缓存。
所以,在场景切换时我们应该加入如下的清理缓存操作:
? void releaseCaches()
? {
? CCAnimationCache::purgeSharedAnimationCache();
? CCSpriteFrameCache::sharedSpriteFrameCache()->removeUnusedSpriteFrames();
? CCTextureCache::sharedTextureCache()->removeUnusedTextures();?
}
值得注意的是清理的顺序,应该先清理动画缓存,然后清理精灵帧,最后是纹理。按照引用层级由高到低,以保证释放引用有效。
对象池机制:可回收与重复使用————
另一个能有效提高内存和计算效率的是对象池机制。其本质与缓存类似,即希望能减少那些频繁使用的对象的重复创建和销毁
使用对象池机制能带来两方面的收益,首先是减少对象初始化阶段的重复计算,其次是避免反复地向操作系统申请归还内存。一个很好的例子就是捕鱼游戏中的鱼,鱼和鱼之间的属性是类似的,不一样的仅仅是当前的坐标位置及正在播放的动画帧。那么,当鱼游出屏幕后,可以不对其进行销毁,而是暂存起来。某一时刻需要重新创建鱼时,我们可以将其从对象池中取出,重新申请内存并初始化,这样就大大减轻了CPU的负担。
对象池和缓存很像,但比缓存更抽象,也更简单一些,因为我们不需要考虑从哪里加载的问题:都已经被抽象为初始化函数了。而且更简化的是,加入对象池的每一个对象都是无差别的,我们不需要对每一个对象进行特定的标记,直接取出任意一个未使用的对象即可。
看完上面的描述,读者应该有了初步的认识:缓存是一个字典,而对象池则是一个数组。得益于引用计数的内存管理机制,只需要在数组上做适当封装就可以提供一个对象池的功能了。尽管如此,一个高效实现的对象池还要考虑如何有效地处理对象的生成和归还,以及占用内存的动态增长等问题。因此,我们不妨借助前人成果,在已有对象池的基础上搭建适合我们游戏使用的对象池。
对象池实现(1)
Boost是一个可移植、免费开源的C++库,提供了大量实用的开发组件,而且由于对跨平台和C++标准的强调,其实现的功能几乎不依赖于操作系统和标准库外的其他组件,因此可以在任何支持C++的平台上运作良好。
oost提供了一个对象池object_pool,它位于boost库的"boost/pool/object/_pool.hpp"中。这是一个泛型的对象池,能够针对指定类型的对象进行分配。一个对象池的声明和使用规范为如下结构:
object_pool
CCSprite* sp =spritePool.construct(); //从对象池得到一个对象,并调用默认构造函数
spritePool.destroy(sp); //对从对象池得到的对象调用析构函数,并返还到对象池中备用
object_pool的一大特色是可以针对不同的参数调用被分配对象的构造函数。可惜在Cocos2d-x对象生命周期管理中,对象的创建和初始化是分离的,大部分类的初始化都不在构造函数中完成,构造函数中仅仅作引用计数的初始化。这里也引入了一个新的问题,Cocos2d-x对象在引用计数为零的时候会自动触发delete。对于从对象池分配的对象来说,不能通过delete而必须通过destroy来删除。因此,在不修改引擎源码的前提下,我们需要在object_pool的基础上作一点小小的包装使其可以配合引擎的内存管理使用,相关代码如下:
template
class MTPoolFromBoost : public ObjectPoolProtocol
{
? object_pool
? CCArray* objects;
? MTPoolFromBoost() : pool(256){
? objects = CCArray::create();
? objects->retain();
? }
? public:
? ~MTPoolFromBoost()
? {
? objects->removeAllObjects();
? objects->release();
? }
? static MTPoolFromBoost
? {
? static MTPoolFromBoost
? return &__sharedPool;
? }
? T* getObject()
? {
? T* pObj = pool.construct();
? objects->addObject(pObj);
? pObj->release();
return pObj;
? }
? void freeObjects(int maxScan = 100)
? {
? static int lastIndex =0;
?
? int count = objects->count();
? if(lastIndex >= count)
? lastIndex = 0;
? if(maxScan > count) maxScan = count;
?
? CCArray* toRemove = CCArray::create();
? for(int i = 0; i < maxScan; i++) {
? CCObject* obj = objects->objectAtIndex((i + lastIndex) % count);
? if(obj->retainCount() == 1) {
? toRemove->addObject(obj);
? }
? }
?
? objects->removeObjectsInArray(toRemove);
? for(int i=0; i < toRemove->count(); i++) {
T* obj = dynamic_cast
? obj->retain();
? toRemove->removeLastObject();
? pool.destroy(obj);
? }
? CCLOG("%s ends. Obj now = %d", __FUNCTION__, objects->count());
? }
? };
由于做成了模板类的形式,类的实现就全部存在于头文件中了。在这个包装类中,我们仅仅做了一件事情--在分配对象的时候,同时将对象添加到一个数组中,数组会增加对象的一次引用计数,因此可以保证在正常使用的情况下,不会有对象会被触发delete操作。由此引出的便是,需要在合适的时候回收对象,否则对象池将持续增长直到耗尽内存。
在提供的内存释放函数freeObjects中,我们检查当前缓冲数组中每个元素的引用计数,对于引用计数为1的对象,表示已经没有其他对象在引用这个对象,将其回收归还到对象池中。值得注意的是,在释放对象的循环中,我们将一个待回收的对象retain后并没有release,这是对引用计数内存管理的一个小小破例,保证了该对象在从数组清理之后仍然不会触发delete操作
另外,这里设计了一个回收的扫描步长,每次回收仅在数组中扫描一定数量的对象就返回。这样做的好处在于,我们可以将整个对象池的回收扫描分散到每一帧中,隐性地完成并发。这个步长可以根据工程规模和所需的清理频率进行调整,对于游戏中对象生成和销毁并不频繁的情况,可以设置一个较长的清理周期,在每次清理时设置一个较大的扫描步长以回收更多的对象,同时减轻计算压力。
模板化之后,实际上每个类对应了一个对象池,以硬编码的形式清理这些对象池是十分费劲的,因此我们再在此基础上扩展一个管理器,管理这些对象池的清理。
对象池实现(2)
首先,需要做的是将回收操作分离抽象。我们定义一个接口并让MTPoolFromBoost继承,这样就能够在运行时用统一接口调用内存池回收对象:
class ObjectPoolProtocol : public CCObject{
? public:
? virtual void freeObjects(int maxScan = 100) = 0;
? };
这样抽象的目的是将所有用到的对象池添加到数组内,以便统一管理。首先,为管理器封装一个获取对象指针的函数:
? class MTPoolManager : public CCObject{
CCArray* pools;
? class PoolCounter {
? public:
? PoolCounter(ObjectPoolProtocol* pool,CCArray* pools)
? {
? pools->addObject(pool);
? }
? };
?
? MTPoolManager()
? {
? pools = CCArray::create();
? pools->retain();
? }
? ~MTPoolManager()
? {
? pools->release();
? }
?
? public:
? static MTPoolManager* sharedManager()
{
static MTPoolManager __sharedManager;
?
? return &__sharedManager;
? }
? void freeObjects(ccTime dt)
? {
? for(int i = 0; i < pools->count(); i++) {
? ObjectPoolProtocol* pool = dynamic_cast
? objectAtIndex(i));
? pool->freeObjects();
? }
? }
?
? template
? T* getObject(T*& pObj)
? {
? static PoolCounter ___poolCounter(MTPoolFromBoost
? return pObj = MTPoolFromBoost
? }
在管理器中我们设计了一个获取对象的接口函数getObject,可以根据传入的指针类型调用相应类型的对象池获得对象。这里我们设置一个静态变量,使用这个变量的构造函数添加当前对象池到类的对象池数组中。由于这个函数是模板化的,最终将把每种调用到的对象池添加到管理器的对象池数组中。这样设计的另一个好处是,管理器调用某一类型的对象池之前,不会在管理器的清理函数中触发该对象池的清理。
而在管理器的清理函数中,可以获取每一个曾经使用过的管理器,调用其清理接口清理对象。 最后,我们只需要在程序初始化完毕后添加该管理器到引擎的定时触发器中:
CCDirector::sharedDirector()->getScheduler()->scheduleSelector(
? schedule_selector(MTPoolManager::freeObjects),
? MTPoolManager::sharedManager(),
? 1,
? false
? );
落实到工厂方法
经过上面的封装,显式地使用对象池已经很方便了,但我们还可以做得更好。借助于工厂方法,我们可以将对象池隐藏起来,透明化其使用:
FishSprite* FishSprite::spriteWithSpriteFrameName(const char* file)
? {
? FishSprite *pRet = MTPoolManager::sharedManager()->getObject(pRet);
? if(pRet && !pRet->initWithSpriteFrameName(file)) {
? CC_SAFE_RELEASE_NULL(pRet);
?
? }
? return pRet;
? } ?
这样做的好处是,游戏中的逻辑代码是没有改动的,直接无缝引入了对象池增强内存管理。
可以看到,使用对象池后,分配对象的耗时降低到了单纯使用new的1/10左右,这个提升是非常可观的。
使用时机———————————
最后需要强调的是,缓存和对象池都不是万金油,我们需要把握好它们的使用时机。缓存和对象池的使用动机都是为频繁使用的资源作优化(这里的资源可以是外存的纹理,也可以是一个对象),避免大量的重复计算。缓存和对象池内都做了一些额外的小计算量的标记来满足这一需求。对于游戏中那些使用频率并不高的部分,加入缓存或者对象池反而很可能因为额外的标记计算而降低性能。这也是引擎只为我们提供了3个缓存器的原因
值得一提的是,缓存和对象池不仅仅适用于C++这类偏底层的开发语言,在C#和JavaScript等语言中,内存的开销更大,使用好缓存和对象池能有效减少不必要的系统内存管理,提升游戏执行效率。
单线程的尴尬——————————————————
并发编程
并发编程是利用线程实现的一系列技术,广泛用于执行耗时的任务。利用多线程技术,可以使游戏显示载入页面的同时在后台加载数据,也可以使游戏在运行的同时在后台进行下载任务等。并发编程的优势巨大,使用起来也并不困难,在这一章中,我们会详细介绍并发编程的方法。
单线程的尴尬
重新回顾下Cocos2d-x的并行机制。引擎内部实现了一个庞大的主循环,在每帧之间更新各个精灵的状态、执行动作、调用定时函数等,这些操作之间可以保证严格独立,互不干扰。不得不说,这是一个非常巧妙的机制,它用一个线程就实现了并发,尤其是将连续的动作变化切割为离散的状态更新时,利用帧间间隔刷新这些状态即实现了多个动作的模拟。
但这在本质上毕竟是一个串行的过程
本来这个问题是难以避免的,但是随着移动设备硬件性能的提高,双核甚至四核的机器已经越来越普遍了,如果再不通过多线程挖掘硬件潜力就过于浪费了。
pthread——————————————————————
pthread是一套POSIX标准线程库,可以运行在各个平台上,包括Android、iOS和Windows,也是Cocos2d-x官方推荐的多线程库。它使用C语言开发,提供非常友好也足够简洁的开发接口。
一个线程的创建通常是这样的:
void* justAnotherTest(void *arg)
? {
? LOG_FUNCTION_LIFE;
? //在这里写入新线程将要执行的代码
? return NULL;
? }
? void testThread()
? {
? LOG_FUNCTION_LIFE;
? pthread_t tid;
? pthread_create(&tid, NULL, &justAnotherTest, NULL);
? } ? 这里我们在testThread函数中用pthread_create创建了一个线程,新线程的入口为justAnotherTest函数。pthread_create函数的代码如下所示:
? PTW32_DLLPORT int PTW32_CDECL pthread_create(
? pthread_t * tid,
? const pthread_attr_t * attr,
? void *(*start) (void *),
? void *arg);
pthread_create是创建新线程的方法,它的第一个参数指定一个标识的地址,用于返回创建的线程标识;第二个参数是创建线程的参数,在不需要设置任何参数的情况下,只需传入NULL即可;第三个参数则是线程入口函数的指针,被指定为void* (void*)的形式。函数指针接受的唯一参数来源于调用pthread_create函数时所传入的第四个参数,可以用于传递用户数据。
线程安全————————————————
使用线程就不得不提线程安全问题。线程安全问题来源于不同线程的执行顺序是不可预测的,线程调度都视系统当时的状态而定,尤其是直接或间接的全局共享变量。如果不同线程间都存在着读写访问,就很可能出现运行结果不可控的问题。
在Cocos2d-x中,最大的线程安全隐患是内存管理。引擎明确声明了retain、release和autorelease三个方法都不是线程安全的。如果在不同的线程间对同一个对象作内存管理,可能会出现严重的内存泄露或野指针问题。比如说,如果我们按照下述代码加载图片资源,就很可能出现找不到图片的报错:
{
? LOG_FUNCTION_LIFE;
? CCTextureCache::sharedTextureCache()->addImage("fish.png");
? return NULL;
? }
?
? void makeAFish()
? {
? LOG_FUNCTION_LIFE;
? pthread_t tid;
? pthread_create(&tid, NULL, &loadResources, NULL);
? CCSprite* sp = CCSprite::create("fish.png");
? }
因此,使用多线程的首要原则是,在新建立的线程中不要使用任何Cocos2d-x内建的内存管理,也不要调用任何引擎提供的函数或方法,因为那可能会导致Cocos2d-x内存管理错误。
同样,OpenGL的各个接口函数也不是线程安全的。也就是说,一切和绘图直接相关的操作都应该放在主线程内执行,而不是在新建线程内执行。
线程间任务安排————————————————————
使用并发编程的最直接目的是保证界面流畅,这也是引擎占据主线程的原因。因此,除了界面相关的代码外,其他操作都可以放入新的线程中执行,主要包括文件读写和网络通信两类。
文件读写涉及外部存储操作,这和内存、CPU都不在一个响应级别上。如果将其放入主线程中,就可能会造成阻塞,尤为严重的是大型图片的载入。对于碎图压缩后的大型纹理和高分辨率的背景图,一次加载可能耗费0.2 s以上的时间,如果完全放在主线程内,会阻塞主线程相当长的时间,导致画面停滞,游戏体验很糟糕。在一些大型的卷轴类游戏中,这类问题尤为明显。考虑到这个问题,Cocos2d-x为我们提供了一个异步加载图片的接口,不会阻塞主线程,其内部正是采用了新建线程的办法。
并发编程————
p456- 472
跨平台——————————————————————————————————
Cocos2d-x对于iOS、Android和Windows已经支持已久了,而最近的Cocos2d-x将支持推广到了微软的Windows 8(指Windows 8的现代UI,曾经代号为Metro)和Windows Phone 8两大移动平台上。同时,借助Cocos2d-HTML5,也可以轻松开发浏览器上的游戏。
win8
可以不作任何修改即可直接移植到Windows 8平台上,而我们只需要在Windows 8的Visual Studio 2012上重新编译一次即可。
可能引起迷惑的是底层绘图的相关调用。目前,Windows 8的现代风格界面应用仅由DirectX提供绘图支持,也就是说,我们必须放弃OpenGL绘图API。好在二者区别不是特别大,实现同样的功能几乎都能互相找到替代品,只需要简单地替换即可。
Windows Phone平台
为了保护Windows Phone 7的底层硬件,并没有开放C++接口,而是统一使用C#进行开发。于是Cocos2d-x也就扩展到了Cocos2d-x for XNA版本,使用XNA支持底层绘图,向上提供几乎和同期C++版本完全一致的开发接口。加上C#和C++同为面向对象的语言,实际上,不需要太多的改动就可以将游戏从其他平台移植到Windows Phone 7平台上。
Cocos2d-HTML5————————————————
最后值得一提的是Cocos2d-HTML5这一版本。这是Cocos2d搭建在HTML5上的版本,基于Canvas绘图,使用JavaScript开发。
首先,现在任何一个操作系统都一定具备浏览器,基于HTML5编写的游戏可以说天生具备了跨平台的特性。其次,JavaScript作为一种脚本语言可以直接运行在任何设备之上,Cocos2d-x与Cocos2d-iPhone都提供了一套完全兼容Cocos2d-HTML5的API,这意味着我们编写的Cocos2d-HTML5游戏不需要进行额外的工作就可以无缝集成到现有引擎之中。
/************************************************************************
HTML(Hyper Text Mark-up Language )即超文本标记语言,是 WWW 的描述语言,由 Tim Berners-lee提出。设计 HTML 语言的目的是为了能把存放在一台电脑中的文本或图形与另一台电脑中的文本或图形方便地联系在一起,形成有机的整体,人们不用考虑具体信息是在当前电脑上还是在网络的其它电脑上。这样,你只要使用鼠标在某一文档中点取一个图标,Internet就会马上转到与此图标相关的内容上去,而这些信息可能存放在网络的另一台电脑中。
HTML文本是由 HTML命令组成的描述性文本,HTML 命令可以说明文字、 图形、动画、声音、表格、链接等。 HTML的结构包括头部 (Head)、主体 (Body) 两大部分。头部描述浏览器所需的信息,主体包含所要说明的具体内容
************************************************************************/
/************************************************************************
Canvas 对象表示一个 HTML 画布元素 -
移植————————————
对于采用Cocos2d-x引擎实现的跨平台游戏,iOS、Android、Windows Phone 8三个基础的平台是可以无缝切换的,许多情况下只要重新编译就可以了。
而如果是Windows Phone 7、浏览器这些非原生Cocos2d-x支持的平台,那么我们将面临大量一比一转换的代码,这时候可以考虑先使用语言间的机械转换机,结合正则表达式等全文替换,将游戏移植到目标平台后再进行细微的调试。
而对于从其他引擎转入Cocos2d-x引擎的游戏,实际上相当于一次重构。从现在的发展方向看,几乎不可能丢下iOS、Android和Windows Phone中的任何一个平台,横跨所有平台是大势所趋,我们推荐在这个时候使用JavaScript开发基于Cocos2d-HTML5的游戏,利用强大的可移植性为我们的开发节省大量时间。
Cocos2d-HTML5——————————————————————————
在前面的章节中我们已经提到,目前移动设备的游戏开发趋势是多平台开发,Cocos2d-x就是为了实现让游戏运行在多个平台而开发的引擎。虽然引擎已经做到了iOS、Android与Windows下的跨平台,但对于新兴的HTML5平台来说则无能为力,Cocos2d-HTML5就是为了解决这个问题而产生的。
概述 ?
Cocos2d-HTML5是基于HTML和JavaScript的游戏引擎,采用HTML5提供的Canvas对象与DOM进行绘图,因而使用Cocos2d-HTML5创建的游戏可以运行在各种主流的浏览器上,不需要依赖操作系统。表19-1列举出了各种主流平台与浏览器对HTML5的支持情况。可以看出,到目前为止,Cocos2d-HTML5可以运行在绝大多数平台上。从这个角度来讲,Cocos2d-HTML5是首个真正实现平台无关开发的Cocos2d引擎。
基于Cocos2d-HTML5开发游戏时,游戏逻辑采用JavaScript实现。JavaScript是一种动态、弱类型、基于原型(prototype)的脚本语言,可以直接在各种主流的浏览器上运行
采用Cocos2d-HTML5开发的游戏可以运行在几乎所有的图形操作系统之上,只要此系统拥有支持HTML5的浏览器,就可以运行我们开发的游戏。同样,在任何拥有HTML5浏览器的操作系统上,我们都可以进行游戏开发。
只需要一个文本编辑器就可以编写游戏代码,只需要一个支持HTML5的浏览器就可以运行游戏。相对于其他的Cocos2d版本而言,Cocos2d-HTML5对开发环境的要求是最低的。
在Cocos2d-HTML5开发环境中,我们必须拥有的两个工具是文本编辑器和浏览器。文本编辑器用于创建并编辑游戏代码,实现游戏开发中所有的编码工作,浏览器用于运行和调试游戏。这两个工具构成了最基本的Cocos2d-HTML5开发环境
当我们拥有了基本的开发环境之后,理论上已经就可以任意地开发游戏了。然而文本编辑器毕竟只是一个简单的文字编辑工具,在更复杂的开发工作中,单纯的文本编辑器就显得软弱无力了。在JavaScript工程的开发中,也有功能强大的IDE,在此我们向读者推荐JetBrains公司的开发工具WebStorm。 ?
Cocos2d-HTML5可以运行在任何浏览器中,当然也可以部署到Chrome Web Store。通俗地讲,Chrome Web Store是Chrome浏览器版本的App Store。与移动设备应用商店不同的是,Chrome Web Store提供的是运行在浏览器下的网页应用。使用Cocos2d-HTML5移植的《捕鱼达人》不久之前就在Web Store中发布了。
开发环境介绍 ?
与Cocos2d-x开发的流程一样,在正式开始开发游戏之前,我们需要搭建开发环境。Cocos2d-HTML5所采用的开发环境由以下几个部分组成。 ?
编辑器:用于编辑项目的代码文件,对于中大型项目,建议使用集成开发环境来编写 ? 代码。 ?
浏览器:用于查看游戏运行效果并进行调试。
Web服务器:用于托管游戏内容,这是可选组件。Web服务器主要用来解决Chrome的安全性问题,因此使用Firefox的用户可以不必安装。
Cocos2d采用XMLHttpRequest对象来读取文件。对于偏好使用Chrome浏览器的用户来说,它的默认安全配置禁止了本地HTML文件使用XMLHttpRequest对象,我们有两个办法来解决这个问题。第一个办法是配置一个Web服务器端,把游戏页面发布到网站中;第二个办法是启动Chrome的时候加入-allow-file-access-from-files或-disable-web-security参数。
通常,我们可以选择轻量级的文本编辑器,如免费的EditPlus,也可以选择较为复杂的IDE;对于浏览器,我们可以选择目前执行效率最高的Google Chrome;对于Web服务器,我们可以选择开源软件Apache或微软IIS。
搭建开发环境 开始开发————————
p487 -509
命名原则————————————————————————————————————————————
移植
正是由于不同平台下的Cocos2d都一定程度地遵循着同样的命名原则,才使得游戏的移植成为可能。
无论是Cocos2d-iPhone,还是派生自Cocos2d-iPhone的Cocos2d-x,甚至派生自Cocos2d-x的Cocos2d-XNA与Cocos2d-HTM5,都遵循类似Objective-C的命名原则
类名称
Cocos2d的所有类名称都包含"CC"前缀
类函数
在Objective-C中,并不存在严格意义上的构造函数,开发者需要调用以"init"为前缀的初始化方法,或者类提供的静态工厂函数来创建类
属性
Objective-C使用@property关键字来实现属性,这种方法在本质上把创建get和set访问器的工作交给了编译器。
选择器
选择器是Objective-C中用来代替类函数指针的类型。
全局变量、函数与宏
对于C#来说,它完全没有全局变量、函数和宏的概念。因此对于全局变量和函数,我们只能采用静态类的方式来模拟。例如,可以创建一个Global类,在Global中添加静态变量以及静态函数
avaScript没有C#严格,它允许存在全局变量和函数,但是为了避免变量名称产生冲突,我们可以把所有的全局变量和函数保存到一个全局对象中。例如,在Cocos2d-HTML5中所有的类、对象、函数都保存在cc全局对象中
JavaScript同样不支持宏,因此我们只能利用代码来实现宏的效果。对于C语言中的条件编译,我们可以采用if语句来判断
跨语言移植—————————————————————————————————————————————
许多情况下,第一次开发所采用的平台并不一定合适,例如许多游戏采用Cocos2d-iPhone开发,但随着游戏的成功,开发者决定把游戏平台扩展到Android,这就需要把基于Cocos2d-iPhone的游戏移植到Cocos2d-x下。而在其他情况下,当希望把游戏平台进一步扩展到Windows Phone 7上时,则需要把Cocos2d-x代码移植到Cocos2d-XNA上;当希望把游戏改写成基于JavaScript的脚本代码以便部署和升级维护时,则需要把游戏移植到Cocos2d-HTML5上。
无论是Cocos2d-iPhone、Cocos2d-x、Cocos2d-XNA,还是Cocos2d-HTML5,每一种游戏引擎采用的语言都不相同,因此我们就需要进行一项十分烦琐的工作:把一种语言实现的游戏转换成另一种语言实现。以《捕鱼达人》为例,现行Android版本的《捕鱼达人》1.6.3版本的代码量高达40 000余行,如果逐行移植这个游戏,那将是一个不可想象的庞大工程。为此,我们将在下面介绍跨语言移植游戏的基本步骤以及技巧。
为了便于介绍,我们把移植游戏的过程划分为相对独立的3个阶段,每个阶段在开始之前都需要完成前一个阶段。而相对地,在每个阶段中多个人员可以同时进行移植工作,这使得移植速度得以保障。
第一阶段:代码移植————————————————
最初我们需要进行的任务是把原游戏代码移植到目标平台上。为了便于描述,我们把待移植的版本称作原游戏,把原游戏使用的平台称作原平台。相对地,我们把新的游戏称作目标游戏,把新的平台称作目标平台。
在这个阶段,我们首先需要选取一个版本的游戏作为原游戏,然后在目标平台上对原游戏的代码进行1:1的翻译。如果按照翻译方式进行分类,则可以把翻译的顺序分为自顶向下与自底向上两种。自顶向下的移植适合进行小规模的全人工移植,通常不易出错,但速度较慢;自底向上的移植适合配合自动化工具进行大规模移植,速度较快,但是由于容易出错,还需要安排严密的测试。
自顶向下的移植
在我们所说的"自顶向下"的移植方式中,"顶"指的是代码的高级架构,即类与类之间的关系,而"下"则指的是具体的语句。自顶向下的移植方式类似于工程开发的过程。回想在游戏开发中,我们通常先搭建游戏的架构,创建好所有的结构之后再逐个进行实现,最终完成游戏。自顶向下的移植方式与这种工作方式类似,我们同样根据原游戏的架构在目标平台上创建一个一致的架构,然后开始向框架中添加代码。
当框架创建好后,翻译工作对整个工程翻译的完成度依赖不大,因此可以把工作分配给多个人员并行完成,以加快翻译的速度。
自顶向下移植的优势在于,移植过程中首先创建与原游戏一致的架构,而这个过程是人工完成的,通常不易在架构的层次上出现错误,因此即使遇到了问题也可以轻易排查并解决——————需要良好的阅读代码能力 才能看懂框架!!!!!!!!!!!!
而自顶向下移植的缺点在于不便于引入自动化工具,整个代码移植的过程都需要人工参与,对于大型项目来说开销较大。因此,自顶向下的移植方式非常适合小型项目的移植。
自底向上的移植
自底向上的移植方式指的是在不建立游戏架构的情况下,先直接对源码逐行进行对等的移植工作,然后再处理模块间的关系,最终完成整个架构的移植。在这种移植方式中,我们的工作主要集中于两个部分:第一部分是逐行的代码翻译,第二部分是修正模块间的关系。
在自顶向下的翻译中,由于工作流程是首先建立架构,然后向架构中填充代码,填充的代码分散在各个函数或方法中,因此并不适合利用自动化的翻译工具进行处理。然而当我们采用自底向上的移植方法时,因为首先需要对全部代码进行翻译,然后再对架构上的错误进行修正,所以可以直接在翻译的时候引入自动化翻译工具。
逐行翻译代码实际上是一种不断重复的体力劳动,这种工作完全可以利用工具或脚本来代替人们的劳动,因此在此处我们引入自动化的翻译工具,用来降低逐行翻译代码的工作量。然而翻译工具并不能完全代替人们的工作。由于跨语言的翻译是一项十分困难的工作,工具通常也只能完成一部分工作,并且翻译的结果并不能保证完全正确。因此,我们利用工具翻译代码后还需要大量时间来修复。
第一类翻译工具的原理是正则表达式替换。正则表达式替换完全可以胜任许多简单的转换工作,例如C++中的域符号"::"、箭头符号"->"、点符号".",它们都等价于C#中的点符号".",于是我们可以创建一个正则表达式的替换,把C++中的3种符号都替换成点符号。第一类翻译工具实现较为简单,任何一个开发者都可以轻松地编写正则表达式,然而正则表达式没有能力识别复杂的语句,处理后还需要手动修复许多代码。即使如此,这一类翻译工具仍然可以为我们减少许多时间。下面列举常用的此类型的工具或脚本
第二类翻译工具又称作翻译编译器(source-to-source compiler、transcompiler或transpiler)。第一类翻译器只能转换词法以及部分简单的语法,而第二类翻译器的原理是语法分析,根据分析的结果生成目标语言的代码。它的原理与编译器类似,相对于第一类翻译工具,这类翻译工具可以转换较为复杂的语句,甚至直接处理整个工程。可以说,第二类翻译器是比较理想的翻译工具,但越是强大的工具,数量就越少。
C++ to C# Converter
ObjC2J:由Andremoniy开发的开源翻译器,可以把Objective-C代码翻译成对应的Java代码。
Emscripten:一个免费的JavaScript代码生成器,接受任何LLVM支持的语言(如C和C++)并生成等价的JavaScript代码。
翻译主要能体现出两个问题。
第一个问题是从支持宏的语言翻译到不支持宏的语言(例如从C++翻译到C#),宏的翻译容易出现错误,此时我们就必须手工修复宏的翻译了。
第二个问题是翻译工具对于每条语句的把握相对较为准确,但是对于继承关系的翻译常常会出错,如果忽略了检查工作,在测试阶段这种问题通常难以察觉与修复,因此翻译过后需要逐个类检查继承、重载关系,确保目标游戏的架构与原游戏一致。
4个游戏引擎在内存管理方面并不完全一致。
基于Objective-C的Cocos2d-iPhone与基于C++的Cocos2d-x所使用的语言并不具备垃圾回收器,需要手动管理内存,而基于C#的Cocos2d-XNA和基于JavaScript的Cocos2d-HTML5则可以使用垃圾回收器来自动管理内存。因此,在前两种语言中,我们不得不采取本书第一部分介绍的引用计数机制来管理对象的释放时机;而在后两种语言中,我们不需要关心对象的内存管理,因此所有的内存管理语句(如retain方法等)都不必保留。 ?
第二阶段:消除平台差异(1)————————————
完成了代码翻译后,我们还面临着许多问题。此时我们的游戏还是不能编译运行,因为游戏中多多少少会用到一些非游戏引擎的库,其中一部分是语言的公共库,另一部分是第三方库,如各种社交网络的分享API。因此,我们需要修改代码来实现与新平台上各种库或API的对接。
对于公共库而言,最值得我们讨论的功能涵盖了容器、XML文件处理和网络通信这3个方面,其他公共库的功能虽然也可能使用,但是用到的频率不高
容器————
XML处理
对于一般化的XML文件,在各个平台下的处理方式不尽相同。不同平台下有许多可供选择的XML库
在使用Objective-C或C++进行开发的时候,不但可以使用各自语言中的库,也可以使用标准的C语言库。由于C语言库可以同时在3种语言中使用,十分便于移植,因此,LibXML2也是一个很好的选择。C#与JavaScript都有各自提供的XML解析器,通常没必要去使用第三方库。
网络通信
Cocos2d系列引擎并没有负责网络通信的模块。如果我们开发的游戏用到了网络通信的功能,就需要使用第三方库,这为我们的移植带来了一些麻烦。
在游戏开发中,许多简单的通信使用HTTP实现。HTTP在各个平台的开发中都是十分常见的,每个平台下都有许多可供我们选择的库来使用。例如,Cocos2d-x官方推荐使用libcurl来实现HTTP、FTP等协议的通信。libcurl使用C语言实现,因此可以同时在C、C++以及Objective-C中使用而不必额外移植。在表20-5中,我们简单地列举了各个平台下常用的HTTP通信库。
HTTP是无状态的短连接协议,因此适合用于进行简单的通信。而当需要开发实时在线的网络游戏时,就需要使用网络套接(socket)来保持长连接了。在各个平台下,套接字的实现也不完全相同,然而由于大多数操作系统都支持POSIX标准,套接字也是此标准的一部分,因此在C、C++以及Objective-C中,我们都可以使用标准BSD Socket函数来实现长连接。在C#中,套接字的实现位于System.Net.Sockets命名空间中,而JavaScript则可以使用WebSocket来实现长连接。
第三方库
在游戏中,我们经常会使用到第三方提供的广告平台、游戏中心与社交网络等组件,这些API通常以闭源库的形式提供给我们。当我们的游戏需要移植到另一个平台时,这些闭源库就必须替换为目标平台上的库了。
替换第三方库是有条件的,那就是我们所使用的库必须在目标平台上有对应的版本,否则替换就无从谈起。根据目标平台的不同以及库所在语言的不同,替换第三方库的难度也会有所不同。替换时可能会有以下3种情况,我们一一讨论
第一种情况,在目标平台上,引擎所使用的语言与库的语言相同,此时库的替换是最轻松的。例如,当我们打算把Cocos2d-x移植到Cocos2d-XNA,游戏中使用的MD5算法库恰好又有C++与C#两个版本,那么我们只需要把目标游戏的代码中涉及MD5算法库的调用替换为C#版本的代码即可。值得注意的是,在Windows Phone 7平台上存在两种应用--Silverlight与XNA,两者并不能友好地共存。Cocos2d-XNA创建的游戏是XNA应用,因此基于Silverlight的许多广告API就不能用于游戏开发。 ?
第二种情况,当我们的目标平台是Android时,也许只能找到Java版本的库供我们使用,例如许多广告平台都只提供Android上的Java库。如果我们的游戏是基于C++开发的,并不能直接使用Java上的第三方库。所幸Android NDK提供了Java到C++的绑定功能,称为Java Native Interface(JNI),使得我们可以在Java与C++代码中互相自由调用函数。关于JNI技术,限于篇幅,我们在此不做介绍。
第三种情况,当库没有提供目标平台上的实现时,我们就几乎无能为力了。这种情况通常在JavaScript语言上出现,因为JavaScript仍然是一个十分年轻的新生平台,大多数第三方库并没有提供对JavaScript的支持。所幸基于JavaScript的Cocos2d-HTML5并不是只能运行在浏览器中,当它运行在Cocos2d-iPhone或Cocos2d-x的脚本引擎中时,如果库提供了iOS版本或Android版本,则我们可以把库通过脚本引擎的JavaScript绑定功能暴露给脚本,从而实现JavaScript上的库替换。举一个简单的例子,我们需要把一个使用AdMob广告平台的游戏移植到Cocos2d-HTML5中,并使用Cocos2d-x在移动设备上以脚本的形式运行游戏,则需要以下3个步骤。
利用Android NDK的JNI工具在C++代码中创建AdMob的包装。 ?
在Cocos2d-x初始化时,利用JavaScript绑定技术把AdMob在C++中的包装暴露到脚本引擎中。
在JavaScript代码中调用AdMob的相关函数。 ?
其他问题
然而Android以及Windows Phone设备众多,屏幕像素可谓五花八门,最新的iPhone 5也采用了与上一代手机完全不同的屏幕比例。在这种情况下,我们就需要对游戏的布局系统进行重新设计,以保证游戏可以运行在不同分辨率下。