人工知能に関する断創録

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

ゲームオーバー画面

タイトル画面とゲームオーバー画面を追加します。画面間の遷移はゲーム状態を導入するとわりと簡単にできます。

invader07.zip
f:id:aidiary:20100808000102p:plain
f:id:aidiary:20100808000101p:plain

ゲームのクラス化

前回までと違ってmain()に書いていた処理をInvaderクラスに変えました。

START, PLAY, GAMEOVER = (0, 1, 2)  # ゲーム状態
SCR_RECT = Rect(0, 0, 640, 480)

class Invader:
    def __init__(self):
        pygame.init()
        screen = pygame.display.set_mode(SCR_RECT.size)
        pygame.display.set_caption(u"Invader 07 ゲームオーバー画面")
        # 素材のロード
        self.load_images()
        self.load_sounds()
        # ゲームオブジェクトを初期化
        self.init_game()
        # メインループ開始
        clock = pygame.time.Clock()
        while True:
            clock.tick(60)
            self.update()
            self.draw(screen)
            pygame.display.update()
            self.key_handler()
    def init_game(self):
        """ゲームオブジェクトを初期化"""
        # ゲーム状態
        self.game_state = START
        # スプライトグループを作成して登録
        self.all = pygame.sprite.RenderUpdates()
        self.aliens = pygame.sprite.Group()  # エイリアングループ
        self.shots = pygame.sprite.Group()   # ミサイルグループ
        self.beams = pygame.sprite.Group()   # ビームグループ
        # デフォルトスプライトグループを登録
        Player.containers = self.all
        Shot.containers = self.all, self.shots
        Alien.containers = self.all, self.aliens
        Beam.containers = self.all, self.beams
        Explosion.containers = self.all
        # 自機を作成
        self.player = Player()
        # エイリアンを作成
        for i in range(0, 50):
            x = 20 + (i % 10) * 40
            y = 20 + (i / 10) * 40
            Alien((x,y))
    def update(self):
        """ゲーム状態の更新"""
        if self.game_state == PLAY:
            self.all.update()
            # ミサイルとエイリアンの衝突判定
            self.collision_detection()
            # エイリアンをすべて倒したらゲームオーバー
            if len(self.aliens.sprites()) == 0:
                self.game_state = GAMEOVER
    def draw(self, screen):
        """描画"""
        screen.fill((0, 0, 0))
        if self.game_state == START:  # スタート画面
            # タイトルを描画
            title_font = pygame.font.SysFont(None, 80)
            title = title_font.render("INVADER GAME", False, (255,0,0))
            screen.blit(title, ((SCR_RECT.width-title.get_width())/2, 100))
            # エイリアンを描画
            alien_image = Alien.images[0]
            screen.blit(alien_image, ((SCR_RECT.width-alien_image.get_width())/2, 200))
            # PUSH STARTを描画
            push_font = pygame.font.SysFont(None, 40)
            push_space = push_font.render("PUSH SPACE KEY", False, (255,255,255))
            screen.blit(push_space, ((SCR_RECT.width-push_space.get_width())/2, 300))
            # クレジットを描画
            credit_font = pygame.font.SysFont(None, 20)
            credit = credit_font.render(u"2008 http://pygame.skr.jp", False, (255,255,255))
            screen.blit(credit, ((SCR_RECT.width-credit.get_width())/2, 380))
        elif self.game_state == PLAY:  # ゲームプレイ画面
            self.all.draw(screen)
        elif self.game_state == GAMEOVER:  # ゲームオーバー画面
            # GAME OVERを描画
            gameover_font = pygame.font.SysFont(None, 80)
            gameover = gameover_font.render("GAME OVER", False, (255,0,0))
            screen.blit(gameover, ((SCR_RECT.width-gameover.get_width())/2, 100))
            # エイリアンを描画
            alien_image = Alien.images[0]
            screen.blit(alien_image, ((SCR_RECT.width-alien_image.get_width())/2, 200))
            # PUSH STARTを描画
            push_font = pygame.font.SysFont(None, 40)
            push_space = push_font.render("PUSH SPACE KEY", False, (255,255,255))
            screen.blit(push_space, ((SCR_RECT.width-push_space.get_width())/2, 300))
    def key_handler(self):
        """キーハンドラー"""
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == KEYDOWN and event.key == K_ESCAPE:
                pygame.quit()
                sys.exit()
            elif event.type == KEYDOWN and event.key == K_SPACE:
                if self.game_state == START:  # スタート画面でスペースを押したとき
                    self.game_state = PLAY
                elif self.game_state == GAMEOVER:  # ゲームオーバー画面でスペースを押したとき
                    self.init_game()  # ゲームを初期化して再開
                    self.game_state = PLAY
    def collision_detection(self):
        """衝突判定"""
        # エイリアンとミサイルの衝突判定
        alien_collided = pygame.sprite.groupcollide(self.aliens, self.shots, True, True)
        for alien in alien_collided.keys():
            Alien.kill_sound.play()
            Explosion(alien.rect.center)  # エイリアンの中心で爆発
        # プレイヤーとビームの衝突判定
        beam_collided = pygame.sprite.spritecollide(self.player, self.beams, True)
        if beam_collided:  # プレイヤーと衝突したビームがあれば
            Player.bomb_sound.play()
            self.game_state = GAMEOVER  # ゲームオーバー!
    def load_images(self):
        """イメージのロード"""
        # スプライトの画像を登録
        Player.image = load_image("player.png")
        Shot.image = load_image("shot.png")
        Alien.images = split_image(load_image("alien.png"), 2)
        Beam.image = load_image("beam.png")
        Explosion.images = split_image(load_image("explosion.png"), 16)
    def load_sounds(self):
        """サウンドのロード"""
        Alien.kill_sound = load_sound("kill.wav")
        Player.shot_sound = load_sound("shot.wav")
        Player.bomb_sound = load_sound("bomb.wav")

