改写《魔塔》中篇04:重构代码之单例类

来源:互联网 发布:魔兽数据修改器 编辑:程序博客网 时间:2024/06/04 18:10

       写在前面:中期阶段我们只是按照书上的方式来介绍重构代码的过程,由于版本等原因,粘贴出来的代码已经不符合我们的需要。所以具体改动的代码以中期阶段项目里面的源码文件为准,项目源码下载请移步《改写《魔塔》中篇06:善后工作和注意事项(附:中期阶段项目下载)》。还有我们对原书中的项目进行了一些改动,包括用CCArray替换了CCMutableArray,增加了用于勇士初始化的getSpriteFrame方法等,因此,大家要注意项目源码改动的地方,并且留意文档注释。

       大家应该注意到上一篇还有两个小部分没有完成,一是动画相关的代码,二是在Hero类中如何访问GameMap对象。

       我们先来完成动画相关的代码。这一部分单独拿出来讲,是考虑到定义动画的工作很耗费代码行数,并且十分单调重复,没有必要放在具体的游戏对象里,可以专门设计一个动画管理器来管理游戏中所有的动画。在场景创建之初,将可能用到的动画,从配置文件一次性加载;在场景运行期间,就可以直接从管理器取得相应的动画资源了。在场景被销毁的时候,再控制动画管理器销毁不用的资源。

        好,在实现具体的动画管理器之前,我们需要多方位思考,请先考虑这几个问题:动画管理器究竟会以什么样的形式被调用?在游戏的各个对象中,哪些会有访问动画管理器的需求?不同的类需要访问不同的管理器实例吗?是否会有多线程同时访问?在仔细思考完这些问题以后,相信你已经得到了答案:我们在整个游戏过程中只需要一个动画管理器,它需要以尽可能方便的形式被所有游戏对象访问到,最后的多线程问题在这款游戏中不存在。所以让我们异口同声地说出心中的那两个字吧,没错!就是单例。

        我们先写一个单例模式的模板容器,以后就可以方便地实现单例了。模板容器的具体代码如下,其中定义了一个静态变量,用于保存单例的实例;有两个公用方法,用于获取唯一实例和释放操作。

#ifndef __SINGLETON_H__#define __SINGLETON_H__template <class T>class Singleton{public://获取类的唯一实例static inline T* instance();//释放类的唯一实例void release();protected:Singleton(void){}~Singleton(void){}static T* _instance;};template <class T>inline T* Singleton<T>::instance(){if(!_instance)_instance=new T;return _instance;}template <class T>void Singleton<T>::release(){if(!_instance)return;delete _instance;_instance=0;}//cpp文件中需要先声明静态变量#define DECLARE_SINGLETON_MEMBER(_Ty)\template <> _Ty* Singleton<_Ty>::_instance=NULL;#endif//_SINGLETON_H

      然后我们新建一个AnimationManager类,继承Singleton类。可以直接重用基类的获取实例和销毁实例的方法。同时,它拥有一个动画映射表,用来缓存动画模板,可以根据int型的键值查询对应的动画模板。定义了一个枚举类型,罗列了目前所有动画对应的键值。对外暴露的公告方法有3个:initAnimationMap用于初始化动画模板资源到AnimationCache中,getAnimation可以根据键值返回对应的动画模板定义,createAnimate方法用来生成指定的动画实例,可以直接供CCNode的runAction调用。具体头文件定义如下:

#ifndef __ANIMATION_MANAGER_H__#define __ANIMATION_MANAGER_H__#include "MTGame.h"typedef enum{aDown=0, //向下行走动画aLeft, //向左行走动画aRight, //向右行走动画aUp, //向上行走动画}AnimationKey; //动画模板键值using namespace cocos2d;class AnimationManager:public Singleton<AnimationManager>{public:AnimationManager();~AnimationManager();//初始化动画模板缓存表bool initAnimationMap();//根据名字得到一个动画模板CCAnimation* getAnimation(int key);//创建一个动画实例CCAnimate* createAnimate(int key);protected://加载勇士行走动画模板CCAnimation* createHeroMovingAnimationByDirection(HeroDirection direction);};//定义动画管理器实例的别名#define sAnimationMgr AnimationManager::instance()#endif

      各方法的具体实现如下,在initAnimationMap方法中,将勇士的行走动画加载到全局的CCAnimationCache中。getAnimation获取动画模板时,也是从CCAnimationCache中获取。

