レッスンの目標

落ちものキャッチゲームをpython(pygame)で作ろう

ファイルを作成する

テキストエディタを使ってmcc1/code/pythonの下にcatchgame.pyというファイルを作成します。(既にcatchgame.pyは作成済みなので、「プログラムの実行」までの部分は、読むだけで、実際にプログラムを入力しなくても大丈夫です)

モジュールのインポート

作成したcatchgame.pyに以下のようなpythonコードを入力します。

import sys
import pygame
from pygame.locals import QUIT, KEYDOWN, K_SPACE, K_j, K_f
import random

先頭のimport sysではシステム関係のモジュールをインポートします。 次のimport pygameではpygameというゲーム作成モジュールをインポートし、次の行ではpygameの中で使用する各種イベントをインポートします。 最後のimport randomではランダム発生モジュールをインポートします。

初期化処理を行う

初期化の処理を追加します。

WIDTH = 480
HEIGHT = 360

pygame.init()
SURFACE = pygame.display.set_mode((WIDTH, HEIGHT))
FPSCLOCK = pygame.time.clock()

最初にpygame.init()でpygameを初期化します。 スクラッチの座標はx座標系は-240〜240(幅480)、y座標が-180〜180(高さ360)なのでスクラッチの座標系に合わせ480×360で画面を初期化します。 最後に、画像描画間隔を制御するための変数FPSCLOCKを獲得します。

スプライトの骨組みを作る

スクラッチの「スプライト」の仕組みを真似るため、スプライトの骨組みを作成します。

class Sprite:
	def __init__(self, image):
		self.image = image
		self.rect = self.image.get_rect()
		self.setxy(0, 0)
	def setx(self, x):
		self.rect.centerx = x + WIDTH / 2
	def sety(self, y):
		self.rect.centery = HEIGHT / 2 - y
	def setxy(self, x, y):
		self.setx(x)
		self.sety(y)
	def setdx(self, dx):
		self.rect.centerx += dx
	def setdy(self, dy):
		self.rect.centery -= dy
	def getx(self):
		return self.rect.centerx - WIDTH / 2
	def gety(self):
		return HEIGHT / 2 - self.rect.centery
	def draw(self):
		SURFACE.blit(self.image, self.rect.topleft)

初期化関数__init__()では、スプライトの画像を受取り、スプライトの画像変数に設定し、画像の描画範囲(矩形)を設定し、位置を(0, 0)に初期化します。 次に座標を設定する関数(setx, sety, setxy)を作成します。 スクラッチの各スプライトの位置は画像の中央なので、画像描画範囲(矩形)の中央に位置を設定します。 また、座標系をスクラッチに合わせるため、設定されたxの値にWIDTH/2を足し、設定されたyの値をHEIGHT/2から引きます。 座標変更関数(setdx, setdy)は設定した値をxに足し、yから引きます。 座標を返す関数(getx, gety)はスクラッチの座標系に変換して返します。 描画関数draw()では、スプライトの画像を座標位置に描画します。

コップを作る

スプライトの骨組みを利用して、コップを作成します。

class Cup(Sprite):
	_image = pygame.image.load("cup.svg")
	def __init__(self, x, y):
		super().__init__(Cup._image)

コップの画像ファイル(cup.svg)をpygame.image.load()関数に渡して画像に変換してクラス変数(_image)に設定しておき、初期化関数(init())の中でスプライトの初期化関数に渡して初期化します。

メイン関数を作り、関数を呼び出す。

def main():
	cup = Cup()
	while True:
		for event in pygame.event.get():
			if event.type == QUIT:
				pygame.quit()
				sys.exit()
			elif event.type == KEYDOWN:
				if event.key == K_SPACE:
					cup.setxy(0, -100)
				elif event.key == K_j:
					if cup.getx() < 100:
						cup.setdx(100)
				elif event.key == K_f:
					if cup.getx() > -100:
						cup.setdx(-100)
		SURFACE.fill((255, 255, 255))
		cup.draw()
		pygame.display.update()
		FPSCLOCK.tick(30)
		
if __name__ == "__main__":
	main()

main()という関数を定義し、コップを初期化した後、無限ループで繰り返し処理を行います。 無限ループの中ではイベントを獲得し、イベントが「QUIT」(ウィンドウを閉じた)だったらプログラムを終了します。 イベントが「スペースキーを押す」だったら、カップの位置を(0, -100)に設定します。 イベントが「jキーを押す」だったら、x座標が100未満だったら右に100移動します。 イベントが「fキーを押す」だったら、x座標が-100より大きかっら左に移動します。 画面を「白く」描画し、コップを描画し、画面を再描画し、スクラッチ同様、1秒に30回描画するようにします。 最後にメイン関数main()を呼びます。

