Pygame を使ってエイリアンゲームを作成する

PythonBeginner
オンラインで実践に進む

はじめに

このプロジェクトでは、Pygame ライブラリを使って「エイリアン」という簡単なゲームを作成するプロセスを案内します。このゲームでは、エイリアンの侵入者を撃ち落として防御することができます。プロジェクトファイルのセットアップから完成したゲームの実行まで、開発プロセスを複数のステップに分けます。

Pygame は、Python で 2D ゲームを作成するための人気のあるライブラリです。グラフィック、サウンド、ユーザー入力を扱う機能が備えており、ゲーム開発に興味がある初心者にとって優れた選択肢になっています。

👀 プレビュー

Aliens Game

このゲームはPygame のサンプルを元に修正されています。

🎯 タスク

このプロジェクトでは、以下のことを学びます。

  • 初期のプロジェクト構造をセットアップし、画像やサウンドなどの必要なリソースを読み込む方法。
  • プレイヤーキャラクターとエイリアンの侵入者のクラスを定義する方法。
  • 爆発、プレイヤーの射撃、エイリアンの爆弾、ゲームスコアを扱うための追加のクラスを作成する方法。
  • ゲームを初期化し、リソースを読み込み、ゲームウィンドウをセットアップする方法。
  • メインゲームループを実装し、プレイヤー入力を処理し、ゲームエンティティを更新し、衝突を処理し、ゲームシーンを描画する方法。

🏆 成果

このプロジェクトを完了すると、以下のことができるようになります。

  • Pygame ライブラリを使って 2D ゲームを開発する。
  • Pygame で画像を読み込み表示する。
  • ユーザー入力を処理し、プレイヤーの移動を制御する。
  • スプライトクラスを使ってゲームエンティティを作成し更新する。
  • ゲームエンティティ間の衝突を処理する。
  • ゲームシーンを描画し、画面を更新する。
  • ゲーム内でサウンドエフェクトと音楽を再生する。
  • ゲームロジックを管理するためのメインゲームループを実装する。

プロジェクトファイルを作成する

このステップでは、初期のプロジェクト構造をセットアップし、画像やサウンドなどの必要なリソースを読み込みます。

aliens.py という新しいファイルを作成し、以下のコードを追加します。

cd ~/project
touch aliens.py
sudo pip install pygame

その後、好きなテキストエディタでファイルを開き、以下のコードを追加します。

import os
import random
from typing import List

## 基本的な pygame モジュールをインポート
import pygame as pg

## 標準の BMP 以外の画像を読み込めるかどうか確認
if not pg.image.get_extended():
    raise SystemExit("Sorry, extended image module required")


## ゲーム定数
MAX_SHOTS = 2  ## 画面上の最大プレイヤーの弾数
ALIEN_ODDS = 22  ## 新しいエイリアンが出現する確率
BOMB_ODDS = 60  ## 新しい爆弾が落ちる確率
ALIEN_RELOAD = 12  ## 新しいエイリアンの出現までのフレーム数
SCREENRECT = pg.Rect(0, 0, 640, 480)
SCORE = 0

main_dir = os.path.split(os.path.abspath(__file__))[0]


def load_image(file):
    """画像を読み込み、ゲーム用に準備する"""
    file = os.path.join(main_dir, "data", file)
    try:
        surface = pg.image.load(file)
    except pg.error:
        raise SystemExit(f'Could not load image "{file}" {pg.get_error()}')
    return surface.convert()


def load_sound(file):
    """pygame がミキサーなしでコンパイルされる場合があるため"""
    if not pg.mixer:
        return None
    file = os.path.join(main_dir, "data", file)
    try:
        sound = pg.mixer.Sound(file)
        return sound
    except pg.error:
        print(f"Warning, unable to load, {file}")
    return None

このステップでは、必要なモジュールをインポートし、ゲーム定数を定義し、画像とサウンドエフェクトを読み込む関数を作成します。また、メインプロジェクトディレクトリもセットアップします。

def load_imagedef load_sound では、os.path.join 関数を使って、メインプロジェクトディレクトリと data ディレクトリを結合しています。ここにゲームリソースを保存します。次のステップでこのディレクトリを作成します。

✨ 解答を確認して練習

プレイヤーとエイリアンのクラスを定義する

次に、ゲームのキャラクター用のクラスである PlayerAlien を定義します。Player クラスはプレイヤーのキャラクターを表し、Alien クラスはエイリアンの侵入者を表します。

