人工知能に関する断創録

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

戦闘画面

まだ戦えるわけではありませんが、戦闘画面を実装してみました。フィールドを歩いていると何か強そうな(笑)モンスターに襲われます。コマンドの選択などはすべてスペースキーを使います。とりあえずにげることしかできません。モンスターのグラフィックは、HOT TOKE(リンク切れ)からお借りしています。ここは大量のかっこいいモンスターグラフィックがあってすばらしいです。戦闘のBGMは、煉獄庭園さんからお借りしています。「最期のレクイエム」というMP3の曲です。聴いた瞬間に戦闘に使いたいと思いました。

pyrpg27.zip
f:id:aidiary:20100807225033p:plain f:id:aidiary:20100807225034p:plain

戦闘の状態遷移

ゲーム状態の導入でタイトル画面を作ったのと同じように戦闘画面もゲーム状態で管理します。戦闘は流れが複雑なのでとりあえず下図に示すようにBATTLE_INIT、BATTLE_COMMAND、BATTLE_PROCESSの3つの状態を用意しました。

f:id:aidiary:20100807225035p:plain

BATTLE_INITは、戦闘の開始準備を行う状態です。モンスターの配置や戦闘曲の再生などを行います。スクリーンショットで「やまたのおろちが あらわれた」はこの状態です。

    def battle_init_handler(self, event):
        """戦闘開始のイベントハンドラ"""
        global game_state
        if event.type == KEYDOWN and event.key == K_SPACE:
            self.msgwnd.hide()
            sounds["pi"].play()
            self.battle.cmdwnd.show()
            for bsw in self.battle.status_wnd:
                bsw.show()
            game_state = BATTLE_COMMAND

BATTLE_INITのイベントハンドラは、battle_init_handler()です。スペースキーを押すと「やまたのおろちが あらわれた」というメッセージウィンドウを隠し、代わりに戦闘コマンドウィンドウ(batttle.cmdwnd)とパーティー4人分のステータスウィンドウ(battle.status_wnd)を表示します。その後にBATTLE_COMMAND状態へ移行します。BATTLE_COMMAND状態は、たたかう、にげるなどのコマンドが表示されている状態です。スクリーンショットの右側がそうです。

    def battle_cmd_handler(self, event):
        """戦闘コマンドウィンドウが出ているときのイベントハンドラ"""
        global game_state
        # バトルコマンドのカーソル移動
        if event.type == KEYUP and event.key == K_UP:
            if self.battle.cmdwnd.command == 0: return
            self.battle.cmdwnd.command -= 1
        elif event.type == KEYDOWN and event.key == K_DOWN:
            if self.battle.cmdwnd.command == 3: return
            self.battle.cmdwnd.command += 1
        # バトルコマンドの決定
        if event.type == KEYDOWN and event.key == K_SPACE:
            sounds["pi"].play()
            # たたかう
            if self.battle.cmdwnd.command == BattleCommandWindow.ATTACK:
                self.msgwnd.set(u"ちょっっ まじで?/LV1でたおせるわけないよう。/じっそうしてないから かんべんして。")
            # じゅもん
            elif self.battle.cmdwnd.command == BattleCommandWindow.SPELL:
                self.msgwnd.set(u"じゅもんを おぼえていない。")
            # どうぐ
            elif self.battle.cmdwnd.command == BattleCommandWindow.ITEM:
                self.msgwnd.set(u"どうぐを もっていない。")
            # にげる
            elif self.battle.cmdwnd.command == BattleCommandWindow.ESCAPE:
                self.msgwnd.set(u"けんしたちは にげだした。")
            self.battle.cmdwnd.hide()
            game_state = BATTLE_PROCESS

