Celowin - パート 6: ループ

前置き


この一連のレッスンの目的は、全くの初心者をプログラミングの世界に誘い、モジュールを作成するためにNWNのスクリプトをどのように使うかを教えることです。初めの 部分のレッスンはとても基本的なものになると思われるので、何かのプログラムを書いたことがある人は飛ばしても構わないでしょう。これらのレッスンの最終目的は、プログラムと聞いただけで身震いするような人々でも学べる場を提供することです。


これらのレッスンをフォーラムに掲示したり、印刷したり、修正を加えることは大いに構いません。しかし、その際には私が作成したということをどこかで触れて下さい。良い悪いを含めて、これらのレッスンについて何かコメントがあれば私に送って下さい。Celowin.


これらのレッスンは、オーロラツールセットをある程度触っている人を想定して書かれています。これらのレッスンにおいて解りにくい部分があるという意見が多数寄せられれば、必要な部分をさらに詳しく説明することも考えています。


今までのレッスンを見逃した人で、興味のある人は、BiowareのスクリプトフォーラムにあるScripting FAQ and Tutorialsを見て下さい。そこに全て置いてあります。[NWN Lexicon's Lyceumにも同様にミラーされていいます。].


警告の言葉


一連のレッスンに、今回の話題を一回分として掲載することに少し狼狽しています。全てはこの「基本のスクリプト」にいきつくと考えています。これまでの5つのレッスンで使われているテクニックは、 今までに作ってきたスクリプトにほどんど集約されています。しかし、今回のレッスンは、中間的な概念について説明するつもりです。それはとても重要なテクニックですが、今までのスクリプトでは使っていません。


私が心配をしている本当の理由は、このレッスンで初めて「危険なスクリプト」について述べるからです。確かに、スクリプトにおけるミスはよくありますし、問題があればスクリプトが思ったように動かないだけです。しかし、ループを使うスクリプトの場合は、本当に困った状態を引き起こす可能性を持っています。


幸運にも、BioWareは手に負えない状態にならないようにコントロールする方法を用意しています。それでも、私がこれから述べる問題について常に十分注意する必要があります。


ループとは何ですか?


簡単な言葉で言えば、ループとはスクリプトのある部分を何回も繰り返し処理させる方法です。まず、一回目の処理を行い、その後再び実行する必要があるかをチェックします。もしそうであれば、再びスタート部分に戻り繰り返し処理を行います。そして、繰り返しを必要としない条件になるまで、繰り返し処理を行い続けます。


ループ処理を書く上でのキーとなる部分は主に、それを何時使うか、そして、ループが正しいタイミングで終わるようにどのように「チェック」の設定を行うかです。特にチェックは重要です。もし間違ったチェックを行ってしまうと、ループを決して「抜ける」ことなく、エラーになるまで同じ処理を延々と繰り返す可能性があります。


ループには2つのタイプがあります。「for ループ」と「while ループ」です。まず最初に頭に浮かぶ質問は、「この2つのタイプをいつどのようにして使い分けするのですか?」ということでしょう。それは簡単なことであり、100%正確なルールを述べることができると思います。しかし、実際は、どのタイプのループを使えばいいのかを分かるためにはいくらかの経験が必要と言えます。また、全てのforループは、whileループでも書けるので、「何故2つもあるのですか?どっちを使えば十分なのですか?」というように困惑してしまいます。


しかし、一般的なルールの指標となるものがあります。このルールは完璧ではありませんが、大抵のケースをカバーできます。ある回数分処理を行わせたいのなら、for ループを使います。処理が何回行われるか知る術がない場合は、while ループを使います。それぞれのタイプについて個別に見てみましょう。


For ループ


for ループのキーポイントは、ループを通じて記録される「インデックス」変数です。


基本的な構文は以下のようになります。

int nIndex;
for (nIndex = 1; nIndex <= 7; nIndex++)
{
  ある処理;
}

少し複雑ですね。それぞれの部分を見てみましょう。


まず、インデックス変数を定義する必要があります。(C言語のプログラマーは、ループ定義の中に変数を定義する習慣があるかもしれませんが、NWNスクリプトでは前もって宣言する必要があります。) 初期値を与える必要はありませんが、変数を設定する必要があります。


for ループには3つの項目があります。最初の(nIndex = 1)は、変数の初期化です。既に変数を設定(宣言')したので、その変数に初期値を与えています。


2番目の(nIndex <= 7)は、条件と呼ばれています。ここで実際にループをいつまで「実行」するかを決定しています。 この条件にマッチしている限り、ループ処理は実行されます。始めから再びループするかどうかを毎回チェックして、条件がtrueでなくなると速やかにループを「終了」させます。よって、今回の場合、何回かのプールを経たあとnIndexは8という値になり、ループは終了します。


最後の(nIndex++)は、毎回のループ処理の最後にスクリプトが行う処理です。「++」については、以前のレッスンで簡単に説明しました。ここでは、変数nIndex1ずつ増加させます。この部分は、nIndex = nIndex + 1のように置き換えることもできます。しかし、一般的に1ずつ増加させるケースが最も多いので、ほとんどのスクリプト経験者は「++」を使うことを好みます。使うには少し慣れが必要ですが、スクリプトコードをすっきりさせることができます。


結局のところ、ループ内ではどんなことが起こっているのでしょうか?まず、nIndexが1としてスタートします。17以下なので、ある処理の部分が実行されます。そして、最初の繰り返しが終わり、nIndex2に増加します。それから、条件部分に戻ります。2は、まだ7以下なので、ある処理の部分が再び実行されます。2回目の繰り返しが終わり、nIndex3に増加します。この処理を繰り返しながら、nIndex4, 5, 6, 7と増えていき、最終的に8になります。この時点の条件チェックで、nIndex7より大きいので、ループは終了します。


For ループの例


ちょっとややこしくなってきたので、具体的な例を見てみましょう。チェストを壊そうとするキャラクターを罰してみることにします。よって、壊された時に5体のゾンビと5体のスケルトンを召喚するタンスを作成したいと思います。CreateObject関数を10回呼び出すために、ループを使います。(注意: テストするときには、多くのゾンビとスケルトンの攻撃に耐えられるPCが必要だと思います。もし用意できない場合は、呼び出すクリーチャーをニワトリと平民にするか、攻撃に耐えられるPCを作成した方がいいでしょう。)


  1. モジュール内に、タンスを配置します。
  2. プロパティの「錠」タブで、「施錠されている」をチェックし、DCを100にします。
  3. スクリプトタブを選択し、「OnDeath」スロットに以下のスクリプトを設定します。
    // OnDeathスロットに設定するスクリプト: tm_chest_dt
    //
    // このスクリプトは、タンスを破壊したPCの近くに5体のゾンビと
    // 5体のスケルトンを召喚します。
    //
    // 作成者 Celowin
    // 最終更新日: 7/13/02
    //
    void main()
    {
      // 初期化: タンスを破壊したPCのロケーションを得ます。
      // ループの設定をします。
      object oPC = GetLastKiller();
      int nUndeadIndex;
      location lSpawn = GetLocation(oPC);
      
      // 5回ループします。
      for (nUndeadIndex = 1; nUndeadIndex <= 5; nUndeadIndex++)
      {
        // 毎回のループにおいて、PCのロケーションにゾンビ一体とスケルトン一体を作成します。
        
        CreateObject(OBJECT_TYPE_CREATURE, "nw_zombie01", lSpawn, TRUE);
        CreateObject(OBJECT_TYPE_CREATURE, "nw_skeleton", lSpawn, TRUE);
      }
    }

モジュールを保存してテストしてみて下さい。チェストを破壊すると、突然あなたのキャラクターはアンデッドの大群に囲まれます。簡単ですよね?



先ほどのスクリプトを少し修正してみましょう。5体ずつのスケルトンとゾンビを召喚する代わりに、ゾンビだけを召喚してみましょう。しかし、召喚するゾンビの数はPCのレベルに等しい数にしてみます。ほんんど一緒なので、スクリプト全体は書きません。必要な変更箇所は以下のとおりです。


  1. 「object oPC」から始まる行の下に、次の行を追加します。int nPCLevel = GetHitDice( oPC );
  2. 追加した行を別々の行に分けたいと思うかもしれません。セミコロンが行の終わりに付いている限りは大丈夫です。
  3. そして、ループの設定で、2番目の部分をnUndeadIndex <= nPCLevel; に変更します。
  4. スケルトンを召喚する行をコメントアウトして、必要であればコメントを更新します。これで修正は完了です。

モジュールを異なったキャラクターで2、3回テストして、召喚されるゾンビの数がどのように変わるか確認してみて下さい。


For ループのまとめ


forループを使う場合のキーポイントは、for文内の3つの部分を理解することです。初期化はスタート時に一回行われます。条件は初回を含め、毎回のループでチェックされます。trueであればループ部分が繰り返され、そうでなければ、ループが終了します。3番目の部分は、それぞれのループ処理の最後に実行されます。


あなたが作成したループが間違いなく終わるとは断言できません!もし、条件をnUndeadIndex > 0 に変えたらどうなるでしょうか?変数がいくら増加しても、条件はいつもtrueです。すると、スクリプトはエラーが起きるまでゾンビを召喚し続けるでしょう。ゾンビがどこまで増えるか見るのは楽しいかもしれませんが、そのようなエラーはどんな犠牲を払っても回避すべきです。従って、ループを作成するときは、常に構文のダブルチェックをして下さい。実際にループがどの時点で終わるかを、明確に把握しておく必要があります。


While ループ


While ループは、forループよりもいくらか分かりやすいです。しかし、条件が確かではないので、より使うのは難しいです。以前に、ループを何回行えば良いのか分からない時に、大抵whileループが使われると言いました。少しややこしいので、説明をさせて下さい。エリア内を走っているヘルハウンドの群れがいると想定して下さい。ある祭壇が壊された時に、全てのヘルハウンドが突然死ぬようにしたいとします。この場合の問題は、PCがヘルハウンドを殺すかもしれないので、何体のヘルハウンドが存在しているかが確実に分からない点です。この問題のため、祭壇は破壊されるまで、ヘルハウンドを召喚し続けるでしょう。そして、周りは多くのヘルハウンドで溢れることになるでしょう。

また、実際に「ヘルハウンドを殺す」命令を何回行えばいいのかが分かりません。1回も行わないかもしれませんし、50回行う必要があるかもしれません。そこでwhile ループの出番となります。基本的には、「全滅するまでヘルハウンドを殺し続けなさい」、言い換えれば、「ヘルハウンドが存在している限り、一体ずつ殺し続けなさい」ということを言いたいのです。


while ループの基本的な構文は以下のとおりです。

while (条件)
{
  ある処理;
}

正に単純明快です。スクリプトはwhile ループの条件をチェックします。もしtrueであれば、内部の処理が行われます。そして、条件を再びチェックします。まだtrueであれば、処理を繰り返します。条件がtrueでなくなるまで、処理が繰り返されます。単純ですよね?実際、多くの面で for に似ており、同じ3つの部分を持っています。変数の初期化と更新を自分自身で確認して書く必要があるだけです。


ヘルハウンドについては後ほど触れますので、まず、より単純なものから挑戦してみましょう。PCがクエストを完了したときに、その苦労に対して経験値を与えるのは一般的になっています。PCが一人であれば簡単です。しかし、マルチプレイをしている時はどうでしょうか?どのようにしてそれぞれのPCに確実に報酬を与えればいいのでしょうか?



あなたがこれまでのレッスンをずっと辿ってきているのならば、何人かのNPCが作成されていると思います。(そうでなければ、PASSRINGというタグのリングを作成し、何人かのNPCを作成して下さい。) NPCの中から1人を選んで、簡単な会話を作成します。(あなたがカンバセーション・エディタの使い方を知っていると仮定しています。)


(ルート) 指輪を手に入れましたか?
|-(PCの返答1) いいえ。
| |- ダイアログ終了
|
|-(PCの返答2) はい。
  |- (NPCの返答) ありがとう。

(実際には会話をもう少し膨らましたいと思いますが、あなたの文章作成能力のためにそのままにしておきます。)


PCの返答2の「テキスト表示の条件」で、スクリプトウィザードを使います。条件の中の「所持品」をチェックします。そして、新しいタグにPASSRING を追加します。


NPCの返答「ありがとう」の「テキストによる処理」タブのスクリプトを編集して、以下のスクリプトを書きます。


// 会話スクリプト: tm_guard_c1
// 話しているPCからリングを取り去り、
// 全てのPCに経験値50を与えます。
//
void main()
{
  object oPC = GetFirstPC();
  DestroyObject(GetObjectByTag("PASSRING"));

  while (oPC != OBJECT_INVALID)
  {
    GiveXPToCreature(oPC, 50);
    oPC = GetNextPC();
  }
}

スクリプト、カンバセーション、そしてモジュールを保存して下さい。そして、テストしてみて下さい。本当に適切に動作するかどうか確認するためには、手伝ってくれる友達が必要ですが、自分自身だけでも基本動作は確認できます。


今回はループの基本的な勉強をしなければならないので、DestroyObject関数については触れません。まず、( oPC = GetFirstPC() )で初期化を行っています。( oPC != OBJECT_INVALID )という条件を設定して、( oPC = GetNextPC() )で変数を更新しています。


つまり、このスクリプトは、OBJECT_INVALIDとなるまで、つまり、経験値を与えるPCがいなくなるまでループして、それぞれのPCに経験値50を与えます。


繰り返しになりますが、while ループを書くときには、最終的にループが終わる設定になっていることを必ず確認して下さい。無限ループになるとやっかいです。


演習


ふーむ。私はヘルハウンドのスクリプトを作成しようと思っていましたが、考えれば考えるほど、やらなければならないという気持ちが薄れてきました。実際、基本的には、スクリプト作成に必要なテクニックはこのレッスンのどこかで使われています。どのスロットを使えばいいのか、スクリプトにどんな関数を使うのか、材料は全てあります。よって、あなた自身でやってみて下さい。祭壇と何体かのヘルハウンドをモジュールに作成して、祭壇を破壊するとヘルハウンドが全て死ぬようにしてみて下さい。(祭壇が破壊されるまで、ヘルハウンドを召喚し続ける場合は、難易度は中くらいです。一定のレベルを超えないようにヘルハウンドの数を確認する場合には、難易度がアップします。)


別の演習: トリガーを作成し、トリガーに入った時にモジュール内のPCの数の2倍のクリーチャー(どんな種類でもいいです)を召喚するように設定して下さい。


終わりに


レッスンを進めるにつれて、みなさんからの反応がだんだん少なくなってきています。みんな役に立つと思っているのでしょうか?まだこのレッスンで学んでいるかどうか是非教えて下さい。私を満足させるのは、クールなモジュールを作るために私のレッスンが役に立っているという情報だけです。


実際、私にとって最もほめ言葉となるのは、みんなが学んできた結果を私に見せてもらうことでしょう。私が扱ってきたアイディアを使って、クールなスクリプトを書いて、その作品を私に見せて下さい。レッスン4終了後、ある人が正に私を打ち負かすような、ダーツプレイヤーのスクリプトを拡張したスクリプトを見せてくれました。私は、その人のスクリプト作成の手助けになれてとても良かったと思っています。


とにかく、あたなのスクリプト作成がうまくいっているかどうか情報を下さい。特になければ、私が与えた演習について理解できたかどうかを教えて下さい。





 author: Celowin, editor: Charles Feduke, additional contributor(s): Jotham, JP team: katsu794
 Send comments on this topic.