Pythonで作るインベーダーゲーム_スコアシステムの実装

この章では、ゲーム「インベーダーゲーム」の開発を完了させます。プレイボタンを追加して、必要に応じてゲームを開始し、ゲーム終了後に再開できるようにします。また、プレイヤーのレベルが上がるにつれてゲームのペースを速くし、スコアシステムを実装します。この章を読み終えると、レベルが上がるにつれて難易度が上がり、スコアが表示されるゲームを作成するための十分な知識が得られるでしょう。

1. プレイボタンの追加

このセクションでは、ゲーム開始前に表示され、ゲーム終了後に再び表示されるプレイボタンを追加して、プレイヤーが新しいゲームを開始できるようにします。

現在、このゲームはプレイヤーがalien_invasion.pyを実行するとすぐに開始されます。ここで、ゲームを最初は非アクティブ状態にし、プレイヤーがプレイボタンをクリックして開始するように促します。そのために、game_stats.pyに以下のコードを入力します:

game_stats.py

def __init__(self, ai_settings):
    """統計情報の初期化"""
    self.ai_settings = ai_settings
    self.reset_stats()
    # ゲームを最初は非アクティブ状態にする
    self.game_active = False

これでゲームは最初は非アクティブ状態になり、プレイボタンを作成した後、プレイヤーはゲームを開始できるようになります。

1.1 Buttonクラスの作成

Pygameにはボタンを作成する組み込みメソッドがないため、ラベル付きの塗りつぶされた四角形を作成するためのButtonクラスを作成します。このコードを使用して、ゲーム内で任意のボタンを作成できます。以下はButtonクラスの最初の部分です。このクラスをbutton.pyというファイル名で保存してください:

button.py

import pygame.font

class Button():
    def __init__(self, ai_settings, screen, msg):
        """ボタンの属性を初期化"""
        self.screen = screen
        self.screen_rect = screen.get_rect()
        
        # ボタンの寸法とその他の属性を設定
        self.width, self.height = 200, 50
        self.button_color = (0, 255, 0)
        self.text_color = (255, 255, 255)
        self.font = pygame.font.SysFont(None, 48)
        
        # ボタンのrectオブジェクトを作成し、中央に配置
        self.rect = pygame.Rect(0, 0, self.width, self.height)
        self.rect.center = self.screen_rect.center
        
        # ボタンのラベルは一度だけ作成
        self.prep_msg(msg)

まず、pygame.fontモジュールをインポートします。これによりPygameはテキストを画面にレンダリングできるようになります。init()メソッドは、self、ai_settingsオブジェクト、screenオブジェクト、そしてボタンに表示するテキストであるmsgを受け取ります。ボタンの寸法を設定し、button_colorを設定してボタンのrectオブジェクトを明るい緑色にし、text_colorを設定してテキストを白色にします。

テキストをレンダリングするフォントを指定します。Noneを引数として渡すとPygameはデフォルトフォントを使用し、48はテキストのフォントサイズを指定します。ボタンを画面の中央に配置するために、ボタンを表すrectオブジェクトを作成し、そのcenterプロパティを画面のcenterプロパティに設定します。

Pygameは、表示する文字列を画像としてレンダリングすることでテキストを処理します。prep_msg()を呼び出して、このようなレンダリングを処理します。

prep_msg()のコードは以下の通りです:

button.py

def prep_msg(self, msg):
    """msgを画像としてレンダリングし、ボタン上で中央に配置"""
    self.msg_image = self.font.render(msg, True, self.text_color,
                                      self.button_color)
    self.msg_image_rect = self.msg_image.get_rect()
    self.msg_image_rect.center = self.rect.center

prep_msg()メソッドはselfと、画像としてレンダリングするテキスト(msg)を受け取ります。font.render()を呼び出して、msgに格納されているテキストを画像に変換し、その画像をmsg_imageに格納します。font.render()メソッドは、アンチエイリアス機能を有効にするか無効にするかを指定するブール引数も受け取ります(アンチエイリアスによりテキストの端が滑らかになります)。残りの2つの引数は、テキストの色と背景色です。アンチエイリアス機能を有効にし、テキストの背景色をボタンの色に設定します(背景色を指定しない場合、Pygameは透明な背景でテキストをレンダリングします)。

テキスト画像をボタンの中央に配置するために、テキスト画像に基づいてrectを作成し、そのcenterプロパティをボタンのcenterプロパティに設定します。

最後に、このボタンを画面に表示するために呼び出すdraw_button()メソッドを作成します:

button.py

def draw_button(self):
    # 色で塗りつぶされたボタンを描画し、その上にテキストを描画
    self.screen.fill(self.button_color, self.rect)
    self.screen.blit(self.msg_image, self.msg_image_rect)

screen.fill()を呼び出してボタンを表す四角形を描画し、次にscreen.blit()を呼び出して、画像とその画像に関連付けられたrectオブジェクトを渡すことで、画面上にテキスト画像を描画します。これでButtonクラスが完成しました。

1.2 画面上にボタンを描画する

Buttonクラスを使用してプレイボタンを作成します。プレイボタンは1つだけ必要なので、alien_invasion.pyに直接作成します:

alien_invasion.py

--snip--
from game_stats import GameStats
from button import Button
--snip--
def run_game():
    --snip--
    pygame.display.set_caption("Alien Invasion")
    
    # プレイボタンを作成
    play_button = Button(ai_settings, screen, "Play")
    --snip--
    
    # ゲームのメインループを開始
    while True:
        --snip--
        gf.update_screen(ai_settings, screen, stats, ship, aliens, bullets,
                         play_button)
