Celowin - パート 9: 値を返す関数、デフォルトパラメーター、ライブラリ
前置き
この一連のレッスンの目的は、全くの初心者をプログラミングの世界に誘い、モジュールを作成するためにNWNのスクリプトをどのように使うかを教えることです。初めの 部分のレッスンはとても基本的なものになると思われるので、何かのプログラムを書いたことがある人は飛ばしても構わないでしょう。これらのレッスンの最終目的は、プログラムと聞いただけで身震いするような人々でも学べる場を提供することです。
これらのレッスンをフォーラムに掲示したり、印刷したり、修正を加えることは大いに構いません。しかし、その際には私が作成したということをどこかで触れて下さい。良い悪いを含めて、これらのレッスンについて何かコメントがあれば私に送って下さい。Celowin.
これらのレッスンは、オーロラツールセットをある程度触っている人を想定して書かれています。これらのレッスンにおいて解りにくい部分があるという意見が多数寄せられれば、必要な部分をさらに詳しく説明することも考えています。
今までのレッスンを見逃した人で、興味のある人は、BiowareのスクリプトフォーラムにあるScripting FAQ and Tutorialsを見て下さい。そこに全て置いてあります。[NWN Lexicon's Lyceumにも同様にミラーされていいます。].
イントロダクション
前回のレッスンは難しい内容でしたが、今回のレッスンも長いものになりそうです。従って、レッスン8が全てあやふやであれば、もう一回読んでみることをお奨めします。マスターする必要はありませんし、実際、ほとんどの人がマスターできるとは思っていません。また、このレッスンの例を見ることが、まだ混乱している人の手助けになると期待しています。しかし、少なくとも前回のレッスンの一般的な事を理解していないと、今回のレッスンを全て理解することはできないと思います。
今回は、前回のレッスンで触れた関数について、いくつか異なった方法で機能を拡張してみます。前回は、関数を使用するときの基本について触れました。また、関数についての概念の説明も限られていました。ここでは、関数がもっと多方面で活躍できるようにいくつかの機能を追加してみたいと思います。
値を返す関数
ここまでは、我々が書いてきた全ての関数は、それ自体ある「アクション」を行ってきました。それは、何かを行いましたが、答えは返しませんでした。タイプをvoid型にすることで、その関数の種類を指定してきました。
おそらく、あなたが書く関数のほとんどは、アクションを行う関数になるでしょう。しかし、何かを計算する必要があるときが何回かあると思います。単純な例を挙げてみましょう。魔法使いのギルドがあると想定して下さい。彼らはプライバシーを重んじているので、その塔には魔法のポータルを通じてしかアクセスできません。そして、 ポータルはキャラクターのメイジクラスが主であるかどうかをチェックし、条件を満たした者だけ転送します。
この方法を実現するために、キャラクターのメイジクラスのレベルに応じたパーセンテージをチェックする関数を書きます。マルチクラスではない純粋なレベル1のウィザードの場合は、100%を返し、レベル2のファイターとレベル3のソーサラーのマルチクラスのキャラクター場合は、60%を返します。これらの場合は、塔に入ることを許可します。しかし、レベル7のローグとレベル2のウィザードのマルチクラスのキャラクターの場合は、22.22%しかないので入れてもらえず、冷たい風の中に置き去りにされることになります。
まず、関数を書いてみましょう。関数には入力としてオブジェクト (クリーチャー)を与え、パーセンテージを返すようにします。パーセンテージは、少数値とします。少数値はfloat型であることを思い出して下さい。よって、関数の宣言は以下のようになります。
float GetMagePercent(object oPerson)
実際、この関数で行われる計算はとても簡単なものです。まず、関数に入力として与えられたクリーチャーのトータルレベルが必要です。そして、同様にウィザード、あるいはソーサラーレベルも必要です。これらは、GetLevelByClass関数を使って簡単に得ることができます。その後、簡単な割り算を行ってパーセンテージを求めます。
それでは、「計算結果」をどのようにして関数に伝えればいいのでしょうか?簡単なことです。returnを使って値を伝えればいいのです。全体のコードを見てみましょう。
// この関数はクリーチャーのメイジ関連のレベルを、少数値で表した // パーセンテージで返します。 // 関数に与えられたオブジェクトがクリーチャーでない場合は、 // 0.0を返します。 float GetMagePercent(object oPerson) { int nTotalLevel = GetHitDice(oPerson); if (nTotalLevel == 0) // クリーチャーでない場合は、0.0を返します。 return 0.0; else // それ以外の場合は、パーセンテージを計算し、その結果を返します。 { int nMageLevel = GetLevelByClass(CLASS_TYPE_WIZARD, oPerson) + GetLevelByClass(CLASS_TYPE_SORCERER, oPerson); float fMagePercent = IntToFloat(nMageLevel)/IntToFloat(nTotalLevel); return fMagePercent; } }
スクリプトの大部分は、見慣れた形でしょう。新しいのは最後の行だけです。スクリプトの後半部分でfMagePercentを計算しています。最後の行は、関数呼ばれた時にその結果を返すという意味です。
補足: GetHitDice関数が0を返すとどうなるかについても、気にする必要があります。今回の関数を適切に使っていれば、それはあり得ないのですが、エラー時のためにも「チェック」を行った方がいいでしょう。間違って、武器オブジェクトが関数に入力として与えられるかもしれません。そうすると、「0で割る」というエラーとなり、大混乱を引き起こすことになります。
さて、書いた関数をどのように活用すればいいのでしょうか?それは、BioWareの関数を使うときとよく似ています。(繰り返しますが、自作の関数を使いたいときは、そのコード全体を使いたいスクリプトに記載する必要があります。) よって、配置用オブジェクトからポータルを配置(私は実際には、変化に富むように魔法の光を使いました。)し、その周りにトリガーを配置して下さい。ウェイポイントをどこかに配置し、タグをWIZTOWER_WPにします。
トリガーのOnEnterスロットに以下のスクリプトを記載して下さい。
// この関数はクリーチャーのメイジ関連のレベルを、少数値で表した // パーセンテージで返します。 // 関数に与えられたオブジェクトがクリーチャーでない場合は、 // 0.0を返します。 float GetMagePercent(object oPerson) { int nTotalLevel = GetHitDice(oPerson); if (nTotalLevel == 0) // クリーチャーでない場合は、0.0を返します。 return 0.0; else // それ以外の場合は、パーセンテージを計算し、その結果を返します。 { int nMageLevel = GetLevelByClass(CLASS_TYPE_WIZARD, oPerson) + GetLevelByClass(CLASS_TYPE_SORCERER, oPerson); float fMagePercent = IntToFloat(nMageLevel)/IntToFloat(nTotalLevel); return fMagePercent; } } void main() { object oPC = GetEnteringObject(); if (GetIsPC(oPC) && (GetMagePercent(oPC) > 0.50)) AssignCommand(oPC, JumpToLocation(GetLocation(GetWaypointByTag("WIZTOWER_WP")))); }
AssignCommand 関数の行を、まず、ウェイポイントの取得、次にロケーションの取得、そして、そのロケーションにジャンプするというように、数行に分けて記述するのも悪くないと思います。これはスタイルの問題であり、私はこれ位の関数の入れ子であれば簡単に追跡できるので、複数行に分けて記載する必要はないと思っています。
別の例
次に何かを計算する関数の別の例を示します。時々、私はPCに対して少しサディスティックになる時があります。私は、彼らが必要としている鍵を持っているインプを作りました。インプは、一定のゴールドがあれば鍵を売ってもいいと提案します。しかし、彼の要求する金額は、いつもパーティー全体が持っているゴールドより1ゴールド多い金額です。
よって、パーティ全体の持っているゴールドを調べる必要があります。お分かりの通り、結果は整数値です。あるPCの持ってるゴールドを調べる関数で、全てのPCをループします。そして、パーティメンバーが持ってるゴールドを合計していきます。
以下が私の考えた関数です。
// oPCと同じパーティである全てのPCの持っているゴールドの合計値を返します。 // 入力がPCでなければ、0を返します。 int GetPartyGold(object oPC) { if (!GetIsPC(oPC)) // PCでなければ、0を返します。 return 0; else // ゴールドを合計するためにループします。 { object oCharacter = GetFirstPC(); int nGoldCount = 0; while (oCharacter != OBJECT_INVALID) { if (GetFactionEqual(oPC, oCharacter)) // 異なったパーティはそれぞれ、異なったファクションを持っています。 nGoldCount = nGoldCount + GetGold(oCharacter); oCharacter = GetNextPC(); } return nGoldCount; } }
スクリプトをテストするために、NPCをモジュールに配置して下さい。そして、カンバセーションを作成します。カンバセーションはあなたが好きなように作成して構いませんが、あるポイントで鍵を売ってもいいという提案をする以下のカンバセーションノードを作成して下さい。
「もちろん、鍵は売ろう。人間よ。ほんの少しゴールドを持っていれば。」
そして、「テキストによる処理」タブに以下のスクリプトを記載します。
// oPCと同じパーティである全てのPCの持っているゴールドの合計値を返します。 // 入力がPCでなければ、0を返します。 int GetPartyGold(object oPC) { if (!GetIsPC(oPC)) // if not a PC, return 0 return 0; else // PCでなければ、0を返します。 { object oCharacter = GetFirstPC(); int nGoldCount = 0; while (oCharacter != OBJECT_INVALID) { if (GetFactionEqual(oPC, oCharacter)) // 異なったパーティはそれぞれ、異なったファクションを持っています。 nGoldCount = nGoldCount + GetGold(oCharacter); oCharacter = GetNextPC(); } return nGoldCount; } } void main() { object oTalker = GetPCSpeaker(); // 現在話しているPCを得ます。 int nPrice = GetPartyGold(oTalker) + 1; // パーティ全体のゴールドよりも1ゴールド多く設定します。 SetCustomToken(1000,IntToString(nPrice)); // カンバセーションのためのトークンを設定します。 }
SetCustomToken関数は、会話内だけに使われる興味深い関数です。基本的に、カンバセーションにどんな種類のテキストでも入れ込むことができます。我々が設定したテキストに注目して下さい。そのカンバセーションが「話される」時、トークン番号1000として設定されたテキストに置き換えられます。
つまり、最後の行は値段を文字列に変換して、それをトークン1000に保管しています。
カスタムトークンについての注意: カスタムトークンを使用する時は、その都度設定を行うのがベストです。設定した後から使うのは可能ですが、一般的に悪い方法とみなされています。また、トークン0から9についてはBioWareによって使用されており、それを使おうとするとカンバセーションが思ってもみない結果になる可能性があります。よって、それらの番号は避けるのがベストです。
デフォルト入力
以下に、多くのシチュエーションを網羅した簡単な関数を示します。
// レバー(または、他の配置用オブジェクト)を2つのポジション間で切り替えます。 // レバーのSTATE変数は、始めは1に設定され // レバーが使用されると0に設定されます。 // // 作成者 Celowin // 最終更新日: 7/23/02 // void FlipSwitch(object oLever = OBJECT_SELF) { // STATE 変数。オフの時は0で、オンの時は1になります。 int nUsed = GetLocalInt(oLever, "STATE"); if (nUsed == 0) { AssignCommand(oLever, ActionPlayAnimation(ANIMATION_PLACEABLE_DEACTIVATE)); SetLocalInt(oLever, "STATE", 1); } else { AssignCommand(oLever, ActionPlayAnimation(ANIMATION_PLACEABLE_ACTIVATE)); SetLocalInt(oLever, "STATE", 0); } }
基本的に、この関数は入力として配置用オブジェクト(例えば、レバー)をもらい、それの2つの状態オン・オフを切り替えます。
このスクリプトには、刺激的なことはありませんが、宣言における新しい概念が含まれています。
void FlipSwitch(object oLever = OBJECT_SELF)
「 = OBJECT_SELF」は何を行うのでしょうか? 基本的には、関数に何の入力もない場合は、OBJECT_SELFが入力されたと見なすと言う意味です。
よって、レバーにスクリプトを設定する場合には、関数を
FlipSwitch();
のようにスクリプトに書くだけです。すると、スクリプトが設定されたレバーの状態がアニメーションをして変わります。
反対に、他の状況(例えば、タンスが開いていた時)において、状態を変化させたい場合には、以下のように使うことができます。
oRemoteLever = GetObjectByTag("Lever6");
FlipSwitch(oRemoteLever);
必ずしもこのようなデフォルト入力を宣言する必要はありませんし、実際、私も扱うときには注意しています。しかし、正しい状況で使うことで、関数を多種多様に使うことができます。
別の例
以下にデフォルト入力の別の例を示します。私は私が作った冒険に、多くのホラーやミステリーを設定するのが好きです。大部分において、私は状況を上手く設定して、プレイヤーに何が起こっているかを推測させます。しかし、時々、昔ながらの血のりの演出が一番ふさわしい時もあります。それ故、私はI use the following function once in awhile:
// Kill a creature in an explosion of gore. // // oVictimはエフェクトの対象です。 // bAffectPlotがTRUEであれば、プロットフラグが設定されてる // クリーチャーにも動作します。 // // 作成者 Celowin // 最終更新日: 7/23/02 // void BloodExplode(object oVictim = OBJECT_SELF, int bAffectPlot = FALSE) { if ((!GetPlotFlag(oVictim) || bAffectPlot) && (GetObjectType(oVictim) == OBJECT_TYPE_CREATURE)) // エフェクトの対象がプロットフラグを持っていなければ、スクリプトが実行されます。 // bAffectPlotがTRUEであれば、スクリプトが実行されます。 // クリーチャーのみに動作します。 { // エフェクトを作成します。爆発と死のエフェクトです。 effect eBloodShower = EffectVisualEffect(VFX_COM_CHUNK_RED_LARGE); effect eDeath = EffectDeath(); // クリーチャーを殺せるようにします。 SetPlotFlag(oVictim, FALSE); // エフェクトを適用します。 ApplyEffectToObject(DURATION_TYPE_INSTANT, eDeath, oVictim); ApplyEffectAtLocation(DURATION_TYPE_INSTANT, eBloodShower, GetLocation(oVictim)); } }
この関数では2つの入力があります。爆発させたいクリーチャー(デフォルトではOBJECT_SELF)とプロットフラグが設定されているクリーチャーにエフェクトを適用したいかどうかのtrue/false(デフォルトではFALSE)の入力です。
よって、以下の宣言全ては、スクリプト内では全く同じものになります。
BloodExplode();
BloodExplode(OBJECT_SELF);
BloodExplode(OBJECT_SELF, FALSE);
( )内の入力は全て省略することができますが、2番目の入力を指定したい場合には、1番目の入力を省略できないことに注意して下さい。ややこしいので、例を挙げて説明します。
プロットフラグが設定されているかどうかに係わらず、攻撃したときにゾンビを爆発させたいと仮定します。すると、最初の入力はOBJECT_SELF (デフォルト)ですが、2番目の入力はTRUE (デフォルトではない)になります。2番目の入力を変更したい場合は、1番目のデフォルトの入力を指定する以外の方法はありません。可能なのは以下の宣言だけです。
BloodExplode(OBJECT_SELF, TRUE);
以下のように
BloodExplode(,TRUE);
と宣言すると構文エラーになります。
他に説明しておいた方がいいと思われる部分は、条件を扱った以下の行でしょう。
if ( (!GetPlotFlag(oVictim) || bAffectPlot) && (GetObjectType(oVictim) == OBJECT_TYPE_CREATURE) )
この表現は見慣れているにも関わらず、この行を見ると、頭を掻いて、再度見て、そして理解するのを諦めてしまいます。繰り返しになりますが、これはスタイルの問題です。私はif文を入れ子にするのが嫌いです。時々if文を入れ子にすることも必要ですが、そればかりやっているとコードが悪夢のように複雑になってしまいます。おそらく私の思考が、プログラミングというよりはむしろ数学的だからだと思いますが、私にとってはif文の入れ子を1つ1つ追跡するよりも、上の行のように書いた方がはるかに分かりやすいです。
さて、分析してみましょう。このスクリプトは、実際にいつ動作するのでしょうか?
クリーチャーだけに動作します。
プロットフラグが設定されていないクリーチャーに動作します。
プロットフラグが設定されているクリーチャーであっても、bAffectPlotがTRUEであれば動作します。
最初の条件は不変なものです。クリーチャーである必要があり、それ以外は重要ではありません。言い換えれば、クリーチャーである必要があり、かつ、他の条件がtrueである必要があります。&&は、論理演算子「and」です。よって、スクリプトはオブジェクトのタイプがクリーチャーで、かつ他の条件がtrueであるかどうかをチェックしています。
条件部分の一部です。
(!GetPlotFlag(oVictim) || bAffectPlot)
||は、「or」という意味です。どちらかがtrueであれば、全体がtrueとなります。bAffectPlotは我々が与える入力で、TRUE か FALSEです。trueであれば、特に心配することはありません。!GetPlotFlag(oVictim)は、oVictimがプロットフラグを持ってなれけば、trueとなります。(頭に ! を付けると、「逆」という意味になります。! を付けなければ、oVictimがプロットフラグを持っている場合に、trueとなります。)
まとめると、クリーチャーであって、かつ、クリーチャーがプロットフラグを持っていない、または、関数がプロットフラグを指定している場合ということになります。ややこしいですが、論理演算子の使い方を勉強すれば、スクリプトコードがよりすっきりしたものになるでしょう。
関数ライブラリ
繰り返しになりますが、自作の関数を使うためには、それを実際に使うスクリプトに記載しなければなりませんでした。それでも便利ですが、一種の制約でもあります。
特に、FlipSwitch関数とBloodExplode関数は、モジュール内のあらゆる場所で何回も使いたいと思うかもしれません。(私がそうであるように、ほとんどの人はBloodExplode関数は使いたくないと思うかもしれませんが、まあいずれにしろ) もちろん、カット・アンド・ペーストを何度も行えばいいのですが、もっと良い方法があります。
始める前に、一般的なことについて少し書きます。私は、 関数を多くの異なったスクリプトで使おうとするような場合のみ、ライブラリにします。もし一カ所でだけしか使わない場合には、関数全体を書いても気にしません。2つか3つのスクリプトで関数を使う場合には、おそらく必要なところにカット・アンド・ペーストすると思います。繰り返し使う関数だけをライブラリとして作成します。
先に進んで、モジュール内で使うために簡単なライブラリを設定してみましょう。新しいスクリプトを作成して、「tm_myfunctions」といったような名前にします。FlipSwitch関数とBloodExplode関数の両方をカット・アンド・ペーストして、スクリプトを保存して下さい。
スクリプトを保存するときに、NO FUNCTION MAIN() IN SCRIPTといったものが表示されますが、ここでは無視します。
ここからが見せ場です。これで、どんなスクリプトにおいても、スクリプトの頭に
#include "tm_myfunctions"
のように書くことで、その関数ファイルに保存した関数を使うことができるのです!
簡単な例を示します。レバーを引くと、最も近いゾンビが爆発するようにしたいとします。(ゾンビにはZOMBOMBというタグを与え、レバーにはZOMBLEVというタグを与えています。) スクリプト自体は簡潔です。
// tm_zomblev_ou // レバーに最も近く、タグがZOMBOMBのゾンビを破壊します。 // // 作成者 Celowin // 最終更新日: 7/23/02 // #include "tm_myfunctions" void main() { FlipSwitch(); if (GetLocalInt(OBJECT_SELF, "STATE") == 1) { object oZomb = GetNearestObjectByTag("ZOMBOMB"); BloodExplode(oZomb); } }
基本的に、#includeが行うことは、指定したテキストファイルをその場所に挿入することです。本質的には、「カット・アンド・ペースト」しているのと同じです。
ライブラリに関する注意
ライブラリは驚くほど便利ですが、いくつか注意しなければならないことがあります。
まず、言ったことの繰り返しになりますが、繰り返し使う関数だけライブラリとして作成して下さい。ライブラリが大きくなるほど、コンパイルにかかる時間は長くなります。大規模なモジュールでは、嫌なくらいの長さになります。
繰り返し使う関数だけをライブラリとして作成したとしても、結果的にライブラリが大きくなってしまうかもしれません。もしそうなった場合は、小さなライブラリに分けることを考えてみて下さい。クリーチャーのためのライブラリ、配置用オブジェクトのためのライブラリ、ドアのためのライブラリ、その他といったように分けたいように分けます。結果的に、使いたい関数がどのライブラリにあるかを知るための手助けにもなります。
他にも、include文は、ただテキストを挿入しているだけですので、スクリプト内で1つだけしか使えないという制約は全くありません。
#include "tm_creaturelib"
#include "tm_doorlib"
のようにしても、全く問題ありません。
最後に、多くのプログラム経験の無い人が忘れやすい重要なことついて述べます。ライブラリを修正したい場合には、そのライブラリを使用している全てのスクリプトを再コンパイルする必要があります。どのスクリプトがライブラリを使っているかについては、非常に忘れがちなことなので、これはとても不快な作業です。幸運にも、それを回避する方法があります。
ツールセットの中に、「ビルド」ボタンがあります。これは基本的に、「このモジュールの全てのスクリプトを再コンパイルしなさい」という意味です。それ故、ライブラリを修正したら、全てのスクリプトをチェックするために、すぐさま、ビルドボタンを押す必要があります。繰り返しになりますが、ライブラリにvoid main()が無いというエラメッセージが表示されると思いますが、そのエラメッセージだけが表示されている限りは、設定が上手くいっていることになります。
前進
このレッスンはとても長くなり、多くの細かい概念について述べてきました。私はこのレッスンを読んでいる人々に、このレッスンに書いてあることが全て分からなくても、自分自身を責める必要はないということを強く言いたいです。このレッスンに書かれていることは、確かにスクリプト作成の手助けにはなりますが、必ずしも必要ではありません。全く理解できなくても、それで世界が終わるわけではありません。
次のレッスンは、もし私が理解できれば、ヘンチマンについて説明しようと思います。実際、ヘンチマンを扱ったスクリプトが多くあるとは思ってないのですが、1日に50件もヘンチマンについての質問があるので参っています。
今後は、レッスンの形式を変えることにします。この時点で、あなたは一般的なスクリプトに必要な文法をほとんど理解しているので、これ以上私が説明できることはないです。代わりに、「スクリプト作成のプロセス」に焦点を当ててみようと思います。"
スクリプトのアイディア、設計上で考慮すること、スコープの制限、そして実装について説明します。(スクリプトのデバッギングについても触れるかもしれません。なぜなら、フォーラムのモデレーターたちは、私のデバッギング方法を理解してくれてないからです。)
author: Celowin, editor: Iskander Merriman, JP team: katsu794
Send comments on this topic.