クラス化すると各メソッドで共通して使う変数にアクセスしやすくなります。たとえば、デフォルトスプライトグループのallや自機のplayer は、self.all、self.playerとしてクラスのインスタンス変数にすることで他のすべてのメソッドからアクセスできます。そのため、 collision_detection()にも引数で渡さなくてすみます。ゲームを初期化する処理はinit_game()にまとめました。ゲームオーバーになって再スタートするときにinit_game()を呼び出すことでエイリアンの配置が初期化されます。その他にも、今までmain()にまとめて書いていた処理をdraw()、update()、key_handler()、load_images()、load_sounds()などのメソッドに移して機能ごとにまとめてあります。機能ごとにメソッドにまとめるとプログラムが読みやすくなります。

ゲーム状態

タイトル画面やゲームオーバー画面など新しい画面を導入するにはゲーム状態を使うとわりと簡単に整理できます。ゲーム状態はself.game_stateという変数で管理しています。

  START, PLAY, GAMEOVER = (0, 1, 2)  # ゲーム状態
  self.game_state = PLAY

とりえる値はSTART(オープニング画面)、PLAY(プレイ画面)、GAMEOVER(ゲームオーバー画面)の3つです。self.game_stateによって画面に描画される内容や更新内容を変えます。たとえば、draw()を見てください。

    def draw(self, screen):
        """描画"""
        screen.fill((0, 0, 0))
        if self.game_state == START:  # スタート画面
            # タイトルを描画
            title_font = pygame.font.SysFont(None, 80)
            title = title_font.render("INVADER GAME", False, (255,0,0))
            screen.blit(title, ((SCR_RECT.width-title.get_width())/2, 100))
            # エイリアンを描画
            alien_image = Alien.images[0]
            screen.blit(alien_image, ((SCR_RECT.width-alien_image.get_width())/2, 200))
            # PUSH STARTを描画
            push_font = pygame.font.SysFont(None, 40)
            push_space = push_font.render("PUSH SPACE KEY", False, (255,255,255))
            screen.blit(push_space, ((SCR_RECT.width-push_space.get_width())/2, 300))
            # クレジットを描画
            credit_font = pygame.font.SysFont(None, 20)
            credit = credit_font.render(u"2008 http://pygame.skr.jp", False, (255,255,255))
            screen.blit(credit, ((SCR_RECT.width-credit.get_width())/2, 380))
        elif self.game_state == PLAY:  # ゲームプレイ画面
            self.all.draw(screen)
        elif self.game_state == GAMEOVER:  # ゲームオーバー画面
            # GAME OVERを描画
            gameover_font = pygame.font.SysFont(None, 80)
            gameover = gameover_font.render("GAME OVER", False, (255,0,0))
            screen.blit(gameover, ((SCR_RECT.width-gameover.get_width())/2, 100))
            # エイリアンを描画
            alien_image = Alien.images[0]
            screen.blit(alien_image, ((SCR_RECT.width-alien_image.get_width())/2, 200))
            # PUSH STARTを描画
            push_font = pygame.font.SysFont(None, 40)
            push_space = push_font.render("PUSH SPACE KEY", False, (255,255,255))
            screen.blit(push_space, ((SCR_RECT.width-push_space.get_width())/2, 300))