プログラムの実行

ここまでのプログラムをcatchgame.pyという名前でmcc1\code\python\CatchGameの下の作成してありますので、python3 catchgame.pyを実行してみましょう。 スクラッチの「落ちものキャッチゲームの作り方」で「コップを作る」まで作成して実行した結果と同様、スペースキーを押すとコップが中央下に移動し、fキーで左に、jキーで右に移動することが確認できます。

ボールを作る

ここからは、実際に自分でプログラムを入力していきましょう。 スプライトの骨組みを利用して、ボールを作成します。 class Cup(Sprite):の後、def main():の前に以下のプログラムを追加します。

class Ball(Sprite):
	_image = pygame.image.load("ball.svg")
	def __init__(self):
		super().__init__(Ball._image)

画像ファイル以外は、コップとほぼ同一です。 次にスタートしたら、1秒毎に自分自身のクローンを作り、座標を(0, 150)に設定してずっとy座標を-5ずつ変える部分を作成します。

...
def main():
	cup = Cup()
	balls = []			# <-- 追加
	ballcnt = 0			# <-- 追加 
	start = False		# <-- 追加
	while:
		...
			if event.key == K_SPACE:
				cup.setxy(0, -100)
				start = True		# <-- 追加
		...
		if start:								# <-- 追加
			ballcnt += 1					# <-- 追加
			if ballcnt == 30:			# <-- 追加
				ball = Ball()				# <-- 追加
				ball.setxy(0, 150)	# <-- 追加
				balls.append(ball)	# <-- 追加
				ballcnt = 0					# <-- 追加
		SURFACE.fill((255, 255, 255))
		cup.draw()
		for ball in balls:			# <-- 追加
			ball.draw()						# <-- 追加
			ball.setdy(-5)				# <-- 追加
		pygame.display.update()
		...

ボールは複数作られるので、それを格納するリスト(配列)ballsを作成し、空で初期化します。 またボールのクローンを作る間隔をカウントするballcntという変数を用意し、0に初期化します。 また、スタートしたか否かを判断する変数startを用意し、Falseに初期化します。 スペースキーが押されたら、start変数をTrueにします。 start変数がTrueだったらballcntを1つずつカウントアップし、30になったら(1秒たったら)ボールのクローンを作成して、位置を(0, 150)に設定してボールのリストに追加し、ballcntを0に戻します。 最後に、ボールリストに入っている各ボールを描画し、座標を更新します。 ここまで、できたら実行し、スタートしたらボールが1秒毎に上から落ちてくることを確認してください。

ボールが下まで落ちたら、「失敗音」を出し、ボールを削除する

ボールが下まで落ちたら、「失敗音」を出し、ボールを削除します。

class Ball(Sprite):
	_image = pygame.image.load("ball.svg")
	def __init__(self):
		super().__init__(Ball._image)
		self.alive = True		# <-- 追加

無効なボールを削除するため、ボールが有効か否かの変数aliveを追加します。

def main():
	...
	start = False
	failsound = pygame.mixer.Sound("failed.wav")	# <-- 追加
	while True:
		...
		for ball in balls:
			ball.draw()
			if ball.gety() < -50:	# <-- 追加
				failsound.play()		# <-- 追加 
				ball.alive = False	# <-- 追加
			else:									# <-- 追加
				ball.setdy(-5)			# <-- 変更(インデント追加)
		balls = [ball for ball in balls if ball.alive]	# <-- 追加

まず、失敗音「Oops」を追加し、failsound変数に設定します。 ボールのy座標が-50未満の場合、失敗音を鳴らし(play())、ボールを無効にします。 最後に、ボールリストから無効なボールを削除(有効なボールだけ残す)します。

ボールが下コップに触れたら、「成功音」を出し、ボールを削除する

def main():
	...
	start = False
	failsound = pygame.mixer.Sound("failed.wav")
	succeedsound = pygame.mixer.Sound("succeed.wav")	# <-- 追加
	while True:
		...
		for ball in balls:
			ball.draw()
			if ball.rect.colliderect(cup.rect):		# <-- 追加
				succeedsound.play()									# <-- 追加
				ball.alive = False									# <-- 追加
			elif ball.gety() < -50:								# <-- 変更(if --> elif)
				failsound.play()
				ball.alive = False
			else:
				ball.setdy(-5)
		balls = [ball for ball in balls if ball.alive]

「失敗音」に加え、「成功音」も追加します。 ボールがコップに触れたらというのは、ボールの描画範囲(矩形)ball.rectとコップの描画範囲(矩形)cup.rectが重なっているか否かcolliderect()で判定し、重なっていたら成功音を鳴らして、ボールを無効にします。

