您的位置:首页 > 编程语言

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

2013-03-31 11:38 274 查看
       写在前面:中期阶段我们只是按照书上的方式来介绍重构代码的过程,由于版本等原因,粘贴出来的代码已经不符合我们的需要。所以具体改动的代码以中期阶段项目里面的源码文件为准,项目源码下载请移步改写《魔塔》中篇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 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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  cocos2d-x 魔塔