人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

会話する

キャラクターイベント(2008/6/22)で作ったイベントファイルに各キャラクターのメッセージを登録したの覚えてますか?今回は話しかけたときにそのメッセージを話すようにしてみます。メッセージウィンドウの表示(2008/6/28)ではすべてのウィンドウの基礎となるWindowクラスを作りました。今回は、Windowクラスを拡張してはなしたときのメッセージを表示する機能を持ったMessageWindowクラスを作ります。キャラクターの方を向き、スペースキーをおすと話します。

pyrpg15.zip
f:id:aidiary:20100731153946p:plain

はなす

MessageWindowクラスの機能を説明する前にどのように使うか解説します。MessageWindowの詳細がわからない場合はとりあえずどのように使うかわかればよいと思います。使うだけならそんなに難しくありません。キャラクターにはなす処理はmain()にあります。

    # メッセージウィンドウ
    msgwnd = MessageWindow(Rect(140,334,360,140))

    # ゲームループ
    for event in pygame.event.get():
        if event.type == QUIT:
            sys.exit()
        if event.type == KEYDOWN and event.key == K_ESCAPE:
            sys.exit()
        if event.type == KEYDOWN and event.key == K_SPACE:
            if msgwnd.is_visible:
                # メッセージウィンドウ表示中なら次ページへ
                msgwnd.next()
            else:
                # 表示中でないならはなす
                chara = player.talk(map)
                if chara != None:
                    msgwnd.set(chara.message)
                else:
                    msgwnd.set(u"そのほうこうには だれもいない。")

MessageWindowはWindowのサブクラスです。引数にはWindowと同じように表示範囲の矩形Rectを指定します。MessageWindowオブジェクトはmsgwndに格納します。

イベントハンドラでスペースキーを押したとき、メッセージウィンドウが表示中でないなら「はなす」処理を開始します。Playerクラスのtalk()でプレイヤーの目の前にいるキャラクターを取得します。目の前にキャラクターがいるならMessageWindowのset()にそのキャラクターのmessageをセットします。set()すると自動的にメッセージウィンドウが表示され、セットしたメッセージが表示されるしかけです。もし目の前にキャラクターがいないなら「そのほうこうには だれもいない。」というメッセージをset()します。

メッセージウィンドウが表示されている状態でスペースキーを押した場合は、MessageWindowのnext()を呼び出します。next()はメッセージを次ページに進めます。次ページがない場合は、メッセージウィンドウを消します。メッセージウィンドウの使い方はこれで全部です。使うだけなら簡単でしょ?

ここで、プレイヤーの目の前にいるキャラクターを取得するPlayerクラスのtalk()を詳しく見てみます。

    def talk(self, map):
        """キャラクターが向いている方向のとなりに
        キャラクターがいるか調べる"""
        # 向いている方向のとなりの座標を求める
        nextx, nexty = self.x, self.y
        if self.direction == DOWN:
            nexty = self.y + 1
        elif self.direction == LEFT:
            nextx = self.x - 1
        elif self.direction == RIGHT:
            nextx = self.x + 1
        elif self.direction == UP:
            nexty = self.y - 1
        # その方向にキャラクターがいるか?
        chara = map.get_chara(nextx, nexty)
        # キャラクターがいればプレイヤーの方向へ向ける
        if chara != None:
            if self.direction == DOWN:
                chara.direction = UP
            elif self.direction == LEFT:
                chara.direction = RIGHT
            elif self.direction == RIGHT:
                chara.direction = LEFT
            elif self.direction == UP:
                chara.direction = DOWN
            chara.update(map)  # 向きを変えたので更新
        return chara

コメントに書いたとおりなのですが、まずプレイヤーの向いている方向の隣の座標を調べます。向いている方向によって隣の座標は違うのでifで分岐して調べています。次に隣の座標 (nextx, nexty) がわかったらそこにキャラクターがいるか調べます。これは、Mapクラスのget_chara()を使います。talk()にmapオブジェクトを渡していたのはこのためです。

    def get_chara(self, x, y):
        """(x,y)にいるキャラクターを返す。いなければNone"""
        for chara in self.charas:
            if chara.x == x and chara.y == y:
                return chara
        return None

マップの (x,y) にキャラクターがいるか調べます。マップ内のキャラクターはすべてcharasリストに格納されているのでそこから1人ずつキャラクターを取り出して座標が一致するか調べます。もし一致するキャラクターがいたらそいつを返します。いなかったらNoneを返します。

talk()の最後ですが、これはそのキャラクターをプレイヤーの方向に向ける処理です。プレイヤー様が話しかけてんだからこっち向かせたいですよね(笑)これは、プレイヤーの向きと逆方向に向かせることで実現できます。たとえば、プレイヤーが右向きにキャラクターに話しかけたならそのキャラクターは左に向ければ向かい合います。

メッセージウィンドウの詳細

