【AI + pygame】pygameで作るインベーダー風ゲーム 第1回 改造編その1(壁を作る)

最終更新:2019年04月22日 公開:2019年04月15日

(2019年4月22日更新)

お久しぶりです。エンジニアのMです。
今回はPython用ゲームライブラリ「pygame」を使ったインベーダー風ゲームを題材として、連載していきたいと思います。
第1回でいきなり改造編から始まるのは、「Pythonでゲーム作りますが何か? – 人工知能に関する断創録」という記事で公開されているインベーダー風ゲームをベースとして進めていくためです。
10年以上前の記事ですが、情報が豊富でpygameの勉強にもってこいです。
基礎の部分はこちらにお任せすることにしますので、先に目を通しておくことをおすすめします。

この連載ではゲーム内容の細かい改造を重ねていき、最終回はこのゲームに対する強化学習(deep Q-network)を予定しています。
今この記事を書いている裏でも、強化学習は進行中です。
ATARIのゲームを攻略して、強化学習の新たな可能性を拓いた「deep Q-network」。
果たしてAIは無事にこのゲームも攻略できるのでしょうか?

それでは早速ゲームを改造していきましょう。
ベースは、「ゲームオーバー画面 – 人工知能に関する断創録」のコードとなりますので、必要であればリンク先の「invader07.zip」をダウンロードしてください。
また、改造後のコードと画像データは、こちらからダウンロードできます。
-> pygame_invader_1.zip

以降のコードの動作確認はWindows版「PyCharm」上で行っています。
「PyCharm」はPythonの統合開発環境(IDE)です。「Community Edition」は無料で使用することができます。

  • Windows10 64bit
  • PyCharm 2017.3.3 (Community Edition)
  • Python 3.6.2
  • pygame 1.9.4

GUI環境のあるMac、Linuxでも動作するはずです。
環境構築についてはこの記事で解説しませんので、インターネット上の情報(Qiitaの記事など)を駆使して行ってください。

Python2.xで書かれたコードを3.x仕様に

最近はQiitaの記事でも、「Python2.x」仕様のコードはほとんど見なくなりました。
まずは10年前の「Python2.x」用コードを「Python3.x」用に修正しましょう。

修正点は以下の通りです。漏れなく修正しましょう。

print文を関数に + try,except文の修正

[code lang=”python”]
except pygame.error, message:
print "Cannot load image:", filename
raise SystemExit, message
[/code]

[code lang=”python”]
except pygame.error as message:
print("Cannot load image:", filename)
raise SystemExit(message)
[/code]

文字列のUnicode指定は不要

[code lang=”python”]
pygame.display.set_caption(u"Invader 07 ゲームオーバー画面")
[/code]

[code lang=”python”]
pygame.display.set_caption("Invader 07 ゲームオーバー画面")
[/code]
「u」があっても動作しますが、他の修正ついでに消してしまいましょう。

演算子「/」を「//」に変更

[code lang=”python”]
y = 20 + (i / 10) * 40
[/code]

[code lang=”python”]
y = 20 + (i // 10) * 40
[/code]
そのままでも動いてしまうので見落としがちですが、2.x系と3.x系で挙動が違うので移植に際しては注意が必要です。
[code lang=”python”]
3 / 2 = 1 # Python 2.x
3 / 2 = 1.5 # Python 3.x

3 // 2 = 1 # Python 3.x
[/code]
2.x系では、整数値同士の除算で小数点以下切り捨てとなります。
3.x系で同じ計算をすると、小数点以下まで計算されます。
3.x系で切り捨てた結果を得たい場合は、演算子「//」を使う必要があります。

弾から自機を守る壁を作る

移植作業も済んだので、ゲームの改造に取り掛かりましょう。

今のままでは自機が隠れる場所が無いので、弾を防ぐ壁を用意します。
「スペースインベーダー」では、あの壁のことを「トーチカ」と呼ぶそうです。

壁のクラスを作る

他のクラスからコピーして修正します。
壁へのダメージ判定や破壊判定は別で行いますので、このクラス自体は非常にシンプルです。
update の記述は無くても問題ないですが、敢えて何もしないことを明示しておきます。
[code lang=”python”]
class Wall(pygame.sprite.Sprite):
"""ミサイル・ビームを防ぐ壁"""
shield = 100 # 耐久力

def __init__(self, pos):
# imagesとcontainersはmain()でセット
pygame.sprite.Sprite.__init__(self, self.containers)
self.rect = self.image.get_rect()
self.rect.center = pos

def update(self):
pass
[/code]

画像の読み込み

忘れずに画像も登録しておきます。
[code lang=”python” highlight=”9″]
class Invader:
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")
Wall.image = load_image("wall.png")
Explosion.images = split_image(load_image("explosion.png"), 16)
[/code]

スプライトの登録

エイリアンと同様にして、壁を登録・作成します。
[code lang=”python” highlight=”11,17,26-30″]
class Invader:
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() # ビームグループ
self.walls = 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
Wall.containers = self.all, self.walls
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))
# 壁を作成
for i in range(4):
x = 95 + i * 150
y = 400
Wall((x, y))
[/code]