BATTLE_COMMAND状態のイベントハンドラは、battle_cmd_handler()です。矢印キーでバトルコマンドを選択して、スペースキーで決定します。ここらへんは、コマンドウィンドウと同じですね。バトルコマンドを選択したら、コマンドウィンドウ(battle.cmdwnd)を閉じて、対応するメッセージを表示した後、BATTLE_PROCESS状態へ移行します。BATTLE_PROCESS状態は、実際に戦闘イベントを処理する状態です。たとえば、

 〜のこうげき(メッセージ)
 ばしっ(効果音)
 画面フラッシュ(赤)
 〜にXXXのダメージ(メッセージ)
 〜はしんでしまった(メッセージ)
 プレイヤー死亡(ステータス変化)

のような処理です。とはいうものの今の段階でここまでは実装できていません。たぶんイベントスクリプトを作ってからでないと無理だと思います。

    def battle_proc_handler(self, event):
        global game_state
        if event.type == KEYDOWN and event.key == K_SPACE:
            self.msgwnd.hide()
            if self.battle.cmdwnd.command == BattleCommandWindow.ESCAPE:
                # フィールドへ戻る
                self.map.play_bgm()
                game_state = FIELD
            else:
                # コマンド選択画面へ戻る
                self.battle.cmdwnd.show()
                game_state = BATTLE_COMMAND

BATTLE_COMMAND状態のイベントハンドラは、battle_proc_handler()です。スペースキーを押すと、メッセージウィンドウを隠します。もしにげるコマンドを選んでいたならフィールドへ戻します。もし他のコマンドならコマンドウィンドウを再表示し、 BATTLE_COMMAND状態へ戻ります。気づいたと思いますが、game_stateはどのクラスからでもアクセスできるようにグローバル変数にしました。Pythonの場合、関数内でグローバル変数にアクセスするにはglobal文を使う必要があります。global文を使わないと新しいローカル変数を作ってしまいます。ここは他の言語と違っていて知らないとはまります。

エンカウント

モンスターの出現(エンカウント)は、Player.update()に追加しました。移動が完了したときにPROB_ENCOUNTの確率で戦闘に突入します。

    PROB_ENCOUNT = 0.05  # エンカウント確率
    
    # エンカウント発生
    if map.name == "field" and random.random() < PROB_ENCOUNT:
        game_state = BATTLE_INIT
        battle.start()

戦闘に突入するには、BATTLE_INIT状態に移動し、Battleのstart()を呼び出して戦闘を初期化します。

Battleクラス

戦闘画面を実装したクラスです。タイトル画面と良く似た方法を使っています。戦闘コマンドウィンドウ(BattleCommandWindow)とプレイヤーの戦闘ステータスウィンドウ(BattleStatusWindow)のオブジェクトを作って保持しているのがポイントです。draw()で2 つを画面に描画しています。

class Battle:
    """戦闘画面"""
    def __init__(self, msgwnd, msg_engine):
        self.msgwnd = msgwnd
        self.msg_engine = msg_engine
        # 戦闘コマンドウィンドウ
        self.cmdwnd = BattleCommandWindow(Rect(96, 338, 136, 136), self.msg_engine)
        # プレイヤーステータス(Playerクラスに実装した方がよい)
        status = [[u"けんし ", 16, 0, 1],  
                  [u"エルフ ", 15, 24, 1],
                  [u"そうりょ", 10, 8, 1],
                  [u"まどうし", 8, 12, 1]]
        # 戦闘ステータスウィンドウ
        self.status_wnd = []
        self.status_wnd.append(BattleStatusWindow(Rect(90, 8, 104, 136), status[0], self.msg_engine))
        self.status_wnd.append(BattleStatusWindow(Rect(210, 8, 104, 136), status[1], self.msg_engine))
        self.status_wnd.append(BattleStatusWindow(Rect(330, 8, 104, 136), status[2], self.msg_engine))
        self.status_wnd.append(BattleStatusWindow(Rect(450, 8, 104, 136), status[3], self.msg_engine))
        self.monster_img = load_image("data", "dragon.png", -1)
    def start(self):
        """戦闘の開始処理、モンスターの選択、配置など"""
        self.cmdwnd.hide()
        for bsw in self.status_wnd:
            bsw.hide()
        self.msgwnd.set(u"やまたのおろちが あらわれた。")
        self.play_bgm()
    def update(self):
        pass
    def draw(self, screen):
        screen.fill((0,0,0))
        screen.blit(self.monster_img, (200, 170))
        self.cmdwnd.draw(screen)
        for bsw in self.status_wnd:
            bsw.draw(screen)
    def play_bgm(self):
        bgm_file = "battle.mp3"
        bgm_file = os.path.join("bgm", bgm_file)
        pygame.mixer.music.load(bgm_file)
        pygame.mixer.music.play(-1)

