galapagosit’s blog

本家 > http://galapagosit.com/

Cocos2d-JSのAssets Managerを使う

Cocos2d-JSにはAssets Managerという機能がある。

http://www.cocos2d-x.org/docs/manual/framework/html5/v3/assets-manager/en

これを使えばサーバ上に

スクリプトファイル

・画像ファイルなどのリソースファイル

等を設置することで、アプリ内ファイルの追加、更新が可能になる

通常のアプリ更新フローだとappleandroidのストアにアプリ全体をアップロードし、アプリごとマルっと更新してもらうというフローになるのだが、その作業をせずに一部分だけ修正を行えるのは魅力的。

※Cocos2d-x + luaな環境でも同じ様な事が出来るっぽい

※ポジティブな修正に関しては、ストアからアプリを更新してもらったほうが、ユーザに気づかれやすくアプリに戻ってくれるという効果もあるみたい

前提条件

cocos2d-js-v3.0(final)を使用

参考

cocos2d-js本体(cocos2d-js-v3.0)に入っている下記のサンプル

samples/js-tests/src/ExtensionsTest/AssetsManagerTest/AssetsManagerTest.js

下記のサイトも参考になった

http://itessays.com/mobile-development-technology/cocos2d-js-script-dynamically-update-online-update-hot-update.html

実装

必要になりそうなものを実装してみました。

script/create_manifest.py

manifestファイルを作成

#!/usr/bin/env python
# -*- coding:utf-8 -*-

"""
create manifest file

usage: script/create_manifest.py (-v <version>)

options:
    -v version
"""

import os
import shutil
import hashlib
import subprocess
import json

from docopt import docopt


# ファイル配信URL
STATIC_URL = 'http://SITE_DOMAIN/static/'

# マニフェストファイルパス
MANIFEST_PATH = 'res/Manifests/project.manifest'
VERSION_PATH = 'res/Manifests/version.manifest'

# cocos2d-jsのバージョン
ENGINE_VERSION = '3.0'

# 通常配信したいファイルパス
ASSETS_DIRS = ['res']
# zip圧縮で配信したいファイルパス
ZIP_ASSETS_DIRS = ['src']


def zip_assets():
    """
    zipファイルを作成
    """
    for z_dir in ZIP_ASSETS_DIRS:
        z_path = z_dir + '.zip'
        subprocess.check_call(['zip', '-r', z_path, z_dir])

def init_manifest(version):
    """
    マニフェストファイルのベース部分
    """
    manifest = {}
    manifest['packageUrl'] = STATIC_URL
    manifest['remoteManifestUrl'] = STATIC_URL + MANIFEST_PATH
    manifest['remoteVersionUrl'] = STATIC_URL + VERSION_PATH
    manifest['version'] = version
    manifest['engineVersion'] = ENGINE_VERSION
    return manifest

def assets_md5():
    """
    通常ファイルのmd5
    """
    assets_dic = {}
    for assets_dir in ASSETS_DIRS:
        for dpath, dnames, fnames in os.walk(assets_dir):
            for fname in fnames:
                path = os.path.join(dpath, fname)
                with open(path, 'r') as f:
                    byte = f.read()
                    assets_dic[path] = {'md5': hashlib.md5(byte).hexdigest()}
    return assets_dic

def zip_assets_md5():
    """
    zipファイルのmd5
    """
    assets_dic = {}
    for z_dir in ZIP_ASSETS_DIRS:
        z_path = z_dir + '.zip'
        with open(z_path, 'r') as f:
            byte = f.read()
            assets_dic[z_path] = {
                'md5': hashlib.md5(byte).hexdigest(),
                'compressed': True
            }
    return assets_dic

def create_manifest(version):
    """
    マニフェストファイルを出力
    """
    manifest = init_manifest(version)
    with open(VERSION_PATH, 'w') as f:
        json.dump(manifest, f, sort_keys=True, indent=2)

    manifest['assets'] = {}
    manifest['assets'].update(assets_md5())
    manifest['assets'].update(zip_assets_md5())
    with open(MANIFEST_PATH, 'w') as f:
        json.dump(manifest, f, sort_keys=True, indent=2)


def main(version):
    zip_assets()
    create_manifest(version)


if __name__ == '__main__':
    args = docopt(__doc__)
    main(args['-v'])
script/copy_assets.py

ファイルを配信したいサーバ側で、nginxやapacheの公開ディレクトリに ファイルをコピーするだけのスクリプト

#!/usr/bin/env python
# -*- coding:utf-8 -*-

"""
copy assets

usage: script/copy_assets.py

"""

import os
import shutil


# ファイル配信ディレクトリへのパス
STATIC_ROOT = '/srv/static/'

# 通常配信したいファイルパス
ASSETS_DIRS = ['res']
# zip圧縮で配信したいファイルパス
ZIP_ASSETS_DIRS = ['src']


