【前言和思路整理】
千呼萬喚Shǐ出來!終於到最后一章啦~
很抱歉這一章卡了那么久才發布。主要原因是家里電腦主板的內存插槽炸了,返廠后這周才收到,平時在公司也基本沒什么時間寫……再次表示歉意。
上一章我們實現了用戶輸入、打擊判定和音效播放的功能,讓游戲可以玩起來了。這一章我們加上一些附屬的UI和特效,把游戲界面做完善。
本章的難點是沒有什么難點,基本上是往現有功能上做一些簡單的添磚加瓦。在這一章中,我們需要實現如下功能:
1、分數的顯示
2、Combo數和打擊判定的顯示
3、打擊特效
4、彈出對話框的實現
本章的模塊設計簡圖如下:

要實現的功能不多,主要需求為圖像資源。對話框是一個較為獨立的低耦合模塊,就不放在設計圖中了。
【UI層分析與制作】
以一張游戲的截圖來分析,UI層包含這些東西:

圖中的“得分增加”是卡牌技能觸發的表現,這個項目中不會有,就無視掉吧。
這些玩意都是用CocoStudio可以直接制作的。於是打開CocoStudio的UI Editor(貌似官方都發布2.x的版本了?我這個還是1.6的落后版),然后把資源拖進去拼好:
★Cocos2d-x 3.2有個不知道算不算Bug的問題:如果UI層的“交互”選項被勾上了,那么這個UI層會吞噬掉所有的觸摸消息。所以這里需要把LiveScene畫布的Panel_14層的“交互”的鈎去掉。
注意畫布大小設置為960×640。打擊判定和得分特效都在這里,只是被隱藏了。本文最后的附件中會附上UI工程文件。為了表示我用的資源不是從LL里解密出來的,這里的UI和LL的稍微有一些差別。
還有一點是分數條和血條在不同數值的時候顏色不同,需要根據當前進度條的百分比更換材質。更換材質的功能由代碼完成,所以UI編輯器不會用到條的不同顏色的資源,所以最后將工程導出的時候需要把沒有添加到界面上的圖像也打進去。用CocoStudio的“導出全部大圖”會把fnt圖字的資源也打進去,所以推薦使用Texture Packer,手動把要打包的圖整合出來。如何使用Texture Packer在后文中有講解。
中間的裝飾動畫用CocoStudio的AnimationEditor制作(體力值過低的時候動畫會變,所以動作列表中有兩個):