BattleCommandWindowクラス

戦闘のコマンドウィンドウです。フィールドのコマンドウィンドウと非常によく似ているので特に問題ないと思います。

class BattleCommandWindow(Window):
    """戦闘のコマンドウィンドウ"""
    LINE_HEIGHT = 8  # 行間の大きさ
    ATTACK, SPELL, ITEM, ESCAPE = range(4)
    COMMAND = [u"たたかう", u"じゅもん", u"どうぐ", u"にげる"]
    def __init__(self, rect, msg_engine):
        Window.__init__(self, rect)
        self.text_rect = self.inner_rect.inflate(-32, -16)
        self.command = self.ATTACK  # 選択中のコマンド
        self.msg_engine = msg_engine
        self.cursor = load_image("data", "cursor2.png", -1)
        self.frame = 0
    def draw(self, screen):
        Window.draw(self, screen)
        if self.is_visible == False: return
        # コマンドを描画
        for i in range(0, 4):
            dx = self.text_rect[0] + MessageEngine.FONT_WIDTH
            dy = self.text_rect[1] + (self.LINE_HEIGHT+MessageEngine.FONT_HEIGHT) * (i % 4)
            self.msg_engine.draw_string(screen, (dx,dy), self.COMMAND[i])
        # 選択中のコマンドの左側に&#9654;を描画
        dx = self.text_rect[0]
        dy = self.text_rect[1] + (self.LINE_HEIGHT+MessageEngine.FONT_HEIGHT) * (self.command % 4)
        screen.blit(self.cursor, (dx,dy))
    def show(self):
        """オーバーライド"""
        self.command = self.ATTACK  # 追加
        self.is_visible = True

BattleStatusWindowクラス

最後にステータスウィンドウです。1人分のステータスを表しています。4人パーティーのときはBattleStatusWindowのオブジェクトが4つ必要になります。

class BattleStatusWindow(Window):
    """戦闘画面のステータスウィンドウ"""
    LINE_HEIGHT = 8  # 行間の大きさ
    def __init__(self, rect, status, msg_engine):
        Window.__init__(self, rect)
        self.text_rect = self.inner_rect.inflate(-32, -16)
        self.status = status  # status = ["なまえ", HP, MP, LV]
        self.msg_engine = msg_engine
        self.frame = 0
    def draw(self, screen):
        Window.draw(self, screen)
        if self.is_visible == False: return
        # ステータスを描画
        status_str = [self.status[0], u"H%3d" % self.status[1], u"M%3d" % self.status[2], u"%s%3d" % (self.status[0][0], self.status[3])]
        for i in range(0, 4):
            dx = self.text_rect[0]
            dy = self.text_rect[1] + (self.LINE_HEIGHT+MessageEngine.FONT_HEIGHT) * (i % 4)
            self.msg_engine.draw_string(screen, (dx,dy), status_str[i])

実は戦闘画面まで取り組んだのは今回が初めてでこのままつっぱしていって本当に完成できるのかわかりません・・・すこし実装に無理が出始めているかもしれません。そろそろイベントスクリプトをきちんと設計しないとなーと思ってます。