run_game()

Buttonクラスをインポートし、play_buttonという名前のインスタンスを作成し、次にplay_buttonをupdate_screen()に渡して、画面更新時にボタンが表示されるようにします。

次に、ゲームが非アクティブ状態のときにプレイボタンを表示するようにupdate_screen()を変更します:

game_functions.py

def update_screen(ai_settings, screen, stats, ship, aliens, bullets,
                  play_button):
    """画面上の画像を更新し、新しい画面に切り替える"""
    --snip--
    
    # ゲームが非アクティブ状態の場合、プレイボタンを描画
    if not stats.game_active:
        play_button.draw_button()
    
    # 最近描画した画面を表示
    pygame.display.flip()

プレイボタンを他のすべての画面要素の上に配置するために、他のすべてのゲーム要素を描画した後にこのボタンを描画し、次に新しい画面に切り替えます。今ゲームを実行すると、画面中央にプレイボタンが表示されます。

1.3 ゲームの開始

プレイヤーがプレイボタンをクリックしたときに新しいゲームを開始するには、game_functions.pyに以下のコードを追加して、このボタンに関連するマウスイベントを監視します:

game_functions.py

def check_events(ai_settings, screen, stats, play_button, ship, bullets):
    """キーとマウスのイベントに応答"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            --snip--
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_x, mouse_y = pygame.mouse.get_pos()
            check_play_button(stats, play_button, mouse_x, mouse_y)

def check_play_button(stats, play_button, mouse_x, mouse_y):
    """プレイヤーがプレイボタンをクリックしたときに新しいゲームを開始"""
    if play_button.rect.collidepoint(mouse_x, mouse_y):
        stats.game_active = True

check_events()の定義を変更して、statsとplay_buttonの引数を追加しました。statsを使用してgame_activeフラグにアクセスし、play_buttonを使用してプレイヤーがプレイボタンをクリックしたかどうかを確認します。

プレイヤーが画面のどこをクリックしても、PygameはMOUSEBUTTONDOWNイベントを検出しますが、プレイヤーがプレイボタンをマウスでクリックした場合にのみゲームが応答するようにしたいです。そのために、pygame.mouse.get_pos()を使用します。これは、プレイヤーがクリックしたときのマウスのx座標とy座標を含むタプルを返します。これらの値を関数check_play_button()に渡し、この関数はcollidepoint()を使用してマウスクリック位置がプレイボタンのrect内にあるかどうかを確認します。もしそうであれば、game_activeをTrueに設定して、ゲームを開始します!

alien_invasion.pyでcheck_events()を呼び出すには、さらに2つの引数—statsとplay_button—を渡す必要があります:

alien_invasion.py

# ゲームのメインループを開始
while True:
    gf.check_events(ai_settings, screen, stats, play_button, ship,
                    bullets)
    --snip--

これで、ゲームを開始できるはずです。ゲームが終了すると、game_activeはFalseになり、プレイボタンが再表示されます。

1.4 ゲームのリセット

前に書いたコードは、プレイヤーが初めてプレイボタンをクリックした場合のみを処理しており、ゲーム終了の場合を処理していません。ゲーム終了の原因となった条件がリセットされていないからです。

プレイヤーがプレイボタンをクリックするたびにゲームをリセットするには、統計情報をリセットし、既存のエイリアンと弾丸を削除し、新しいエイリアンの群れを作成し、宇宙船を中央に配置する必要があります:

game_functions.py

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens,
                      bullets, mouse_x, mouse_y):
    """プレイヤーがプレイボタンをクリックしたときに新しいゲームを開始"""
    if play_button.rect.collidepoint(mouse_x, mouse_y):
        # ゲーム統計情報をリセット
        stats.reset_stats()
        stats.game_active = True
        
        # エイリアンリストと弾丸リストを空にする
        aliens.empty()
        bullets.empty()
        
        # 新しいエイリアンの群れを作成し、宇宙船を中央に配置
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()

check_play_button()の定義を更新して、ai_settings、stats、ship、aliens、bulletsにアクセスできるようにしました。ゲーム中に変更された設定をリセットし、ゲームの視覚要素を更新するために、これらのオブジェクトが必要です。

ゲーム統計情報をリセットし、プレイヤーに3つの新しい宇宙船を提供します。次に、game_activeをTrueに設定し(これにより、この関数のコードが実行された後、ゲームが開始されます)、グループaliensとbulletsを空にし、新しいエイリアンの群れを作成し、宇宙船を中央に配置します。

check_events()の定義とcheck_play_button()を呼び出すコードを変更する必要があります:

game_functions.py

def check_events(ai_settings, screen, stats, play_button, ship, aliens,
                  bullets):
    """キーとマウスのイベントに応答"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            --snip--
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_x, mouse_y = pygame.mouse.get_pos()
            check_play_button(ai_settings, screen, stats, play_button, ship,
                              aliens, bullets, mouse_x, mouse_y)

check_events()の定義にはaliensの引数が必要で、それをcheck_play_button()に渡せるようにします。次に、check_play_button()を呼び出すコードを変更して、適切な引数を渡すようにします。

alien_invasion.pyでcheck_events()を呼び出すコードを変更して、引数aliensを渡すようにします:

alien_invasion.py

# ゲームのメインループを開始
while True:
    gf.check_events(ai_settings, screen, stats, play_button, ship,
                    aliens, bullets)
    --snip--

これで、プレイヤーがプレイボタンをクリックするたびに、ゲームが正しくリセットされ、プレイヤーは好きなだけ回数プレイできるようになります!

