【前言和思路整理】
千呼萬喚Shi出來啊(好像也沒人呼喚),最近公司項目緊,閑暇時間少更得慢,請見諒。
上一章我們分析並實現了打擊物件類BeatObject,和它的父節點BeatObjectColume。這一章來完成BeatObjectManager類,並讓它可以根據數據運作起來。
既然要讓物件根據數據聯動起來,我們在開工前應該構思一下程序的框架。如下是我的設計圖:

這個設計圖表示每次更新時的流程。設計思維依然是將數據和顯示分開,使用LiveController類連接數據和顯示。接下來我們來一一實現它們。
【歌曲數據結構的實現】
一個歌曲數據類中應當包含哪些數據呢?
1、 歌曲文件名
2、 物件飛行速度索引
3、 打擊判定索引
4、 存放物件數據的列表
成員很少。因為是一個簡化版的游戲,所以砍掉了歌曲名藝術家名作詞作曲編曲等等,只保留游戲中會用到的數據。
歌曲文件名好說,二三四是嘛玩意?
先說二和三。玩過《節奏大師》的話,就知道一首歌可以有多個難度。不同難度中,物件飛過來的速度和打擊判定嚴格程度也不同。物件飛行速度表示物件每ms移動多少px,打擊判定則是打擊時間和物件時間相差多少ms獲得什么判定。
在這里我們制定一個映射表來表示這兩個數值,根據索引映射出物件飛行速度是多少px/ms,以及打擊的時間和物件時間相差多少ms獲得什么打擊判定。如此做可以便於統一數據標准。兩個數據的暫定取值范圍值都是[1, 7],而這個映射表暫時放在Common.h里面:
const float DropVelocity[] = { -1, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f };
本章不涉及到打擊判定的規則,故只設置了飛行速度。
然后說四。從數據上講,單個物件應該包含如下信息:
1、 物件類型,表明它是圈還是條
2、 起始時間,如果它是圈,則表明它飛到按鈕上的時間;如果它是條,則表明它的頭部飛到按鈕上的時間
3、 結束時間,僅對條類有效,表示條的尾部飛到按鈕上的時間
4、 是否同時出現,表明物件繪制時是否應該加上橫條(若有橫條,則表示這幾個物件的起始時間相同)
5、 是否星星,表明物件繪制時是否應該加上星星(若有星星,則打出Good及以下的判定時,會扣除體力值)
我采用一個列表結構來存放每一列的打擊物件數據,如下是歌曲數據的頭文件:
#ifndef __SONG_DATA_H__
#define __SONG_DATA_H__
#include
#include "Common.h"
struct BeatObjectData;
class SongData
{
public:
SongData(const std::string& pFileName);
SongData();
~SongData();
public: // Getter
std::string GetSongFileName() const { return m_SongFileName; }
int GetVelocity() const { return m_nVelocity; }
int GetJudgement() const { return m_nJudgement; }
const std::vector<BeatObjectData>& GetObjColume(int pIndex);
private:
void LoadFile(const std::string& pFileName);
std::vector<BeatObjectData>* GetObjColumeInternal(int pIndex);
private:
std::string m_SongFileName;
int m_nVelocity;
int m_nJudgement;
std::vector<BeatObjectData> m_Colume_1,
m_Colume_2,
m_Colume_3,
m_Colume_4,
m_Colume_5,
m_Colume_6,
m_Colume_7,
m_Colume_8,
m_Colume_9;
};
struct BeatObjectData
{
BeatObjectType Type = BeatObjectType::Invalid;
long StartTime = -1;
long EndTime = -1;
bool Star = false;
bool SameTime = false;
};
#endif // __SONG_DATA_H__
實現:
#include "SongData.h"
#include "platform/CCFileUtils.h"
#include "tinyxml2/tinyxml2.h"
USING_NS_CC;
//////////////////////////////////////////////////////////////////////////
// Defines Begin
#define LOADXML_BEGIN(__FILENAME__) { auto tData = FileUtils::getInstance()->getStringFromFile(__FILENAME__); \
tinyxml2::XMLDocument* xmlDoc = new tinyxml2::XMLDocument(); \
xmlDoc->Parse(tData.c_str(), tData.size());
#define LOADXML_END delete xmlDoc; }
#define GET_ATTR_NEXT(__ATTR__) __ATTR__ = __ATTR__->Next()
#define GET_STR_VALUE(__VAR__, __ATTR__) __VAR__ = __ATTR__->Value(); \
GET_ATTR_NEXT(__ATTR__)
#define GET_INT_VALUE(__VAR__, __ATTR__) __VAR__ = __ATTR__->IntValue(); \
GET_ATTR_NEXT(__ATTR__)
#define GET_BOOL_VALUE(__VAR__, __ATTR__) __VAR__ = __ATTR__->BoolValue(); \
GET_ATTR_NEXT(__ATTR__)
// Defines End
//////////////////////////////////////////////////////////////////////////
SongData::SongData()
: m_nVelocity(-1)
, m_nJudgement(-1)
{
}
SongData::SongData(const std::string& pFileName)
{
this->LoadFile(pFileName);
}
void SongData::LoadFile(const std::string& pFileName)
{
LOADXML_BEGIN(pFileName)
auto songNode = xmlDoc->FirstChildElement();
auto attr = songNode->FirstAttribute();
GET_STR_VALUE(this->m_SongFileName, attr);
GET_INT_VALUE(this->m_nVelocity, attr);
GET_INT_VALUE(this->m_nJudgement, attr);
auto trackNode = songNode->FirstChildElement();
while (trackNode)
{
long starTime;
attr = trackNode->FirstAttribute();
GET_INT_VALUE(starTime, attr);
auto beatobjNode = trackNode->FirstChildElement();
int count = 0;
while (beatobjNode)
{
BeatObjectData obj;
obj.StartTime = starTime;
attr = beatobjNode->FirstAttribute();
int colume;
GET_INT_VALUE(colume, attr);
int objtype;
GET_INT_VALUE(objtype, attr);
if (objtype == BeatObjectType::Strip)
{
obj.Type = BeatObjectType::Strip;
GET_INT_VALUE(obj.EndTime, attr);
}
else
{
obj.Type = BeatObjectType::Block;
}
GET_BOOL_VALUE(obj.Star, attr);
auto nextElement = beatobjNode->NextSiblingElement();
obj.SameTime = count != 0 || nextElement;
count++;
this->GetObjColumeInternal(colume)->push_back(obj);
beatobjNode = nextElement;
}
trackNode = trackNode->NextSiblingElement();
}
LOADXML_END
}
const std::vector<BeatObjectData>& SongData::GetObjColume(int pIndex)
{
switch (pIndex)
{
case 0:return this->m_Colume_1;
case 1:return this->m_Colume_2;
case 2:return this->m_Colume_3;
case 3:return this->m_Colume_4;
case 4:return this->m_Colume_5;
case 5:return this->m_Colume_6;
case 6:return this->m_Colume_7;
case 7:return this->m_Colume_8;
case 8:return this->m_Colume_9;
default:
WASSERT(false);
return std::vector();
}
}
std::vector<BeatObjectData>* SongData::GetObjColumeInternal(int pIndex)
{
switch (pIndex)
{
case 0:return &this->m_Colume_1;
case 1:return &this->m_Colume_2;
case 2:return &this->m_Colume_3;
case 3:return &this->m_Colume_4;
case 4:return &this->m_Colume_5;
case 5:return &this->m_Colume_6;
case 6:return &this->m_Colume_7;
case 7:return &this->m_Colume_8;
case 8:return &this->m_Colume_9;
default:
WASSERT(false);
return nullptr;
}
}
SongData::~SongData()
{
for (int i = 0; i < 9; i++)
{
this->GetObjColumeInternal(i)->clear();
}
}
★感謝博友 肖志棟 提醒這里寫錯了一個地方(代碼中已修復)
★數據類做好譜面讀取,提供獲取數據的接口即可。
★將歌曲的數據保存在代碼中絕對是不科學的,我們應該將數據保存在文件中,歌曲類實例化時,從文件讀取數據存儲在內存中。我采用XML格式來存儲的,所以讀取的時候需要使用tinyXML2——cocos2d-x自帶的XML解析器來讀取。當然,你也可以使用JSON(cocos2d-x自帶RapidJSON解析器),或者自定義的格式。反正目的是把文本字符串轉化為我們需要的東西。在這個項目中,我使用的XML是這樣的格式:
<?xml version="1.0" encoding="UTF-8"?>
<SongData SongFile="歌曲文件" Velocity="飛行速度" Judgement="判定">
<TimeTrack Time="1000">
<BeatObject Colume="1" Type="1" Star="false"/>
<BeatObject Colume="2" Type="1" EndTime="2000" Star="false"/>
</TimeTrack>
</SongData>
是否有星星是隨機生成的,和官方譜面不同。本章使用的譜面數據由一個自制的譜面編輯器生成,參考官方的《START:DASH!!》Expert難度制作。這個歌曲名是不是想說明START類繼承於DASH

