main() blog

プログラムやゲーム、旅、愛する家族について綴っていきます。

バグを出さないプログラマになるための「心得」

1.はじめに

長年コンシューマゲーム開発に携わってきた中で毎回思うことがある。
開発の終盤には必ずテストプレイが行われ、毎回大量のバグと闘っている。
なぜ大量のバグが出ているのか、どうしたら大量のバグが出なくなるのかについて考え、まとめてみることにした。

バグは「潰す」のではなく「出さない」ことに注力すべき!

バグを「出さない」ようにするためには以下の2つを心掛ける必要がある。

「自分」のところでいかにして「バグを出さない」ようにするか。

  • 実装確認をする
  • 自己完結させる
  • 常に自分を疑う

「他の人」のところでいかにして「バグを出させない」ようにするか。

  • エラーは早い段階でエラーとして処理する
  • エラーを分かりやすく教えてあげる

バグを「出している」というのは新人でもベテランでも一緒。
たとえ学術的なコーディングスキルがあっても、新人が書いたコードでもバグは出る。
実装範囲の大小も関係ない。

いかに「責任」を持って取り組んでいるかで、バグを出すか出さないかに違いが出てくる。

これがイケてる(=バグを出さない)プログラマである。



2.現状の問題点

  • 大量の不具合報告
  • 大量のバグを潰すのは相当の労力を必要とする
  • 精神衛生上もよろしくない

コンシューマ開発において一つのプロジェクトでの不具合報告が数千件を超えるケースも珍しくない。
それが仕様の増大化、複雑化によるものなのか、短期のプロジェクトによるものなのか原因は色々考えられる。
最近ではゲームのボリュームやオンライン要素などに伴いテストプレイは最低でも2カ月、3か月かけて行われるようになっている。

※以下あくまでもイメージとしての事例

  • 事例①

ジャンル:アクションRPG
プラットフォーム:PSVitaPS3
言語:日本、北米、欧州
オンラインによるマルチプレイあり

不具合報告件数:5000件
テストプレイ期間:3ヶ月

  • 事例②

ジャンル:アクション
プラットフォーム:PSVita
言語:日本
オフラインのみ

不具合報告件数:2000件
テストプレイ期間:2ヶ月

プロジェクトの規模、内容にもよるが、概ねこれくらいの期間のテストプレイが行われ、これぐらいの不具合報告の件数がある。
最近ではリリース前のテスプレイだけではなく、開発途中の各種マイルストーン毎にもテストプレイが行われたり、プロジェクトによっては開発中も開発メンバーのみで不具合報告を行っているケースもあるので実際にはもっと多くの不具合報告が行わていることになる。

バグが多ければ多いほどデバッグ工数が増える。
当然コストも膨れ上がる。

大量のバグのチケットを捌くことで本来のテストプレイの質も下がり致命的な不具合が見過ごされる可能性がある。

3.対応方法、解決方法は?

ここに挙げたのは一例でプロジェクトに合わせて実践したり、導入したりしている。

コーディング規約は保守性は高くなる。
導入コストも低く、プロジェクトでやっているところも多い。
プロジェクトで徹底させる必要がある。

ユニットテストは社内ライブラリやプロジェクト共有のプログラムで継続的に使用されるようなケースでは適用できる可能性はある。
ゲーム側の場合、短期の開発で導入が困難であったり、ゲーム側の場合テストする範囲としての切り出しが困難だったりする。

静的解析はあると良い。
ただし、ちゃんと運用しないと意味がない。
大量の報告があると放置されたり、どう対処すべきかなどのスタディが必要になってくる。

コードレビューも継続するのが大変。
どう指摘するのが良いのか、レビュワーのスキルによってもばらつきが出やすい。

コンシューマ開発でペアプログラミングを実践しているところはあるのだろうか?

デバッガはバグを追うのには必須。
ただステップで見ていくだけでなく、使い方もちゃんと習得する必要がある。
でも、大量不具合が出ている場合は大量に潰すことには変わりない。

CIは安定的なビルドを提供するために必要。
ただ、ビルドは通っているけど朝の最新版で起動したら落ちたという場合もある。

手法はたくさんあるが全てを導入したり、継続的に行うのは困難なものある。

4.まずは「心掛け」を考えよう

  • 方法論を駆使すれば効率は良くなるかもしれない
  • 方法論はまた別の機会にでも議論する
  • それ以前にコーディングする人の「心掛け」一つでバグは減らせる