1.5 プレイボタンを非アクティブ状態に切り替える

現在、プレイボタンには、プレイボタンが見えなくても、プレイヤーが元の場所をクリックするとゲームが応答してしまうという問題があります。ゲームが開始した後、プレイヤーが誤ってプレイボタンがあった領域をクリックすると、ゲームが再開始してしまいます!

この問題を修正するには、game_activeがFalseの場合にのみゲームを開始するようにします:

game_functions.py

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens,
                      bullets, mouse_x, mouse_y):
    """プレイヤーがプレイボタンをクリックしたときに新しいゲームを開始"""
    button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)
    if button_clicked and not stats.game_active:
        # ゲーム統計情報をリセット
        --snip--

button_clickedフラグの値はTrueまたはFalseであり、プレイヤーがプレイボタンをクリックし、かつゲームが現在非アクティブ状態である場合にのみ、ゲームが再開始されます。この動作をテストするには、新しいゲームを開始し、プレイボタンがあった領域を繰り返しクリックします。すべてが期待通りに機能する場合、プレイボタンがあった領域をクリックしても影響はないはずです。

1.6 カーソルを非表示にする

プレイヤーがゲームを開始できるように、カーソルを表示する必要がありますが、ゲームが開始した後、カーソルは邪魔になります。この問題を修正するために、ゲームがアクティブ状態のときにカーソルを非表示にします:

game_functions.py

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens,
                      bullets, mouse_x, mouse_y):
    """プレイヤーがプレイボタンをクリックしたときに新しいゲームを開始"""
    button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)
    if button_clicked and not stats.game_active:
        # カーソルを非表示にする
        pygame.mouse.set_visible(False)
        --snip--

set_visible()にFalseを渡すことで、Pygameがカーソルをゲームウィンドウ内に非表示にします。

ゲームが終了したら、カーソルを再表示して、プレイヤーがプレイボタンをクリックして新しいゲームを開始できるようにします。関連するコードは以下の通りです:

game_functions.py

def ship_hit(ai_settings, screen, stats, ship, aliens, bullets):
    """宇宙船がエイリアンに衝突したときに応答"""
    if stats.ships_left > 0:
        --snip--
    else:
        stats.game_active = False
        pygame.mouse.set_visible(True)

ship_hit()では、ゲームが非アクティブ状態になった直後にカーソルを可視にします。このような詳細に注意を払うことで、ゲームがよりプロフェッショナルに見え、プレイヤーはユーザーインターフェースを理解するのに苦労するのではなく、ゲームプレイに集中できるようになります。

2. レベルの向上

現在、画面上のエイリアンをすべて消滅させると、プレイヤーはレベルアップしますが、ゲームの難易度は変わりません。ここで少し面白さを加えましょう:プレイヤーが画面上のエイリアンをすべて消滅させるたびに、ゲームのペースを速くして、ゲームをより難しくします。

2.1 速度設定の変更

まず、Settingsクラスを再編成して、ゲーム設定を静的と動的の2つのグループに分けます。ゲームの進行に伴って変化する設定については、新しいゲームが開始されたときにリセットされるようにします。settings.pyの__init__()メソッドは以下の通りです:

settings.py

def __init__(self):
    """ゲームの静的設定を初期化"""
    # 画面設定
    self.screen_width = 1200
    self.screen_height = 800
    self.bg_color = (230, 230, 230)
    
    # 宇宙船設定
    self.ship_limit = 3
    
    # 弾丸設定
    self.bullet_width = 3
    self.bullet_height = 15
    self.bullet_color = 60, 60, 60
    self.bullets_allowed = 3
    
    # エイリアン設定
    self.fleet_drop_speed = 10
    
    # どのくらいの速さでゲームのペースを上げるか
    self.speedup_scale = 1.1
    self.initialize_dynamic_settings()

依然として__init__()で静的設定を初期化します。speedup_scale設定を追加しました。これはゲームのペースを上げる速度を制御します:2はプレイヤーがレベルアップするたびにゲームのペースが2倍になることを意味し、1はゲームのペースが常に変わらないことを意味します。これを1.1に設定すると、ゲームのペースが十分に速くなり、ゲームは難しくなりますが、不可能ではありません。最後に、initialize_dynamic_settings()を呼び出して、ゲームの進行に伴って変化する属性を初期化します。

initialize_dynamic_settings()のコードは以下の通りです:

settings.py

def initialize_dynamic_settings(self):
    """ゲームの進行に伴って変化する設定を初期化"""
    self.ship_speed_factor = 1.5
    self.bullet_speed_factor = 3
    self.alien_speed_factor = 1
    
    # fleet_directionが1は右を意味し、-1は左を意味する
    self.fleet_direction = 1

このメソッドは、宇宙船、弾丸、エイリアンの初期速度を設定します。ゲームの進行に伴ってこれらの速度を上げますが、プレイヤーが新しいゲームを開始するたびにこれらの速度をリセットします。このメソッドでは、fleet_directionも設定し、ゲーム開始時にエイリアンが常に右に移動するようにします。プレイヤーがレベルアップするたびに、increase_speed()を使用して宇宙船、弾丸、エイリアンの速度を上げます:

settings.py

def increase_speed(self):
    """速度設定を上げる"""
    self.ship_speed_factor *= self.speedup_scale
    self.bullet_speed_factor *= self.speedup_scale
    self.alien_speed_factor *= self.speedup_scale

これらのゲーム要素の速度を上げるために、各速度設定にspeedup_scaleの値を掛けます。

