人工知能に関する断創録

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

ゲーム状態の導入

今回はタイトル画面をつけます。タイトル画面のように別の画面が入ってくると実装がこんがらがってくるため、ゲーム状態によって更新・描画・イベントハンドラを分離するようにスクリプトを修正します。また、今までmain()に書いていた処理をクラス化します。今回は細かい修正が多いです。タイトル画面では、矢印の上下でメニューを選択し、スペースキーで実行します。STARTを実行すると今までのゲーム画面に移ります。EXITを実行するとプログラムが終了します。CONTINUEは未実装です。今回からフルスクリーンモードにしました。スクリーンショットをよく見るとタイトルバーがないですよね。プログラムはESCキーで終了します

pyrpg26.zip
f:id:aidiary:20100807224445p:plain f:id:aidiary:20100807224446p:plain

main()のクラス化 + ゲームループの整理

まず、今回行った大きな修正は、main()関数を排除してPyRPGというクラスを作成したことです。今までmain()に書いていた処理は PyRPGクラスのコンストラクタ__init__()に移っています。また、main()で使っていたもろもろの関数はPyRPGクラスのメソッドになっています。さらに、main()で使っていたいろいろな変数はPyRPGクラスのインスタンス変数(selfがつく変数)になっています。インスタンス変数にするメリットはけっこう大きくてPyRPGクラス内のどのメソッドからもアクセスできるようになります。今までのように引数で渡す必要がなくなったのでプログラムがすっきりします。メソッドだけ並べると下のようになっています。

class PyRPG:
    def __init__(self):
    def mainloop(self):
        """メインループ"""
    def update(self):
        """ゲーム状態の更新"""
    def render(self):
        """ゲームオブジェクトのレンダリング"""
    def check_event(self):
        """イベントハンドラ"""
    def title_handler(self, event):
        """タイトル画面のイベントハンドラ"""
    def field_handler(self, event):
        """フィールド画面のイベントハンドラ"""
    def cmd_handler(self, event):
        """コマンドウィンドウが開いているときのイベントハンドラ"""
    def talk_handler(self, event):
        """会話中のイベントハンドラ"""
    def calc_offset(self, player):
        """オフセットを計算する"""
    def show_info(self):
        """デバッグ情報を表示"""
    def load_sounds(self, dir, file):
        """サウンドをロードしてsoundsに格納"""
    def load_charachips(self, dir, file):
        """キャラクターチップをロードしてCharacter.imagesに格納"""
    def load_mapchips(self, dir, file):
        """マップチップをロードしてMap.imagesに格納"""

main()に関連のあるグローバル領域にあった関数が全部PyRPGクラスのメソッドになっていることがわかります(見たことないメソッドは後で説明します)。あとメインループ内の更新・描画・イベントハンドラも独立したメソッドにしました。そのおかげでメインループが下のように非常にすっきりした構造になります。

    def mainloop(self):
        """メインループ"""
        clock = pygame.time.Clock()
        while True:
            clock.tick(60)
            self.update()             # ゲーム状態の更新
            self.render()             # ゲームオブジェクトのレンダリング
            pygame.display.update()   # 画面に描画
            self.check_event()        # イベントハンドラ

流れがすっきりわかって美しいですね!

if __name__ == "__main__":
    PyRPG()

PyRPGクラスを動かすにはオブジェクトを作るだけです。あとはかってに__init__()が呼ばれてメインループが起動してゲームが始まります。

ゲーム状態

今回の修正の目玉はゲーム状態を表す変数(game_state)を導入したことです。ゲーム状態というのはその名の通りで今ゲームがどういう状態にあるかを表した変数です。今回は、4つのゲーム状態を定義しています。

  • タイトル状態(TITLE): タイトル画面を表示している状態
  • フィールド状態(FIELD):ゲームのメイン画面、フィールドを移動できる状態
  • コマンド状態(COMMAND):コマンドウィンドウを表示している状態
  • 会話状態(TALK):メッセージウィンドウを表示している状態

