改写《魔塔》中篇04:重构代码之单例类
2013-03-31 11:38
274 查看
写在前面:中期阶段我们只是按照书上的方式来介绍重构代码的过程,由于版本等原因,粘贴出来的代码已经不符合我们的需要。所以具体改动的代码以中期阶段项目里面的源码文件为准,项目源码下载请移步《改写《魔塔》中篇06:善后工作和注意事项(附:中期阶段项目下载)》。还有我们对原书中的项目进行了一些改动,包括用CCArray替换了CCMutableArray,增加了用于勇士初始化的getSpriteFrame方法等,因此,大家要注意项目源码改动的地方,并且留意文档注释。
大家应该注意到上一篇还有两个小部分没有完成,一是动画相关的代码,二是在Hero类中如何访问GameMap对象。
我们先来完成动画相关的代码。这一部分单独拿出来讲,是考虑到定义动画的工作很耗费代码行数,并且十分单调重复,没有必要放在具体的游戏对象里,可以专门设计一个动画管理器来管理游戏中所有的动画。在场景创建之初,将可能用到的动画,从配置文件一次性加载;在场景运行期间,就可以直接从管理器取得相应的动画资源了。在场景被销毁的时候,再控制动画管理器销毁不用的资源。
好,在实现具体的动画管理器之前,我们需要多方位思考,请先考虑这几个问题:动画管理器究竟会以什么样的形式被调用?在游戏的各个对象中,哪些会有访问动画管理器的需求?不同的类需要访问不同的管理器实例吗?是否会有多线程同时访问?在仔细思考完这些问题以后,相信你已经得到了答案:我们在整个游戏过程中只需要一个动画管理器,它需要以尽可能方便的形式被所有游戏对象访问到,最后的多线程问题在这款游戏中不存在。所以让我们异口同声地说出心中的那两个字吧,没错!就是单例。
我们先写一个单例模式的模板容器,以后就可以方便地实现单例了。模板容器的具体代码如下,其中定义了一个静态变量,用于保存单例的实例;有两个公用方法,用于获取唯一实例和释放操作。
然后我们新建一个AnimationManager类,继承Singleton类。可以直接重用基类的获取实例和销毁实例的方法。同时,它拥有一个动画映射表,用来缓存动画模板,可以根据int型的键值查询对应的动画模板。定义了一个枚举类型,罗列了目前所有动画对应的键值。对外暴露的公告方法有3个:initAnimationMap用于初始化动画模板资源到AnimationCache中,getAnimation可以根据键值返回对应的动画模板定义,createAnimate方法用来生成指定的动画实例,可以直接供CCNode的runAction调用。具体头文件定义如下:
各方法的具体实现如下,在initAnimationMap方法中,将勇士的行走动画加载到全局的CCAnimationCache中。getAnimation获取动画模板时,也是从CCAnimationCache中获取。
接下来,我们在哪里调用initAnimationMap方法呢?如果游戏资源比较多,最好创建一个加载(loading)界面,在后台加载资源,加载完毕后再进入游戏场景。如果资源不多,可以简单地在AppDelegate的applicationDidFinishLaunching中初始化AnimationManager,然后在AppDelegate的析构函数中释放它。
在Hero类的move方法中,修改heroSprite的runAction方法如下:
上面我们用单例模式创建了一个动画管理器,在游戏的任意对象中都能方便地使用。接着我们再介绍一种单例模式的应用场景。大家可能已经发现,随着代码的重构,单独的类被拆分成多个功能相对单一的类,这样类之间的数据访问成了一个很大的问题。比如,在Hero类的checkCollision方法中需要用到GameMap对象,在moveDone事件回调中,需要访问GameLayer的setSceneScrollPosition方法。我们当然可以通过传递对象的方式来进行数据交互,但这种方式相对来说比较麻烦。如果可以保证各个对象的唯一性,比如当前游戏主场景、游戏地图和勇士等只可能有一个实例存在,那么最简便的方法就是使用单例模式创建一个类,保存全局都可以访问的唯一性变量。下面,新建一个Global类继承于Singleton类,其中包含了几个需要全局访问的游戏对象,即GameLayer、GameMap、和Hero等,具体头文件如下:
注意,要在各类的构造函数中对Global中的变量进行赋值:
由于我们只保存了上述3个对象的指针,所以对象本身的释放工作不需要Global类来完成,只需要在析构函数中将各个指针变量设置为NULL即可。
创建好了Global类,修改Hero类中所有需要使用gameMap实例的地方,部分代码如下:
大家应该注意到上一篇还有两个小部分没有完成,一是动画相关的代码,二是在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 object CCScene *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); //移动完毕,设置移动状态为false isHeroMoving=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; }
相关文章推荐
- 改写《魔塔》中篇03:重构代码之分离勇士类
- 改写《魔塔》中篇05:重构代码之控制层
- 改写《魔塔》中篇01:重构代码之分离场景和图层
- 改写《魔塔》中篇02:重构代码之分离游戏地图类
- 为自己代码重构了一部分内容
- 重构代码(2)-处理空字符串
- 代码重构 - 表驱动和工厂模式结合
- rails 代码重构之级联模型与include之间如何处理
- 重构改善既有代码设计--重构手法13:Inline Class (将类内联化)
- 代码重构方向原则指导
- eclipse 重构代码自动抽取函数
- 步步为营 .NET 代码重构学习笔记 八
- 重构—改善代码既有设计 之switch/case使用
- (转帖)小菜编程成长记(二 代码规范、重构)
- 步步为营 .NET 代码重构学习笔记 七
- SQL SERVER 到 MYSQL 迁移存储过程代码改写详细对照表
- 代码重构
- 从案例深入了解如何重构代码-重构计划
- 代码重构-寻找坏味道
- 重构改善既有代码设计--重构手法18:Self Encapsulate Field (自封装字段)