check_bullet_alien_collisions()では、エイリアンの群れがすべて消滅した後にincrease_speed()を呼び出してゲームのペースを上げ、新しいエイリアンの群れを作成します:

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, ship, aliens, bullets):
    --snip--
    if len(aliens) == 0:
        # 既存の弾丸を削除し、ゲームのペースを上げ、新しいエイリアンの群れを作成
        bullets.empty()
        ai_settings.increase_speed()
        create_fleet(ai_settings, screen, ship, aliens)

速度設定ship_speed_factor、alien_speed_factor、bullet_speed_factorの値を変更するだけで、ゲーム全体のペースを上げることができます!

2.2 速度のリセット

プレイヤーが新しいゲームを開始するたびに、変更された設定を初期値にリセットする必要があります。そうしないと、新しいゲームの開始時に速度設定が前回のゲームで増加した値になってしまいます:

game_functions.py

def check_play_button(ai_settings, screen, stats, play_button, ship, aliens,
                      bullets, mouse_x, mouse_y):
    """プレイヤーがプレイボタンをクリックしたときに新しいゲームを開始"""
    button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)
    if button_clicked and not stats.game_active:
        # ゲーム設定をリセット
        ai_settings.initialize_dynamic_settings()
        # カーソルを非表示にする
        pygame.mouse.set_visible(False)
        --snip--

これで、ゲーム「インベーダーゲーム」はより面白く、より挑戦的なものになりました。プレイヤーが画面上のエイリアンをすべて消滅させるたびに、ゲームのペースが速くなり、難易度が上がります。ゲームの難易度が速すぎる場合は、settings.speedup_scaleの値を下げます。ゲームの挑戦性が十分でない場合は、この設定の値を少し上げます。この設定の最適な値を見つけて、難易度の上昇速度を合理的にします:最初の数グループのエイリアンは簡単に消滅できます。次の数グループは消滅させるのがやや難しいですが、不可能ではありません。そして、後のエイリアンの群れを消滅させるのはほぼ不可能になります。

3. スコアシステム

ここで、プレイヤーのスコアをリアルタイムで追跡し、ハイスコア、現在のレベル、残りの宇宙船の数を表示するスコアシステムを実装します。

スコアはゲームの統計情報なので、GameStatsにscore属性を追加します:

game_stats.py

class GameStats():
    --snip--
    def reset_stats(self):
        """ゲームの進行に伴って変化する可能性のある統計情報を初期化"""
        self.ships_left = self.ai_settings.ship_limit
        self.score = 0

ゲームを開始するたびにスコアをリセットするために、init()ではなくreset_stats()でscoreを初期化します。

3.1 スコアの表示

画面にスコアを表示するために、まず新しいクラスScoreboardを作成します。現時点では、このクラスは現在のスコアのみを表示しますが、後でハイスコア、レベル、残りの宇宙船の数を表示するためにも使用します。以下はこのクラスの前半部分で、scoreboard.pyというファイル名で保存されます:

scoreboard.py

import pygame.font

class Scoreboard():
    """スコア情報を表示するクラス"""
    def __init__(self, ai_settings, screen, stats):
        """スコアの表示に関連する属性を初期化"""
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.ai_settings = ai_settings
        self.stats = stats
        
        # スコア情報を表示する際に使用するフォント設定
        self.text_color = (30, 30, 30)
        self.font = pygame.font.SysFont(None, 48)
        
        # 初期スコア画像を準備
        self.prep_score()

Scoreboardは画面上にテキストを表示するため、まずpygame.fontモジュールをインポートします。次に、init()にai_settings、screen、statsの引数を含めて、追跡している値を報告できるようにします。次に、テキストの色を設定し、フォントオブジェクトをインスタンス化します。

表示するテキストを画像に変換するために、prep_score()を呼び出します。その定義は以下の通りです:

scoreboard.py

def prep_score(self):
    """スコアをレンダリングされた画像に変換"""
    score_str = str(self.stats.score)
    self.score_image = self.font.render(score_str, True, self.text_color,
                                        self.ai_settings.bg_color)
    
    # スコアを画面の右上に配置
    self.score_rect = self.score_image.get_rect()
    self.score_rect.right = self.screen_rect.right - 20
    self.score_rect.top = 20

prep_score()では、まず数値stats.scoreを文字列に変換し、次にこの文字列を画像を作成するrender()に渡します。画面上でスコアを明確に表示するために、render()に画面の背景色とテキストの色を渡します。

スコアを画面の右上に配置し、スコアが増加して数字が広くなったときに左に伸びるようにします。スコアが常に画面の右に固定されるように、score_rectという名前のrectを作成し、その右端を画面の右端から20ピクセル離れた位置に設定し、その上端を画面の上端から20ピクセル離れた位置に設定します。

最後に、レンダリングされたスコア画像を表示するshow_score()メソッドを作成します:

scoreboard.py

def show_score(self):
    """画面上にスコアを表示"""
    self.screen.blit(self.score_image, self.score_rect)

このメソッドはスコア画像を画面に表示し、score_rectで指定された位置に配置します。

3.2 スコアボードの作成

スコアを表示するために、alien_invasion.pyでScoreboardのインスタンスを作成します:

alien_invasion.py

--snip--
from game_stats import GameStats
from scoreboard import Scoreboard
--snip--
def run_game():
    --snip--
    # ゲーム統計情報を格納するインスタンスを作成し、スコアボードを作成
    stats = GameStats(ai_settings)
    sb = Scoreboard(ai_settings, screen, stats)
    --snip--
    
    # ゲームのメインループを開始
    while True:
        --snip--
        gf.update_screen(ai_settings, screen, stats, sb, ship, aliens,
                         bullets, play_button)