self.game_stateがどの状態にあるかによって描画する内容を変えていることがわかります。START状態では、タイトル、エイリアン、PUSH_START、クレジットを描画しているのに対し、PLAY状態ではすべてのスプライトを描画しています。また、ゲーム状態を更新する update()も同様にゲーム状態によって更新内容を変えます。

    def update(self):
        """ゲーム状態の更新"""
        if self.game_state == PLAY:
            self.all.update()
            # ミサイルとエイリアンの衝突判定
            self.collision_detection()
            # エイリアンをすべて倒したらゲームオーバー
            if len(self.aliens.sprites()) == 0:
                self.game_state = GAMEOVER

この場合、スタート画面とゲームオーバー画面は動きがまったくないので画面は更新せず、PLAY状態のときのみスプライトを更新しています。また、キー入力を受け付けるkey_handler()もゲーム状態によってキー処理を変えています。

    def key_handler(self):
        """キーハンドラー"""
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == KEYDOWN and event.key == K_ESCAPE:
                pygame.quit()
                sys.exit()
            elif event.type == KEYDOWN and event.key == K_SPACE:
                if self.game_state == START:  # スタート画面でスペースを押したとき
                    self.game_state = PLAY
                elif self.game_state == GAMEOVER:  # ゲームオーバー画面でスペースを押したとき
                    self.init_game()  # ゲームを初期化して再開
                    self.game_state = PLAY

START状態でスペースキーを押したときはゲーム状態をPLAYに変え、GAMEOVER状態でスペースキーを押したときはゲームを初期化した後にゲーム状態をPLAYに変えています。ゲーム状態を変えると画面に表示される内容も自動的に変化します。このようにゲーム状態によって描画する内容、更新する内容、キー入力を変えるのがポイントです。

ゲーム状態の遷移

次にSTART、PLAY、GAMEOVERの状態間でどのように遷移するか考えます。

f:id:aidiary:20100808000104p:plain

ゲームをプレイするとすぐわかりますがSTART状態でスペースキーを押すとPLAY状態へ、GAMEOVER状態でスペースキーを押すとPLAY 状態へ移ります。この処理は先ほどkey_handler()のところで説明しました。また、PLAY状態で自機がエイリアンの出すビームに当たるとゲームオーバーになります。この処理は、collision_detection()に追加してあります。

    def collision_detection(self):
        """衝突判定"""
        # エイリアンとミサイルの衝突判定
        alien_collided = pygame.sprite.groupcollide(self.aliens, self.shots, True, True)
        for alien in alien_collided.keys():
            Alien.kill_sound.play()
            Explosion(alien.rect.center)  # エイリアンの中心で爆発
        # プレイヤーとビームの衝突判定
        beam_collided = pygame.sprite.spritecollide(self.player, self.beams, True)
        if beam_collided:  # プレイヤーと衝突したビームがあれば
            Player.bomb_sound.play()
            self.game_state = GAMEOVER  # ゲームオーバー!

このように画面間の遷移は、ゲーム状態を導入してその状態遷移を考えることでわりと簡単に実現できます。タイトル画面やゲームオーバー画面でアニメーションを使うなどもっとこったことをしたいときはdraw()とupdate()を持った専用のTitleクラス、GameOverクラスを作ることもできます。