残りの耐久値を描画する

見た目は少し不格好ですが、壁に耐久値を表示するようにしましょう。
文字の描画サイズを取得して、ちょうど壁の中央に描画されるように調整しています。
[code lang=”python” highlight=”7-12″]
class Invader:
def draw(self, screen):
"""描画"""
# 中略
elif self.game_state == PLAY: # ゲームプレイ画面
self.all.draw(screen)
shield_font = pygame.font.SysFont(None, 30)
for wall in self.walls:
shield = shield_font.render(str(wall.shield), False, (0,0,0))
text_size = shield_font.size(str(wall.shield))
screen.blit(shield, (wall.rect.center[0]-text_size[0]//2,
wall.rect.center[1]-text_size[1]//2))
# 略
[/code]

壁との衝突判定を実装する

sprite.groupcollide の返り値は辞書型です。
壁とミサイル、ビームの衝突判定を一旦 hit_dict にまとめて、一括で処理します。
(ミサイル=プレイヤーの撃った弾(shots)、ビーム=エイリアンの撃った弾(beams))
groupcollide の引数で与えている真偽値は消去判定です。
shots,beams は無条件で消滅するので True を指定し、walls は条件付きで消滅するのでここでは False を指定します。

稀なケースですが、全く同じタイミングで1つの壁に複数のビームが当たった場合も想定しています。
各オブジェクトの耐久値が「0」以下になったら、kill で消去します。
[code lang=”python” highlight=”5-11″]
class Invader:
def collision_detection(self):
"""衝突判定"""
# 中略
# 壁とミサイル、ビームの衝突判定
hit_dict = pygame.sprite.groupcollide(self.walls, self.shots, False, True)
hit_dict.update(pygame.sprite.groupcollide(self.walls, self.beams, False, True))
for hit_wall in hit_dict:
hit_wall.shield -= len(hit_dict[hit_wall])
if hit_wall.shield <= 0:
hit_wall.kill()
[/code]

まとめ

以上で、インベーダー風ゲームに壁が実装されました。
「pygame」の機能を利用すると、簡単にオブジェクトを追加・生成できることがわかります。
次回も色々改造して、「pygame」についての理解を深めていきましょう。

また、Python 2.x から Python 3.x への移植も行いました。
今後移植せざるを得ない状況というのは滅多にないと思いますが、豆知識程度に覚えておくとどこかで役に立つかもしれません。

今回のデータはこちらから(再掲) -> pygame_invader_1.zip

コード全体は以下をクリックすると表示されます。
[code lang=”python” title=”Invader1_All_code” collapse=”true”]
#!/usr/bin/env python
#coding: utf-8
import pygame
from pygame.locals import *
import os
import random
import sys

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("Invader Part1")
# 素材のロード
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() # ビームグループ
self.walls = 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
Wall.containers = self.all, self.walls
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))
# 壁を作成
for i in range(4):
x = 95 + i * 150
y = 400
Wall((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("2019 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)
shield_font = pygame.font.SysFont(None, 30)
for wall in self.walls:
shield = shield_font.render(str(wall.shield), False, (0,0,0))
text_size = shield_font.size(str(wall.shield))
screen.blit(shield, (wall.rect.center[0]-text_size[0]//2,
wall.rect.center[1]-text_size[1]//2))
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 # ゲームオーバー!
# 壁とミサイル、ビームの衝突判定
hit_dict = pygame.sprite.groupcollide(self.walls, self.shots, False, True)
hit_dict.update(pygame.sprite.groupcollide(self.walls, self.beams, False, True))
for hit_wall in hit_dict:
hit_wall.shield -= len(hit_dict[hit_wall])
if hit_wall.shield <= 0:
hit_wall.kill()

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")
Wall.image = load_image("wall.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")

class Player(pygame.sprite.Sprite):
"""自機"""
speed = 5 # 移動速度
reload_time = 15 # リロード時間
def __init__(self):
# imageとcontainersはmain()でセット
pygame.sprite.Sprite.__init__(self, self.containers)
self.rect = self.image.get_rect()
self.rect.bottom = SCR_RECT.bottom # プレイヤーが画面の一番下
self.reload_timer = 0
def update(self):
# 押されているキーをチェック
pressed_keys = pygame.key.get_pressed()
# 押されているキーに応じてプレイヤーを移動
if pressed_keys[K_LEFT]:
self.rect.move_ip(-self.speed, 0)
elif pressed_keys[K_RIGHT]:
self.rect.move_ip(self.speed, 0)
self.rect.clamp_ip(SCR_RECT)
# ミサイルの発射
if pressed_keys[K_SPACE]:
# リロード時間が0になるまで再発射できない
if self.reload_timer > 0:
# リロード中
self.reload_timer -= 1
else:
# 発射!!!
Player.shot_sound.play()
Shot(self.rect.center) # 作成すると同時にallに追加される
self.reload_timer = self.reload_time

class Alien(pygame.sprite.Sprite):
"""エイリアン"""
speed = 2 # 移動速度
animcycle = 18 # アニメーション速度
frame = 0
move_width = 230 # 横方向の移動範囲
prob_beam = 0.005 # ビームを発射する確率
def __init__(self, pos):
# imagesとcontainersはmain()でセット
pygame.sprite.Sprite.__init__(self, self.containers)
self.image = self.images[0]
self.rect = self.image.get_rect()
self.rect.center = pos
self.left = pos[0] # 移動できる左端
self.right = self.left + self.move_width # 移動できる右端
def update(self):
# 横方向への移動
self.rect.move_ip(self.speed, 0)
if self.rect.center[0] < self.left or self.rect.center[0] > self.right:
self.speed = -self.speed
# ビームを発射
if random.random() < self.prob_beam:
Beam(self.rect.center)
# キャラクターアニメーション
self.frame += 1
self.image = self.images[self.frame//self.animcycle%2]

class Shot(pygame.sprite.Sprite):
"""プレイヤーが発射するミサイル"""
speed = 9 # 移動速度
def __init__(self, pos):
# imageとcontainersはmain()でセット
pygame.sprite.Sprite.__init__(self, self.containers)
self.rect = self.image.get_rect()
self.rect.center = pos # 中心座標をposに
def update(self):
self.rect.move_ip(0, -self.speed) # 上へ移動
if self.rect.top < 0: # 上端に達したら除去
self.kill()

class Beam(pygame.sprite.Sprite):
"""エイリアンが発射するビーム"""
speed = 5 # 移動速度
def __init__(self, pos):
# imagesとcontainersはmain()でセット
pygame.sprite.Sprite.__init__(self, self.containers)
self.rect = self.image.get_rect()
self.rect.center = pos
def update(self):
self.rect.move_ip(0, self.speed) # 下へ移動
if self.rect.bottom > SCR_RECT.height: # 下端に達したら除去
self.kill()

class Explosion(pygame.sprite.Sprite):
"""爆発エフェクト"""
animcycle = 2 # アニメーション速度
frame = 0

def __init__(self, pos):
# imagesとcontainersはmain()でセット
pygame.sprite.Sprite.__init__(self, self.containers)
self.image = self.images[0]
self.rect = self.image.get_rect()
self.rect.center = pos
self.max_frame = len(self.images) * self.animcycle # 消滅するフレーム

def update(self):
# キャラクターアニメーション
self.image = self.images[self.frame//self.animcycle]
self.frame += 1
if self.frame == self.max_frame:
self.kill() # 消滅

class Wall(pygame.sprite.Sprite):
"""ミサイル・ビームを防ぐ壁"""
shield = 100 # 耐久力

def __init__(self, pos):
# imagesとcontainersはmain()でセット
pygame.sprite.Sprite.__init__(self, self.containers)
self.rect = self.image.get_rect()
self.rect.center = pos

def update(self):
pass

def load_image(filename, colorkey=None):
"""画像をロードして画像と矩形を返す"""
filename = os.path.join("data", filename)
try:
image = pygame.image.load(filename)
except pygame.error as message:
print("Cannot load image:", filename)
raise SystemExit(message)
image = image.convert()
if colorkey is not None:
if colorkey is -1:
colorkey = image.get_at((0,0))
image.set_colorkey(colorkey, RLEACCEL)
return image

def split_image(image, n):
"""横に長いイメージを同じ大きさのn枚のイメージに分割
分割したイメージを格納したリストを返す"""
image_list = []
w = image.get_width()
h = image.get_height()
w1 = w // n
for i in range(0, w, w1):
surface = pygame.Surface((w1,h))
surface.blit(image, (0,0), (i,0,w1,h))
surface.set_colorkey(surface.get_at((0,0)), RLEACCEL)
surface.convert()
image_list.append(surface)
return image_list

def load_sound(filename):
"""サウンドをロード"""
filename = os.path.join("data", filename)
return pygame.mixer.Sound(filename)

if __name__ == "__main__":
Invader()
[/code]

アバター画像
フォーム作成クラウドサービス「Formzu(フォームズ)」を運営しているフォームズ株式会社です。
オフィスで働く方、ホームページを運営されている皆様へ
仕事の効率化、ビジネススキル、ITノウハウなど役立つ情報をお届けします。
  • 【初めての方へ】Formzuで仕事の効率化
  • 【初めての方へ】メールフォームについて