#include "AnimationManager.h"DECLARE_SINGLETON_MEMBER(AnimationManager);AnimationManager::AnimationManager(){}AnimationManager::~AnimationManager(){//CCDirector会自己清除AnimationCache//CCAnimationCache::purgeSharedAnimationCache();}bool AnimationManager::initAnimationMap(){char temp[20];sprintf(temp,"%d",aDown);//加载勇士向下走的动画CCAnimationCache::sharedAnimationCache()->addAnimation(createHeroMovingAnimationByDirection(kDown),temp);sprintf(temp,"%d",aRight);//加载勇士向右走的动画CCAnimationCache::sharedAnimationCache()->addAnimation(createHeroMovingAnimationByDirection(kRight),temp);sprintf(temp,"%d",aLeft);//加载勇士向左走的动画CCAnimationCache::sharedAnimationCache()->addAnimation(createHeroMovingAnimationByDirection(kLeft),temp);sprintf(temp,"%d",aUp);//加载勇士向下走的动画CCAnimationCache::sharedAnimationCache()->addAnimation(createHeroMovingAnimationByDirection(kUp),temp);return true;}CCAnimation* AnimationManager::createHeroMovingAnimationByDirection(HeroDirection direction){CCTexture2D* heroTexture=CCTextureCache::sharedTextureCache()->addImage("hero.png");CCSpriteFrame *frame0,*frame1,*frame2,*frame3;//第二个参数表示显示区域的x,y,width,height,根据direction来确定显示的y坐标frame0=CCSpriteFrame::frameWithTexture(heroTexture,cocos2d::CCRectMake(32*0,32*direction,32,32));frame1=CCSpriteFrame::frameWithTexture(heroTexture,cocos2d::CCRectMake(32*1,32*direction,32,32));frame2=CCSpriteFrame::frameWithTexture(heroTexture,cocos2d::CCRectMake(32*2,32*direction,32,32));frame3=CCSpriteFrame::frameWithTexture(heroTexture,cocos2d::CCRectMake(32*3,32*direction,32,32));CCMutableArray<CCSpriteFrame *> *animFrames=new CCMutableArray<CCSpriteFrame *>(4);animFrames->addObject(frame0);animFrames->addObject(frame1);animFrames->addObject(frame2);animFrames->addObject(frame3);CCAnimation *animation=new CCAnimation();//0.05f表示每帧动画间的间隔animation->initWithFrames(animFrames,0.05f);animFrames->release();return animation;}//获取指定动画模板CCAnimation* AnimationManager::getAnimation(int key){char temp[20];sprintf(temp,"%d",key);return CCAnimationCache::sharedAnimationCache()->animationByName(temp);}//获取一个指定动画模板的实例CCAnimate* AnimationManager::createAnimate(int key){//获取指定动画模板CCAnimation* anim=getAnimation(key);if(anim){   //根据动画模板生成一个动画实例return cocos2d::CCAnimate::actionWithAnimation(anim);}return NULL;}

      接下来,我们在哪里调用initAnimationMap方法呢?如果游戏资源比较多,最好创建一个加载(loading)界面,在后台加载资源,加载完毕后再进入游戏场景。如果资源不多,可以简单地在AppDelegate的applicationDidFinishLaunching中初始化AnimationManager,然后在AppDelegate的析构函数中释放它。

