【前言和思路整理】
千呼万唤Shǐ出来!最近莫名被基友忽悠着进舰坑了,加上要肝LL活动,又碰上公司项目紧张经常加班,这一章发得比以往时候来得更晚一些,抱歉啊。
上一章我们实现了BeatObjectManager等几个类,让游戏可以播放预设好的谱面了。这一章我们给游戏加入用户输入和判定,并引入音频系统,最后部署到移动平台上,让游戏可以玩起来。
本章的难点是物件判定的流程设计,和对物件判定逻辑的理解。
关于音频系统,我采用了一个第三方的非开源库,严格意义上讲和Cocos2dx基本无关,可以选择性跳过。
关于部署,因为我的手机是诺基亚的,所以只能弄WP平台。想部署到其他平台请自行百度了。
本章的模块设计简图如下:
【物件判定逻辑】
物件判定可以说是Live场景的核心逻辑,我认为在开始敲代码前应该先把思维理清,把过程想透。
执行物件判定功能的模块可以称为打击判定器。需要判定的时候,将物件输入模块,在输出端得到判定结果。通过第一章的分析可以知道,对于输入的任何的物件,模块的输出只能是如下几种情况:
·Perfect
·Great
·Good
·Bad
·Miss
·None
和第一章不同的是,这里我加上了一个None。None表示不对这个物件进行判定。什么时候会用到None呢?情况之一是长条在按住的时候,是不需要判定结果的;情况之二是判定触发时间早于物件时间太多(也就是按得太早)的时候也不需要结果。
那么什么时候才需要进行判定呢?
1、用户触发了九个圆形按钮的触摸事件时
2、控制器更新物件时
第一个很好理解,只要用户点击了按钮,就需要针对这次点击操作进行一次判定。第二个是嘛?
玩过LL的话就知道,如果开始游戏后不进行任何操作,物件飞过了按钮一定时间后,会报出Miss。而这个Miss的判定,就是在物件进行更新的时候触发的。上一章中我们设计的更新物件是放在LiveController类中,于是第二个判定操作也应当由LiveController发起。
那么什么操作会触发什么样的判定结果呢?我们知道这是一个音乐游戏,那么对于物件的判定可以理解为我打得准不准。这个“准不准”是通过时间来体现的。
如图所示,“准不准”遵循这个规则:
轴下方的时间表示触发判定的时间和物件时间的差值。数值刻度根据需求不一定线性变化,但一定是对称的。
还有一点需要注意的是,对于块物件,只要触发了非None判定,物件就会消失,而对于条物件,则稍微复杂一些:若头部判定为Miss,则物件消失;若头部点击过且为失Miss,则尾部判定(模式1)或长按判定(模式2)非None时物件消失。
光用文字描述还是不容易理解,画成流程图看看,先是模式1:
第二种模式的功能应当仅用于判定是否Miss:
其中,头部和尾部判定输出除Miss外的所有情况,Miss判定则仅输出Miss或None判定。
【判定器的实现】
大致的思路有了,可以开始编码了,先从关键部分开始做。从外部来说判定器的结构非常简单,而它内部的核心逻辑应该是这样的:
判定器对外有三个接口,分别用于判定块和条头、判定条尾、判定Miss。其中,判定块和条头在TouchBegan时调用(模式1),判定条尾在TouchEnded或TouchCanceled时调用(模式1),判定Miss每帧调用(模式2)。
如下是判定器的代码。因为判定等级是1-7,默认0档为无效值,而每一档有4个判定时间阈,故采用一个二维数组来存放。其实想透了代码还是不复杂的:
#ifndef __HIT_JUDGEMENT_H__
#define __HIT_JUDGEMENT_H__
#include "SongData.h"
enum HitJudgeType;
class HitJudger
{
public:
HitJudger();
~HitJudger(){}
public:
HitJudgeType JudgeHead(int pJudgementLevel, long pHitTime, const BeatObjectData* pObjData);
HitJudgeType JudgeTail(int pJudgementLevel, long pHitTime, const BeatObjectData* pObjData);
bool JudgeMiss(int pJudgementLevel, long pCurTime, const BeatObjectData* pObjData);
private:
/*
* 根据物件和点击的时间计算出判定
* @param pJudgementLevel 判定等级
* @param pTimeOffset 点击时间减去物件时间
*/
HitJudgeType GetResult(int pJudgementLevel, long pTimeOffset);
private:
int m_JudgeValue[8][4];
};
enum HitJudgeType
{
Perfect,
Great,
Good,
Bad,
Miss,
None
};
#endif // __HIT_JUDGEMENT_H__
实现:
#include "HitJudger.h"
HitJudger::HitJudger()
{
// 列分别对应:Perfect, Great, Good, Bad
// 大于Bad则为Miss
//
for (int i = 0; i < 4; i++)
{
this->m_JudgeValue[0][i] = -1;
}
for (int i = 1; i < 8; i++)
{
for (int j = 0; j < 4; j++)
{
this->m_JudgeValue[i][j] = 150 - 20 * i + 30 * j;
}
}
}
HitJudgeType HitJudger::JudgeHead(int pJudgementLevel, long pHitTime, const BeatObjectData* pObjData)
{
return this->GetResult(pJudgementLevel, pHitTime - pObjData->StartTime);
}
HitJudgeType HitJudger::JudgeTail(int pJudgementLevel, long pHitTime, const BeatObjectData* pObjData)
{
auto ret = this->GetResult(pJudgementLevel, pHitTime - pObjData->EndTime);
// 若松手时间比miss还早,同样判定为miss
//
if (ret == HitJudgeType::None)
{
ret = HitJudgeType::Miss;
}
return ret;
}
bool HitJudger::JudgeMiss(int pJudgementLevel, long pCurTime, const BeatObjectData* pObjData)
{
if (pObjData->HeadHitted)
{
return false;
}
auto timeOffset = pCurTime - pObjData->StartTime;
if (abs(timeOffset) > this->m_JudgeValue[pJudgementLevel][3])
{
return timeOffset > 0;
}
return false;
}
HitJudgeType HitJudger::GetResult(int pJudgementLevel, long pTimeOffset)
{
auto value = this->m_JudgeValue[pJudgementLevel];
auto offsetABS = abs(pTimeOffset);
if (offsetABS <= value[0])
{
return HitJudgeType::Perfect;
}
else if (offsetABS <= value[1])
{
return HitJudgeType::Great;
}
else if (offsetABS <= value[2])
{
return HitJudgeType::Good;
}
else if (offsetABS <= value[3])
{
return HitJudgeType::Bad;
}
else
{
return HitJudgeType::None;
}
}
★这里的判定阈使用代码生成,实际应用中应当把这个值做成配置文件方便修改。
【用户输入UI的实现】
从视频中可以看出,Live场景中涉及到用户输入的部分很少,除开右上角的暂停按钮,就只有中间呈扇形分布的九个圆形按钮了。
极端情况下,如果有人做了全押的谱,在某一时刻可能需要9个按钮同时按下(虽然到目前LL已有的谱最多同时按俩)。Cocos2dx默认最多支持5个点,再多的话需要修改一下底层,让它支持9点触控。
修改很简单,只需要把这个常量的值改为9即可(CCEventTouch.h, 39行):
static const int MAX_TOUCHES = 9; //changed for EasyLive, default = 5;
UI的功能就是在每个按钮收到消息时向LiveController类发送消息。使用过DX SDK的人可能会觉得这里需要采用轮询方式获取触摸状态。但是,游戏是每秒60帧运行的=>每秒更新60次=>每两次更新间隔16ms,也就是说每次点击有16ms的误差,对音游来说比较大。虽然可以采用多线程来降低误差,但需要考虑异步啊死锁啊一大堆问题,麻烦。
于是就使用原生的事件触发机制来做这个功能,然后按钮的位置使用圆的参数方程计算得出。代码如下:
#ifndef __HIT_INPUT_H__
#define __HIT_INPUT_H__
#include "cocos2d.h"
#include "ui/CocosGUI.h"
USING_NS_CC;
using namespace cocos2d::ui;
class HitInputUI : public Node
{
public:
~HitInputUI(){}
CREATE_FUNC(HitInputUI);
private:
HitInputUI(){}
bool init();
private:
void CircleOnTouchEvent(Ref* sender, Widget::TouchEventType type);
private:
Button* m_BeatCircles[9];
};
#endif // __HIT_INPUT_H__
实现:
#include "HitInputUI.h"
#include "Common.h"
#include "GameModule.h"
bool HitInputUI::init()
{
if (!Node::init())
{
return false;
}
for (int i = 0; i < 9; i++)
{
std::ostringstream oss;
oss << "BeatCircle_" << (i + 1) << ".png";
auto rad = CC_DEGREES_TO_RADIANS(22.5f * i + 180);
auto circle = Button::create(oss.str(), oss.str(), oss.str());
circle->setPosition(Vec2(
400 * cos(rad),
400 * sin(rad)));
this->addChild(circle);
circle->addTouchEventListener(CC_CALLBACK_2(HitInputUI::CircleOnTouchEvent, this));
this->m_BeatCircles[i] = circle;
}
return true;
}
void HitInputUI::CircleOnTouchEvent(Ref* sender, Widget::TouchEventType type)
{
if (type == Widget::TouchEventType::MOVED)
{
return;
}
int touchedIndex = -1;
for (int i = 0; i < 9; i++)
{
if (this->m_BeatCircles[i] == sender)
{
touchedIndex = i;
break;
}
}
WASSERT(touchedIndex != -1);
switch (type)
{
case Widget::TouchEventType::BEGAN:
GameModule::GetLiveController()->HitButtonsOnEvent(touchedIndex, true);
break;
case Widget::TouchEventType::ENDED:
case Widget::TouchEventType::CANCELED:
GameModule::GetLiveController()->HitButtonsOnEvent(touchedIndex, false);
break;
}
}
★需要将libGUI项目(位于解决方案目录\cocos2d\cocos\ui\下,根据目标平台选择)引入解决方案中,设为主项目的生成依赖项,并在主项目的属性——链接器——附加依赖项中加入“libGUI.lib”
【歌曲数据的修改】
上一章中我们设计的歌曲数据是在外部仅能访问,不能修改的。而在现在的情况下得做一下修改了。
删掉GetObjColumeInternal方法,统一使用GetObjColume来获取列数据的指针。同时,BeatObjectData结构中需要加入Enabled和HeadHitted两个bool型变量,用于指示物件是否可见,以及物件的头部是否已被点击(仅限于条):
class SongData
{
//...
//
public:
std::vector<BeatObjectData>* GetObjColume(int pIndex);
//
//...
};
struct BeatObjectData
{
//...
//
bool Enabled;
bool HeadHitted;
BeatObjectData()
{
//...
//
this->Enabled = true;
this->HeadHitted = false;
}
};
#endif // __SONG_DATA_H__
cpp:
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 nullptr;
}
}
★修改后其他调用SongData的部分也需要做修改,把常量引用改为指针。修改很简单,这里不细说了。
【音频系统的引入】
对于这个项目,我们需要音频引擎提供如下功能:
1、 音乐和音效分轨播放,即播放音乐的时候音效也可以播放出来,声音不冲突;
2、 相同音效分轨播放,即同一音效可以叠加播放;
3、 控制音乐播放、暂停、继续、停止
4、 获取当前音乐的播放时间,精确到ms
Cocos2dx自带一个SimpleAudioEngine,可以做到上面1和3的功能。要做到2和4则需要修改底层代码。是个Cocos2dx码农都知道这引擎是相当地不好用。当然本来这玩意的名字都说明了它是一个简单的音频引擎,图森破。
改这里的底层代码会遇到一个很蛋疼的问题:SimpleAudioEngine在不同平台上的实现都不一样,基本上是做哪个平台就得改一下对应的代码。我是懒逼,懒得去折腾这个。
所以这里隆重向大家安利一个灰常强大的第三方的音频库:FMOD。我最早是在解包LOL的语音的时候发现他们用了这玩意,然后查了一下卧槽通用API跨平台挺牛逼啊。据我所知目前国内不少手游使用了FMOD。
FMOD是什么这里不做解释了,有兴趣的自行百度百科吧。直接放出地址:FMOD Ex地址
请注意FMOD不是一个完全免费的库。商业项目中使用FMOD需要购买它的许可。
往下拉一点可以看到FMOD Ex Programmer’s API,下载对应平台的版本装上即可。装好后,目录下有个api文件夹,里面有C#接口、头文件、lib和dll。然后把FMOD的头文件拷贝到Classes下,引入到项目中。
如果要在其他平台使用FMOD(比如下文说的部署到WP上),只需要换一下lib和dll就行,代码层是不需要修改的,那是相当地方便(说实话我很希望Cocos2dx的音频引擎也能有这么牛逼啊,毕竟这玩意的商业许可证不便宜)。
然后在VS中打开项目属性,打开链接器项,把lib文件名加入到附加依赖项中。别忘了把lib文件和fmodex.dll文件拷贝到输出目录(Debug.win32或Release.win32)下。
然后我直接放代码了,FMOD怎么用不是这一系列文章的重点,自行看安装目录中的Sample吧:
#ifndef _SOUND_SYSTEM_H_
#define _SOUND_SYSTEM_H_
#include <string>
#include "HitJudger.h"
#include "fmod/fmod.hpp"
#include "fmod/fmod_errors.h"
using namespace FMOD;
class SoundSystem
{
public:
SoundSystem();
~SoundSystem();
public:
void SetSong(const std::string& pFilename);
void PlaySong();
void PauseSong();
void ResumeSong();
void StopSong();
long GetCurPosition();
void PlayHitSound(const HitJudgeType& pType, int pColume);
private:
void PlaySound(Sound* pSound, bool pIsSong, int pColume);
void CreateSound(Sound** pOutSound, const char* pFilename, bool pIsStream);
void ERRCHECK(FMOD_RESULT result);
private:
System *m_pSystem;
Sound *m_pSong;
Sound *m_pSound_Prefect,
*m_pSound_Great,
*m_pSound_Good,
*m_pSound_Bad,
*m_pSound_Miss;
Channel *m_pChannel_Song;
Channel *m_Channel_HitSound[9];
};
#endif // _SOUND_SYSTEM_H_
实现:
#include "SoundSystem.h"
#include "Common.h"
USING_NS_CC;
#define SAFE_RELEASE_FMOD_COMPONENT(__COM__) { if((__COM__)) (__COM__)->release(); (__COM__) = nullptr; }
SoundSystem::SoundSystem()
: m_pSong(nullptr)
, m_pChannel_Song(nullptr)
{
// 初始化系统
//
auto result = FMOD::System_Create(&this->m_pSystem);
ERRCHECK(result);
unsigned int version;
result = this->m_pSystem->getVersion(&version);
ERRCHECK(result);
if (version < FMOD_VERSION)
{
log("Error!\r\nYou are using an old version of FMOD %08x.\r\nThis program requires %08x\n", version, FMOD_VERSION);
WASSERT(false);
}
result = this->m_pSystem->init(32, FMOD_INIT_NORMAL, 0);
ERRCHECK(result);
// 初始化音轨
//
for (int i = 0; i < 9; i++)
{
this->m_Channel_HitSound[i] = nullptr;
}
// 创建打击音效
//
this->CreateSound(
&this->m_pSound_Prefect,
FileUtils::getInstance()->fullPathForFilename("perfect.wav").c_str(),
false);
this->CreateSound(
&this->m_pSound_Great,
FileUtils::getInstance()->fullPathForFilename("great.wav").c_str(),
false);
this->CreateSound(
&this->m_pSound_Good,
FileUtils::getInstance()->fullPathForFilename("good.wav").c_str(),
false);
this->CreateSound(
&this->m_pSound_Bad,
FileUtils::getInstance()->fullPathForFilename("bad.wav").c_str(),
false);
this->CreateSound(
&this->m_pSound_Miss,
FileUtils::getInstance()->fullPathForFilename("miss.wav").c_str(),
false);
}
void SoundSystem::SetSong(const std::string& pFilename)
{
SAFE_RELEASE_FMOD_COMPONENT(this->m_pSong);
this->CreateSound(
&this->m_pSong,
FileUtils::getInstance()->fullPathForFilename(pFilename).c_str(),
true);
}
void SoundSystem::PlaySong()
{
WASSERT(this->m_pSong);
this->PlaySound(this->m_pSong, true, -1);
}
void SoundSystem::PauseSong()
{
WASSERT(this->m_pSong);
auto result = this->m_pChannel_Song->setPaused(true);
ERRCHECK(result);
}
void SoundSystem::ResumeSong()
{
WASSERT(this->m_pSong);
auto result = this->m_pChannel_Song->setPaused(false);
ERRCHECK(result);
}
void SoundSystem::StopSong()
{
WASSERT(this->m_pSong);
auto result = this->m_pChannel_Song->stop();
ERRCHECK(result);
}
long SoundSystem::GetCurPosition()
{
WASSERT(this->m_pSong);
unsigned int ret = -1;
auto result = this->m_pChannel_Song->getPosition(&ret, FMOD_TIMEUNIT_MS);
ERRCHECK(result);
return ret;
}
void SoundSystem::PlayHitSound(const HitJudgeType& pType, int pColume)
{
Sound* hitSound = nullptr;
switch (pType)
{
case HitJudgeType::Perfect: hitSound = this->m_pSound_Prefect; break;
case HitJudgeType::Great: hitSound = this->m_pSound_Great; break;
case HitJudgeType::Good: hitSound = this->m_pSound_Good; break;
case HitJudgeType::Bad: hitSound = this->m_pSound_Bad; break;
case HitJudgeType::Miss: hitSound = this->m_pSound_Miss; break;
}
if (hitSound)
{
this->PlaySound(hitSound, false, pColume);
}
}
void SoundSystem::PlaySound(Sound* pSound, bool pIsSong, int pColume)
{
auto result = this->m_pSystem->playSound(
pIsSong ? FMOD_CHANNEL_REUSE : FMOD_CHANNEL_FREE,
pSound,
false,
pIsSong ? &this->m_pChannel_Song : &this->m_Channel_HitSound[pColume]);
ERRCHECK(result);
}
void SoundSystem::CreateSound(Sound** pOutSound, const char* pFilename, bool pIsStream)
{
FMOD_RESULT result;
if (pIsStream)
{
result = this->m_pSystem->createStream(
pFilename,
FMOD_HARDWARE | FMOD_LOOP_OFF | FMOD_2D,
0,
pOutSound);
}
else
{
result = this->m_pSystem->createSound(
pFilename,
FMOD_HARDWARE | FMOD_CREATESAMPLE | FMOD_LOOP_OFF | FMOD_2D,
0,
pOutSound);
}
ERRCHECK(result);
}
void SoundSystem::ERRCHECK(FMOD_RESULT pResult)
{
if (pResult != FMOD_OK)
{
log(FMOD_ErrorString(pResult));
WASSERT(false);
}
}
SoundSystem::~SoundSystem()
{
SAFE_RELEASE_FMOD_COMPONENT(this->m_pSong);
SAFE_RELEASE_FMOD_COMPONENT(this->m_pSound_Prefect);
SAFE_RELEASE_FMOD_COMPONENT(this->m_pSound_Great);
SAFE_RELEASE_FMOD_COMPONENT(this->m_pSound_Good);
SAFE_RELEASE_FMOD_COMPONENT(this->m_pSound_Bad);
SAFE_RELEASE_FMOD_COMPONENT(this->m_pSound_Miss);
// 不可在释放系统前释放音频,否则报错
//
auto result = this->m_pSystem->close();
ERRCHECK(result);
SAFE_RELEASE_FMOD_COMPONENT(this->m_pSystem);
}
【整合模块】
让我们把完成的模块链接在一起,再修改一下之前的代码,为后面的部署做准备。
首先把HitJudger和SoundSystem加入GameModule中。代码和其他模块一致,别忘了在析构方法中CC_SAFEDELETE一下。同时修改一下SetSongData方法(不修改的话部署后找不到文件会崩):
void GameModule::SetSongData(const std::string& pName)
{
CC_SAFE_DELETE(m_pSongData);
m_pSongData = new SongData(FileUtils::getInstance()->fullPathForFilename(pName));
}
然后是SongTimer类。因为加入了音频引擎,时间应当从引擎中取得,而不是逐桢递加:
long SongTimer::GetTime()
{
return GameModule::GetSongSystem()->GetCurPosition();
}
Common.h中的WASSERT宏调用了DebugBreak方法用于触发断点。但是这个方法是个WinAPI,上了其他平台就没这玩意了。同时考虑到如果项目编译Release版本,断言不需要了,所以得改改:
#ifdef _DEBUG
#if CC_TARGET_PLATFORM == CC_PLATFORM_WIN32
#define WASSERT(__COND__) if (!(__COND__)) { DebugBreak(); }
#else
#define WASSERT(__COND__) CC_ASSERT(__COND__)
#endif
#else
#define WASSERT(__COND__) do {} while (0);
#endif
★经测试__debugbreak方法在WP上可用,但是MSDN说这方法是“Microsoft Specific”的,估计在安卓和iOS等其他平台没有对应的实现。
接下来在LiveController类中加入一个变量和一个方法。变量用于保存离按钮最近的活动的物件索引,方法用于接受UI发送的消息并调用判定器:
public:
void HitButtonsOnEvent(int pColume, bool pIsPress);
private:
int m_CurIndexes[9];
m_CurIndexes变量在构造方法中需要全部赋值0,按钮事件方法实现如下:
void LiveController::HitButtonsOnEvent(int pColume, bool pIsPress)
{
if (this->m_CurStatus != LCStatus::Running)
{
return;
}
auto songData = GameModule::GetSongData();
auto objData = &(songData->GetObjColume(pColume)->at(this->m_CurIndexes[pColume]));
auto curTime = GameModule::GetTimer()->GetTime();
auto judger = GameModule::GetHitJudger();
auto result = HitJudgeType::None;
if (pIsPress)
{
result = judger->JudgeHead(songData->GetJudgement(), curTime, objData);
if (result != HitJudgeType::None)
{
if (objData->Type == BeatObjectType::Block)
{
objData->Enabled = false;
}
else
{
objData->HeadHitted = true;
}
}
}
else if (objData->Type == BeatObjectType::Strip && objData->HeadHitted)
{
result = judger->JudgeTail(songData->GetJudgement(), curTime, objData);
objData->Enabled = false;
}
GameModule::GetSongSystem()->PlayHitSound(result, pColume);
}
同时修改Update方法,加入Miss判定:
void LiveController::Update()
{
//...
//
// 防止Strip在飞行时消失
//
if (bottomIndex > 0)
{
auto obj = columeData->at(bottomIndex - 1);
if (obj.Type == BeatObjectType::Strip)
{
if (obj.EndTime > bottomTime && obj.Enabled)
{
bottomIndex--;
}
}
}
this->m_CurIndexes[i] = bottomIndex;
// Miss判定
//
auto curObj = &columeData->at(bottomIndex);
if (GameModule::GetHitJudger()->JudgeMiss(songData->GetJudgement(), curTime, curObj))
{
curObj->Enabled = false;
GameModule::GetSongSystem()->PlayHitSound(HitJudgeType::Miss, i);
if (bottomIndex > 0)
{
bottomIndex--;
}
}
// 更新物件
// ...
}
在ResetObjs方法中加入初始化音频文件的代码:
void LiveController::Reset()
{
auto data = GameModule::GetSongData();
WASSERT(data);
this->m_pBeatObjectManager->ResetObjsFromData(data);
GameModule::GetSongSystem()->SetSong(data->GetSongFilename());
}
再修改一下StartLive方法,加入播放歌曲的代码:
void LiveController::StartLive()
{
this->m_CurStatus = LCStatus::Running;
GameModule::GetSongSystem()->PlaySong();
}
最后是最上面的GetNearlyIndex方法,插入一小段代码以跳过不显示的物件:
inline int GetNearlyIndex(int pTime, const std::vector<BeatObjectData>* pColume)
{
//
//...
while ((index_Start + 1) < index_End)
{
if (!pColume->at(index_Start).Enabled)
{
index_Start++;
continue;
}
//...
//
}
然后是LiveScene类,在init方法中加入HitInputUI,然后把之前用代码写死的坐标改成相对坐标:
bool LiveScene::init()
{
if (!Layer::init())
{
return false;
}
Size visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
// 加入背景图
//
auto bg = Sprite::create("bg.jpg");
bg->setPosition(Vec2(
visibleSize.width / 2 + origin.x,
visibleSize.height / 2 + origin.y));
this->addChild(bg);
// 加上黑色半透明蒙层
//
auto colorLayer = LayerColor::create(Color4B(0, 0, 0, 192));
this->addChild(colorLayer);
// 初始化BeatInputUI
//
auto hiu = HitInputUI::create();
hiu->setPosition(Vec2(
visibleSize.width / 2 + origin.x,
480 + origin.y));
this->addChild(hiu);
// 初始化BeatObjectManager
//
auto bom = BeatObjectManager::create();
bom->setPosition(Vec2(
visibleSize.width / 2 + origin.x,
480 + origin.y));
this->addChild(bom);
// 初始化歌曲数据
//
GameModule::SetSongData("start_dash.xml");
// 初始化控制器
//
GameModule::GetLiveController()->SetBeatObjectManager(bom);
GameModule::GetLiveController()->ResetObjs();
this->runAction(Sequence::createWithTwoActions(
DelayTime::create(2),
CallFunc::create([]()
{
GameModule::GetLiveController()->StartLive();
})));
this->scheduleUpdate();
return true;
}
★我发现之前很逗逼地把BeatObjectManager做成LiveScene类的成员变量了,现在看来完全没有必要,删掉吧。
BeatObjectManager的init方法也要修改一下,去掉设置自身坐标的代码,也就是对setPosition()的调用。删一行而已,代码就不发了。
看了视频可以知道LL里面的条飞到按钮上之后,头部就不会移动了。为了做到这个效果来修改一下BeatObject类的setPositionY方法:
void BeatObject::setPositionY(float y)
{
// 如果该物件是一个Block
//
if (this->IsBlock())
{
Node::setPositionY(y);
auto headScale = GetMoveScale(y);
this->m_pHead->setScale(headScale);
this->m_pHead->setVisible(headScale > 0.05f);
}
// 如果该物件是一个Strip,则需要处理其身体和尾部
//
else
{
if (y < -400)
{
Node::setPositionY(-400);
}
else
{
Node::setPositionY(y);
}
auto posY = this->getPositionY();
auto headScale = GetMoveScale(posY);
this->m_pHead->setScale(headScale);
this->m_pHead->setVisible(headScale > 0.05f);
// 模拟无限远处飞来的效果,保证尾部的y坐标小于0
//
if (y + this->m_fLength > 0)
{
this->m_fCurLength = posY > -400 ? -posY : 400;
}
else
{
this->m_fCurLength = posY > -400 ? this->m_fLength : 400 + y + this->m_fLength;
}
if (this->m_fCurLength < 0)
{
this->m_fCurLength = 0;
}
auto tailScale = GetMoveScale(posY + this->m_fCurLength);
this->m_pTail->setPositionY(this->m_fCurLength);
this->m_pTail->setScale(tailScale);
this->m_pTail->setVisible(tailScale > 0.05f);
auto harfHeadWidth = headScale * 124 / 2.0f;
auto harfTailWidth = tailScale * 124 / 2.0f;
this->m_pBody->SetVertex(
Vec2(-harfTailWidth, this->m_fCurLength),
Vec2(-harfHeadWidth, 0),
Vec2(harfTailWidth, this->m_fCurLength),
Vec2(harfHeadWidth, 0));
}
}
★和LL不同的是,LL中条的尾部可以飞过按钮,我不认为这是一个好的设计,所以代码中限制条的长度最小为0,即条的尾部最远飞到按钮上。
最后改一下AppDelegate类的applicationDidFinishLaunching方法,把设置设计分辨率的调用放在if外(修改前WP上测试发现分辨率总是设置了无效,后来才发现WP上就没进if,听说安卓也是这样的):
bool AppDelegate::applicationDidFinishLaunching() {
//
//...
if(!glview) {
glview = GLView::create("My Game");
director->setOpenGLView(glview);
}
glview->setDesignResolutionSize(960, 640, ResolutionPolicy::SHOW_ALL);
//...
//
}
感觉改了好多东西,都是以前自己给自己挖的坑orz修改完成后,可以编译运行了。如果不出错的话,你会看到这样的界面,还能听到歌曲和音效。
用LL的图片怕起纠纷,所以我自己做了一套按钮,顺便想起武媚娘剪胸事件,干脆把背景也换了:
当然用鼠标的话只能一次点一个,基本上没法玩,接下来部署到移动设备上试试。
【部署到WP设备】
因为设备原因,以及家里电脑没装eclipse还有懒得去下ADK、NDK,只有部署到WP了。
啥,你问我WP是啥?既然你诚心诚意地问了,那么我大发慈悲地建议你略过这一小节,或者去百度一下。
在公司部署过安卓项目,感觉对比一下WP真的是比安卓的部署调试爽太多了,那是相当地爽,简直和iOS有一拼。而且在VS里面可以直接对真姬,呸,真机进行断点调试native层的代码。
部署只需要四个步骤。首先我们调整一下VS的WP项目文件。打开proj.wp8-xaml目录下的sln文件,将xxxxxxComponent(xxxxxx是你创建Cocos2dx工程时输入的名字)中的Classes筛选器下面所有代码清空,把我们的Classes目录下的所有文件拖进去,别忘了FMOD的头文件。
然后,我们需要下载FMOD的WP8版本。安装后,在xxxxxxComponent项目中的链接器选项中加入fmodex_80_arm.lib(如果在WP模拟器上调试,则需要fmodex_80_x86.lib)。
再然后,把fmodex_80_arm.dll(如果在WP模拟器上调试,则需要fmodex_80_x86.dll)拖到xxxxxx项目中,调整它的属性:复制到输出目录 - 始终复制,生成操作 - 内容。
如果在其他平台上使用FMOD,也需要引入对应的库文件。
最后,可以编译项目了。要让编译器把应用部署在设备上运行,请这样设置:
插入已经使用开发者账号解锁的WP设备,保持屏幕打开,编译完成后VS会将项目部署到手机上并运行。
如果想调试C++层的代码,需要在xxxxxx项目的属性——调试页卡中,将“UI任务”设为“仅限本机”即可。设置后,在cpp中的断点啊log啊啥的都生效了。
如果要修改应用的图标啊名称啊啥的,双击打开xxxxxx项目下的Properties——WMAppManifest.xml,可以直接进行修改,那是相当地方便。
然后试试我们的成果吧,可以试着打一下~
【本章结束语】
最头疼的一章终于弄出来了。做用户输入的时候试了好几种方案最后才定下来。所以建议各位在遇上复杂的,一时想不透的逻辑的时候,拿出笔记本或者打开Visio这类的软件,把思路画下来,整理好,弄清楚了再敲代码,省得返工。
本章用到的资源:点击下载(解压后放在Resources目录下,完全覆盖已有文件。不包含FMOD组件,请自行上官网下载)
★打击音效资源取自网络
下一章我们给游戏加入显示分数、血条等等的UI,以及打击的特效。
最后感叹一下如果要做下一系列我一定全部做好了再写博文……免得遇上加班等情况延期发布……毕竟加班乃码农之常情orz