プログラムを記述していると、自分でも想定していない動作バグが発生する事があります。こういった「バグ」の修正を行うアプローチについて今回は書いていこうと思います。
個人的には「Unityが出来る(中級者以上)」は「思った通りに動かない物に出会った時に、自力で解決出来るかどうか」…かなと思っています。
ので、初心者を脱出したい人に向けて本稿を書いていましたが、うまく纏まらなかったので余計分かりにくくなっているかもしれません。ぐぬぬ。
バグは何処から来るのか
プログラムは「だいたい」書いた通りにしか動きません。しかし人間は不完全なので何時も過ちを繰り返します。間違ったプログラムは思い通りに動きません。
今回はこの「思った通りに動かない」を「バグ」と定義します。
さて、思い通りに動かない原因は常にプログラムが犯人とは限りません。バグは大きく分けて4種類の真犯人が居ます。
全てのシステム(OSやドライバのソースコードやコンパイラやハードウェアの挙動)を100%完璧に把握もしくはカスタマーの環境を自由に出来れるのであればプログラムかデータか入力の3種類に絞れますが、現実的にそれが出来る環境はゲーム開発環境では残ってないと思うので、この4種類のうちどれかと思われます。
バグの犯人たち
バグの内情をもう少し詳しく紹介します。
まず「プログラムやアルゴリズムが悪い」場合、話は簡単です。書いたコードが悪く、コードを書いた奴が悪いです。プログラムの挙動を確認し、自分の想定と異なる挙動を発見し修正します。
例えば、キャラクターの向きや条件分岐の条件、データが正しく反映されているか、データを与えたら期待した挙動を行うか問うか、呼び出し順番は正しいか等々、様々なデバッグ機能を駆使し「何処まで正常か、何処から異常か」を見極めます。
「使用するデータが悪い」場合、このデータとはデータベースから取得するデータであり、WebからダウンロードするJSON等のメタデータであり、Unityにインポートしたモデルであり、アニメーションであり、画像であり、オーディオであり、シナリオを記述したファイルです。また「データが無い(取得できない)」ケースも考えられます。
この問題が発生した場合は「確実に問題無いデータを用意」するのが一番手っ取り早いです。例えば公式が用意しているデータであったり、必要最低限まで絞り込んだデータ等です。なおこのデータバグはシーンにおける設定ミスも該当します。
「入力が悪い」ケースは、入力した時に期待したレスポンスが無いケースです。接続不良だったり、繋いだ先が間違っていたり、そもそも操作に誤りがある等です。
この項目は最終的にデータかその他、もしくはオッチョコチョイに統合されます。
他の項目、OSやUnity・ハードウェア・ネットワーク・フレームワーク等々です。自分の操作出来る内容全てが確実に正常な場合、これらの項目が問題である可能性があります。
今回はユーザー側でも割と何とかなる「プログラムが悪い場合」と「データが悪い場合」について色々と書いていきます。
問題の切り分け
なにはともあれ最初は問題の切り分けです。
例えばトラブルシューターの中に一人コミー(共産主義者)が紛れ込んだ時どうすれば良いのでしょう。答えは簡単、全員殺せばコミーも殺せます。
とはいきません。疑わしきものを全部破棄して作り直したら、物凄い手間です。
なので、逆に「疑わしくない・正常に動作している物をバグっているパーツの一覧から取り除いていく」事を行います。
作り直すもの・調査するものの範囲が小さければ小さいほど、問題の発見と解決は簡単になります。
Unityでこれを行う場合は、「どの実機のみで再現するのか、PCでも再現するのか」「オブジェクトやコンポーネントを減らした状態で再現できるのか」「再現できるパターンにどんな要素があるのか」「単純なシーンで再現できるか」「異なるPCやプロジェクトでも再現するか」といった推理材料を揃えていきます。
これで出来た「動かない環境」「動く環境」を比較して条件を抽出、推理材料が揃ったら、次は実際に仮説を立てて検証し、犯人をつるし上げます。
仮説と検証
バグを探すうえで大事なのは仮説と検証だと個人的には思っています。
このバグが発生している箇所や症状を確認し、仮説を立てて、実際にどうなっているのかを確認します。
違ったら…おめでとうございます!そのアプローチは正常に動作する事が証明されました。
では実際に、「ボールにAddForceで力を加えても跳ねない」といったバグが発生した場合、どのような可能性が考えられるのか考えてみます。
- ボールにrigidbodyが無い
- ボールはボールでも⑨に力を加えていた
- ボールのrigidboyにiskinematicが付いてる
- ボールにstaticが入ってる
- ボールに与える力より重力の方が高い
- ボールの質量が高すぎて動かない
- ボールの座標を他のコンポーネントが固定している
- ボールに与えた力が他のコンポーネントで0にされている
- ボールの動きが速すぎて跳ねたことに気づかない
- AddForceを行う操作が動作しなかった
- スクリプト書いてるファイル間違ってた
- 実は画面が固まってた
等々。ありえる可能性を上げて、Try And Errorで潰していきます。
データから来るバグを探す
データのバグは上に書いた通り、取得したデータに何等かの問題があり正常に動作しないバグです。逆を言えばデータバグとは「正しいデータを設定すれば正常に動く」ものです。
データのバグは、大まかに3種類の可能性があります。
- データが見つからない
- データが壊れている
- データが間違っている
このうち上二つはそもそも動かないケースが多く、一番下は何となく動いてしまうが間違っている事が多いです。
データが見つからない。
サーバーが機能していなかったり、URL間違っていたり、ネットに繋がらなくてアクセス出来なかったり、ファイルパス間違ってたり、アクセス権限が無くてアクセスできない等々、様々な要因が考えられます。
またコンポーネントの参照が見つからない(missingやnone)といったケースもあり得ます。例えばUnityではアセットのGUIDを誤って更新してしまった場合、GUIDを頼りに参照していたアセットが見つからなくなり、問題が発生するかもしれません。
FileNotFoundExceptionやNullReferenceException等が発生したら6割がたこれです。
ちなみにエディタのmissingの場合は、これを使って解決すると便利かもしれません。
データが間違っている
データが間違っているケース、つまり「データフォーマットは正しいが取得するデータが間違っている」ケースもあり得ます。
例えばモーションを適応するデータが間違っていたり、読み込むシナリオや音声が間違っているケースや、そもそもデータに間違った数値が居れてあるケース、データの設定が間違っている等です。
例えば以前あった事としては、読むシナリオデータが間違っており何時までたってもシナリオのコマンドが実行できない…といった事がありました。
ずっと試行錯誤した末、現在読んでるシナリオにコマンド自体入ってなかったと気づいた時のガッカリといったらそれはもう。
特に外部データやメタデータを使用して開発している場合は、この問題が往々にして発生する事があります。「概ね正常に見えなくもないけど何かおかしい気がする」というケースにおいては、この項目が疑わしいです。その場合は、入力したデータが本当に正しいのかを目視で確認するのが必要になります。
データが壊れている
データがそもそもおかしいケースは「フォーマットが確定してる(フォーマットが確定しているとは言ってない)」といった、複雑で意外と共通化されていないフォーマットを読み込んだ際に起こります。
つまり「読み込めたけど、上手く読めない」「○○なら上手く読めるけど他は上手く読めない」といったケースです。拡張子は同じだけど古いフォーマットで読めないとかもあるかもしれませんし、文字コードって奴もあります。
特に3Dモデルやモーションといったデータは同一フォーマットでも実はモデリングツールによって微妙に内部が異なるという微妙な処にあるため、この問題が引き起こされやすい印象があります。
ただ、これは発生すると割とどうしようもない所ではあるので、データを作り直してもらうか、対応するかの2択しかない気がします。
プログラムのバグを探す
では実際にプログラムのバグを探していきます。
データのバグでは無い場合、プログラムのバグが考えられます。
プログラムのバグをUnityで探す場合、幾つか使える機能があります。
これらの機能は強力で常時使えるケースもありますが、有効なケースは機能ごとに少し異なります。
個人的には「実際の動きと想定する挙動を視覚的に比較したい」「コードの問題発生箇所やその時のデータを確認したい」「頻度の低いエラーを絞り込みたい」「変動する値を確認したい」の4パターンで考えています。
実際の動きと想定する挙動を視覚的に比較する
プログラムを実行した際、考えている場所と実際の向きや範囲が異なるケースがあります。これをデータ的に問題を確認する事は可能ですが、直観的なのは実際に画面に範囲や方向・向き等が表示するような方法です。
壁との接触にRaycastのような線判定を使用している場合を考えます。前方向に判定用に伸ばしている線を表示し、実際に接触しているか確認します。
つまりtransform.positionからtransform.forward方向に伸ばしたらどうなるのか。
上手く判定できません。バグってます。
実際にどのような判定を行っているのかシーン上に表示してみます。
void Update()
{
Debug.DrawRay(transform.position, transform.forward, Color.red);
}
その結果、下の画像のように「奥」に対して線が引かれていたりします。実際はtransform.rightでやるべきだった訳です。
こういった「想定と違う動作」を見つけたり、「実際に範囲内なのかを見つける」といったケースにおいては、グラフィカルに表現する手法は非常に有用です。
この「シーン上にグラフィカルに表現する機能」で便利なのはGizmoやHandle、Debugクラスの3つです。基本的に似たような扱いが出来ますが、少しずつ挙動が異なります。
HandleはEditorの専用機能のため、エディタ拡張を前提とした複雑な描画をサポートします。例えば「シーンビューにボタンを追加する」や「シーンビューにテキストを表示する」等々の柔軟な表現が可能ですが、ゲーム内向けのスクリプトとは分けて記述する必要があります。
GizmoとDebugクラスは、より簡易な描画機能です。
この二つの違いは幾つかありますが、特に大きいのが、GizmoはOnDrawGizmosもしくはOnDrawGizmosSelectedといったmonobehaviourのコールバックから呼ばれる事を想定しており、Debugは何処からでも呼べる点です。しかしDebug発行後1フレームしか反映されないので、実質的にUpdateやOnDrawGizmos等に配置する事が多いです。
もう一つが描画の特性です。Debugはオブジェクトの奥である事を明確にアピールしますが、Hnaldeは余りそういった事を行いません。
コードの問題発生箇所
バグとして一番に思いつくのは「Exception(エラー)が発生する」事だと思います。この手のものは親切にもエラーを出してくれているので解決は割と簡単です。
例えば配列外にアクセスしたり、nullの変数からメソッドを呼ぶようなコードを書いた場合、実行時にExceptionとしてコンソールに表示してくれます。
あとはエラーメッセージをダブルクリックしてエラー行へジャンプするので、そこからプログラムを逆算して読んでいけば問題の発生原因が見つけられます。
ちなみに、ビルドが通らないのは「コンパイラちゃんがソースコードを理解できない」だけです。ちゃんとコンパイラちゃんが理解出来るように書き直してあげれば通ります。
期待の動きと異なる動きをした要因を探す
面倒くさいのはエラーが発生しないバグです。つまりスクリプトとしての動作は正常ですがバグっているケースです。
こういったケースで追跡対象が絞れているならば、スクリプトデバッガを活用します。
エラーの出る所で停止し、watchやlocalsを駆使して「フィールド変数」や「ローカル変数」を確認します。特にスタックトレースを利用してメソッド呼び出しを巻き戻ると、呼び出し元の内容も確認できるので、実際どのような値が入っているのかを確認することが出来ます。
あとは「何故その値が入ったのか」を推測し、修正を施します。
頻度の低いエラーを絞り込みたい
バグとは必ず1回で出る物ではありません。物によっては「1日に1件でる可能性がある」といったケースも存在します。
また「大量のデータの内どれかがバグを発症するケース」や「タイミングによってバグが発生したりしなかったりするケース」等も存在します。
膨大な要素に対して一々スクリプトデバッガを実行していては、年が暮れますし、スクリプトデバッガを逐次挟む事でバグが発生しなくなるケースもあります。
そういったケースではコンソールに表示されるログやAssertを使用し、動作結果をログとして出力・後で検証するといったアプローチが必要になります。
Assert
Assertはデータが指定のデータ以外だった場合エラーログを出力する機能です。これはエディタやDebugBuild以外のケースでは無効化されます。
この機能は戻り値の保証や、戻り値が何かおかしい等、広い範囲でコードを確認する場合に便利です。
例えば戻り値が「hoge」となるはずのメソッドの戻り値がHoge以外を返している可能性がある場合、以下の様な形でチェックします。
string hoge = GetHoge();
UnityEngine.Assertions.Assert.AreEqual
これでGetHogeが"hoge"以外の値を返した場合のみ、エラーログが表示されます。他にもtrueやfalse、nullかnullではないか等の判別もこの処理で行えます。
あとはこれを怪しいデータを返しているコードの前後に仕込み、想定外の動作を行う挙動を探します。
データの正常なデータが分からない場合や、少し条件付けが複雑な場合は、とりあえずDebug.Logでログを出力してしまうアプローチもありです。ずらっとデータを確認した際、違和感のあるログを探したり、処理の呼び出し頻度を確認したりします。
例えばデバッグログを出力し、キャラクター名とキャラクターの持つアイテムの数を表示します。多くのキャラクターの場合は「アイテムを保持していない事が正常」ですが、一部の本来持っていないとおかしいキャラクターがアイテムを持っていないケースがあるかもしれません。
ここである程度の法則性を見たら、上のAssertでより絞り込みを行い、最後にスクリプトデバッガで犯人を突き止めます。
変動する値を確認したい場合
スクリプトの動作を検証するにあたり、値の変動を見る事があります。例えば「値が変化するタイミングがおかしい」等を確認したいケースです。
値は凄い勢いで変動するので、スクリプトデバッガでは追い切れません。また、コンソールログに表示すると物凄い勢いでログを流してくれます。
こういった凄い変化する値を見たい場合、Inspectorの内容を確認する手もあります。
この項目にはシリアライズ可能なパラメータが全て表示されます。privateなパラメータはdebugモードに変更して確認します。
Inspectorでは表示できないMonobehaviourを継承していないクラスの値等もエディタ拡張(EditorWindow等)を使えば見る事が出来たりします。
但し更新頻度が少し少な目です。
実機で確認する場合はOnGUIでパラメータを画面に表示するのが便利です。例えばUIの無いオブジェクトのステータスを表示する等に使用できます。おかしなパラメータはこの項目で確認出来ます。
ただしOnGUIのGUILayoutはコンポーネント内ならば整形してくれますが全体を通すと整形してくれません。なので、全体で共有できるデバッグウィンドウのような物を用意すると色々と便利かもしれません。(むしろ何故公式で無いってレベルで)
なお、いつもの再生ボタンの一番右は1フレームずつ進むので、こういった検証においては結構役立ちます。
感想
要するに、とりあえずバグったら「問題を切り分けて仮説と検証」が自分のよく使用しているアプローチです。
それ以前にバグらせないように作るのが一番ではありますが…にんげんだもの。
あとで手直しする予定。
この記事はUnity Advent Calendar 2015にも載せています。
次はneueccさんです。