温故而知新,新篇开始前简单回顾一下学习过的章节,环境搭建、项目创建、项目解析、游戏原型、屏幕适配,今天要开始的是重构角色……

一、本篇前提:

完成前一篇的内容。

具体参考:Cocos2d-x 3.3塔防游戏《保卫萝卜》教程05:对游戏原型进行屏幕适配完善


二、本篇目标:

说说游戏中各种角色的动作、属性以及重构思路;

进行代码重构让色狼大叔和女主角生动鲜活起来;

三、内容:

说说游戏中各种角色的动作、属性以及重构思路

通过前两篇我们建立的一个简陋的游戏原型,但是游戏中的人物比如色狼大叔、女主角都看去来很呆板不够鲜活,比如色狼会沿着道路移动,那这个只能说是移动根本不像是在走路,没有走的动作感觉就是沿着道路在漂移,女主角也是一动不动的站那里。这样的游戏很没有乐趣,所以需要给这些游戏角色加入动作和表情,让人看去来他们是鲜活的,对这些角色进行一下简单的动画以及属性分析如下:

色狼大叔的动画:

1、静止动画:游戏刚开始处于静止状态

2、走路动画:沿着道路走

3、死亡动画:当子弹击中血量消耗完时死亡消失

色狼大叔的属性:

1、是否运动:色狼是否处于激活沿着道路行走

2、是否死亡:是否被炮塔打死

3、行走速度:不同色狼的行走速度不同

4、色狼血量:不同色狼血量不同

5、行走:沿着道路行走

女主角的动画:

1、静止动画:游戏刚开始处于静止状态

2、卖萌动画:不能像木头似的,就加点表情动作

3、死亡动画:当纯洁值被色狼玷污光了就死亡了

女主角的属性:

1、女主角贞洁值:相当于生命值

根据上面的分析我们把每个角色拆分成动画显示和业务属性逻辑两个部分,对色狼和女主角进行代码重构。


luobo1.jpg

重构后大概结构如上图:

ActionSprite:属于CCSprite类,负责游戏角色精灵的动画显示,Luoli(萝莉)、DaShu(大叔)、JiaoShou(叫兽)等角色精灵均继承自ActionSprite,都属于动画显示类。

Luoli(萝莉):属ActionSprite的子类,负责女主角的动画效果展示。

DaShu(大叔):属ActionSprite的子类,负责大叔色狼的动画效果展示。

JiaoShou (叫兽):属ActionSprite的子类,负责叫兽色狼的动画效果展示。

Selang(色狼):属于CCNode类,负责色狼的行走、血量、速度、攻击等具体的业务,每个Selang都包含一个DaShu(大叔)或JiaoShou(叫兽)类的游戏精灵。并且具备最重要的行为实现沿着道路行走。

Girl(女孩):属于CCNode类,负责女主角的血量等具体的业务,每个Girl都包含一个Luoli类的游戏精灵。

进行代码重构让色狼大叔和女主角生动鲜活起来

打开项目工程按照上面的思路重点对色狼和女主角的代码实现进行重构。

色狼大叔代码重构

第一步:新建ActionSprite.h、ActionSprite.cpp类(角色动画类),这个类继承自CCSprite负责游戏角色的动画效果显示,色狼和女孩都会是这个类的子类。

ActionSprite.h代码:

//声明一个动作状态的枚举类型

typedef enum _ActionState{

kActionStateNone = 0, //无状态

kActionStateIdle, //静止状态

kActionStateWalk, //行走状态

kActionStateDeath //死亡状态

}ActionState;

class ActionSprite: public cocos2d::CCSprite

{

public:

ActionSprite(void);

~ActionSprite(void);

//静止

void idle();

//死亡

void death();

//行走

void walk();

//价格

CC_SYNTHESIZE(int,_price,Price);

//生命值

CC_SYNTHESIZE(float,_hp,HP);

//静止状态动作

CC_SYNTHESIZE_RETAIN(cocos2d::Action*,_idleAction,IdleAction);

//死亡状态动作

CC_SYNTHESIZE_RETAIN(cocos2d::Action*,_deathAction,DeathAction);

//行走状态动作

CC_SYNTHESIZE_RETAIN(cocos2d::Action*,_walkAction,WalkAction);

//当前动作状态

CC_SYNTHESIZE(ActionState,_actionState,ActionState);

//行走速度

CC_SYNTHESIZE(float,_walkSpeed,WalkSpeed);

//伤害值

CC_SYNTHESIZE(float,_damage,Damage);

//金钱

CC_SYNTHESIZE(float,_money,Money);

//是否有光环

CC_SYNTHESIZE(bool,_halo,Halo);

};

