1. 程式人生 > 其它 >Cocos2dx 4.0 遊戲開發系列(C++)《國子祭酒的橫板遊戲開發之旅》【二、學習 Part5 】

Cocos2dx 4.0 遊戲開發系列(C++)《國子祭酒的橫板遊戲開發之旅》【二、學習 Part5 】

技術標籤:遊戲c++遊戲開發cocos2d

自我介紹:本人網名國子祭酒,若有同名純屬偶然,本人喜歡歷史,原姓謝,謝氏歷史上淝水之戰成名的謝安是我祖上,謝安父親謝衡 曾經擔任 國子博士,國子祭酒、太子少傅、散騎常侍。對國子祭酒一名稱深覺有意,遂取網名國子祭酒。

技術介紹:一名外行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,用於識別當前碰撞體是什麼,目前這一還是太過簡單的,一旦有大量的敵人的時候,還需要再做整合,好的,下一篇目標還是實現擊打。