Celowin - パート 3: 条件
前置き
この一連のレッスンの目的は、全くの初心者をプログラミングの世界に誘い、モジュールを作成するためにNWNのスクリプトをどのように使うかを教えることです。初めの 部分のレッスンはとても基本的なものになると思われるので、何かのプログラムを書いたことがある人は飛ばしても構わないでしょう。これらのレッスンの最終目的は、プログラムと聞いただけで身震いするような人々でも学べる場を提供することです。
これらのレッスンをフォーラムに掲示したり、印刷したり、修正を加えることは大いに構いません。しかし、その際には私が作成したということをどこかで触れて下さい。良い悪いを含めて、これらのレッスンについて何かコメントがあれば私に送って下さい。Celowin.
これらのレッスンは、オーロラツールセットをある程度触っている人を想定して書かれています。これらのレッスンにおいて解りにくい部分があるという意見が多数寄せられれば、必要な部分をさらに詳しく説明することも考えています。
イントロダクション
このレッスンでは、スクリプト作成においてもっとも重要であろうと思われる部分について触れます。それは、ある条件にマッチした時だけコードを実行する方法です。私がフォーラムを見ている限りにおいて、質問の90%位はこのテーマに落ち着くと思います。
レッスン2の最後のスクリプトでこの方法を用いましたが、まだ説明はしていませんでした。私がこのレッスンの最後で紹介したいスクリプトは、ちょっと複雑です。従って、まず概念について少し説明しようと思います。
基本的な条件または、「if文」の構文は以下の通りです。
if ( 条件 ) { ある処理; 他の処理; 別の処理; }
(上記は、本当のスクリプトではありません。ただ一般的な考え方を示しているだけです。この種のにせの関数を用いたスクリプト例を、一般的に「疑似コード」と呼んでいます。)
基本的に、「条件」にマッチした場合、スクリプトは{ }に囲まれた部分の処理を実行します。条件が真でなければ、何も行いません。(構文上の注意:初心者は、if ( 条件 )の後にセミコロンを付けるという間違いをしがちです。セミコロンを付けることはできません。)
条件部分には、スクリプトの実行に影響を及ぼす数多くのパターンの条件を指定することができます。このレッスンでその全てをカバーできるとは思っていません。私自身、使える関数を全て知っているかどうか怪しいものです。ここでは、他の人が書いたスクリプトから学ぶことができるように、基本を十分理解することを目標とします。
ほとんどの条件では、比較が使われます。例えば、レッスン2の最後のスクリプトでは以下のような比較を使いました。
if (nCount == 1)
「==」が、2つの等号であることに注目して下さい。レッスン2においては、1つの等号は変数を「設定」するために使っています。2つの等号は、「両側のものが同じであるか?」という問いをします。 (比較)
上の文を見る限りでは、両側は全く異なったものに見えます。一方はテキストで、もう一方は数字です。これらが同じであるかと言う質問がどうしてできるのでしょうか?
ここで、「nCount」は変数であり、実際は数字と等しいことを思い出して下さい。よって、全ては「nCount」に保管されている値に依存するということです。もし「nCount」が1という数字を保管していれば、条件は真となり、指定された処理が実行されます。もし「nCount」が7という数字を保管していれば、条件は偽となり、何の処理も行われません。
条件の実行をテストするポピュラーな方法として、あらかじめ定義された関数を利用するという方法があります。次のスクリプト例で行ってみましょう。
スクリプト例
簡単な条件文を含んだスクリプトを作ってみましょう。スクリプトの中でいくつかの新しい関数を使っていますが、それについては後で説明することにします。まず、スクリプトを実行させるための準備をいくつか行います。
- ツールセットで、テストモジュールを開きます。
- 新しいエリアを作って、タイルセットを森林地帯(forest)、大きさを縦4、横 2に設定して、「Test Area 002」という名前を付けます。
- エリアの端にスタート地点を配置します。
- もう一方の端に、NPCを配置し、シンプルにコモナーに設定します。
- NPCのタグをGUARDに変更します。
- NPCのスクリプトタブを選択し、デフォルトで設定されている全てのスクリプトを削除します。(NPCが望んでいない行動をすることを防ぐためであり、次のレッスンではこのステップを省略できると思います。)
- OnPerceptionスロットに以下のスクリプトを設定します。
object oSeen = GetLastPerceived(); void main() { if (GetIsPC(oSeen)) { ActionSpeakString("こんにちは、友よ。"); } }
- いつもの標準化の名前を用いて、tm_guard_op (opはOnPerceptionを表しています)という名前で保存します。
- Okを押してNPCのプロパティウィンドウを閉じ、モジュールを保存します。そして、モジュールをテストしてみて下さい。
ガードの方へ走ってみます。ガードの近くまで行くと、ガードは挨拶をします。ガードから十分離れたあとに戻ってくると、ガードは再び挨拶をします。しかし、ガードの近くに止まっていると、ガードは何も話しません。
Q&A方式でスクリプトについて説明したいと思います。
- 別の新しいイベントスロットが出てきましたね? OnPerceptionスロットは何をするのですか?
このスロットに設定されたスクリプトは、NPCがゲーム中に何かに気づいた時に実行されます。透明なもの、隠れているもので、NPCがそれに気づかない時には、スクリプトは実行されません。
PCを見た時にNPCに反応させたいので、このスロットを使っています。
- 最初の行にあるobjectというのは一体何ですか? 前回main関数の前に何かを書いた時もよく分からなかったのに、また同じことをしているじゃないですか!
新しい「データ型」を使っていることを除いては、前回と同じことをしています。前は、変数に整数を保管しました。今回は、オブジェクトを保管しています。
I前にも言いましたし、これからも何度も言うかもしれませんが、ゲーム内のほとんどの物はオブジェクトです。NPC、プレイヤー、アイテム、ウェイポイント、配置用オブジェクトなど、これらは全てオブジェクトです。ゲーム内の多くの関数は、オブジェクトが何であるかを得るために書かれ、他の多くの関数はオブジェクトを操作するために書かれています。
従ってここでは、oSeen(oで始めているのは、それがオブジェクトであることを連想させるためです)という一時変数に値を保管しています。
- GetLastPerceived()の行はどういう意味ですか?
これはBioWareによって書かれた関数です。「Get」で始まる関数は、ある種のデータを返します。ほとんどの関数は説明的な名前を持っています。この関数は、NPCによって最後に見られたオブジェクトを返します。
(注意:この関数は基本的にOnPerceptionスロット以外では使用しないで下さい。)
まとめると、最初の行は、NPCが最後に見たオブジェクトをスクリプトで参照できるように設定しています。
- 比較の説明に散々時間を掛けたのに、if 文の( )の中にはたった1つの項目しかないじゃないですか!これはどういうことですか?
これは、GetIsPC()関数特有の表現です。この関数は、オブジェクトを入力情報として、それがPCであればTRUE、そうでなければFALSEを返します。
しかし、条件に必要なのはこれだけでいいのです!PCであれば、割り当てられた行の処理が実行されます。PCでなければ、何も実行されません。望むならば、より比較らしく見せるために、以下のように変更することもできます。
if (GetIsPC(oSeen)==TRUE)
- if文のチェックはこれで全てですか? NPCが気づくのはいつもPCとは限らないのでは?
この小さなテストモジュールでは大丈夫です。ガードが気づくのが、PC以外に何もないからです。
このレッスンでは、実際のモジュールで通用するようなスクリプトを書いていこうと思っています。敵対的なゴブリンが近くに来たらどうなるでしょうか?ガードに「友よ」とは 言わせたくないですよね。
友好的なNPCであっても、話す気がしないPCが話しかけてきたら迷惑じゃないでしょうか?
- あなたの言いたいことは分かりました!全てのPCが友好的だとは限らないということですね!
良い指摘です。モジュールを少しいじってみましょう。
ガードスクリプトの変更
ここでは、ガードが特定の指輪を持っていないPCを見たときに攻撃するように、モジュールに修正を加えます。PCがその指輪を持っている時だけ、友好的な挨拶をするようにします。
- ツールセットを起動して、テストモジュールを開きます。そして、Test Area 002を開きます。
- 始めに、指輪を作ります。「アイテムをペイント」、「その他」、「装飾品類類」、「リング」、「カッパーリング」を選択して、モジュールのスタート地点の近くに配置します。
- カッパーリングのプロパティを開いて、タグをPASSRINGに変更します。
- 必要であれば、リングの名前を編集したり、説明を加えたりしても構いません。このスクリプトでは、タグだけが重要です。
- ガードのプロパティを開いて、スクリプトタブのOnPerceptionスロットのスクリプトを以下に変更します。
// 「味方か敵か」スクリプト: tm_guard_op // このスクリプトはガードのOnPerceptionスロットに設定する必要があります。 // // ガードはPCがpassringを持っているかどうかをチェックし、持っていなければPCを攻撃します。 object oSeen = GetLastPerceived(); object oRing = GetItemPossessedBy(oSeen, "PASSRING"); void main() { // ガードが見たものがPCでなければ、何も行いません。 if (GetIsPC(oSeen)) { if (oRing == OBJECT_INVALID) { // PCがリングを持っていなければ、PCを攻撃します。 ActionSpeakString("死ね、不法侵入者め!"); ActionAttack(oSeen); } else { // PCがリングを持っていれは、友好的に接します。 ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING); ActionSpeakString("こんにちは、友よ。"); } } }
スクリプトには新しい関数がいくつか使われています。始めは少し戸惑うかもしれませんが、基本的な構造が理解できれば、これらの関数を自在に使えるようになります。
ともかく、モジュールを保存して、実際に何が起きるかテストしてみて下さい。始めに、地面からリングを拾ってガードに近づいてみて下さい。ガードは友好的な態度を示すでしょう。次に、ガードから離れてリングを捨てます。そして、再びガードに近づいてみて下さい。今回は、ガードがPCを攻撃するでしょう。
解説
- また別の初期化をしていますね!もううんざりです!
私が言えるのは、私を信じて下さいということだけです。この初期化がなければ、スクリプトは悪くなってしまいます。そこで、新しい初期化が行われている行を見てみましょう。
object oRing = GetItemPossessedBy(oSeen, "PASSRING");
この行は、オブジェクトを保持する一時変数を設定しています。GetItemPossessedBy()関数は、2つの入力情報を必要とします。1つ目は、アイテムを持っているかどうかを調べたいクリーチャーオブジェクト、2つ目は、調べたいアイテムのタグです。
NPCによって気づかれたPCは、既にoSeenとして定義していますので、oRingはそのPCが持っているPASSRINGというタグのリングを指しています。
- PCがリングを持っていない場合はどうなりますか?
それでもGetItemPossessedBy()関数は実行されます。その場合は、OBJECT_INVALIDを返します。「そんなものはない」という意味です。
- 「//」で始まる行は何ですか?スクリプトというよりは、英文ではないですか?
「//」で始まる行は全て、スクリプトでは無視されます。それらは、「コメント」と呼ばれています。親切なスクリプト作成者は、そこでどんな処理を行っているかをコメントに記述しています。
コメントを書くのはあなた自身のためにもなります。スクリプトを書いたときには完璧に理解しているかもしれませんが、1ヶ月後にスクリプトを修正したいと思った時には、その全てを憶えていない可能性もあるからです。
あなたが書いたスクリプトを他の誰かが見る時に、コメントが書いてあれば親切だといえます。あなたが何をしようとしているかを細かく書けば書くほど、他の人にとっては理解しやすくなります。
- main関数のスクリプトは、とても複雑に見えます。{ }で囲まれた部分が4つもあります!どのようにしてそれら全てを覚えればいいのですか?
確かにそうですね。スクリプトが複雑になるほど、このような「入れ子」文が多くなります。見やすくする方法の1つとして、インデントを使うという方法があります。
空白行を入れることによって、スクリプトを「ブロック」単位に分ける人もいます。このスクリプトでは余り効果があるか分かりませんが、私も多少そのようにしています。コメントでブロック分けするのもいい手でしょう。
これらの対策をしても、まだ複雑であることに変わりはないかもしれません。「読む練習を重ねた方がいい」と言う以外は、他に適当な言葉が見つかりません。
- elseは何ですか?
elseは、if文の追加形式です。構文は以下のようになります。
if ( 条件 ) { 処理1; 処理2; } else { 代わりの処理1; 代わりの処理2; }
基本的に、「if」部分が実行されなければ、「else」部分が代わりに実行されます。
スクリプトのif文全体を解析してみましょう。まず、一時変数oRingがOBJECT_INVALIDと等しいかどうかを調べます。(すなわち、PCがリングを持っていないかどうかを調べます。) もし持っていなければ、PCを攻撃します。
反対に、PCがリングを持っている場合は条件はtrueではないので、「else」部分が実行され、友好的な態度を示します。
多くの人にとって、このようにスクリプトにおいてロジックを追跡することは難しいことです。もっと視覚的にとらえたいというならば、フローチャートを作成すると分かりやすいです。
- 新しく追加したアクションは何ですか?
関数名が説明的なので幾分か分かると思います。ActionAttack関数は、入力情報に与えたオブジェクトを攻撃させます。 ActionPlayAnimation関数は、NPCにあるアニメーションをさせます。今回は、ANIMATION_FIREFORGET_GREETINGというアニメーションを指定しており、これはただNPCに手を振らせるアニメーションです。
もう一工夫
すでにこのレッスンは長くなりましたが、もう1つ触れておきたいことがあります。(わかりました。私は嘘をついてました...。本当は5,217個位言いたいことがあるのですが、なるべくまとめたいと思います。)
ガードに反対の行動をさせたい場合はどうすればいいのですか? PCがリングを持っていれば攻撃して、そうでない場合は友好的に接するようにしたい場合は? 例えば、リングが盗まれた物かもしれないとか?
if文のブロックを間違いなく入れ替えれば、正しく動作するでしょう。しかし、実際は、スクリプトを以下のように1文字変えるだけで実現できます。
if (oRing == OBJECT_INVALID)
を
if (oRing != OBJECT_INVALID)
に変えます。「!=」は、別の比較演算子です。両方が等しくないかどうかをチェックします。したがって、今回は、PCがリングを持っていれば、ガードはPCを攻撃するということになります。
結論
このレッスンでは多くの新しい手法が出てきてます。しかし、これらをマスターすれば、スクリプトの真のパワーを解き放ったと言ってもいいでしょう。特に、ローカル変数と条件を組み合わせることは、あらゆることができる可能性を秘めています。
分からないことがあれば、いつでも気軽に質問して下さい。出来る限り全ての人に分かるような形で説明したいと思いますが、今までにスクリプトを扱ったことがない人にとっては複雑に感じるかもしれません。私にとって明らかであることでも、他の人にとっては難しいものもあるからです。
レッスン4はもっと簡単になるでしょう。間もなくお披露目できると思います。興味がある人のために話しますと、次のレッスンでは、ユーザー定義イベントについて説明し、それを使ってBioWareによって書かれた複雑なスクリプトを削除せずにNPCにスクリプトを割り当てる方法を述べます。
author: Celowin, editor: Iskander Merriman, JP team: katsu794
Send comments on this topic.