仿《雷霆戰機》飛行射擊手遊開發--資源預加載

項目源碼庫:https://git.oschina.net/thorqq/RaidenFreenode

 

    絕大多數遊戲在啓動後首先出現的是一個「載入中」的場景,此場景的用處是將遊戲所需的圖片、音樂、數據等資源從存儲卡(或磁盤、閃存)讀入內存,這樣,後面須要用到這些資源時,能夠直接從內存讀取,以加快遊戲的運行,提升流暢性。下面,就對資源的預加載機制作一個介紹。git

資源的類型

預加載的目的是爲了後續讀取的快捷,因此,通常會預加載那些較大較複雜的文件,例如如下這些:sql

  • 單張大圖:背景大圖
  • 合成圖:可多幅圖片合成的大圖,這裏咱們使用TexturePacker合成plist+png文件
  • 骨骼動畫:使用Cocos Skeletal Animation Editor建立的骨骼動畫文件,ExportJson+plist+png文件
  • 場景:使用Cocos Studio建立的csd文件
  • 聲音:ogg音樂文件
  • 本地數據:遊戲存檔數據(格式爲json文件)、遊戲配置數據(例如關卡、飛機屬性、子彈屬性等固定的數據,格式爲sqlite數據庫文件)
  • 遠程數據:因爲本遊戲是弱聯網遊戲,因此保存在服務器上的數據很少。這裏僅僅實現了用戶登陸、獲取時間的功能

下面,咱們將逐一介紹不一樣資源載入的方法。數據庫

加載方法

單張大圖

  1. 定義std::vector<std::string> m_imageArray,將須要加載的圖片路徑放到容器中
  2. 對每一個圖片逐個調用Director::getInstance()->getTextureCache()->addImageAsync()函數進行加載,注意他的第二個參數CC_CALLBACK_1(Preload::asynLoadingImageDone, this, m_imageArray[i]),當一張圖片加載結束後,系統就會調用Preload::asynLoadingImageDone函數,同時傳入圖片的路徑做爲輸入參數。
  3. 在回調函數asynLoadingImageDone中,首先要通知界面加載進度,而後根據圖片的總數和待加載數判斷是否已經所有記載完成,若所有加載成功,則通知loadingDone(PreloadType::Image)

詳細的代碼以下所示:json

//一、須要加載的png或jpg
    m_imageArray.push_back("BigImg/Bag_Bg.png");
    m_imageArray.push_back("BigImg/BigScreen_Bg.png");
    m_imageArray.push_back("BigImg/Daily_Bg.png");
    m_imageArray.push_back("BigImg/MainUI_Bg.jpg");


void Preload::asynLoadingImage()
{
    //二、將圖片加入全局cache中
    m_iImageCnt = m_imageArray.size();
    for (unsigned i = 0; i < m_imageArray.size(); i++)
    {
        Director::getInstance()->getTextureCache()->addImageAsync(
            m_imageArray[i],
            CC_CALLBACK_1(Preload::asynLoadingImageDone, this, m_imageArray[i]));
    }
}

//三、單張圖片加載成功後的回調函數
void Preload::asynLoadingImageDone(Texture2D* texture, const std::string& filename)
{
    //通知觀察者加載進度
    this->notifyProgress(++m_iTmpProgress);
    m_iImageCnt--;
    //所有加載完成
    if (0 == m_iImageCnt)
    {
        m_bImageLoaded = true;
        this->loadingDone(PreloadType::Image);
    }
}

合成圖

合成圖的加載與單張圖片的加載相似,不一樣之處在於在回調函數中多了一步加載plist文件:服務器

SpriteFrameCache::getInstance()->addSpriteFramesWithFile(file.append(".plist"), texture);網絡

//plist圖片
    std::vector<std::string> m_plistArray;

    //一、須要加載的圖片,不包含後綴名
    m_plistArray.push_back("Bag");
    m_plistArray.push_back("Common");
    m_plistArray.push_back("Daily");

void Preload::asynLoadingPlist()
{
    //二、加載圖片文件
    m_iImagePlistCnt = m_plistArray.size();
    for (unsigned i = 0; i < m_plistArray.size(); i++)
    {
        Director::getInstance()->getTextureCache()->addImageAsync(
            std::string(m_plistArray[i]).append(".png"),
            CC_CALLBACK_1(Preload::asynLoadingPlistDone, this, m_plistArray[i]));
    }
}

void Preload::asynLoadingPlistDone(Texture2D* texture, const std::string& filename)
{
    this->notifyProgress(++m_iTmpProgress);

    //三、加載plist文件
    std::string file = filename;
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile(file.append(".plist"), texture);
    m_iImagePlistCnt--;

    if (0 == m_iImagePlistCnt)
    {
        //所有加載完成
        m_bImagePlistLoaded = true;
        this->loadingDone(PreloadType::Plist);
    }
}

