【精品教程】Cocos2d-x v3.6制作射箭游戲(二)
本章我們的主要任務(wù)是創(chuàng)建射箭的弓箭手(也就是游戲豬腳),并且讓這個(gè)豬腳隨著觸摸點(diǎn)的改變不斷的旋轉(zhuǎn)手中的弓箭。
分析:
對(duì)于這個(gè)射箭的角色而言,它能不停的射出弓箭。當(dāng)我們按住屏幕上某點(diǎn)時(shí),會(huì)從該角色拿弓箭的手的位置“畫”一條標(biāo)注箭支運(yùn)動(dòng)軌跡的紅線(看似拋物線);當(dāng)在屏幕上滑動(dòng)手指或鼠標(biāo)時(shí),這條紅線會(huì)隨著觸摸點(diǎn)的位置不停的變換軌跡;當(dāng)松開屏幕上的手指或鼠標(biāo)時(shí),會(huì)射出一支弓箭,這支弓箭會(huì)按最終的紅線路徑移動(dòng)。另外,玩家手中的弓箭會(huì)隨著屏幕上的手指或鼠標(biāo)旋轉(zhuǎn)。
Player 類
下面我們一起來創(chuàng)建這個(gè) Player 豬腳類,其初步定義如下:
- class Player: public Sprite
- {
- public:
- Player();
- bool init(Vec2 playerPos);
- static Player* create(Vec2 playerPos);
- void createPlayer();
- void createPlayerHpBar();
- void rotateArrow(Point touchPoint);
- void createAndShootArrow( Point touchPoint);
- void shootArrow();
- void finishRunAction();
- void update(float dt);
- CC_SYNTHESIZE(int, playerHp, PlayerHp); // 玩家血量值
- CC_SYNTHESIZE(bool, startDraw, StartDraw); // 是否開始畫紅色的路徑線
- CC_SYNTHESIZE(bool, isRunAction, IsRunAction); // 玩家是否正在執(zhí)行射箭動(dòng)畫
- private:
- Vec2 playerPos; // 角色在 tmx 地圖上的位置
- Size playerSize; // 角色尺寸
- Size winSize; // 屏幕窗口尺寸
- Sprite* playerbody; // 角色身體
- Sprite* playerarrow; // 角色的弓箭,也就是會(huì)隨觸摸點(diǎn)旋轉(zhuǎn)的弓和箭部分
- Sprite* hPBgSprite; // 角色血條背景精靈
- ProgressTimer* hpBar; // 角色血條
- ccQuadBezierConfig bezier; // 路徑貝賽爾
- DrawNode* drawNode; // 這里表示我們的線條對(duì)象
- };
以上的各方法都是我們這兩章需要實(shí)現(xiàn)的,其他更多的方法我們將在后面需要的時(shí)候再擴(kuò)充。
其中CC_SYNTHESIZE宏的作用是定義一個(gè)保護(hù)型的變量,并聲明一個(gè)getfunName函數(shù)和setfunName函數(shù),你可以用getfunName函數(shù)得到變量的值,用setfunName函數(shù)設(shè)置變量得值。如:CC_SYNTHESIZE(int, playerHp, PlayerHp);定義了一個(gè)整型的 playerHp 變量,同時(shí)還聲明了 getPlayerHp() 和 setPlayerHp() 兩個(gè)方法。
ccQuadBezierConfig是我們新定義的一個(gè)結(jié)構(gòu)體,后面我們會(huì)詳細(xì)的講解。
下面我們就從上到下依次來看看以上的各方法。
創(chuàng)建角色
首先是 Player 的初始化(init)和創(chuàng)建(create),這里我們通過給定 Player 的位置來創(chuàng)建該角色,而這個(gè)傳入的坐標(biāo)位置應(yīng)該是我們從 TiledMap 的對(duì)象層中讀取到的位置(上章有講)。具體代碼如下:
- Player * Player::create(Vec2 playerPos)
- {
- Player *pRet = new Player();
- if (pRet && pRet->init(playerPos))
- {
- pRet->autorelease();
- return pRet;
- }else
- {
- delete pRet;
- pRet = NULL;
- return NULL;
- }
- }
- bool Player::init(Vec2 playerPos)
- {
- if (!Sprite::init())
- {
- return false;
- }
- this->playerPos = playerPos;
- createPlayer(); // 創(chuàng)建角色
- createPlayerHpBar(); // 創(chuàng)建角色血量條
- scheduleUpdate();
- return true;
- }
下面我們接著來看看 createPlayer 方法,該方法將初始化我們的 Player 角色,代碼如下所示:
- void Player::createPlayer()
- {
- playerbody = Sprite::createWithSpriteFrameName("playerbody.png");
- playerSize = Size(playerbody->getContentSize().width/2, playerbody->getContentSize().height / 3*2);
- // 設(shè)置Player的尺寸,大小略小于playerbody的尺寸,這樣利于我們后面更準(zhǔn)確的進(jìn)行碰撞設(shè)置。
- playerbody->setAnchorPoint(Vec2(0.7f, 0.4f));
- this->addChild(playerbody);
- this->setPosition(Vec2(playerPos.x+ GameManager::getInstance()->getObjectPosOffX(), playerPos.y + playerSize.height * 0.4f));
- playerarrow = Sprite::createWithSpriteFrameName("playerarrow.png");
- playerarrow->setPosition(Vec2(0, 0));
- playerarrow->setAnchorPoint(Vec2(0.3f, 0.5f));
- this->addChild(playerarrow);
- }
createPlayer 方法中我們將創(chuàng)建如下所示的一個(gè)游戲角色。

