Celowin - パート 4: ユーザー定義イベント
前置き
この一連のレッスンの目的は、全くの初心者をプログラミングの世界に誘い、モジュールを作成するためにNWNのスクリプトをどのように使うかを教えることです。初めの 部分のレッスンはとても基本的なものになると思われるので、何かのプログラムを書いたことがある人は飛ばしても構わないでしょう。これらのレッスンの最終目的は、プログラムと聞いただけで身震いするような人々でも学べる場を提供することです。
これらのレッスンをフォーラムに掲示したり、印刷したり、修正を加えることは大いに構いません。しかし、その際には私が作成したということをどこかで触れて下さい。良い悪いを含めて、これらのレッスンについて何かコメントがあれば私に送って下さい。Celowin.
これらのレッスンは、オーロラツールセットをある程度触っている人を想定して書かれています。これらのレッスンにおいて解りにくい部分があるという意見が多数寄せられれば、必要な部分をさらに詳しく説明することも考えています。
レッスン3の整理
今までこの一連のレッスンを読んできた人の中で鋭い人は、レッスン3で作ったガードのスクリプトは「まあまあ」ではあるけれども、我々が望むような行動を正確にはしていないことに気づいているでしょう。 ガードは、友好的な時はいつでも2回挨拶をします。注意深い人だけが気づくと思いますが、ここで問題を解決しておいた方がいいでしょう。加えて、別の概念について説明する機会としても利用したいと思います。
まず、ガードが何故そのそうな行動をしたのかを正確に把握し、それをOnPerceptionスロットのスクリプトに要約する必要があります。OnPerceptionスロットのスクリプトが実行される要因としては、以下の4つがあります。
- NPC何かを見るとき
- NPCが何かを聞くとき
- NPCが視界から何かが消えることに気づくとき
- NPCが何かの音が止むことに気づくとき
したがって、NPCはPCを見て、かつPCの音を聞いたのでスクリプトが2回実行されたのです。重大なことではありませんが、どっちにしろ我々が望んだ行動ではありません。
この問題を次の方法で解決してみましょう。何かを見るときだけ、ガードに反応させます。
if文を入れ子にすることで、このことを実現できますが、既にスクリプトが少し複雑になっています。代わりに、スクリプトに2つのことを同時にチェックさせることにします。つまり、気づかれたオブジェクトがPCで、かつ視認されたということを確認させます。両方の条件が満たされる時だけ、後の処理を行います。
条件に演算子「&&」(シンプルに 'and'と呼びます)を追加することで可能です。条件に用いられる場合、「&&」は、2つの条件を繋ぎ合わせます。両方の条件がtrueの時だけ、全体の条件がtrueとなります。スクリプトを以下のように修正するだけです。
if (GetIsPC(oSeen))
を
if (GetIsPC(oSeen) && GetLastPerceptionSeen())
に修正します。そうすると、スクリプトは、PCがガードに気づかれた時、しかも、他の方法ではなく視認によって気づかれた時だけ処理を実行します。
GetLastPerceptionSeen関数は、視認によって気づかれた場合は、TRUE、そうでない場合はFALSEを返す関数です。
今回は例を示すつもりはありませんが、条件を結び繋げる演算子として「&&」の他に「||」があります。「||」は「or」と読み、いずれかの条件がtrueであれば、trueであるということを意味します。これは、ガードがPCを攻撃する理由が複数ある場合に利用できるかもしれません。(例えば、PCがリングを持っていない場合、PCが村長の首を持っている場合、いずれかの条件が満たされればガードがPCを攻撃するとか)
告白
ここで、私はこれまでのレッスンで嘘をついていたと認めなければなりません。私は何回も、実際のモジュール作成を想定したスクリプトの書き方をしますと言ってきました。しかし、これまで書いてきたスクリプトは、私が最終的に書く形ではないということを白状しなければなりません。
問題は、我々が作ったNPCが大抵の刺激に無反応であるということです。我々がこれまでに作成したガードは、スタートしたらその場に立っているだけで、現実的な行動をしているとは到底言えません。興味があれば次のことを試してみて下さい。ガードのHPとレベルを増強して、モジュールをスタートします。リングを拾って近づくと、ガードは友好的に接するでしょう。それから、ガードを攻撃します。ガードは攻撃されるがままに、その場に立っているだけでです。これは、大抵のNPCに望む行動とは決して言えません。
このようなNPCとなってしまった理由は、BioWareによって丹念に作られたデフォルトのスクリプトを削除したためです。デフォルトのスクリプトには、ほとんどのケースにおいて我々が求めると思われる便利な「標準」の行動が定義されています。 (実際、このレッスンで作成するNPCの中に、デフォルトのスクリプトを削除するものもありますが、他についてはデフォルトのスクリプトを使用します。)
では、どのようにしてデフォルトのスクリプトを削除せずに我々独自の行動を定義すればいいのでしょうか? それには、「OnUserDefined」スロットのスクリプトを使用します。上手く使えば、OnSpawnのスクリプトを少し修正し、大部分のスクリプトをOnUserDefinedスロットのスクリプトに書くことで、他の全てのスロットのスクリプトを利用することができます。
ここまで、ガードの作成に多くの時間を費やしてきました。先に進んで、あるべき姿に修正してみましょう。
- テストモジュールを開きます。
- ガードを削除します。
- ガードが居た場所に新しいNPCを作成します。(これでデフォルトのスクリプトが復活します。)
- NPCのタグをGUARDに変更します。
- 「詳細設定」タブで、ガードのファクションが「コモナー」であることを確認します。
- スクリプトタブを選択します。
- OnSpawnスロットのスクリプトを編集します。数多くのスクリプトが書かれていますが、今回はほとんど手を加えません。
- スクリプト後半から、以下の行を探します。
//SetSpawnInCondition(NW_FLAG_PERCIEVE_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1002
- 行の頭の「//」を削除します。(「OPTIONAL」の前にある2番目の「//」は削除しないで下さい。)
- スクリプトをtm_guard_osという名前で保存します。
- スクリプトエディタを閉じて、OnUserDefinedスロットを選択します。
- スクリプトリストの中から、tm_guard_opを選択します。
- 編集ボタンをクリックして、エディタで開きます。スクリプトをtm_guard_udという名前で保存します。
- 新しいスロットに設定され、新しいスクリプト名となったのでスクリプトのコメントを更新します。
- もう一度保存します。
- 「OK」をクリックしてウィンドウを閉じ、モジュールを保存します。
モジュールをスタートすると、今度はガードがよりリアルな行動をするでしょう。ガードは、我々が書いたスクリプトに従って反応しますが、今回は他の刺激に対しても反応します。ガードを攻撃すれば、反撃してきます。他にも数多くの行動がスクリプトに書かれていますが、それらの多くは内密に行われており、通常は決して気づくことはないでしょう。
では、実際に何を行ったのでしょうか? OnSpawnスクリプトの「//」を削除するということは、何かの「コメントアウトを外す」ということです。「//」の後のスクリプトは全て無視されると言うことを思い出して下さい。つまり、BioWareはOnSpawnにちょっとした「オプション」を設定しており、その前に「//」をつけることで処理されないようにしたのです。
しかし、今回は処理させたいので、「//」を削除することで「その行を処理して欲しい」と伝えています。
では、その「復活した」行は実際に何を行っているのでしょうか? それは、「OnPerceptionスロットのスクリプトを実行するときに、OnUserDefinedスロットのスクリプトも実行する」ということです。
より鋭い人は、「NPCに複数の新しい行動をさせたい場合は?つまり、OnPerceptionとOnHeartbeatにおいて、特別な行動をさせたい場合はどうすればいいのでしょうか?」と思うでしょう。
それは可能ですが、少し追加の作業が必要です。スクリプトを単純にするために、NPCの行動も単純にしましょう。6秒毎に「退屈だ」と言い、PCを見たときにお辞儀をするNPCを作成してみます。
- ツールセットを起動して、NPCを作成し、タグをBOREDに変更します。
- OnSpawnスロットのスクリプトを編集して、OnHeartBeat と OnPerceptionに該当する行のコメントアウトを外します。
- それぞれ関連づけられた「数字」を元に該当する行を探します。OnPerceptionが1002で、OnHeartBeatが1001です。
- スクリプトをtm_bored_osという名前で保存します。.
- 以下のスクリプトをOnUserDefinedスロットに設定し、tm_bored_udという名前で保存します。:
// OnUserDefinedスロットに設定するスクリプト: tm_bored_ud // OnHeartbeatスロットとOnPerceptionスロットのスクリプトから呼ばれます。 // // NPCは6秒毎に退屈であると不満を言います。 // そして、PCを見たときにお辞儀をします。 // int nCalledBy=GetUserDefinedEventNumber(); void main() { switch(nCalledBy) { case 1001: // OnHeartbeatスロットから呼ばれます。 ActionSpeakString("退屈だな"); break; // case 1002: // OnPerceptionスロットから呼ばれます。 object oSeen=GetLastPerceived(); if (GetIsPC(oSeen) && GetLastPerceptionSeen()) ActionPlayAnimation(ANIMATION_FIREFORGET_BOW); break; } }
Q&A
- GetUserDefinedEventNumberとは何ですか?
この数字には注目する必要があります。BioWareはとてもかしこい事をしました。それぞれの異なったスロットからユーザ定義のスクリプトを呼べるようにしただけでなく、呼ばれた時にそれぞれ別々の数字を情報として渡すようにしました。 よって、最初の行で行っているのは、スクリプトがHeartbeat (1001)、または、Perception (1002)のどのスロットから呼ばれたのかを調べています。
- switch命令とは何ですか?
ここでは全て詳しく説明するつもりはありません。 基本的に、switch命令は与えられた整数に対して、その数字が含まれている「case」の行に「ジャンプ」します。よって、nCalledByが1001であれば、スクリプトは「case 1001」の行にジャンプします。そして、break文までの間にある処理を実行します。
書式上の注意: switch命令は、{ }で囲まれた部分の処理だけを実行します。また、if文のようにswitchの行の後ろにはセミコロンは必要ありません。
私は、OnUserDefinedスロットのスクリプトを書くとき、例え1つのスロットからしか呼ばれなくても、いつもswitch文を使うようにしています。なぜなら、後で気が変わって複数のスロットから呼ぶようにするかもしれないからです。前もって計画を立てることを怠ったがために、スクリプトをいじくらなければならない羽目になるよりも、今後の可能性を考え準備しておいた方が良いと言えます。
- おい、if文に{ } が使われていないじゃないか!
-
if文の処理が1行しかない場合は、{ }は必要ありません。 私にとっては、{ }があった方が良いと思うときもあれば、ない方が良いと思うときもあります。どちらにしろ、スクリプトをすっきりさせるためです。今回は、{ }があると邪魔だと思ってつけませんでした。
追加例
この時点で、あなたはスクリプトを大分扱えるようになっていると思います。 もうこのレッスンを完全な初心者のレッスンとは呼ぶべきではありません。なぜなら、あなたはもう完全な初心者ではないからです。 まだ、学ぶ必要がある関数や、テクニックは多くあります。しかし、今まで学んできたことを上手く組み合わせれば、クールなことがたくさんできます。
その例をこれから示したいと思います。スクリプトはちょっと複雑で、多くの設定、いくつかの新しい関数を使います。しかし、最後までやる価値はあると思います。
- テストモジュールを開きます。
- とりあえずガードからは離れようと思うので、始めに作ったエリアを開きます。
- 先に作ったSINGER NPCを削除します。
- スタート地点をペイントします。
- コモナーのNPCを部屋の一方の角に作成します。
- 「基本設定」タブで、それぞれ、名をダーツボード、タグをDARTBRD、種族を「人造」、外観を「弓の標的」、性別を「なし」、そして、ポートレイトをpo_PLC_F01_ に変更します。(「配置用オブジェクトとドア」をクリックすると見つかります。)
- 「詳細設定」タブで、「プロット」をチェックします。
- 「詳細設定」タブで、ファクション・エディタを開きます。元ファクションを「敵対的」として、新しいファクション「Target」を作成します。Target対コモナー、コモナー対Targetの値を両方とも50に設定します。
- ダーツボードのファクションをTargetに変更します。
- 「スクリプト」タブで、デフォルトのスクリプトを全て削除します。(今回は、ダーツボードに暴れたり、人々を攻撃したりさせたくないからです。)
- 「OnDamaged」スロットのスクリプトを編集して、以下のスクリプトを設定します。
// OnDamagedスロットに設定するスクリプト: tm_dartbrd_dm // Dartboard のスクリプト // 長距離武器で攻撃された時に、「ゴトッ」となります。 // void main() { if (GetWeaponRanged(GetLastWeaponUsed(GetLastAttacker()))) { SpeakString("**ゴトッ**"); } }
- この時点でモジュールを保存して試すことはできますが、まだクールとは言えません。
- ダーツボードから1ブロックほど離れたところに、ウェイポイントを作成します。
- ウェイポイントのタグをDARTWP001に変更します。
- ウェイポイントの近くにコモナーのNPCを作成します。
- NPCのタグをDARTPLAYに変更します。
- たまには「ランダムの名称を作成」ボタンをクリックするのもいいでしょう。
- 「特技」タブで、「単純武器習熟」を選択します。
- NPCのポートレイトの下にある「所持品編集」ボタンを押します。
- 右側から「武器」、「投擲系武器」、「ダーツ」を選択し、左側の「内容」の方にドラッグします。
- ドラッグしたダーツの上で右クリックしてプロパティを編集し、最大スタック数を3に変更します。
- OKをクリックし、ダーツをNPCの装備にドラッグします。
- OKをクリックし、「入れ物の内容」画面を抜けます。.
- 次にスクリプトタブを選択します。
- OnSpawnスロットのスクリプトを編集し、HeartBeat(1001)の行のコメントアウトを外します。
- 同様に次の行のコメントアウトも外します。: SetSpawnInCondition (NW_FLAG_SET_WARNINGS);
- そして、スクリプトの適当な場所に次の行を追加します。: SetLocalInt(OBJECT_SELF, "DARTSTATE", 1);
- スクリプトをtm_dartplay_osという名前で保存します。
- OnUserDefinedスロットのスクリプトを編集して、以下のスクリプトを設定し、tm_dartplay_udという名前で保存します。
// OnUserDefinedスロットに設定するスクリプト // tm_dartplay_ud // NPCにダーツをプレイさせます。OnHeartBeatスロットのスクリプトから呼ばれます。 // // ダーツプレイヤーは、所持品のダーツを全て投げます。(スタートは3本です。) // そして、dartboardに歩いて行き、3本のダーツを拾って元の場所に戻り、再びダーツを繰り返します。 // void main() { int nCalledBy = GetUserDefinedEventNumber(); object oTarget = GetNearestObjectByTag("DARTBRD"); int nDartsReady = GetLocalInt(OBJECT_SELF, "DARTSTATE"); // switch(nCalledBy) { case 1001: // OnHeartbeatスロットから呼ばれた場合 // // ダーツを投げる準備が出来ていれば、nDartsReadyは1、そうでなければ2となるでしょう。 // if ((GetIsObjectValid(GetItemInSlot(INVENTORY_SLOT_RIGHTHAND))) && (nDartsReady == 1)) { // 右手にダーツを持っていれば、投げる準備が出来ているので、ダーツを投げます。 ClearAllActions(); ActionAttack(oTarget, TRUE); } else { // 他の場合は、2つのケースがあります。ダーツを切らしたか、 // ダーツを拾いに行く途中であるかです。既にダーツを拾いに行っているのであれば、 // その行動を邪魔させないようにしています。 if (nDartsReady == 1) { SetLocalInt(OBJECT_SELF, "DARTSTATE", 2); ActionMoveToObject(oTarget); ActionWait(0.5); ActionPlayAnimation(ANIMATION_LOOPING_GET_MID, 1.0, 1.0); ActionWait(0.5); ActionPlayAnimation(ANIMATION_LOOPING_GET_MID, 1.0, 1.0); ActionWait(0.5); ActionPlayAnimation(ANIMATION_LOOPING_GET_MID, 1.0, 1.0); object oDestination=GetNearestObjectByTag("DARTWP001"); ActionMoveToObject(oDestination); CreateItemOnObject("nw_wthdt001", OBJECT_SELF, 3); ActionEquipMostDamagingRanged(); ActionDoCommand(SetLocalInt(OBJECT_SELF, "DARTSTATE", 1)); } } break; } }
さて、これは本当に複雑なスクリプトです。 全てについて説明するつもりはありませんが、いくつかポイント的に説明します。
ローカル変数 DARTSTATEを使って、準備が出来てない時にはダーツを投げないようにしたり、ダーツを拾いに行く行動を邪魔させないようにしています。
ActionDoCommand関数は、驚くほど便利です。この関数は、アクションキューに配置されないコマンドを、キューに配置するように変更します。通常、スクリプトはSetLocalInt関数を見つけると、すぐにローカル変数を設定します。ここでのActionDoCommand関数は、キューに配置されたNPCの行動が全て終わるまで、スクリプトにSetLocalInt関数の処理を待つように伝えます。
CreateItemOnObject関数は、NPCのために新しいダーツを作成するために使われています。「nw_wthdt001」は、ダーツのブループリントで、最後の3はスタック数です。
他の部分は、スクリプトをじっくり追跡出来るときに確認して下さい。新しい関数がいくつかありますが、そのほとんどは説明的な名前です。いずれにしろ、分からないことがあればいつでも質問して下さい。
author: Celowin, editor: Charles Feduke, additional contributor(s): Jenn Jimerson, Sergio Paschoal, JP team: katsu794
Send comments on this topic.