zoukankan      html  css  js  c++  java
  • 我用Cocos2dx模拟《Love Live!学院偶像祭》的Live场景(四)

    【前言和思路整理】

      千呼万唤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

  • 相关阅读:
    《破茧成蝶》读书笔记——价值
    《破茧成蝶》读书笔记——技能(2)
    《破茧成蝶》读书笔记——技能(1)
    《破茧成蝶》读书笔记——信念
    《HeadFirst:HTML & CSS(第二版)》——开篇废话
    图片旋转方法
    uploadify 3.0 详细使用说明
    提高mysql千万级大数据SQL查询优化30条经验(Mysql索引优化注意)
    Sql 中 不等于'<>'与 NULL
    c#中获取存储过程的返回值(return Value)
  • 原文地址:https://www.cnblogs.com/GuyaWeiren/p/4535066.html
Copyright © 2011-2022 走看看