bool AppDelegate::applicationDidFinishLaunching(){    // initialize director    CCDirector *pDirector = CCDirector::sharedDirector();    pDirector->setOpenGLView(&CCEGLView::sharedOpenGLView());    // sets landscape mode    pDirector->setDeviceOrientation(kCCDeviceOrientationLandscapeLeft);    // turn on display FPS    pDirector->setDisplayFPS(false);    // set FPS. the default value is 1.0/60 if you don't call this    pDirector->setAnimationInterval(1.0 / 60);//初始化动画管理器sAnimationMgr->initAnimationMap();    // create a scene. it's an autorelease objectCCScene *pScene = GameScene::playNewGame();    // run    pDirector->runWithScene(pScene);    return true;}

AppDelegate::~AppDelegate(){    SimpleAudioEngine::end();//释放动画管理器sAnimationMgr->release();}

在Hero类的move方法中,修改heroSprite的runAction方法如下:

heroSprite->runAction(sAnimationMgr->createAnimate(direction));

       上面我们用单例模式创建了一个动画管理器,在游戏的任意对象中都能方便地使用。接着我们再介绍一种单例模式的应用场景。大家可能已经发现,随着代码的重构,单独的类被拆分成多个功能相对单一的类,这样类之间的数据访问成了一个很大的问题。比如,在Hero类的checkCollision方法中需要用到GameMap对象,在moveDone事件回调中,需要访问GameLayer的setSceneScrollPosition方法。我们当然可以通过传递对象的方式来进行数据交互,但这种方式相对来说比较麻烦。如果可以保证各个对象的唯一性,比如当前游戏主场景、游戏地图和勇士等只可能有一个实例存在,那么最简便的方法就是使用单例模式创建一个类,保存全局都可以访问的唯一性变量。下面,新建一个Global类继承于Singleton类,其中包含了几个需要全局访问的游戏对象,即GameLayer、GameMap、和Hero等,具体头文件如下:

#ifndef _GLOBAL_H_#define _GLOBAL_H_#include "MTGame.h"using namespace cocos2d;class GameLayer;class GameMap;class Hero;class Global:public Singleton<Global>{public:Global(void);~Global(void);//游戏主图层GameLayer* gameLayer;//游戏地图GameMap* gameMap;//勇士Hero* hero;};#define sGlobal Global::instance()#endif

注意,要在各类的构造函数中对Global中的变量进行赋值:

Hero::Hero(){sGlobal->hero=this;}

GameMap::GameMap(){sGlobal->gameMap=this;}

GameLayer::GameLayer(){sGlobal->gameLayer=this;}

      由于我们只保存了上述3个对象的指针,所以对象本身的释放工作不需要Global类来完成,只需要在析构函数中将各个指针变量设置为NULL即可。

Global::~Global(){this->gameScene=NULL;this->hero=NULL;this->gameMap=NULL;this->gameLayer=NULL;}

创建好了Global类,修改Hero类中所有需要使用gameMap实例的地方,部分代码如下:

void Hero::onMoveDone(CCNode *pTarget,void *data){//将void*转换为int,再从int转换到枚举类型int direction=(int) data;setFaceDirection((HeroDirection)direction);//移动完毕,设置移动状态为falseisHeroMoving=false;sGlobal->gameLayer->setSceneScrollPosition(this->getPosition());}

//判断碰撞类型CollisionType Hero::checkCollision(CCPoint heroPosition){//cocos2dx坐标转换为Tilemap坐标CCPoint targetCoord=sGlobal->gameMap->tileCoordForPosition(heroPosition);//如果勇士坐标超出地图边界,则返回kWall,阻止其移动if(heroPosition.x<0||targetCoord.x>sGlobal->gameMap->getMapSize().width-1||targetCoord.y<0||targetCoord.y>sGlobal->gameMap->getMapSize().height-1)return kWall;//获取当前位置图块int tileGrid=sGlobal->gameMap->getWallLayer()->tileGIDAt(targetCoord);//如果图块ID不为0表示有墙if(tileGrid){return kWall;       }//可以通行return kNone;}