骨骼動畫

骨骼動畫也是相似的加載方法,先使用addArmatureFileInfoAsync()函數加載骨骼動畫的圖片、合圖信息(plist文件)、動畫信息(ExportJson文件),而後回調函數asynLoadingArmatureDone()。app

std::vector<std::string> m_armatureArray;
    m_armatureArray.push_back("Anim/Anim_Plane_01");
    m_armatureArray.push_back("Anim/Anim_Plane_02");
    m_armatureArray.push_back("Anim/Anim_Plane_03");

void Preload::asynLoadingArmature()
{
    auto p = m_armatureArray[m_iArmatureCnt];
    DEBUG_LOG("Preload::asynLoadingArmature: %s", p.c_str());
    ArmatureDataManager::getInstance()->addArmatureFileInfoAsync(
        std::string(p).append("0.png"),
        std::string(p).append("0.plist"),
        std::string(p).append(".ExportJson"),
        this,
        CC_SCHEDULE_SELECTOR(Preload::asynLoadingArmatureDone));
}

void Preload::asynLoadingArmatureDone(float dt)
{
    this->notifyProgress(++m_iTmpProgress);

    m_iArmatureCnt++;
    if (m_armatureArray.size() == m_iArmatureCnt)
    {
        m_bArmatureLoaded = true;
        this->loadingDone(PreloadType::Armature);
    }
    else
    {
        asynLoadingArmature();
    }
}

場景

場景並無特殊的異步加載函數,只能經過CSLoader::createNode()和CSLoader::createTimeline()根據csd文件生成node,而後保存到自定義的map中,之後要使用場景數據時,從map中獲取。異步

注意,此加載方法在cocos2dx-3.4中能夠正常運行,在3.8中會出現錯誤,緣由未知。不過加載單個場景文件的時間很短,通常並不會影響遊戲的體驗,因此本遊戲的最新版本中並無預加載場景文件。tcp

std::vector<std::string> m_uiArray;
    std::map<std::string, Node*> m_uiMap;

    //菜單
    m_uiArray.push_back("Bag.csb");
    m_uiArray.push_back("Daily.csb");
    m_uiArray.push_back("Instruction.csb");

void Preload::syncLoadingUI()
{
    //不能在非主線程中調用CSLoader::createNode,不然會致使OpenGL異常
    for (auto file : m_uiArray)
    {
        auto node = Preload::getUI(file);
        node->retain();
        m_uiMap.insert(std::map<std::string, Node*>::value_type(file, node));

        auto timeLine = CSLoader::createTimeline(file);
        timeLine->retain();
        m_actionMap.insert(std::map<std::string, cocostudio::timeline::ActionTimeline*>::value_type(file, timeLine));

        DEBUG_LOG("Preload::syncLoadingUI: %s", file.c_str());
        this->notifyProgress(++m_iTmpProgress);
    }

    m_bUILoaded = true;
    this->loadingDone(PreloadType::Ui);
}

Node* Preload::getUI(const std::string& filename)
{
    DEBUG_LOG("Preload::getUI: %s", filename.c_str());
    return CSLoader::createNode(filename);;

    //cocos2dx-3.8 不支持如下操做。3.4支持
    //auto ui = m_uiMap.find(filename);
    //if (ui != m_uiMap.end())
    //{
    //    return ui->second;
    //}
    //else
    //{
    //    auto csb = CSLoader::createNode(filename);
    //    csb->retain();
    //    m_uiMap.insert(std::map<std::string, Node*>::value_type(filename, csb));

    //    return csb;
    //}
}

聲音

因爲cocos提供了新老兩種音頻接口,因此聲音文件的預加載也分紅兩種。

對於老的接口,需區分音樂和音效文件,而且函數沒有返回值;

對於新的接口,不區分音樂和音效文件,經過回調來判斷加載的結果。

//老的音頻接口
CocosDenshion::SimpleAudioEngine::getInstance()->preloadBackgroundMusic(filename);
CocosDenshion::SimpleAudioEngine::getInstance()->preloadEffect(filename);

//新的音頻接口
AudioEngine::preload(filename, [filename](bool isSuccess){
    if (!isSuccess)
    {
        DEBUG_LOG("Load fail: %s", path.c_str());
    }
});

本地數據

本地數據包括了:存檔數據、遊戲配置數據,及其餘一些定製化的數據。這裏咱們可使用cocos提供的異步任務接口+回調加載結果來進行預加載。

