スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

アーカイブファイル作成プログラムを作ってみた!

今までゲームを作る際、
直接 画像ファイルやら音楽ファイルやらをぶちこんでたのですが、

【参考画像】
sonomannma.jpg


いくらなんでも改造され放題なので、これはまずいだろと思い、
アーカイブファイル作成&解析プログラムを作ってみました。
※今回は単純にファイルを連結するだけです。
-------------------------------------------------------------------------
・アーカイブファイル ・・・ 複数のファイルを一つのファイルにまとめたファイル。
             時と場合によって圧縮したり、暗号化をしたりもする。

-------------------------------------------------------------------------

sonomannma2.jpg




~やり方~
-------------------------------------------------------
【1】FindFirstFile関数、FindNextFile関数を用いて、
   同じ階層のフォルダに入っているファイル名を取得。
【2】std::ifstreamを使用して、各ファイルのサイズを取得。
【3】ヘッダー部分と、ファイルの実データ部分のバッファを作成。
【4】ヘッダーと実データのバッファを合体させる。
【5】std::ofstreamを使用して「××.arc」の形式で書き出す。

-------------------------------------------------------

【1】ファイル名取得
ファイル名の取得の方法は以下の通り。
※フォルダ内のファイルのみを検索してます。以下のプログラムでは、
 深い階層が無くなるまで、再帰的にファイルを探すという事は出来ません。

見慣れない関数を使ってるだけで、複雑なことはしていません。

std::vector<std::string> g_FileNameVec;

//中略//

HANDLE t_FindHandle;
WIN32_FIND_DATA t_FindData;