從第二章的視頻中可以看到Combo數變化、分數變化時,得分特效會播放,“Combo”字符、數字和得分判定都會發生縮放和透明度變化。雖然可以用ScaleTo + FadeOut來實現,但是每當一個Action創建時,Cocos2d-x底層就會創建一個線程(看VS的輸出窗口)。如果物件比較密集,就會頻繁地改變Combo數,進而頻繁地創建和銷毀線程。要盡量避免這樣的操作。所以對於物件的縮放、淡出處理都放在Update方法中,不會有線程開銷。所以應當將這些操作放在主線程中,不使用Action。打擊判定的圖像和得分特效也是同理。於是可以在項目中添加LiveSceneUI類,綁定控件留出接口。
綁定控件時,Cocos2d-x默認的GetChildrenByName只能找當前結點的一級子節點,要找子節點的子節點就得寫鏈式調GetChildrenByName的代碼,感覺略蛋疼。所以這里借鑒Unity3D中的Transform.Find方法的模式(Find方法的字符串參數可以是"a/b/c"這樣像文件路徑的格式),封裝了一個類似的方法。這個方法放在Common.h中:
inline cocos2d::Node* Find(cocos2d::Node* pParent, const std::string& pName)
{
std::vector<std::string> nameList;
// 以'/'號分割字符串
//
size_t last = 0;
size_t index = pName.find_first_of("/", last);
while (index != std::string::npos)
{
nameList.push_back(pName.substr(last, index - last));
last = index + 1;
index = pName.find_first_of("/", last);
}
if (index - last > 0)
{
nameList.push_back(pName.substr(last, index - last));
}
// 查找子節點
//
auto ret = pParent;
for (int i = 0; i < nameList.size(); i++)
{
ret = ret->getChildByName(nameList.at(i));
if (ret == nullptr)
{
std::ostringstream oss;
oss << "Child: ";
for (int j = 0; j <= i; j++)
{
oss << nameList.at(j) << "/";
}
oss << " Not found";
cocos2d::log(oss.str().c_str());
return ret;
}
}
return ret;
}
接下來編寫LiveSceneUI的代碼。我們不希望在游戲暫停的時候特效還在繼續播放,所以需要提供一個接口供LiveController的暫停和繼續放方法中使用。代碼如下:
#ifndef __LIVE_SCENE_UI_H__
#define __LIVE_SCENE_UI_H__
#include "cocos2d.h"
#include "ui/CocosGUI.h"
#include "editor-support/cocostudio/CocoStudio.h"
#include "HitJudger.h"
USING_NS_CC;
using namespace cocos2d::ui;
using namespace cocostudio;
class LiveSceneUI : public Layer
{
public:
CREATE_FUNC(LiveSceneUI);
~LiveSceneUI(){}
public:
void SetScore(int pValue, float pPercent, bool pShowEffect = true);
void SetVIT(int pValue, float pPercent);
void SetCombo(int pValue);
void SetJudgement(const HitJudgeType& pValue);
public:
void Pause();
void Resume();
public:
void UpdateUI();
private:
LiveSceneUI();
private:
bool init();
void PauseOnClicked(Ref* sender, Widget::TouchEventType type);
private:
// 分數
//
LoadingBar* m_pBar_Score;
TextBMFont* m_pText_Score;
ImageView* m_pImageView_ScoreBarEffect;
ImageView* m_pImageView_ScoreEffect;
// 體力
//
LoadingBar* m_pBar_VIT;
TextBMFont* m_pText_VIT;
ImageView* m_pImageView_VIT;
// Combo數
//
TextBMFont* m_pText_Combo;
ImageView* m_pImageView_Combo;
// 打擊判定
//
ImageView* m_pImageView_Perfect;
ImageView* m_pImageView_Great;
ImageView* m_pImageView_Good;
ImageView* m_pImageView_Bad;
ImageView* m_pImageView_Miss;
// 裝飾動畫
//
Armature* m_pArmature_Ornament;
private:
ImageView* m_pImageView_CurJudge;
GLubyte m_nEffectAlpha;
GLubyte m_nJudgeAlpha;
float m_fComboScale;
std::string m_LastScoreBar;
int m_nLastOrnIndex;
bool m_bIsPausing;
};
#endif // __LIVE_SCENE_UI_H__
實現:
#include "LiveSceneUI.h"
#include "GameModule.h"
#include "Common.h"
#define ORNAMENT_INDEX_NORMAL 0
#define ORNAMENT_INDEX_FAST 1
LiveSceneUI::LiveSceneUI()
: m_pImageView_CurJudge(nullptr)
, m_nEffectAlpha(0)
, m_nJudgeAlpha(0)
, m_fComboScale(1)
, m_LastScoreBar("ui_bar_score_c.png")
, m_nLastOrnIndex(ORNAMENT_INDEX_NORMAL)
, m_bIsPausing(false)
{
}
bool LiveSceneUI::init()
{
if (!Layer::init())
{
return false;
}
// 音符裝飾動畫
//
ArmatureDataManager::getInstance()->addArmatureFileInfo("UI/UIOrnament/UIOrnament.ExportJson");
this->m_pArmature_Ornament = Armature::create("UIOrnament");
this->m_pArmature_Ornament->setPosition(Vec2(480, 480));
this->addChild(this->m_pArmature_Ornament);
this->m_pArmature_Ornament->getAnimation()->playWithIndex(ORNAMENT_INDEX_NORMAL);
// UI控件
//
auto uiWidget = GUIReader::getInstance()->widgetFromJsonFile("UI/EasyLiveUI_LiveScene.ExportJson");
this->addChild(uiWidget);
this->m_pBar_Score = (LoadingBar *)Find(uiWidget, "Image_ScorePanel/ProgressBar_Score");
this->m_pBar_VIT = (LoadingBar *)Find(uiWidget, "Image_VITBar_BG/ProgressBar_VIT");
this->m_pText_Score = (TextBMFont *)Find(uiWidget, "Image_ScorePanel/BitmapLabel_Score");
this->m_pText_VIT = (TextBMFont *)Find(uiWidget, "Image_VITBar_BG/BitmapLabel_VIT");
this->m_pImageView_ScoreBarEffect = (ImageView *)Find(uiWidget, "Image_ScorePanel/Image_ScoreBarEffect");
this->m_pImageView_ScoreEffect = (ImageView *)Find(uiWidget, "Image_ScorePanel/Image_ScoreEffect");
this->m_pImageView_Perfect = (ImageView *)Find(uiWidget, "Image_Judge_Perfect");
this->m_pImageView_Great = (ImageView *)Find(uiWidget, "Image_Judge_Great");
this->m_pImageView_Good = (ImageView *)Find(uiWidget, "Image_Judge_Good");
this->m_pImageView_Bad = (ImageView *)Find(uiWidget, "Image_Judge_Bad");
this->m_pImageView_Miss = (ImageView *)Find(uiWidget, "Image_Judge_Miss");
this->m_pText_Combo = (TextBMFont *)Find(uiWidget, "BitmapLabel_Combo");
this->m_pImageView_Combo = (ImageView *)Find(uiWidget, "Image_Combo");
auto pauseBtn = (Button *)Find(uiWidget, "Button_Pause");
pauseBtn->addTouchEventListener(CC_CALLBACK_2(LiveSceneUI::PauseOnClicked, this));
return true;
}
void LiveSceneUI::SetScore(int pValue, float pPercent, bool pShowEffect)
{
std::ostringstream oss;
oss << pValue;
this->m_pText_Score->setString(oss.str());
std::string newBar;
if (pPercent < 55)
{
newBar = "ui_bar_score_c.png";
}
else if (pPercent < 75)
{
newBar = "ui_bar_score_b.png";
}
else if (pPercent < 90)
{
newBar = "ui_bar_score_a.png";
}
else
{
newBar = "ui_bar_score_s.png";
}
if (newBar != this->m_LastScoreBar)
{
this->m_LastScoreBar = newBar;
this->m_pBar_Score->loadTexture(this->m_LastScoreBar, Widget::TextureResType::PLIST);
}
if (pShowEffect)
{
this->m_pBar_Score->setPercent(pPercent);
this->m_nEffectAlpha = 255;
}
}
void LiveSceneUI::SetVIT(int pValue, float pPercent)
{
std::ostringstream oss;
oss << pValue;
this->m_pText_VIT->setString(oss.str());
this->m_pBar_VIT->setPercent(pPercent);
auto index = pPercent < 20 ? ORNAMENT_INDEX_FAST : ORNAMENT_INDEX_NORMAL;
if (this->m_nLastOrnIndex != index)
{
this->m_nLastOrnIndex = index;
this->m_pArmature_Ornament->getAnimation()->playWithIndex(this->m_nLastOrnIndex);
}
}
void LiveSceneUI::SetCombo(int pValue)
{
std::ostringstream oss;
oss << pValue;
this->m_pText_Combo->setString(oss.str());
}
void LiveSceneUI::SetJudgement(const HitJudgeType& pValue)
{
if (pValue == HitJudgeType::None)
{
return;
}
if (this->m_pImageView_CurJudge != nullptr)
{
this->m_pImageView_CurJudge->setVisible(false);
}
switch (pValue)
{
case HitJudgeType::Perfect:
this->m_pImageView_CurJudge = this->m_pImageView_Perfect;
break;
case HitJudgeType::Great:
this->m_pImageView_CurJudge = this->m_pImageView_Great;
break;
case HitJudgeType::Good:
this->m_pImageView_CurJudge = this->m_pImageView_Good;
break;
case HitJudgeType::Bad:
this->m_pImageView_CurJudge = this->m_pImageView_Bad;
break;
case HitJudgeType::Miss:
this->m_pImageView_CurJudge = this->m_pImageView_Miss;
break;
}
this->m_pImageView_CurJudge->setVisible(true);
this->m_nJudgeAlpha = 255;
this->m_fComboScale = 1.4f;
}
void LiveSceneUI::Pause()
{
this->m_bIsPausing = true;
this->m_pArmature_Ornament->getAnimation()->pause();
}
void LiveSceneUI::Resume()
{
this->m_bIsPausing = false;
this->m_pArmature_Ornament->getAnimation()->resume();
}
void LiveSceneUI::UpdateUI()
{
if (this->m_bIsPausing)
{
return;
}
// 打擊判定
//
if (this->m_pImageView_CurJudge != nullptr)
{
this->m_pImageView_CurJudge->setOpacity(this->m_nJudgeAlpha);
this->m_nJudgeAlpha = this->m_nJudgeAlpha - 2 > 0 ? this->m_nJudgeAlpha - 2 : 0;
}
// Combo縮放
//
this->m_pImageView_Combo->setScale(this->m_fComboScale);
this->m_pText_Combo->setScale(this->m_fComboScale);
this->m_fComboScale = this->m_fComboScale - 0.02f > 1 ? this->m_fComboScale - 0.02f : 1;
// 得分特效
//
this->m_pImageView_ScoreEffect->setOpacity(this->m_nEffectAlpha);
this->m_pImageView_ScoreBarEffect->setOpacity(this->m_nEffectAlpha);
this->m_nEffectAlpha = this->m_nEffectAlpha - 2 > 0 ? this->m_nEffectAlpha - 2 : 0;
}
void LiveSceneUI::PauseOnClicked(Ref* sender, Widget::TouchEventType type)
{
if (type == Widget::TouchEventType::ENDED)
{
GameModule::GetLiveController()->PauseLive();
GameModule::GetMsgBox()->Show(
"Restart Live?",
"NO", []()
{
GameModule::GetLiveController()->ResumeLive();
},
"YES", []()
{
GameModule::GetLiveController()->RestartLive();
});
}
}
★需要添加libCocosStudio項目(位於解決方案目錄\cocos2d\cocos\editor-support\cocostudio\下,根據目標平台選擇)到工程中。
★體力值進度條的換材質方法和分數進度條一樣,我是懶逼就沒加了,代碼中只做了分數進度條換材質。
【打擊特效的實現】
圈和條的打擊特效是幀動畫,前者不循環,后者無限循環。於是使用CocoStudio的AnimationEditor制作它們。

