Celowin - パート 8: 関数
前置き
この一連のレッスンの目的は、全くの初心者をプログラミングの世界に誘い、モジュールを作成するためにNWNのスクリプトをどのように使うかを教えることです。初めの 部分のレッスンはとても基本的なものになると思われるので、何かのプログラムを書いたことがある人は飛ばしても構わないでしょう。これらのレッスンの最終目的は、プログラムと聞いただけで身震いするような人々でも学べる場を提供することです。
これらのレッスンをフォーラムに掲示したり、印刷したり、修正を加えることは大いに構いません。しかし、その際には私が作成したということをどこかで触れて下さい。良い悪いを含めて、これらのレッスンについて何かコメントがあれば私に送って下さい。Celowin.
これらのレッスンは、オーロラツールセットをある程度触っている人を想定して書かれています。これらのレッスンにおいて解りにくい部分があるという意見が多数寄せられれば、必要な部分をさらに詳しく説明することも考えています。
今までのレッスンを見逃した人で、興味のある人は、BiowareのスクリプトフォーラムにあるScripting FAQ and Tutorialsを見て下さい。そこに全て置いてあります。[NWN Lexicon's Lyceumにも同様にミラーされていいます。].
イントロダクション
このレッスンは難しいものになると思います。レッスン1からずっと、我々はBioWareが作成した関数を使ってきました。今回は、自分自身の関数を書く方法を学ぼうと思います。
このレッスンについて、私が言うことを全て理解できなくても心配しないで下さい。今回のテーマは決して1回では理解できません。そして、事実、分かるまでには多くの練習を要します。頭に入らないと感じたら、少しの間離れてみて下さい。他のことをやって後に、再び取り組んで下さい。
始める前に少し注意事項があります。例え関数を書いたとしても、それは「ローカル」でしか使えません。例えば、特定のスロットのためにスクリプトを書いていると想定して下さい。それは、NPCのOnPerceptionスロットだとします。我々はそのスロットで使うために関数を書きます。そのスクリプトの「main」関数の中では使うことができますが、他の場所では使うことはできません。そのNPCの他のスロットでは使うことができませんし、別のNPCのOnPerceptionスロットでも使うことはできません。
そうです。このことは、関数を書く際に多くの制限を設けてしまいます。しかし、同時に、それは多くの可能性を広げているとも言えます。心配いりません。将来のレッスンで、もっと普通に便利な関数を書く方法を説明したいと思います。しかし、ここでは一歩一歩進まなければなりません。
関数の定義
全ての関数は以下のような行から始まります。
void GateIn(string sBluePrint, location lGate)
短い行ですが、多くの複雑な概念が詰め込まれています。
始めの部分のvoid は、その関数の出力が何であるかを示しています。この場合は、出力は .... うーん ... 何もないです。この部分には、int、float、object、eventなど全てのデータタイプを使用することができます。最も一般的なのはvoidで、このレッスンではvoidだけを扱おうと思います。
次の部分、GateInは関数の名前です。名前を一度定義すると、BioWareが定義した関数のように、この名前を使って関数を呼び出すことができます。(繰り返しになりますが、現在のスロットのスクリプト中でしかその関数は使えないという制限があることを忘れないで下さい。)
( )の中の部分は、最も理解するのが難しい部分です。ここで、関数の入力を設定しています。(これらの専門的な名称は、「パラメーター」ですが、覚えなくてもいいでしょう。) ここでは、関数に2つの入力があるということを宣言しています。1つは文字列で、もう1つはロケーションです。
ここまではいいでしょう。ここから難しい部分に入ります。実際に関数を書く前に、その関数がどんな動作をするかを理解する必要があります。恐らく、我々はこれらの入力を元に何らかの処理をする関数を書いています。そうでなければ、それらの入力は必要ありません。
スクリプトのどこかで、以下のように関数を呼び出すと想定して下さい。
GateIn("nw_fireelder", lSummonPoint);
そうすると、文字列"nw_fireelder"が1つめの入力です。事実上、関数が最初に行うことはsBluePrint を"nw_fireelder"を設定することです。関数の何処においても、"nw_fireelder"を表す変数としてsBluePrintを使うことができます。2つめの入力も同じです。lGateは、lSummonPointが保持している何らかのロケーションに設定されます。
関数の定義2
さて、関数全体を見てみましょう。
// この関数はsBluePrintで与えられたブループリントのクリーチャーを // ロケーションlGateに召喚します。そして、召喚されたクリーチャーは、 // この関数を呼び出したオブジェクトに最も近いPCを攻撃します。 // void GateIn(string sBluePrint, location lGate) { // クリーチャーを作成します。 object oNewCreature = CreateObject(OBJECT_TYPE_CREATURE, sBluePrint, lGate); // 最も近いPCを見つけます。 object oPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC, OBJECT_SELF); // クリーチャーにPCを攻撃させます。 AssignCommand(oNewCreature, ActionAttack(oPC)); }
上の関数を入力してコンパイルしようとすると、次のエラーメッセージが表示されるでしょう。ERROR: NO FUNCTION MAIN() IN SCRIPT 関数自体だけでは、スクリプトにはなりません。void main()が必要ですが、今は関数自体だけを見てみたいと思います。
おそらく、最も重要な部分は、オブジェクトを作成している最初の行でしょう。ここでは、過去に何度も使ったCreateObject関数を使っていますが、我々の関数の入力情報を与えています。GateInに入力を与えると、それらはCreateObject関数に与えられます。ややこしいと思うかもしれませんが、ここで我々の作った関数の真の力が発揮されます。GateIn関数に異なった入力を与えることによって、異なったクリーチャーを何度も呼び出せるのです。
「object oPC =」で始まる次の行は長いですが、「OBJECT_SELFに最も近いPCを見つけなさい」と言っているだけです。GateIn関数をどんなオブジェクトが呼ぶか分からないので、OBJECT_SELFは何になるか分かりません。
最後に、先ほど見つけたPCを攻撃するように、作成されたクリーチャーに伝えています。
挑戦してみよう
ここでは、先ほど作成した関数を使って少し複雑なスクリプトを書いてみます。もっと簡単な方法があるに違いありませんが、何故その関数を使いたいかの例を示したいと思います。
- ツールセットを起動します。
- まず、アイテムウィザードを使って、雑貨(中)を作ります。
- アイテムの名前を、「魔法使いの頭蓋骨」にします。
- パレットのカテゴリー選択で、プロットアイテムに設定します。
- ウィザードを終了して、プロパティを編集します。
- タグをALTSKULLにします。
- 外観タブでiit_midmisc_021を選択します。
- OKを押して、ウィンドウを閉じます。
- ウェイポイントを作成して、タグをALTSUMWPにします。
- ウェイポイントの側に祭壇を作成して、プロパティを編集します。
- 祭壇のタグをSUMMALTRにします。
- 「使用可」と「入れ物」をチェックします。
- 祭壇の所持品編集ボタンを押して、先ほど作った頭蓋骨を入れます。
- 「OK」を押して所持品編集を抜け、スクリプトタブを選択します。
- OnDisturbedスロットに以下のスクリプトを書きます。
// OnDisturbedスロットに設定するスクリプト: tm_summaltr_ds // // このスクリプトは、祭壇から頭蓋骨ALTSKULLが取り去られた時に // 10種類のクリーチャーの中からランダムに1種類のクリーチャーを選んで召喚します。 // クリーチャーはウェイポイントALTSUMWPに召喚されます。 // // // 作成者 Celowin // 最終更新日: 7/16/02 // この関数はsBluePrintで与えられたブループリントのクリーチャーを // ロケーションlGateに召喚します。そして、召喚されたクリーチャーは、 // この関数を呼び出したオブジェクトに最も近いPCを攻撃します。 // void GateIn(string sBluePrint, location lGate) { // クリーチャーを作成します。 object oNewCreature = CreateObject(OBJECT_TYPE_CREATURE, sBluePrint, lGate); // 最も近いPCを見つけます。 object oPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC, OBJECT_SELF); // クリーチャーにPCを攻撃させます。 AssignCommand(oNewCreature, ActionAttack(oPC)); } // GateIn関数の終わり // ここからはmain関数 void main() { // 祭壇の中に骸骨があれば問題ないので、何もしません。 // OBJECT_SELFは祭壇を参照しています。 if (GetItemPossessor(GetObjectByTag("ALTSKULL")) != OBJECT_SELF) { // ウェイポイントを通じて召喚場所を見つけます。 location lSummonPoint = GetLocation(GetWaypointByTag("ALTSUMWP")); // 召喚する時の視覚エフェクトを作成します。 effect eGate = EffectVisualEffect(VFX_FNF_SUMMON_GATE); ApplyEffectAtLocation(DURATION_TYPE_TEMPORARY, eGate, lSummonPoint, 3.0); // 召喚するクリーチャーを決めるためにランダムな数を決定します。 int nCreature = d10(); switch(nCreature) { case 1: // ポーラーベアーを召喚します。 DelayCommand(3.0,GateIn("nw_bearpolar", lSummonPoint)); break; case 2: // ウシを召喚します。 DelayCommand(3.0,GateIn("nw_cow", lSummonPoint)); break; case 3: // ボーンゴーレムを召喚します。 DelayCommand(3.0,GateIn("nw_golbone", lSummonPoint)); break; case 4: // エルダー・ファイアーエレメンタルを召喚します。 DelayCommand(3.0,GateIn("nw_fireelder", lSummonPoint)); break; case 5: // オーガ・ハイメイジを召喚します。 DelayCommand(3.0,GateIn("nw_ogremageboss", lSummonPoint)); break; case 6: // ユアンティ・メイジを召喚します。 DelayCommand(3.0,GateIn("nw_yuan_ti002", lSummonPoint)); break; case 7: // スピッティング・ファイアービートルを召喚します。 DelayCommand(3.0,GateIn("nw_btlfire02", lSummonPoint)); break; case 8: // クレンシャーを召喚します。 DelayCommand(3.0,GateIn("nw_kreshar", lSummonPoint)); break; case 9: // ワーキャットを召喚します。 DelayCommand(3.0,GateIn("nw_werecat001", lSummonPoint)); break; case 10: // ハイリッチを召喚します。 DelayCommand(3.0,GateIn("nw_lichboss", lSummonPoint)); break; } // switch文の終わり } // if文の終わり } // main関数の終わり
全てを保存して、モジュールをテストしてみて下さい。祭壇から頭蓋骨を取り去ると、10種類のクリーチャーの中からランダムに1種類のクリーチャーが召喚され、PCを攻撃します。(もちろん、ウシは攻撃しないでしょうが、その他のクリーチャーは攻撃するでしょう。)
まず、誰が頭蓋骨を持っているかを調べます。祭壇以外が頭蓋骨を持っていれば、if文内部の処理を行います。
召喚時の視覚エフェクトをウェイポイントに作成します。これは前回のレッスンで行ったエフェクト作成と同じ方法です。それから、1から10の間のランダムな数字を決定して、クリーチャーを召喚するためにGateIn関数を呼び出しています。
さて、ここで、何故自分で作成した関数を使いたかったについて述べます。それには2つの理由があります。1つは単純明快な理由で、もう1つは少し複雑な理由です。
まず、1つ目の理由について述べましょう。基本的には、GateIn 関数を呼びだすことで、クリーチャーを召喚する3行のスクリプトを毎回書かなくても済みます。GateIn 関数を呼び出すだけで、クリーチャーを召喚し、ターゲットを見つけ、PCを攻撃することが1度にできます。10種類の場合分けを行っているので、コメント部分を除いても約20行のスクリプトを書かずに済んでいるということになります。
2つ目の理由は、少し理解しにくいです。しかし、基本的に、今回の場合はその関数を使う必要があったということです。スクリプトでは、召喚のゲートが形成されるまで、召喚を遅らせています。なぜなら、そのままだと視覚エフェクトがクリーチャーの姿を覆い隠してしまうからです。つまり、DelayCommand 関数を使う必要があったからです。しかし、DelayCommand 関数は、voidを返す関数にしか使用できません。CreateObject 関数は、オブジェクトを返します。DelayCommand 関数に直接CreateObject 関数を指定するとコンパイルできません。CreateObject関数を別の関数に入れ、その関数がvoidを返すようにすることで、その制限を回避しています。
ここまでの整理
このレッスンは、自分自身の関数を作ったことのない人にとっては、ちょっと複雑過ぎたと思っています。しかし、今回作ったスクリプトは少し修正の必要がありそうです。頭蓋骨が祭壇の中にあれば、全てはOKです。祭壇の中に頭蓋骨が残っている限り、アイテムを祭壇の中に入れたり、それを取り出したりできます。そして、頭蓋骨を取り去るやいなや、クリーチャーが召喚されます。特に問題はありません。
しかし、祭壇をいじり続けるとどうなるでしょうか? 頭蓋骨を持った状態で、他のアイテムを祭壇の中にいれてみると、別のクリーチャーが召喚されるではありませんか! 入れたアイテムを取り去ると、また別のクリーチャーが召喚されます!これはおそらく望んだ結果ではありません。よって、頭蓋骨が取り去られた時だけクリーチャーが召喚されるように修正してみて下さい。関数全体を修正する必要はありません。ちょっとif文の条件をいじるだけで大丈夫です。
例 2: プロットアイテムを取り去る
青白い顔をして、性格のひねくれたDM (おっと待って下さい。それは私でした。)が、プレイヤーに超現実的な夢の世界を作って見せます。言葉を話すペンギン、怪奇なパズル、クエスト。しかし、結局はプレイヤーは夢の世界から目覚めます。ペンと紙の世界では、夢の世界でプレイヤーが拾ったアイテムを全て取り除くのは簡単です。しかし、NWNにおいてはどうでしょうか?
ちょっと考える必要がありますが、可能です。まず、私は夢の世界の全てのアイテムを似たタグ名にしました。全てのプロットアイテムは、DREAMITM、全ての武器は DREAMWPN、全ての防具は、DREAMARM、そして鍵(1つだけです)は、DREAMKEYというタグにしました。
そして、夢の世界のOnExit スロットに以下のスクリプトを設定しました。
// エリアのOnExitスロットに設定するスクリプト: tm_area002_ex // // このスクリプトは、タグがDREAMITM、DREAMWPN、 // DREAMARM、または、DREAMKEYの全てのアイテムをエリアを出たPCから取り去ります。 // // 作成者 Celowin // 最終更新日: 7/16/02 // この関数は、オブジェクトoStrippeeからタグsTagのアイテムを // 全て取り去ります。 void StripItems(object oStrippee, string sTag) { // 初期化: 所持品欄の最初のアイテムを得ます。 object oCurrentItem = GetFirstItemInInventory(oStrippee); // 所持品欄の全てのアイテムに対してループします。 while (oCurrentItem != OBJECT_INVALID) { if (GetTag(oCurrentItem) == sTag) DestroyObject(oCurrentItem); // 正しいタグのアイテムを破壊します。 oCurrentItem = GetNextItemInInventory(oStrippee); } // whileループの終わり } // StripItems関数の終わり void main() { // まず、エリアを出たオブジェクトを得ます。 object oPC = GetExitingObject(); if (GetIsPC(oPC)) // それがPCであれば、アイテムを取り去ります。 { StripItems(oPC, "DREAMITM"); // 一般的な夢のアイテム StripItems(oPC, "DREAMWPN"); // 夢の武器 StripItems(oPC, "DREAMARM"); // 夢のアーマー StripItems(oPC, "DREAMKEY"); // 夢の鍵 } // if文の終わり } // main関数の終わり
(このスクリプトでは、文字列操作を使った方がより良いでしょう。このバージョンは、PCの所持品欄を4回もループするので、効率的ではありません。)
必要であれば試してみることも可能です。DREAM***のアイテムをいくつか作ってモジュールに置き、1つのエリアのOnExitスロットにスクリプトを設定して、プレイしてみて下さい。あなたが残酷なDMならば、プレイヤーから全てのアイテムを取り去るように修正することも簡単にできます。
要約
このレッスンでは、関数の機能の一部を紹介しました。何かを計算するための関数を作ったり、関数のライブラリを作ったり、スクリプトの複雑さを劇的に軽減するために関数を使うこともできます。しかし、関数のパワーは計り知れないので、一度で全てをカバーすることは難しいです。
ほとんどの部分において、みなさんからのフィードバックはポジティブなものです。次の様な、少数派の意見が1つだけ何度も来ました。「私はあなたのレッスンについていけるし、あなたの説明も良いです。だから、どのように、そしてなぜかが理解できます。しかし、それらのテクニックをいつ使えば良いかが分かりません。」
私はそれも説明しようと頑張っていますが、問題はいつもその時間がないということです。スクリプトを十分見ることをできるようになったら、そのテクニックをいつ使えばいいのかが直感的に分かり始めます。それは簡単とは言えません。私は何をしているか分かっている時でさえも、しばしば複雑なスクリプトに取り組んで、何時間もキーボードの前で頭を抱えることもあります。
では、関数を使うときにおける「一般的なルール」とは何でしょうか?それは、あなた自身がやってきたことを見て下さいと私は言うでしょう。もしあなたが同じ事を何度も何度も書いていると気づいたら、簡潔にするために関数を使いたいと思うでしょう。繰り返し書いているスクリプトが例え2、3行であっても、その行を関数として囲むことによって、あなたのスクリプトコードがより簡単に理解できるものになります。
今後について
このレッスンで取り扱うアイディアが尽き果てようとしています。「基本」は、全てカバーしてきましたし、実際、いくつかの高度な概念についても扱い始めています。確かに説明は詳しくないし、表面だけ触れて省いたものもありますが、この時点で、あなたはほとんどのスクリプトを見ることができて、「おい、これが何をするか知っているぜ」と言うことができるだろうと思います。自分自身で一からスクリプトを書くことはできないかもしれませんが、スクリプトを分析して、それがどんな動作をするのか理解することがきっとできるでしょう。
あとは、「include」文について、そして、自分自身の関数ライブラリの書き方について少し説明する必要があると感じていますが、とても複雑なレッスンになるとは思っていません。
従って、突然のインスピレーションが無い限りは、このレッスンは10回位で終わろうと思っています。「続編」シリーズのアイディアもありますが、それは異なったアプローチになると思います。その詳細については、もう少し考え出してからお披露目する予定です。
author: Celowin, editor: Iskander Merriman, JP team: katsu794
Send comments on this topic.