void Preload::asynLoadingDatabase()
{
    auto loadEnd = [this](void*)
    {
        DEBUG_LOG("asynLoadingDatabase OK");

        m_bOtherLoaded = true;
        this->loadingDone(PreloadType::Other);
    };

    AsyncTaskPool::getInstance()->enqueue(AsyncTaskPool::TaskType::TASK_IO, loadEnd, (void*)NULL, [this]()
    {
        if (!GlobalData::getInstance()->initialize(this))
        {
            CCLOG("Initialize globla data failed");
            this->notifyError("Initialize globla data failed");
            return;
        }

        m_iTmpProgress += PreloadProgress::GlobalData;
        this->notifyProgress(m_iTmpProgress);

        if (!GameData::getInstance()->loadData())
        {
            CCLOG("Initialize game data failed");
            this->notifyError("Initialize game data failed");
            return;
        }

        m_iTmpProgress += PreloadProgress::GameData;
        this->notifyProgress(m_iTmpProgress);

        if (!AchievementMgr::getInstance()->init())
        {
            CCLOG("Initialize achievement data failed");
            this->notifyError("Initialize achievement data failed");
            return;
        }

        m_iTmpProgress += PreloadProgress::AchievementMgr;
        this->notifyProgress(m_iTmpProgress);

        Sound::preload(this);

        m_iTmpProgress += PreloadProgress::Sound;
        this->notifyProgress(m_iTmpProgress);
    });
}

遠程數據

遠程數據通常是經過發送異步http或者其餘tcp請求來實現數據的加載,根據網絡協議的不一樣,相關的接口也各不相同,這裏再也不詳述。

加載界面

在此加載界面中,咱們使用一個儀表盤和轉動的指針來告訴用戶當前的加載進度。那麼,後臺加載任務與前臺的指針轉動是如何關聯起來的呢?咱們使用了觀察者模式。下面上一張百度找出的觀察者模式的圖:


Observer模式的角色:
Subject(被觀察者)
    被觀察的對象。當須要被觀察的狀態發生變化時,須要通知隊列中全部觀察者對象。Subject須要維持(添加,刪除,通知)一個觀察者對象的隊列列表。
ConcreteSubject
    被觀察者的具體實現。包含一些基本的屬性狀態及其餘操做。
Observer(觀察者)
    接口或抽象類。當Subject的狀態發生變化時,Observer對象將經過一個callback函數獲得通知。
ConcreteObserver
    觀察者的具體實現。獲得通知後將完成一些具體的業務邏輯處理。

 

在本遊戲中實現了一個簡化版的觀察者模式:

一、首先,咱們定義一個被觀察者抽象類。其中定義了開始、進度、錯誤、警告、結束等接口。

class PreloadListener
{
public:
    virtual void onStart() = 0;
    virtual void onProgress(int percent) = 0;
    virtual void onError(const char* info) = 0;
    virtual void onWarning(const char* info) = 0;
    virtual void onEnd(PreloadError errorCode) = 0;
};

二、定義載入界面場景,繼承自PreloadListener,並實現onXXX接口。

class LoadingLayer :
    public Layer, public PreloadListener
{
public:
    static Scene* scene();

    LoadingLayer();
    virtual ~LoadingLayer();

    virtual bool init();
    virtual void update(float dt) override;

    CREATE_FUNC(LoadingLayer);

    void initUI();
    void ToMainMenu();

    virtual void onStart() override;
    virtual void onProgress(int percent) override;
    virtual void onError(const char* info) override;
    virtual void onWarning(const char* info) override;
    virtual void onEnd(PreloadError errorCode) override;

private:
    Node* m_pRootNode;

    Sprite* m_pNeedle;
    ui::LoadingBar* m_pLoadingBar;
    ui::Text* m_pTxtErrorInfo;

    long m_iBeginTime;
    long m_iEndTime;

    int m_iStart;
};

特別注意一下onProgress接口,這裏須要實現指針轉動的邏輯:

void LoadingLayer::onProgress(int percent)
{
    float degree = LoadingLayerConstant::NeedleMinDegree +
        (LoadingLayerConstant::NeedleMaxDegree - LoadingLayerConstant::NeedleMinDegree) * percent / 100;
    m_pNeedle->setRotation(degree);
}

三、在加載任務中添加上報載入進度的函數。這樣,每當載入一張圖片或者任意一個資源文件的時候,就能夠調用notifyProgress函數以使得界面上的指針轉動了。

void Preload::notifyProgress(int progress)
{
    //這裏的m_pListener其實就是LoadingLayer的實例
    if (m_pListener)
    {
        m_pListener->onProgress((int)(progress * 100.f / m_iAllProgress));
    }
}

 

轉載請註明:http://www.javashuo.com/article/p-wmbojtio-bo.html

項目首頁:https://www.oschina.net/p/raiden

相關文章
相關標籤/搜索