run_game()

新しく作成したクラスScoreboardをインポートし、インスタンスstatsを作成した後にsbという名前のScoreboardインスタンスを作成します。次に、sbをupdate_screen()に渡して、画面上にスコアを表示できるようにします。

スコアを表示するために、update_screen()を以下のように変更します:

game_functions.py

def update_screen(ai_settings, screen, stats, sb, ship, aliens, bullets,
                  play_button):
    --snip--
    
    # スコアを表示
    sb.show_score()
    
    # ゲームが非アクティブ状態の場合、プレイボタンを表示
    if not stats.game_active:
        play_button.draw_button()
    
    # 最近描画した画面を表示
    pygame.display.flip()

update_screen()の引数リストにsbを追加し、プレイボタンを描画する前にshow_scoreを呼び出します。

今ゲームを実行すると、画面の右上に0が表示されます(現在、スコアシステムをさらに開発する前に、スコアが正しい場所に表示されることを確認したいだけです)。次に、各エイリアンが何点 worth であるかを指定しましょう!

3.3 エイリアンが消滅したときにスコアを更新

画面上にスコアをリアルタイムで表示するために、エイリアンが撃たれるたびにstats.scoreの値を更新し、prep_score()を呼び出してスコア画像を更新します。その前に、プレイヤーがエイリアンを1体撃墜するごとに得られるポイント数を指定する必要があります:

settings.py

def initialize_dynamic_settings(self):
    --snip--
    # スコアリング
    self.alien_points = 50

ゲームの進行に伴って、各エイリアンの価値があるポイント数を上げます。新しいゲームを開始するたびにこの値がリセットされるように、initialize_dynamic_settings()で設定します。

check_bullet_alien_collisions()では、エイリアンが撃墜されるたびにスコアを更新します:

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,
                                  aliens, bullets):
    """弾丸とエイリアンの衝突に応答"""
    # 衝突した弾丸とエイリアンを削除
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
    
    if collisions:
        stats.score += ai_settings.alien_points
        sb.prep_score()
    --snip--

check_bullet_alien_collisions()の定義を更新して、statsとsbの引数を含め、スコアとスコアボードを更新できるようにします。弾丸がエイリアンに当たると、Pygameは辞書(collisions)を返します。この辞書が存在するかどうかを確認し、存在する場合はスコアにエイリアン1体分のポイントを加えます。次に、prep_score()を呼び出して、最新のスコアを表示する新しい画像を作成します。

update_bullets()を変更して、関数間で適切な引数が渡されるようにする必要があります:

game_functions.py

def update_bullets(ai_settings, screen, stats, sb, ship, aliens, bullets):
    """弾丸の位置を更新し、消えた弾丸を削除"""
    --snip--
    check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,
                                  aliens, bullets)

update_bullets()の定義には、statsとsbの新しい引数が必要で、check_bullet_alien_collisions()を呼び出すときにも、statsとsbの引数を渡す必要があります。

メインのwhileループでupdate_bullets()を呼び出すコードも変更する必要があります:

alien_invasion.py

# ゲームのメインループを開始
while True:
    gf.check_events(ai_settings, screen, stats, play_button, ship,
                    aliens, bullets)
    if stats.game_active:
        ship.update()
        gf.update_bullets(ai_settings, screen, stats, sb, ship, aliens,
                          bullets)
        --snip--

update_bullets()を呼び出すときには、statsとsbの引数を渡す必要があります。

今ゲームを実行すると、スコアが増加し続けるはずです!

3.4 消滅した各エイリアンのポイントをスコアに計上

現在、私たちのコードは一部の消滅したエイリアンを見逃している可能性があります。例えば、1回のループで2発の弾丸がエイリアンに当たった場合や、弾丸が広くて複数のエイリアンに同時に当たった場合、プレイヤーは消滅したエイリアン1体分のポイントしか得られません。この問題を修正するために、弾丸とエイリアンの衝突を検出する方法を調整します。

check_bullet_alien_collisions()では、エイリアンに衝突した弾丸はすべて辞書collisionsのキーです。各弾丸に関連する値は、その弾丸が当たったエイリアンを含むリストです。辞書collisionsを反復処理して、消滅した各エイリアンのポイントがすべてスコアに計上されるようにします:

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,
                                  aliens, bullets):
    --snip--
    if collisions:
        for aliens in collisions.values():
            stats.score += ai_settings.alien_points * len(aliens)
            sb.prep_score()
    --snip--

辞書collisionsが存在する場合、そのすべての値を反復処理します。各値は、同じ弾丸で撃たれたすべてのエイリアンを含むリストであることを忘れないでください。各リストについて、エイリアン1体分のポイントにリストに含まれるエイリアンの数を掛けた結果を現在のスコアに加えます。これをテストするために、弾丸の幅を300ピクセルに変更し、広い弾丸が当たった各エイリアンのポイントを得ていることを確認してから、弾丸の幅を通常の値に戻してください。

3.5 ポイントの向上

プレイヤーがレベルアップするたびに、ゲームはより難しくなるため、高いレベルではエイリアンのポイントはより高くなるべきです。この機能を実装するために、ゲームのペースが上がるときにポイントを上げるコードを追加します:

settings.py

