top / 1 / 2 / 3 / 4 / ex1 / 5 / 6 / ex2 / 7 / 8 / 9 / 10 / 11 / ex3 / 12 / 13 / 14 /..../ 関数 / 覚え書き / 倉庫
第9回 【 デバッグ方法 】

バグの見つけ方を説明するに当たり、幾つかの予備知識が必要になります。
まず前提として、バグには大きく分けて2種類あります。
それは「クラッシュするバグ」「そうでないバグ」です。

「クラッシュするバグ」は遭遇するとゲームが強制終了するのでゲーム進行は不可能、バグを取らなければ何も出来ないと言うような状態に陥ります。
「そうでないバグ」とは、例えば重量軽減MODなんかで良く話題になる「MODで改造した部分が反映されていない」とか、武器のステータスがおかしいとか、明らかにバグではあるものの、ゲームが続行可能なバグの事です。

発生した時の精神的ダメージの大きさでは前者の方が大きい気がしますが、深刻さでは後者の方が大きいのが特徴です。
何故ならば、前者であるならばクラッシュする事により確実にバグがあることが判明していますし、バグ報告画面によりある程度情報を得る事が出来ます。
しかし、後者の場合、そもそもバグが存在しているのか気づく事が非常に困難です。「MODを導入したにも関わらず期待していた効果が発揮されない」などと言う時に結果的にバグがあることを知る事ができるのみです。
実際、後者の方はこれと言ったデバッグの方法を示す事ができません。現象から推察して「多分、あの辺りが間違ってるのかなー」と絞り込む必要があるのです。

よって、ここでは「クラッシュするバグ」のデバッグ方法の基本を説明しようと思います。


何はともあれ、クラッシュしてくれないと説明のしようもないので、クラッシュさせて見ます。
前回のスクリプトをちょいと弄って、わざと間違えた命令を書いてみましょう。
弄るべき部分は、ファイル「ngc_mod.script」、この中には前回作った以下のスクリプトがありますね。
function ngclordcheck() -- If flug of "game start" is TRUE? if (xr_logic.pstor_retrieve(db.actor, "x_first_run", true) == true) then -- ADD Items alife():create("wpn_rpg7", db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id(), db.actor:id()) alife():create("ammo_og-7b", db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id(), db.actor:id()) alife():create("ammo_og-7b", db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id(), db.actor:id()) alife():create("ammo_og-7b", db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id(), db.actor:id()) alife():create("ammo_og-7b", db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id(), db.actor:id()) alife():create("ammo_og-7b", db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id(), db.actor:id()) -- Lowers flug of "game start" xr_logic.pstor_store(db.actor, "x_first_run",false) end end
この中で、ロケットランチャーを出現させる命令があります。
alife():create("wpn_rpg7", db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id(), db.actor:id())
これですね。
これの先頭の「alife」の記述をわざと間違えて「blife」に書き換えてみましょう。
もちろん「blife」なんて命令は存在しません。
こんな命令を与えると、ゲームはどんな動きをするのでしょうか?
では実行してみます。

ニューゲームで始めると、「CLIENT: Synchronizing...」の辺りでクラッシュすると思います。
コンティニューすると上手く動く場合がありますが、これはこの間違いを挿入した箇所は前回説明した条件分岐により、「ゲーム開始時のみ」動くようにしてあるからです。
コンティニュー時には条件が成立せずに、この間違ったコードを読まずにゲームが動いてしまうと言うわけです。

めでたく(?)クラッシュしたので、早速クラッシュ内容を見てみましょう。
とりあえず、クラッシュした時にやる事。
こんな画面が出ますよね。

bug00

たまに出ない事もありますが、「たまたま調子が悪くて表示されなかっただけ」な場合が多いので、もう一度ゲームを起動して、同じ要因でクラッシュさせる事を繰り返せば、2、3回のうちには出るはずです。
出たら、「More...」のボタンを押してください。

bug01

何だか小難しそうなのが出てきましたね。
ここでは「Save...」ボタンを押してください。保存ダイアログが出るので、好きな場所に保存しておきましょう。
これでこの画面は終了して構いません。「Close」を押しちゃってください。

こんなファイルが保存されているはずです。zip形式ですね。

XRayEngine_error_report_YYMMDD-HHMMSS.zip

YYMMDDの部分には日付が、HHMMSSの部分には時刻が入ってると思います。
では、これを解凍してみると・・・。

crashdump.dmp
errorlog.log