#... (ステップ 1 のコード)

## Player クラスを定義する
class Player(pg.sprite.Sprite):
    """月のバギー型の車としてのプレイヤーを表す。"""

    speed = 10
    bounce = 24
    gun_offset = -11
    images: List[pg.Surface] = []

    def __init__(self, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.image = self.images[0]
        self.rect = self.image.get_rect(midbottom=SCREENRECT.midbottom)
        self.reloading = 0
        self.origtop = self.rect.top
        self.facing = -1

    def move(self, direction):
        if direction:
            self.facing = direction
        self.rect.move_ip(direction * self.speed, 0)
        self.rect = self.rect.clamp(SCREENRECT)
        if direction < 0:
            self.image = self.images[0]
        elif direction > 0:
            self.image = self.images[1]
        self.rect.top = self.origtop - (self.rect.left // self.bounce % 2)

    def gunpos(self):
        pos = self.facing * self.gun_offset + self.rect.centerx
        return pos, self.rect.top

ここでは、Player クラスを定義しました。速度、跳ね返り、銃のオフセットなどの属性が含まれています。move メソッドはプレイヤーの移動を処理します。gunpos メソッドはプレイヤーの銃の位置を返します。Player クラスは、可視ゲームオブジェクトのベースクラスである pg.sprite.Sprite クラスから継承されています。

## Alien クラスを定義する
class Alien(pg.sprite.Sprite):
    """宇宙船のエイリアン。画面をゆっくりと下に移動する。"""

    speed = 13
    animcycle = 12
    images: List[pg.Surface] = []

    def __init__(self, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.image = self.images[0]
        self.rect = self.image.get_rect()
        self.facing = random.choice((-1, 1)) * Alien.speed
        self.frame = 0
        if self.facing < 0:
            self.rect.right = SCREENRECT.right

    def update(self):
        self.rect.move_ip(self.facing, 0)
        if not SCREENRECT.contains(self.rect):
            self.facing = -self.facing
            self.rect.top = self.rect.bottom + 1
            self.rect = self.rect.clamp(SCREENRECT)
        self.frame = self.frame + 1
        self.image = self.images[self.frame // self.animcycle % 3]

Alien クラスはエイリアンの侵入者を表します。速度とアニメーション用の属性があります。エイリアンは左右に移動し、その画像がサイクルします。update メソッドはエイリアンの移動とアニメーションを処理します。Alien クラスは、可視ゲームオブジェクトのベースクラスである pg.sprite.Sprite クラスから継承されています。

✨ 解答を確認して練習

追加のゲームクラスを定義する

このステップでは、爆発、プレイヤーの射撃、エイリアンの爆弾、ゲームスコアを扱う追加のゲーム関連クラス:ExplosionShotBombScore を定義します。

#... (ステップ 2 のコード)

## Explosion クラスを定義する
class Explosion(pg.sprite.Sprite):
    """爆発。望ましくはエイリアンに、プレイヤーではなく!"""

    defaultlife = 12
    animcycle = 3
    images: List[pg.Surface] = []

    def __init__(self, actor, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.image = self.images[0]
        self.rect = self.image.get_rect(center=actor.rect.center)
        self.life = self.defaultlife

    def update(self):
        """ゲームループの各回で呼び出されます。

        'defaultlife' の間、爆発の画像を表示します。
        ゲームの各フレーム (update) で、'life' を減らします。

        また、爆発をアニメーション化します。
        """
        self.life = self.life - 1
        self.image = self.images[self.life // self.animcycle % 2]
        if self.life <= 0:
            self.kill()

## Shot クラスを定義する
class Shot(pg.sprite.Sprite):
    """プレイヤーのスプライトが発射する弾丸。"""

    speed = -11
    images: List[pg.Surface] = []

    def __init__(self, pos, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.image = self.images[0]
        self.rect = self.image.get_rect(midbottom=pos)

    def update(self):
        """ゲームループの各回で呼び出されます。

        各フレームで、弾丸を上に移動させます。
        """
        self.rect.move_ip(0, self.speed)
        if self.rect.top <= 0:
            self.kill()

## Bomb クラスを定義する
class Bomb(pg.sprite.Sprite):
    """エイリアンが落とす爆弾。"""

    speed = 9
    images: List[pg.Surface] = []

    def __init__(self, alien, explosion_group, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.image = self.images[0]
        self.rect = self.image.get_rect(midbottom=alien.rect.move(0, 5).midbottom)
        self.explosion_group = explosion_group

    def update(self):
        """ゲームループの各回で呼び出されます。

        各フレームで、スプライトの'rect' を下に移動させます。
        画面の下に到達したときは、以下を行います。

        - 爆発を起こす。
        - 爆弾を削除する。
        """
        self.rect.move_ip(0, self.speed)
        if self.rect.bottom >= 470:
            Explosion(self, self.explosion_group)
            self.kill()

## Score クラスを定義する
class Score(pg.sprite.Sprite):
    """スコアを追跡するためのクラス。"""

    def __init__(self, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.font = pg.font.Font(None, 20)
        self.font.set_italic(1)
        self.color = "white"
        self.lastscore = -1
        self.update()
        self.rect = self.image.get_rect().move(10, 450)

    def update(self):
        """update() では、スコアが変更された場合のみ更新します。"""
        if SCORE!= self.lastscore:
            self.lastscore = SCORE
            msg = f"Score: {SCORE}"
            self.image = self.font.render(msg, 0, self.color)

これらのクラスは、爆発、射撃、爆弾、プレイヤーのスコアの表示など、ゲームのさまざまな側面を処理します。

✨ 解答を確認して練習

ゲームを初期化してリソースを読み込む

次に、ゲームを引き続き初期化し、画像やサウンドなどの必要なリソースを読み込みます。

#... (ステップ 3 のコード)

def main(winstyle=0):
    ## pygame を初期化する
    if pg.get_sdl_version()[0] == 2:
        pg.mixer.pre_init(44100, 32, 2, 1024)
    pg.init()
    if pg.mixer and not pg.mixer.get_init():
        print("Warning, no sound")
        pg.mixer = None

    fullscreen = False
    ## 表示モードを設定する
    winstyle = 0  ## |FULLSCREEN
    bestdepth = pg.display.mode_ok(SCREENRECT.size, winstyle, 32)
    screen = pg.display.set_mode(SCREENRECT.size, winstyle, bestdepth)

    ## 画像を読み込み、スプライトクラスに割り当てる
    ## (これはクラスが使用される前、画面設定の後に行う)
    img = load_image("player1.gif")
    Player.images = [img, pg.transform.flip(img, 1, 0)]
    img = load_image("explosion1.gif")
    Explosion.images = [img, pg.transform.flip(img, 1, 1)]
    Alien.images = [load_image(im) for im in ("alien1.gif", "alien2.gif", "alien3.gif")]
    Bomb.images = [load_image("bomb.gif")]
    Shot.images = [load_image("shot.gif")]

ここでは、Pygame を初期化し、ゲームウィンドウをセットアップし、プレイヤー、エイリアン、爆発、爆弾、射撃用の画像を読み込みます。

✨ 解答を確認して練習

ゲームセットアップとメインループを完了する

次に、ゲームのセットアップを完成させ、すべてのゲームロジックと相互作用が行われるメインゲームループを作成します。

#... (ステップ 4 のコード)

def main(winstyle=0):
    #... (前のコード)

    ## ゲームウィンドウを装飾する
    icon = pg.transform.scale(Alien.images[0], (32, 32))
    pg.display.set_icon(icon)
    pg.display.set_caption("Pygame Aliens")
    pg.mouse.set_visible(0)

    ## 背景を作成し、背景画像をタイル状に配置する
    bgdtile = load_image("background.gif")
    background = pg.Surface(SCREENRECT.size)
    for x in range(0, SCREENRECT.width, bgdtile.get_width()):
        background.blit(bgdtile, (x, 0))
    screen.blit(background, (0, 0))
    pg.display.flip()

    ## サウンドエフェクトを読み込む
    boom_sound = load_sound("boom.wav")
    shoot_sound = load_sound("car_door.wav")
    if pg.mixer:
        music = os.path.join(main_dir, "data", "house_lo.wav")
        pg.mixer.music.load(music)
        pg.mixer.music.play(-1)

    ## ゲームグループを初期化する
    aliens = pg.sprite.Group()
    shots = pg.sprite.Group()
    bombs = pg.sprite.Group()
    all = pg.sprite.RenderUpdates()
    lastalien = pg.sprite.GroupSingle()

    ## 初期値を作成する
    alienreload = ALIEN_RELOAD
    clock = pg.time.Clock()

    ## 初期のスプライトを初期化する
    global SCORE
    player = Player(all)
    Alien(
        aliens, all, lastalien
    )  ## 注:これはスプライトグループに入るため「生きています」
    if pg.font:
        all.add(Score(all))

    ## プレイヤーが生きている間、メインループを実行する
    while player.alive():
        ## 入力を取得する
        for event in pg.event.get():
            if event.type == pg.QUIT:
                return
            if event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE:
                return
            if event.type == pg.KEYDOWN:
                if event.key == pg.K_f:
                    if not fullscreen:
                        print("Changing to FULLSCREEN")
                        screen_backup = screen.copy()
                        screen = pg.display.set_mode(
                            SCREENRECT.size, winstyle | pg.FULLSCREEN, bestdepth
                        )
                        screen.blit(screen_backup, (0, 0))
                    else:
                        print("Changing to windowed mode")
                        screen_backup = screen.copy()
                        screen = pg.display.set_mode(
                            SCREENRECT.size, winstyle, bestdepth
                        )
                        screen.blit(screen_backup, (0, 0))
                    pg.display.flip()
                    fullscreen = not fullscreen

        keystate = pg.key.get_pressed()

        ## 最後に描画されたスプライトをクリア/消去する
        all.clear(screen, background)

        ## すべてのスプライトを更新する
        all.update()

        ## プレイヤーの入力を処理する
        direction = keystate[pg.K_RIGHT] - keystate[pg.K_LEFT]
        player.move(direction)
        firing = keystate[pg.K_SPACE]
        if not player.reloading and firing and len(shots) < MAX_SHOTS:
            Shot(player.gunpos(), shots, all)
            if pg.mixer and shoot_sound is not None:
                shoot_sound.play()
        player.reloading = firing

        ## 新しいエイリアンを作成する
        if alienreload:
            alienreload = alienreload - 1
        elif not int(random.random() * ALIEN_ODDS):
            Alien(aliens, all, lastalien)
            alienreload = ALIEN_RELOAD

        ## 爆弾を落とす
        if lastalien and not int(random.random() * BOMB_ODDS):
            Bomb(lastalien.sprite, all, bombs, all)

        ## エイリアンとプレイヤーの間の衝突を検出する
        for alien in pg.sprite.spritecollide(player, aliens, 1):
            if pg.mixer and boom_sound is not None:
                boom_sound.play()
            Explosion(alien, all)
            Explosion(player, all)
            SCORE = SCORE + 1
            player.kill()

        ## 射撃がエイリアンに当たったかどうかを確認する
        for alien in pg.sprite.groupcollide(aliens, shots, 1, 1).keys():
            if pg.mixer and boom_sound is not None:
                boom_sound.play()
            Explosion(alien, all)
            SCORE = SCORE + 1

        ## エイリアンの爆弾がプレイヤーに当たったかどうかを確認する
        for bomb in pg.sprite.spritecollide(player, bombs, 1):
            if pg.mixer and boom_sound is not None:
                boom_sound.play()
            Explosion(player, all)
            Explosion(bomb, all)
            player.kill()

        ## シーンを描画する
        dirty = all.draw(screen)
        pg.display.update(dirty)

        ## フレームレートを 40fps に制限する。または 40HZ とも呼ばれ、1 秒間に 40 回という意味です。
        clock.tick(40)

    if pg.mixer:
        pg.mixer.music.fadeout(1000)
    pg.time.wait(1000)


## このスクリプトを実行している場合、「main」関数を呼び出す
if __name__ == "__main__":
    main()
    pg.quit()

メイン関数では、さまざまなエンティティ用のゲームグループを初期化し、初期値を作成し、メインゲームループを実装します。このループ内でゲームロジック、プレイヤーの入力処理、衝突検出、ゲームシーンの描画が行われます。

ゲームの実行

次に、以下のコマンドを実行することでゲームを実行できます。

cd ~/project
python aliens.py
✨ 解答を確認して練習

ゲームを実行する

次に、以下のコマンドを実行することでゲームを実行できます。

cd ~/project
python aliens.py
ゲーム実行のスクリーンショット
✨ 解答を確認して練習

まとめ

このプロジェクトでは、Pygame を使って簡単な「エイリアン」ゲームを作成するプロセスを複数のステップに分けました。プロジェクトファイルをセットアップし、プレイヤーとエイリアンのクラスを定義し、追加のゲーム関連クラスを作成し、リソースを使ってゲームを初期化しました。最後に、ゲームロジックと相互作用を処理するためのメインゲームループを実装しました。

次のステップでは、プレイヤーとエイリアンのクラスに機能を追加し、移動と射撃を処理し、ゲームの視覚的要素を更新します。