Cocos2d-JSのAssets Managerを使う
Cocos2d-JSにはAssets Managerという機能がある。
http://www.cocos2d-x.org/docs/manual/framework/html5/v3/assets-manager/en
これを使えばサーバ上に
・スクリプトファイル
・画像ファイルなどのリソースファイル
等を設置することで、アプリ内ファイルの追加、更新が可能になる
通常のアプリ更新フローだとappleやandroidのストアにアプリ全体をアップロードし、アプリごとマルっと更新してもらうというフローになるのだが、その作業をせずに一部分だけ修正を行えるのは魅力的。
※Cocos2d-x + luaな環境でも同じ様な事が出来るっぽい
※ポジティブな修正に関しては、ストアからアプリを更新してもらったほうが、ユーザに気づかれやすくアプリに戻ってくれるという効果もあるみたい
前提条件
cocos2d-js-v3.0(final)を使用
参考
cocos2d-js本体(cocos2d-js-v3.0)に入っている下記のサンプル
samples/js-tests/src/ExtensionsTest/AssetsManagerTest/AssetsManagerTest.js
下記のサイトも参考になった
実装
必要になりそうなものを実装してみました。
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に移動する事