項目源碼庫:https://git.oschina.net/thorqq/RaidenFreenode
絕大多數遊戲在啓動後首先出現的是一個「載入中」的場景,此場景的用處是將遊戲所需的圖片、音樂、數據等資源從存儲卡(或磁盤、閃存)讀入內存,這樣,後面須要用到這些資源時,能夠直接從內存讀取,以加快遊戲的運行,提升流暢性。下面,就對資源的預加載機制作一個介紹。git
預加載的目的是爲了後續讀取的快捷,因此,通常會預加載那些較大較複雜的文件,例如如下這些:sql
下面,咱們將逐一介紹不一樣資源載入的方法。數據庫
詳細的代碼以下所示: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)); } }