バグを出さないように「心掛ける」ができていないと、方法論を駆使しても結局バグが出ることには変わりがない。

5.そもそも大量に出ているバグは...

  • それって本当に致命的な不具合?
  • デバッグ期間を実装確認と勘違いしていないか?
  • プログラムなんだから「バグ」が出て当然と思っていないか?

5000件出ている不具合の全てがプログラムによるものではない。
仕様的な不具合や文言の不具合、キャラクタモデルの形状のチェックやフィールドのコリジョンチェック、モーションのチェックなどリソースの不具合も相当な数含まれている。

ただ、この様な不具合の報告もあったりする。

  • 不具合報告①
特定のスキルを使用すると必ずハングアップします。
再現頻度:10回/10回中
手順:
1.主人公のレベルを50にします。
2.それで習得できるスキル「×××」を使用します。
3.ハングアップします。

他のスキルを使用してもハングアップしません。
:
:
  • 不具合報告②
ステータス画面で特定のキャラクタを開くと必ずハングアップします。
再現頻度:10回/10回中
手順:
1.キャラクター「○○○」を選択します。
2.ステータス画面を開きます。
3.ハングアップします。
:
:
  • 不具合報告③
オプションメニューのカーソル移動時のSEが再生されていません。
再現頻度:10回/10回中

この場合、カーソル移動音が再生されるのが適切かと思われます。
手順:
:
:

これは単に実装漏れというか、テスト前に誰も確認していないのか?
実装したプログラマ、仕様を書いたプランナー、音をつけるサウンドエンジニア、もちろんディレクターも。
誰もチェックしていないのにテストプレイで指摘って、実装者が確認すれば見つけられたバグもあるはず。

デバッグ期間は商品としてのクオリティを上げるための作業、もっと致命的なバグを見つけるための期間だということを忘れていないか?

6.どうしてバグが出ているの?

  • とりあえず動いているから大丈夫
  • サンプルプログラムをそのまま流用
  • バグを人のせいにする
  • テスターさんからのツッコミがあったら修正

とりあえず動いているから大丈夫

誰でもとりあえず動いているものは大丈夫だと思う。
そして当然のことながら不具合が出ない限りは何もしない。
ところがこういった実装が自分とは全く関係ないところに影響を及ぼす可能性があり、
不具合が起きた時に特定しづらいケースを生むことがよくある。
エラーチェックを強化した途端、動かなくなることもしばしばある。

サンプルプログラムの流用

SDKのサンプルプログラムや書籍のサンプルプログラムをそのまま流用しているケースも見かける。
サンプルプログラムはあくまでもサンプル。
動作保証はまったくない。
バグあって当然だと思わなければならない。

昔、DirectXのサンプルプログラムにIMEを使ったサンプルがあり、そのサンプルに不具合があった。
それをそのまま流用しているケースが多々あり、マイクロソフトセミナーでも「こういう対応をお願いします」という話をしていた。

バグを人のせいにする

ベテランでも初心者でもある。
自分のところは大丈夫と誰もが思う。
ベテランの場合はさらに厄介なのが自分の非をなかなか認めない。

テスターさんからツッコミがあったら修正

とりあえず実装してみてテスターさんからツッコミがあったら修正。
最近はプログラマだけでなく、プランナー側でもよくあるケース。
メッセージやテキストなどの文言であればまだしも(本当はこれも良くはないが)、ヘタをするとフローまで変更する可能性まで出てくる場合がある。

テストの段階では仕様も含めてFixしている必要がある。

仕様通りに実装すればよいというものではない。
なぜなら完璧な仕様書は存在しないのだから。

こんな心構えで実装しているから大量のバグが出ている!(かも?)

  • とりあえず動いている大丈夫

    「とりあえず動いている」は「動いていない」と思うようにする。

  • サンプルプログラムを流用

    サンプルプログラムはコピーしない。
    参考にしても良いが自分でちゃんと咀嚼し、理解して実装すること。

  • バグを人のせいにする

    自分の不具合は素直に認めよう。

  • テスターさんからのツッコミがあったら修正

    実装してみて問題があるところや改善点があるのであれば早くフィードバックしよう。

7.「たち」の悪いバグ

メモリ、リソース関連のバグ

メモリリークやメモリ破壊、エージングバグはマスター直前になって現れる「たち」の悪いバグ。

再現性が低く、原因を特定するのがものすごく困難な厄介なバグ。
関連している人全員が疑われるために出してはいけない。