この4つの状態では、画面に描画するもの、更新するもの、キー入力の意味がまったく異なる点に注意してください。たとえば、タイトル画面はフィールド画面と描画するものが違います。タイトル画面ではゲームのタイトル、メニュー、クレジットを描画するのに対し、フィールド画面ではプレイヤー、マップを描画します。また、フィールド状態でスペースキーを押したときと会話状態でスペースキーを押したときでは意味が違います。フィールド状態でスペースキーを押すとコマンドウィンドウが出ますし、会話状態でスペースキーを押すと次ページへ移ります。こういう場合は、ゲーム状態を導入するとすっきり整理できます。下のプログラムを見てください。

    def update(self):
        """ゲーム状態の更新"""
        if self.game_state == TITLE:
            self.title.update()
        elif self.game_state == FIELD:
            self.map.update()
            self.party.update(self.map)
        elif self.game_state == TALK:
            self.msgwnd.update()
    def render(self):
        """ゲームオブジェクトのレンダリング"""
        if self.game_state == TITLE:
            self.title.draw(self.screen)
        elif self.game_state == FIELD or self.game_state == TALK or self.game_state == COMMAND:
            offset = self.calc_offset(self.party.member[0])
            self.map.draw(self.screen, offset)
            self.party.draw(self.screen, offset)
            self.msgwnd.draw(self.screen)
            self.cmdwnd.draw(self.screen)
            self.show_info()  # デバッグ情報を画面に表示
    def check_event(self):
        """イベントハンドラ"""
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            if event.type == KEYDOWN and event.key == K_ESCAPE:
                pygame.quit()
                sys.exit()
            # 表示されているウィンドウに応じてイベントハンドラを変更
            if self.game_state == TITLE:
                self.title_handler(event)
            elif self.game_state == FIELD:
                self.field_handler(event)
            elif self.game_state == COMMAND:
                self.cmd_handler(event)
            elif self.game_state == TALK:
                self.talk_handler(event)

game_stateがゲーム状態です。game_stateは4つの状態TITLE、FIELD、COMMAND、TALKを取ります。そして、更新(update)、描画(render)、キー入力の処理(check_event)では、ゲーム状態によって処理を分岐しているのがポイントです。

たとえば、キー入力の処理をしているcheck_event()を見てみます。ゲーム状態によって異なるイベントハンドラを呼び出していることがわかります。タイトル状態では、それ専用のtitle_handler()を呼び出してキー入力を処理させます。フィールド状態では、field_handler()を呼び出してキー入力を処理させます。このようにゲーム状態で処理を分岐させるとプログラムの構造がすっきりします。

状態遷移

もう1つ重要なのが状態遷移です。ゲーム状態がどのように移り変わるかを表す言葉です。たとえば、タイトル状態でSTARTメニューに合わせてスペースキーを押すとフィールド状態に移って移動できるようになります。さらにフィールド状態でスペースキーを押すとコマンドウィンドウが表示されるコマンド状態に移動します。さらにはなすメニューを実行するとメッセージウィンドウが表示されて会話ウィンドウが表示されます。会話が終了するとメッセージウィンドウが閉じてフィールド状態に移動します。このような状態の移り変わりをわかりやすく示した図を状態遷移図と言います。

f:id:aidiary:20100807224447p:plain

例としてフィールド画面でスペースキーを押したときにコマンド状態へ移動するコードを見てみます。

    def field_handler(self, event):
        """フィールド画面のイベントハンドラ"""
        # スペースキーでコマンドウィンドウ表示
        if event.type == KEYDOWN and event.key == K_SPACE:
            sounds["pi"].play()
            self.cmdwnd.show()
            self.game_state = COMMAND  # <-- COMMAND状態へ移動

