1122: 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文は存在しないことにしときます。

プロフィール

  • 1983年生まれ。
    C++、PHP、JavaScript、ActionScriptで何か作ってます。

技術書

Effective C++ 原著第3版 (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
C++ Coding Standards―101のルール、ガイドライン、ベストプラクティス (C++ in‐depth series)
JavaScript 第5版
まるごとJavaScript & Ajax ! Vol.1
CakePHPガイドブック
CakePHP ポケットリファレンス (Pocket Reference)
Powered by Movable Type 4.01

このアーカイブについて

このページには、11222008年5月に書いたブログ記事が含まれています。

前のアーカイブは1122: 2008年4月です。

次のアーカイブは1122: 2008年6月です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

2008年10月

      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  

リンク