class Settings():
    """ゲーム「インベーダーゲーム」のすべての設定を格納するクラス"""
    def __init__(self):
        --snip--
        # ゲームのペースを上げる速度
        self.speedup_scale = 1.1
        # エイリアンポイントの上昇速度
        self.score_scale = 1.5
        self.initialize_dynamic_settings()
        
    def increase_speed(self):
        """速度設定とエイリアンポイントを上げる"""
        self.ship_speed_factor *= self.speedup_scale
        self.bullet_speed_factor *= self.speedup_scale
        self.alien_speed_factor *= self.speedup_scale
        self.alien_points = int(self.alien_points * self.score_scale)

ポイントの上昇速度を定義し、score_scaleと呼びます。小さなペース上昇速度(1.1)はゲームをすぐに非常に挑戦的なものにしますが、スコアリングに顕著な変化をもたらすためには、ポイントの上昇速度をより大きな値(1.5)に設定する必要があります。現在、ゲームのペースを上げると同時に、各エイリアンのポイントも上げています。ポイントを整数にするために、int()関数を使用します。

settings.py

def increase_speed(self):
    --snip--
    self.alien_points = int(self.alien_points * self.score_scale)
    print(self.alien_points)

これでレベルが上がるたびに、ターミナルウィンドウに新しいポイント値が表示されます。

注意 ポイントが増加していることを確認したら、必ずこのprintステートメントを削除してください。そうしないと、ゲームのパフォーマンスに影響を与えたり、プレイヤーの注意をそらしたりする可能性があります。

3.6 スコアの丸め

ほとんどのアーケードスタイルのシューティングゲームでは、スコアは10の倍数として表示されます。以下で、私たちのスコアシステムもこの原則に従うようにします。また、大きな数字にカンマで区切られた千の位の区切り文字を追加するようにスコアの形式を設定します。この変更はScoreboardで行います:

scoreboard.py

def prep_score(self):
    """スコアをレンダリングされた画像に変換"""
    rounded_score = int(round(self.stats.score, -1))
    score_str = "{:,}".format(rounded_score)
    self.score_image = self.font.render(score_str, True, self.text_color,
                                        self.ai_settings.bg_color)
    --snip--

round()関数は通常、小数を小数点以下何桁かに丸めますが、2番目の引数を負数に指定すると、round()は最も近い10、100、1000などの整数倍に丸めます。上記のコードはPythonにstats.scoreの値を最も近い10の倍数に丸めさせ、その結果をrounded_scoreに格納します。

注意 Python 2.7では、round()は常に小数値を返すため、報告されるスコアが整数であることを保証するためにint()を使用しています。Python 3を使用している場合は、int()の呼び出しを省略できます。

文字列フォーマット設定ディレクティブを使用して、数値を文字列に変換するときにカンマを挿入するようにPythonに指示します。例えば、1000000ではなく1,000,000と出力されます。今ゲームを実行すると、スコアが非常に高くなっても、10の倍数の整然としたスコアが表示されます。

3.7 ハイスコア

すべてのプレイヤーはゲームのハイスコア記録を超えたいと思っています。ここで、ハイスコアを追跡し、表示して、プレイヤーに超えるべき目標を提供します。ハイスコアをGameStatsに格納します:

game_stats.py

def __init__(self, ai_settings):
    --snip--
    # いかなる場合でもリセットすべきではないハイスコア
    self.high_score = 0

いかなる場合でもハイスコアをリセットしないため、init()でhigh_scoreを初期化し、reset_stats()では初期化しません。

次に、Scoreboardを変更してハイスコアを表示します。まず、init()メソッドを変更します:

scoreboard.py

def __init__(self, ai_settings, screen, stats):
    --snip--
    # ハイスコアと現在のスコアを含む画像を準備
    self.prep_score()
    self.prep_high_score()

ハイスコアは現在のスコアとは別に表示されるため、ハイスコアを含む画像を準備するための新しいメソッドprep_high_score()を作成する必要があります。

prep_high_score()メソッドのコードは以下の通りです:

scoreboard.py

def prep_high_score(self):
    """ハイスコアをレンダリングされた画像に変換"""
    high_score = int(round(self.stats.high_score, -1))
    high_score_str = "{:,}".format(high_score)
    self.high_score_image = self.font.render(high_score_str, True,
                                            self.text_color, self.ai_settings.bg_color)
    
    # ハイスコアを画面の上部中央に配置
    self.high_score_rect = self.high_score_image.get_rect()
    self.high_score_rect.centerx = self.screen_rect.centerx
    self.high_score_rect.top = self.score_rect.top

ハイスコアを最も近い10の倍数に丸め、カンマで区切られた千の位の区切り文字を追加します。次に、ハイスコアに基づいて画像を生成し、水平方向に中央揃えし、そのtopプロパティを現在のスコア画像のtopプロパティに設定します。

次に、show_score()メソッドは画面の右上に現在のスコアを表示し、画面の上部中央にハイスコアを表示する必要があります:

scoreboard.py

def show_score(self):
    """画面上に現在のスコアとハイスコアを表示"""
    self.screen.blit(self.score_image, self.score_rect)
    self.screen.blit(self.high_score_image, self.high_score_rect)

新しいハイスコアが誕生したかどうかを確認するために、game_functions.pyに新しい関数check_high_score()を追加します:

game_functions.py

def check_high_score(stats, sb):
    """新しいハイスコアが誕生したかどうかを確認"""
    if stats.score > stats.high_score:
        stats.high_score = stats.score
        sb.prep_high_score()

関数check_high_score()は2つの引数を含みます:statsとsbです。statsを使用して現在のスコアとハイスコアを比較し、必要に応じてsbを使用してハイスコア画像を変更します。現在のスコアがハイスコアより高い場合、high_scoreの値を更新し、prep_high_score()を呼び出してハイスコアを含む画像を更新します。

