Cocos2dx 4.0 遊戲開發系列(C++)《國子祭酒的橫板遊戲開發之旅》【二、學習 Part5 】
自我介紹:本人網名國子祭酒,若有同名純屬偶然,本人喜歡歷史,原姓謝,謝氏歷史上淝水之戰成名的謝安是我祖上,謝安父親謝衡 曾經擔任 國子博士,國子祭酒、太子少傅、散騎常侍。對國子祭酒一名稱深覺有意,遂取網名國子祭酒。
技術介紹:一名外行C++開發人員,(硬體測試和裝置相關,主要Windows / Mac 搭建一些帶UI的程式,C++ 4+年,Python4+年經驗,Javascript 1+年經驗 , Object-c 2+年經驗)
製作動機:本人特別喜歡橫板類的遊戲,比如FC遊戲熱血系列、Nintendo 掌機上的一些精彩的橫板都是我熱愛的遊戲,但是實實在在的沒有找到一款我個人想象中的橫板闖關型別的遊戲,我決定動用我的資源自己做一個,在此之前Cocos2dx 被我選中,因為free,和C++都是我熟悉的東西,本文只為做遊戲,不在程式碼優勢效能優勢上提供任何內容。
總算抽出了時間,上一次本身計劃直接研究擊打的實現,後來在測試過程中發現之前的程式碼有很多的BUG,經過一番折騰,這一篇主要完善一下上一篇的問題。
再次奉上gif 動圖,感謝Screen To Gif 這種免費強大的工具
內容提要:
1、Hero Animation和運動 切換整合到 一個狀態切換函式,可以叫狀態機,但是目前沒有獨立出來
2、Animation & Physical 共用的時候的亂飛異常解決
3、Hero落地後恢復IDEL狀態使用碰撞實現,所以這裡會提及Physical裡面的碰撞。
1、Hero Animation和運動 切換整合到 一個狀態切換函式
首先為什麼要重寫整合,因為在控制角色做一些運動的時候,如果單純的呼叫doRun / doIdle ...等等給到外面使用的時候就顯得太麻煩了,有了Hero State 一說,請看程式碼(注意 PHY_TYPE 和 playHeroAnimation)
#ifndef __GAMEHERO_LAYER_H__ #define __GAMEHERO_LAYER_H__ #include "cocos2d.h" enum RES_NAME_TYPE { HERO_STATIC, HERO_IDLE, HERO_RUN, HERO_FLY, HERO_JUMP, HERO_UP, HERO_DOWN, HERO_WALK, CONTENT_PLIST, CONTENT_PNG, }; enum PHY_TYPE { PHY_GROUND = 0x01, PHY_OBJECT = 0x02, PHY_BACKGROUND = 0x03, PHY_HERO =0x04, }; class cGameHero : public cocos2d::Layer { private: cocos2d::Sprite *m_HeroSprite = nullptr; std::string m_HeroName = ""; public: // implement the "static create()" method manually CREATE_FUNC(cGameHero); virtual bool init(); static cGameHero* createHero(std::string strHeroName); public: bool initWithHeroName(std::string strHeroName); public: static std::string GetResNameFormat(std::string strHeroName, int nResType, int nIndex = -1); cocos2d::Animation* cGameHero::createHeroAnimation(int nAnimationType); void cGameHero::setHeroState(int nState, int nDirection=1); cocos2d::Sprite * getHeroSprite(); int getHeroState(); void setNeedMoveFlag(bool isMove); bool getNeedMoveFlag(); private: bool m_needMoveFlag = false; float m_Vec = 100.0; float m_Life = 100.0; float m_Level = 0; int m_currentDrection = 1; int m_currentState = HERO_IDLE; void playHeroAnimation(int nType); public: void doChangeSize(float fSize); void doIdle(); void doWalk(int direction); void doRun(int nDircetion = 0); void doJump(); void doDown(); void update(float delt); }; #endif // __GAMEHERO_LAYER_H__
這裡說一下思路,doRun / doJump 位移運動,裡面改成只觸發力或衝量,playHeroAnimation用於播放一個RepeatForever的動畫,如,角色在懸空狀態下播放懸空動畫,跑狀態下播放跑動畫,然後我們提供一個函式專門用於切換Hero的狀態
先看以下函式
void cGameHero::playHeroAnimation(int nType) {
cocos2d::Animation* animation = createHeroAnimation(nType);
animation->setDelayPerUnit(1.0f / 24.0f);
if (m_currentDrection < 0)
{
m_HeroSprite->setFlippedX(true);
}
else
{
m_HeroSprite->setFlippedX(false);
}
auto repet= cocos2d::RepeatForever::create(cocos2d::Animate::create(animation));
m_HeroSprite->stopAllActions();
m_HeroSprite->runAction(repet);
}
void cGameHero::doJump() {
m_HeroSprite->getPhysicsBody()->applyImpulse(m_HeroSprite->getPhysicsBody()->getVelocity() + cocos2d::Vec2(0, 600000));
}
void cGameHero::doWalk(int direction) {
m_HeroSprite->getPhysicsBody()->applyForce(cocos2d::Vec2(500000 * direction, 0));
}
void cGameHero::doRun(int direction) {
if (direction == 0) {
direction = m_currentDrection;
}
m_HeroSprite->getPhysicsBody()->applyForce(cocos2d::Vec2(10000000 * direction, 0));
}
最後我們看一下至關重要的一個狀態切換函式,這個函式用於控制角色的各個狀態切換,目前暫時沒有測試出bug,後續有的話會持續更新
void cGameHero::setHeroState(int nState, int nDirection) {
if (m_currentState == HERO_RUN)
{
if (nState == HERO_RUN)
{
m_currentState = HERO_RUN;
m_currentDrection = nDirection;
}
else if(nState == HERO_DOWN)
{
m_currentState = HERO_DOWN;
setNeedMoveFlag(false);
playHeroAnimation(HERO_DOWN);
}
else if (nState == HERO_IDLE)
{
m_currentState = HERO_IDLE;
setNeedMoveFlag(false);
playHeroAnimation(HERO_IDLE);
}
else if (nState == HERO_FLY)
{
m_currentState = HERO_FLY;
doJump();
playHeroAnimation(HERO_FLY);
}
}
else if(m_currentState == HERO_STATIC || m_currentState == HERO_IDLE)
{
if (nState == HERO_RUN)
{
m_currentState = HERO_RUN;
m_currentDrection = nDirection;
setNeedMoveFlag(true);
playHeroAnimation(HERO_RUN);
}
else if (nState == HERO_IDLE)
{
m_currentState = HERO_IDLE;
playHeroAnimation(HERO_IDLE);
}
else if (nState == HERO_DOWN)
{
m_currentState = HERO_DOWN;
playHeroAnimation(HERO_DOWN);
}
else if (nState == HERO_FLY)
{
m_currentState = HERO_FLY;
doJump();
playHeroAnimation(HERO_FLY);
}
}
else if (m_currentState == HERO_FLY )
{
if (nState == HERO_RUN)
{
m_currentDrection = nDirection;
setNeedMoveFlag(false);
playHeroAnimation(HERO_FLY);
}
else if (nState == HERO_IDLE)
{
m_currentState = HERO_IDLE;
setNeedMoveFlag(false);
playHeroAnimation(HERO_IDLE);
}
else if (nState == HERO_FLY)
{
//留作二段跳功能
}
}
else if(m_currentState == HERO_DOWN)
{
if (nState == HERO_RUN)
{
m_currentState = HERO_RUN;
m_currentDrection = nDirection;
playHeroAnimation(HERO_RUN);
}
else if (nState == HERO_IDLE)
{
m_currentState = HERO_IDLE;
playHeroAnimation(HERO_IDLE);
}
}
}
這裡有一個問題我提出來一下,Hero在跑動時,我們時提供了一個力,用於產生速度,但是有一個問題,就是鍵盤的事件,只能觸發按下和釋放,比如我們控制角色向右走的時候,按著不放會一直運動,放開停止運動,為了實現持續的推動力,還是使用了update()函式,當然這個函式我放在Scene中目前,使用一個需要持續推動力的Flag,來控制是否繼續呼叫doRun,來推動Hero
void cGameStart::update(float delt) {
cocos2d::Scene::update(delt);
for (size_t i = 0; i < 3; i++)
{
getPhysicsWorld()->step(1 / 180.0f);
}
if (m_hero->getNeedMoveFlag())
{
m_hero->doRun();
}
}
2、Animation & Physical 共用的時候的亂飛異常解決
關於這個問題,我搜遍了中文網路,沒有找到有人遇到類似的問題,確實讓我比較意外,不過好在我在Google的時候,找到歪果仁的答案,但是它的方法並沒有解決我的問題,我通過以下方法解決的問題。
這個問題的現象時,Hero 播放Animation的時候,如果是在物理環境下,也就是添加了PhysicalBody並且設定為Dynamic的情況下,Hero會到處亂飛。
根本原因還是我們在使用SpriteFrameCache生成的動畫Frame上,因為frames 每一幀的大小不一樣導致的,最後我在生成動畫幀的時候添加了一句,解決了所有問題
注意這一句:frame->setOriginalSize(m_HeroSprite->getContentSize());//
cocos2d::Animation* cGameHero::createHeroAnimation(int nAnimationType ) {
cocos2d::SpriteFrameCache* spriteCache = cocos2d::SpriteFrameCache::getInstance();
cocos2d::Vector<cocos2d::SpriteFrame*> idleframes = cocos2d::Vector<cocos2d::SpriteFrame*>();
int nFrameSize = 1;
for (int i = 0; i < nFrameSize; i++)
{
std::string szImageFileName = GetResNameFormat(m_HeroName, nAnimationType, i+1);
cocos2d::SpriteFrame* frame = spriteCache->getSpriteFrameByName(szImageFileName);
if (frame)
{
frame->setOriginalSize(m_HeroSprite->getContentSize());//這句可以解決亂飛問題!
frame->setAnchorPoint(cocos2d::Vec2(0.5, 0.5));
idleframes.pushBack(frame);
nFrameSize += 1;
}
else
{
break;
}
}
cocos2d::Animation* ani = cocos2d::Animation::createWithSpriteFrames(idleframes);
return ani;
}
3、Hero落地後恢復IDEL狀態使用碰撞實現,所以這裡會提及Physical裡面的碰撞。
這一部分,主要是想說一下碰撞,包括如何觸發,我覺得最重要的是對3個Mask的理解,我先丟擲一個列舉定義
enum PHY_TYPE {
PHY_GROUND = 0x01,
PHY_OBJECT = 0x02,
PHY_BACKGROUND = 0x03,
PHY_HERO =0x04,
};
之所以寫這個,是為了後續方便設定一些物體的類別和掩碼,並且簡單的通過這個來理解會發生什麼,實際上我覺得官網的解釋太官方了。。。。
physicsBody->setCategoryBitmask(PHY_OBJECT | PHY_HERO); //設定分類掩碼
physicsBody->setContactTestBitmask(PHY_OBJECT | PHY_GROUND); //設定接觸掩碼
physicsBody->setCollisionBitmask(PHY_OBJECT | PHY_GROUND); //設定碰撞掩碼
我這樣寫,你會容易理解一點,現在我來解釋一下。
a、setCategoryBitmask(PHY_OBJECT | PHY_HERO); 表示設定我是什麼,我是OBJECT也是 HERO
b、setContactTestBitmask(PHY_OBJECT | PHY_GROUND); //告訴引擎,可以觸發onContact函式的只有對方是OBJECT 和 GROUND的時候才會觸發
c、setCollisionBitmask(PHY_OBJECT | PHY_GROUND); //告訴引擎,可以和我發生碰撞的 對方要求是OBJECT 和 GROUND
d、那麼這個碰撞和onContact有什麼區別??區別就是onContact只是我們需要執行的程式碼,Collision則將是兩個物體之間發生碰撞發生的運動,比如被彈飛之類的
OK,我還是奉上程式碼,這部分實現了Hero 所謂的在空中還是不在空中的狀態切換
void cGameStart::onEnterTransitionDidFinish()
{
auto lisener = EventListenerPhysicsContact::create();
lisener->onContactBegin = CC_CALLBACK_1(cGameStart::onContectBegin, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(lisener, this);
m_hero = cGameHero::createHero("kkx");
m_hero->setPosition(Vec2(100, 50));
addChild(m_hero, 2, "hero");
}
bool cGameStart::onContectBegin(cocos2d::PhysicsContact& contact) {
auto sp1 = (Sprite*)contact.getShapeA()->getBody()->getNode();
auto sp2 = (Sprite*)contact.getShapeB()->getBody()->getNode();
if (sp1->getTag() == PHY_HERO) {
m_hero->setHeroState(HERO_IDLE);
}
if (sp2->getTag() == PHY_HERO) {
m_hero->setHeroState(HERO_IDLE);
}
return true;
}
這裡面使用了getTag()和setTag,用於識別當前碰撞體是什麼,目前這一還是太過簡單的,一旦有大量的敵人的時候,還需要再做整合,好的,下一篇目標還是實現擊打。