def clear_static_root():
    """
    ファイル配信ディレクトリを空に
    """
    if os.path.exists(STATIC_ROOT):
        shutil.rmtree(STATIC_ROOT)
    os.mkdir(STATIC_ROOT)

def copy_assets():
    """
    通常ファイルをファイル配信ディレクトリにコピー
    """
    for assets_dir in ASSETS_DIRS:
        dst_dir = os.path.join(STATIC_ROOT + assets_dir)
        shutil.copytree(assets_dir, dst_dir)

def copy_zip_assets():
    """
    圧縮ファイルをファイル配信ディレクトリにコピー
    """
    for z_dir in ZIP_ASSETS_DIRS:
        z_path = z_dir + '.zip'
        dst_path = os.path.join(STATIC_ROOT + z_path)
        shutil.copy(z_path, dst_path)


def main():
    clear_static_root()
    copy_assets()
    copy_zip_assets()


if __name__ == '__main__':
    main()
src/scene/load_assets.js

ファイル更新時に滞在するscene

var LoadAssetsLayer = cc.Layer.extend({

    MANIFEST_PATH:"res/Manifests/project.manifest",
    manager:null,

    ctor:function () {
        this._super();
        this.load_assets();
        return true;
    },
    storagePath:function () {
        var storagePath = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : "./");
        cc.log("storagePath:" + storagePath);
        return storagePath;
    },
    load_assets:function () {
        var storagePath = this.storagePath()
        var manager = new jsb.AssetsManager(this.MANIFEST_PATH, storagePath);
        this.manager = manager;
        // As the process is asynchronised, you need to retain the assets manager to make sure it won't be released before the process is ended.
        manager.retain();

        var failCount = 0;
        var maxFailCount = 3;   //The maximum error retries

        if (!manager.getLocalManifest().isLoaded()) {
            cc.log("Fail to update assets, step skipped.");
            this.loadGame();
        } else {
            var that = this;
            var listener = new jsb.EventListenerAssetsManager(manager, function(event) {
                switch (event.getEventCode())
                {
                    case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                        cc.log("jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST");
                        that.loadGame();
                        break;
                    case jsb.EventAssetsManager.UPDATE_PROGRESSION:
                        cc.log("jsb.EventAssetsManager.UPDATE_PROGRESSION");
                        var percent = event.getPercent();
                        var filePercent = event.getPercentByFile();
                        cc.log("Download percent : " + percent + " | File percent : " + filePercent);
                        break;
                    case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
                        cc.log("jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST");
                        that.loadGame();
                        break;
                    case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                        cc.log("jsb.EventAssetsManager.ERROR_PARSE_MANIFEST");
                        that.loadGame();
                        break;
                    case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                        cc.log("jsb.EventAssetsManager.ALREADY_UP_TO_DATE");
                        that.loadGame();
                        break;
                    case jsb.EventAssetsManager.UPDATE_FINISHED:
                        cc.log("jsb.EventAssetsManager.UPDATE_FINISHED");
                        cc.log("Update finished. " + event.getMessage());
                        that.loadGame();
                        break;
                    case jsb.EventAssetsManager.UPDATE_FAILED:
                        cc.log("jsb.EventAssetsManager.UPDATE_FAILED");
                        cc.log("Update failed. " + event.getMessage());

                        failCount++;
                        if (failCount < maxFailCount) {
                            that.manager.downloadFailedAssets();
                        } else {
                            cc.log("Reach maximum fail count, exit update process");
                            failCount = 0;
                            that.loadGame();
                        }
                        break;
                    case jsb.EventAssetsManager.ERROR_UPDATING:
                        cc.log("jsb.EventAssetsManager.ERROR_UPDATING");
                        cc.log("Asset update error: " + event.getAssetId() + ", " + event.getMessage());
                        that.loadGame();
                        break;
                    case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                        cc.log("jsb.EventAssetsManager.ERROR_DECOMPRESS");
                        cc.log(event.getMessage());
                        that.loadGame();
                        break;
                    default:
                        break;
                }
            });
            cc.eventManager.addListener(listener, 1);
            manager.update();
        }
    },
    loadGame:function(){
        this.manager.release();
        cc.loader.loadJs(["src/jsList.js"], function(){
            cc.loader.loadJs(jsList, function(){
                cc.director.runScene(new IndexScene());
            });
        });
    }
});

var LoadAssetsScene = cc.Scene.extend({
    onEnter:function () {
        this._super();
        var layer = new LoadAssetsLayer();
        this.addChild(layer);
    }
});

まとめ

ポイントは project.jsonのjsListにはsrc/scene/load_assets.jsのみ設置し、その他のjsファイル群はsrc/jsList.jsに移動する事