ボールの出る位置を左・真ん中・右の中からでたらめに選ぶ

ボールがクローンされたとき、ボールのx座標を-100、0、100のいずれかにします。 そのためには-1, 0, 1の数をでたらめに作ってその値に100をかけ、その値をボールのx座標にします。

				ball = Ball()
				ball.setxy(random.randint(-1, 1) * 100, 150)	# <-- 変更

random.randint(a, b)はa以上b以下の整数をでたらめ(ランダム)に返す関数です。

3回失敗したらゲームを終了させる

失敗の回数を数える変数failcntを作り、ゲーム開始時に0にします。

def main():
	...
	failcnt = 0		# <-- 追加
	while True:
	...
				if event.key == K_SPACE:
					cup.setxy(0, -100)
					start = True
					failcnt = 0	# <-- 追加

メイン関数main()の初期化部分で失敗回数変数failcntを定義し、0に初期化します。 また、ゲーム開始時には、0に初期化します。 次に失敗したら、失敗回数を1増やし、3回(2より大きく)なったら、ゲームを終了させます。

			elif ball.gety() < -50:
				failsound.play()
				ball.alive = False
				failcnt += 1			# <-- 追加
				if failcnt > 2:		# <-- 追加
					start = False		# <-- 追加
					balls.clear()		# <-- 追加

ウィンドウを閉じたときのようにpygame.quit()sys.exit()を呼ぶようにすれば完全にプログラムは終了しますが、スクラッチのように再開できないので、start変数とボールリストを空にすることで、擬似的にゲーム終了状態にします。 プログラムは終了していないので、「スペースキーを押す」ことでいつでもゲームを再開できます。

失敗回数を画面に表示する

スクラッチでは、新規に作成した変数は初期値の「表示チェック」をはずさなければ、画面上に値が表示されます。 一方、python(pygame)では、変数を定義しただけでは表示してくれないので、スクラッチのように表示したい場合は、表示用のプログラムを追加する必要があります

def main():
	...
	font = pygame.font.Font("../onodera/ipaexg.ttf", 16)	# <-- 追加
	while True:
	...

まず、メイン関数の先頭の初期化部分でフォント(文字)の設定を行います。16というのはフォントの大きさが16ポイント(文字の大きさの単位)であるということです。

	...
	fail_image = font.render("失敗回数 "+str(failcnt), True, (0, 0, 0))	# <-- 追加
	SURFACE.blit(fail_image, (0, 0))	# <-- 追加
	pygame.display.update()

pygame.display.update()の直前に失敗変数を描画する処理を追加します。 変数名(「失敗回数」)と失敗回数変数failcntを文字列に変換したものstr()を文字イメージに変換render()し、描画blit()します。 render()では色も一緒に指定します。 blit()では描画位置も一緒に指定します。 ここまでできたら、実行し、失敗回数が表示され、ボールがでたらめに発生し、3回失敗したらプログラムが終了することを確認してください。

ボールの落ちる速さを変える

スクラッチ同様、落ちる速さをspeedという変数にします。 クローン毎に別の変数にするため各ボールの変数として作成します。

class Ball(Sprite):
	...
	def __init__(self):
		...
		self.speed = random.randint(-9, -5)	# <-- 追加

ボールの初期化関数__init__()の最後にspeedという変数を追加し、-9から-5まででたらめ(ランダム)な値を設定します。

	else:
		ball.setdy(ball.speed)	# <-- 変更

setdy()関数で各ボールのy座標を更新する部分を-5という固定値からball.speedという変数に変更します。

ボールのクローンを作る間隔を短くする

ゲームを難しくするため、ボールのクローンを作る間隔を0.5秒にします。

			if ballcnt == 15:	# <-- 変更
				ball = Ball()

カウンタballcntは1秒に30回更新されるので、1秒に1回から0.5秒に1回にするためには30から15に変更します。

点数を数える

点数の変数scoreを作成し、メイン関数main()の初期化部分で定義し、ゲーム開始時に0に初期化する。

def main():
	...
	score = 0		# <-- 追加
	while True:
	...
			if event.key == K_SPACE:
				...
				score = 0		# <-- 追加

コップに触れたとき、点数を1増やす。

			...
			if ball.rect.colliderect(cup.rect):
			...
				score += 1	# <-- 追加

画面の失敗回数の下に表示する

		...
		SURFACE.blit(fail_image, (0, 0))
		score_image = font.render("点数 "+str(score), True, (0, 0, 0))		# <-- 追加
		SURFACE.blit(score_image, (0, 20))	# <-- 追加

ここまでできたら、実行し、点数が表示され、ボールの速さがでたらめ(ランダム)に変わり、ボールの出現間隔が短くなったことを確認してください。

