cocos2d-xのAssets manager extensionでアセットのダウンロードを自動化


こんにちは、谷川です。

最近、cocos2d-xの3.x系を利用する上で、
Assets Manager Extensionという拡張機能を使って良かったので、
簡単にまとめたいと思います。

Assets Manager Extensionって?

モバイルアプリでは、起動時にアセットファイルをダウンロードしたり、
アプリ自体の更新をせずに、追加コンテンツをダウンロードして対応するケースが多いかと思います。
Assets Manager Extensionは、そんな追加アセットファイルのダウンロードを
簡単に自動でやってくれる機能です。

基本機能としては以下のように紹介されています。
- マルチスレッドでのダウンロードをサポート
- ダウンロードに関しての詳細情報の取得や、フック処理が可能
- zipファイルのサポート
- ダウンロードのresume/retryをサポート
自前でやると結構手間がかかるところを自動的にやってくれます。

これまではアセットファイルのバージョン管理やダウンロードの仕組みを
毎度自前で開発していたのですが、
Assets Managerを使うことで実装する手間がなくなり、開発がめっちゃ楽になりました。

仕組み

Assets Managerではバージョン管理と差分の比較のためにmanifestファイルという
定義ファイルをモバイルアプリ側とサーバーサイド側に置いておきます。

アセット全体のバージョンを管理するmanifestファイルと
ファイル毎の詳細情報を管理するmanifestファイルがあり、
まずはバージョンmanifestをサーバー側とモバイルアプリ側で比較して、
違いがあれば更新があるとみなし、詳細情報のmanifest比較を行ってくれます。

manifestファイルには管理するファイルの一覧とURL、
そしてファイルごとのmd5チェックサムが記載されています。
md5チェックサムが違えば更新と判断してダウンロードが始まりますし、
manifestに定義されていないファイルがあれば、追加と判断されダウンロードされます。

manifestファイルは以下の様な感じです。

{
    "packageUrl" : "http://example.com/assets_manager/TestScene/",
    "remoteVersionUrl" : "http://example.com/assets_manager/TestScene/version.manifest",
    "remoteManifestUrl" : "http://example.com/assets_manager/TestScene/project.manifest",
    "version" : "1.0.0",
    "engineVersion" : "Cocos2d-JS v3.0 RC0",

    "assets" : {
        "Images/background.jpg" : {
            "md5" : "..."
        },
        "Images/icon.png" : {
            "md5" : "..."
        },
        "Images/button.png" : {
            "md5" : "..."
        },
        "src/game.js" : {
            "md5" : "..."
        },
        "src/layer.js" : {
            "md5" : "..."
        },
        "compressed.zip" : {
            "md5" : "...",
            "compressed" : true
        }
    },

    "searchPaths" : [
        "res/"
    ]
}

実装方法

  • AssetsManagerExのインスタンス作る
  • イベント毎に処理を記述
  • イベントリスナーに登録
  • update()を呼び出す
    と、至って簡単。
    // manifestファイルのpathと書き込み先を指定してインスタンス作る
   _am = AssetsManagerEx::create(manifestPath, storagePath);
   _am->retain();

    // イベント毎の処理を記述
    _amListener = EventListenerAssetsManagerEx::create(_am, [=](EventAssetsManagerEx* event){
        static int failCount = 0;
        string storageDir = _am->getStoragePath(); // for log
        switch (event->getEventCode()) {
            case EventAssetsManagerEx::EventCode::ERROR_NO_LOCAL_MANIFEST: {
                CCLOG("No local manifest file found, skip assets update.");
                this->onAssetsUpdateDone(event);
            }
                break;
                            
            case EventAssetsManagerEx::EventCode::NEW_VERSION_FOUND: {
                CCLOG("New version found.");
                AssetsManagerEx::State state = _am->getState();
                CCLOG("AM state:%d", (int)state);
            }
                break;
            // 進行中 進捗に関しての情報が取得できる    
            case EventAssetsManagerEx::EventCode::UPDATE_PROGRESSION: {
                string msg= event->getMessage();
                string assetId = event->getAssetId();
                if (msg.size() > 0) {
                    CCLOG("UPDATE_PROGRESSION(%s: %s): %s", assetId.c_str(), storageDir.c_str(), msg.c_str());
                }
                float percent = event->getPercent();
                float percentByFile = event->getPercentByFile();
                int progressNumber = (int)percentByFile;
                if (assetId == AssetsManagerEx::VERSION_ID) {
                    
                } else if (assetId == AssetsManagerEx::MANIFEST_ID) {
                    
                } else {
                    CCLOG("%.1f percent (%.1f percent each a file)", percentByFile, percent);
                }
            }
                break;
                
            case EventAssetsManagerEx::EventCode::ERROR_DOWNLOAD_MANIFEST:
            case EventAssetsManagerEx::EventCode::ERROR_PARSE_MANIFEST: {
                CCLOG("Fail to download manifest file, update skipped.");
            }
                break;
                
            case EventAssetsManagerEx::EventCode::ALREADY_UP_TO_DATE:
            case EventAssetsManagerEx::EventCode::UPDATE_FINISHED: {
                CCLOG("Update finished(%s). %s", storageDir.c_str(), event->getMessage().c_str());
                
                AssetsManagerEx::State state = _am->getState();
                CCLOG("AM state:%d", (int)state);
            }
                break;
                
            case EventAssetsManagerEx::EventCode::UPDATE_FAILED: {
                CCLOG("Update failed(%s). %s",storageDir.c_str(), event->getMessage().c_str());
                
                failCount ++;
                // retry
                if (failCount <= 2) {
                    _am->downloadFailedAssets();
                } else {
                    CCLOG("Reach maximum fail count, exit update process");
                    failCount = 0;
                    this->onAssetsUpdateDone(event);
                }
            }
                break;
                
            case EventAssetsManagerEx::EventCode::ERROR_UPDATING: {
                CCLOG("Asset %s : %s", event->getAssetId().c_str(), event->getMessage().c_str());
            }
                break;
                
            case EventAssetsManagerEx::EventCode::ERROR_DECOMPRESS: {
                CCLOG("%s", event->getMessage().c_str());
            }
                break;
                
            default:
                break;
        }
    });
    // add the event listener to dispacher
    Director::getInstance()->getEventDispatcher()->addEventListenerWithFixedPriority(_amListener, 1);

    // start updating
    _am->update();

弊社では

アプリ内データベースとしてsqliteを使っていますが、
最近では、sqliteファイル丸ごとサーバーサイドからダウンロードして
データを更新することにより、アセットだけでなくデータに関しても
更新処理を簡略化することができました。

もちろん、sqliteファイルもAssets Manager経由で更新しており、
アップデートに関する処理をすべてAssets Managerに任せている感じです。

また、manifestファイルを手書きするのは面倒なので、
サーバーサイドでファイルを出力する仕組みなどを構築すると、運用面でも捗ります。

Assets Managerに関しては、導入実績をあまりみかけないために、
最初は不安がありましたが、検証したり実際に使ってみて、今のところ問題はありません。
自動でやってくれて、便利なので今後は離れられない存在になりつつあります。

最後に

弊社ではモバイルアプリ開発のメンバーを募集してます。
Assets Managerのように、最新のものを導入することなどやりやすい環境で、
こども向けアプリですが、ゲーム開発とやってることは技術的には同じなので、
経験を活かすことができます。
特にこどもに興味がなくともゲーム開発のノリで入ってくるメンバーもいますので
興味のある方はこちらをご覧いただきご連絡いただければと思います!