书生教你cocos2d-x-保卫萝卜(二)

      上一章搭建了主界面。这一章开始,我们构建游戏里要用的动画类。动画是游戏开发里很重要的一个概念,可惜的是公司不会安排一个新人去写这一块。许多新人朋友接手代码的时候,这一块已经完成了。他们会以为我要创建动画,只要new一个animation抑或是create一个场景里的对象就行了。这就直接导致当策划告诉你“我希望主角在***的第三帧发出×××”时,新人朋友会回答“什么是帧?”。因此,我们在这个游戏里抛弃cocos2d-x的动画类,写一个属于我们自己的。如果有机会,以后加入切片,到时候把module映射的概念也分享给大家。

大概用2个篇幅记录开发动画类的过程。本文介绍最初的分析,以及cocos2d-x给我们哪些接口,让我们实现动画类。下一篇会给大家一个封装好的动画类,这些测试代码都会删掉。本文相关代码下载地址:

×××地址 http://down.51cto.com/data/1015763

备用地址 http://down.51cto.com/data/1015743

先明确这个几个概念,Animation,Action,Frame。

Animation往往指我们创建出来的可绘制的对象类型,譬如说一个主角。

Action是指动作(不要和ccaction弄混,不一样)。一个Animation有很多动作,譬如主角有***动作,跑步动作。譬如说保卫萝卜里的敌兵只有跑的动作,而塔有待机和***两种动作。萝卜则有个调皮和眨眼的动作。

Frame是帧,一个动作不是一瞬间就没了,也往往不是静止不动的。譬如说***可能是抬刀,挥刀,收刀三帧。即是说这个动作有3帧组成。

切片和module在保卫萝卜这个游戏里用不到,以后再说吧。停留帧什么的,遇到了就告诉大家。

大致可以理解为,一个动画有很多动作,一个动作有好几帧。而如何描述记录这些信息呢,我们不需要记录在创建出来的每个animation对象里。因为就算你创建出N个相同的人物动画,他们有几个动作,每个动作有多少帧,每帧用哪张纹理,这些信息是相同的。这些数据只要一份就行了。叫AnimationData太难听了,我们叫AnimationBase好了。

那么我们动画类的结构就出来了。

1.FrameAnimationBase,这个类记录某个动画的基本信息,描述它有多少动作,动作有哪些帧,用到什么图之类的。

2.FrameAnimation,这个类是根据base创建出来的可见的动画对象。它具有切换动作的接口(play),以及更新显示,让自己从这一帧切换到下一帧的的接口(step)。

如果你下载源码,会发现还有这么一个类。

3.AnimationManager ,这个类是动画管理类,全局一份,本文里没有实现,之后会整理代码,使它具有帮助我们管理动画资源,以及自动更新动画播放的功能。


下面看代码,TestScene是我们专门用于单元测试的场景,我们在这里测试某个功能,确定OK后,才用到游戏里去。点击主界面上的新浪图标,会进到这里来。

p_w_picpath

左边有四个精灵,其中一个会自动切帧,动来动去的。

右边有四个按钮,第一个是返回主菜单。下面三个分别对应静止的三个精灵。点击按钮,精灵显示会改变,切到下一帧。我们一个一个看。

bool TestLayer::init(){
if(!cocos2d::CCLayer::init()){
returnfalse;
    }
    cocos2d::CCSize win_size=cocos2d::CCDirector::sharedDirector()->getWinSize();
    cocos2d::CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("Items04.plist");    
    cocos2d::CCMenu* menu=cocos2d::CCMenu::create();
    menu->setPosition(ccp(0,0));
this->addChild(menu);
    cocos2d::CCMenuItemSprite* bac_btn=GCreateBtnWithFrameSprite("NT-1.png","NT-2.png");
    menu->addChild(bac_btn);
    bac_btn->setPosition(ccp(win_size.width*0.9,win_size.height*0.9));
    bac_btn->setTarget(this,menu_selector(TestLayer::BtnBackCallBack));
    TestStepFrame();
    TestAnimation();
returntrue;
}

首先创建一个返回按钮,点击回主界面,这个不用说。其中的GCreateBtnWithFrameSprite是我在工具类里封装的一个方便我创建按钮的函数,大家有兴趣可以自己看一看。

TestStepFrame()是我们测试帧切换的相关代码,进去看看。