フィールド画面のイベントハンドラでスペースキーを押したときにgame_stateをCOMMANDに変えています。別の例としてコマンド状態ではなすに合わせてスペースキーを押したときに会話状態へ移動するコードを見てみます。

    def cmd_handler(self, event):
        """コマンドウィンドウが開いているときのイベントハンドラ"""
        # スペースキーでコマンド実行
        if event.type == KEYDOWN and event.key == K_SPACE:
            if self.cmdwnd.command == CommandWindow.TALK:  # はなす
                sounds["pi"].play()
                self.cmdwnd.hide()
                chara = player.talk(self.map)
                if chara != None:
                    self.msgwnd.set(chara.message)
                    self.game_state = TALK  # <-- TALK状態へ移動
                else:
                    self.msgwnd.set(u"そのほうこうには だれもいない。")
                    self.game_state = TALK  # <-- TALK状態へ移動

別のゲーム状態へ移動することでupdate()やrender()によって別のオブジェクトが更新や描画されるようになります。

タイトル画面

次にTITLE状態のときに表示されるタイトル画面がどのように実装されているか見てみます。タイトル画面の描画や更新はTitleというクラスにまとまっています。まあここまでスクリプトを読んできたかたには説明不要ですかね?

class Title:
    """タイトル画面"""
    START, CONTINUE, EXIT = 0, 1, 2
    def __init__(self, msg_engine):
        self.msg_engine = msg_engine
        self.title_img = load_image("data", "python_quest.png", -1)
        self.cursor_img = load_image("data", "cursor2.png", -1)
        self.menu = self.START
        self.play_bgm()
    def update(self):
        pass
    def draw(self, screen):
        screen.fill((0,0,128))
        # タイトルの描画
        screen.blit(self.title_img, (20,60))
        # メニューの描画
        self.msg_engine.draw_string(screen, (260,240), u"START")
        self.msg_engine.draw_string(screen, (260,280), u"CONTINUE")
        self.msg_engine.draw_string(screen, (260,320), u"EXIT")
        # クレジットの描画
        self.msg_engine.draw_string(screen, (130,400), u"2008 PYTHONでゲームつくりますがなにか?")
        # メニューカーソルの描画
        if self.menu == self.START:
            screen.blit(self.cursor_img, (240, 240))
        elif self.menu == self.CONTINUE:
            screen.blit(self.cursor_img, (240, 280))
        elif self.menu == self.EXIT:
            screen.blit(self.cursor_img, (240, 320))
    def play_bgm(self):
        bgm_file = "title.mp3"
        bgm_file = os.path.join("bgm", bgm_file)
        pygame.mixer.music.load(bgm_file)
        pygame.mixer.music.play(-1)

タイトル画面でのキー入力の処理は、PyRPGクラスのtitle_handler()にまとまっています。

    def title_handler(self, event):
        """タイトル画面のイベントハンドラ"""
        if event.type == KEYUP and event.key == K_UP:
            self.title.menu -= 1
            if self.title.menu < 0:
                self.title.menu = 0
        elif event.type == KEYDOWN and event.key == K_DOWN:
            self.title.menu += 1
            if self.title.menu > 2:
                self.title.menu = 2
        if event.type == KEYDOWN and event.key == K_SPACE:
            sounds["pi"].play()
            if self.title.menu == Title.START:
                self.game_state = FIELD
                self.map.create("field")  # フィールドマップへ
            elif self.title.menu == Title.CONTINUE:
                pass
            elif self.title.menu == Title.EXIT:
                pygame.quit()
                sys.exit()

矢印キーの上下でメニューを選択し、スペースキーで実行しています。STARTメニューではFIELD状態に移動してゲームを開始します。CONTINUEメニューは未実装ですね。EXITメニューはプログラムを終了します。

フルスクリーンモード

最後になりますが、今回からフルスクリーン表示してみました。書き換える場所は下の1カ所だけです。これだけでフルスクリーンにできるんですからすごいですね!フルスクリーンがやだったら簡単に戻せます。フルスクリーンの方法はフルスクリーンモードに詳しく書いたので参照してください。

        # フルスクリーン化 + Hardware Surface使用
        self.screen = pygame.display.set_mode(SCR_RECT.size, DOUBLEBUF|HWSURFACE|FULLSCREEN)

さあタイトルもついてゲームっぽくなってきました。今回説明したゲーム状態を使えばタイトル画面を作ったように戦闘画面もあっさり作れそうですね!