ActionSprite.cpp代码:

ActionSprite::ActionSprite(void)

{

_price=0;

_idleAction=NULL;

_walkAction=NULL;

_deathAction=NULL;

}

ActionSprite::~ActionSprite(void)

{

//释放内存

CC_SAFE_RELEASE_NULL(_idleAction);

CC_SAFE_RELEASE_NULL(_deathAction);

CC_SAFE_RELEASE_NULL(_walkAction);

}

//设置精灵为静止状态

void ActionSprite::idle()

{

if (_actionState != kActionStateIdle)

{

//先停止所有动作

this->stopAllActions();

//运行静止动作

this->runAction(_idleAction);

_actionState=kActionStateIdle;

}

}

//设置精灵为行走状态

void ActionSprite::walk()

{

if (_actionState != kActionStateWalk)

{

//先停止所有动作

this->stopAllActions();

//运行行走动作

this->runAction(_walkAction);

_actionState=kActionStateWalk;

}

}

//设置精灵为死亡状态

void ActionSprite::death()

{

//先停止所有动作

this->stopAllActions();

this->runAction(_deathAction);

_actionState=kActionStateDeath;

}

第二步:


luobo22.jpg

素材准备,设计2张大叔不同动作的图片,交替显示模拟色狼行走动画,完成后把图片拷贝到Resources的iphonehd文件夹中,为了适应小分辨率的手机把这个2张图片按比例缩小一半并且拷贝到Resources的iphone文件夹中。

第三步:

新建DaShu.h、DaShu.cpp类(色狼大叔动画类),这个类继承自上面的ActionSprite负责游戏色狼大叔的动画效果显示。

DaShu.h:

class DaShu : public ActionSprite

{

public:

DaShu(void);

~DaShu(void);

CREATE_FUNC(DaShu);

//初始化方法

bool init();

//设置光环,拥有光环的色狼生命值加倍

void setHalo(bool halo);

};

DaShu.cpp:

bool DaShu::init()

{

bool bRet=false;

do

{

CC_BREAK_IF(!ActionSprite::initWithFile("dashu_2.png"));

//设置静止状态动作

Vector idleFrames(1);

SpriteFrame *frame1=SpriteFrame::create("dashu_2.png", Rect(0, 0, 60, 83));

idleFrames.pushBack(frame1);

Animation *idleAnimation=Animation::createWithSpriteFrames(idleFrames,float(6.0 / 12.0));

this->setIdleAction(CCRepeatForever::create(CCAnimate::create(idleAnimation)));

int i=0;

//设置行走状态动作

Vector walkFrames(2);

for (i=0;i<2;i++)

{

SpriteFrame *frame2=SpriteFrame::create(CCString::createWithFormat("dashu_%d.png", i+1)->getCString(), Rect(0, 0, 60, 83));

walkFrames.pushBack(frame2);

}

Animation *walkAnimation=Animation::createWithSpriteFrames(walkFrames,float(6.0 / 12.0));

this->setWalkAction(CCRepeatForever::create(CCAnimate::create(walkAnimation)));

//设置死亡状态动作

Vector deathFrames(1);

SpriteFrame *frame3=SpriteFrame::create("dashu_2.png", Rect(0, 0, 60, 83));

deathFrames.pushBack(frame3);

Animation *deathAnimation=Animation::createWithSpriteFrames(deathFrames,float(6.0 / 12.0));

this->setDeathAction(Animate::create(deathAnimation));

//设置攻击值

this->setDamage(1);

//设置行走速度

this->setWalkSpeed(0.4f);

//设置生命值

this->setHP(18);

//设置金钱数

this->setMoney(1.0f/10.0f);

bRet=true;

} while (0);

return bRet;

}

//设置光环