次にMessageWindowクラスの詳細を説明します。MessageWindowはメッセージをセットしてウィンドウを表示するset()とメッセージを次ページへ進めるnext()だけ知ってれば内部は知らなくても使えます。もしここの説明が難しいと感じたら飛ばしてしまっても大丈夫です。とりあえずクラスの全体コードを示します。たぶん今までの中で一番ぐちゃぐちゃしたコードです。

class MessageWindow(Window):
    """メッセージウィンドウ"""
    MAX_CHARS_PER_LINE = 20    # 1行の最大文字数
    MAX_LINES_PER_PAGE = 3     # 1行の最大行数(4行目は▼用)
    MAX_CHARS_PER_PAGE = 20*3  # 1ページの最大文字数
    MAX_LINES = 30             # メッセージを格納できる最大行数
    LINE_HEIGHT = 8            # 行間の大きさ
    
    def __init__(self, rect):
        Window.__init__(self, rect)
        # テキストを表示する矩形
        self.text_rect = self.inner_rect.inflate(-32, -32)
        self.text = []  # メッセージ
        self.cur_page = 0  # 現在表示しているページ
        self.max_page = 0  # 最終ページ
        self.msg_engine = MessageEngine()  # メッセージエンジン
        self.cursor = load_image("cursor.png", -1)  # カーソル画像
    def set(self, message):
        """メッセージをセットしてウィンドウを画面に表示する"""
        self.cur_page = 0
        # 全角スペースで初期化
        self.text = [u' '] * (self.MAX_LINES*self.MAX_CHARS_PER_LINE)
        # メッセージをセット
        p = 0
        for i in range(len(message)):
            ch = message[i]
            if ch == "/":  # /は改行文字
                self.text[p] = "/"
                p += self.MAX_CHARS_PER_LINE
                p = (p/self.MAX_CHARS_PER_LINE)*self.MAX_CHARS_PER_LINE
            elif ch == "%":  # \fは改ページ文字
                self.text[p] = "%"
                p += self.MAX_CHARS_PER_PAGE
                p = (p/self.MAX_CHARS_PER_PAGE)*self.MAX_CHARS_PER_PAGE
            else:
                self.text[p] = ch
                p += 1
        self.max_page = p / self.MAX_CHARS_PER_PAGE
        self.show()
    def draw(self, screen):
        """メッセージを描画する
        メッセージウィンドウが表示されていないときは何もしない"""
        Window.draw(self, screen)
        if self.is_visible == False: return
        # 現在のページ(curPage)1ページ分の内容を描画
        for i in range(self.MAX_CHARS_PER_PAGE):
            ch = self.text[self.cur_page*self.MAX_CHARS_PER_PAGE+i]
            if ch == "/" or ch == "%": continue  # 制御文字は表示しない
            dx = self.text_rect[0] + MessageEngine.FONT_WIDTH * (i % self.MAX_CHARS_PER_LINE)
            dy = self.text_rect[1] + (self.LINE_HEIGHT+MessageEngine.FONT_HEIGHT) * (i / self.MAX_CHARS_PER_LINE)
            self.msg_engine.draw_character(screen, (dx,dy), ch)
        # 最後のページでない場合は▼を表示
        if self.cur_page < self.max_page:
            dx = self.text_rect[0] + (self.MAX_CHARS_PER_LINE/2) * MessageEngine.FONT_WIDTH - MessageEngine.FONT_WIDTH/2
            dy = self.text_rect[1] + (self.LINE_HEIGHT + MessageEngine.FONT_HEIGHT) * 3
            screen.blit(self.cursor, (dx,dy))
    def next(self):
        """メッセージを先に進める"""
        # 現在のページが最後のページだったらウィンドウを閉じる
        if self.cur_page == self.max_page:
            self.hide()
        self.cur_page += 1

まずは、MessageWindowは下図のような構成になってます。Windowクラスを拡張してテキストを描画する赤い部分の領域 text_rectがあるのがポイントです(実際の色は赤ではなく黒です)。inner_rectに直接テキストを描画すると少しきつきつな感じがしたのでtext_rectを作りました。メッセージウィンドウのサイズは決めうちです。文字のサイズや一行の文字数などを考慮してこの大きさにしました。

f:id:aidiary:20100731153947p:plain

    MAX_CHARS_PER_LINE = 20    # 1行の最大文字数
    MAX_LINES_PER_PAGE = 3     # 1行の最大行数(4行目は▼用)
    MAX_CHARS_PER_PAGE = 20*3  # 1ページの最大文字数
    MAX_LINES = 30             # メッセージを格納できる最大行数
    LINE_HEIGHT = 8            # 行間の大きさ

この部分は、表示できるメッセージの文字数などを定義しています。文字のサイズはメッセージエンジンで説明したように16x22ピクセルです。下図のように行間にLINE_HEIGHT=8ピクセルとると1文字16x30ピクセルになります。行間を入れないと文字がつまった感じがして読みにくいです。