void TestLayer::TestStepFrame(){
    cocos2d::CCSize win_size=cocos2d::CCDirector::sharedDirector()->getWinSize();
    cocos2d::CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("Monsters01.plist");    //测试精灵test_frame_sprite=cocos2d::CCSprite::createWithSpriteFrameName("fly_boss_yellow01.png");this->addChild(test_frame_sprite);
    test_frame_sprite->setPosition(ccp(win_size.width*0.1,win_size.height*0.9));//菜单cocos2d::CCMenu* menu=cocos2d::CCMenu::create();this->addChild(menu);
    menu->setPosition(ccp(0,0));//测试按钮cocos2d::CCMenuItemSprite* btn_step_frame=  GCreateBtnText("stepframe");
    menu->addChild(btn_step_frame);
    btn_step_frame->setPosition(ccp(win_size.width*0.9,win_size.height*0.8));
    btn_step_frame->setTarget(this,menu_selector(TestLayer::btnTestFrameCallBack));//测试精灵autostep_frame_sprite=cocos2d::CCSprite::createWithSpriteFrameName("fly_boss_yellow01.png");this->addChild(autostep_frame_sprite);
    autostep_frame_sprite->setPosition(ccp(win_size.width*0.3,win_size.height*0.9));this->schedule(schedule_selector(TestLayer::Autostepframe),0.3);
}
这里创建了2个精灵,test_frame_sprite和autostep_frame_sprite

由于他们用的图片和切片信息都来自Monsters01.plist,所以我们先手动缓存了这个文件关联的spriteframe。

对spriteframe不熟悉的朋友老老实实回去看我上一篇讲主界面的博文。

p_w_picpath

刚创建出来应该是这样的。之后有个btn_step_frame按钮,对应了一个回调TestLayer::btnTestFrameCallBack。同时这个layer也每0.3也会调用一个回调TestLayer::Autostepframe

看看这两个函数的内容

void TestLayer::btnTestFrameCallBack(cocos2d::CCObject* pSender){static std::string currer_frame="fly_boss_yellow01.png";if(currer_frame=="fly_boss_yellow01.png"){
        cocos2d::CCSpriteFrame* frame=cocos2d::CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName("fly_boss_yellow02.png");
        test_frame_sprite->setDisplayFrame(frame);
        currer_frame="fly_boss_yellow02.png";
    }else{
        cocos2d::CCSpriteFrame* frame=cocos2d::CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName("fly_boss_yellow01.png");
        test_frame_sprite->setDisplayFrame(frame);
        currer_frame="fly_boss_yellow01.png";
    }
}

void TestLayer::Autostepframe(float dt){static std::string currer_frame="fly_boss_yellow01.png";if(currer_frame=="fly_boss_yellow01.png"){
        cocos2d::CCSpriteFrame* frame=cocos2d::CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName("fly_boss_yellow02.png");
        autostep_frame_sprite->setDisplayFrame(frame);
        currer_frame="fly_boss_yellow02.png";
    }else{
        cocos2d::CCSpriteFrame* frame=cocos2d::CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName("fly_boss_yellow01.png");
        autostep_frame_sprite->setDisplayFrame(frame);
        currer_frame="fly_boss_yellow01.png";
    }
}
内容差不多,每次执行回调,会改变精灵对应的纹理和纹理区域,

test_frame_sprite->setDisplayFrame(frame);

setDisplayFrame(frame)这个函数的意思是设置一个新的SpeiteFrame改变精灵的显示。之前说了CCSpriteFrame是对 纹理以及纹理中的显示区域的描述,叫切片信息更合适。但是由于早期做iphone游戏那会,大家认为iphone内存够大,往往一个切片就是整个一动画帧的显示了(没有分成手,脚,身体来拼凑,全部画出来了),所以cocos2d-x那帮人把它命名为CCSpriteFrame(精灵帧)了。

p_w_picpath

点击按钮或等0.3秒,我们的精灵的显示就被刷新了。这个效果就是我们要的帧的切换。不过现在还没有加入动作的概念,所以最多用它做个金币翻转什么的效果,没法做动作。但是这个效果确实实现了帧的切换,之后我们以它为基础,实现动画。


下面我们看testaniamtion(),内容比刚才多点。2个部分,先看前面

//测试stepFrameAnimationBase* base=FrameAnimationBase::create();base->retain();base->AddFrame("run","land_boss_pink01.png");base->AddFrame("run","land_boss_pink02.png");

     ani=FrameAnimation::create(base);this->addChild(ani);
    ani->setPosition(ccp(win_size.width*0.1,win_size.height*0.7));


我们创建了一个AnimationBase,往里面添加了一个叫“run”的动作,这个动作有2帧,之后基于这个base创建创建了一个实例化动画ani

ps(我写代码不用插件,base在我的vs里不变色,大家写代码要注意,不要用容易和关键字混淆的变量名,别用这个变量名)

