BubaDragons NWNスクリプトのデバッギングガイド
概要
NWNのスクリプトのトラブルシューティングは、やりがいがありますし、時間を要します。この作業を能率のいい方法で行うことで、スクリプトのバグを見つけ修正することで生じるフラストレーションを減らすことができます。このドキュメントでは、スクリプトのトラブルシューティングにおける基本的な発見的方法と、いくつかのサンプルシナリオについて説明します。
読者の前提
このドキュメントは、オーロラツールセットに慣れ親しんでいて、スクリプト言語の基本を理解している読者を想定して書かれています。このドキュメントは、スクリプトの基本を教えるというよりはむしろ、スクリプトのエラーの原因を見つけるプロセス、そして、エラーの修正方法についての説明に力を注いでいます。
トラブルシューティングとは?
プログラムのトラブルシューティング、あるいは、デバッギング*とは、プログラムコードのエラー箇所を見つけて、それを修正、または、回避するプロセスをいいます。デバッグとは、問題を発見し、それを隔離して、それを修正することです。スクリプトにエラーが起きた時に行う重要な作業は以下のとおりです。
·問題を明らかにする
·問題箇所を隔離する
·問題を修正する
·修正されているかテストする
悲しいことに、このように簡単に述べることは、それを成し遂げることよりよほど簡単です。以下で、私がNWNのスクリプトコードとモジュールに対して 行うトラブルシューティングの方法について述べます。
基本的なトラブルシューティングのガイドライン
明らかなことを疑う
オーロラツールセットを非難したり、BioWareの開発者を呪ったり、スクリプトのエラーのために憂鬱になることを責める前に、以下のリストから可能性のありそうな原因を調べて下さい。
·(=)と(==)の使い分けをチェックする
·全ての文がセミコロン(;)で終わっているか確認する
· 適切な型、適切な関数が使われているかチェックする
· 割り当てた変数を間違って呼んでないかチェックする(fooとFooは別の変数)
· スクリプトが呼ばれているか確認する
シンプルに見えますが、私はこの方法で数え切れないくらい問題を解決してきました。
エラーを隔離する
時々この作業だけで問題を解決できることもあります。始めに、問題箇所を関数だけに制限し、それからセクションに広げて、最終的に1行のコードにします。
簡単なテストケースを作ります。問題をテストするために十分なオブジェクトだけを含んだ小さなモジュールを作ります。
事実を調べる
知ったかぶりをしていないかチェックします。ツールセットで関数を調べます。実際に動いているスクリプトを見つけて、それを調べます。関数の全てのパラメーターを調べます。
ツールセットとスクリプト言語について少し勉強する
イベントを扱ったプログラミングのドキュメントをいくつか調べます。基礎を理解することが、スクリプトにおける問題を解決する道へと続きます。
スクリプト作成は論理学の練習
動作のアイディアを、それを実行するために一連の命令に変換することは芸術であり、科学でもあります。全てのプログラムは、最初にコンパイルされた時には完璧に動くでしょう。しかし、いつかそうでない時が来ます。
しかし、落胆しないで下さい。この記事を読んでいることや、ツールセットを学ぶことがトラブルシューティングを成し遂げるために必要な材料を示しています。あなたがこの作業を楽しんでやっているか確かめて下さい。そうでなければ、作業をしばし止めて、フラストレーションを笑い飛ばして再び取り組んでみて下さい。
デバッギングツール
まず最初にみなさんが言うことは、「デバッギングツールとは?」ということででしょう。私は、それはBjornがC++のコードを書いたときに使った主に両耳の間にあるもの(脳)と同じ答えます。それは、ネアンデルタール人がマンモスを狩っていた時から備わっていたツールです。
文章を打ち出す
どのようにしてパラメーターの値をチェックすればいいのでしょうか? 簡単です。文字に打ち出して下さい。以下のようにして打ち出します。
SpeakString(“ある文字列“+IntToString(iVariable));
SpeakString(“他の文字列”+FloatToString(fVariable));
SpeakString(“オブジェクトの名前は”+GetTag(oObject));
//等々
他には、以下のようにログファイルに書き出す方法があります。
void PrintString(string sMessage).
ログファイルがあることを知っていましたか? ログファイルはNWNがインストールされているフォルダのLogsフォルダの中にあります。ログファイルはnwclient1.txtという名前のファイルで、どんなテキストエディタでも開けます。(私はこのファイルのショートカットをデスクトップに作成しています。)
スクリプトエディタ
おそらく、最も便利で有益なデバッギングツールは、スクリプトエディタそれ自体でしょう。スクリプトエディタからは、豊富な関数の情報やObject、Effect、Vectorといったオブジェクトタイプの情報を得ることができます。パラメーターリストのタイプを見て、それらがどのように作成され使われているか調べて下さい。
スクリプトエディタには、あなたの友達であるコンパイラが含まれています。コンパイラによって生み出される全てのエラーには、行番号とエラーの概要が含まれています。私が遭遇したいくつかの難解なエラーは以下のとおりです。
·
ERROR: UNKNOWN STATE IN COMPILER
このエラーの原因のほどんどは、括弧のアンマッチによるものである可能性があります。
·
ERROR: NO SEMICOLON AFTER EXPRESSION
このエラーは、最後にセミコロンが付いている行をハイライトすることがあります。これは実際は、その行の上の行にエラーがある場合がしばしばあるためです。
·
ERROR: DECLARATION DOES NOT MATCH PARAMETERS
関数の定義を見てパラメーターリストを再確認する必要があると想定されます。間違ったタイプのパラメーターが使用されている、あるいは、パラメーターが抜けている時にこのエラーが起きます。
· ERROR: PARSING VARIABLE LIST
私のお気に入りのエラーの1つです。なぜなら、とても説明的だからです。このエラーは大抵、関数名の右側部分のスペルが間違っている時に起こります。
コンパイラの一番良い点は、いつもエラーの場所を示してくれる所です。コンパイラが示している行とその直前の行にエラーがないかどうか調べることを決して忘れないで下さい。スクリプトのエラーである限りは、見つける のはとても簡単です。
BioWare メッセージボード
スクリプトフォーラム、カスタムコンテンツフォーラム、ツールセットフォーラムには、あなたが抱いている質問が既に質問され、回答されている可能性が非常に高いです。質問する前にフォーラムを検索する習慣を身につけて下さい。同じ質問を何度も何度もすることは、回線を無駄に使っていることになります。加えて、あなたが非難の対象にもなります。
NWN Lexicon
共同作業で作成された数多くの素晴らしいAPIドキュメントがあります。それから情報を得て、それを利用して、それを愛して、あなたの友達にLexiconの事を教えて下さい。
トラブルシューティングの例
以下は、トラブルシューティングに関する上記の法則を示した実例です。
問題の出発点
私の目標は、特定のタイルの照明をオフにする方法を見つけることでした。そこで、私が最初に行ったことは、「light」をキーワードに含む関数を検索することでした。その結果はもちろん以下のとおりでした。
GetTileMainLight1Color
GetTileMainLight2Color
GetTileSourceLight1Color
GetTileSourceLight2Color
RecomputeStaticLighting
SetTileMainLightColor
SetTileSourceLightColor
それぞれの関数から得られたデータは以下のとおりでした。
// lTileにあるタイルの主照明1の色(TILE_MAIN_LIGHT_COLOR_*)を返します。
//
// - lTile: ベクター値部分はタイルのグリッド位置(x,y)です。
int GetTileMainLight1Color(location lTile)
// lTileにあるタイルの主照明2の色(TILE_MAIN_LIGHT_COLOR_*)を返します。
//
// - lTile: ベクター値部分はタイルのグリッド位置(x,y)です。
//
int GetTileMainLight2Color(location lTile)
// lTileにあるタイルの光源1の色(TILE_MAIN_LIGHT_COLOR_*)を返します。
//
// - lTile: ベクター値部分はタイルのグリッド位置(x,y)です。
//
int
GetTileSourceLight1Color(location lTile)
// lTileにあるタイルの光源2の色(TILE_MAIN_LIGHT_COLOR_*)を返します。
//
// - lTile: ベクター値部分はタイルのグリッド位置(x,y)です。
//
int
GetTileSourceLight2Color(location lTile)
// oArea内の静的な照明を全て再計算します。
// タイルの照明を変えた後、または、照明を伴った配置用オブジェクトが追加/削除されている場合に
// 照明を更新するために使うことが可能です。
void RecomputeStaticLighting(object oArea)
// lTileLocationにあるタイルの主照明の色を設定します。
// - lTileLocation: ベクター値部分はタイルのグリッド位置(x,y)です。
//
// - nMainLight1Color:
主照明1の色(TILE_MAIN_LIGHT_COLOR_*
// - nMainLight2Color:
主照明2の色(TILE_MAIN_LIGHT_COLOR_*
void SetTileMainLightColor(location lTileLocation, int
nMainLight1Color, int nMainLight2Color)
// lTileLocationにあるタイルの光源の色を設定します。
// - lTileLocation: ベクター値部分はタイルのグリッド位置(x,y)です。
//
// - nSourceLight1Color:
光源1の色(TILE_SOURCE_LIGHT_COLOR_*
// - nSourceLight2Color:
光源2の色(TILE_SOURCE_LIGHT_COLOR_*
void SetTileSourceLightColor(location lTileLocation, int
nSourceLight1Color, int nSourceLight2Color)
問題を隔離する
見ての通り私が求めていた関数は以下の関数でした。
- SetTileMainLightColor(location lTileLocation, int nMainLight1Color, int nMainLight2Color)
- SetTileSourceLightColor(location lTileLocation, int
nSourceLight1Color, int
nSourceLight2Color).
これらの関数のパラメーターリストと概要を見てみたところ、これらの関数にはロケーションオブジェクト(“タイルのグリッド位置(x,y)”を含んだベクターオブジェクト)と2つの整数(照明色)が必要でした。このことで私は堂々巡りになってしまいました。「グリッド 位置」とは何だろう?
私はグリッド座標が何であるか全く分かりませんでしたが、それがローケーションオブジェクトの一部であろうということは知っていました。そこで、私が次に行ったことは、ローケーションオブジェクトについて調べることでした。その結果は、私が求めていたもの以上のものでしたが、以下のようにいくつか有望なものがありました。
// lLocationからエリアオブジェクトのIDを得ます。
object
GetAreaFromLocation(location lLocation)
// oObjectのロケーションを得ます。
location GetLocation(object oObject)
// lLocationのベクター位置を得ます。
vector
GetPositionFromLocation(location lLocation)
そして、もちろん全てにおいて重要な以下のコンストラクタも含まれます。
// ロケーションを作成します。
location Location(object oArea, vector
vPosition, float fOrientation)
よって、この時点で私はローケーションオブジェクトについていくらか知ることができて、SetTileMainLightColor関数が、どのエリアに影響を与えるかを知るためにGetPositionFromLocationを利用するのではないかということも推測できました。私は、“グリッド位置”が何であるかはまだ分かりませんでしたが、それを含んだロケーションオブジェクトの作成方法を知りました。このパズルの欠片が集まり始めました。
グリッド座標の問題は置いておいて、私は照明色について調べ始めました。私は、これらの定数が全てTILE_MAIN_LIGHT_COLOR_ から始まり、ツールセットの定数セクションにこれら全てが詰め込まれていることを知りました。私は文字をタイプするのが嫌いなので、周り道を避けるために、エリアの照明色をテストするためにそれらの定数が定義されたスクリプトを単純に見ました。エリアを配置した後に、色を明るい白に設定すると以下のように定義されました。
int TILE_MAIN_LIGHT_COLOR_BRIGHT_WHITE = 3;
この時点で先に進むためには、“グリッド位置” が何であり、それがどの様な形式なのかを知る必要がありました。そこで私は、PrintString(string sMessage)を使ってスイッチのロケーションをログファイルに書き出すことを始めました。しばらくして、私はスイッチのロケーションを手に入れたので、それを以下のようにSetLight関数に直接指定しました。
location lLocation =
getLocation(OBJECT_SELF);
SetTileMainLightColor(lLocation, 3, 3);
もちろんこれは失敗したので、私はツールセットを起動してマウスのカーソルをスイッチのロケーションの上に載せてみました。その時、ステータスラインに以下のようなメッセージが表示されました。
マウス位置(x:n, y:n) グリッド位置(行:n, 列:n) タイル(tileSet_tile_name)
それから、私はマウス位置(x:n, x:n)とグリッド位置(行:n, 列:n)の間には関係があり、Row と Colは[x|y]/10(x あるいは y を10で割った値と読みます。)の整数部分であることが分かり始めました。よって、この時点でタイルが左下(0,0)から右上へ向かって参照されていることを知りました。
これで、特定のタイルへの参照が必要であることが分かりました。しかし、エリアについてはどうでしょうか?再びツールセットで“area”というキーワードを使って検索してみたところ以下のような関数があることが分かりました。
// oTargetが現在いるエリアを得ます。
// * エラー時にはOBJECT_INVALIDを返します。
object GetArea(object oTarget)
// lLocationからエリアオブジェクトのIDを得ます。
object
GetAreaFromLocation(location lLocation)
エリアはオブジェクトに他ならないので、オブジェクトについて調べる必要がありました。そこで、オブジェクトについて検索すると以下の関数が見つかりました。
// 指定されたタグを持っているnNth番目のオブジェクトを得ます。
// - sTag
// - nNth: 要求されたタグを持っているnNth番目のオブジェクト
// * オブジェクトを見つけられない場合にはOBJECT_INVALIDを返します。
object GetObjectByTag(string sTag, int
nNth=0)
これは、もしエリアのタグを知っていたら、そのタグを使ってエリアを取り出せるということを確証するものでした。私はエリアを作っていたので、そのタグを知ることができました。この時点で、以下の関数のための全てのパラメーターを入手しました。
SetTileMainLightColor(location,
int, int)
簡単に言うと、エリア内のタイルの位置のベクター値を含んだロケーションオブジェクト、そして、nwscriptファイルに定義されている2つの色を表す整数でした。
問題を修正する
この時点で、私は小さなパズルの全ての欠片を手に入れました。特定のタイルを参照するためには以下のものが必要であることを知りました。
1) タイルがあるエリアへオブジェクトの値
2) タイル自体のグリッド位置
次に私は、3X3サイズのエリアを作って、それに「foo」という名前をつけました。そして、エリアのプロパティで照明をoffにしました。 (torchlight
only). それから、以下のスクリプトを作成しました。
void main()
{
object oArea = GetObjectByTag("foo");
vector vLocation =
Vector(1.0, 1.0, 0.0);
location lTile =
Location(oArea, vLocation, 0.0);
SetTileMainLightColor(lTile, 3, 3);
}
このスクリプトをコンパイルしたときの私の驚きを想像してみて下さい!このスクリプトをスイッチのOnUsedスロットに設定して、モジュールを起動し、照明がついた時の私の喜びようを想像できますか?私は、次に再びエリアオブジェクトを得て次のグリッドの新しいロケーションを得るアルゴリズムを作成する必要があると分かりました。しかし、それは必要なく、もしエリア内の全ての照明をオフにしたければ、以下のようにエリアのグリッドを捜査する2つのforループを入れ子にして使えばいいことが分かりました。
for (int x = 0; x < MaxX; x++)
for (int y = 0; y < MaxY; y++)
tileVector.x = x
tileVector.y = y
tileLocation = Location( oArea, tileVector, 0.0)
SetTileMainLightColor( tileLocation, color1,
color2)
SetTileSourceLightColor( tileLocation, color3,
color4)
修正結果をテストする
次は実際のテストを行いました。nwscriptファイルのTILE_LIGHT_MAIN_COLOR_* セクション以下の行を見つけました。
int
TILE_MAIN_LIGHT_COLOR_BLACK
= 1;
私はもちろん、誰でも黒はオフと思うでしょう。TILE_MAIN_LIGHT_COLOR_BLACKを使って簡単なテストを行うと、少し落胆する結果でした。確かに、暗くはなりましたが私が望んでいた暗さではありませんでした。ここで、トラブルシューティングを行う新しい問題が発生しました。照明がオフとはどういう状態かということでした。そこで、私は小規模なデバッグサイクルを開始しました。この問題は特定され、隔離されました。照明をオフにするためにはオフの状態とはどういうことであるかを知る必要がありました。エリアの照明をオフ(たいまつのみ)にして、二次的な光源を持っていないタイルを使うと私が望んだ効果になりました。しかし、その色とは何でしょうか?ツールセットで色に関する関数を検索すると以下のリストが表示されました。
int GetTileMainLight1Color(location lTile)
int GetTileMainLight2Color(location lTile)
int
GetTileSourceLight1Color(location lTile)
int
GetTileSourceLight2Color(location lTile)
以下のコードを使えば、照明がオフの色の値を知ることができるというのは明らかでした。
object oArea =
GetObjectByTag("foo");
location lTile =
Location(oArea, 1.0, 1.0,
0.0);
PrintString("主照明1の色は "+IntToString(GetTileMainLight1Color(lTile)));
PrintString("主照明2の色は"+IntToString(GetTileMainLight2Color(lTile)));
PrintString("光源1の色は"+IntToString(GetTileSourceLight1Color(lTile)));
PrintString("光源2の色は"+IntToString(GetTileSourceLight2Color(lTile)));
このコードをモジュールでテストして、ログを見ると以下の結果が書かれていました。
主照明1の色は
255
主照明2の色は 255
光源1の色は 255
光源2の色は 255
これで、照明がオフの状態とは何であるかを知りました。すぐに、 SetTileMainLightColor(lTile, 255, 255)、 SetTileSourceLightColor(lTile, 255, 255)でテストすると正しく照明をオフにできました。これが、私の小規模なデバッグサイクルの結論です。
ここで、1つのタイル、あるいはグループ単位のタイルの照明をオフにするために必要な情報を全て得ることができました。繰り返すと以下のようになります。
- 影響を与えたいタイルがあるエリアオブジェクトの値
- エリア上のタイルのグリッド位置
(グリッド上のロケーションを10で割った値の整数部分)
- 1と2を含んだ新しいロケーションオブジェクト
- 新しい照明色を表す整数値 (または、あらかじめ定義された定数)
最後のステップは、これをコードに組み込んで再びテストすることでした。このコードの利便性を最大にしたかったので、私が集めた情報を元に最も一般的なスクリプトを作る必要がありました。スクリプトは以下のようになりました。
void
ChangeLights(int c1, int c2, int
c3, int
c4)
{
string sAreaTag = "Crypt"; //エリア名に合わせて名前を変更して下さい。
float maxX = 3.0; //Max Width.あなたのモジュールに合わせて変更して下さい。
float maxY = 3.0; //Max Height
あなたのモジュールに合わせて変更して下さい。
//それぞれのタイルに必要なベクター値
vector vTile = Vector(0.0,
0.0,
0.0);
//エリア上の始めのタイル(location[0,0])
location lTile =
Location(GetObjectByTag(sAreaTag, 0), vTile, 0.0);
//whileループに必要な変数
float fx = 0.0;
float fy = 0.0;
int notDone = TRUE;
while (notDone)
{
while (fx < maxX)
{
vTile.x = fx;
vTile.y = fy;
lTile = Location(GetObjectByTag(sAreaTag, 0),
vTile, 0.0);
SetTileMainLightColor (lTile, c1, c2);
SetTileSourceLightColor (lTile, c3, c4);
fx += 1.0;
}
fy += 1.0;
fx = 0.0;
if (fy >= maxY)
{
notDone = FALSE;
}
}
RecomputeStaticLighting(GetObjectByTag(sAreaTag));
}
私の計画は、このスクリプトをそれ自体で保管して、別のスクリプトからChangeLights(int
c1, int
c2, int
c3, int
c4)
として呼び出すことでした。スクリプトは、それを呼び出すスクリプトから#include 文を介して参照されます。 (これは私にとってはスタイル的な問題です。私はスクリプトにおいて#include文を使う回数を減らすために、結合可能な多くの小さな関数をライブラリとして持っています。私にとっては、数多くの小さなスクリプトよりも1つか2つの巨大なスクリプトの方が脅威です。特に、小さなスクリプトをテストするときに、それが言えます。)
ついに、多くの苦難を越えたあとに私は言えました。「照明スイッチが生まれたぞ!」
結論
第一に問題を特定する:
問題を1つか2つの文に特定します。できるだけ簡潔にします。まず、1つの問題を解決するように努力して下さい。全てを1度に解決しようとしないで下さい。スクリプトのデバッギングは、象のチョコレートを食べるようなものです。一度に1口しか噛めません。
次に問題を隔離する:
問題を再現するために、最小限度のテストケースを作ります。あなたの問題に影響を与えるものだけではなく、全ての変数についてスクリプトやツールセットで検索して下さい。モジュールとテストケース内の全てのオブジェクトをチェックして下さい。全ての関数について、パラメーターを調べて下さい。パラメーターについては、そのコンストラクタを含めて調べて下さい。パラメーターがどのように初期化され、予想される値がどのようにして入力されるか注目して下さい。
次に問題を修正する:
上の2つの項目を終えたら、テストケースで全てをチェックできます。もしデフォルトのスクリプトで生成されるオブジェクトが問題に含まれていたら、それを外して下さい。テストをして、少しずつそれを戻して下さい。あなたと同じ問題を抱えた人がいないかどうか、Biowareのフォーラムをチェックして下さい。あなたが抱えている問題を他の誰かが抱えているかもしれません。時間が過ぎるほど、その可能性は低くなります。(サーチパラメーターをJuly 28 2002に戻すことを忘れないで下さい。デフォルトのサーチでは、1ヶ月前からになっています。) これら全てを行っても情報が得られない場合に、あなたの問題をフォーラムに投稿して下さい。その際には、あなたが持っている十分な情報とあなたの意見を書いて下さい。完璧な質問に近づくほど、より素早く的確な答えが返ってくるでしょう。
最後に結果をテストする:
あなたのテストモジュールでは修正したものが上手く動くかもしれませんが、より大きなモジュールではどうでしょうか?私は、よく自分のコードを、私が作っていないカスタムモジュールや、公式キャンペーンの第1章でテストします。
これらはデバッギングボタンを押すようにはいきませんが、プログラムのエラーの修正における確かな方法といえます。スクリプト作成は簡単ではないですが、これらの方法を活用すれば、楽しくやりがいのあるものにきっとなるでしょう。
脚注:
*Grace Murray Hopperがハーバード大学においてMark IIコンピュータで作業しているときに、初めてコンピュータのバグを発見しました。彼女はそれをコンピュータのログブックに記録しました。それ以来、コンピュータが止まる時、彼らはディレクターのHoward Aikenにコンピュータを “デバッギング” していると言うようになりました。一番最初のコンピュータのバグは、今でもスミソニアン協会の国立アメリカ歴史博物館に存在しています。