void DaShu::setHalo(bool halo)

{

if (halo)

{

//拥有光环后生命值加4倍

float h=this->getHP()*4.0f;

this->setHP(h);

}

}

第四步:新建Selang.h、Selang.cpp类(色狼类),这个类继承自CCNode游戏场景中的每一个色狼都有这个类产生,它肯定包含一个ActionSprite的色狼动画类,并且之前在MainScene.cpp的update方法中实现的色狼沿路行走代码也将转移到这个类的update方法中。

Selang.h:

#include "cocos2d.h"

#include "Waypoint.h"

#include "GameMediator.h"

#include "ActionSprite.h"

class Selang : public cocos2d::CCNode

{

public:

Selang(void);

~Selang(void);

//根据提供的spriteIndex实例化成不同的色狼

static Selang* nodeWithType(int spriteIndex);

//初始化方法

bool initWithType(int spriteIndex,bool halo);

//激活色狼

void doActivate(float dt);

//获取精灵Rect

virtual cocos2d::Rect getRect();

//设置精灵是否激活

void setActive(bool active);

//是否死亡

bool isDie;

void update(float delta);

//色狼精灵

CC_SYNTHESIZE_RETAIN(ActionSprite*,_mySprite,MySprite);

private:

//精灵序号,为每种精灵编一个序号

int _spriteIndex;

GameMediator* m;

//当前精灵的位置

cocos2d::Point myPosition;

//走路速度

float walkingSpeed;

//开始路点

Waypoint *beginningWaypoint;

//结束路点

Waypoint *destinationWaypoint;

//是否激活

bool active;

//色狼高度

float myHeight;

//两个点的碰撞检测

bool collisionWithCircle(cocos2d::Point circlePoint,float radius,cocos2d::Point circlePointTwo, float radiusTwo);

};

Selang.cpp:

//根据提供的spriteIndex实例化成不同的色狼

Selang* Selang::nodeWithType(int spriteIndex)

{

Selang* pRet=new Selang();

bool b=false;

if (pRet && pRet->initWithType(spriteIndex,b))

{

pRet->autorelease();

return pRet;

}

else

{

delete pRet;

pRet=NULL;

return NULL;

}

}

//初始化方法

bool Selang::initWithType(int spriteIndex,bool halo)

{

bool bRet=false;

do

{

//色狼类型index

_spriteIndex=spriteIndex;

//

m = GameMediator::sharedMediator();

//不激活

active=false;

//行走速度

walkingSpeed=0.2f;

ActionSprite* sprite=NULL;

if (spriteIndex==1)//如果类型是1初始化成大叔色狼

{

sprite=DaShu::create();

sprite->setHalo(halo);

//设置速度

walkingSpeed=sprite->getWalkSpeed();

}

this->setMySprite(sprite);

//添加精灵到当前Selang中

this->addChild(_mySprite);

//计算当前色狼精灵1/2高

myHeight=sprite->getTextureRect().size.height/2.0f;

//获得路点集合中的最后一个点

Waypoint *waypoint=(Waypoint*)m->getWayPoints().back();

//设置为色狼出发点

beginningWaypoint=waypoint;

//获取出发点的下个点为色狼目标点

destinationWaypoint=waypoint->getNextWaypoint();

//获得出发点坐标

Point pos=waypoint->getMyPosition();

//对坐标进行校正提供半个身位高度

pos.add(Vec2(0,myHeight));

//记录位置坐标

myPosition=pos;

//设置精灵的初始坐标

_mySprite->setPosition(pos);

//设置初始不可见

this->setVisible(false);

//把当前色狼添加到游戏的MainScene场景中显示

m->getNowScene()->addChild(this);

//启动定时器

this->scheduleUpdate();

bRet=true;

} while (0);

return bRet;

}

void Selang::doActivate(float dt)

{

//激活色狼

active=true;

//设置色狼可见

this->setVisible(true);

}

//获取精灵Rect

Rect Selang::getRect()

{

Rect rect =Rect(_mySprite->getPosition().x - _mySprite->getContentSize().width * 0.5f,

_mySprite->getPosition().y - _mySprite->getContentSize().height* 0.5f,

_mySprite->getContentSize().width,

_mySprite->getContentSize().height);

return rect;

}

