📚

pygamesでシューティングゲームを作ってみよう!

2023/12/11に公開

プログラムの概要

このプログラムは、Pythonで書かれたシンプルなビデオゲームです。pygameというライブラリを使用しており、プレイヤーがキャラクターを操作して敵から逃れるゲームです。プログラムの流れは大きく分けて、初期設定、ゲームオブジェクトの定義、イベント定義、ゲームオブジェクトの初期化、ゲームループの実行、終了処理の四つのステップに分かれています。

先に、プログラムの全体はこちらです。

# Import the pygame module
import pygame
from pygame.locals import *
import random
import sys

# Import pygame.locals for easier access to key coordinates
# Updated to conform to flake8 and black standards
from pygame.locals import (
    RLEACCEL,
    K_UP,
    K_DOWN,
    K_LEFT,
    K_RIGHT,
    K_ESCAPE,
    KEYDOWN,
    QUIT,
)

# Define constants for the screen width and height
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600

# Setup for sounds. Defaults are good.
pygame.mixer.init()


pygame.mixer.music.load("materials-pygame-a-primer/Apoxode_-_Electric_1.mp3")
pygame.mixer.music.play(loops=-1)

move_up_sound = pygame.mixer.Sound("materials-pygame-a-primer/Rising_putter.ogg")
move_down_sound = pygame.mixer.Sound("materials-pygame-a-primer/Falling_putter.ogg")
collision_sound = pygame.mixer.Sound("materials-pygame-a-primer/Collision.ogg")

# Initialize pygame
pygame.init()