check_bullet_alien_collisions()では、エイリアンが消滅するたびに、スコアを更新した後にcheck_high_score()を呼び出す必要があります:

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,
                                  aliens, bullets):
    --snip--
    if collisions:
        for aliens in collisions.values():
            stats.score += ai_settings.alien_points * len(aliens)
            sb.prep_score()
        check_high_score(stats, sb)
    --snip--

辞書collisionsが存在する場合、消滅したエイリアンの数に基づいてスコアを更新し、次にcheck_high_score()を呼び出します。

初めてこのゲームをプレイするとき、現在のスコアがハイスコアであるため、両方の場所に現在のスコアが表示されます。しかし、このゲームを再開すると、ハイスコアが中央に表示され、現在のスコアが右側に表示されます。

3.8 レベルの表示

ゲーム内でプレイヤーのレベルを表示するために、まずGameStatsに現在のレベルを表す属性を追加します。新しいゲームを開始するたびにレベルをリセットするように、reset_stats()で初期化します:

game_stats.py

def reset_stats(self):
    """ゲームの進行に伴って変化する可能性のある統計情報を初期化"""
    self.ships_left = self.ai_settings.ship_limit
    self.score = 0
    self.level = 1

Scoreboardが現在のスコアの下に現在のレベルを表示できるようにするために、init()で新しいメソッドprep_level()を呼び出します:

scoreboard.py

def __init__(self, ai_settings, screen, stats):
    --snip--
    # スコアを含む初期画像を準備
    self.prep_score()
    self.prep_high_score()
    self.prep_level()

prep_level()のコードは以下の通りです:

scoreboard.py

def prep_level(self):
    """レベルをレンダリングされた画像に変換"""
    self.level_image = self.font.render(str(self.stats.level), True,
                                        self.text_color, self.ai_settings.bg_color)
    
    # レベルをスコアの下に配置
    self.level_rect = self.level_image.get_rect()
    self.level_rect.right = self.score_rect.right
    self.level_rect.top = self.score_rect.bottom + 10

prep_level()メソッドは、stats.levelに格納されている値に基づいて画像を作成し、そのrightプロパティをスコアのrightプロパティに設定します。次に、topプロパティをスコア画像のbottomプロパティより10ピクセル大きい値に設定して、スコアとレベルの間にスペースを確保します。

show_score()も更新する必要があります:

scoreboard.py

def show_score(self):
    """画面上に宇宙船とスコアを表示"""
    self.screen.blit(self.score_image, self.score_rect)
    self.screen.blit(self.high_score_image, self.high_score_rect)
    self.screen.blit(self.level_image, self.level_rect)

このメソッドでは、画面上にレベル画像を表示するコードを1行追加しました。

check_bullet_alien_collisions()でレベルを上げ、レベル画像を更新します:

game_functions.py

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,
                                  aliens, bullets):
    --snip--
    if len(aliens) == 0:
        # エイリアンの群れがすべて消滅した場合、レベルを1つ上げる
        bullets.empty()
        ai_settings.increase_speed()
        
        # レベルを上げる
        stats.level += 1
        sb.prep_level()
        
        create_fleet(ai_settings, screen, ship, aliens)

エイリアンの群れがすべて消滅した場合、stats.levelの値を1増やし、prep_level()を呼び出して、新しいレベルが正しく表示されるようにします。

新しいゲームを開始するときにスコアとレベルの画像を更新するために、プレイボタンがクリックされたときにリセットをトリガーします:

game_functions.py

def check_play_button(ai_settings, screen, stats, sb, play_button, ship,
                      aliens, bullets, mouse_x, mouse_y):
    """プレイヤーがプレイボタンをクリックしたときに新しいゲームを開始"""
    button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)
    if button_clicked and not stats.game_active:
        --snip--
        # ゲーム統計情報をリセット
        stats.reset_stats()
        stats.game_active = True
        
        # スコアボード画像をリセット
        sb.prep_score()
        sb.prep_high_score()
        sb.prep_level()
        
        # エイリアンリストと弾丸リストを空にする
        aliens.empty()
        bullets.empty()
        --snip--

check_play_button()の定義にはオブジェクトsbを含める必要があります。スコアボード画像をリセットするために、関連するゲーム設定をリセットした後にprep_score()、prep_high_score()、prep_level()を呼び出します。

check_events()では、check_play_button()にsbを渡して、スコアボードオブジェクトにアクセスできるようにする必要があります:

game_functions.py

