Pygameを使って RPGを作る（21.Start画面）

# 概要
・ゲーム開始画面を追加
・音楽BGMを追加

# 画面遷移ロジック

Start画面でStartボタン→フィールドマップへ
敵衝突→Battle画面
戦闘で死亡→gameover
戦闘で残存→フィールドマップへ
gameover→Start画面

Start画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576081/5067cca0-9d98-fbea-c93a-eed441be99f0.png)

フィールドマップ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576081/cb7e0279-47ee-e13c-ae7a-1779ad9761d9.png)

バトル画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576081/989edfbb-9872-49cd-83c0-c6011e7ba3f7.png)

Game Over
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576081/16c1c578-3d70-655a-f6e5-c4619256c3e7.png)

# Start Gameクラス
```py 
class StartMenu:
    def __init__(self, parent):
        self.parent = parent
        self.screen = self.parent.display_surface
        self.start_sprites = pg.sprite.Group()
        self.font_title = pg.font.Font("appmin.otf", 36)
        self.font_description = pg.font.Font("appmin.otf", 24)
        self.forecolor = "#FFFFFF"
        self.text = TextSprite('北境の黎明', self.font_title, 
                               self.forecolor,  
                               (0,0,255), 
                               WIDTH / 2 - 200, 50, self.start_sprites)
        
        self.speed = 50
        
        
        self.img = pg.image.load('../img/winter-forest1.jpg')
        self.back_ground_img = pg.transform.scale(self.img, (WIDTH, HEIGHT))
        self.rect = self.back_ground_img.get_rect()
        
        self.story_description = '清朝末期。歴史に稀を見る時代は混乱を極み、\n\
中原の人々は天災、戦乱、飢饉により、酷寒で漢民族の禁足の地の満州に渡った、\n\
その中、ひとりの若者が静かに動き出すのだった...'
        self.descriptions = TextAnimation(
            self.font_description,
            self.forecolor, (0,0,255),
            WIDTH*0.15 , HEIGHT /2 -200 , 
            self.speed,
            self.screen
        )
        
        self.px = WIDTH / 2
        self.pos_y = [HEIGHT /2 + 100, HEIGHT /2 + 150, HEIGHT /2 + 200]

        self.buttons = self.create_buttons(self.get_actions(), 
                                           self.px, 
                                           self.pos_y)
        
        self.counter = 0

    def get_actions(self):
        return [
            ("Start", self.start),
            ("Load", self.load),
            ("Settings", self.option),
        ]
    
    def create_buttons(self, actions, x, y_positons):
        btn_width, btn_height = 100, 40
        return [
            Button(x, y_positons[i], btn_width, btn_height, name, action)
            for i, (name, action) in enumerate(actions)
        ]

    def draw(self):
        self.screen.blit(self.back_ground_img, self.rect)
        self.text_area = pg.Rect(WIDTH*(0.15)-5, HEIGHT*0.05, WIDTH * 0.7, HEIGHT * 0.4)
        surf = pg.Surface(self.text_area.size, pg.SRCALPHA)
        surf.fill((10,15,5,128))
        self.screen.blit(surf, self.text_area)
        self.text.draw(self.screen)
        for button in self.buttons:
            button.draw(self.screen)

    def handle_mouse_event(self, event):
        if event.type == pg.MOUSEBUTTONDOWN:
            mouse_pos = event.pos  # クリックした位置を取得
            for button in self.buttons:
                if button.check_click(mouse_pos):  # ボタンがクリックされたか判定
                    button.action()  # ボタンに設定された関数を呼び出し
                    return True

    def start(self):
        print('start')
        self.parent.reset_game_state()
        self.parent.game_stage = 'main'

    def load(self):
        print('load')

    def option(self):
        print('option')
```

