会話テストプログラムついに完成! その構造について解説しています。
2001年03月25日更新
ずいぶんと手間取った2号の制作ですが、なんとか形になりました。会話の種類はまだまだ少ないがらも、 前回のコラムで述べた「選択肢の動的生成」により、 会話の展開のパターンに多様性が生まれているのが分かってもらえると思います。
この会話テストプログラムには、「選択肢の動的生成」以外にも、いくつかの工夫が組み込まれています。 その一つが、会話の自然な流れを演出するための「会話フェーズ」と 「会話文の系統分類」です。 何気に会話が進行しているように見えますが、その流れは4段階のフェーズによって区分され、 各会話文は5種類の系統に分類されています。
フェーズ | リストアップ | ||
---|---|---|---|
あいさつフェース | あいさつ系 | 話題系 | 質問系 |
話題フェーズ | 話題系 | 質問系 | さよなら系 |
返事フェーズ | 返事系 | ||
さよならフェーズ | さよなら系 |
系統 | 移行フェーズ |
---|---|
あいさつ系 | あいさつフェーズ |
話題系 | 話題フェーズ |
質問系 | 返事フェーズ |
返事 | 話題フェーズ |
さよなら系 | さよならフェーズ |
えー、何のこっちゃさっぱりわからん表かとは思いますが、 簡単にいってしまえば、「あいさつ→おしゃべり→さようなら」 という会話の流れを基本軸にすえて、流れの逆戻りはさせないということです。 たとえば、話題系である「何してるの?」を選択して話題フェーズに進行してしまうと、 「こんにちは」などのあいさつ系会話は、それ以降の選択肢に出現することはありません。
当初は、とにかく多様性を出そうと、可能性のある会話は全部リストアップされる方向で 制作を進めてました。そしたら、「おはよう」との呼びかけに「さよなら」と答えたり、 「じゃあね」に「今日はいい天気だね」と答えたり、全く脈絡のない会話になってしまいました。 そこで、会話を秩序立てる要素として追加したのが、この「フェーズ」と「系統」です。 ごく単純なモデリングですが、予想以上に上手く機能したみたいで、 会話の展開がより自然なものになってくれました。
そして、今回の制作での一番の目玉は、会話をデータとして扱うことをあきらめたことです。 「あきらめた」という表現には、それなりの理由があるんですが、 何はともあれ、そのデータ構造を見てもらいましょう。 ソースの一部をそのまま引っぱってきますと……
/** * 会話classの説明 * * type : 会話のタイプ指定 * * topic : 話題番号の指定 * * permit : 選択可能キャラの指定 * * getMatchRate(Talk) : その会話の適合率を計算して返す * 適合率は0〜100の整数で * 計算した値はmatchRateに入れてそれをリターン * * getMessage(Talk) : その会話の文字列を返す * * doEffect(Talk talk) : 会話選択後に行われる処理 */ // #0022 返事系 class mes0022 extends Message { private String mes = "僕はミルコ 魔法の修行にこの森に来ました"; // 初期設定 mes0022() { type = ANSWER_TYPE | TOPIC_TYPE; topic = LEARN_MAGIC_TOPIC; permit = MILCO; } // 適合率 public int getMatchRate(Talk talk) { if((topic == WHO_TOPIC)||!talk.listener.isKnown[talk.talker.id]) { matchRate = 100; // 誰だ?と聞かれた or 自ら名乗り出る } else { matchRate = 0; } return(matchRate); } // メッセージ public String getMessage(Talk talk) { return(mes); } // 後処理 public void doEffect(Talk talk) { talk.listener.isKnown[talk.talker.id] = true; // 会話相手チョイスの更新 はPCなので必要ない // Common.mainPanel.isPeopleChanged = true; } }
ズバリ、データというより、データとプログラムを一体化したオブジェクト、 それもJavaのclassそのまんまです。 とりあえずお断りしておきますが、少なくともゲームプログラムにおいて、 このようなデータの持ち方は、一般的ではないと思われます。 普通なら、文字列といくつかのパラメータをひとまとめにした 構造体の配列で処理するでしょう(とはいえJavaには構造体はありませんが)。 拡張処理用の関数のポインタを持つことくらい(これまたJavaにポインタはありませんが)は考えられますが、 これほどまで、会話データ側からゲームシステム全体のリソースに アクセスできる作りにはなってないはずです。
基本的に、データをプログラムの中に直接埋めこむのは、お行儀の悪いこととされています。
とりわけ、チームを組んで、それぞれが分業でゲーム制作を行う場合は、
プログラムとデータを分離するのは当然ですし、そこをきっちり分離することこそ、
プロジェクトの生産性の向上につながると認識されています。
なのにあえて、この構造を選んだのは、「選択肢の動的生成」と「セリフのオブジェクト化」の
相性が非常に良いからです。選択肢を状況に合わせてリストアップするには、とにかく、
会話一つ一つの独立性が求められるんです。
すでにプロトタイプ2号をプレイされた方は、お気づきかと思いますが、
はじめて会う人に話しかけたときは、あいさつやら、自己紹介やら、質問やら、天気の話題やら、
全く違った種類の会話がずらっと並びますよね。
これは、セリフそれぞれで、独自に選択可能かどうかを判断しているからであって、
最初からああいうリストになるようにプログラムしたわけではありません。
会話のオブジェクトを一つ一つ作っていたら、結果的にああなったというだけのことです。
これこそ、会話の独立性なんですよね。
ですから、会話データを一ヶ所にまとめて、汎用の関数で処理するんではなくて、
一つ一つのセリフに、自分自身を制御するための関数を持たせたデータ構造を採用したわけです。
この独立性の高さはデータ作成面でも威力を発揮します。
会話を汎用的に扱うプログラムがすでに存在してる場合は、
無意識的に(あるときは意識的に)その処理に合わせた文章ばかりを作ってしまいがちです。
新しい文章を作るたびに、毎回のごとくプログラムを書き換えるのは非効率ですから、
ついつい、型にはまった会話文を書いてしまうんです。
でも、今回の場合は、自由に考えた文章と、それに付随する一連のプログラムを、
おおもとの会話処理プログラムに修正を加えることなく、
好きなときに好きなだけ追加することができます。
セリフの拡張とプログラムの拡張が、同時に同じ次元で行われるのも大きなメリットです
この、データとそれを扱う関数をひとまとめにするというのは、まさにオブジェクト指向的な データ設計です。もちろん、ゲームプログラム一般において、オブジェクト指向的なアプローチは 普通に見うけられると思いますが、たかがメッセージ一つをその構造で管理するのは、 少ないんじゃないでしょうか。
ちょっと制作者の思い込みっぽくなっちゃいますが、 こうやって、言葉一つ一つに関数を与えることで、「言葉に命を吹き込んでる」 って感覚を覚えたりもします。 汎用的な一括処理でリンクをたどるだけの会話より、 なんとなく暖かみがある言葉に思えてくるのは……僕だけでしょうね。
ま、その辺の話はおいといても、このオブジェクト指向のデータ構造は、とても魅力的なものといえます。 ただ、それと同時に、ある意味、危険なカケでもあります。 まず、データの一覧性がありません。 ざっと目を通して、漢字の間違いを集中的に調べるなんてことは不可能です。 データの再利用性もかなり低いです。今度、プロトタイプの合体バージョンを作るときには、 もう一度全部のデータを書き直さなければならないかもしれません。 また、現段階のような小規模なデータ量では制御も楽ですが、 データ量が増えれば増えるにつれ、あっちの会話を立てればこっちが立たずみたいな感じで、 収拾がつかなくなる恐れもあります。
このような「お行儀の悪い」ことは、賢いプロフェッショナルなプログラマーなら、 避けることかもしれません。開発が進めば進むにつれ、自分の首を絞めることになりかねないからです。 幸いにというか不幸にもというか、僕は、しょせんは趣味プログラマーなんで、 「とりあえずやってみるか」とあんまり先を考えずにやってしまうんですよね。 一人で作ってるんで、困るのは自分で、誰に迷惑をかけるわけでもありませんから。
そう、趣味プログラマーとして、今回の制作で認識を改めさせられたことがありました。 それはスピード感覚です。 選択肢を生成するたびに全メッセージをサーチするという構造に決めたとき、 「もしかしたら、すごく処理が重くなるかもしれない」との懸念が頭をよぎりました。 試しに、どの程度のものなのかと、ダミー会話オブジェクトを1000個用意して、 選択肢の生成までにかかる時間を計測するプログラムを組みました。 この1000個の会話をチェックする処理というのは、その昔、10MHzのMPU68000で鍛えられた僕としては、 それなりに重い処理だと思ったんですが、 実際に計測してみると時間は0ms(msは1/1000秒)だったのです。
どこかミスったか? とソースを見なおしても間違いはありません。 物は試しと、メッセージを5000個に増やして、さらに、毎回呼ばれる関数getMatchRate()の中で かけ算を100回行うループを仕込んでみたところ、 実行時間は60msと表示されました。そう、あの結果は正しかったんです。 (実際はマルチスレッドで動いてるので、数百回の平均をとるとかしないと正確な値はでませんが)
開発に使ってるパソコンは、Pentium3の600MHzなんですが、この環境においては、 「数バイトをアクセスして演算後、大小比較する」関数を1000回呼ぼうが、 へのかっぱということなんですね。 逆に、選択リストとか、いわゆるGUIにアクセスするAPIの方が、圧倒的に遅かったりします。 そんなの常識だよとおっしゃる方もいるとは思いますが、僕は真剣に、 「こりゃ重いぜ」と信じて疑いませんでした。
「この処理って重いかも?」と感じとる洞察力は、プログラマーにとって必要な能力なのですが、 自前のコードよりも、豊富なAPIを駆使することが主流となった今、 あらゆる言語の、あらゆる環境下において、的確なスピード感覚を保つのは、 非常に難しい時代なんだなと感じます。 これ言い訳じゃないっすよ。ホントにホント。
話を、プロトタイプのほうに戻しましょう。 会話の量的な問題に関しては、もちろん、今回ので満足してるわけはありません。 「なんだ、結局はすぐマンネリ会話になるじゃないか」と思った人もいることでしょう。 意志をもって行動しているのが、プレイヤーのみなので、世界の状況が変化せず、 結局はそうなってしまいます。 でも、プレイヤーと同様に目的をもって行動するNPCが登場したら、話は変わります。 NPCの行動思考ルーチンが動きだせば、会話もより生き生きとした臨場感のある物になるはずです。
そのためには、行動の目的となるもの、及び、その世界の法則が必要となります。 ということで、次のプロトタイプ3号では、価値観の創設や、経済原理の確立に主眼をおいた プログラム開発を行います。 これはいわば、魔法の森の社会のルールを規定することです。 で、そのルールに基づいてNPCが思考し、行動をするのが、次の次の「プロトタイプ1号改」で、 彼らと会話が出来るようになるのが「1号・2号・3号合体バージョン」です。 それ位になれば、結構遊べるもんになってるだろうなと思ってます。
でも、それって、いつになるかなぁ? 桜前線の上昇とともに、
制作スピードも上昇させたいものです。
それでは、今日はこのへんで。