因?yàn)闆]有找到合適的游戲資源(原游戲中得到的資源都是零件,要使用需要把它們一幀一幀重組),所以我們的游戲一切從簡(jiǎn),不整那些復(fù)雜的。
這里我們只把角色簡(jiǎn)單分成了兩個(gè)部分,第一部分當(dāng)然是玩家的身體playerbody,第二部分是隨著觸摸點(diǎn)/鼠標(biāo)旋轉(zhuǎn)的手和弓箭playerarrow。(PS:當(dāng)然因?yàn)橘Y源限制這個(gè)原因,可能會(huì)稍稍降低咱游戲的檔次,應(yīng)該不能怪我啰!O(∩_∩)O~)
設(shè)置playerbody位置時(shí),你可能已經(jīng)發(fā)現(xiàn),我們并沒有把角色身體設(shè)置在傳入的playerPos處,而是對(duì)它稍微做了一定的調(diào)整。這是因?yàn)槲覀儌魅氲奈恢盟蔷o貼本格瓦片底部的(我們制作tmx文件時(shí),需要這樣做。上章沒說清楚,這章補(bǔ)起,要記住哦!)。如下圖所示:

Y值坐標(biāo)也不可太接近本格瓦片底部,也就是不要設(shè)為9.990,9.998這類太接近10的,因?yàn)?tmx 文件中存放的坐標(biāo)值是整數(shù),如果設(shè)為9.990,9.998,那么存放的值會(huì)是9.990 X 32 = 319.68 = 320,同理 9.998 X 32 也是 320。320 對(duì)于瓦片大小是32 X 32的地圖來說是個(gè)特殊的數(shù)字,因?yàn)?320 /32 = 10。這樣在程序中就會(huì)誤以為9.990,9.998之類的點(diǎn)是坐標(biāo)上的第10個(gè)點(diǎn)。
而且上章我們也說過,由于分辨率適配的原因,對(duì)象組中對(duì)象的位置與實(shí)際的位置是有一定的偏差的,所以我們?cè)谠O(shè)置角色身體位置時(shí),需要修正這些偏差。
以上代碼中設(shè)置位置的原理圖如下:

