• 仿《雷霆战机》飞行射击手游开发--资源预加载


    转载请注明:http://www.cnblogs.com/thorqq/p/5639022.html 

    项目首页:https://www.oschina.net/p/raiden

        绝大多数游戏在启动后首先出现的是一个“载入中”的场景,此场景的用处是将游戏所需的图片、音乐、数据等资源从存储卡(或磁盘、闪存)读入内存,这样,后面需要用到这些资源时,可以直接从内存读取,以加快游戏的运行,提高流畅性。下面,就对资源的预加载机制做一个介绍。

    资源的类型

    预加载的目的是为了后续读取的快捷,所以,一般会预加载那些较大较复杂的文件,例如以下这些:

    • 单张大图:背景大图
    • 合成图:可多幅图片合成的大图,这里我们使用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)

    详细的代码如下所示:

     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请求来实现数据的加载,根据网络协议的不同,相关的接口也各不相同,这里不再详述。

    加载界面

    在此加载界面中,我们使用一个仪表盘和转动的指针来告诉用户当前的加载进度。那么,后台加载任务与前台的指针转动是如何关联起来的呢?我们使用了观察者模式。下面上一张百度找出的观察者模式的图:


    Observer模式的角色:
    Subject(被观察者)
        被观察的对象。当需要被观察的状态发生变化时,需要通知队列中所有观察者对象。Subject需要维持(添加,删除,通知)一个观察者对象的队列列表。
    ConcreteSubject
        被观察者的具体实现。包含一些基本的属性状态及其他操作。
    Observer(观察者)
        接口或抽象类。当Subject的状态发生变化时,Observer对象将通过一个callback函数得到通知。
    ConcreteObserver
        观察者的具体实现。得到通知后将完成一些具体的业务逻辑处理。

    在本游戏中实现了一个简化版的观察者模式:

    1、首先,我们定义一个被观察者抽象类。其中定义了开始、进度、错误、警告、结束等接口。

    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 }

     下载源代码

    转载请注明:http://www.cnblogs.com/thorqq/p/5639022.html 

    下一篇,我们将分析游戏的核心:“飞机”

  • 相关阅读:
    php解决与处理网站高并发 大流量访问的方法
    mysql事务和锁InnoDB
    从一个死锁看mysql innodb的锁机制
    Git如何删除自己创建的项目
    公众号的坑
    字符串转Unicode码
    字符串转UTF-8码(%开头)
    git介绍和使用
    ng2中文文档地址
    两个数组的排序方法
  • 原文地址:https://www.cnblogs.com/thorqq/p/5639022.html
Copyright © 2020-2023  润新知