読者です 読者をやめる 読者になる 読者になる

WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

Cocos2d-xでファイルをパッキングしてみよう

Cocos2d-x

今回のエンジニアブログ担当の岩原です。
今まではC#の記事が多かったのですが、たまにはCocos2d-xの話題にも触れてみようかと思います。

通常、Cocos2d-xではファイルを指定して画像を読み込んだり、音を鳴らしたりします。
2.2.x系しかまだ触ったことないのですが、この辺りは3.x系でもそこまで変わってないかと思います。
これでも特に問題ないのですが、ファイルアクセスのオーバーヘッドなどを考えるとまだまだ効率化の余地はあるかと思います。
また、画像ファイルをそのまま端末に保存しておくと、覗き見される可能性があるのも事実です。

そこで、複数のファイルをまとめて1つのファイルにしてしまおう、というのが今回の記事です。
スクウェア・エニックス モバイルオープンカンファレンス | SQUARE ENIXを参考にして、やってみましょう。

パッキングファイルの仕様

今回は2つのファイルを用意します。
・indexファイル
パッキングされた各ファイルがどこにあるのかを保持しているファイルです。
ファイルを識別するID(32bit)、どこから読みこめばいいのかを示すオフセット値(32bit)、対象ファイルのサイズ(32bit)を保持しています。

・datファイル
パッキングファイル本体です。
リソースファイルを結合させるだけの簡単なお仕事です。

パッキングするクラス: ResourcePacker

ResourcePackerは指定されたパスに存在するファイルをパッキングするだけのクラスです。
とりあえず、毎回新しくパッキングファイルを作成し直します。
なお、ここで登場するUtil::crc32関数やUtil::getSize関数はそれぞれファイルパスからCRC32を求める独自関数とファイルサイズを取得する独自関数になります。
今回はあまり関係ないので割愛させていただきます。
(CRC32でも衝突可能性は結構ありますが…)CRC32関数は、ファイルごとにユニークな32bitの値が取得できる値であれば、どのような処理でも問題ありません。
今回はCRC32で生成された32bit値をIDとして扱います。

addSourceFile関数で対象ファイルを追加し、packAndWrite関数で一気にindexファイルとdatファイルを作成します。

#ifndef __PackedFileSample__ResourcePacker__  
#define __PackedFileSample__ResourcePacker__  
  
#include <iostream>  
#include <map>  
#include <string>  
  
using namespace std;  
  
  
class ResourcePacker {  
      
private:  
      
    ResourcePacker(){};  
    ~ResourcePacker(){};  
    map<uint32_t, string> resourceList;  
      
public:  
      
    static ResourcePacker* getInstance();  
      
    void addSourceFile(std::string filePath);  
      
    bool packAndWrite();  
};  
  
#endif /* defined(__PackedFileSample__ResourcePacker__) */  
#include "ResourcePacker.h"  
#include "cocos2d.h"  
#include "Util.h"  
  
USING_NS_CC;  
  
/**  
 * @brief インスタンス取得  
 * @return ResourcePackerのインスタンス  
 */  
ResourcePacker* ResourcePacker::getInstance()  
{  
    static ResourcePacker instance;  
    return &instance;  
}  
  
void ResourcePacker::addSourceFile(std::string filePath)  
{  
    uint32_t crc32 = Util::crc32(filePath.c_str(), filePath.size());  
    if (resourceList.find(crc32) != resourceList.end()) {  
        //すでに存在している場合はちょい怪しい  
        CCLOG("path[%s],crc32[%u]",filePath.c_str(),crc32);  
    }  
    resourceList[crc32] = filePath;  
}  
  
  
bool ResourcePacker::packAndWrite()  
{  
    string datPath = CCFileUtils::sharedFileUtils()->getWritablePath();  
    datPath += "pack.dat";  
      
    string indexPath = CCFileUtils::sharedFileUtils()->getWritablePath();  
    indexPath += "pack.index";  
      
    FILE* datfp = fopen(datPath.c_str(), "wb+");  
    FILE* indexfp = fopen(indexPath.c_str(), "wb+");  
      
    if (!datfp || !indexfp) {  
        //ファイルオープン失敗  
        return false;  
    }  
      
    fseek(datfp, 0L, SEEK_SET);  
    fseek(indexfp, 0L, SEEK_SET);  
      
    map<uint32_t, string>::iterator it = resourceList.begin();  
    uint32_t offset = 0;  
    bool ret = true;  
    while (it != resourceList.end()) {  
        string path = it->second;  
          
          
        //ここからdatファイル系  
          
        //DLファイルの読み込み  
        unsigned long datSize = 0;  
        unsigned char* datData = CCFileUtils::sharedFileUtils()->getFileData(path.c_str(), "rb+", &datSize);  
          
        if (!datData) {  
            it++;  
            continue;  
        }  
        int ret = fwrite(datData,sizeof(unsigned char) ,datSize, datfp);  
        if (ret < datSize)  
        {  
            it++;  
            continue;  
        }  
        delete[] datData;  
          
        //ここからindexファイル系  
        uint32_t crc32 = it->first;  
        uint32_t size = Util::getSize(path.c_str());  
        //CCLOG("write : crc32[%u],offset[%u],size[%u]",crc32,offset,size);  
        ret = fwrite(&crc32, sizeof(crc32), 1, indexfp);  
        if (ret  < 1) {  
            ret = false;  
            break;  
        }  
          
        ret = fwrite(&offset, sizeof(offset), 1, indexfp);  
        if (ret  < 1) {  
            ret = false;  
            break;  
        }  
          
        ret = fwrite(&size, sizeof(size), 1, indexfp);  
        if (ret  < 1) {  
            ret = false;  
            break;  
        }  
        offset += size;  
        it++;  
    }  
      
    fclose(datfp);  
    fclose(indexfp);  
    return ret;  
}  

