【精品教程】Cocos2d-x v3.6制作射箭游戲(二)
本章我們的主要任務是創建射箭的弓箭手(也就是游戲豬腳),并且讓這個豬腳隨著觸摸點的改變不斷的旋轉手中的弓箭。
分析:
對于這個射箭的角色而言,它能不停的射出弓箭。當我們按住屏幕上某點時,會從該角色拿弓箭的手的位置“畫”一條標注箭支運動軌跡的紅線(看似拋物線);當在屏幕上滑動手指或鼠標時,這條紅線會隨著觸摸點的位置不停的變換軌跡;當松開屏幕上的手指或鼠標時,會射出一支弓箭,這支弓箭會按最終的紅線路徑移動。另外,玩家手中的弓箭會隨著屏幕上的手指或鼠標旋轉。
Player 類
下面我們一起來創建這個 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); // 玩家是否正在執行射箭動畫
- private:
- Vec2 playerPos; // 角色在 tmx 地圖上的位置
- Size playerSize; // 角色尺寸
- Size winSize; // 屏幕窗口尺寸
- Sprite* playerbody; // 角色身體
- Sprite* playerarrow; // 角色的弓箭,也就是會隨觸摸點旋轉的弓和箭部分
- Sprite* hPBgSprite; // 角色血條背景精靈
- ProgressTimer* hpBar; // 角色血條
- ccQuadBezierConfig bezier; // 路徑貝賽爾
- DrawNode* drawNode; // 這里表示我們的線條對象
- };
以上的各方法都是我們這兩章需要實現的,其他更多的方法我們將在后面需要的時候再擴充。
其中CC_SYNTHESIZE宏的作用是定義一個保護型的變量,并聲明一個getfunName函數和setfunName函數,你可以用getfunName函數得到變量的值,用setfunName函數設置變量得值。如:CC_SYNTHESIZE(int, playerHp, PlayerHp);定義了一個整型的 playerHp 變量,同時還聲明了 getPlayerHp() 和 setPlayerHp() 兩個方法。
ccQuadBezierConfig是我們新定義的一個結構體,后面我們會詳細的講解。
下面我們就從上到下依次來看看以上的各方法。
創建角色
首先是 Player 的初始化(init)和創建(create),這里我們通過給定 Player 的位置來創建該角色,而這個傳入的坐標位置應該是我們從 TiledMap 的對象層中讀取到的位置(上章有講)。具體代碼如下:
- 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(); // 創建角色
- createPlayerHpBar(); // 創建角色血量條
- scheduleUpdate();
- return true;
- }
下面我們接著來看看 createPlayer 方法,該方法將初始化我們的 Player 角色,代碼如下所示:
- void Player::createPlayer()
- {
- playerbody = Sprite::createWithSpriteFrameName("playerbody.png");
- playerSize = Size(playerbody->getContentSize().width/2, playerbody->getContentSize().height / 3*2);
- // 設置Player的尺寸,大小略小于playerbody的尺寸,這樣利于我們后面更準確的進行碰撞設置。
- 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 方法中我們將創建如下所示的一個游戲角色。
因為沒有找到合適的游戲資源(原游戲中得到的資源都是零件,要使用需要把它們一幀一幀重組),所以我們的游戲一切從簡,不整那些復雜的。
這里我們只把角色簡單分成了兩個部分,第一部分當然是玩家的身體playerbody,第二部分是隨著觸摸點/鼠標旋轉的手和弓箭playerarrow。(PS:當然因為資源限制這個原因,可能會稍稍降低咱游戲的檔次,應該不能怪我啰!O(∩_∩)O~)
設置playerbody位置時,你可能已經發現,我們并沒有把角色身體設置在傳入的playerPos處,而是對它稍微做了一定的調整。這是因為我們傳入的位置它是緊貼本格瓦片底部的(我們制作tmx文件時,需要這樣做。上章沒說清楚,這章補起,要記住哦!)。如下圖所示:
Y值坐標也不可太接近本格瓦片底部,也就是不要設為9.990,9.998這類太接近10的,因為 tmx 文件中存放的坐標值是整數,如果設為9.990,9.998,那么存放的值會是9.990 X 32 = 319.68 = 320,同理 9.998 X 32 也是 320。320 對于瓦片大小是32 X 32的地圖來說是個特殊的數字,因為 320 /32 = 10。這樣在程序中就會誤以為9.990,9.998之類的點是坐標上的第10個點。
而且上章我們也說過,由于分辨率適配的原因,對象組中對象的位置與實際的位置是有一定的偏差的,所以我們在設置角色身體位置時,需要修正這些偏差。
以上代碼中設置位置的原理圖如下:
其中,對象組在 X 軸上的偏移值我們把它保存在了 GameManager 中,而 GameManager 是個單例類,后面章節我們會詳細的講解。當然如果你現在就想運行代碼,那就先把GameManager::getInstance()->getObjectPosOffX()部分去掉吧。
創建好角色后,接下來我們需要創建角色的血量條,血量條可通過 Cocos2d-x 中封裝好的進度條類 ProgressTimer 來創建。其代碼段如下:
- void Player::createPlayerHpBar()
- {
- // 創建血條底,即進度條的底背景
- hPBgSprite = Sprite::createWithSpriteFrameName("hpbg.png");
- hPBgSprite->setPosition(Vec2(playerbody->getContentSize().width / 2, playerbody->getContentSize().height));
- playerbody->addChild(hPBgSprite);
- // 創建血條
- hpBar = ProgressTimer::create(Sprite::createWithSpriteFrameName("hp1.png"));
- hpBar->setType(ProgressTimer::Type::BAR); // 設置進度條樣式(條形或環形)
- hpBar->setMidpoint(Vec2(0, 0.5f)); // 設置進度條的起始點,(0,y)表示最左邊,(1,y)表示最右邊,(x,1)表示最上面,(x,0)表示最下面。
- hpBar->setBarChangeRate(Vec2(1, 0)); // 設置進度條變化方向,(1,0)表示橫方向,(0,1)表示縱方向。
- hpBar->setPercentage(100); // 設置當前進度條的進度
- hpBar->setPosition(Vec2(hPBgSprite->getContentSize().width / 2, hPBgSprite->getContentSize().height / 2 ));
- hPBgSprite->addChild(hpBar);
- hPBgSprite->setVisible(false); // 設置整個血條不可見,我們將在Player 遭受攻擊的時候再顯示血條。
- }
#p#
旋轉角色弓箭
接下來我們來讓 Player 的弓箭部分跟隨著觸摸點/鼠標旋轉。所以我們定義了如下的函數:
- 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方法的參數為觸摸點的位置。
1)獲取角色弓箭在游戲場景中位置;
2)計算弓箭的旋轉角度。
這里利用三角正切函數來計算,原理如下圖所示:
vector(offX,offY) 是觸摸點到弓箭之間的向量,通過 getAngle 方法,我們可以得到 vector 向量與X軸之間的弧度。
再者,我們需要把弧度 rotateRadians 轉化為角度,CC_RADIANS_TO_DEGREES就是能把弧度轉化為角度的宏。轉化時乘 -1 是因為Cocos2d-x中規定順時針方向為正,這與我們計算出的角度方向相反,所以轉化的時候需要把角度a變為-a。
3)控制旋轉角度的范圍,即只讓它在角色右半邊內旋轉。
4)計算弓箭旋轉時間。
speed表示炮塔旋轉的速度,0.5 / M_PI其實就是 1 / 2PI,它表示1秒鐘旋轉1個圓。
rotateDuration表示旋轉特定的角度需要的時間,計算它用弧度乘以速度。
5)讓弓箭執行旋轉動作。
觸摸響應
好了,現在 Player 就初步定義好了。接下來,我們回到游戲場景把Player加入進去,并來測試下弓箭是否跟隨觸摸點旋轉。
在 Cocos2d-x 3.x 引擎中,實現觸摸響應的流程基本是一致的。所以在 3.6 中,其過程依舊是:
- 重載觸摸回調函數;
- 創建并綁定觸摸事件;
- 實現觸摸回調函數。
所以我們要測試弓箭是否跟隨觸摸點旋轉,第一步請先在 GameScene 中重寫如下的觸摸回調函數,并聲明變量:
- virtual bool onTouchBegan(Touch *touch, Event *unused_event); // 開始觸摸屏幕時響應
- virtual void onTouchMoved(Touch *touch, Event *unused_event); // 觸摸屏幕并在屏幕上滑動時響應
- virtual void onTouchEnded(Touch *touch, Event *unused_event); // 觸摸結束時響應
- private:
- Point preTouchPoint; // 上一個觸摸點
- Point currTouchPoint; // 當前觸摸點
接著,我們需要在 GameScene 的 init 初始化函數中創建并綁定觸摸事件,并先隨便創建一個 Player 對象,用于測試。如下:
- SpriteFrameCache::getInstance()->addSpriteFramesWithFile("texture.plist", "texture.pvr.ccz");
- player = Player::create(Vec2(winSize.width / 4, winSize.height/5));
- this->addChild(player);
- // 獲取事件分發器
- auto dispatcher = Director::getInstance()->getEventDispatcher();
- // 創建單點觸摸監聽器
- auto listener = EventListenerTouchOneByOne::create();
- // 讓監聽器綁定事件處理函數
- listener->onTouchBegan = CC_CALLBACK_2(GameScene::onTouchBegan,this);
- listener->onTouchMoved = CC_CALLBACK_2(GameScene::onTouchMoved,this);
- listener->onTouchEnded = CC_CALLBACK_2(GameScene::onTouchEnded,this);
- // 將事件監聽器添加到事件調度器
- dispatcher->addEventListenerWithSceneGraphPriority(listener,this);
Player 的位置是固定的,我們當然不能隨便設,這里只是為了測試。后面的章節中我們會創建一個類來專門管理從 TiledMap 中得到的對象,包括Player、敵人、道具,磚塊等。
以上 plist 和 pvr.ccz文件是我們的打包資源,它們是用 Texturepacker 編輯器打包而來。更多詳細內容請點此查看。
綁定好觸摸事件后,最后我們需要實現它們,代碼如下:
- 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)
- {
- // 射箭,下章內容
- }
在 onTouchBegan 和 onTouchMoved 函數中,處理方法是一樣的。即當當前觸摸點與之前的觸摸點不一致時,就旋轉 Player 的弓箭。
getLocation 方法將 touch 對象中保存的屏幕坐標轉換成我們需要的 Cocos2d 坐標。 分不清屏幕坐標和Cocos2d 坐標的童鞋請參考Cocos2d-x3.0坐標系詳解一文。
當觸摸結束時,Player 對象需要射出弓箭,這個我們暫時不寫。
運行游戲,此時你就可以看到想要的效果了。關于本章資源,請點此下載。