こんなのがありますね。
上の「crashdump.dmp」は、恐らくシングルプレイヤーSDK(開発環境)があれば見れるのかも知れませんが、少なくとも今は閲覧出来ません。
バイナリエディタ(データを16進数で直接読めるエディタ)などで閲覧出来たとしても、それが何を意味しているのかの情報が何ひとつ無いので、ここでは使わない事にします。

ここで使うのは下のファイル、「errorlog.log」です。
これはテキストエディタで見る事が出来ます。
早速中を開いて見ましょう。

中の情報の大半は上記と同じ、それが何を意味するのかの情報ソースが提供されないと意味を持たないものです。
が、良く見るとほんの少しですが、意味の判る部分があります。
12行目辺りからこんな文章がありますね。
User Message: ---------------------------------------- Expression : fatal error Function : CScriptEngine::lua_error File : E:\stalker\sources\trunk\xr_3da\xrGame\script_engine.cpp Line : 73 Description : Arguments : LUA error: ... shadow of chernobyl\gamedata\scripts\ngc_mod.script:24: attempt to call global 'blife' (a nil value)
ここで見るべきところは一番下の項目、「Arguments」の文章です。
この一文が現時点で我々に与えられた唯一のバグの手がかりと言っても過言ではありません。
LUA error: ... shadow of chernobyl\gamedata\scripts\ngc_mod.script:24: attempt to call global 'blife' (a nil value)
エラー名称に続いて、なにやら書いてありますね。
shadow of chernobyl\gamedata\scripts\ngc_mod.script:24:
これはバグの位置です。
フォルダ「shadow of chernobyl\gamedata\scripts\」、ファイル「ngc_mod.script」の24行目でクラッシュしたと言っているわけですね。
attempt to call global 'blife' (a nil value)
これがクラッシュの直接的な原因です。
直訳すると、「中身が空っぽ(a nil value)の共通機能“blife”を呼び出そうと試みました」

出ました。「blife」です。
「中身が空っぽ(a nil value)」とか、プログラム開発やって無い人には意味不明ですが、まあ要するに「blifeなんて存在しねえYO!」と言われてると思って問題ありません。
ここで、先ほどわざと間違えて記述した命令「blife」が原因である事が判明すると言うわけです。


バグの位置と原因が判ったので(今回は最初から判ってたけど)、早速それを修正しましょう。
修正できましたか?一応、これでまともに動くようになった事を確認した方が良いかもしれませんね。

今回バグの手がかりにした項目「Arguments」の内容は、
LUA error: ... shadow of chernobyl\gamedata\scripts\ngc_mod.script:24: attempt to call global 'blife' (a nil value)
これですが、この部分には色々なパターンがあるようです。
例えば、追加した武器「wpn_rpg7」を存在しない武器「wpn_rpg77」とかに書き換えて実行すると、やはりクラッシュしますが、その時の「Arguments」の文章はこんなふうになります。
Can't open section 'wpn_rpg77'
これはやはり「wpn_rpg77なんて存在しねえYO!」と言う意味ですが、この場合は問題のファイルや行番号を教えてもらえません。
まあ、「wpn_rpg77」と言う手がかりはあるので、それで検索かけて探せば良いと言う事ですね。
このタイプのいわゆる「機能やアイテムの名前を間違えた」事が原因のバグの場合には殆どの場合、何が決定的にまずいのかを教えてくれます。
他にもパターンはあると思いますが、「blife」「wpn_rpg77」などのキーワードがあれば、そう苦労せずにバグ位置を特定できるでしょう。


これがデバッグの基本ですが、もう一つ、スクリプトを弄ると頻繁にお目にかかるバグがあります。
こちらは少々厄介、と言うか面倒臭いです。

先ほどわざと間違えて記述したファンクションを、もう一度わざと間違えてみましょう。
今度は最初のif条件式を弄ります。
if (xr_logic.pstor_retrieve(db.actor, "x_first_run", true) == true) then
これが本来のif条件式ですね。これを・・・。
if xr_logic.pstor_retrieve(db.actor, "x_first_run", true) == tru) then
先頭のカッコだけを消してみました。
カッコは“(”“)”が対になって始めて意味を持つものですが、この文では始まりのカッコが無いのに終わりのカッコだけが存在すると言う状態になっていますね?
これで実行してみましょう。
やはりクラッシュするので、「Arguments」のエラー内容を見てみると・・・。
LUA error: ...ow of chernobyl\gamedata\scripts\bind_stalker.script:57: attempt to index global 'ngc_mod' (a nil value)
先ほどと様子が違いますね。
エラー箇所はファイル「ngc_mod.script」では無く「bind_stalker.script」に、しかもエラー内容は「ngc_modなんて存在しねえYO!」になっています。
どう言うことでしょうか?
これはつまり、ファイル「ngc_mod.script」がそもそもファンクションとして機能していないと言う事なのです。

