今天实现读报僵尸,需要对僵尸类进行重写一下,目前的代码结构并不能很好地实现读报僵尸。
class ZombieBase : public Entity
{
SDL_BOOL_SYNTHESIZE(m_bDead,Dead);//是否死亡
SDL_SYNTHESIZE(int,m_nHitPoint,HitPoint);//当前血量
SDL_SYNTHESIZE(float,m_fCriticalPoint,CriticalPoint);//临界点
SDL_SYNTHESIZE(float,m_fBaseSpeed,BaseSpeed);//基础移动速度
SDL_SYNTHESIZE(int,m_nRow,Row);//当前行数
SDL_SYNTHESIZE(string,m_zombieName,ZombieName);//僵尸名称
SDL_SYNTHESIZE(ZombieDir,m_dir,ZombieDir);//僵尸当前移动方向
SDL_SYNTHESIZE(float,m_fColdDownTime,ColdDownTime);//攻击冷却时间
SDL_SYNTHESIZE(int,m_nDamage,Damage);//伤害值
protected:
Terrain*m_pAim;//当前塔基
ZombieDelegate*m_pDelegate;
MoveBehavior*m_pMoveBehavior;
HpBar*m_pHpBar;
Garnishry*m_pGarnishry;//饰品
protected:
static const int ANIMATION_TAG;
public:
ZombieBase();
~ZombieBase();
void setDelegate(ZombieDelegate*pDelegate);
void setAim(Terrain*plant);
Terrain*getAim();
void clearAim();
//设置饰品
void setGarnishry(Garnishry*garnishry);
Garnishry*getGarnishry();
bool isDying()const;
void hurt(int baseDamage,AttackType attackType);
void setMoveBehavior(MoveBehavior*behavior);
MoveBehavior*getMoveBehavior();
//绑定血量条
void bindHpBar(HpBar*hpBar);
virtual float getCurSpeed()const;
virtual Rect getCollisionBoundingBox()const;
virtual void update(float dt);
//活着更新函数
virtual void updateAlive(float dt) = 0;
//移动行为回调函数
virtual void performMove(float dt);
//受伤回调函数
virtual void onHurt();
//吞噬死亡回调函数
virtual void onSwallowDead();
//炸死回调函数
virtual void onBoomDead();
//死亡回调函数
virtual void onNormalDead();
//临界点死亡即为0回调函数
virtual void onCRPDead();
//饰品死亡函数
virtual void onGarnishryDead();
};
临界点是所有僵尸共有的属性、所以把它放到僵尸基类中,同时,僵尸基类提供了很多hook钩子方法,负责对不同的情况做出相应的反应。
void ZombieBase::hurt(int baseDamage,AttackType attackType)
{
int afterDamage = baseDamage;
//调用饰品,吸收伤害
if (m_pGarnishry != nullptr && m_pGarnishry->getHitPoint() > 0)
{
afterDamage = m_pGarnishry->absorbDamage(baseDamage,attackType);
//如果饰品死亡,回调饰品死亡函数
if (m_pGarnishry->getHitPoint() <= 0)
onGarnishryDead();
}
if (afterDamage <= 0)
return ;
auto afterHP = this->getHitPoint() - afterDamage;
bool bDead = false;
if (afterHP <= 0)
{
afterHP = 0;
bDead = true;
}
this->setHitPoint(afterHP);
//设置血量条
if (m_pHpBar != nullptr)
{
m_pHpBar->setValue((float)afterHP);
}
onHurt();
//如果死亡,回调死亡函数
if (bDead)
{
if (attackType == AttackType::Swallow)
{
onSwallowDead();
}
else if (attackType == AttackType::Boom)
{
onBoomDead();
}
else
{
onNormalDead();
}
}
}
后面增加了僵尸的死亡回调函数,分为吞噬(Swallow),炸死(Boom),正常死亡(Normal,然后进行回调不同的方法。
void ZombieBase::bindHpBar(HpBar*hpBar)
{
m_pHpBar = hpBar;
this->addChild(m_pHpBar);
}
以前的代码中,僵尸类只是保存了对饰品的引用而已,这里添加到僵尸类中,因为有的僵尸的饰品是需要显示在游戏中的。
void ZombieBase::update(float dt)
{
//当前僵尸血量小于0,死亡
if (this->getHitPoint() <= 0)
{
auto afterCRP = this->getCriticalPoint() - 0.4f;
this->setCriticalPoint(afterCRP);
//僵尸真正死亡
if (afterCRP <= 0.f)
{
onCRPDead();
}
}
else
{
updateAlive(dt);
}
performMove(dt);
}
update()函数也进行了更新,同样把临界点的操作放到了基类中,并且只有在僵尸还没死亡时才会调用updateAlive()函数。
void ZombieBase::performMove(float dt)
{
if (m_pMoveBehavior)
m_pMoveBehavior->performMove(this);
}
perrformMove也是为了子类做出的扩展。
void ZombieBase::onNormalDead()
{
this->setDead(true);
}
void ZombieBase::onCRPDead()
{
this->setDead(true);
//显示死亡动画
m_pDelegate->showZombieDie(this->getPosition());
}
void ZombieBase::onSwallowDead()
{
this->setDead(true);
}
void ZombieBase::onBoomDead()
{
this->setDead(true);
m_pDelegate->showZombieBoomDie(this->getPosition());
}
void ZombieBase::onGarnishryDead()
{
}
僵尸基类中对虚函数都定义了一个实现,如果子类和父类的需求不同重写这些方法就可以了。
接下来就是僵尸类了,以前的实现是让僵尸一开始处于站立状态,然后在updateAlive()中切换成行走状态,这里打算直接就是运动状态。站立动画只有在画面右移的时候能看到,是为了大概显示出当前关卡的僵尸的种类和相对的数目,我打算使用精灵显示这站立动画而不是僵尸类。
bool Zombie::init(const string&zombieName)
{
this->setZombieName(zombieName);
//设置类型todo
m_nType = 1;
//获取行走状态贴图
auto animationName = StringUtils::format("%sWalk%02d",m_zombieName.c_str(),m_nType);
auto animation = AnimationCache::getInstance()->getAnimation(animationName);
//设置贴图
auto firstFrame = animation->getFrames().front()->getSpriteFrame();
m_pSprite = Sprite::createWithSpriteFrame(firstFrame);
auto size = m_pSprite->getContentSize();
m_pSprite->setPosition(size.width/2,size.height/2);
this->setContentSize(size);
this->addChild(m_pSprite);
//运行动画
auto animate = Animate::create(animation);
animate->setTag(ANIMATION_TAG);
m_pSprite->runAction(animate);
//设置状态
m_state = State::Walk;
return true;
}
这里只是简单地更改了贴图和动画以及状态。
void Zombie::updateAlive(float dt)
{
if (m_state == State::Idle)
return;
//当前存在攻击目标
if (m_pAim != nullptr)
{
if (m_pAim->getInnerPlant() != nullptr)
{
this->changeState(State::Attack);
m_elapsed += dt;
//到达攻击时间
if (m_elapsed >= this->getColdDownTime())
{
m_elapsed -= this->getColdDownTime();
//进行攻击
auto topPlant = m_pDelegate->getTopPlant(m_pAim);
topPlant->hurt(this->getDamage());
}
}
else//重新计时
{
this->clearAim();
m_elapsed = 0.f;
}
}
//没有攻击目标,行走
else if (m_pAim == nullptr)
{
this->changeState(State::Walk);
}
}
updateAlive的更新就是,如果当前处于Idle状态,则不再进行其他的更新,算是对读报僵尸的一些改变。然后就是读报僵尸的实现了,读报僵尸类似于一般的僵尸,只不过多了一个报纸饰品,并且在饰品死亡后会发怒,主要的难题就是这些动画的更改,其他都好说,毕竟僵尸基类和僵尸类都为读报僵尸进行了更改。
class PaperZombie : public Zombie
{
private:
bool m_bIsAngry;//读报僵尸是否生气
public:
PaperZombie();
~PaperZombie();
static PaperZombie*create(const string&zombieName);
bool init(const string&zombieName);
protected:
virtual void changeState(State state);
virtual void showZombieHead();
virtual float getCurSpeed()const;
virtual void onGarnishryDead();
virtual void onCRPDead();
};
读报僵尸是僵尸的子类,这样实现的就没那么多了,注意下面的虚函数,读报僵尸只要写下这些代码就能实现刚才所说的逻辑了,这就是刚才重写的好处,另外项目就是这样,没有哪个代码是一成不变的。
void PaperZombie::changeState(State state)
{
//状态没有发生改变,直接退出
if (m_state == state)
return ;
m_state = state;
string animationName;
int status = m_bIsAngry ? 0 : 1;
if (m_state == State::Walk)
{
animationName = StringUtils::format("%sWalk%d",m_zombieName.c_str(),status);
}
else if (m_state == State::Attack)
{
animationName = StringUtils::format("%sAttack%d",m_zombieName.c_str(),status);
}
else if (m_state == State::WalkDead)
{
animationName = StringUtils::format("%sLostHeadWalk%d",m_zombieName.c_str(),status);
this->showZombieHead();
}
else if (m_state == State::AttackDead)
{
animationName = StringUtils::format("%sLostHeadAttack%d",m_zombieName.c_str(),status);
this->showZombieHead();
}
//改变的状态没有动画,则直接返回
if (animationName.empty())
{
return;
}
//停止原先的动画
this->getSprite()->stopActionByTag(ANIMATION_TAG);
auto animation = AnimationCache::getInstance()->getAnimation(animationName);
auto animate = Animate::create(animation);
animate->setTag(ANIMATION_TAG);
this->getSprite()->runAction(animate);
}
这里需要注意的是,读报僵尸是有两套动画的,一套是发怒前,即报纸还在的情况下,一个是发怒后,这两个的行走,攻击动画都是不同的,其他的则是通用的。
void PaperZombie::showZombieHead()
{
//调整位置
Point bornPos = this->getPosition();
Size size = this->getContentSize();
bornPos.x += size.width/4.f;
m_pDelegate->showPaperZombieHead(bornPos);
}
和僵尸类类似,只不过改变了要显示的是哪个特效
float PaperZombie::getCurSpeed()const
{
auto speed = this->getBaseSpeed();
if (m_bIsAngry)
speed *= 2.f;
return speed;
}
这里表示僵尸发怒后会加速,目前加速一倍。
void PaperZombie::onGarnishryDead()
{
m_bIsAngry = true;
//改为站立状态
this->changeState(State::Idle);
//停止原先动画
m_pSprite->stopActionByTag(ANIMATION_TAG);
//报纸掉落动画
auto animationName = "PaperZombieLostNewspaper";
auto animation = AnimationCache::getInstance()->getAnimation(animationName);
Animate*animate = Animate::create(animation);
animate->setTag(ANIMATION_TAG);
DelayTime*delay = DelayTime::create(animate->getDuration());
CallFunc*end = CallFunc::create([this]()
{
this->changeState(State::Walk);
});
auto seq = Sequence::createWithTwoActions(delay,end);
this->stopAllActions();
//开始动画
m_pSprite->runAction(animate);
this->runAction(seq);
}
这里是饰品死亡后的回调函数,在这里读报僵尸会停止运动,并且会显示报纸掉落动画,之后开始发怒。
void PaperZombie::onCRPDead()
{
this->setDead(true);
//显示死亡动画
m_pDelegate->showPaperZombieDie(this->getPosition());
}
这个则是在临界点,即僵尸彻底死亡后的回调函数,这里调用的也是特效,因为读报僵尸和普通僵尸穿着打扮并不同。接着就是饰品报纸的实现了。
class Paper : public Garnishry
{
SDL_SYNTHESIZE(int,m_nHitPoint,HitPoint);//当前血量
private:
HpBar*m_pHpBar;
public:
Paper();
~Paper();
static Paper*create(int hp);
bool init(int hp);
virtual int absorbDamage(int baseDamage,AttackType attackType);
};
bool Paper::init(int hp)
{
this->setHitPoint(hp);
this->setType(Type::Common);
//添加血量条
m_pHpBar = HpBar::create("hpBar2.png","hpBarBG.png",(float)hp);
auto size = m_pHpBar->getContentSize();
m_pHpBar->setPosition(size.width/2.f,size.height/2.f);
this->addChild(m_pHpBar);
this->setContentSize(size);
return true;
}
int Paper::absorbDamage(int baseDamage,AttackType attackType)
{
int afterDamage = 0;
//跟踪性子弹,无法吸收
if (attackType == AttackType::Track)
{
afterDamage = baseDamage;
}
else//吸收伤害TODO
{
auto afterHP = this->getHitPoint() - baseDamage;
//饰品即将死亡
if (afterHP <= 0)
{
afterDamage = SDL_abs(afterHP);
afterHP = 0;
}
this->setHitPoint(afterHP);
m_pHpBar->setValue((float)afterHP);
}
return afterDamage;
}
报纸中添加了一个血条,表示当前的血量。同时absorbDamage()也需要对以后实现的屋顶类,大喷菇作个扩展,屋顶类植物是可以直接攻击到读报僵尸本身的,而大喷菇这种穿透型植物是一起伤害的。接着就是在ZombieFactory中实现对应的方法进行创建。
PaperZombie*ZombieFactory::createPaperZombie(const string&zombieName)
{
auto zombie = PaperZombie::create(zombieName);
//设置基础属性
zombie->setDead(false);
zombie->setHitPoint(200);
zombie->setBaseSpeed(0.15f);
zombie->setColdDownTime(1.f);
zombie->setDamage(100);
zombie->setCriticalPoint(70.f);
//添加血条
auto pos = this->bindHpBarForZombie(zombie);
//添加报纸装饰品
Paper*paper = Paper::create(150);
auto paperSize = paper->getContentSize();
zombie->setGarnishry(paper);
paper->setPosition(pos - Point(0,paperSize.height));
return zombie;
}
这里创建了一个读报僵尸,并且添加了饰品类。另外就是血条我封装了一个方法,因为所有的僵尸的血条都是类似的。
Point ZombieFactory::bindHpBarForZombie(ZombieBase*zombie)
{
HpBar*hpBar = HpBar::create("hpBar1.png","hpBarBG.png",200.f);
//设置血条位置
auto rect = zombie->getSprite()->getSpriteFrameRect();
Size size = zombie->getContentSize();
Point pos = Point(rect.origin.x + rect.size.width/2,rect.size.height/2 - size.height/2);
hpBar->setPosition(pos);
zombie->bindHpBar(hpBar);
return pos;
}
特效层函数的增加不再叙述。另外,大家有什么意见或者建议的话可以评论,有什么要批评的话也可以说,有则改之,无则加勉。本节截图。
可以看到,读报僵尸有两个血条,一个是报纸的,一个是自己本身的血条,因为目前的豌豆只能先打死报纸才能攻击到读报僵尸,所以这里读报僵尸的血量并没有减少。值得一提的是,读报僵尸卡片也需要添加,这里不再赘述。