パッキングリソースローダークラス: PackedResourceLoader

PackedResourceLoaderクラスはパッキングされたファイルからリソースを取り出すクラスです。
今回はファイルパスを元にIDを作成してパッキングしているので、
ローダーでもファイルパスを与えてリソースを取り出せるようにします。

IndexData構造体はその名の通りindexファイルの情報をファイルごとに保持するための構造体です。
IndexData構造体配列からIDをキーにして対象のIndexData構造体を探すのはバイナリサーチを利用します。

init関数でファイルをindexファイルとdatファイルをオープン&indexファイルをメモリに展開します。
datファイルはこの後も基本的にオープンしっぱなしにしておきます。

loadPackedResource関数は、パッキングされたファイルから対象のファイルをバイト列で取得するための関数です。
loadSpriteFromPackedResource関数は、パッキングされたファイルから対象のファイルをバイト列で取得後、Cocos2d-xのスプライトにして返却する関数です。
loadPackedResourceInternalはinline関数で、loadSpriteFromPackedResource内に展開する用です。

#ifndef __PackedFileSample__PackedResourceLoader__  
#define __PackedFileSample__PackedResourceLoader__  
  
#include <iostream>  
#include "cocos2d.h"  
  
struct IndexData {  
    /**  
     *  CRC32ハッシュ(ファイル見分け用)  
     */  
    uint32_t hash;  
      
    /**  
     *  ファイル開始位置(0オリジン)  
     */  
    uint32_t offset;  
      
    /**  
     *  ファイルサイズ  
     */  
    uint32_t size;  
};  
  
class PackedResourceLoader {  
private:  
    FILE* datfp;  
    std::string lastError;  
    int lastErrroNo;  
    IndexData* indexList;  
    int indexListCount;  
      
      
    PackedResourceLoader();  
    ~PackedResourceLoader();  
    static int compSearch( const void *c1, const void *c2 );  
      
    unsigned char* loadPackedResourceInternal(const char* path,uint32_t* outSize);  
      
public:  
    static PackedResourceLoader* getInstance();  
    std::string getLastError(){return lastError;};  
    int getLastErrorNo(){return lastErrroNo;}  
    bool init();  
      
    unsigned char* loadPackedResource(const char* path,uint32_t* outSize);  
      
    cocos2d::CCSprite* loadSpriteFromPackedResource(const char* path);  
};  
  
#endif /* defined(__PackedFileSample__PackedResourceLoader__) */
#include "PackedResourceLoader.h"  
#include <errno.h>  
#include "Util.h"  
  
using namespace std;  
using namespace cocos2d;  
  
/**  
 * @brief インスタンス取得  
 * @return ResourcePackerのインスタンス  
 */  
PackedResourceLoader* PackedResourceLoader::getInstance()  
{  
    static PackedResourceLoader instance;  
    return &instance;  
}  
  
PackedResourceLoader::PackedResourceLoader()  
:lastError("")  
,lastErrroNo(0)  
,datfp(NULL)  
,indexList(NULL)  
,indexListCount(0)  
{  
}  
  
PackedResourceLoader::~PackedResourceLoader()  
{  
    delete [] indexList;  
}  
  