//设置精灵是否激活

void Selang::setActive(bool aactive)

{

active=aactive;

this->setVisible(true);

}

void Selang::update(float delta)

{

if (!active)

{

return;

}

Point destinationPos=destinationWaypoint->getMyPosition();

//提升色狼半个身位

destinationPos.add(Vec2(0,myHeight));

//是否拐弯

if (this->collisionWithCircle(myPosition,1,destinationPos,1))

{

if (destinationWaypoint->getNextWaypoint())

{

//设置新的出发点和目标点

beginningWaypoint=destinationWaypoint;

destinationWaypoint=destinationWaypoint->getNextWaypoint();

}

}

Point targetPoint=destinationWaypoint->getMyPosition();

//提升色狼半个身位

targetPoint.add(Vec2(0,myHeight));

float movementSpeed=walkingSpeed;

//计算目标点的向量

Point normalized=Point(targetPoint.x-myPosition.x,targetPoint.y-myPosition.y).getNormalized();

//根据速度和向量分别计算x,y方式上的偏移值

float ox=normalized.x * walkingSpeed;

float oy=normalized.y *walkingSpeed;

//更新色狼移动后的位置

myPosition = Point(myPosition.x + ox, myPosition.y +oy);

_mySprite->setPosition(myPosition);

}

//两个点的碰撞检测

bool Selang::collisionWithCircle(cocos2d::Point circlePoint,float radius,cocos2d::Point circlePointTwo, float radiusTwo)

{

float xdif = circlePoint.x - circlePointTwo.x;

float ydif = circlePoint.y - circlePointTwo.y;

//计算两点间的距离

float distance = sqrt(xdif * xdif + ydif * ydif);

if(distance <= radius + radiusTwo)

{

return true;

}

return false;

}

第五步:

如果运行一下那么上面的代码中Waypoint *waypoint=(Waypoint*)m->getWayPoints().back();这行应该会报错,因为GameMediator中没有提供getWayPoints()这个方法,所以我们要对GameMediator类进行修改加上这个方法,代码如下:

void GameMediator::setWayPoints(cocos2d::Vector wayPoints)

{

_wayPoints=wayPoints;

}

Vector GameMediator::getWayPoints()

{

return _wayPoints;

}

第六步:

在MainScene的init方法中把路点集合通过setWayPoints方法赋值给GameMediator,这样在Selang.cpp中就可以取到路点集合了:

……

this->wayPositions.pushBack(waypoint12);

GameMediator::sharedMediator()->setWayPoints(wayPositions);

……

第七步:

测试这个Selang类具体效果,先给MainScene添加一个void startGame(float delta)的方法,用这个方法开始游戏。

//开始游戏

void MainScene::startGame(float delta)

{

//实例化一个大叔类型的色狼

Selang* selang=Selang::nodeWithType(1);

//激活这个色狼

selang->setActive(true);

//设置色狼动画为行走动画

selang->getMySprite()->walk();

//取消定时器方法,保证startGame只执行一次

this->unschedule(schedule_selector(MainScene::startGame));

}

第八步:

我们在MainScene的init方法末尾处调用这个startGame的方法:

1

2//0.5秒后调用startGame方法

this->schedule(schedule_selector(MainScene::startGame),0.5f);

到这里,把第一篇中临时添加色狼的代码删除,就可以运行测试游戏了,会看到色狼大叔一扭一扭的沿着道路靠近女主角。效果非常好,我们成功的对色狼的代码进行了重构。

女主角代码重构

第一步:


luobo3.jpg

素材准备,设计4张萝莉不同动作的图片,交替显示模拟萝莉卖萌动画,完成后把图片拷贝到Resources的iphonehd文件夹中,为了适应小分辨率的手机把这个4张图片按比例缩小一半并且拷贝到Resources的iphone文件夹中。

第二步:

新建Luoli.h、Luoli.cpp类(女主角动画类),这个类继承自上面的ActionSprite负责游戏女主角的动画效果显示。

Luoli.h:

class Luoli : public ActionSprite

{

public:

Luoli(void);

~Luoli(void);

CREATE_FUNC(Luoli);

bool init();

};

Luoli.cpp:

bool Luoli::init()

{

bool bRet=false;

do

{

CC_BREAK_IF(!ActionSprite::initWithFile("girl1_1.png"));

//设置静止状态动作

Vector idleFrames(1);

SpriteFrame *frame1=SpriteFrame::create("girl1_1.png", Rect(0, 0, 100, 126));

idleFrames.pushBack(frame1);

Animation *idleAnimation=Animation::createWithSpriteFrames(idleFrames,float(6.0 / 12.0));

this->setIdleAction(CCRepeatForever::create(CCAnimate::create(idleAnimation)));

//设置行走状态动作

int i;

Vector walkFrames(4);

for (i=0;i<4;i++)

{

SpriteFrame *frame1=SpriteFrame::create(CCString::createWithFormat("girl1_%d.png", i+1)->getCString(), Rect(0, 0, 100, 126));

walkFrames.pushBack(frame1);

}

Animation *walkAnimation=Animation::createWithSpriteFrames(walkFrames,float(6.0 / 12.0));

this->setWalkAction(CCRepeatForever::create(CCAnimate::create(walkAnimation)));

bRet=true;

} while (0);

return bRet;

}

第三步:

新建Girl.h、Girl.cpp类(女孩类),这个类继承自CCNode游戏场景中的女主角由这个类产生,它肯定包含一个ActionSprite的萝莉动画类。

Girl.h:

class Girl : public cocos2d::CCNode

{

public:

Girl(void);

~Girl(void);

//根据提供的type实例化成不同的女主角

static Girl* nodeWithType(int type);

//初始化方法

bool initWithLocation(cocos2d::Point location);

//获取精灵Rect

cocos2d::Rect getRect();

private:

//萝莉精灵

CC_SYNTHESIZE_RETAIN(ActionSprite*,_mySprite,MySprite);

};

Girl.cpp:

//根据提供的type实例化成不同的女主角

Girl* Girl::nodeWithType(int type)

{

Girl* pRet=new Girl();

GameMediator* m = GameMediator::sharedMediator();

Waypoint *waypoint=(Waypoint*)m->getWayPoints().front();

Point pos=waypoint->getMyPosition();

if (pRet && pRet->initWithLocation(pos))

{

pRet->autorelease();

return pRet;

}

else

{

delete pRet;

pRet=NULL;

return false;

}

}

//初始化方法

bool Girl::initWithLocation(cocos2d::Point location)

{

bool bRet=false;

do

{

//实例化一个萝莉

ActionSprite *sprite= Luoli::create();

this->setMySprite(sprite);

//添加精灵到当前Gril中

this->addChild(sprite);

//设置为静止

sprite->idle();

//计算当前萝莉精灵1/2高

int myHeight=sprite->getTextureRect().size.height/2.0f;

//对坐标进行校正提供半个身位高度

location.add(Vec2(0,myHeight));

sprite->setPosition(location);

//把当前女主角添加到游戏的MainScene场景中显示

GameMediator* m = GameMediator::sharedMediator();

m->getNowScene()->addChild(this,10000);

bRet=true;

}

while (0);

return bRet;

}

Rect Girl::getRect()

{

Rect rect = Rect(_mySprite->getPosition().x - _mySprite->getContentSize().width * 0.5f,

_mySprite->getPosition().y - _mySprite->getContentSize().height* 0.5f+20,

_mySprite->getContentSize().width,

_mySprite->getContentSize().height-40);

return rect;

}

第四步:

在MainScene的 startGame(float delta)的方法中加上初始化女主角的代码。

1

2

3

4

5

6

7……

//初始一个女主角

Girl* girl=Girl::nodeWithType(1);

//设置女主角动画为卖萌动画

girl->getMySprite()->walk();

//取消定时器方法,保证startGame只执行一次

this->unschedule(schedule_selector(MainScene::startGame));

到这里,把第一篇中临时添加女主角的代码删除,就可以运行测试游戏了,本篇的任务到此为止,本篇完成后Android真机的运行效果如下:


020903392498779.gif

结束语:

这个塔防游戏系列已经写了3篇了,到现在为止还没有出现炮塔,说好的炮塔呢?请期待下一篇炮塔姑娘的保护神。