# Define a player object by extending pygame.sprite.Sprite
# The surface drawn on the screen is now an attribute of 'player'
class Player(pygame.sprite.Sprite):
    def __init__(self):
        super(Player, self).__init__()
        self.surf = pygame.image.load("materials-pygame-a-primer/jet.png").convert()
        # self.surf = pygame.Surface((75, 25))
        # self.surf.fill((255, 255, 255))
        self.surf.set_colorkey((255,255,255), RLEACCEL)
        self.rect = self.surf.get_rect(center=(SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
    
    # Move the sprite based on user keypresses
    def update(self, pressed_keys):
        if pressed_keys[K_UP]:
            self.rect.move_ip(0, -5)
            move_up_sound.play()
        if pressed_keys[K_DOWN]:
            self.rect.move_ip(0, 5)
            move_down_sound.play()
        if pressed_keys[K_LEFT]:
            self.rect.move_ip(-5, 0)
        if pressed_keys[K_RIGHT]:
            self.rect.move_ip(5, 0)
        if self.rect.left < 0:
            self.rect.left = 0
        if self.rect.right > SCREEN_WIDTH:
            self.rect.right = SCREEN_WIDTH
        if self.rect.top < 0:
            self.rect.top = 0
        if self.rect.bottom >= SCREEN_HEIGHT:
            self.rect.bottom = SCREEN_HEIGHT

class Enemy(pygame.sprite.Sprite):
    def __init__(self):
        super(Enemy, self).__init__()
        self.surf = pygame.image.load("materials-pygame-a-primer/missile.png").convert()
        self.surf.set_colorkey((255, 255, 255), RLEACCEL)
        self.rect = self.surf.get_rect(
            center = (
                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
                random.randint(0, SCREEN_HEIGHT)
            )
        )
        self.speed = random.randint(5, 20)
    
    def update(self):
        self.rect.move_ip(-self.speed, 0)
        if self.rect.right < 0:
            self.kill()

class Cloud(pygame.sprite.Sprite):
    def __init__(self):
        super(Cloud, self).__init__()
        self.surf = pygame.image.load("materials-pygame-a-primer/cloud.png").convert()
        self.surf.set_colorkey((0,0,0), RLEACCEL)

        self.rect = self.surf.get_rect(
            center = (
                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
                random.randint(0, SCREEN_HEIGHT)
            )
        )

    def update(self):
        self.rect.move_ip(-5, 0)
        if self.rect.right < 0:
            self.kill()

# Create the screen object
# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
ADDENEMY = pygame.USEREVENT + 1
pygame.time.set_timer(ADDENEMY, 250)
ADDCLOUD = pygame.USEREVENT + 2
pygame.time.set_timer(ADDCLOUD, 1000)
# Instantiate player. Right now, this is just a rectangle.
player = Player()

enemies = pygame.sprite.Group()
clouds  = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()
clock = pygame.time.Clock()
# all_sprites.add(player)


# Variable to keep the main loop running
running = True

# Main loop
while running:
    # for loop through the event queue
    for event in pygame.event.get():
        # Check for KEYDOWN event
        if event.type == KEYDOWN:
            # If the Esc key is pressed, then exit the main loop
            if event.key == K_ESCAPE:
                running = False
        # Check for QUIT event. If QUIT, then set running to false.
        elif event.type == QUIT:
            running = False
        
        elif event.type == ADDENEMY:
            new_enemy = Enemy()
            enemies.add(new_enemy)
            all_sprites.add(new_enemy)
        elif event.type == ADDCLOUD:
            new_cloud = Cloud()
            clouds.add(new_cloud)
            all_sprites.add(clouds)
    # print("ref_count" ,sys.getrefcount(enemies))
    # print("count enemies list", len(enemies))


    pressed_keys = pygame.key.get_pressed()

    # Update the player sprite based on user keypresses
    player.update(pressed_keys)

    enemies.update()
    clouds.update()

    screen.fill((135, 206, 250))

    for entity in all_sprites:
        screen.blit(entity.surf, entity.rect)

    if pygame.sprite.spritecollideany(player, enemies):
        player.kill()
        move_up_sound.stop()
        move_down_sound.stop()
        collision_sound.play()
        running = False

    # Draw the player on the screen
    screen.blit(player.surf, player.rect)
    # print(player.rect)

    # Update the display
    pygame.display.flip()

    clock.tick(30)

このプログラム理解するために、五つのステップに分けて解説します。
プログラムを実行するとこのようなゲームが起動します。
https://youtu.be/4ySOv9OmA_Q

ステップ1: モジュールのインポート

最初に、ゲーム開発に必要なpygameモジュールと他の必要なモジュールをインポートします。

import pygame
from pygame.locals import *
import random
import sys

from pygame.locals import (
    RLEACCEL,
    K_UP,
    K_DOWN,
    K_LEFT,
    K_RIGHT,
    K_ESCAPE,
    KEYDOWN,
    QUIT,
)

  • import pygame: pygameモジュールをインポートします。これはゲームを作るために必要な関数やクラスが含まれています。
  • from pygame.locals import *: pygameで定義されている定数(例えば、キーの名前)をインポートします。*はすべての定数をインポートすることを意味します。
  • import random: 乱数を生成するためのモジュールです。敵の出現位置や動きをランダムにするのに使用します。
  • import sys: Pythonのシステム関連の機能を利用するためのモジュールです。プログラムを終了させるために使用します。
  • from pygame.locals import ... の行は、pygame.localsモジュールから特定の関数や定数をインポートしています。これらは、ゲーム開発時に頻繁に使用されるものです。

了解しました。from pygame.locals import ... の部分について詳しく解説します。

この行は、pygame.localsモジュールから特定の関数や定数をインポートしています。これらは、ゲーム開発時に頻繁に使用されるものです。

各要素の説明

  1. RLEACCEL:

    • RLEACCELは、Surfaceオブジェクト(画像や図形など)のブリット(描画)速度を高速化するためのフラグです。Run Length Encoding(RLE)圧縮を使用してSurfaceを加速します。特に、透過色を持つ画像に対して効果的です。
  2. K_UP, K_DOWN, K_LEFT, K_RIGHT:

    • これらはキーボードの矢印キー(上、下、左、右)に対応する定数です。これらを使用して、プレイヤーがこれらのキーを押したときの動作をプログラムで定義できます。
  3. K_ESCAPE:

    • これはキーボードの「Esc」キーに対応する定数です。多くのゲームでは、「Esc」キーを押すとメニューを開いたり、ゲームを一時停止したりするために使用されます。
  4. KEYDOWN:

    • KEYDOWNはイベントタイプの一つで、キーボードのキーが押されたときに発生します。このイベントを使って、どのキーが押されたかを検出し、それに応じたアクションをプログラムできます。
  5. QUIT:

    • QUITイベントは、ユーザーがゲームウィンドウの閉じるボタンをクリックしたときに発生します。このイベントを検出することで、ゲームを適切に終了させる処理を行うことができます。

使用例

これらの定数は、ゲームのメインループ内でイベント処理を行う際に使われます。例えば、KEYDOWNイベントが発生したときに、どのキー(K_UP, K_DOWN など)が押されたかを確認し、それに応じたキャラクターの動きを制御することができます。また、QUITイベントを検出したときにゲームを終了させる処理を書くこともできます。

ステップ2: スクリーンとサウンドの設定

次に、画面のサイズを定義し、pygameの音声再生システムを初期化します。さらに、ゲームに使用する音楽や効果音を読み込み、再生を設定します。
サウンド、画像をダウンロードはこちら。

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
pygame.mixer.init()

pygame.mixer.music.load("materials-pygame-a-primer/Apoxode_-_Electric_1.mp3")
pygame.mixer.music.play(loops=-1)

move_up_sound = pygame.mixer.Sound("materials-pygame-a-primer/Rising_putter.ogg")
move_down_sound = pygame.mixer.Sound("materials-pygame-a-primer/Falling_putter.ogg")
collision_sound = pygame.mixer.Sound("materials-pygame-a-primer/Collision.ogg")

# Initialize pygame
pygame.init()
  • SCREEN_WIDTH = 800SCREEN_HEIGHT = 600: ゲームの画面サイズを定義します。ここでは、幅800ピクセル、高さ600ピクセルとしています。
  • pygame.mixer.init(): pygameのサウンドシステムを初期化します。これにより、音楽や効果音を再生する準備が整います。

ステップ3: イベント定義、ゲームオブジェクトの初期化

このコードは、Pygameを使ったゲームの画面の設定、イベントの定義、およびゲーム内のオブジェクト(プレイヤー、敵、雲)の初期化を行っています。それぞれの行について詳しく解説します。

# Create the screen object
# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
ADDENEMY = pygame.USEREVENT + 1
pygame.time.set_timer(ADDENEMY, 250)
ADDCLOUD = pygame.USEREVENT + 2
pygame.time.set_timer(ADDCLOUD, 1000)
# Instantiate player. Right now, this is just a rectangle.
player = Player()

enemies = pygame.sprite.Group()
clouds  = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()
clock = pygame.time.Clock()
# all_sprites.add(player)

画面の設定

screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
  • pygame.display.set_mode(): ゲームのメイン画面(ウィンドウ)を生成します。
  • (SCREEN_WIDTH, SCREEN_HEIGHT): ウィンドウのサイズを設定します。これは前のコードで定義された定数に基づいています。

イベントの定義

ADDENEMY = pygame.USEREVENT + 1
pygame.time.set_timer(ADDENEMY, 250)
ADDCLOUD = pygame.USEREVENT + 2
pygame.time.set_timer(ADDCLOUD, 1000)
  • ADDENEMYADDCLOUD: カスタムイベントを定義しています。PygameではUSEREVENTから始まる独自のイベントを定義できます。
  • pygame.time.set_timer(ADDENEMY, 250): ADDENEMYイベントを250ミリ秒ごとに発生させます。これは敵キャラクターを定期的に生成するために使用されます。
  • pygame.time.set_timer(ADDCLOUD, 1000): ADDCLOUDイベントを1000ミリ秒(1秒)ごとに発生させます。これは雲を定期的に生成するために使用されます。

オブジェクトの初期化

player = Player()
enemies = pygame.sprite.Group()
clouds  = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()
clock = pygame.time.Clock()
  • player = Player(): プレイヤーオブジェクトを生成します。Playerクラスは以前に定義されていると仮定します。
  • pygame.sprite.Group(): スプライトグループを生成します。これは、ゲーム中の複数のオブジェクト(敵、雲など)を管理するためのものです。
  • enemies, clouds, all_sprites: 敵、雲、およびすべてのスプライトを格納するためのグループをそれぞれ作成します。
  • clock = pygame.time.Clock(): ゲームのフレームレートを管理するためのクロックオブジェクトを生成します。

これらのコードにより、ゲームの基本的なフレームワークが構築され、プレイヤーの操作や敵の出現、背景の雲などが管理されるようになります。これによって、ゲームの主要な構造と動作の基盤が設定されます。

ステップ4: プレイヤーと敵のクラス定義

ゲームの主要なオブジェクトとして、プレイヤー、敵、雲を定義します。これらはそれぞれクラスとして定義され、画像の読み込み、位置の設定、動きの更新などの機能を持っています。特にプレイヤークラスでは、キーボードの入力に応じてキャラクターを動かす機能が備わっています。

# Define a player object by extending pygame.sprite.Sprite
# The surface drawn on the screen is now an attribute of 'player'
class Player(pygame.sprite.Sprite):
    def __init__(self):
        super(Player, self).__init__()
        self.surf = pygame.image.load("materials-pygame-a-primer/jet.png").convert()
        self.surf.set_colorkey((255,255,255), RLEACCEL)
        self.rect = self.surf.get_rect(center=(SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
    
    # Move the sprite based on user keypresses
    def update(self, pressed_keys):
        if pressed_keys[K_UP]:
            self.rect.move_ip(0, -5)
            move_up_sound.play()
        if pressed_keys[K_DOWN]:
            self.rect.move_ip(0, 5)
            move_down_sound.play()
        if pressed_keys[K_LEFT]:
            self.rect.move_ip(-5, 0)
        if pressed_keys[K_RIGHT]:
            self.rect.move_ip(5, 0)
        if self.rect.left < 0:
            self.rect.left = 0
        if self.rect.right > SCREEN_WIDTH:
            self.rect.right = SCREEN_WIDTH
        if self.rect.top < 0:
            self.rect.top = 0
        if self.rect.bottom >= SCREEN_HEIGHT:
            self.rect.bottom = SCREEN_HEIGHT

class Enemy(pygame.sprite.Sprite):
    def __init__(self):
        super(Enemy, self).__init__()
        self.surf = pygame.image.load("materials-pygame-a-primer/missile.png").convert()
        self.surf.set_colorkey((255, 255, 255), RLEACCEL)
        self.rect = self.surf.get_rect(
            center = (
                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
                random.randint(0, SCREEN_HEIGHT)
            )
        )
        self.speed = random.randint(5, 20)
    
    def update(self):
        self.rect.move_ip(-self.speed, 0)
        if self.rect.right < 0:
            self.kill()

class Cloud(pygame.sprite.Sprite):
    def __init__(self):
        super(Cloud, self).__init__()
        self.surf = pygame.image.load("materials-pygame-a-primer/cloud.png").convert()
        self.surf.set_colorkey((0,0,0), RLEACCEL)

        self.rect = self.surf.get_rect(
            center = (
                random.randint(SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
                random.randint(0, SCREEN_HEIGHT)
            )
        )

    def update(self):
        self.rect.move_ip(-5, 0)
        if self.rect.right < 0:
            self.kill()

  • class Player(pygame.sprite.Sprite): プレイヤーを表すクラスです。pygame.sprite.Spriteから継承しています。
    • __init__: コンストラクタ。プレイヤーの初期化を行います。
    • surf: プレイヤーの画像を読み込んでいます。
    • rect: 画像の矩形部分を取得します。これはプレイヤーの位置や衝突判定に使われます。
    • update: キー入力に応じてプレイヤーの位置を更新します。
  • class Enemy(pygame.sprite.Sprite): 敵を表すクラスです。
    • __init__: 敵の初期化を行います。
    • surfrect: 敵の画像と位置を設定します。
    • update: 敵が画面を横切って移動するように更新します。画面の左端を超えたら、敵を削除します。

updateのコードは、Pygameでのキャラクターの移動と画面内での位置制御を行うためのメソッドです。具体的には、プレイヤーキャラクターの移動と、それが画面外に出ないようにするためのロジックが含まれています。

update メソッドの解説

def update(self, pressed_keys):
  • update は、Playerクラスのメソッドで、ゲームの各フレームでキャラクターの状態を更新するために使用されます。
  • self はメソッドが属するオブジェクト(ここではPlayerオブジェクト)を指します。
  • pressed_keys は、どのキーが押されているかを表す辞書(または配列)です。

キー入力による移動

if pressed_keys[K_UP]:
    self.rect.move_ip(0, -5)
    move_up_sound.play()
  • pressed_keys[K_UP]: もし上矢印キーが押されているなら、以下のコードが実行されます。
  • self.rect.move_ip(0, -5): Playerの位置を上に5ピクセル動かします。move_ipは「in place」の略で、オブジェクトの現在の位置を基に移動します。
  • move_up_sound.play(): 上矢印キーが押されたときに音を再生します。

画面内での位置制御

if self.rect.left < 0:
    self.rect.left = 0
if self.rect.right > SCREEN_WIDTH:
    self.rect.right = SCREEN_WIDTH
if self.rect.top < 0:
    self.rect.top = 0
if self.rect.bottom >= SCREEN_HEIGHT:
    self.rect.bottom = SCREEN_HEIGHT
  • これらの条件は、Playerが画面の外に出ないように制御するためのものです。
  • self.rect はPlayerの位置とサイズを表す矩形(Rectオブジェクト)です。
  • left, right, top, bottom は、その矩形の左端、右端、上端、下端の位置を表します。
  • このコードは、Playerが画面の境界を超えた場合に、Playerを画面内にとどめるためにその位置を調整しています。

これらのコードにより、キャラクターの動きと画面内での位置制御が実現されています。これによって、プレイヤーはキーボードの矢印キーを使ってキャラクターを自由に操作できるようになり、キャラクターが画面外に出ることなくゲームを楽しむことができます。

ステップ5: ゲームのメインループ

ゲームのメインループでは、プレイヤーの入力を受け取り、プレイヤーと敵の位置を更新し、画面に表示します。また、プレイヤーが敵と衝突した場合にはゲームを終了させます。このループはゲームが実行されている間続きます。

while running:
    # for loop through the event queue
    for event in pygame.event.get():
        # Check for KEYDOWN event
        if event.type == KEYDOWN:
            # If the Esc key is pressed, then exit the main loop
            if event.key == K_ESCAPE:
                running = False
        # Check for QUIT event. If QUIT, then set running to false.
        elif event.type == QUIT:
            running = False
        
        elif event.type == ADDENEMY:
            new_enemy = Enemy()
            enemies.add(new_enemy)
            all_sprites.add(new_enemy)
        elif event.type == ADDCLOUD:
            new_cloud = Cloud()
            clouds.add(new_cloud)
            all_sprites.add(clouds)
    # print("ref_count" ,sys.getrefcount(enemies))
    # print("count enemies list", len(enemies))


    pressed_keys = pygame.key.get_pressed()

    # Update the player sprite based on user keypresses
    player.update(pressed_keys)

    enemies.update()
    clouds.update()

    screen.fill((135, 206, 250))

    for entity in all_sprites:
        screen.blit(entity.surf, entity.rect)

    if pygame.sprite.spritecollideany(player, enemies):
        player.kill()
        move_up_sound.stop()
        move_down_sound.stop()
        collision_sound.play()
        running = False

    # Draw the player on the screen
    screen.blit(player.surf, player.rect)
    # print(player.rect)

    # Update the display
    pygame.display.flip()

    clock.tick(30)
  • while running: ゲームのメインループ。この中でゲームの処理が繰り返されます。
  • for event in pygame.event.get(): イベントを取得し、それぞれのイベントに応じた処理を行います。
  • pressed_keys = pygame.key.get_pressed(): キーボードの状態を取得します。
  • player.update(pressed_keys): プレイヤーの位置を更新します。
  • enemies.update()clouds.update(): 敵と雲の位置を更新します。
  • screen.fill((135, 206, 250)): 画面を青空の色で塗りつぶします。
  • for entity in all_sprites: すべてのスプライトを画面に描画します。
  • pygame.sprite.spritecollideany(player, enemies): プレイヤーが敵と衝突したかどうかを判定します。
  • pygame.display.flip(): 画面を更新します。
  • clock.tick(30): フレームレートを制御します。

衝突判定について

衝突判定のコードは、Pygameでのプレイヤーと敵の衝突判定および、衝突が発生した場合の処理を行っています。

コードの解説

if pygame.sprite.spritecollideany(player, enemies):
    player.kill()
    move_up_sound.stop()
    move_down_sound.stop()
    collision_sound.play()
    running = False
  • pygame.sprite.spritecollideany(player, enemies): この関数は、playerスプライトがenemiesグループのいずれかのスプライトと衝突しているかどうかをチェックします。衝突が検出されると、Trueが返されます。

  • player.kill(): これはplayerスプライトを全てのスプライトグループから削除します。これにより、プレイヤーはゲームの表示や処理から取り除かれます。

  • move_up_sound.stop(), move_down_sound.stop(): これらの行は、プレイヤーが動いている際に再生されているかもしれない音(上昇音や下降音)を停止します。

  • collision_sound.play(): 衝突時の音を再生します。これにより、プレイヤーが敵と衝突したことを音で伝えます。

  • running = False: ゲームのメインループを制御する変数runningをFalseに設定します。これにより、ゲームのメインループが終了し、ゲームが終了します。

衝突判定の効果

この処理により、プレイヤーが敵に衝突した場合、ゲームの状態が更新され、ゲームが終了に向かいます。プレイヤーは削除され、衝突時の音が再生され、ゲームのループが停止します。これは一般的なビデオゲームにおける「ライフを失う」や「ゲームオーバー」の処理に相当します。

ステップ5: ゲームの終了処理

  • ゲームループが終了した後に、必要に応じてリソースを解放し、pygameを終了させます。

このように、各ステップはゲームの異なる側面を扱い、プログラム全体が一つのゲームを形作っています。

まとめ

この記事を通じて学べる主なポイントは以下の通りです。

  1. 基本的なPythonプログラミング: プログラム全体を通して、Pythonの基本的な構文や概念について学ぶことができます。

  2. Pygameライブラリの使用: PygameはPythonでゲームを作るための人気のあるライブラリです。この記事では、Pygameの基本的な使い方と、ゲーム開発におけるその応用について学びます。

  3. ゲーム開発の基本要素: ゲームループ、スプライトの管理、イベント処理、衝突判定といったゲーム開発の基本的な要素について理解を深めることができます。

  4. オブジェクト指向プログラミングの理解: クラスとオブジェクトの定義、継承、メソッドの使用といったオブジェクト指向プログラミングの概念を実践的に学ぶことができます。

  5. キーボード入力の処理: ユーザーのキーボード入力を受け取り、それに応じてゲーム内のキャラクターを動かす方法を学びます。

  6. ゲームのビジュアルとオーディオ: ゲームにビジュアル(スプライト、背景など)とオーディオ(効果音、BGMなど)を追加する方法について学びます。

  7. ゲームのフロー管理: ゲームの開始、実行、終了といったライフサイクルを管理する方法について理解します。

参照

Discussion