本書はTADS製作者用手引きの一部です。
Copyright (C) 1987, 1996 by
Michael J. Roberts. All rights reserved.
この手引きはN. K. Guy、tela designによってHTMLに変換されました。
第3章
Text Adventure Development Systemはゲーム製作者に多芸かつ強力な、テキストアドベンチャーゲームの基礎を成す模型化された世界の造形に最適の言語を用意しています。
TADS言語はCを基にした強力なオブジェクト指向言語です。TADSはCと同じキーワードと演算子のほとんどを使用しますが、使いやすさを向上させるために変更点が若干あります。TADSはまた、「ランタイム・タイピング」を使用しますが、それにより変数、関数、プロパティーのデータ型をあらかじめ宣言する必要がありません。さらに、TADSはリストやストリングのような高レベルのデータ型も持っていて、それがメモリ管理を全体的に自動化してくれます。
この章は言語の特徴全体の概観を提供します。
関数はCやPascalのような言語の利用者にはおなじみでしょう。関数は少量のコードがグループ化され名前を与えられたものです。プログラムの他の部分は関数を呼び出してそのコードを実行させることができます。これはTADSにおける関数の一例です:
showSum: function( arg1, arg2, arg3 ) { "The sum is: "; say( arg1 + arg2 + arg3 ); "\n"; }
この関数はshowSumといいます。関数名は文字と数字のどんな組み合わせでもかまいませんが、文字から始まっていなければなりません。TADSでは大文字と小文字は区別されるので、showSumはshowsum や ShowSumとは別個の関数になります。
大かっこは関数の始まりと終わりを示します。中にあるコードは関数の本体であり、呼び出される度に実行されます。
かっこ内のアイテム、arg1、arg2、arg3は関数の引数です。中には引数をまったく持たない関数もあります。そういう関数の場合、かっこと引数リストはありません。関数が呼び出されるとき、数値が引数に対応して指定されます。たとえば、次のようなコードが実行されると:
showsum( 1, 2, 3 ); showsum( 9, 8, 7 );
関数は二度呼ばれます。まず、arg1、arg2、arg3はそれぞれ、1、2、3です。二回目は9、8、7です。その出力はこうなります:
The sum is: 6 The sum is: 24
引数は関数にとって完全に「ローカル」なものです。arg1は関数showSumの内部でだけ意味を持ちます。関数はlocal構文を使って独自のローカル変数を定義することもできます。これは後で詳しく述べます。
関数は三つの数の合計を表示することよりはるかに高度なことができます。if構文によって条件付きの実行が可能になりますし、while構文でプログラムを繰り返し実行させることもできます。TADSは完全な整数演算を備えていて、文字列とリスト(間もなく説明します)についての演算も提供します。さらに、関数はそれを呼び出したものへ値を返します。そして、もちろん、ある関数は別の関数(または自分自身でさえ)を呼ぶことができます。手短に言えば、CやPascal、BASICで記述できたプログラムのほとんどはTADSの関数で書くことができるわけです。
オブジェクトはすべてのTADSゲームにとって最も重要な要素です。テキストアドベンチャーがモデル化する現実世界の実在物の一つ一つはTADSのオブジェクトによって記述されるのです。
注意:「オブジェクト」はTADSプログラミングの文脈中特定の意味を有し、テキストアドベンチャーにおける通常の使われ方とは異なります。ゲームを遊ぶ時は、オブジェクトのことを拾って操作できるものととらえます。TADSプログラムにおいてそれらのものは本当にオブジェクトとして表現されているのですが、プレーヤーが直接操ることはできない部屋、登場人物、動詞、その他多くのものも存在するのです。
TADSオブジェクトはCの構造体やPascalのレコードのようなもので、操作上の便宜のために単一の名前の下に集められた関連する(数と文字列のような)データの集合です。オブジェクトはオブジェクトに関連したデータアイテムであるプロパティーを持っていますが、メソッドという、オブジェクトと結び付いた少量のコードも有しています。メソッドはきわめて関数に近いものです。その違いは、メソッドはオブジェクトの一部であるのに対し、関数は独立していて、いかなるオブジェクトの一部ではないということです。
次の例では、一つのオブジェクトについてメソッドではなくプロパティーだけが定義されています。
robot: object name = 'Lloyd' weight = 350 height = 72 speed = 5 ;
これはrobotをオブジェクトとして定義し、プロパティーのリストとその値を指定しています。name = 'Lloyd'という行はnameというプロパティーが値として'Lloyd'という文字列を持つことを述べています。それと同様に、weight = 350はプロパティーweightが数値の350を持つこと、等々です。(TADSで使用できるのは整数だけだということに注意してください。3.1415926のような浮動小数は使えません。TADSが許容する最大の数は約20億です。)セミコロンはオブジェクト定義を終わりにします。
オブジェクトのプロパティーを得るには、オブジェクトの名前、次にドット、そしてプロパティーの名前、を用います。たとえば、robot.nameはオブジェクト robotのnameプロパティーを指します。下の関数はあるオブジェクトのnameプロパティーを表示します。
showName: function( obj ) { say( obj.name ); }
この関数の一つしかない命令は他の関数sayをobj.nameという引数を供えた上で呼び出します。sayはあらかじめ備わっている関数で数または文字列を表示します。(sayは呼び出された時点でどのタイプのデータを表示すべきか決定し、適切に行動します。組み込み済み関数の多くは送られてきたデータのタイプに基づいて自らの機能を果たします。)
robotにおいて、私たちはある数と文字列を定義しました。TADSにはもう一つ、ダブル引用符で挟まれる特殊な文字列が存在します。この種の文字列は参照されるたびに自動的に表示されます。これは便利な速記法です。なぜなら、ある文字列を表示させたいときに、その都度sayを呼び出す手間を省いてくれるからです。テキストアドベンチャーゲームは大量の文章を表示するので、この特長がとても便利なことが証明されます。
newobj: object greeting = "Hello!\n" ;
文字列の最後にある\nは新しい行を出力します(つまり、カーソルを新しい行の初めに移動します)。これに似た\bというコードは、空の行を出します。\nを二つ使えばいいのに、どうして\bが要るんだろうと思われるかもしれません。その理由は、TADSが余分な改行を無視するということです。ほとんどの場合、これは表示の整形を簡易にします。なぜなら、たくさんの改行について配慮しなくて済むからです。しかし、たまに本当に空行が必要なときがあるでしょう。そういう時は\bを使ってください。
プロパティーgreetingは評価されると単にHello!という文字と改行を表示します。だから、内蔵のsayルーチンを呼んでnewobj.greetingの値を表示させるより、単にプロパティー自体を評価したほうがいいのです:
printGreeting: function( obj ) { obj.greeting; }
プロパティーはリスト値を持つこともできます。リストは下のように[ ]で挟まれた値のセットです:
listobj: object mylist = [ 1 2 3 ] ;
リストの要素はデータの型が同一である必要はありませんが、普通は同じです。リストは、関連する項目を一箇所にまとめておく必要があるとき、特に項目のグループが時々変化する場合に便利です。
いくつかの演算子および組み込み済み関数はリスト操作を行います。演算子はリストに項目を加えたり、リストから外したり、リスト中の個々の項目を参照したりするために用意されています。組み込み関数はリストの中を検索し、項目を見つけるためにあります。
リストの検索法の一例として、下の関数はlistobj.mylistに含まれるリストの全項目を表示します。この例ではリスト索引演算子の[index]を使って、リスト中の個々の項目を得ます。また、リスト中の要素数を測定するために組み込み関数のlength()を使います。
showList: function { local ind, len; len := length( listobj.mylist ); // リストの長さを知る ind := 1; // 最初の要素から始める while ( ind <= len ) // 各要素を巡回する { say( listobj.mylist[ ind ] ); // この要素を表示する "\n"; // 改行を表示する ind := ind + 1; // 次の要素に移る } }
あなたがCに親しんでいるなら、おそらくこの例の中のループをfor構文を使って書こうと思うかもしれません。forなら一行の中にループの初期化、条件、ループ変数の増加を一緒に収めることができます。TADSではCと同様にforを使えるので、下のようにforループを伴ったshowList関数に書き直すことができます。
showList: function { local ind; local len := length(listobj.mylist); // リストの長さを保存する for ( ind := 1 ; ind <= len ; ind++ ) { say( listobj.mylist[ ind ] ); // この要素を表示する "\n"; // 改行を表示する } }
リスト中の項目を調べるもう一つの方法は組み込み関数のcar() と cdr()を使うものです。下の例がこれらの関数を説明します。
showList2: function { local cur; // リストの残りに対応する変数 cur := listobj.mylist; // リスト全体から始める while ( car ( cur ) ) // car(list)はリストの最初の要素 { say( car ( cur ) ); // リストの残りの中で最初の要素を表示する "\n"; // 改行を表示する cur := cdr( cur ); // cdr(list)はリストの残り; // つまり、リストのcar以外のすべて } }
この関数はリストを要素ごとに巡回します。whileループが一回りする毎にcurは、リストからその最初の要素を除いたものであるcurのcdrによって置き換えられます。car(cur)がnilになると、ループが終了しますが、それはリストに何も残っていないことを意味します(nilは値が存在しないことを効果的に意味する特別なデータ型です)。whileループはその条件がゼロまたはnilになるまで続きます。
私たちはループを開始する前に、リストをローカル変数に割り付けました。それはループを通過する度にリストのcdr()を取りたいからです。もしこれをその都度listobj.mylistへ割り付けたら、listobj.mylistは関数が終了したときになにも残らない形になってしまいます。リストをローカル変数に割り当てることによって、listobj.mylistを元の姿のまま保存しておくことができるのです。
TADSには基本的なデータ型がいくつかあります。つまり、数、文字列(シングル引用符で挟まれた、渡される単なる値)、画面に表示される文字列(ダブル引用符で挟まれた、値は持たないが評価されると画面に表示される)、リスト([ ]に挟まれた値)、nil とtrue(1 < 2のような表現によって返される)、そしてオブジェクトです。プロパティーはこれらのデータ型のうちどれでも持ちえます。
ここはこの言語のオブジェクト指向性がより明白になるところです。すなわち、プロパティーは単なるデータ項目ではなくコードを内蔵できるという点です。プロパティーがコードを含んでいるとき、それをメソッドといいます。
methodObj: object c = { local i; i := 0; while ( i < 100 ) { say( i ); " "; i := i + 1; } return( 'that is all' ); } ;
このオブジェクトは私たちが今までに見てきたものよりもかなり複雑です。メソッド cはステートメントの集合で、methodObj.cが査定される度に実行されます。この例のメソッドはある値を返しますが、これは必須ではありません。しばしばメソッドはそれが出力するメッセージのような副作用のために厳格に評価されます。
ダブル引用符で挟まれた文字列を値として持つプロパティーは、ダブル引用符で挟まれた同じ文字列を唯一の命令として備えるメソッドとまったく同じ働きをします。だから、次の三つの定義は同じことを表わします:
string1: object myString = "This is a string." ; string2: object myString = { "This is a string."; } ; string3: object myString = { say( 'This is a string.' ); } ;
引用符で挟まれた文字列はいかなる値も持ちません。その唯一の機能はそれらの評価がその文字列を表示するということです。
関数と同じく、メソッドも引数を取れます。引数を指定するには、関数のときに行うように単にメソッド名の後にそれらを列挙してください。それらは関数の引数と同様にメソッド内でローカル変数として行動します。
argObj: object sum( a, b, c ) = { "The sum is: "; say( a + b + c ); "\n"; } ;
引き数のあるメソッドを呼び出すために、関数の呼び出しと同様に、引き数の値がかっこでくくられ、メソッド名の後に置かれます:
argObj.sum( 1, 2, 3 );
オブジェクトは他のオブジェクトからプロパティーとメソッドを継承できます。objectというキーワードは一般的な種類のオブジェクトのほとんどを定義します。また、objectの代わりに他のオブジェクトの名前を使うことができます。たとえば、
book: object weight = 1 // 本がかなり軽い ; redbook: book description = "This is a red book. " ; bluebook: book weight = 2 // 普通の本より重たい description = "This is a big blue book. " ;
最初のオブジェクトのbookは一般的範疇を定義します。redbookは特定の本、つまり本のプロパティーを全部持っていて、なおかつ特有の何かを持っているものを定義します。これと同じようにbluebookも違う本を定義します。これもまた本の通常のプロパティーを全部持っていますが、独自のweightプロパティーを持っているので、もっと一般的な本のオブジェクトのweightプロパティーは無視されます。こういう理由で、redbook.weightは1ですが、bluebook.weightは2になります。
あるオブジェクトが別のオブジェクトからプロパティーを継承するとき、その別のオブジェクトは最初のオブジェクトのスーパークラスと呼ばれます。あるオブジェクトが他のオブジェクトのスーパークラスであるとき、そのオブジェクトはクラスと呼ばれます。いくつかのオブジェクト指向言語はオブジェクトとクラスをはっきりと区別します。しかし、TADSでは、両者の違いはあまりありません。とはいえ、classというキーワードは、あるオブジェクトがクラスとして厳格に奉仕していることを明記するために用意されています。ですから、bookは次のように定義することも可能でした:
class book: object noun = 'book' 'text' weight = 1 // 本はとても軽い ;
classというキーワードが必要になるのは、それ自身はオブジェクトではないクラスがボキャブラリーワードプロパティーまたはlocationプロパティーを有するときだけです。classの明記はプレーヤーコマンドパーサーが、プレーヤーは自分のコマンドでそのクラスを参照しているのだと誤解するのを予防します。
オブジェクトは複数の他のオブジェクトからプロパティーを継承することが可能です。これを多重継承といいます。これはかなり複雑な様相を呈します。それは主に、あるオブジェクトがプロパティーをどこから継承しているのかを正確に理解するのは時として難しいからです。実質的にいうと、あるオブジェクトがそのスーパークラスのいくつかから同一のプロパティーを継承できる場合に、そのオブジェクトのスーパークラスを明記する順番が継承の優先順位を決定します。
multiObj: class1, class2, class3 ;
ここで私たちはmultiObjについて、プロパティーを最初にclass1、次にclass2、その次にclass3から継承するように定義しました。もしも三つのクラスがすべてprop1というプロパティーを定義しているなら、multiObjはclass1からprop1を継承します。class1が最初に明記されているからです。
多重継承が利用されるのはごくまれですが、時にそれが非常に役に立つ特長であることに気づくでしょう。たとえば、巨大な花瓶を定義したいとしましょう。それは運ぶには重すぎるので部屋の中に固定されなければいけません。しかし、それは同時に物を収納するものでもある必要があります。多重継承を使えば、オブジェクトを(adv.tの中で定義されているクラスである)fixeditemとcontainerの両方であるように定義することができます。
言葉を変えるには、生活を変えなければならない。
DEREK WALCOTT, Codicil (1965)
第2章 | 目次 | 第4章 |