が、こういった不具合こそ真の不具合である。

アクションゲームの事例

組み合わせのワーストのチェックが抜けていた。
マスター提出後に特定のキャラクタ、装備、ミッションでメモリ不足が起きた。
各キャラクタ、装備でのメモリ使用量のリスト、各フィールドのメモリ使用量、各ミッション開始時のメモリ使用量のリストを出力してテスターに連絡はしていてもこういったチェックが抜けてしまうケースがある。

60時間後に出た不具合

MMOのプロジェクト。
クライアントにもASSERTと同様の処理を仕込んでおき、エラーが出たらその時のログをファイルに出力してユーザーに報告してもらっていた。
大概は「起動できません」といった内容の報告で低スペックによるドライバのエラーなどによるものが大半を占めていた。
もちろん有益な情報のフィードバックもある。
あるときの報告のエラーで普段とは違うエラーが来ていた。
自前のwidgetの実装で、そのwidgetをゲーム側で用意したHANDLEで管理していた。
そのHANDLEが枯渇してASSERTに引っかかっていた。
調べてみるとアイテムウィンドウの詳細ウィンドウの中のタブを切り替えるとウィンドウのHANDLEがリークしていた。

デバッグ機能でウィンドウのHANDLE数の表示を追加し、一定数を超えたらゲーム中にワーニングを表示するようにした。
また、シーンの遷移(といってもMMOなのでタイトル画面に戻る時)にリークチェックを挟むようにした。

メモリ破壊の調査事例

メモリ、リソースの使用量は常に把握しておく必要がある!

他人のバグ

他人のバグも厄介。
自分のところにきたバグのチケットが調べてみたら他人のバグによるものだった。

MMOの事例

MMOのプロジェクトで運営が開始されてからクライアント側でファイル読み込み中に読み込みに失敗する不具合報告があった。
クライアント側と疑って、読み込みのところのエラーチェックを強化したり、データの整合性チェックする機能を入れたりしたが原因が特定できずにいた。
あるときランチャの機能更新があったためエラーチェックを強化したところファイルのダウンロード処理でエラー処理が正しく行われていなかったことがわかった。
これによりデータの一部が不正な状態になっていたためにクライアント側で該当のファイルを読みに行った時にエラーになっていた。

自分(当事者)が出したバグが他の人たちに影響を与えていることは確か!

8.自分のところでバグを「出さない」

メインプログラマだと...

  • メインプログラマになると自分のバグより他人のバグに関わる方が多くなる
    • 不具合報告のチケットの割り振り
    • 良くわからないバグは担当じゃなくても調査しなければならない

不具合のチケットの割り振りも意外に時間がかかる。
プログラム以外の不具合であっても担当が不明な場合はメインプログラマにチケットが回ってくる。
内容を精査し、原因、対応方法なども追記してチケットを担当に割り振る。

再現性が低いものは考えられるケースや可能性も検討し、他の人の不具合であっても継続的に調査しなければならない。

正直、自分のバグに付き合っている時間が無い。

どういった「心掛け」が必要?

  • 自分のところではバグは出さない!
  • 自分のバグは自分のところで完結させる!
  • バグを出してもすぐに特定できるようにする!
エラーチェックを強固に!

当たり前のことだがエラーをエラーとして処理することが重要!
ASSERTをとにかく仕込むようにする。
値をセットする場合は有効な範囲かどうかチェックする。
配列にアクセスする場合は要素の範囲をチェックする。
引数や返却値で受け取ったポインタのチェック。
ロジック的に通らないような場所にもASSERTを仕込む。
switch文でdefaultが不定であればASSERTにする。

void Chara::SetHP(int hp)
{
    if(!(hp >= 0 && hp <= MaxHP()))
    {
        // 範囲外であればASSERT.
        // 不正な値をセットしないようにreturnで抜けておく.
        ASSERT(false);
        return;
    }

    HP = hp;
}
void hoge(const Chara *chara)
{
    // 引数で受け取ったポインタをチェック.
    // そもそも参照渡しにすべきという話もあるが、ポインタで受け取った場合はエラーチェックを仕込む.
    // リリースビルドでもハングしないようにnullだったらreturnさせておく.
    if(!chara)
    {
        ASSERT(false);
        return;
    }

    chara->SetState(State::Attack);
}
void hoge()
{
    const Chara* chara = GetChara();

    // ここでのcharaの取得でnullは想定されていないのであればASSERT.
    if(!chara)
    {
        ASSERT(false);
        return;
    }

    chara->Damage(10);
}