# メインクラス
```py
import pygame as pg
from settings import *

from map import Map
from groups import AllSprites
from battle import BattleScreen
from game_over import GameOver, StartMenu
from utils import Backmusic
import os


class Game:
    def __init__(self):
        pg.mixer.init()
        pg.init()

        # screen
        self.display_surface = pg.display.set_mode((WIDTH, HEIGHT))
        # title
        pg.display.set_caption(TITLE)

        # clock
        self.clock = pg.time.Clock()
        self.running = True

        # all sprite
        self.all_sprites = AllSprites()

        self.battle_sprites = AllSprites()

        self.game_stage = 'start_menu'
        self.current_stage = self.game_stage

        self.start = StartMenu(self)

        
        # バトル管理
        self.init_battle = True  # バトル画面初期化フラグ
        self.update_message_flag = False

        # バトル初期化
        self.battle = BattleScreen(self, self.battle_sprites)

        # Map
        self.player, self.current_map = Map(self, self.all_sprites).create()

        self.bgm_dict ={
            "start_menu": '../music/kaisou/winter_flut.mp3',
            "main": '../music/town/Sky-Airship.mp3',
            "battle": '../music/battle/Battle-Rosemoon.mp3',
            "game_over": '../music/kaisou/reminiscence.mp3',
        }

    def show_game_menu(self, stage, dt):
        menus = {
            "main": self.main_screen,
            "battle": self.show_battle_screen,
            "game_over": self.show_game_over,
            "start_menu": self.start_menu
        }
        return menus[stage](dt)
    
    def play_background_music(self, stage):
        """ 音楽を再生する """
        bgm_path = self.bgm_dict.get(stage)

        if bgm_path and os.path.exists(bgm_path):  # ファイルの存在チェック
            self.bgm = Backmusic(bgm_path)
            self.bgm.play()
            print(bgm_path)
        else:
            print(f"音楽ファイルが見つかりません: {bgm_path}")

        
    def run(self):
        """ゲームループ"""
        dt = self.clock.tick(FPS) /1000

        while self.running:

            # events
            self.events()
   
            self.show_game_menu(self.game_stage, dt)

            if self.current_stage != self.game_stage:
                self.play_background_music(self.game_stage)
                self.current_stage = self.game_stage

            if not pg.mixer.music.get_busy():
                self.play_background_music(self.game_stage)

            pg.display.flip()
        
        pg.quit()

    def start_menu(self, dt):
        self.display_surface.fill((BLUE))
        
        if self.start.counter >= len(self.start.story_description) * self.start.speed:
            pass
        else:
            self.start.counter += 1
        # print(self.start.counter)
        # スタート画面
        # self.start = StartMenu(self)
        self.start.draw()
        self.start.descriptions.draw_anime(self.start.story_description, self.start.counter)
        

    def main_screen(self, dt):
        self.all_sprites.update(dt, self.current_map)
        self.display_surface.fill(BLUE)
        self.all_sprites.draw()

    def cal_text_speed(self):
        if len(self.battle.battle_message) > MAX_MESSAGE:
            del self.battle.battle_message[0]
        if len(self.battle.battle_message) > 0:
            if self.battle.counter <= self.battle.speed * len(self.battle.battle_message[-1]):
                self.battle.counter += 1

    def replace_message(self, message):
        temp_message = []
        for m in message:
            sp = m.split('\n')
            for s in sp:
                temp_message.append(s)
                if len(temp_message) > MAX_MESSAGE:
                    del temp_message[0]

        return temp_message

    def show_battle_screen(self, dt):

        # バトル画面の初期化
        if self.init_battle:
            self.battle.battle_message = []
            self.battle.counter = 0
            self.init_battle = False  

            # バトル画面描画
            self.battle.draw(self.player, self.display_surface)
            self.battle_sprites.draw_battle()
        
        # 戦闘時メッセージの更新
        if self.update_message_flag:
            self.battle.update_message(self.display_surface)
            self.battle_sprites.draw_battle()
            self.battle.battle_message = self.replace_message(self.battle.battle_message)
            # self.battle.get_battle_result()
            self.update_message_flag = False
            self.battle.counter = 0

        # 戦闘コマンドの描画(マウスホーバーを検知するため、ループの外側で実装)
        if self.battle.battle_active:
            self.battle.draw_buttons()

        self.battle.status.draw_status(self.display_surface)
        
        self.battle.text_sprites.update(dt)

        self.cal_text_speed()
        
        if len(self.battle.battle_message) > 0:
            # 戦闘メッセージの描画
            self.battle.text_sprites.draw(self.battle.battle_message, self.battle.counter)
        

    def show_game_over(self, dt):
        self.display_surface.fill((0, 0, 0))  # RGBで黒 (0, 0, 0)
        self.game = GameOver(self)
        self.game.draw()

    # メインステージ、敵の数、Playerの数上手くリセットできていない
    def reset_game_state(self):
        # 全てのスプライトを再生成
        self.all_sprites = AllSprites()

        # プレイヤーとマップを再生成
        self.player, self.current_map = Map(self, self.all_sprites).create()

        # バトルの状態を完全にリセット
        self.battle = BattleScreen(self, self.battle_sprites)
        self.init_battle = True
        self.battle.reset()

        # ゲームステージを"main"に戻す
        self.game_stage = 'main'

    def events(self):
        for event in pg.event.get():
            if event.type == pg.QUIT:
                self.running = False

            if event.type == pg.KEYDOWN:
                # バトル終了後、メイン画面に戻る処理
                if event.key == pg.K_SPACE and self.game_stage == 'battle':
                    self.battle.get_battle_result()

            # BattleScreenのマウスイベントを処理
            self.update_message_flag = self.battle.handle_mouse_event(event)

            self.start.handle_mouse_event(event)

if __name__ == "__main__":
    new_game = Game()
    new_game.run()

```
# 感想
ゲームコードを書いているとどんどん冗長になってしまうので、
1.とりあえず、動くものをつくる
2.コードをクラス分割
3.コードスリム化

今回は、バトル画面終了判定について、クラス内へ移動し、クラス内で画面遷移できるようにした。
メリットとしては、メインクラスのスリム化ができて、画面遷移は、各クラス内に移譲することで、
個別管理ができるので、問題があった時の影響範囲は小さくなると考えられる。

before
```py 
def events(self):
    # バトル終了後、メイン画面に戻る処理
    if event.key == pg.K_SPACE and self.game_stage == 'battle':
        if self.battle.get_battle_result() == 0:
            self.player.game_stage = 'game_over'
        elif self.battle.get_battle_result() == 1:
            self.init_battle = True
            self.battle.battle_active = True
            self.player.game_stage = "main"  # "main"に戻す
        
    # Game Overからメイン画面に戻る処理
    if event.key == pg.K_SPACE and self.game_stage == 'game_over':
        self.reset_game_state()
```
after
```py 
if event.key == pg.K_SPACE and self.game_stage == 'battle':
    self.battle.get_battle_result()
```

battleクラス内
before
```py
def get_battle_result(self):
    if self.enemy.mob_info['HP'] <= 0:
        return 1
    elif self.status.view_status['HP'] <=0:
        return 0
    else:
        return 3
```
after
```py
def get_battle_result(self):
    if self.enemy.mob_info['HP'] <= 0:
        self.parent.init_battle = True
        self.battle_active = True
        self.parent.game_stage = 'main'
    elif self.status.view_status['HP'] == 0:
        self.parent.game_stage = 'game_over'
```