f:id:aidiary:20100731153948p:plain

1文字16x30ピクセルで横に20文字、4行だとちょうどtext_rectと同じサイズ320x120になる計算です。ただし、4行目はページ送り用の▼を中央に表示するだけで文字列は表示しません。MAX_LINES=30は、1回のset()で格納できる最大行数です。30行ということは 20x30=600文字が1回の会話の最大サイズです。原稿用紙1枚半ですね。この世界には学校の校長のように長話する人はいないのです(笑)メモリは食いますがもっと長くしておいてもOKです。

メッセージエンジンは、改行や改ページもサポートしています。文字列中に/を挿入するとそこで改行されます。また、%を挿入するとそこで改ページされます。よくわかんない人は黒猫とはなしてください。

set()でメッセージの内容をself.textという1次元リストに格納します。たとえば、イベントファイルに書いた黒猫のセリフ

 これはTESTです。/ちゃんとひょうじされてますか?%ざんねんながら かんじはつかえませんが/なんとかよめるでしょ?

は、self.textに次のように格納されます。_は全角空白文字です。わかりやすいように行ごとに2次元リストで書いていますが、実際は1行でずっとつながった1次元リストです。改行文字/や改ページ文字%の場所でちゃんと改行、改ページされていて飛ばした部分に全角空白文字が格納される点に注意してください。

 これはTESTです。/_________
 ちゃんとひょうじされていますか?%___
 ____________________  ここまでが1ページ目
 ざんねんながら_かんじはつかえませんが/ 
 なんとかよめるでしょ?_________
 ____________________  ここまでが2ページ目
 ____________________
 ...
 ____________________  30行目

set()では、self.textのどこの位置に格納するかをpという変数で制御しています。普通の文字の場合、そのまま文字を格納してp+=1するだけですが、改行文字や改ページ文字が入ったあとpを飛ばしています。どこまで飛ばせばよいかの式は少し複雑ですが上のサンプルを具体例として自分で計算してみてください。どういうふうにやっているか納得できると思います。

最後にself.max_pageに最大ページ数を格納したあと、show()でメッセージウィンドウを表示しています。show()するとメッセージウィンドウがdraw()で描画されるようになります。

メッセージウィンドウとset()で格納した文字列self.textを描画するのがdraw()です。今回は、1ページの内容をまとめて表示します。ドラクエのように文字が流れるように表示する方法は次回取り上げます。

    def draw(self, screen):
        """メッセージを描画する
        メッセージウィンドウが表示されていないときは何もしない"""
        Window.draw(self, screen)
        if self.is_visible == False: return
        # 現在のページ(curPage)1ページ分の内容を描画
        for i in range(self.MAX_CHARS_PER_PAGE):
            ch = self.text[self.cur_page*self.MAX_CHARS_PER_PAGE+i]
            if ch == "/" or ch == "%": continue  # 制御文字は表示しない
            dx = self.text_rect[0] + MessageEngine.FONT_WIDTH * (i % self.MAX_CHARS_PER_LINE)
            dy = self.text_rect[1] + (self.LINE_HEIGHT+MessageEngine.FONT_HEIGHT) * (i / self.MAX_CHARS_PER_LINE)
            self.msg_engine.draw_character(screen, (dx,dy), ch)
        # 最後のページでない場合は▼を表示
        if self.cur_page < self.max_page:
            dx = self.text_rect[0] + (self.MAX_CHARS_PER_LINE/2) * MessageEngine.FONT_WIDTH - MessageEngine.FONT_WIDTH/2
            dy = self.text_rect[1] + (self.LINE_HEIGHT + MessageEngine.FONT_HEIGHT) * 3
            screen.blit(self.cursor, (dx,dy))

現在表示中のページはself.cur_pageに入っています。draw()では現在表示中のself.cur_page、1ページ分の内容をメッセージウィンドウ内に描画します。各文字をself.text_rect内のどこに表示するかを (dx, dy) を計算してMessageEngineのdraw_character()で描画しています。このとき、制御文字(/、%)は描画しない点に注意してください。現在、表示中のページ(self.cur_page)が最終ページでない場合は、▼をメッセージウィンドウの4行目、中央に表示します。next()は、次ページへ進めるメソッドです。

    def next(self):
        """メッセージを先に進める"""
        # 現在のページが最後のページだったらウィンドウを閉じる
        if self.cur_page == self.max_page:
            self.hide()
        self.cur_page += 1

現在表示中のページ(self.cur_page)が最終ページだったらhide()でメッセージウィンドウを閉じます。そうでなかったら self.cur_pageに+1して次ページへ進めます。self.cur_pageを+1するとdraw()では次ページの描画が自動的に行われます。

実行してみるとわかりますが、1ページの内容がぱっと表示されるため何か味気ないです。また▼が点滅しないと何かやです。ドラクエのようなピロピロと文字が流れる表示方法は次回取り上げます。