取ってはいけないカミナリを作る

取ると失敗になるカミナリを追加します。 ボールに似ているので、ボールをコピーし、一部を書き換えます。

class Lightning(Sprite):	# <-- 追加
	_image = pygame.transform.scale_by(pygame.image.load("lightning.svg"), 0.5)		# <-- 追加
	def __init__(self):		# <-- 追加
		super().__init__(Lightning._image)	# <-- 追加
		self.alive = True		# <-- 追加
		self.speed = -10		# <-- 追加

クラスの名前をBallからLightningに変更し、クラス変数_imageを参照する部分もBall._imageからLightning._imageに変更します。 なお、このままの大きさだとカミナリ画像が大きすぎるのでpygame.transform.scale_by()関数で画像のサイズを半分に変更します。 速度変数の値を固定値(-10)にします。

コップに触れた時の処理

コップに触れた時の処理は、ボールとカミナリでは異なるので、ボールとカミナリそれぞれにtouch()という関数を用意して、それぞれ処理を変えるようにします。

変数のグローバル化

ボールとカミナリからメイン関数の変数にアクセスできるよう必要な変数をグローバル化します。

def main():
	global succeedsound, failsound, score, failcnt, start, balls	# <-- 追加

ボールの処理の変更

次にボールがコップに触れた処理をボールのtouch()関数に移し、ボールがコップに触れた場合、touch()関数を呼ぶように変更します。

def Ball(Sprite):
	...
	def touch(self):	# <-- 追加
		global succeedsound, score	# <-- 追加
		succeedsound.play()	# <-- 追加
		self.alive = False	# <-- 追加
		score += 1	# <-- 追加
	...
def main():
	...
				if ball.rect.colliderect(cup.rect):
					ball.touch()	# <-- 変更

カミナリの処理の追加

カミナリがコップに触れた処理をカミナリのtouch()関数に追加します。

class Lightning(Sprite):
	...
	def touch(self):	# <-- 追加
		global failsound, failcnt, start, balls	# <-- 追加
		failsound.play()		# <-- 追加
		self.alive = False	# <-- 追加
		failcnt += 1				# <-- 追加
		if failcnt > 2:			# <-- 追加
			start = False			# <-- 追加
			balls.clear()			# <-- 追加

落ちた時の処理

落ちた時の処理も、ボールとカミナリでは異なるので、ボールとカミナリそれぞれにdrop()という関数を用意して、それぞれ処理を変えるようにします。

ボールの処理の変更

ボールが落ちた処理をボールのdrop()関数に移し、ボールが落ちた場合、drop()関数を呼ぶように変更します。

class Ball(Sprite):
	...
	def drop(self):		# <-- 追加
		global failsound, failcnt, start, ball	# <-- 追加
		failsound.play()		# <-- 追加
		self.alive = False	# <-- 追加
		failcnt += 1				# <-- 追加
		if failcnt > 2:			# <-- 追加
			start = False			# <-- 追加
			balls.clear()			# <-- 追加
	...
def main():
	...
				elif ball.gety() < -50:
					ball.drop()		# <-- 変更

カミナリの処理の追加

カミナリが落ちた処理をカミナリのdrop()関数に追加します。

class Lightning(Sprite):
	...
	def drop(self):	# <-- 追加
		self.alive = False	# <-- 追加

5秒に1回カミナリを発生させる

0.5秒間隔でボールを発生させる部分を変更し、5秒に1回はカミナリが発生するようにします。 発生カウントballcntはそのまま使用するので、15になったらボールを発生させて0に戻す処理を以下のように変更します。

			ballcnt += 1
			if ballcnt % 15 == 0:	# <-- 変更
				ball = Ball()
				ball.setxy(random.randint(-1, 1) * 100, 150)
				balls.append(ball)

15になったらではなく、15で割って余りが0ならにして、カウンタballcntは増やし続けます。 次に、150(30×5=5秒)で割って余りが0ならボールの代わりにカミナリを発生するように変更します。

			ballcnt += 1
			if ballcnt % 15 == 0:
				if ballcnt % 150 == 0:	# <-- 追加
					ball = Lightning()		# <-- 追加
				else:										# <-- 追加
					ball = Ball()					# <-- 変更(インデント追加)
				ball.setxy(random.randint(-1, 1) * 100, 150)
				balls.append(ball)

150で割り切れる値は15でも割り切れるので「ballcntが15で割り切れたら」の処理の中で行っても問題ありません。 以上、できあがったら、実行し、5秒に1回はカミナリが発生し、カミナリに触れると失敗、カミナリをよければ何も起こらないことを確認しましょう。