2008年5月アーカイブ
ゲームプログラムで使うデバッグトレース、ログ出力、エラーメッセージ表示について。それぞれどういうタイミングで使用するべきか、どういう位置づけで使用するべきか、決めてみた。
[デバッグトレース]
- ファイル読み込みや、シーンの切り替えなど、重要処理や全体の処理フローがわかるようにトレースする
- エラー発生時に、そのときの関数の名前や行数などをトレースする
[ログ出力]
- エラー発生時に、そのときの関数の名前や行数などを、エラーログとして出力する
[エラーメッセージ表示]
- エラー発生時に、そのことがプレイヤーにわかるよう、メッセージダイアログにて、エラーメッセージを出力する
- 基本的には、ゲーム起動時に出力する
例)「DirectXの初期化に失敗しました」
「画像ファイルの読み込みに失敗しました」
重要度でいくと、
エラーメッセージ表示 > ログ出力 > デバッグトレース
っていう順番かな。
Cで時刻を表すのに、time_t型を使ってるけど、その型が2038年の1月位までしか対応してない。調べてみたら、2038年問題ってのがあった。
2038年問題 - Wikipedia
2038年1月19日3時14分7秒を越えると、この値がオーバーフローし、負と扱われるため、誤作動する可能性が高い。
time_t型のサイズを増やしても、まだ問題があるらしい。
2038年問題と同じように考えると、292277026596年に桁あふれが起こる可能性があり、これを「292277026596年問題」と呼ぶこともある。
292277026596年。ニクニ、ニナナオ、ニムゴクムね。勉強になるぜ。
ファイルから読み込んだバイナリデータを扱うプログラムを改造してんだけど、読み込むファイルのデータがビッグエンディアンになってて、動かす環境はx86系でリトルエンディアンなんで。
1バイトだけのデータとか文字列は別にエンディアンを考慮する必要なし。1バイトずつ見る分にはひっくりかえりようがないから。2バイト以上の整数値は変換しないといけない。
memcpy(&shNum, pFileData, sizeof(short));
memcpy(&lNum, pFileData, sizeof(long));
とかやったら、shNum、lNumは変換必要。そもそも、memcpy関数使わないで、エンディアン変換込みのメモリコピー関数を独自で作ればいいけど。
嫌だったのは、バイナリデータが構造体を表してて、それを取得するところ。構造体は、long型(4バイト)とかのメンバをめちゃくちゃ持ってた。
struct ST_TEST
{
long lDataA;
long lDataB;
long lDataC;
…
…果てなく続く(嘘)…
}
この構造体をそのまま
memcpy(&stTestData, pFileData, sizeof(ST_TEST));
ってしたら、↓こんなバイナリデータが、
00 00 00 01 00 00 00 0A 00 00 00 02 …
それぞれ4バイト区切りでひっくり返った値になって解釈されちゃう。↓こんな風に。
lDataA:0x01000000 ※本当は0x00000001というデータで欲しい
lDataB:0x0A000000
lDataC:0x02000000
…
というわけで、せっかく構造体として存在してるのに、わざわざメンバ一個ずつ見てエンディアン変換かけることに。めんどい。
リトルエンディアンって、よくいう「バッドノウハウ」ってやつになるのかな。
関数内で、do~while (false)を使うのが好き。
使う人は当たり前のように使ってるけど、この書き方が嫌いな人も結構いるっぽい。
メリットは、
- if文の多用や、それによるインデントの数を防げる
- 終了時の処理が分散しない(途中returnを無くせる)
ってとこだと思う。
実際↓こんな感じになる。
/**
* なんかの初期化関数
*
* @param[in] a_nIn 入力値
*
* @return 処理結果
* @retval true 成功
* @retval false 失敗
*/
bool Init(int a_nIn)
{
bool bRet = true; // 戻り値
char* pszError = NULL; // エラーメッセージ
do
{
// パラメータチェック
if (a_nIn < 0)
{
pszError = "パラメータエラー";
bRet = false;
break;
}
// オブジェクトの生成
if (! CreateObject())
{
pszError = "オブジェクト生成エラー";
bRet = false;
break;
}
// オブジェクト2の生成
if (! CreateObject2())
{
pszError = "オブジェクト2生成エラー";
bRet = false;
break;
}
}
while (false);
// エラーメッセージ表示
if (! bRet)
{
OutputErrorMessage(pszError);
}
// ログ出力とか?必要なら
OutputLog(__FUNCTION__);
return bRet;
}
途中returnあると、終了時の処理が分散しちゃって見にくいし、管理しにくい。終了処理は一箇所にまとまってる方がいいかなと。
ちなみに、while文使って、
while (true)
{
// パラメータチェック
if (a_nIn < 0)
{
pszError = "パラメータエラー";
bRet = false;
break;
}
…(略)…
break;
}
でもいいかと思ったけど、会社の人と話したときに、
「breakは忘れるから怖い」
って言ってた。while(true)でbreak忘れて無限ループになるのが怖いと。
do~while文だったら、最後のwhile忘れたらコンパイルがそもそもできないし、その最後のwhileにfalseじゃなくて敢えてtrueを指定するって間違いは無いと思う。
でも、while文のときは、while(true)の中で最後にbreak忘れても、コンパイル通っちゃう。そのbreakだけ忘れるとかだったらやっちゃいそう。
あ、goto文は存在しないことにしときます。