用着是挺简单,但是可能有人看不懂,我们去看看AnimationBase的结构。

class FrameAnimationBase:public cocos2d::CCObject{
public:
    friend class FrameAnimation;
virtual ~FrameAnimationBase();
static FrameAnimationBase* create();
bool init();
void AddFrame(std::string action_name,cocos2d::CCSpriteFrame* frame);
void AddFrame(std::string action_name,std::string  frame_name);
private:
    FrameAnimationBase();
    cocos2d::CCDictionary* action_dic;
    std::string default_action;
    cocos2d::CCSpriteFrame* GetFrame(std::string action_name,int frame_index);
};


由于保卫萝卜资源的问题,我们这次实现的动画是帧动画,因此我最终把这个动画类的名字加了Frame前缀。

之前说了,FrameAnimationBase是记录并描述一个动画有多少动作,每个动作有哪些帧的数据信息。

因此它有一个动画列表。action_dic,key是动作名字,value是一个帧列表。

default_action是我“画蛇添足”加上去的,因为我们创建的动画对象必须有个默认动作。

AddFrame(动作名,动画帧)是我们添加数据的入口。如果没有动作,会创建一个新动作把帧加进去,如果已有动作,会直接把帧加进去。下面看具体实现。


void FrameAnimationBase::AddFrame(std::string action_name,cocos2d::CCSpriteFrame* frame){
    cocos2d::CCArray* frame_array=dynamic_cast<cocos2d::CCArray*>( action_dic->objectForKey(action_name));
if(frame_array==NULL){
        frame_array=cocos2d::CCArray::create();
        action_dic->setObject(frame_array,action_name);
if(default_action==""){
            default_action=action_name;
        }
    }
    frame_array->addObject(frame);
}
void FrameAnimationBase::AddFrame(std::string action_name,std::string  frame_name){
    cocos2d::CCSpriteFrame* frame=cocos2d::CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(frame_name.c_str());
    AddFrame(action_name,frame);
}

我们要把frame加入一个动作名为action_name的动作里去。

则先会判断有没有这个动作的帧队列,如果没有创建一个新的动作插入动作字典表,然后把帧加入动作对应的帧列表。

ps:1.为了确保每个动作都至少有一帧,我们没开放单独的创建空动作的接口。

       2.这里用的CCArray和CCDictionary数据结构未必是高效的,但是写代码切记“无目的的优化会导致代码写不下去”,同时毕竟是测试代码,我们只在有需求时才去优化。

对应使用base的代码

FrameAnimationBase* base=FrameAnimationBase::create(); base->retain();

base->AddFrame("run","land_boss_pink01.png");

base->AddFrame("run","land_boss_pink02.png");

创建了一个动画base,它有一个run的动作,这个动作有2帧,由于run是它的第一个动作,所以默认动作就是run。


然后看我们是如何创建实例化的Animation对象的。先看Animation的结构

class FrameAnimation :public cocos2d::CCNode{
public:
virtual ~FrameAnimation();
static FrameAnimation* create(FrameAnimationBase* base);
bool init(FrameAnimationBase* base);
void Step();
void Play(std::string action_name);
private:
    FrameAnimation();
    std::string currer_action_name;
int currer_frame_index;
    FrameAnimationBase* base;
    cocos2d::CCSprite* sprite;
};
由于是要加入场景中的可绘制对象,所以继承了CCNode,内部有一个CCSprite,创建函数必须有个AnimationBase为基础。

currer_action_name是当前动作的名字currer_frame_index是当前绘制的帧的索引,这2个变量决定了当前精灵是哪个动作的第几帧

step()是切换帧的接口,而Play()是切片动作的接口。

PS:成员变量base变色的问题,大家自己注意,不要这么命名变量,这么写是不好的,会和关键字混淆。

bool FrameAnimation::init(FrameAnimationBase* base){
if(!cocos2d::CCNode::init()){
returnfalse;
    }
this->base=base;
    std::string default_action=base->default_action;
    currer_action_name=default_action;
    cocos2d::CCSpriteFrame* frame=base->GetFrame(default_action,0);
     sprite=cocos2d::CCSprite::createWithSpriteFrame(frame);
this->addChild(sprite);
returntrue;
}
初始化函数里,根据一个base创建,取得默认动作名,取得这个动作的第一帧,用之前说的方法,创建精灵。

再看切换帧的函数step()