以下の様な実装だとRESULT_OK,RESULT_ERROR以外の処理が想定されていない。

void hoge()
{
    result = GetResult();

    switch(result)
    {
        case RESULT_OK:
            break;
        case RESULT_ERROR:
            break;
    }
}

関数の仕様が変わって返却値が変更されたとしても最初の想定から外れた値が返ってきてもエラーを拾える。
SDKやライブラリなども更新されると挙動が変更される可能性がある。
続編や移植などの際はこういったところのエラー処理も強化しておく方が良い。

void hoge()
{
    result = GetResult();

    switch(result)
    {
        case RESULT_OK:
            break;
        case RESULT_ERROR:
            break;
        default:
            ASSERT(false,"Invalid result:[%d]",result);
            break;
    }

最近では続編や移植で追加、拡張が行われてもエラーチェックを仕込んでおかないと動作してしまう可能性がある。
そのようなケースも含め早期にエラーを検出できるので、パフォーマンスに影響がない限りはとにかくASSERTを仕込むようにする。

メンバ変数は全て初期化!

リリース版のエージングで起きる不具合で、まれに挙動がおかしくなったり、場合によってはハングする不具合を引き起こす。
最近では通信、オンラインなどのタイトルも増えているため、同期エラーの原因にもなる。

普段から意識して初期化しておかないと、最終的にはCoverityなどの静的解析ツールのやっかいになる。

テストケースを考える!

自分の実装確認という意味でも最低限のテストケースは考えておく。
エンバグを防ぐ意味でも考えておき、実装、修正などを行った後で実装のチェックをする。
過去のプロジェクトでは実装者がテスト用のチェックリストを作成し、実装者自らチェックしてからテストに回すということもあった。

保守性は重要!

そうは言ってもやっぱり保守性は重要。
プロジェクトでも他の人のソースも読まなければいけなくなし、続編や移植などで再利用される可能性もある。
なるべくシンプルで読みやすいコーディングを心がける。

ライブラリ、コンパイラすら疑え!

ライブラリやコンパイラが引き起こす不具合もある。
この場合はきちんと原因を特定するまで調査する必要がある。

コンパイラの事例

とあるコンパイラで64bitの比較演算子に不具合があった。

移植案件でとりあえず動作はしていたが、なぜか特定の挙動だけ呼ばれないという報告が来た。
調べてみると以下のようなコードでif文がtrueにならないことが分かった。
defineによる定数と64bitの変数の比較演算子が正しくないようだ。

#define ITEM_MAX (64)

func()
{
    int64 itemNum = 10;

    if(itemNum < ITEM_MAX)
    {
        // trueにならない!?
    }
}

これを64bitの変数同士での比較や、defineの定数をキャストなどして動作することを確認。

コンパイラのメーカーに問い合わせたら「コンパイラのバグです」とのこと。
こんなことが、と思うかもしれないが実際にあった話だ。

報告しないでゲーム側でこの箇所だけ修正してしまったら、どこか他の場所で同じような不具合が出ても気が付かないままになっていたかもしれない。

ライブラリの事例

とある携帯機で通信プレイ中にスリープモードに移行した際にハングアップするとの報告が来た。
頻度はかなり低く、1日のテストプレイで回しても1、2回程度。

ライブラリに渡すパラメータを直前でチェックしたり、ライブラリが返す返却値も全部チェックし、適切に処理するようにしても改善されず稀に不具合が発生した。

原因が分からないので問い合わせをしたら不具合が発生したらダンプファイルを送って欲しいとの事。
マスター直前だったため土日かけてテストをし、ようやく再現したのでダンプファイルを送ったところ、後日「ライブラリの不具合です。次回のアップデートで修正しますので今回の不具合は問題ありません」とのことだった。

9.他の人のところでバグを「出させない」

ゲーム開発では多くの人が作成に関わっている。
大規模なプロジェクトになると50人とか100人、もっと多いプロジェクトだってある。
関わった人の分だけバグが出る。
プランナー、レベルデザイン、アーティスト、サウンド、こういったメンバーも当然バグを出す。

当然、その分プログラマの作業も増える。

最後に不具合の原因を突き止めて解消できるのはプログラマのみ。
いかにバグを出させないようにするかが重要。

プログラマによるバグ

パラメータ、スクリプトなどのリソース化に伴い非プログラマのバグも増えてきている。
この様な場合、エラーを早い段階でエラーにする必要がある。

パラメータをExcelで管理し、CSVでエクスポートしてデータテーブルとしてゲーム側で読み込ませている。
プランナーがパラメータの上限が100にも関わらず110と入力してしまった。
さらにゲーム中はカスタマイズなどでこのパラメータに係数がかかり、実際のパラメータは補正されてしまう。

エラーチェックが無い場合

テストプレイからの報告で「なんかダメージの値がおかしい気がします」。
何度も繰り返し検証を行い、統計を取って不具合かもしれないという不具合報告。
プログラマが不具合報告を受けてキャラクタに設定されているパラメータの値を確認する。
範囲外の値が設定されているのでプランナーに指示をしてバグチケットを回す。
プランナーは修正してチケットを戻す。
テスターは修正されたビルドで繰り返しテストを行い、問題ないことを確認してチケットを終了にする。

パラメータの設定なのに問題が起こるところまでが遠い。

エラーチェックがある場合

プログラマがキャラクタのパラメータにセットする段階でエラーチェックを仕込んでいたら...

ASSERT:Invalid attack param:[110] chara_status.cpp line:[253]

不正なパラメータがセットされた段階でエラーにできる。

データテーブル(CSV)を読み込んだタイミングでエラーチェックしていたら...
起動時のパラメータテーブルの読み込みのタイミングで整合性チェックをしてエラーにできる。

起動直後にエラーにできる。

Excelからエクスポートするタイミングでエラーチェックをしていたら...
プランナーが入力して、エクスポートしたタイミングでエラーにできる。

データ入力時にエラーにできる。

つまり、早い段階でエラーチェックができればゲームを起動しなくてもバグを防ぐことができることになる。
不具合の責任の所在もはっきりする。

テストプレイの繰り返しのチェックも、プログラマの確認も不要になる。

開発当初から防げるバグも...

開発当初はリソースが無かったり、パラメータが仮であっても開発の効率化という名目でとりあえず動作させてしまう。
そのまま開発を続けていると結局終盤やテストプレイ前にそれらを潰していく必要が出てくる。

リソースの不備やパラメータの設定ミスをワーニングとして分かりやすく画面に表示する。

WARNING:Effect_Fireを設定しているノード名'Eff_1'が存在しません。effect.csvの設定を確認してください。

ただし、たとえ画面に表示しても非プログラマの担当者は指摘をされないと修正はしない。
本来であればエラーなのでASSERTで停止させるのが望ましい。

また、テストプレイの段階で仕様バグが出るとフローの見直しであったり、それに関連するグラフィックリソースの手直し、
再実装など修正の範囲が大きくなることがある。

仕様バグを早めに問題化、表面化させるために1ヶ月単位などのイテレーション開発が効果がある。
1ヶ月ごとに成果物を確認し、仕様面での不具合やブラシュアップを早めることで仕様バグを減らすことができる。

自動化の重要性

毎日動作するものをリリースする。

さらに自動チェック、エージングの環境も整える。
ビルドが通るだけでなく、起動チェック、例えば先ほどの起動時のパラメータのチェックが実装されていればこの時点でパラメータの不備が検出できる。
人力のテストプレイにも限界はあるので自動化の環境は早めに準備する方が良い。

また、エージング環境が整えば人が作業しない時間を有効に活用できる。
例えば、アクションゲームでバトルシーンをキャラクタの組み合わせを変更しながらAIでプレイさせ、ミッションを繰り返しプレイするエージング環境を用意したとする。
退社する時間から出社するまでの時間、例えば22時から9時までエージングをかければ11時間×機材分のテストプレイが行える。

もちろん人の手でしか出せないバグもあるだろうがエージングで発見できるバグも存在するし、逆に不具合が出なければ正常に動作している実績にもなる。

これらを問題視して環境の構築、改善ができるのはプログラマのみ。

10.最後に

  • バグを完全に出さなくすることは不可能...
  • でも、バグを減らすことは可能!
  • 「バグは出て当たり前」という考えは捨てよう!
  • バグは「潰す」のではなく「出さない」ことに力を注ぐべき!
  • 個人が少し意識するだけでも格段に「バグ」は減るハズ

商品のクオリティを向上させるためにも、開発終盤のデバッグのストレスを軽減するためにも「心掛ける」ようにしよう。