Cocos2d-xでfread・fwriteを使いバイナリファイルを読み書きする方法
こんにちは。エンジニアのS.Kと申します。 今回はCocos2d-xを用い、iOS・Android環境でfread・fwriteを使ってバイナリファイルを読み書きする方法についての記事です。 本記事で使用しているCocos2d-xのバージョンは2.x系です。 3.x系とは一部クラス名関数名が異なると思いますが、ご了承ください。
目次
・前置き ・iOSでfread・fwriteを使ってファイル読み書きする方法 ・Cocos2d-x提供のファイル入出力関数を使わなかった理由 ・エラーの原因調査 ・解決方法 ・補足(char型の符号について) ・まとめ
■前置き
Cocos2d-xを用い、iOS・Android環境でfread・fwriteを使ってバイナリファイルを読み書きする処理が必要になった経緯を説明します。 弊社ではフライハイトクラウディアというRPGを配信しております。 (AppStore(iOS)、NTTドコモスゴ得/R.P.G-mode(Android)で配信中もしくは配信予定です) このゲームは元々フィーチャーフォン向けに配信されておりました。 これを2013年にiOS向けに移植し、リリースしました。 フィーチャーフォン版はJavaで作られておりますが、 iOS版ではCocos2d-xを使いC++で作りました。 iOSへの移植当時はその後Androidへの移植予定はなかったのですが、 今回Androidへも移植することになりました。 iOSへの移植はそこまで大きな問題はなかったのですが、 Androidへの移植ではfread・fwriteを使ってファイル読み書きする箇所でエラーが発生してしまいました。 詳しいことは後述いたしますがこのエラーは読み込むファイルがzip圧縮されていることが原因でした。 このエラーを対処しAndroid環境でもfread・fwriteを使えるようにしていきます。
■iOSでfread・fwriteを使ってファイル読み書きする方法
iOSでは以下の方法で問題なくファイルの読み書きができます。 ・ファイル書き込み
・ファイル読み込み
注意すべきはファイルパスです。プラットフォームによってファイルの場所が異なります。 ですがCocos2d-xが提供している関数に、ファイル読み書き用のディレクトリを取得する関数があるのでそちらを使います。
CCFileUtils::sharedFileUtils()->getWritablePath();
上記で各プラットフォームごとのファイルを新規作成できるディレクトリのパスを取得できます。 このディレクトリ以下にファイルを作成することができます。 このパスの取得関数自体はAndroidでも適宜必要なパスを取得することが出来ます。
■Cocos2d-x提供のファイル入出力関数を使わなかった理由
もちろんCocos2d-xが提供しているファイル入出力関数ならばプラットフォームの違いを意識せずに使用できます。 上記の様にC言語のファイル入出力関数(fread・fwrite)を使った理由は以下の通りです。 理由1 ・JavaからC++に変換するときに簡単な置換で済ませたかった。 先に移植したiOS版ではfread・fwriteを使っても問題がなく、 Javaのファイル入出力関数からC++の入出力関数へ盲目的に置換するだけで良く非常に簡単でした。 理由2 ・なるべく移植元のJavaコードや移植済みのiOS版の処理と同じようにしたかった。 読み込むデータの構造とCoccs2d-xのファイル読み込み関数の仕様の相性が悪く、 既にiOSで動いている処理から大幅に変更を変えなければなりませんでした。 具体的にCocos2d-xで提供されているファイル読み込み関数について説明します。 関数は以下の通りです。
unsigned char* CCFileUtils::getFileData(const char* pszFileName, const char* pszMode, unsigned long * pSize)
第一引数はファイル名、第二引数はオープンモード("r"や"rb"など)、第三引数は読み込んだファイルの大きさを受け取る変数のポインタです。 問題なのは戻り値です。読み込んだファイルがいかなる形式でもchar配列で返します。 テキストファイルであろうが画像ファイルであろうがchar配列で返します。 つまり、char以外のデータを読み込みたい場合は、受け取った値をchar配列から型変換しないといけません。 そして今回読み込むデータですが、1つのバイナリファイルの中にいろいろな型のデータがひとまとめに入っています。 例えばモンスター用のデータだと、 順に、4バイト整数(モンスターID)、文字列(モンスター名)、要素数4つの2バイト整数配列(ステータス)、4バイト整数(次のモンスターID)...。 の様な形式になります。
(※これは説明用のデータで、実際にゲームで使用しているデータとは形式も値も異なります。) この様に色々な型が混ざったデータを上記のCocos2d-xの関数を使うと、値を取り出す都度、適切な型に変換しなければなりません。 例の値を上記の関数getFileDataで取り出すと、 [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, ...] の様なchar配列が返ってきます。 この配列から順番に取り出して、元々の型に変換し、使える値に直していく必要があります。 例の場合は、 先頭から4要素目までを4バイト整数型に変換し、 5要素目から16要素目を文字列型に変換し、 17要素目から24要素目を2バイト整数型の配列に変換します。 これをデータの数分繰り返します。 この様に色々な型が混ざったデータの場合は、データを読み込んだ後のデータの整形に手順が必要になります。 freadを使えば第二引数で型を指定できるため整形の手順がなくなります。 また、getFileDataで配列として取得した場合は要素数の管理も自分でしなければなりませんが、 freadを使った場合はファイルポインタが内部でインデックスを更新するので、自分で管理する必要はありません。 このためgetFileDataを使うよりは、簡潔な処理になります。 上記の理由によりAndroid用にCocos2d-x提供のファイル入出力関数を使った処理に書き換えるのではなく、 fread・fwriteを使う方法で実装することにしました。
■エラーの原因調査
上記の理由から、出来ればAndroidでもfread・fwriteを使いたかったので、 Androidで発生するエラーの原因を調べました。 Cocos2d-xはオープンソースなのでソースコードを見ることができます。 本来のファイル入出力関数では何をやっているのか、試しに見てみることにしました。 cocos2dx\platform\CCFileUtils.cppやcocos2dx\platform\android\CCFileUtilsAndroid.cppです。 (Androidは個別処理のためクラスが別になります。) ソースコードを読み進めていくと、以下の関数の中でfopenやfreadを呼んでいました。
unsigned char* CCFileUtils::getFileData(const char* pszFileName, const char* pszMode, unsigned long * pSize) unsigned char* CCFileUtilsAndroid::doGetFileData(const char* pszFileName, const char* pszMode, unsigned long * pSize, bool forAsync)
freadを使うこと自体には問題が無さそうです。 もう少し読み進めていくとAndroidの場合のみ読み込みファイルをzip展開している処理がありました。 結論として、上手くいかない原因はこれでした。 Androidの場合、Cocos2d-xのプロジェクトのResourcesフォルダの中身は自動でapkファイル直下のassetsに配置されます。 読み込むファイルはこのResourcesフォルダの中にありますが、 Resourcesフォルダ自体がこのapkファイルの中に格納されてしまいます。 apkファイルはzip圧縮されたアーカイブファイルなので、 このままでは読み込めません。 そのため別の場所に展開してから読み込む必要があります。 ちなみに、このzip展開は最初からapk内部に格納されているファイルを読み込むときに必要な処理で、 Androidでも、上記のiOSの説明で記載した方法では、プログラム中に後から動的に生成したファイルの読み込みはzip展開せずに読み込むことができます。
■解決方法
Androidのリソースの読み込み時のみ、先にzip展開してから読み込めば、 以後は上記のfreadを使用した方法で読み込むことができます。 ・zip展開処理
この展開されたpiyo_unzip.binはiOSと同じ方法で読み込むことが出来ます。 getFileDataFromZipで展開したファイルはnewされているためdeleteしないとメモリリークするので注意してください。
■補足(char型の符号について)
読み込み自体は上記方法で上手くいったのですが、Android環境でだけ読み込んだ値がおかしくなる不具合がありました。 調べた結果、ファイル読み込みは関係なくgccコンパイラの設定が原因でした。 gccの設定でchar型を符号あり・なしの設定ができます。この設定が「符号なし」になっている場合があります。 そのためchar型変数に-1を入れたつもりが255が入っていることがあります。 「値がマイナスなら別の処理をする・・・」などの処理をしていたらエラーになります。 これを回避するには、proj.android\jni\Application.mkのAPP_CPPFLAGSに-fsigned-charを追加します。 これでchar型が「符号あり」になります。 例
APP_CPPFLAGS := -fsigned-char -frtti -DCC_ENABLE_CHIPMUNK_INTEGRATION=1 -DCOCOS2D_DEBUG=1
■まとめ
以上でバイナリファイルを読み書きする方法の説明は終わりです。 ファイルパスの指定と、Resourceフォルダ中のファイルの読み込みにさえ注意すれば、実装に大きな問題はないでしょう。 Cocos2d-xが提供している関数の方が実装は容易だと思います。 保存するデータの数が変化する場合はこちらの方法が楽な場合があるかもしれません。 正直かなりニッチな内容であったことは自覚しております。 役に立つ情報であったか不明ではありますが、困っている方の参考になれば幸いです。 ご清覧ありがとうございました。