void FrameAnimation::Step(){
    cocos2d::CCArray* frame_array=dynamic_cast<cocos2d::CCArray*>(base->action_dic->objectForKey(currer_action_name));
if(currer_frame_index<frame_array->count()-1){    
        currer_frame_index++;
    }else{
        currer_frame_index=0;
    }
    cocos2d::CCSpriteFrame* frame=base->GetFrame(this->currer_action_name,currer_frame_index);
    sprite->setDisplayFrame(frame);
}
尝试根据动作名和当前帧,从base里取下一帧的信息,切换精灵的显示。
如果已经是最后一帧了,回到第一帧,实现循环播放。
 
    
PS:这里的写法不肯定是不高效的,同时我们也可以把播放方式从循环播放扩充成倒序播放,播放停留在最后一帧等等,但是“没有需求就没有必要优化”。所以就这样就暂时够我们用了。
之后再看切换动作的接口Play(动作名)
void FrameAnimation::Play(std::string action_name){this->currer_action_name=action_name;this->currer_frame_index=0;
    cocos2d::CCSpriteFrame* frame=base->GetFrame(currer_action_name,0);this->sprite->setDisplayFrame(frame);
}

切换动作会改变到对应动作的第一帧,开始播放。



然后看我们实际的使用

ani=FrameAnimation::create(base);
this->addChild(ani);
ani->setPosition(ccp(win_size.width*0.1,win_size.height*0.7));

简单的三句话,就在屏幕上创建了一个实例化动画对象。


p_w_picpath如果点击按钮,会执行step()切到下一帧p_w_picpath,由于只有一个动作,没什么好切换的。

此时我们已经实现了动画类的基本接口了,可以切换动作,可以靠显性调用step()的方法促使它切帧。有不少改进空间。

目前我们游戏需要的改进有2个:文件读取和自动播放以及资源管理。

每次手动去step每个animation是很不爽的,所以要有AnimationManager,这个我们下一篇再说。

今天显示先从文件读取base的雏形,毕竟每次创建一个animationbase都程序去添加内容是不可行。

//从文件读取base    cocos2d::CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("Items01.plist");
    tinyxml2::XMLDocument* doc=new tinyxml2::XMLDocument();
    doc->LoadFile("luobo.xml");
    tinyxml2::XMLElement *ani_node=doc->RootElement();  
    std::string ani_name=ani_node->FirstAttribute()->Value();//aniFrameAnimationBase* luobo_base=FrameAnimationBase::create();//actiontinyxml2::XMLElement *action_node=ani_node->FirstChildElement("action");  while (action_node)  
    {      
        std::string action_name= action_node->FirstAttribute()->Value();        //frametinyxml2::XMLElement *frame_node=action_node->FirstChildElement("frame");while(frame_node){            
            std::string frame_name=frame_node->FirstAttribute()->Value();
            frame_node=frame_node->NextSiblingElement();
            luobo_base->AddFrame(action_name,frame_name);
        }
        action_node=action_node->NextSiblingElement();  
    }  
    luobo_base->retain();



我弄了一个叫“luobo.xml”的文件,里面记录了luobo这个动画的基本信息。然后通过读取它创建了一个AnimationBase。

xml的结构,大家自己用文本编辑器看一看,很简单。

//创建实例luobo_ani=FrameAnimation::create(luobo_base);this->addChild(luobo_ani);
    luobo_ani->setPosition(ccp(win_size.width*0.3,win_size.height*0.7));
    luobo_ani->Play("happy");

    cocos2d::CCMenuItemSprite* btn_step_file_ani=  GCreateBtnText("stepFileAni");
    menu->addChild(btn_step_file_ani);
    btn_step_file_ani->setPosition(ccp(win_size.width*0.9,win_size.height*0.6));
    btn_step_file_ani->setTarget(this,menu_selector(TestLayer::btnTestFileAniCallBack));    
之后根据这个base创建了一个动画实例,并且切换动作到“happy”,还创建了一个按钮,点击会触发它的step
这一段源码里没有,大家可以自己添加。
经测试,运行良好。
到目前为止,这个动画类似乎可以用了,但是代码质量不高。没关系,毕竟只是雏形,这就是测试场景的作用嘛,下一篇我们来优化这些代码,
把读取xml的部分封装进base的创建函数里,再封装一些接口,实现manager的功能。到时候就有一个漂亮的动画类了。
最后,书生强调一点:
没有目的的优化是没有意义的。我们只需要做一个满足我们游戏需求的组件就可以了,这个动画类到最后会可以添加很多功能,但是是由于我们有具体的需求,
在一次次的版本迭代后,才能成为一个最适合我们这个项目使用的动画类,想在一开始就凭空写一个功能强大的组件,只会分散大家的精力,凭空添加一些无用接口罢了。