bool PackedResourceLoader::init()  
{  
      
    if(datfp && indexList)  
    {  
        //すでに準備完了  
        return true;  
    }  
      
    if(!datfp){  
        string datPath = CCFileUtils::sharedFileUtils()->getWritablePath();  
        datPath += "pack.dat";  
        datfp = fopen(datPath.c_str(),"rb");  
        if (!datfp) {  
            lastErrroNo = errno;  
            lastError = strerror(errno);  
            return false;  
        }  
    }  
      
    if (!indexList) {  
          
        string indexPath = CCFileUtils::sharedFileUtils()->getWritablePath();  
        indexPath += "pack.index";  
          
        unsigned long tmpSize;  
        unsigned char* tmp = CCFileUtils::sharedFileUtils()->getFileData(indexPath.c_str(), "rb", &tmpSize);  
        if (!tmp || !tmpSize) {  
            lastErrroNo = errno;  
            lastError = strerror(errno);  
            return false;  
        }  
          
        if (tmpSize % sizeof(uint32_t) * 3 != 0) {  
            //サイズがおかしい  
            lastErrroNo = -1;  
            lastError = "index data is invaild.";  
            return false;  
        }  
          
        indexListCount = tmpSize / (sizeof(uint32_t) * 3);  
        int startPos = 0;  
        indexList = new IndexData[indexListCount];  
        for (int i = 0; i < indexListCount; i++) {  
              
            uint32_t hash;  
            memmove(&hash, tmp + startPos, sizeof(uint32_t));  
            startPos += sizeof(uint32_t);  
              
            uint32_t offset;  
            memmove(&offset, tmp + startPos, sizeof(uint32_t));  
            startPos += sizeof(uint32_t);  
              
            uint32_t size;  
            memmove(&size, tmp + startPos, sizeof(uint32_t));  
            startPos += sizeof(uint32_t);  
              
            CCLOG("load : hash[%u], offset[%u], size[%u]",hash,offset,size);  
              
            IndexData index;  
            index.hash = hash;  
            index.offset = offset;  
            index.size = size;  
              
            indexList[i] = index;  
        }  
        delete [] tmp;  
    }  
      
    return true;  
}  
  
inline unsigned char* PackedResourceLoader::loadPackedResourceInternal(const char *path,uint32_t* outSize)  
{  
    *outSize = 0;  
    int32_t crc32 = Util::crc32(path, strlen(path));  
    IndexData* targetIndex = (IndexData*)bsearch(&crc32, indexList, indexListCount, sizeof(IndexData), compSearch);  
      
    if (!targetIndex) {  
        return NULL;  
    }  
  
    fseek(datfp, targetIndex->offset, SEEK_SET);  
    unsigned char* ret = new unsigned char[targetIndex->size];  
    int result = fread(ret, sizeof(unsigned char), targetIndex->size, datfp);  
    if (result < targetIndex->size) {  
        //読み込めなかった  
        CC_SAFE_DELETE_ARRAY(ret);  
        return NULL;  
    }  
    *outSize = targetIndex->size;  
    return ret;  
}  
  
unsigned char* PackedResourceLoader::loadPackedResource(const char *path,uint32_t* outSize)  
{  
    return loadPackedResourceInternal(path, outSize);  
}  
  
CCSprite* PackedResourceLoader::loadSpriteFromPackedResource(const char *path)  
{  
    CCTexture2D* texture = CCTextureCache::sharedTextureCache()->textureForKey(path);  
      
    if(!texture){  
          
        uint32_t size;  
        unsigned char* data = loadPackedResourceInternal(path,&size);  
        if (!data) {  
            return NULL;  
        }  
        CCImage* image = new CCImage();  
        bool ret = image->initWithImageData(data, size);  
        CC_SAFE_DELETE_ARRAY(data);  
        if (!ret) {  
            return NULL;  
        }  
        texture = CCTextureCache::sharedTextureCache()->addUIImage(image, path);  
        if (!ret) {  
            return NULL;  
        }  
        CC_SAFE_RELEASE(image);  
    }  
      
    CCSprite* sprite = CCSprite::createWithTexture(texture);  
      
    return sprite;  
}  
  
  
/* 比較関数 バイナリサーチ用 */  
int PackedResourceLoader::compSearch( const void *c1, const void *c2 )  
{  
    IndexData test2 = *(IndexData *)c2;  
      
    uint32_t tmp1 = *(uint32_t *)c1;   
    uint32_t tmp2 = test2.hash;  
      
    return tmp1 - tmp2;  
}  

その他

サンプルではCRC32を使ってIDを生成していますが、結構な頻度で衝突する可能性があります。
そして、衝突した際の考慮は全くありません。
indexには、衝突した際の目印を付けておくと良いかもしれません。

また、作成したindexファイルとdatファイルをCocos2d-xで取得できるWritablePathに保存していますが、
iOSの場合、審査でリジェクトを食らう可能性があるので、Cacheに保存するようにしましょう。