ゲームプログラミングTips(6):ファイル入出力
2012年5月14日……前回の講座より、実に7日ぶりである。
実は最近忙しく、次の講座を書けるのははたしていつになるのやら……ううむ(´Д`)
1.ファイル入出力とは?
……説明するまでもない、ファイルからの読み込みとファイルへの書き込みのことである。
2.ファイル入出力のゲームプログラミングにおける活用事例
むしろ、非活用事例の方を教えてもらいたい(;´Д`)
基本、セーブおよびロードの概念が存在するゲームでは使われている。
いや、セーブおよびロードの概念がなくともコンフィグの自動セーブぐらいやってるだろう。
「セーブもロードもしねぇ!コンフィグも毎回起動時に設定し直せ!」という気概がなきゃ普通は使うだろう。
本項で語る内容はここからが本題である。何故、ファイル入出力なんぞに1回の説明を要するのか?
C言語を使う人は「fprintfとかfwriteとか使えばいいんじゃね?語る内容なくね?」と思う人もいるだろう。
語る内容はあるのである、詳しくは下で!
3.具体例(ヘッダ解説)
本項では、俺が作ったファイル入出力クラスを掲載する。
/************************************************* ファイル名:FileReader.h 作成者 :あびす 役割 :ファイル出力 *************************************************/ /** * @file FileReader.h * @brief ファイル入力を行うクラス関係です。 */ #ifndef DX9A_FILE_FILEREADER_H #define DX9A_FILE_FILEREADER_H namespace nsDX9A{ namespace nsFile{ namespace nsFileReader{ //ファイル入力クラス(インターフェース) /** * @brief ファイル入力を行うクラスのインターフェースです。 */ class IFileReader{ public: /** * @brief 仮想デストラクタです。 */ virtual ~IFileReader(){} //仮想デストラクタ /** * @brief fopen関数の互換メソッドです。 * @param FileName [in]オープンするファイル名を指定します。 * @param Mode [in]ファイルモードを指定します。 * @return ファイルのオープンが成功した場合はtrue、失敗した場合はfalseを返します。 */ virtual bool fopen(const char* FileName, const char* Mode) = 0; //fopen /** * @brief fclose関数の互換メソッドです。 * @return ファイルのクローズが成功した場合はtrue、失敗した場合はfalseを返します。 */ virtual bool fclose() = 0; //fclose /** * @brief ファイルがオープンされているかを取得します。 * @return ファイルがオープンされている場合はtrue、されていない場合はfalseを返します。 */ virtual bool fisopen() = 0; //fisopen /** * @brief ファイルサイズを取得します。 * @return ファイルサイズを返します(単位:バイト) */ virtual long fsize() = 0; //fsize /** * @brief fgetc関数の互換メソッドです。 * @param c [out]読み込んだ文字を格納します。 * @return 文字の読み込みが成功した場合はtrue、失敗した場合はfalseを返します。 */ virtual bool fgetc(char* c) = 0; //fgetc /** * @brief fgets関数の互換メソッドです。 * @param s [out]読み込んだ文字列を格納します。 * @param n [in]読み込む最大文字数を指定します。 * @return 文字列の読み込みが成功した場合はtrue、失敗した場合はfalseを返します。 */ virtual bool fgets(char* s, int n) = 0; //fgets /** * @brief fread関数の互換メソッドです。 * @param buffer [out]読み込んだデータを格納します。 * @param size [in]項目のサイズを指定します。 * @param count [in]読み込む最大項目数を指定します。 * @return 実際に読み込まれた完全な項目の数を返します。 */ virtual size_t fread(void* buffer, size_t size, size_t count) = 0; //fread /** * @brief ftell関数の互換メソッドです。 * @return 現在のファイル位置を返します(単位:バイト) */ virtual long ftell() = 0; //ftell /** * @brief fseek関数の互換メソッドです。 * @param offset [in]originからのバイト数を指定します。 * @param origin [in]初期位置を指定します。 |
/************************************************* ファイル名:FileWriter.h 作成者 :あびす 役割 :ファイル出力 *************************************************/ /** * @file FileWriter.h * @brief ファイル出力を行うクラス関係です。 */ #ifndef DX9A_FILE_FILEWRITER_H #define DX9A_FILE_FILEWRITER_H namespace nsDX9A{ namespace nsFile{ namespace nsFileWriter{ //ファイル出力クラス(インターフェース) /** * @brief ファイル出力を行うクラスのインターフェースです。 */ class IFileWriter{ public: /** * @brief 仮想デストラクタです。 */ virtual ~IFileWriter(){} //仮想デストラクタ /** * @brief fopen関数の互換メソッドです。 * @param FileName [in]オープンするファイル名を指定します。 * @param Mode [in]ファイルモードを指定します。 * @return ファイルのオープンが成功した場合はtrue、失敗した場合はfalseを返します。 */ virtual bool fopen(const char* FileName, const char* Mode) = 0; //fopen /** * @brief fclose関数の互換メソッドです。 * @return ファイルのクローズが成功した場合はtrue、失敗した場合はfalseを返します。 */ virtual bool fclose() = 0; //fclose /** * @brief ファイルがオープンされているかを取得します。 * @return ファイルがオープンされている場合はtrue、されていない場合はfalseを返します。 */ virtual bool fisopen() = 0; //fisopen /** * @brief ファイルサイズを取得します。 * @return ファイルサイズを返します(単位:バイト) */ virtual long fsize() = 0; //fsize /** * @brief fputc関数の互換メソッドです。 * @param c [in]書き込む文字を指定します。 * @return 文字の書き込みが成功した場合はtrue、失敗した場合はfalseを返します。 */ virtual bool fputc(char c) = 0; //fputc /** * @brief fputs関数の互換メソッドです。 * @param str [in]書き込む文字列を指定します。 * @return 文字列の書き込みが成功した場合はtrue、失敗した場合はfalseを返します。 */ virtual bool fputs(const char* str) = 0; //fputs /** * @brief fwrite関数の互換メソッドです。 * @param buffer [in]書き込むデータへのポインタを指定します。 * @param size [in]項目のサイズを指定します。 * @param count [in]書き込む最大項目数を指定します。 * @return 実際に書き込まれた完全な項目の数を返します。 */ virtual size_t fwrite(const void* buffer, size_t size, size_t count) = 0; //fwrite /** * @brief fprintf関数の互換メソッドです。 * @param format [in]出力文字列を指定します。 |
今回は2ファイルに対する解説を行っていく。
ヘッダに対する解説では、本クラスの使用方法ならびに存在意義を説明する。
使用方法は簡単である。
1.インスタンスを生成する
2.コンストラクタまたはfopenメソッドでファイルをオープンする
3.各種メソッドまたは関数を使用しファイルへの入力またはファイルへの出力を行う
4.デストラクタまたはfcloseメソッドでファイルをクローズする
具体例 CFileReader reader("a.txt", "rb"); //ファイル入力クラス CFileWriter writer("b.txt", "wb"); //ファイル出力クラス printf("%d", reader.fsize()); //ファイルサイズを表示 writer.fprintf("てすと"); //ファイルに文字列を書き込み string temp; LoadCryptedString(&reader, 255, &temp); //ファイルから暗号化した文字列を読み込み SaveCryptedString(&writer, 255, temp); //ファイルに暗号化した文字列を書き込み |
……これだけである、C言語を使う人は各種ファイル入出力関数に読み替えれば理解できる筈。
(まあ、fsizeとか〜CryptedString関数とか標準関数に存在しないものもあるが)
では、以下にて具体的な説明を行っていく。
ただし、なるべく説明は簡略に行う。
ヘッダの説明
何故書き込みと読み込みでクラスを分けているの?
同じファイルに対して、書き込みと読み込みを同時に行うことはないからである。
こうしておいた方が、読み込み対象のファイルにうっかりfwriteとか馬鹿な真似を防止できる。
……ファイルモードがその役目だって? はいはいそうでしたすんません、俺が悪うござんした('A`)
何故fscanfメソッドがないの?
ぶっちゃけると俺の技術力不足です、すんません(;´Д`)
LoadCryptedString関数およびSaveCryptedString関数は何?
暗号化文字列の読み込み・書き込みを行う関数である。
何故その必要があるのか、以下に理由を記す。
暗号化の必要性
これはゲームにもよる。
たとえばネットゲームの場合、暗号化を行わないと「チートおk^^」になってしまう。
他プレイヤーに影響を及ぼすことのないゲームの場合は、暗号化を行う必要は一切ない。
ただし、文字列に関しては一応暗号化を行っておいた方がいい。
何故なら、テキストエディタでセーブデータを開いた時に文字列はそのまま見えてしまうからである。
その点、int型等の数値はそのままでは見えない。よって、文字列にかぎり関数を作成し提供している。
そもそもこんなクラス必要なの?
この疑問に対する答えが本項最大の重要ポイントである、まず答えをぶっちゃけてしまうと必要である。
何故なら、各種リソースを取り扱う際には管理クラスとファイル入出力クラスを分けるべきだからである。
以下に、具体例を挙げて説明する。
クラス分割の必要性
グラフィックを取り扱うクラスG、サウンドを取り扱うクラスSが存在したとする。
また、ファイル入出力の方法を通常とパッキングを行ったファイルに対するものの2通りが存在したとする。
この場合、以下のクラスが必要となるだろう。
グラフィック+読み込み通常:GN
グラフィック+読み込みパッキング:GP
サウンド+読み込み通常:SN
サウンド+読み込みパッキング:SP
ここで動画を取り扱うクラスMを新たに作るとすると、
動画+読み込み通常:MN
動画+読み込みパッキング:MP
上記のようになる、つまりひとつの取り扱うクラスを必要とする度に2クラスを作成しなければならない。
2クラスならまだいいのだが、新たなファイル入出力の方法を必要とした場合は更に増えていく。
その点、あらかじめファイル入出力と管理を分割しておくと、
グラフィック+読み込み通常:G+FN
グラフィック+読み込みパッキング:G+FP
サウンド+読み込み通常:S+FN
サウンド+読み込みパッキング:S+FP
動画+読み込み通常:M+FN
動画+読み込みパッキング:M+FP
上記のような形となる(※通常のファイル入出力がFN、パッキングを行ったファイルに対する入出力がFP)
一見何も変わっていないようだが、よく見ると作成するクラス数が変わっている(非分割は6、分割は5)
上記の例だとたいした差ではないが、取り扱うクラスやファイル入出力の方法の数次第では大きな差となる。
よって、管理クラスとファイル入出力クラスは分けておくべきである。
4.具体例(ソース解説)
次はソース解説である。
#include "stdafx.h" #include "../index.h" #include "FileReader.h" namespace nsDX9A{ namespace nsFile{ namespace nsFileReader{ //デフォルトコンストラクタ CFileReader::CFileReader(){ stream = NULL; } //コンストラクタ CFileReader::CFileReader(const char* FileName, const char* Mode){ fopen(FileName, Mode); } //デストラクタ CFileReader::~CFileReader(){ fclose(); } //fopen bool CFileReader::fopen(const char* FileName, const char* Mode){ if(stream){ DX9AErrOut("Failed : CFileReader::fopen Open Data Already Exists\n"); return(false); } if(Mode){ size_t szMode = strlen(Mode); if(szMode>=1 && Mode[0]!='r'){ DX9AErrOut("Failed : CFileReader::fopen Invalid File Mode\n"); return(false); } if(szMode>=2 && Mode[1]=='+'){ DX9AErrOut("Failed : CFileReader::fopen Invalid File Mode\n"); return(false); } } stream = ::fopen(FileName, Mode); return(stream!=NULL); } //fclose bool CFileReader::fclose(){ if(stream){ bool Result = (::fclose(stream) == 0); if(Result){ stream = NULL; } return(Result); } return(true); } //fisopen bool CFileReader::fisopen(){ return(stream!=NULL); } //fsize long CFileReader::fsize(){ long TempFilePos = ftell(); fseek(0, SEEK_END); long Result = ftell(); fseek(TempFilePos, SEEK_SET); return(Result); } //fgetc bool CFileReader::fgetc(char* c){ int Temp; Temp = ::fgetc(stream); *c = static_cast< char >(Temp); return(Temp!=EOF); } //fgets bool CFileReader::fgets(char* str, int n){ return(::fgets(str, n, stream)!=NULL); } //fread size_t CFileReader::fread(void* buffer, size_t size, size_t count){ return(::fread(buffer, size, count, stream)); } //ftell long CFileReader::ftell(){ return(::ftell(stream)); } //fseek bool CFileReader::fseek(long offset, int origin){ return(::fseek(stream, offset, origin)==0); } //fgetpos bool CFileReader::fgetpos(fpos_t* pos){ return(::fgetpos(stream, pos)==0); } //fsetpos bool CFileReader::fsetpos(const fpos_t* pos){ return(::fsetpos(stream, pos)==0); } //feof bool CFileReader::feof(){ return(::feof(stream)!=0); } //ferror int CFileReader::ferror(){ return(::ferror(stream)); } //補助関数 //暗号化文字列を読み込む void LoadCryptedString(IFileReader* FileReader, BYTE CryptKey, string* Data){ unsigned int i=0; char strData[64 * 1024] = {0}; while(true){ FileReader->fread(strData+i, sizeof(char), 1); if(strData[i]=='\0'){ break; } else{ if(strData[i]!=CryptKey){ strData[i] ^= CryptKey; } i += 1; } } Data->assign(strData); } } } } | #include "stdafx.h" #include "../index.h" #include "FileWriter.h" namespace nsDX9A{ namespace nsFile{ namespace nsFileWriter{ //デフォルトコンストラクタ CFileWriter::CFileWriter(){ stream = NULL; } //コンストラクタ CFileWriter::CFileWriter(const char* FileName, const char* Mode){ fopen(FileName, Mode); } //デストラクタ CFileWriter::~CFileWriter(){ fclose(); } //fopen bool CFileWriter::fopen(const char* FileName, const char* Mode){ if(stream){ DX9AErrOut("Failed : CFileWriter::fopen Open Data Already Exists\n"); return(false); } if(Mode){ size_t szMode = strlen(Mode); if(szMode>=1 && Mode[0]=='r'){ DX9AErrOut("Failed : CFileWriter::fopen Invalid File Mode\n"); return(false); } } stream = ::fopen(FileName, Mode); return(stream!=NULL); } //fclose bool CFileWriter::fclose(){ if(stream){ bool Result = (::fclose(stream) == 0); if(Result){ stream = NULL; } return(Result); } return(true); } //fisopen bool CFileWriter::fisopen(){ return(stream!=NULL); } //fsize long CFileWriter::fsize(){ long TempFilePos = ftell(); fseek(0, SEEK_END); long Result = ftell(); fseek(TempFilePos, SEEK_SET); return(Result); } //fputc bool CFileWriter::fputc(char c){ return(::fputc(c, stream)!=EOF); } //fputs bool CFileWriter::fputs(const char* str){ return(::fputs(str, stream)!=EOF); } //fwrite size_t CFileWriter::fwrite(const void* buffer, size_t size, size_t count){ return(::fwrite(buffer, size, count, stream)); } //fprintf int __cdecl CFileWriter::fprintf(const char* format, ...){ char buffer[64 * 1024]; const int n = wvsprintf(buffer, format, reinterpret_cast< va_list >(&format+1)) - 1; if(buffer[n]=='\n'){ buffer[n+1] = '\0'; } return(::fprintf(stream, "%s", buffer)); } //ftell long CFileWriter::ftell(){ return(::ftell(stream)); } //fseek bool CFileWriter::fseek(long offset, int origin){ return(::fseek(stream, offset, origin)==0); } //fgetpos bool CFileWriter::fgetpos(fpos_t* pos){ return(::fgetpos(stream, pos)==0); } //fsetpos bool CFileWriter::fsetpos(const fpos_t* pos){ return(::fsetpos(stream, pos)==0); } //feof bool CFileWriter::feof(){ return(::feof(stream)!=0); } //ferror int CFileWriter::ferror(){ return(::ferror(stream)); } //補助関数 //暗号化文字列を書き込む void SaveCryptedString(IFileWriter* FileWriter, BYTE CryptKey, const string& Data){ char strData[64 * 1024] = {0}; strcpy(strData, Data.c_str()); for(unsigned int i=0;strData[i]!='\0';i++){ if(strData[i]!=CryptKey){ strData[i] ^= CryptKey; } } FileWriter->fwrite(strData, strlen(strData)+1, 1); } } } } |
ぶっちゃけ説明するべきところはないので、ソースコードを参照のこと(´Д`)
……以上で本項は終了である、お疲れ様でした(´Д`)
本項に見るべきところはあまりない、ソースコードを見れば分かるが単に標準関数のラップをしているに過ぎない。
本項で最も重要なのは設計である。メンテナンス性の高いプログラムを書くには、設計こそが最も重要なものである。
今後もこういったイディオムは積極的に紹介していく予定である、こういうところまであんまり書いてるサイトはないっぽいしなぁ(´Д`)