戦闘画面
まだ戦えるわけではありませんが、戦闘画面を実装してみました。フィールドを歩いていると何か強そうな(笑)モンスターに襲われます。コマンドの選択などはすべてスペースキーを使います。とりあえずにげることしかできません。モンスターのグラフィックは、HOT TOKE(リンク切れ)からお借りしています。ここは大量のかっこいいモンスターグラフィックがあってすばらしいです。戦闘のBGMは、煉獄庭園さんからお借りしています。「最期のレクイエム」というMP3の曲です。聴いた瞬間に戦闘に使いたいと思いました。
戦闘の状態遷移
ゲーム状態の導入でタイトル画面を作ったのと同じように戦闘画面もゲーム状態で管理します。戦闘は流れが複雑なのでとりあえず下図に示すようにBATTLE_INIT、BATTLE_COMMAND、BATTLE_PROCESSの3つの状態を用意しました。
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]) # 選択中のコマンドの左側に▶を描画 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])
実は戦闘画面まで取り組んだのは今回が初めてでこのままつっぱしていって本当に完成できるのかわかりません・・・すこし実装に無理が出始めているかもしれません。そろそろイベントスクリプトをきちんと設計しないとなーと思ってます。