知っての通りファンクションを記述する際には文法を守る必要がありますが、その文法に致命的な誤りがある場合、そもそもそのファイルをファンクションとして認めてもらえないのです。
この場合、「そのファイルのどこかに致命的な文法ミスがあるよ」と言う曖昧な形でしか情報を得る事ができません。
行数や単語によってバグ位置を知る事ができないのです。
よって、どこかにカッコ抜けや文法ミスが無いか、穴が開くほど探すしかありません。
テキストエディタによってはカーソルをカッコに合わせると、対になるカッコを強調表示してくれたりする機能を持っている物もあるので、そう言った機能があったらフル活用するとこの作業は楽になる事があります。

しかし厄介な事に文法ミスは“(”だけではないのです。
要するに、「ファンクションとして文法が間違っていたらアウト」なので、if条件式の「then」を書き忘れていたり、「if」と対になる「end」が抜けていたり、原因は様々であり、カッコの場合のようにテキストエディタの機能に手伝ってもらえる例は、むしろ少ないと言えます。

こうなると、むしろ重要なのは「いかにバグを取るか」ではなく、「いかにバグを見つけやすい形でスクリプトを書くか」になってきますね。
例えば、前回に書いたif条件式の記述ですが、
function ニャギ茶ロードチェック if (ゲーム開始フラグが立ってる?) then イベントリにロケットランチャを追加 イベントリにロケット弾を追加x5 end end
これのインデントを一切書かずに、
function ニャギ茶ロードチェック if (ゲーム開始フラグが立ってる?) then イベントリにロケットランチャを追加 イベントリにロケット弾を追加x5 end end
こう記述しても文法的には問題ありません。
しかし、仮に「if」と対になる「end」を書き忘れた場合、
function ニャギ茶ロードチェック if (ゲーム開始フラグが立ってる?) then イベントリにロケットランチャを追加 イベントリにロケット弾を追加x5 end
こうなってるのと、
function ニャギ茶ロードチェック if (ゲーム開始フラグが立ってる?) then イベントリにロケットランチャを追加 イベントリにロケット弾を追加x5 end
こうなってるのでは、明らかに前者の方が「end」の記述忘れを発見しやすいですね。
このように、予めデバッグしやすい形でスクリプトを記述するのも、デバッグのテクニックの一つと言えるのです。
つまりデバッグとは、スクリプトを作っている時に既に始まっていると言っても過言ではないのです。
何だか上手い事言って綺麗にまとめた気がしますが、こんなのは実はプログラム開発では一番最初に覚える基本中の基本なんです。偉そうに言うこっちゃありません。


とりあえず、小規模なMODを作っていて目にするのはこんなところでしょうか?
このブログで紹介している程度のMODでは関わり無いと思いますが、大規模MODではたまに、フォルダ「gamedata\config\」内のファイル「localization.ltx」でエラーが発生する事があります。
現象としては「そもそも起動しない」「S.T.A.L.K.E.Rロゴ表示後、そのままクラッシュ」などと言う形で発生して精神的ダメージを与えていくものが多いです。
ファイル「localization.ltx」では使用言語や読み込むべきXMLファイル一覧などを指定しているので、大方は「XMLファイル一覧と実際のフォルダ内のXMLファイルが一致していない」などが原因だとは思いますが、「へー。こう言うパターンもあるんだー」程度にでも覚えておくと、何かの時に慌てずに済むかもしれません。

とにかく重要なのは、クラッシュしたらまずはエラー内容を見る事。
他人のMODでクラッシュした時にも一応目を通すようにしておくと、色々と得るものもあると思います。
それで解決する場合もあるし、解決しない場合もあります。
例えば、メモリ関係でどうしようもなくクラッシュが起きる事もあります。その時にはそもそもログ内に「User Message:」の記述がありません。
そうであっても、「自分のMODが原因である可能性は低いな」と言う類の情報として知る事が出来るのです。

クラッシュしても、溜息と共に「Close」を押す前に、やれる事はやっておく癖を付けたいものです。


top / 1 / 2 / 3 / 4 / ex1 / 5 / 6 / ex2 / 7 / 8 / 9 / 10 / 11 / ex3 / 12 / 13 / 14 /..../ 関数 / 覚え書き / 倉庫
ニャギ茶★SPOT --工作部屋--