我對特效制作實在苦手,只能從別的地方提取資源了。這里使用的資源提取自《節奏大師》的資源包。因為《節奏大師》使用的資源背景是黑色的,所以每一幀的混合模式的兩項都要設為One,這樣就沒有黑色背景了。
如果短時間出現了兩個物件,也就是在前一個特效播放結束前需要播放第二個特效,這時把第一個特效停掉是不明智,也是影響視覺效果的。特效屬於Armature類,這個類在實例化前要求預加載資源。而資源加載后,類的實例化速度是非常快的。所以這里可以采用需要播放特效的時候create一個的做法。但是如果我們不斷使用create-addChild的方法,必然會導致內存占用越來越大。所以我們需要設置讓特效停止播放的時候(塊特效為播放結束時,條特效為外接操作停止時)把特效從它的父節點上remove掉。
由於特效只會在九個圓形按鈕的位置出現,所以應當創建九個Node作為特效的父節點方便進行添加和移除管理。而對外部來說,只需要播放塊特效、播放條特效和停止條特效三個接口即可。同LiveSceneUI類一樣,這個類也要提供Pause和Resume接口。
添加EffectLayer類:
#ifndef __HIT_EFFECT_H__
#define __HIT_EFFECT_H__
#include "cocos2d.h"
#include "editor-support/cocostudio/CocoStudio.h"
USING_NS_CC;
using namespace cocostudio;
class HitEffect : public Node
{
public:
void PlayBlockEffect(int pColume);
void PlayStripEffect(int pColume);
void StopStripEffect(int pColume);
public:
void Pause();
void Resume();
public:
CREATE_FUNC(HitEffect);
private:
HitEffect(){}
bool init();
private:
Node* m_PosList[9];
};
#endif // __HIT_EFFECT_H__
實現:
#include "HitEffect.h"
#define EFFECT_NAME "Effect_BeatObject"
#define BLOCK_INDEX 0
#define STRIP_INDEX 1
bool HitEffect::init()
{
if (!Node::init())
{
return false;
}
ArmatureDataManager::getInstance()->addArmatureFileInfo(EFFECT_NAME"/"EFFECT_NAME".ExportJson");
for (int i = 0; i < 9; i++)
{
auto node = Node::create();
auto rad = CC_DEGREES_TO_RADIANS(22.5f * i + 180);
node->setPosition(400 * cos(rad), 400 * sin(rad));
this->addChild(node);
this->m_PosList[i] = node;
}
return true;
}
void HitEffect::PlayBlockEffect(int pColume)
{
auto effect = Armature::create(EFFECT_NAME);
effect->setTag(BLOCK_INDEX);
// 播放結束后從父節點中移除
//
effect->getAnimation()->setMovementEventCallFunc([this](Armature *armature, MovementEventType movementType, const std::string& movementID)
{
if (movementType == MovementEventType::COMPLETE)
{
armature->removeFromParent();
}
});
this->m_PosList[pColume]->addChild(effect);
effect->getAnimation()->playWithIndex(BLOCK_INDEX);
}
void HitEffect::PlayStripEffect(int pColume)
{
auto effect = Armature::create(EFFECT_NAME);
effect->setTag(STRIP_INDEX);
this->m_PosList[pColume]->addChild(effect);
effect->getAnimation()->playWithIndex(STRIP_INDEX);
}
void HitEffect::StopStripEffect(int pColume)
{
auto childList = this->m_PosList[pColume]->getChildren();
for (int i = childList.size() - 1; i >= 0; i--)
{
auto chd = childList.at(i);
if (chd->getTag() == STRIP_INDEX)
{
chd->removeFromParent();
}
}
}
void HitEffect::Pause()
{
for (auto node : this->m_PosList)
{
auto childList = node->getChildren();
for (auto child : childList)
{
auto effect = dynamic_cast(child);
if (effect != nullptr)
{
effect->getAnimation()->pause();
}
}
}
}
void HitEffect::Resume()
{
for (auto node : this->m_PosList)
{
auto childList = node->getChildren();
for (auto child : childList)
{
auto effect = dynamic_cast(child);
if (effect != nullptr)
{
effect->getAnimation()->resume();
}
}
}
}
★需要在類初始化時將動畫的資源加載進內存。釋放資源的方法為ArmatureDataManager::getInstance()->removeArmatureFileInfo("動畫文件路徑")
【對話框的實現】
對話框的功能是暫時中斷游戲進程,向用戶展示一些消息,並獲取用戶的選擇以執行對應的邏輯功能的一個控件。這么說的話有點難理解,在Windows編程中,它就是MessageBox,比如這個:

對話框也可以有多個按鈕,比如這個:

好吧,感覺扯遠了。在LL的Live場景中,有三種情況會觸發對話框:
1、 玩家手動點擊右上角的暫停按鈕時
2、 體力變為0時
3、 打完歌曲,提交數據網絡中斷時
我們的項目是一個單機游戲,所以不需要考慮第三種情況。雖然在目前的需求中,對話框只會在Live場景中出現,但是考慮到萬一今后加入了其他功能,也要讓對話框可以繼續使用,所以對話框模塊相對於其他功能的耦合度不能太高。
Cocos2d-x沒有對話框組件,需要我們自己寫。對話框的UI依然使用CocoStudio制作,在之前的UI項目中新建一個畫布,將資源添加上去:

上面放了三個按鈕,在代碼中控制它們的顯示與否。
為了不和Windows API沖突,這個類我們命名為MsgBox。這個類應該公開兩個接口,一個用於顯示一個按鈕的對話框,另一個用於顯示兩個按鈕的對話框。以下是頭文件:
#ifndef __MSG_BOX_H__
#define __MSG_BOX_H__
#include "cocos2d.h"
#include "ui/CocosGUI.h"
#include "editor-support/cocostudio/CocoStudio.h"
USING_NS_CC;
using namespace cocos2d::ui;
using namespace cocostudio;
class MsgBox
{
public:
MsgBox();
~MsgBox();
public:
/**
* 顯示有兩個按鈕的對話框
* @param pContent 文本內容
* @param pLText 左邊按鈕文本
* @param pLCallback 左邊按鈕點擊回調,可以為nullptr
* @param pRText 右邊按鈕文本
* @param pRCallback 右邊按鈕點擊回調,可以為nullptr
*/
void Show(const std::string& pContent,
const std::string& pLText, std::function<void()> pLCallback,
const std::string& pRText, std::function<void()> pRCallback);
/**
* 顯示只有一個按鈕的對話框
* @param pContent 文本內容
* @param pMText 按鈕文本
* @param pMCallback 按鈕點擊回調,可以為nullptr
*/
void Show(const std::string& pContent,
const std::string& pText, std::function<void()> pMCallback);
private:
void Show();
void Hide();
void AfterHidden();
void ButtonClicked(Ref* sender, Widget::TouchEventType type);
private:
Widget* m_pUIWidget;
Text* m_pText_Content;
Button* m_pButton_L;
Button* m_pButton_M;
Button* m_pButton_R;
ImageView* m_pImage_MsgBoxBG;
ImageView* m_pImage_Mask;
private:
std::function<void()> m_Button_L_Clicked; // 左邊按鈕點擊回調
std::function<void()> m_Button_M_Clicked; // 中間按鈕點擊回調
std::function<void()> m_Button_R_Clicked; // 右邊按鈕點擊回調
Button* m_pClickedButton;
};
#endif // __MSG_BOX_H__
實現:
#include "MsgBox.h"
#include "Common.h"
MsgBox::MsgBox()
: m_Button_L_Clicked(nullptr)
, m_Button_M_Clicked(nullptr)
, m_Button_R_Clicked(nullptr)
, m_pClickedButton(nullptr)
{
this->m_pUIWidget = GUIReader::getInstance()->widgetFromJsonFile("UI/EasyLiveUI_MsgBox.ExportJson");
CC_SAFE_RETAIN(this->m_pUIWidget);
this->m_pImage_Mask = (ImageView *)Find(this->m_pUIWidget, "Image_Mask");
this->m_pImage_MsgBoxBG = (ImageView *)Find(this->m_pUIWidget, "Image_MsgBox_BG");
this->m_pText_Content = (Text *)Find(this->m_pUIWidget, "Image_MsgBox_BG/Label_Content");
this->m_pButton_L = (Button *)Find(this->m_pUIWidget, "Image_MsgBox_BG/Button_Left");
this->m_pButton_M = (Button *)Find(this->m_pUIWidget, "Image_MsgBox_BG/Button_Medium");
this->m_pButton_R = (Button *)Find(this->m_pUIWidget, "Image_MsgBox_BG/Button_Right");
this->m_pButton_L->addTouchEventListener(CC_CALLBACK_2(MsgBox::ButtonClicked, this));
this->m_pButton_M->addTouchEventListener(CC_CALLBACK_2(MsgBox::ButtonClicked, this));
this->m_pButton_R->addTouchEventListener(CC_CALLBACK_2(MsgBox::ButtonClicked, this));
this->m_pImage_Mask->setOpacity(0);
this->m_pImage_MsgBoxBG->setScale(0);
}
void MsgBox::Show(const std::string& pContent,
const std::string& pLText, std::function<void()> pLCallback,
const std::string& pRText, std::function<void()> pRCallback)
{
this->m_pText_Content->setString(pContent);
this->m_pButton_M->setVisible(false);
this->m_pButton_L->setVisible(true);
this->m_pButton_R->setVisible(true);
((Text *)Find(this->m_pButton_L, "Label"))->setString(pLText);
((Text *)Find(this->m_pButton_R, "Label"))->setString(pRText);
this->m_Button_L_Clicked = pLCallback;
this->m_Button_R_Clicked = pRCallback;
this->Show();
}
void MsgBox::Show(const std::string& pContent,
const std::string& pText, std::function<void()> pMCallback)
{
this->m_pText_Content->setString(pContent);
this->m_pButton_M->setVisible(true);
this->m_pButton_L->setVisible(false);
this->m_pButton_R->setVisible(false);
((Text *)Find(this->m_pButton_M, "Label"))->setString(pText);
this->m_Button_M_Clicked = pMCallback;
this->Show();
}
void MsgBox::Show()
{
Director::getInstance()->getRunningScene()->addChild(this->m_pUIWidget, 127);
this->m_pImage_Mask->runAction(FadeIn::create(0.2f));
this->m_pImage_MsgBoxBG->runAction(Sequence::create(
ScaleTo::create(0.2f, 1.2f),
ScaleTo::create(0.1f, 1),
nullptr));
}
void MsgBox::Hide()
{
this->m_pImage_Mask->runAction(FadeOut::create(0.2f));
this->m_pImage_MsgBoxBG->runAction(ScaleTo::create(0.2f, 0));
this->m_pUIWidget->runAction(Sequence::create(
DelayTime::create(0.2f),
RemoveSelf::create(),
CallFunc::create(CC_CALLBACK_0(MsgBox::AfterHidden, this)),
nullptr));
}
void MsgBox::AfterHidden()
{
std::function<void()> callFunc = nullptr;
if (this->m_pClickedButton == this->m_pButton_L)
{
callFunc = this->m_Button_L_Clicked;
}
else if (this->m_pClickedButton == this->m_pButton_M)
{
callFunc = this->m_Button_M_Clicked;
}
else if (this->m_pClickedButton == this->m_pButton_R)
{
callFunc = this->m_Button_R_Clicked;
}
if (callFunc != nullptr)
{
callFunc();
}
}
void MsgBox::ButtonClicked(Ref* sender, Widget::TouchEventType type)
{
if (type == Widget::TouchEventType::ENDED)
{
this->m_pClickedButton = (Button *)sender;
this->Hide();
}
}
MsgBox::~MsgBox()
{
CC_SAFE_RELEASE(this->m_pUIWidget);
}
★Director::getInstance()->getRunningScene()->addChild(this->m_pUIWidget, 127)這一行表明了MsgBox顯示的層級為127。如果當前場景中有ZOrder大於127的節點,該節點會顯示在MsgBox層上。在設計程序的時候要注意不要出現層級大於127的結點。
【數值計算的實現】
在LL中,玩家的分數、體力和自己所選用的卡牌(那九個圓形按鈕)的數值有關。在我們的項目中,和卡牌相關的部分都被砍掉了。分數和體力的數值僅和兩個玩意掛鈎:一是譜面本身,二是玩家打出的判定。
嚴格地講,判定和分數這類邏輯都應該寫在腳本里。這里如果要用腳本的話又得加入腳本解析庫。想想項目的代碼都挺多了,於是果斷地偷個懶,把這些都寫死在代碼里面。
總分就是判定過的物件分數的和。物件分數的計算我使用一個簡單公式得到:
物件分數 = 判定分數 × 當前Combo加成
其中,判定分數是一個映射:
●Perfect = 500
●Great = 300
●Good = 100
●Bad = 50
●Miss = 0
而當前Combo加成 = 當前Combo數 / 100。
這個算法是我隨手編的,和LL的算法不同。由於這一部分屬於可以隨意改變的部分,就不去深究LL的加分到底是怎么計算的了。
而Combo和體力數什么時候變化呢?用一個流程圖來表示就是:

那么可以開始編寫代碼了。這一部分功能直接放在LiveController類中即可:
void LiveController::ComputeScore(const HitJudgeType& pType, const BeatObjectData* pObj)
{
if (pType == HitJudgeType::None)
{
return;
}
// 計算物件分數
//
int objScore = 0;
switch (pType)
{
case HitJudgeType::Perfect:
objScore = 500;
break;
case HitJudgeType::Great:
objScore = 300;
break;
case HitJudgeType::Good:
objScore = 100;
break;
case HitJudgeType::Bad:
objScore = 50;
}
// 計算Combo數和體力
//
bool vitChanged = false;
if (pType == HitJudgeType::Bad || pType == HitJudgeType::Miss)
{
this->m_nCombo = 0;
this->m_nVitality--;
vitChanged = true;
}
else if (pType == HitJudgeType::Good)
{
if (pObj->Star)
{
this->m_nVitality--;
vitChanged = true;
}
this->m_nCombo = 0;
}
else
{
if (pObj->Type == BeatObjectType::Strip)
{
if (pObj->HeadHitted)
{
this->m_nCombo += 1;
}
}
else
{
this->m_nCombo += 1;
}
}
// 計算總分數
//
this->m_nScore += objScore * (1 + m_nCombo / 100.0f);
// UI表現
//
this->m_pLiveSceneUI->SetCombo(this->m_nCombo);
if (objScore > 0)
{
this->m_pLiveSceneUI->SetScore(this->m_nScore, this->m_nScore > 400000 ? 100 : this->m_nScore / 4000.0f);
}
this->m_pLiveSceneUI->SetJudgement(pType);
if (vitChanged)
{
this->m_pLiveSceneUI->SetVIT(this->m_nVitality);
}
// 如果體力為零則終止游戲並彈出對話框
//
if (this->m_nVitality == 0)
{
this->PauseLive();
GameModule::GetMsgBox()->Show(
"All of your vitalities have been consumed.\r\nPlease restart the live.",
"OK",
[this]()
{
this->RestartLive();
});
return;
}
}
★對話框顯示的文本內容應當放在配置或腳本中。這里偷懶使用硬編碼。因為源文件的編碼原因直接使用中文會出現亂碼,所以這里使用英文。
【功能整合】
最后是將上面寫的功能整合起來。在整理前我們先將所有的UI和動畫工程導出。
導出后,使用Texture Packer將UI中使用到的圖像資源(不包括圖字)打包。其實就是把需要打包的圖片拖進Texture Packer然后點“Publish”按鈕。左邊“Output”選項中,“Data Format”要選為“cocos2d”:

Texture Packer的整合算法優於CocoStudio,在大部分情況打出的大圖尺寸小於CocoStudio的。例如在本項目的UI工程,用CocoStudio按最大1024×1024導出后會生成兩個png和兩個plst文件,而Texture Packer導出后只有一個。所以要修改一下EasyLiveUI_LiveScene.ExportJson和EasyLiveUI_MsgBox.ExportJson文件,將選中的部分刪掉並保存:

然后是代碼部分。首先打開SoundSystem類修復一個小Bug,將“PlaySound”方法修改為:
void SoundSystem::PlaySound(Sound* pSound, bool pIsSong, int pColume)
{
auto result = this->m_pSystem->playSound(
FMOD_CHANNEL_REUSE,
pSound,
false,
pIsSong ? &this->m_pChannel_Song : &this->m_Channel_HitSound[pColume]);
ERRCHECK(result);
}
如果不修改,在某些情況下打擊音效會占用歌曲音效的音軌導致報錯。
GameModule類中加入GetMsgBox的方法,當然別忘了在析構中添加釋放的代碼。
然后修改LIveController類的代碼。類中添加一個SetLiveSceneUI和SetHitEffect的方法,實現和SetBeatObjectManager方法一樣:
void SetLiveSceneUI(LiveSceneUI* pLSUI){ this->m_pLiveSceneUI = pLSUI; }
void SetHitEffect(HitEffect* pHE){ this->m_pHitEffect = pHE; }
別忘了添加對應的成員變量。
然后是四個Live控制方法:
void LiveController::StartLive()
{
this->m_CurStatus = LCStatus::Running;
GameModule::GetSongSystem()->PlaySong();
this->m_nScore = 0;
this->m_nCombo = 0;
this->m_nVitality = 32;
this->m_pLiveSceneUI->SetScore(0, 0, false);
this->m_pLiveSceneUI->SetCombo(0);
this->m_pLiveSceneUI->SetVIT(32, 100);
this->m_pHitEffect->Resume();
this->m_pLiveSceneUI->Resume();
}
void LiveController::PauseLive()
{
if (this->m_CurStatus == LCStatus::Running)
{
this->m_CurStatus = LCStatus::Pausing;
GameModule::GetSongSystem()->PauseSong();
}
this->m_pHitEffect->Pause();
this->m_pLiveSceneUI->Pause();
}
void LiveController::ResumeLive()
{
this->m_CurStatus = LCStatus::Running;
GameModule::GetSongSystem()->ResumeSong();
this->m_pHitEffect->Resume();
this->m_pLiveSceneUI->Resume();
}
void LiveController::RestartLive()
{
GameModule::GetSongSystem()->StopSong();
GameModule::GetSongData()->ResetHitStatus();
for (int i = 0; i < 9; i++)
{
this->m_CurIndexes[i] = 0;
this->m_pHitEffect->StopStripEffect(i);
}
this->StartLive();
}
如果條物件在按住的時候Miss了,我們不希望特效繼續播放,所以在Update方法的Miss判定部分需要添加上停止特效的代碼。如果物件Miss了還要改變體力和Combo數:
void LiveController::Update()
{
// ...
//
// Miss判定
//
auto curObj = &columeData->at(bottomIndex);
if (GameModule::GetHitJudger()->JudgeMiss(songData->GetJudgement(), curTime, curObj))
{
curObj->Enabled = false;
GameModule::GetSongSystem()->PlayHitSound(HitJudgeType::Miss, i);
this->ComputeScore(HitJudgeType::Miss, curObj);
if (bottomIndex > 0)
{
bottomIndex--;
}
if (curObj->Type == BeatObjectType::Strip)
{
this->m_pHitEffect->StopStripEffect(i);
}
}
//
// ...
}
而我們在點擊的時候,如果判定為非None,需要根據情況播放或停止打擊特效。對於條物件,如果第二次判定屬於非Miss,則不僅要停止條特效,還要額外播放一下塊特效。當然這里也會改變體力和Combo數目:
void LiveController::HitButtonsOnEvent(int pColume, bool pIsPress)
{
// ...
//
if (pIsPress)
{
result = judger->JudgeHead(songData->GetJudgement(), curTime, objData);
if (result != HitJudgeType::None)
{
if (objData->Type == BeatObjectType::Block)
{
objData->Enabled = false;
this->m_pHitEffect->PlayBlockEffect(pColume);
}
else
{
objData->HeadHitted = true;
this->m_pHitEffect->PlayStripEffect(pColume);
}
}
}
else if (objData->Type == BeatObjectType::Strip && objData->HeadHitted)
{
result = judger->JudgeTail(songData->GetJudgement(), curTime, objData);
objData->Enabled = false;
this->m_pHitEffect->StopStripEffect(pColume);
this->m_pHitEffect->PlayBlockEffect(pColume);
}
GameModule::GetSongSystem()->PlayHitSound(result, pColume);
this->ComputeScore(result, objData);
}
然后編譯運行游戲。如果沒有錯的話,可以看到我們制作的UI已經被加上去了。並且玩的時候也是有特效的:

點擊右上角的暫停按鈕:

當體力為0后:

因為這個Demo只有Live場景,沒有上級界面,所以對話框彈出后要么繼續游戲要么重新開始。
【總結】
本章使用資源:下載地址(內有兩個文件夾,Resources為資源,Projects為UI和動畫工程(使用CocoStudio 1.6.0.0制作))
如果出現編譯報錯且自己解決不了,請嘗試下載項目工程:下載地址(Win32環境,使用Cocos2dx 3.2)
從一時興起弄個項目玩玩到今天結稿,磕磕絆絆總算把坑填好了,自我感覺好像還算良好吧。感謝Cocos2d-x官網的宣傳和支持,尤其感謝官網的某位編輯,如果不是她看中我的文章,這一系列也上不了官網。
博主畢竟too young,寫的代碼肯定有不少bug,望各位大神海涵。本項目及其包含的資源文件僅供大家學習交流,請勿用於商業用途。一旦因為不恰當的使用造成版權糾紛,怪我咯?
好了不廢話了我得去收遠征了(死

