書生教你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後,才用到游戲裡去。點擊主界面上的新浪圖標,會進到這裡來。
左邊有四個精靈,其中一個會自動切幀,動來動去的。
右邊有四個按鈕,第一個是返回主菜單。下面三個分別對應靜止的三個精靈。點擊按鈕,精靈顯示會改變,切到下一幀。我們一個一個看。
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不熟悉的朋友老老實實回去看我上一篇講主界面的博文。
剛創建出來應該是這樣的。之後有個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);
setDisplayFrameframe)這個函數的意思是設置一個新的SpeiteFrame改變精靈的顯示。之前說了CCSpriteFrame是對 紋理以及紋理中的顯示區域的描述,叫切片信息更合適。但是由於早期做iphone游戲那會,大家認為iphone內存夠大,往往一個切片就是整個一動畫幀的顯示了沒有分成手,腳,身體來拼湊,全部畫出來了),所以cocos2d-x那幫人把它命名為CCSpriteFrame精靈幀)了。
點擊按鈕或等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));簡單的三句話,就在屏幕上創建了一個實例化動畫對象。
如果點擊按鈕,會執行step)切到下一幀,由於只有一個動作,沒什麼好切換的。
此時我們已經實現了動畫類的基本接口了,可以切換動作,可以靠顯性調用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的功能。到時候就有一個漂亮的動畫類了。最後,書生強調一點:沒有目的的優化是沒有意義的。我們只需要做一個滿足我們游戲需求的組件就可以了,這個動畫類到最後會可以添加很多功能,但是是由於我們有具體的需求,在一次次的版本迭代後,才能成為一個最適合我們這個項目使用的動畫類,想在一開始就憑空寫一個功能強大的組件,只會分散大家的精力,憑空添加一些無用接口罷了。
本文出自 “書生教你cocos2dx” 博客,請務必保留此出處http://luoposhusheng.blog.51cto.com/8148702/1329353