絕大多數游戲在啟動後首先出現的是一個“載入中”的場景,此場景的用處是將游戲所需的圖片、音樂、數據等資源從存儲卡(或磁盤、閃存)讀入內存,這樣,後面需要用到這些資源時,可以直接從內存讀取,以加快游戲的運行,提高流暢性。下面,就對資源的預加載機制做一個介紹。
預加載的目的是為了後續讀取的快捷,所以,一般會預加載那些較大較復雜的文件,例如以下這些:
下面,我們將逐一介紹不同資源載入的方法。
詳細的代碼如下所示:
1 //1、需要加載的png或jpg 2 m_imageArray.push_back("BigImg/Bag_Bg.png"); 3 m_imageArray.push_back("BigImg/BigScreen_Bg.png"); 4 m_imageArray.push_back("BigImg/Daily_Bg.png"); 5 m_imageArray.push_back("BigImg/MainUI_Bg.jpg"); 6 7 8 void Preload::asynLoadingImage() 9 { 10 //2、將圖片加入全局cache中 11 m_iImageCnt = m_imageArray.size(); 12 for (unsigned i = 0; i < m_imageArray.size(); i++) 13 { 14 Director::getInstance()->getTextureCache()->addImageAsync( 15 m_imageArray[i], 16 CC_CALLBACK_1(Preload::asynLoadingImageDone, this, m_imageArray[i])); 17 } 18 } 19 20 //3、單張圖片加載成功後的回調函數 21 void Preload::asynLoadingImageDone(Texture2D* texture, const std::string& filename) 22 { 23 //通知觀察者加載進度 24 this->notifyProgress(++m_iTmpProgress); 25 m_iImageCnt--; 26 //全部加載完成 27 if (0 == m_iImageCnt) 28 { 29 m_bImageLoaded = true; 30 this->loadingDone(PreloadType::Image); 31 } 32 }
合成圖的加載與單張圖片的加載類似,不同之處在於在回調函數中多了一步加載plist文件:
SpriteFrameCache::getInstance()->addSpriteFramesWithFile(file.append(".plist"), texture);
1 //plist圖片 2 std::vector<std::string> m_plistArray; 3 4 //1、需要加載的圖片,不包含後綴名 5 m_plistArray.push_back("Bag"); 6 m_plistArray.push_back("Common"); 7 m_plistArray.push_back("Daily"); 8 9 void Preload::asynLoadingPlist() 10 { 11 //2、加載圖片文件 12 m_iImagePlistCnt = m_plistArray.size(); 13 for (unsigned i = 0; i < m_plistArray.size(); i++) 14 { 15 Director::getInstance()->getTextureCache()->addImageAsync( 16 std::string(m_plistArray[i]).append(".png"), 17 CC_CALLBACK_1(Preload::asynLoadingPlistDone, this, m_plistArray[i])); 18 } 19 } 20 21 void Preload::asynLoadingPlistDone(Texture2D* texture, const std::string& filename) 22 { 23 this->notifyProgress(++m_iTmpProgress); 24 25 //3、加載plist文件 26 std::string file = filename; 27 SpriteFrameCache::getInstance()->addSpriteFramesWithFile(file.append(".plist"), texture); 28 m_iImagePlistCnt--; 29 30 if (0 == m_iImagePlistCnt) 31 { 32 //全部加載完成 33 m_bImagePlistLoaded = true; 34 this->loadingDone(PreloadType::Plist); 35 } 36 }
骨骼動畫也是類似的加載方法,先使用addArmatureFileInfoAsync()函數加載骨骼動畫的圖片、合圖信息(plist文件)、動畫信息(ExportJson文件),然後回調函數asynLoadingArmatureDone()。
1 std::vector<std::string> m_armatureArray; 2 m_armatureArray.push_back("Anim/Anim_Plane_01"); 3 m_armatureArray.push_back("Anim/Anim_Plane_02"); 4 m_armatureArray.push_back("Anim/Anim_Plane_03"); 5 6 void Preload::asynLoadingArmature() 7 { 8 auto p = m_armatureArray[m_iArmatureCnt]; 9 DEBUG_LOG("Preload::asynLoadingArmature: %s", p.c_str()); 10 ArmatureDataManager::getInstance()->addArmatureFileInfoAsync( 11 std::string(p).append("0.png"), 12 std::string(p).append("0.plist"), 13 std::string(p).append(".ExportJson"), 14 this, 15 CC_SCHEDULE_SELECTOR(Preload::asynLoadingArmatureDone)); 16 } 17 18 void Preload::asynLoadingArmatureDone(float dt) 19 { 20 this->notifyProgress(++m_iTmpProgress); 21 22 m_iArmatureCnt++; 23 if (m_armatureArray.size() == m_iArmatureCnt) 24 { 25 m_bArmatureLoaded = true; 26 this->loadingDone(PreloadType::Armature); 27 } 28 else 29 { 30 asynLoadingArmature(); 31 } 32 }
場景並沒有特殊的異步加載函數,只能通過CSLoader::createNode()和CSLoader::createTimeline()根據csd文件生成node,然後保存到自定義的map中,以後要使用場景數據時,從map中獲取。
注意,此加載方法在cocos2dx-3.4中可以正常運行,在3.8中會出現錯誤,原因未知。不過加載單個場景文件的時間很短,一般並不會影響游戲的體驗,所以本游戲的最新版本中並沒有預加載場景文件。
1 std::vector<std::string> m_uiArray; 2 std::map<std::string, Node*> m_uiMap; 3 4 //菜單 5 m_uiArray.push_back("Bag.csb"); 6 m_uiArray.push_back("Daily.csb"); 7 m_uiArray.push_back("Instruction.csb"); 8 9 void Preload::syncLoadingUI() 10 { 11 //不能在非主線程中調用CSLoader::createNode,否則會導致OpenGL異常 12 for (auto file : m_uiArray) 13 { 14 auto node = Preload::getUI(file); 15 node->retain(); 16 m_uiMap.insert(std::map<std::string, Node*>::value_type(file, node)); 17 18 auto timeLine = CSLoader::createTimeline(file); 19 timeLine->retain(); 20 m_actionMap.insert(std::map<std::string, cocostudio::timeline::ActionTimeline*>::value_type(file, timeLine)); 21 22 DEBUG_LOG("Preload::syncLoadingUI: %s", file.c_str()); 23 this->notifyProgress(++m_iTmpProgress); 24 } 25 26 m_bUILoaded = true; 27 this->loadingDone(PreloadType::Ui); 28 } 29 30 Node* Preload::getUI(const std::string& filename) 31 { 32 DEBUG_LOG("Preload::getUI: %s", filename.c_str()); 33 return CSLoader::createNode(filename);; 34 35 //cocos2dx-3.8 不支持以下操作。3.4支持 36 //auto ui = m_uiMap.find(filename); 37 //if (ui != m_uiMap.end()) 38 //{ 39 // return ui->second; 40 //} 41 //else 42 //{ 43 // auto csb = CSLoader::createNode(filename); 44 // csb->retain(); 45 // m_uiMap.insert(std::map<std::string, Node*>::value_type(filename, csb)); 46 47 // return csb; 48 //} 49 }
由於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提供的異步任務接口+回調加載結果來進行預加載。
1 void Preload::asynLoadingDatabase() 2 { 3 auto loadEnd = [this](void*) 4 { 5 DEBUG_LOG("asynLoadingDatabase OK"); 6 7 m_bOtherLoaded = true; 8 this->loadingDone(PreloadType::Other); 9 }; 10 11 AsyncTaskPool::getInstance()->enqueue(AsyncTaskPool::TaskType::TASK_IO, loadEnd, (void*)NULL, [this]() 12 { 13 if (!GlobalData::getInstance()->initialize(this)) 14 { 15 CCLOG("Initialize globla data failed"); 16 this->notifyError("Initialize globla data failed"); 17 return; 18 } 19 20 m_iTmpProgress += PreloadProgress::GlobalData; 21 this->notifyProgress(m_iTmpProgress); 22 23 if (!GameData::getInstance()->loadData()) 24 { 25 CCLOG("Initialize game data failed"); 26 this->notifyError("Initialize game data failed"); 27 return; 28 } 29 30 m_iTmpProgress += PreloadProgress::GameData; 31 this->notifyProgress(m_iTmpProgress); 32 33 if (!AchievementMgr::getInstance()->init()) 34 { 35 CCLOG("Initialize achievement data failed"); 36 this->notifyError("Initialize achievement data failed"); 37 return; 38 } 39 40 m_iTmpProgress += PreloadProgress::AchievementMgr; 41 this->notifyProgress(m_iTmpProgress); 42 43 Sound::preload(this); 44 45 m_iTmpProgress += PreloadProgress::Sound; 46 this->notifyProgress(m_iTmpProgress); 47 }); 48 }
遠程數據一般是通過發送異步http或者其他tcp請求來實現數據的加載,根據網絡協議的不同,相關的接口也各不相同,這裡不再詳述。
1 class PreloadListener 2 { 3 public: 4 virtual void onStart() = 0; 5 virtual void onProgress(int percent) = 0; 6 virtual void onError(const char* info) = 0; 7 virtual void onWarning(const char* info) = 0; 8 virtual void onEnd(PreloadError errorCode) = 0; 9 };
2、定義載入界面場景,繼承自PreloadListener,並實現onXXX接口。
1 class LoadingLayer : 2 public Layer, public PreloadListener 3 { 4 public: 5 static Scene* scene(); 6 7 LoadingLayer(); 8 virtual ~LoadingLayer(); 9 10 virtual bool init(); 11 virtual void update(float dt) override; 12 13 CREATE_FUNC(LoadingLayer); 14 15 void initUI(); 16 void ToMainMenu(); 17 18 virtual void onStart() override; 19 virtual void onProgress(int percent) override; 20 virtual void onError(const char* info) override; 21 virtual void onWarning(const char* info) override; 22 virtual void onEnd(PreloadError errorCode) override; 23 24 private: 25 Node* m_pRootNode; 26 27 Sprite* m_pNeedle; 28 ui::LoadingBar* m_pLoadingBar; 29 ui::Text* m_pTxtErrorInfo; 30 31 long m_iBeginTime; 32 long m_iEndTime; 33 34 int m_iStart; 35 };
特別注意一下onProgress接口,這裡需要實現指針轉動的邏輯:
1 void LoadingLayer::onProgress(int percent) 2 { 3 float degree = LoadingLayerConstant::NeedleMinDegree + 4 (LoadingLayerConstant::NeedleMaxDegree - LoadingLayerConstant::NeedleMinDegree) * percent / 100; 5 m_pNeedle->setRotation(degree); 6 }
3、在加載任務中添加上報載入進度的函數。這樣,每當載入一張圖片或者任意一個資源文件的時候,就可以調用notifyProgress函數以使得界面上的指針轉動了。
1 void Preload::notifyProgress(int progress) 2 { 3 //這裡的m_pListener其實就是LoadingLayer的實例 4 if (m_pListener) 5 { 6 m_pListener->onProgress((int)(progress * 100.f / m_iAllProgress)); 7 } 8 }
下載源代碼
下一篇,我們將分析游戲的核心:“飛機”