写在前面:中期阶段我们只是按照书上的方式来介绍重构代码的过程,由于版本等原因,粘贴出来的代码已经不符合我们的需要。所以具体改动的代码以中期阶段项目里面的源码文件为准,项目源码下载请移步《改写《魔塔》中篇06:善后工作和注意事项(附:中期阶段项目下载)》。还有我们对原书中的项目进行了一些改动,包括用CCArray替换了CCMutableArray,增加了用于勇士初始化的getSpriteFrame方法等,因此,大家要注意项目源码改动的地方,并且留意文档注释。
接下来我们开始分离勇士类,暂命名为Hero类。Hero类中应包括用于显示的精灵、各种属性(攻击、防御和生命值)和勇士移动的方法等。
首先,请大家思考一个问题:Hero类是否应该继承于CCSprite?我们以前实现的勇士移动,仅仅使用了一个CCSprite,那么现在要往里面添加额外的方法,继承自CCSprite应该就可以满足了。这种考虑没错,确实就目前要实现的功能来说,这个方法是可行的。但是如果能看得稍微远一点,就会发现这个方案缺乏灵活性。设想一下,如果我们想在勇士的头顶上加一个用于显示生命值的血条,要求它能随勇士移动而移动,难道需要写一个update方法,不停地刷新生命条的坐标吗?这显然不现实,也不合理。同理还有勇士的装备和光环buff的技能效果等。所以我们让Hero类继承CCNode,然后将用于显示的精灵作为孩子节点添加进去,而游戏主界面将Hero整体作为一个CCNode保存为孩子节点。这样针对Hero主体的缩放、移动、执行动作、显示和隐藏等操作都会影响到Hero类内部的所有显示节点。下面来看下Hero类头文件的定义:
#ifndef __HERO_H__
#define __HERO_H__
#include "MTGame.h"
using namespace cocos2d;
//勇士类继承自CCNode
class Hero:public cocos2d::CCNode
{
public:
Hero();
~Hero();
//静态方法,用于创建勇士实例
static Hero* heroWithinLayer();
//初始化方法
bool heroInit();
//用于显示勇士形象的精灵
CCSprite* heroSprite;
//标识勇士是否在移动状态
bool isHeroMoving;
//让勇士向指定方向移动一格
void move(HeroDirection direction);
//移动完成后的回调函数
void onMoveDone(CCNode* pTarget,void* data);
//设置勇士朝向
void setFaceDirection(HeroDirection direction);
};
#endif
静态方法heroWithinLayer用于创建 勇士实例,仍然遵循cocos2d-x的对象创建方式。与GameMap类中的gameMapWithTMXFile方法作用类似,相信大家对此都已经比较了解。具体代码如下:
//静态方法,用于生成Hero实例
Hero* Hero::heroWithinLayer()
{
//new一个对象
Hero* pRet=new Hero();
//调用init方法
if(pRet&&pRet->heroInit())
{
//将实例放入autorelease池,统一由引擎控制对象的生命周期
pRet->autorelease();
return pRet;
}
CC_SAFE_DELETE(pRet);
return NULL;
}
heroInit是用来初始化Hero类的,主要完成heroSprite的新建、添加、设置锚点以及一些标志位的赋值等。具体代码如下。注意,CCSprite::createWithSpriteFrame方法并没有给完全,暂时先放在这里,因为我们将在下一篇中引入一个新类来管理动画。
bool Hero::heroInit()
{
//根据向下行走动画的第一帧创建精灵
heroSprite=CCSprite::createWithSpriteFrame(/*根据指定动画帧创建对象*/);
//设置锚点
heroSprite->setAnchorPoint(CCPointZero);
//将用于显示的heroSprite加到自己的节点下
this->addChild(heroSprite);
//一开始不处于move状态
isHeroMoving=false;
return true;
}
Hero的move方法除了照搬HelloWorldScene中的menuCallBackMove以外,还需要做一些修改。原来我们为了对单个CCSprite实现移动和播放动画同时进行的效果,使用了CCSpawn。现在由于Hero类可能会承载多个精灵,不能单纯地只移动heroSprite一个精灵,而是要将作为CCNode的Hero主体类整体进行移动。所以heroSprite仅需要播放行走动画,而Hero类进行位移动作。注意,原来heroSprite->getPosition()的地方都需要改成this->getPosition()。另外,为了保证Hero是在移动到目标位置后再执行onMoveDone,我们将其与Hero节点本身的CCMoveBy动作拼接成为一个串行操作。move方法的部分代码如下:
void Hero::move(HeroDirection direction)
{
if(isHeroMoving)
return;
//若干操作
......
//计算目标坐标,用当前勇士坐标加上移动距离
CCPoint targetPosition=ccpAdd(this->getPosition(),moveByPosition);
//调用checkCollision检测碰撞类型,如果是墙壁,则只需要设置勇士的朝向
if(checkCollision(targetPosition)==kWall)
{
setFaceDirection((HeroDirection)direction);
return;
}
//heroSprite仅播放行走动画
heroSprite->runAction(/*获取指定动画对象*/);
//主体进行位移,结束时调用onMoveDone方法
CCAction* action=CCSequence::create(
CCMoveBy::create(0.2f,moveByPosition),
//把方向消息传递给onMoveDone方法
CCCallFuncND::create(this,
callfuncND_selector(Hero::onMoveDone),(void*)direction),
NULL);
this->runAction(action);
isHeroMoving=true;
}
onMoveDone和setFaceDirection方法基本和原来HelloWorldScene中的实现类似,注意要将用到heroSprite->getPosition()的地方都需要改成this->getPosition()。
接下来开始改写checkCollision方法,大家会发现里面用到了gameMap对象。如何在Hero类中访问GameMap对象呢?我们下一篇会介绍一种方法。
好了,目前为止Hero类的功能已经基本完成,现在需要修改一下GameLayer中初始化勇士的方法:
//调用Hero类的静态方法创建实例
hero=Hero::heroWithinLayer();
//设置Hero的起始位置
hero->setPosition(map->positionForTileCoord(ccp(1,11)));
//将Hero加入GameLayer
addChild(hero);
很简单吧,仅需要3行代码就在游戏中创建出了一个勇士。