絕大多數游戲在啟動後首先出現的是一個“載入中”的場景,此場景的用處是將游戲所需的圖片、音樂、數據等資源從存儲卡(或磁盤、閃存)讀入內存,這樣,後面需要用到這些資源時,可以直接從內存讀取,以加快游戲的運行,提高流暢性。下面,就對資源的預加載機制做一個介紹。
預加載的目的是為了後續讀取的快捷,所以,一般會預加載那些較大較復雜的文件,例如以下這些:
下面,我們將逐一介紹不同資源載入的方法。
詳細的代碼如下所示:
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 }
下載源代碼
下一篇,我們將分析游戲的核心:“飛機”