t_FindHandle = FindFirstFile("*.*", &t_FindData);
//有効なハンドルが返ってきていれば
if (t_FindHandle != INVALID_HANDLE_VALUE) {
do {
//ディレクトリでなければファイル名を登録。
if((t_FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0){
g_FileNameVec.push_back(t_FindData.cFileName);
}
} while(FindNextFile(t_FindHandle, &t_FindData));
FindClose(t_FindHandle);
}



【2】各ファイルのサイズを取得
【3】ヘッダー部分と、ファイルの実データ部分のバッファを作成
【4】ヘッダーと実データを合体


【2】~【4】まとめて記載します。

std::string t_ArchiveHeader;	//ヘッダ用
std::string t_FileData; //実データ用
std::string t_ArchiveData; //ヘッダ+実データ用

//取得したファイルの数だけ繰り返す
for(size_t i = 0, count = g_FileNameVec.size(); i < count; i++)
{
std::ifstream t_File;
t_File.open( g_FileNameVec[i].c_str(), std::ios::binary );
if(t_File != 0){
std::vector<char> t_BufVec;
char t_Str[1024];
//ファイルの終わりまで移動
t_File.seekg( 0, std::ifstream::end );

//ファイルサイズを取得
int t_FileSize = (int)t_File.tellg();
t_File.seekg( 0, std::ifstream::beg);

//ファイルを読み込む
t_BufVec.resize(t_FileSize);
t_File.read( &t_BufVec[0], t_FileSize );

//ヘッダー部分を作成(「サイズ"ファイル名"」の形式)
sprintf_s( t_Str, sizeof(t_Str), "%d\"%s\"", t_FileSize, g_FileNameVec[i].c_str());
t_ArchiveHeader += t_Str;

//実データ部分を作成
t_FileData.reserve(t_FileData.size() + t_FileSize);
for(int j = 0; j < t_FileSize; j++)
{
t_FileData += t_BufVec[j];
}
}
}
//ヘッダーと実データ部分を連結
t_ArchiveData = t_ArchiveHeader + '#' + t_FileData;



何をやっているのか分かりにくいですが、
要はアーカイブの一番初めに、「サイズ "ファイル名"」の形式
どんなサイズ/名前のファイルがあるかを列挙して、
#を区切り文字として、その次からデータが埋め込まれてます。

【参考画像】
archive.jpg

【追記】
上記の画像、メモリのTest1.pngの部分「1」が抜けてますね。
すみませんが、気にしないで下さい。


【5】アーカイブファイル書き出し
今回、アーカイブファイルの書き出しには、std::ofstreamを使用しました。
以下のソースを見ても分かりますが、何も難しい部分は無いです。

std::ofstream fout;
fout.open("MyArchive.arc", std::ios::out | std::ios::binary | std::ios::trunc);

if(fout != 0){
fout.write( t_ArchiveData.c_str(), t_ArchiveData.size() );
fout.close(); //ファイルを閉じる
}



次にアーカイブファイルの解析方法を記載します。

~やり方~
----------------------------------------------------------------------------
【1】アーカイブファイルをメモリドマップファイルとして開く
【2】ヘッダー部分を読み込む。
   ファイル名、ファイルサイズを取得
【3】各ファイルの実データのオフセット(開始位置)を計算

(サイズ200、100のデータがある場合、3つ目のデータ開始位置は300から、など)
【4】ヘッダーの長さを計算
【5】ヘッダーの長さ + 実データのオフセット を実データの開始位置として計算する

(サイズ100、200のファイルを合体させた場合、2番目の実データの範囲は
  p[100]~[299]となるが、ヘッダーの長さが45あった場合、当然 実データの開始位置も
    ヘッダーのサイズだけずれる。 (実データの範囲は p[145]~[344]となる。))
----------------------------------------------------------------------------

【1】アーカイブファイルをメモリドマップファイルとして開く
メモリドマップファイルって何じゃーと思うかもしれませんが、
これ一番重要。
(特にアーカイブファイルの読み込みにおいては)

まず、アーカイブファイルを読み込む際の問題点が2つ。
----------------------------------------
・ファイルサイズが巨大になっている為、一度に読み込むわけにはいかない
・ランダムアクセスをしたい

----------------------------------------

■前者について
前者は、例えばアーカイブファイルのサイズが1GBになっていたとします。
ゲーム起動時に1GBとか読み込んでたら間違いなく
起動時間の遅延メモリ不足による異常終了などの問題が出てきます。

■後者について
後者は、例えばタイトル画面で使うBGMや画像が バラバラに保存されているとします。
この場合、必要なファイルの位置まで、Seek関数とかfgets関数とかで
ファイルポインタを移動させるのは非効率です。

※仮にデータのサイズが1000あるとして、200、500の位置に該当ファイルがある場合、
以下の様に添え字を使って即ランダムアクセスできた方が絶対に便利です。
p[199]//該当ファイルの先頭部分
p[499]//該当ファイルの先頭部分

で、上記問題を解決してくれるのがメモリドマップファイルという訳です。
*****************************************************************
★メモリドマップファイルで読み込む利点
・ランダムアクセスが出来る
・ランダムアクセスで実際に読み込みを行った分しかメモリを消費しない。

*****************************************************************

【参考画像】
archive2.jpg

例えば上記のサイズが160のアーカイブファイルがあるとします。

この時、ヘッダーを読み込み(サイズ21)、
ヘッダーの情報からp[60]のファイルBを読み込んだ場合(サイズ48)、
使用メモリは 21 + 48 = 69だけ増加します。
※全体のサイズ分(160)の使用メモリ増加は起こらない。

実にすばらしいですね。

そんな訳で、メモリドマップファイルとして開く部分のコード。 ↓
//ファイルのハンドルを得る
m_FileHandle = CreateFileA( t_ArchiveFileName.c_str(), GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0 );
if( m_FileHandle == INVALID_HANDLE_VALUE ){
return(-1);
}

//ファイルのハンドルから、マップドファイルのハンドルを得る
m_MappedFileHandle = CreateFileMapping( m_FileHandle, 0, PAGE_READONLY, 0, 0, t_ArchiveFileName.c_str() );
if( m_MappedFileHandle <= 0 ){
return(-1);
}

まずファイルハンドルを取得、
そして、その取得したファイルハンドルをCreateFileMapping関数に渡して、
メモリマップドファイルとしてのハンドルを取得すればOKです。


【2】ヘッダー部分を解析(ファイル名、ファイルサイズ取得)
【3】各ファイルの実データのオフセット(開始位置)を計算
【4】ヘッダーの長さを計算


2~4はプログラム側で一気にやってるので、まとめて記載します。
まず、MapViewOfFile関数に、先ほどのメモリドマップファイルのハンドルを渡し、
ランダムアクセスできるポインタを取得します。

後は、そのポインタを用いて、ヘッダー情報の解析、
またファイル名とメモリの位置(添え字)を対応付けるstd::mapの登録
を行います。

//メモリの位置(添え字)、ファイルサイズ、ファイル名の構造体。
struct MyFileInfo {
int m_MemoryNum;
int m_FileSize;
std::string m_FileName;
MyFileInfo(int t_MemoryNum, int t_FileSize, std::string t_FileName){
m_MemoryNum = t_MemoryNum;
m_FileSize = t_FileSize;
m_FileName = t_FileName;
}
};

//ポインタを取得
m_pFile = (char*)MapViewOfFile( m_MappedFileHandle, FILE_MAP_READ, 0, 0, 0);

//アーカイブファイルの、ヘッダー部分の読み込み
int t_Num = 0, t_MemoryNum = 0, t_FileNum = 1;
for( ; m_pFile[t_Num] != '#'; t_Num++)
{
//ファイルサイズを読む
int t_FileSize = 0;
for( ; m_pFile[t_Num] >= '0' && m_pFile[t_Num] <= '9'; t_Num++)
{
t_FileSize = t_FileSize*10 + (m_pFile[t_Num] - '0');
}

//ファイル名を読む
char t_FileName[1024] = {'\0'};
t_Num += 1;
for( int i = 0; m_pFile[t_Num] != '"'; t_Num++, i++)
{
t_FileName[i] = m_pFile[t_Num];
}

//ファイルとポインタの位置を対応させる為のmap登録
MyFileInfo t_Info( t_MemoryNum, t_FileSize, t_FileName);
std::pair<std::string, MyFileInfo> p(t_FileName, t_Info);
m_FileNameMap.insert(p);

//ファイル番号で検索する為のVector登録
m_FileDataVec.push_back(t_Info);

t_MemoryNum += t_FileSize;
t_FileNum += 1;
}

//ヘッダーのサイズを保存
m_HeaderSize = t_Num + 1;


【5】ヘッダーの長さ + 実データのオフセット を算出


ファイル名 サイズ
Test1.png 120
Test2.png 250
Test3.png 330


アーカイブファイルの情報が上記だった場合、
std::mapには以下の様に登録された状態になります。

Key          Second
Test1.png  ( 0, 120, "Test1.png")//ファイル開始位置、サイズ、ファイル名 の順
Test2.png  (120, 250, "Test2.png")//ファイル開始位置、サイズ、ファイル名 の順
Test3.png  (370, 330, "Test3.png")//ファイル開始位置、サイズ、ファイル名 の順


上記std::mapのSecond内の
ファイルの開始位置は ヘッダーのサイズが考慮されていないので、
関数を実装する場合は、 ヘッダーのサイズ + ファイルの開始位置 を
実際のファイル開始位置として算出
する必要があります。

こんな感じ。↓
const char* CArchive::GetPointer(std::string t_FileName)
{
std::map::iterator it;
it = m_FileNameMap.find(t_FileName);

if(it != m_FileNameMap.end()){
//ファイル開始位置 + ヘッダーサイズが、実際のファイル開始位置!
return(&m_pFile[it->second.m_MemoryNum + m_HeaderSize]);
}else{
return(NULL);
}
}

※最初から、std::mapのsecond内のファイル開始位置を、
 (ファイル開始位置 + ヘッダーサイズ)で登録しておいても良い。
その際は、return(&m_pFile[it->second.m_MemoryNum]);と記載すればOKになる。

ただ、自分は何となく、
「各ファイルのサイズから算出した位置に、ヘッダーサイズ分のズレを足してますよ」というのを
分かり易くするために、あえてヘッダーサイズを後から足すプログラムにしてみた。
-----------------

あと最後に、使用したハンドルはCloseすればOKです。
忘れないようにしましょう。

//取得したポインタを開放
if( UnmapViewOfFile( m_pFile ) == 0 ){
// クローズ失敗
}
CloseHandle( m_MappedFileHandle ); // ファイルマッピングオブジェクトハンドルを閉じる;
CloseHandle( m_FileHandle ); // ファイルハンドルを閉じる


以上です。


そして以下がソースコード。
それぞれ、main.cpp、CArchive.h、CArchive.cppとかに変更して
実行させてみてください。

何か問題が発生しそうな動作が無ければ
おそらく正常に動作すると思います。

■約束事?
・ファイル名にマルチバイト文字が入っていない事
・実行ファイルを強制終了させない事
・アーカイブファイルじゃないファイルの拡張子を、.arcに変更しない事
・アーカイブファイルをテキスト形式で開いて、そのまま上書き保存しない事
・複数のアーカイブファイル自体をアーカイブファイル化しない事 (解析できません)
・アーカイブファイル読み込み中に、読み込み中のアーカイブファイルの削除、移動はしない事
・ファイルの拡張子は、全部大文字または小文字である事 (.PnG .Jpgとかは×)

・画像と音楽ファイルのみ再生可能


【ソースコード】Main.txt
【ソースコード】CArchive_h.txt
【ソースコード】CArchive_cpp.txt

操作方法:Z ・・・ 決定    「→」「←」「↑」「↓」 ・・・ カーソル移動
     Enter ・・・ 決定   X ・・・ 選択したファイルのキャンセル 

【実行画像】
archive3.jpg


↑アーカイブファイルを解析した際の画像。

解析した画像のサムネイルを作成して、綺麗に列挙するなんていう機能は無く、
画面中央右くらいに画像が表示されるのみ
(画像がウィンドウ内に収まりきらないから縮小表示、とかそういう考慮はしない。)
という投げやりな仕様になってたりする。

とりあえず解析できれば良いんですよ・・・!(`・ω・´)

【補足】
忘れてた。
DXライブラリを使用する場合、ファイル名ではなくメモリを指定して
画像、音声ファイルを読み込む場合
は以下の関数を使います。

・CreateGraphFromMem関数//メモリ位置を指定して、画像読み込み
・LoadSoundMemByMemImage関数//メモリ位置を指定して、音声読み込み


★使用例
const char* t_p = t_Archive.GetPointer(i + 1);
int t_FileSize = t_Archive.GetFileSize(i + 1);
CreateGraphFromMem( (void*)t_p, t_FileSize );

★使用例
const char* t_p = t_Archive.GetPointer(i + 1);
int t_FileSize = t_Archive.GetFileSize(i + 1);
LoadSoundMemByMemImage( (void*)t_p, t_FileSize );
スポンサーサイト

テーマ : ゲーム製作 関連 - ジャンル : ゲーム

コメント
コメントの投稿
管理者にだけ表示を許可する



上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。