其中,對(duì)象組在 X 軸上的偏移值我們把它保存在了 GameManager 中,而 GameManager 是個(gè)單例類,后面章節(jié)我們會(huì)詳細(xì)的講解。當(dāng)然如果你現(xiàn)在就想運(yùn)行代碼,那就先把GameManager::getInstance()->getObjectPosOffX()部分去掉吧。
創(chuàng)建好角色后,接下來我們需要?jiǎng)?chuàng)建角色的血量條,血量條可通過 Cocos2d-x 中封裝好的進(jìn)度條類 ProgressTimer 來創(chuàng)建。其代碼段如下:
- void Player::createPlayerHpBar()
- {
- // 創(chuàng)建血條底,即進(jìn)度條的底背景
- hPBgSprite = Sprite::createWithSpriteFrameName("hpbg.png");
- hPBgSprite->setPosition(Vec2(playerbody->getContentSize().width / 2, playerbody->getContentSize().height));
- playerbody->addChild(hPBgSprite);
- // 創(chuàng)建血條
- hpBar = ProgressTimer::create(Sprite::createWithSpriteFrameName("hp1.png"));
- hpBar->setType(ProgressTimer::Type::BAR); // 設(shè)置進(jìn)度條樣式(條形或環(huán)形)
- hpBar->setMidpoint(Vec2(0, 0.5f)); // 設(shè)置進(jìn)度條的起始點(diǎn),(0,y)表示最左邊,(1,y)表示最右邊,(x,1)表示最上面,(x,0)表示最下面。
- hpBar->setBarChangeRate(Vec2(1, 0)); // 設(shè)置進(jìn)度條變化方向,(1,0)表示橫方向,(0,1)表示縱方向。
- hpBar->setPercentage(100); // 設(shè)置當(dāng)前進(jìn)度條的進(jìn)度
- hpBar->setPosition(Vec2(hPBgSprite->getContentSize().width / 2, hPBgSprite->getContentSize().height / 2 ));
- hPBgSprite->addChild(hpBar);
- hPBgSprite->setVisible(false); // 設(shè)置整個(gè)血條不可見,我們將在Player 遭受攻擊的時(shí)候再顯示血條。
- }
#p#
旋轉(zhuǎn)角色弓箭
接下來我們來讓 Player 的弓箭部分跟隨著觸摸點(diǎn)/鼠標(biāo)旋轉(zhuǎn)。所以我們定義了如下的函數(shù):
- void Player::rotateArrow(Point touchPoint)
- {
- // 1
- auto playerPos = this->getPosition();
- auto pos = playerPos + playerarrow->getPosition();
- // 2
- Point vector = touchPoint - pos;
- auto rotateRadians = vector.getAngle();
- auto rotateDegrees = CC_RADIANS_TO_DEGREES( -1 * rotateRadians);
- // 3
- if (rotateDegrees >= -180 && rotateDegrees <= -90){
- rotateDegrees = -90;
- }
- else if (rotateDegrees >= 90 && rotateDegrees <= 180){
- rotateDegrees = 90;
- }
- // 4
- auto speed = 0.5 / M_PI;
- auto rotateDuration = fabs(rotateRadians * speed);
- // 5
- playerarrow->runAction( RotateTo::create(rotateDuration, rotateDegrees));
- }
rotateArrow方法的參數(shù)為觸摸點(diǎn)的位置。
1)獲取角色弓箭在游戲場(chǎng)景中位置;
2)計(jì)算弓箭的旋轉(zhuǎn)角度。
這里利用三角正切函數(shù)來計(jì)算,原理如下圖所示:

vector(offX,offY) 是觸摸點(diǎn)到弓箭之間的向量,通過 getAngle 方法,我們可以得到 vector 向量與X軸之間的弧度。
再者,我們需要把弧度 rotateRadians 轉(zhuǎn)化為角度,CC_RADIANS_TO_DEGREES就是能把弧度轉(zhuǎn)化為角度的宏。轉(zhuǎn)化時(shí)乘 -1 是因?yàn)镃ocos2d-x中規(guī)定順時(shí)針方向?yàn)檎@與我們計(jì)算出的角度方向相反,所以轉(zhuǎn)化的時(shí)候需要把角度a變?yōu)?a。
3)控制旋轉(zhuǎn)角度的范圍,即只讓它在角色右半邊內(nèi)旋轉(zhuǎn)。
4)計(jì)算弓箭旋轉(zhuǎn)時(shí)間。
speed表示炮塔旋轉(zhuǎn)的速度,0.5 / M_PI其實(shí)就是 1 / 2PI,它表示1秒鐘旋轉(zhuǎn)1個(gè)圓。
rotateDuration表示旋轉(zhuǎn)特定的角度需要的時(shí)間,計(jì)算它用弧度乘以速度。
5)讓弓箭執(zhí)行旋轉(zhuǎn)動(dòng)作。
觸摸響應(yīng)
好了,現(xiàn)在 Player 就初步定義好了。接下來,我們回到游戲場(chǎng)景把Player加入進(jìn)去,并來測(cè)試下弓箭是否跟隨觸摸點(diǎn)旋轉(zhuǎn)。
在 Cocos2d-x 3.x 引擎中,實(shí)現(xiàn)觸摸響應(yīng)的流程基本是一致的。所以在 3.6 中,其過程依舊是:
- 重載觸摸回調(diào)函數(shù);
- 創(chuàng)建并綁定觸摸事件;
- 實(shí)現(xiàn)觸摸回調(diào)函數(shù)。
所以我們要測(cè)試弓箭是否跟隨觸摸點(diǎn)旋轉(zhuǎn),第一步請(qǐng)先在 GameScene 中重寫如下的觸摸回調(diào)函數(shù),并聲明變量:
- virtual bool onTouchBegan(Touch *touch, Event *unused_event); // 開始觸摸屏幕時(shí)響應(yīng)
- virtual void onTouchMoved(Touch *touch, Event *unused_event); // 觸摸屏幕并在屏幕上滑動(dòng)時(shí)響應(yīng)
- virtual void onTouchEnded(Touch *touch, Event *unused_event); // 觸摸結(jié)束時(shí)響應(yīng)
- private:
- Point preTouchPoint; // 上一個(gè)觸摸點(diǎn)
- Point currTouchPoint; // 當(dāng)前觸摸點(diǎn)
接著,我們需要在 GameScene 的 init 初始化函數(shù)中創(chuàng)建并綁定觸摸事件,并先隨便創(chuàng)建一個(gè) Player 對(duì)象,用于測(cè)試。如下:
- SpriteFrameCache::getInstance()->addSpriteFramesWithFile("texture.plist", "texture.pvr.ccz");
- player = Player::create(Vec2(winSize.width / 4, winSize.height/5));
- this->addChild(player);
- // 獲取事件分發(fā)器
- auto dispatcher = Director::getInstance()->getEventDispatcher();
- // 創(chuàng)建單點(diǎn)觸摸監(jiān)聽器
- auto listener = EventListenerTouchOneByOne::create();
- // 讓監(jiān)聽器綁定事件處理函數(shù)
- listener->onTouchBegan = CC_CALLBACK_2(GameScene::onTouchBegan,this);
- listener->onTouchMoved = CC_CALLBACK_2(GameScene::onTouchMoved,this);
- listener->onTouchEnded = CC_CALLBACK_2(GameScene::onTouchEnded,this);
- // 將事件監(jiān)聽器添加到事件調(diào)度器
- dispatcher->addEventListenerWithSceneGraphPriority(listener,this);
Player 的位置是固定的,我們當(dāng)然不能隨便設(shè),這里只是為了測(cè)試。后面的章節(jié)中我們會(huì)創(chuàng)建一個(gè)類來專門管理從 TiledMap 中得到的對(duì)象,包括Player、敵人、道具,磚塊等。
以上 plist 和 pvr.ccz文件是我們的打包資源,它們是用 Texturepacker 編輯器打包而來。更多詳細(xì)內(nèi)容請(qǐng)點(diǎn)此查看。
綁定好觸摸事件后,最后我們需要實(shí)現(xiàn)它們,代碼如下:
- bool GameScene::onTouchBegan(Touch *touch, Event *unused_event)
- {
- currTouchPoint = touch->getLocation();
- if( !currTouchPoint.equals(preTouchPoint)){
- player->rotateArrow(currTouchPoint);
- }
- preTouchPoint = currTouchPoint;
- return true;
- }
- void GameScene::onTouchMoved(Touch *touch, Event *unused_event)
- {
- currTouchPoint = touch->getLocation();
- if( !currTouchPoint.equals(preTouchPoint)){
- player->rotateArrow(currTouchPoint);
- }
- preTouchPoint = currTouchPoint;
- }
- void GameScene::onTouchEnded(Touch *touch, Event *unused_event)
- {
- // 射箭,下章內(nèi)容
- }
在 onTouchBegan 和 onTouchMoved 函數(shù)中,處理方法是一樣的。即當(dāng)當(dāng)前觸摸點(diǎn)與之前的觸摸點(diǎn)不一致時(shí),就旋轉(zhuǎn) Player 的弓箭。
getLocation 方法將 touch 對(duì)象中保存的屏幕坐標(biāo)轉(zhuǎn)換成我們需要的 Cocos2d 坐標(biāo)。 分不清屏幕坐標(biāo)和Cocos2d 坐標(biāo)的童鞋請(qǐng)參考Cocos2d-x3.0坐標(biāo)系詳解一文。
當(dāng)觸摸結(jié)束時(shí),Player 對(duì)象需要射出弓箭,這個(gè)我們暫時(shí)不寫。
運(yùn)行游戲,此時(shí)你就可以看到想要的效果了。關(guān)于本章資源,請(qǐng)點(diǎn)此下載。


