編輯器制作不屬於這一系列文章討論的范疇,就不多做講解了。原理不復雜的,唯一一個難點就是如何在MFC中使用Cocos2d-x,而網上教程遍地都是。
【物件管理器的實現】
數據類有了,接下來可以編寫BeatObjectManager類了。其作用是根據數據創建打擊物件,並在游戲開始后對其進行更新操作。以下是BeatObjectManager的頭文件:
#ifndef __BEAT_OBJECT_MANAGER_H__
#define __BEAT_OBJECT_MANAGER_H__
#include "cocos2d.h"
#include "SongData.h"
#include "BeatObjectColume.h"
USING_NS_CC;
class BeatObjectManager : public Node
{
public:
CREATE_FUNC(BeatObjectManager);
public:
void ResetObjsFromData(SongData* pData);
void ClearAllObjs();
void SetObjectPositionY(int pColume, int pIndex, float pY);
void UpdateColume(int pColume, int pStartIndex, int pEndIndex);
private:
BeatObjectManager();
bool init();
private:
/*
* 根據索引獲取BeatObjectColume
* @param pIndex 索引取值范圍 [0, 8]
*/
BeatObjectColume* GetColume(int pIndex);
private:
BeatObjectColume *m_pColume_1,
*m_pColume_2,
*m_pColume_3,
*m_pColume_4,
*m_pColume_5,
*m_pColume_6,
*m_pColume_7,
*m_pColume_8,
*m_pColume_9;
};
#endif // __BEAT_OBJECT_MANAGER_H__
實現:
#include "BeatObjectManager.h"
#include "GameModule.h"
BeatObjectManager::BeatObjectManager()
: m_pColume_1(nullptr)
, m_pColume_2(nullptr)
, m_pColume_3(nullptr)
, m_pColume_4(nullptr)
, m_pColume_5(nullptr)
, m_pColume_6(nullptr)
, m_pColume_7(nullptr)
, m_pColume_8(nullptr)
, m_pColume_9(nullptr)
{
}
bool BeatObjectManager::init()
{
if (!Node::init())
{
return false;
}
for (int i = 0; i < 9; i++)
{
auto colume = BeatObjectColume::create();
colume->setPosition(480, 480);
colume->setRotation(90 - 22.5f * i); // 22.5f = 180 / 8
this->addChild(colume);
switch (i)
{
case 0: this->m_pColume_1 = colume;
case 1: this->m_pColume_2 = colume;
case 2: this->m_pColume_3 = colume;
case 3: this->m_pColume_4 = colume;
case 4: this->m_pColume_5 = colume;
case 5: this->m_pColume_6 = colume;
case 6: this->m_pColume_7 = colume;
case 7: this->m_pColume_8 = colume;
case 8: this->m_pColume_9 = colume;
}
}
return true;
}
void BeatObjectManager::ResetObjsFromData(SongData* pData)
{
this->ClearAllObjs();
for (int i = 0; i < 9; i++)
{
auto columeData = pData->GetObjColume(i);
for (int j = columeData.size() - 1; j >= 0; j--)
{
auto objData = columeData.at(j);
auto type = objData.Type | (objData.Star ? BeatObjectType::Star : 0) | (objData.SameTime ? BeatObjectType::SameTime : 0);
auto obj = objData.Type == BeatObjectType::Block
? BeatObject::create(type)
: BeatObject::create(type, (objData.EndTime - objData.StartTime) * DropVelocity[pData->GetVelocity()]);
this->GetColume(i)->AddBeatObject(obj);
}
}
}
void BeatObjectManager::ClearAllObjs()
{
for (int i = 0; i < 9; i++)
{
this->GetColume(i)->ClearObjects();
}
}
void BeatObjectManager::SetObjectPositionY(int pColume, int pIndex, float pY)
{
this->GetColume(pColume)->SetObjectPositionY(pIndex, pY);
}
void BeatObjectManager::UpdateColume(int pColume, int pStartIndex, int pEndIndex)
{
this->GetColume(pColume)->UpdateObjects(pStartIndex, pEndIndex);
}
BeatObjectColume* BeatObjectManager::GetColume(int pIndex)
{
switch (pIndex)
{
case 0: return this->m_pColume_1;
case 1: return this->m_pColume_2;
case 2: return this->m_pColume_3;
case 3: return this->m_pColume_4;
case 4: return this->m_pColume_5;
case 5: return this->m_pColume_6;
case 6: return this->m_pColume_7;
case 7: return this->m_pColume_8;
case 8: return this->m_pColume_9;
}
return nullptr;
}
★注意ResetObjsFromData方法。這個方法的作用是根據傳入的SongData數據創建顯示的BeatObject實例。在方法中第二層for循環中,可以看到這里是采用倒序添加的。為什么呢?
因為,如果使用順序添加,在物件密集的時候就是這樣的效果:
如圖所示,后面的物件會蓋在前面的物件上。雖然LL也是這樣的(找一些較密集的譜面的視頻,放慢看會發現),但是我不認為這是一個好的設計和體驗,所以我采用倒序添加,把時間靠后的物件放在時間靠前的物件的層級下。
然后,我們需要對BeatObject類和BeatObjectColume類做一些修改。
首先修改BeatObject類。BeatObject是根據時間顯示的,所以我們不希望剛添加進去就顯示在屏幕上,所以這里要修改BeatObject類的init方法:
bool BeatObject::init(int pType, float pLength /* = 0 */)
{
// ...
this->setVisible(false);
return true;
}
然后是BeatObjectColume類。極端情況下,一個列中可能有上百上千個物件,所以我們必須保證在每一幀中只更新會在屏幕上顯示的物件,不然會造成FPS降低。
在BeatObjectColume類中添加兩個變量和一個方法:
private:
void UpdateObjects(int pStartIndex, int pEndIndex);
private:
int m_nLastStartIndex;
int m_nLastEndIndex;
方法的實現:
void BeatObjectColume::UpdateObjects(int pStartIndex, int pEndIndex)
{
for (int i = this->m_nLastStartIndex; i <= this->m_nLastEndIndex; i++)
{
this->m_BeatObjList.at(i)->setVisible(false);
}
for (int i = pStartIndex; i <= pEndIndex; i++)
{
this->m_BeatObjList.at(i)->setVisible(true);
}
this->m_nLastStartIndex = pStartIndex;
this->m_nLastEndIndex = pEndIndex;
}
★兩個變量別忘記賦初值,這里使用的0。
★UpdateObjects方法是就是在每一幀更新時由外部調用,隱藏上一幀顯示的物件,再顯示當前幀范圍內的物件。
【Live控制器的實現】
LiveController類將數據和顯示連接在一起,是一個重要的樞紐類。根據設計,這個類在每一幀會計算出更新范圍和物件的Y坐標。怎么計算呢?
首先我們來確定當前要顯示的物件索引范圍,這需要知道屏幕中能夠顯示出的物件的時間區間。
如果我們去掉偽3D效果,再簡化一下貼圖材質,那么游戲運行的時候可以大致用如下的圖片來描述:

我是懶逼所以就只做了3列,意思一下就行。
回憶一下小學物理(還是初中物理?記不清了)中提到的參考系概念。圖中的效果是以窗口為參考系的。如果我們以物件為參考系,上圖描述的過程就變成了物件不動窗口動,窗口的移動速度和以窗口為參考系時物件的移動速度大小相同,方向相反。
那么,取得窗口上某一點對應的時間就非常簡單了。如果窗口底端所在的時間為當前時間,有如下公式:
頂端時間 = 當前時間 + (窗口高度 / 移動速度)
頂端時間 = 當前時間 + (0 / 移動速度)
我們要做的就是只顯示物件時間處於底端時間和頂端時間之間的物件。
根據上一章的分析得出物件從生成點到窗口底邊的距離是480px,到按鈕的距離為400px,按鈕到窗口底邊的距離為80px。所以,在每一幀中,我們只需要顯示y坐標處於[-480, 0]之間的物件。雖然在經過旋轉后,物件飛出窗口的距離可能大於480px,但是根據游戲設計,物件只要飛過了按鈕而用戶並沒有點擊,這個物件就會消失,並判定為miss(視頻中沒有展示)。而飛過按鈕的距離實際上很短,所以取480px完全夠了。
由於按鈕所在的位置才是當前時間,所以公式要稍微變化一下:
頂端時間 =當前時間 + (400 / 物件飛行速度)
底端時間 = 當前時間 + (-80 / 物件飛行速度)
時間范圍確定了,接下來就是根據物件的時間計算它的坐標了。上一章中,我們最后在LiveScene的Update中加入了讓物件移動的代碼,它的作用是:每幀將物件的Y坐標-4。實際使用中,我們是不是應該根據上文說的飛行速度索引取得飛行速度,然后每幀都將物件的坐標減去這個值呢?
答案是否定的。因為這是一個音樂游戲,物件的坐標和歌曲的時間必須緊密相關。以上面的方式設計的話,如果在某一時刻,音頻卡頓了一下,由於音頻播放走的多線程,Update方法不會受到影響,物件繼續在移動,在這一時刻后,物件時間相對於歌曲時間就會提提前一些,嚴重影響節奏感。簡單地說,就是“動次打次動次打次”和物件飛行對不上了。
所以這里需要采取根據歌曲時間來設定物件y坐標的設計。如果某一時刻卡頓了,在下一幀時物件會發生一個瞬移,雖然視覺上看起來不大好,但是物件坐標相對於歌曲時間還是沒有發生變化的,節奏還是跟着歌曲在走的,節奏感就不會亂。
簡略地說,就是這種方式保證物件的飛行和歌曲播放時間軸一致。
在繼續工作前,先用下面一張簡圖來表示一下物件飛行和時間刻度的關系:

能看出,物件自身的時間在移動的過程中是不會改變的,影響物件y坐標的因素是當前時間和物件飛行速度。
可以推算出物件當前y坐標和當前時間的關系:
物件坐標y = (物件時間 – 當前時間) × 物件飛行速度 – 400
接下來就是根據時間取值獲取能顯示的物件,需要知道-480點和0點對應的最接近的物件的索引。
由於BeatObjectManager中的數據取自SongData類,而SongData的數據來自文件。只要我們保證文件中記錄的物件是按時間順序排列的,那么BeatObjectManager中的顯示物件也是順序排列的。順序排列的物件使用二分查找法再合適不過。這里采用了一個二分近似查找法,使用二分查找得到起始時間和傳入參數最接近的物件的索引。計算出結果后,應當對底部的物件索引再進行一次判斷,防止物件頭部時間超過當前時間太多時(如該物件是一個很長的條)整個物件不再顯示。
如下是LiveController類的頭文件:
#ifndef __LIVE_CONTROLLER_H__
#define __LIVE_CONTROLLER_H__
#include "cocos2d.h"
#include "BeatObjectManager.h"
#include "SongData.h"
USING_NS_CC;
enum LCStatus;
class LiveController
{
public:
LiveController();
~LiveController(){}
public:
void ResetObjs();
void SetBeatObjectManager(BeatObjectManager* pBOM) { this->m_pBeatObjectManager = pBOM; }
public:
void StartLive();
void PauseLive();
void ResumeLive();
void RestartLive();
public:
void Update();
private:
BeatObjectManager* m_pBeatObjectManager;
LCStatus m_CurStatus;
};
enum LCStatus
{
Initing,
Running,
Pausing
};
#endif // __LIVE_CONTROLLER_H__
實現:
#include "LiveController.h"
#include "GameModule.h"
namespace
{
inline int GetNearlyIndex(int pTime, const std::vector<BeatObjectData> pColume)
{
if (pColume.size() == 0)
{
return -1;
}
else
{
long index_Start = 0,
index_End = pColume.size() - 1,
index_Middle = (index_Start + index_End) / 2,
time_Start = 0,
time_Middle = 0,
time_End = 0;
while ((index_Start + 1) < index_End)
{
index_Middle = (index_Start + index_End) / 2;
time_Start = pColume.at(index_Start).StartTime;
time_Middle = pColume.at(index_Middle).StartTime;
time_End = pColume.at(index_End).StartTime;
if (pTime < time_Middle)
{
index_End = index_Middle;
}
else
{
index_Start = index_Middle;
}
}
time_Start = pColume.at(index_Start).StartTime;
time_End = pColume.at(index_End).StartTime;
return (pTime - time_Start) > (time_End - pTime) ? index_End : index_Start;
}
}
}
//////////////////////////////////////////////////////////////////////////
// LiveController
LiveController::LiveController()
: m_pBeatObjectManager(nullptr)
, m_CurStatus(LCStatus::Initing)
{
}
void LiveController::ResetObjs()
{
auto data = GameModule::GetSongData();
WASSERT(data);
this->m_pBeatObjectManager->ResetObjsFromData(data);
}
void LiveController::StartLive()
{
this->m_CurStatus = LCStatus::Running;
}
void LiveController::PauseLive()
{
this->m_CurStatus = LCStatus::Pausing;
}
void LiveController::ResumeLive()
{
this->m_CurStatus = LCStatus::Running;
}
void LiveController::RestartLive()
{
this->StartLive();
}
void LiveController::Update()
{
if (this->m_CurStatus == LCStatus::Running)
{
auto songData = GameModule::GetSongData();
auto curVelocity = DropVelocity[songData->GetVelocity()];
auto curTime = GameModule::GetTimer()->GetTime();
long topTime = (400 / curVelocity) + curTime;
long bottomTime = (-80 / curVelocity) + curTime;
for (int i = 0; i < 9; i++)
{
auto columeData = songData->GetObjColume(i);
auto topIndex = GetNearlyIndex(topTime, columeData);
auto bottomIndex = GetNearlyIndex(bottomTime, columeData);
// 防止Strip在飛行時消失
//
if (bottomIndex > 0)
{
auto obj = columeData.at(bottomIndex - 1);
if (obj.Type == BeatObjectType::Strip)
{
if (obj.EndTime > bottomTime)
{
bottomIndex--;
}
}
}
for (int j = bottomIndex; j <= topIndex; j++)
{
auto posY = (columeData.at(j).StartTime - curTime) * curVelocity - 400;
this->m_pBeatObjectManager->SetObjectPositionY(i, j, posY);
}
this->m_pBeatObjectManager->UpdateColume(i, bottomIndex, topIndex);
}
}
}
【歌曲計時器臨時實現】
這一章本應不涉及計時器的。由於我們要測試一下功能,就得先寫一個簡易的計時器。我們知道,這個游戲玩起來的時候,除非中途暫停游戲重新開始,音樂是一直播放到結束的。所以可以認為,“當前時間”是一個線性增加的量。
又因為,游戲是以60fps運行的,每幀耗時約16ms,於是可以這么寫:
#ifndef __SONG_TIMER_H__
#define __SONG_TIMER_H__
class SongTimer
{
public:
SongTimer();
public:
long GetTime();
void Reset();
private:
long m_nCurTime;
};
#endif // __SONG_TIMER_H__
實現:
#include "SongTimer.h"
SongTimer::SongTimer()
: m_nCurTime(0)
{
}
long SongTimer::GetTime()
{
this->m_nCurTime += 16;
return this->m_nCurTime;
}
void SongTimer::Reset()
{
this->m_nCurTime = 0;
}
【功能管理】
隨着加入的功能越來越多,而這些功能塊在程序的很多地方都有用到。如果把這些功能類做成單例模式,就不可避免在各種地方調用GetInstance()方法,而程序退出時還得一個個去釋放內存,略麻煩。
所以我們來做一個GameModule類,將這些功能模塊統一放進這里,而程序退出時也由這里統一釋放內存。所有需要調功能塊的地方,都從這個類里面調。這個類比較簡單,直接上代碼:
#ifndef __GAME_MODULE_H__
#define __GAME_MODULE_H__
#include "SongTimer.h"
#include "LiveController.h"
#include "cocos2d.h"
class GameModule
{
public:
~GameModule();
static void Dispose();
public:
static SongTimer* GetTimer();
static LiveController* GetLiveController();
static SongData* GetSongData(){ return m_pSongData; }
static void SetSongData(const std::string& pName);
public:
static void Update();
private:
GameModule();
private:
static SongTimer* m_pTimer;
static LiveController* m_pController;
static SongData* m_pSongData;
};
#endif // __GAME_MODULE_H__
實現:
#include "GameModule.h"
#include "cocos2d.h"
SongTimer* GameModule::m_pTimer = nullptr;
LiveController* GameModule::m_pController = nullptr;
SongData* GameModule::m_pSongData = nullptr;
SongTimer* GameModule::GetTimer()
{
if (m_pTimer == nullptr)
{
m_pTimer = new SongTimer();
}
return m_pTimer;
}
LiveController* GameModule::GetLiveController()
{
if (m_pController == nullptr)
{
m_pController = new LiveController();
}
return m_pController;
}
void GameModule::SetSongData(const std::string& pName)
{
CC_SAFE_DELETE(m_pSongData);
m_pSongData = new SongData(pName);
}
void GameModule::Update()
{
GetLiveController()->Update();
}
void GameModule::Dispose()
{
CC_SAFE_DELETE(m_pTimer);
CC_SAFE_DELETE(m_pController);
CC_SAFE_DELETE(m_pSongData);
}
★需要注意的是這些功能塊都沒有做ARC,所以需要加入Dispose()方法,在程序退出時手動delete。
★類中加入Update方法供外部在每幀調用,用以支持某些功能塊(如LiveController)每幀更新。
★每次在類中添加功能塊后,別忘了在Dispose()方法里加上釋放內存的代碼。
【測試一下】
在LiveScene中,刪掉上一次加入的Colume類,加上BeatObjectManager類,並在init()方法中初始化數據,在update()方法中調用GameModule類的Update()方法:
#include "LiveScene.h"
#include "GameModule.h"
LiveScene::LiveScene()
: m_pBeatObjectManager(nullptr)
{
}
Scene* LiveScene::createScene()
{
// 'scene' is an autorelease object
auto scene = Scene::create();
// 'layer' is an autorelease object
auto layer = LiveScene::create();
// add layer as a child to scene
scene->addChild(layer);
// return the scene
return scene;
}
// on "init" you need to initialize your instance
bool LiveScene::init()
{
if (!Layer::init())
{
return false;
}
// 加入背景圖
//
auto bg = Sprite::create("bg.jpg");
bg->setPosition(480, 320);
this->addChild(bg);
// 加上黑色半透明蒙層
//
auto colorLayer = LayerColor::create(Color4B(0, 0, 0, 192));
this->addChild(colorLayer);
// 初始化BeatObjectManager
//
this->m_pBeatObjectManager = BeatObjectManager::create();
this->addChild(this->m_pBeatObjectManager);
// 初始化歌曲數據
//
GameModule::SetSongData("start_dash.xml");
// 初始化控制器
//
GameModule::GetLiveController()->SetBeatObjectManager(this->m_pBeatObjectManager);
GameModule::GetLiveController()->ResetObjs();
this->scheduleUpdate();
this->runAction(Sequence::createWithTwoActions(
DelayTime::create(2),
CallFunc::create([]()
{
GameModule::GetLiveController()->StartLive();
})));
return true;
}
void LiveScene::update(float delta)
{
GameModule::Update();
}
LiveScene::~LiveScene()
{
GameModule::Dispose();
}
★別忘了在析構方法中調用GameModule類的Dispose()方法。
然后讓程序跑起來,可以看到物件按譜面設定的順序飛出了。雖然暫時沒有音樂但是還是可以腦補唱一下 "I say… Hey! Hey! Hey! START:DASH!"
截取錄制一小段,大致這個效果:

【本章結束語】
這一章就結束了。本章的難點主要在於對於物件移動時,物件的坐標和時間的相互轉換的理解。我表達能力不算太好,希望大家能看懂上面的分析過程。
下一章我們加入音樂播放功能,並讓這個游戲可以接收用戶輸入,並執行判定邏輯,讓它變得可以玩一玩。
本章所用到的資源:百度網盤 和上一章的資源相比就多了一個start_dash.xml