def check_events(ai_settings, screen, stats, sb, play_button, ship, aliens,
                  bullets):
    """キーとマウスのイベントに応答"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            --snip--
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_x, mouse_y = pygame.mouse.get_pos()
            check_play_button(ai_settings, screen, stats, sb, play_button,
                              ship, aliens, bullets, mouse_x, mouse_y)

check_events()の定義には引数sbを含める必要があり、check_play_button()を呼び出すときにsbを引数として渡せるようにします。

最後に、alien_invasion.pyでcheck_events()を呼び出すコードを更新して、それにもsbを渡します:

alien_invasion.py

# ゲームのメインループを開始
while True:
    gf.check_events(ai_settings, screen, stats, sb, play_button, ship,
                    aliens, bullets)
    --snip--

これで、どのレベルにいるかがわかります。

3.9 残りの宇宙船数の表示

最後に、プレイヤーに残っている宇宙船の数を表示しますが、数字ではなくグラフィックを使用します。そのために、画面の左上隅に宇宙船の画像を描画して、残りの宇宙船の数を示します。これは多くの古典的なアーケードゲームのようにです。

まず、ShipがSpriteを継承して、宇宙船のグループを作成できるようにする必要があります:

ship.py

import pygame
from pygame.sprite import Sprite

class Ship(Sprite):
    def __init__(self, ai_settings, screen):
        """宇宙船を初期化し、その開始位置を設定"""
        super(Ship, self).__init__()
        --snip--

ここでは、SpriteをインポートしてShipがSpriteを継承するようにし、init()の先頭でsuper()を呼び出します。

次に、Scoreboardを変更して、表示できる宇宙船のグループを作成する必要があります。以下はimport文と__init__()メソッドです:

scoreboard.py

import pygame.font
from pygame.sprite import Group
from ship import Ship

class Scoreboard():
    """スコア情報を報告するクラス"""
    def __init__(self, ai_settings, screen, stats):
        --snip--
        self.prep_level()
        self.prep_ships()
       --snip--

宇宙船のグループを作成するために、GroupとShipクラスをインポートします。prep_level()を呼び出した後、prep_ships()を呼び出します。

prep_ships()のコードは以下の通りです:

scoreboard.py

def prep_ships(self):
    """残りの宇宙船の数を表示"""
    self.ships = Group()
    for ship_number in range(self.stats.ships_left):
        ship = Ship(self.ai_settings, self.screen)
        ship.rect.x = 10 + ship_number * ship.rect.width
        ship.rect.y = 10
        self.ships.add(ship)

prep_ships()メソッドは、宇宙船インスタンスを格納するための空のグループself.shipsを作成します。このグループを埋めるために、プレイヤーに残っている宇宙船の数だけループを実行します。このループでは、新しい宇宙船を作成し、そのx座標を設定して、宇宙船のグループ全体が画面の左側に配置され、各宇宙船の左マージンが10ピクセルになるようにします。また、y座標を画面の上端から10ピクセル離れた位置に設定して、すべての宇宙船がスコア画像と揃うようにします。最後に、各新しい宇宙船をグループshipsに追加します。

次に、画面上に宇宙船を描画する必要があります:

scoreboard.py

def show_score(self):
    --snip--
    self.screen.blit(self.level_image, self.level_rect)
    # 宇宙船を描画
    self.ships.draw(self.screen)

画面上に宇宙船を表示するために、グループに対してdraw()を呼び出します。Pygameは各宇宙船を描画します。

ゲーム開始時にプレイヤーが何機の宇宙船を持っているかを知らせるために、新しいゲームを開始するときにprep_ships()を呼び出します。これはgame_functions.pyのcheck_play_button()で行われます:

game_functions.py

def check_play_button(ai_settings, screen, stats, sb, play_button, ship,
                      bullets, mouse_x, mouse_y):
    """プレイヤーがプレイボタンをクリックしたときに新しいゲームを開始"""
    button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)
    if button_clicked and not stats.game_active:
        --snip--
        # スコアボード画像をリセット
        sb.prep_score()
        sb.prep_high_score()
        sb.prep_level()
        sb.prep_ships()
        --snip--

また、宇宙船がエイリアンに衝突したときにもprep_ships()を呼び出して、プレイヤーが宇宙船を1機失ったときに宇宙船画像を更新します:

game_functions.py

def update_aliens(ai_settings, screen, stats, sb, ship, aliens, bullets):
    --snip--
    # エイリアンと宇宙船の間の衝突を検出
    if pygame.sprite.spritecollideany(ship, aliens):
        ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets)
    
    # エイリアンが画面の底端に到達したかどうかを確認
    check_aliens_bottom(ai_settings, screen, stats, sb, ship, aliens, bullets)

def ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets):
    """エイリアンに衝突した宇宙船に応答"""
    if stats.ships_left > 0:
        # ships_leftを1減らす
        stats.ships_left -= 1
        
        # スコアボードを更新
        sb.prep_ships()
        
        # エイリアンリストと弾丸リストを空にする
        --snip--

まず、update_aliens()の定義に引数sbを追加します。次に、ship_hit()とcheck_aliens_bottom()の両方にsbを渡して、それらがスコアボードオブジェクトにアクセスできるようにします。

次に、ship_hit()の定義を更新して、引数sbを含めます。ships_leftの値を1減らした後にprep_ships()を呼び出して、宇宙船を失ったたびに表示される宇宙船の数が正しくなるようにします。

check_aliens_bottom()ではship_hit()を呼び出す必要があるため、この関数を更新します:

game_functions.py

def check_aliens_bottom(ai_settings, screen, stats, sb, ship, aliens,
                        bullets):
    """エイリアンが画面の底端に到達したかどうかを確認"""
    screen_rect = screen.get_rect()
    for alien in aliens.sprites():
        if alien.rect.bottom >= screen_rect.bottom:
            # 宇宙船がエイリアンに衝突した場合と同じように処理
            ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets)
            break

これで、check_aliens_bottom()には引数sbが含まれ、ship_hit()を呼び出すときに引数sbを渡します。

最後に、alien_invasion.pyでupdate_aliens()を呼び出すコードを変更して、それにも引数sbを渡します:

alien_invasion.py

# ゲームのメインループを開始
while True:
    --snip--
    if stats.game_active:
        ship.update()
        gf.update_bullets(ai_settings, screen, stats, sb, ship, aliens,
                          bullets)
        gf.update_aliens(ai_settings, screen, stats, sb, ship, aliens,
                          bullets)
        --snip--

タグ: Python pygame ゲーム開発 スコアシステム インベーダーゲーム

5月25日 04:14 投稿