8.5 アニメーション

(1/1)
レンチキュラー印刷のイラスト
Python は外部ライブラリを活用することで、ゲームなどに使うアニメーション効果をプログラミングすることができる。そこで今回は、グラフを動かしたり、背景画像に雪を降らせたり、打ち上げ花火をあげるアニメーション・プログラムを作ってみることにする。

目次

サンプル・プログラム

圧縮ファイルの内容
moveSinCurve.py正弦波を動かすサンプル・プログラム
snowfall.py背景画像に雪を降らせるサンプル・プログラム
img20150101-001454e.jpg背景画像サンプル
firework.py打ち上げ花火のサンプル・プログラム
img20241020-182730r.jpg背景画像サンプル

正弦波を動かすサンプル・プログラムの使い方

画面に描いた正弦波(サインカーブ)を横方向に動かすには、「5.1 組み込み関数とモジュール」で紹介したグラフ描画の外部ライブラリ Matplotlib と科学技術計算ライブラリ NumPy が必要になる。まだインストールしていない方は、pipコマンドを使ってインストールしてほしい。
pip install matplotlib
pip install numpy
正弦波を動かす - Python
インストールできたら、プログラム "moveSinCurve.py" を実行してみてほしい。左図のように正弦波が横方向に動いていくだろう。

解説:正弦波を動かすサンプル・プログラム

moveSinCurve.py
# パラメータ
# グラフの描画範囲
X_MIN = 0.0
X_MAX = 10.0

# メイン・プログラム ======================================================= # グラフの描画領域 fig = plt.figure()
# x方向の等差数列を生成 x = np.arange(X_MIN, X_MAX, 0.1)
# プロット情報を作成する plotList = [] # プロット情報を格納 for a in range(50): # y座標を計算 y = np.sin(x - a) # プロット情報を作成 pl = plt.plot(x, y, color="blue") plotList.append(pl)
# plotListを動かす ani = animation.ArtistAnimation(fig, plotList)
# 画面に表示する plt.show()
# GIFアニメーションで保存する # ani.save("moveSinCurve.gif", writer="imagemagick")
まず、matplotlib.pyplot.figure を使ってグラフの描画領域を用意する。
次に、NumPy の numpy.arange を使って、グラフのX座標の等差数列(X_MINからX_MAXまで等間隔にプロットするためのX座標)を生成する。
そして、for文を使って、50個分のプロット情報をリスト plotList に格納する。Y座標は NumPy の sin関数を使って求め、matplotlib.pyplot.plot を使って1つ分のプロット情報を求める。引数colorに指定するのは、プロット(サインカーブ)の色である。

プロット情報が用意できたら、matplotlib.animation.ArtistAnimation に渡して動かす。実際に画面に表示するのは matplotlib.pyplot.show だ。
ここではコメントアウトしているが、matplotlib.animation.Animation.save を使うことでGIFアニメーションとして保存することが出来る。

背景画像に雪を降らせるサンプル・プログラムの使い方

背景画像に雪を降らせるには、Pythonでゲーム開発を行うときに使われる外部ライブラリ Pygame と「8.3 Exif情報」で使った外部ライブラリ Pillow(PIL)が必要になるので、まだインストールしていない方は、pipコマンドを使ってインストールしてほしい。
pip install pygame
pip install PIL
背景画像に雪を降らせる - Python
インストールできたら、プログラム "snowfall.py" を実行してみてほしい。左図のように背景画像に雪が降る。ウィンドウの右上×ボタンをクリックするとプログラムは終了する。

解説:背景画像に雪を降らせるサンプル・プログラム

snowfall.py
# 初期値  ===================================================================
# 雪片の数
snowflakeNum = 150
# 雪片の大きさ(最小値,最大値)
snowflakeMinSize, snowflakeMaxSize = 2, 5
# 雪片を降らせるスピード(最小値,最大値)
snowflakeMinSpeed, snowflakeMaxSpeed = 1, 2

# アニメーションのフレームレート fps = 30 # 背景画像ファイル bgfilename = 'img20150101-001454e.jpg'
画面にチラチラと降る白い雪の1つ1つを雪片と呼ぶことにする。同時に画面に表示する雪片の数を、あらかじめ変数 snowflakeNum に代入しておく。この値が大きくなればなるほど大量の切片が降るが、計算量が増えるので画面がちらつくかもしれない。
雪片の大きさは、1つ1つランダムにしているが、その大きさの範囲を snowflakeMinSize, snowflakeMaxSize に代入しておく。
雪片を降らせるスピードも、1つ1つランダムにしており、その範囲を snowflakeMinSpeed, snowflakeMaxSpeed に代入しておく。
アニメーションのフレームレートを fps に代入しておく。
背景画像ファイル名を bgfilename に代入しておく。ここでは圧縮ファイルに同梱の画像ファイル名を代入している。
snowfall.py
# 雪片クラス
class Snowflake:
	def __init__(self):
		self.x = random.randint(0, width)
		self.y = random.randint(0 - int((height / 2)), -10)
		self.speed = random.uniform(snowflakeMinSpeed, snowflakeMaxSpeed)
												# 雪片を降らせる速度
		self.size = random.randint(snowflakeMinSize, snowflakeMaxSize)
		self.wind = random.uniform(-1.5, 0)		# 風の影響を左寄りに設定
		self.angle = 0							# 雪片が揺れる動きの角度

# 1回分の動き def update(self): self.y += self.speed # 雪片を下へ移動させる # 左右に揺れる動き+左方向への強い風を追加 self.angle += random.uniform(-0.1, 0.1) self.x += self.wind + 1.5 * pygame.math.Vector2(1, 0).rotate(self.angle).x # 雪片が画面下に到着したら位置を初期化する if self.y > height: self.x = random.randint(0, width) self.y = random.randint(0 - int((height / 2)), -10)
# 画面に描画する def draw(self): pygame.draw.circle(screen, (255, 255, 255), (int(self.x), int(self.y)), self.size)
雪片の1つ1つをオブジェクトにした。動きのあるゲームでは、動きのある1つ1つをオブジェクトとして扱うことが多い。こうすることで、見た目は同じキャラクターでも、違う動きをさせることができるからだ。消費するメモリ量や計算量は少なくないが、いまどきのパソコンやスマホであれば、そこまで心配する必要はない。

雪片をオブジェクトとして扱うために、ユーザー定義クラス Snowflake を用意する。
コンストラクタ __init__ では、雪片の初期位置(x, y)、落下スピード speed、大きさ size、それから風の影響をランダムに設定してやる。

メソッド update を呼び出すと、スピードに風の影響を加味して雪片の位置を更新する。もし雪片が画面下部に到達したら(画面から外れて見えなくなったら)、位置を初期化する。

メソッド draw は雪片をアニメーションウィンドウに描画するもので、pygame.draw.circle を使って白く塗りつぶした円を描く。
snowfall.py
# メイン・プログラム =======================================================
# 画像の幅、高さを取得する
image = Image.open(bgfilename)
width, height = image.size

# Pygameを初期化 pygame.init()
# アニメーションウィンドウのサイズを指定 screen = pygame.display.set_mode((width, height))
# 背景画像を読み込み backgroundImage = pygame.image.load(bgfilename)
# 雪片を用意する snowflakes = [Snowflake() for _ in range(snowflakeNum)]
アニメーション画面の大きさは背景画像に合わせる。
そこで、PIL を使って背景画像ファイルを開き、imaze.size で画像の幅と高さを取得する。単位はピクセルだ。

ここから Pygame を使う。
まず、pygame.initPygame モジュールを初期化する。
先ほど取得した背景画像の幅、高さを使って、pygame.display.set_mode でアニメーションウィンドウサイズを指定する。
そして、pygame.image.load を使って背景画像をアニメーションウィンドウに読み込む。

そして、あらかじめ設定した雪片の数だけ、Snowflakeオブジェクトを生成し、リスト snowflakes に格納しておく。
snowfall.py
# メインループ
running = True
while running:
	for event in pygame.event.get():
		if event.type == pygame.QUIT:
			running = False
	# 背景を描画
	screen.blit(backgroundImage, (0, 0))
	# 雪片の位置を更新して描画する
	for snowflake in snowflakes:
		snowflake.update()
		snowflake.draw()

# 画面を更新する pygame.display.flip()
# フレームレートを指定する pygame.time.Clock().tick(fps)
# Pygameを終了する pygame.quit()
メインループは無限ループになっていて、pygame.event.get を使ってイベントを取得し、それが終了条件でないかぎり、ループは回り続ける。
screen.blit を使って背景を描画したら、1つ1つの雪片の位置を更新して描画する。最後に pygame.display.flip で画面を更新し、pygame.time.Clock().tick でフレームレートを指定する。
Pygameの終了は pygame.quit を使う。

打ち上げ花火のサンプル・プログラムの使い方

打ち上げ花火 - Python
今度は、背景画像に打ち上げ花火のアニメーションを加えるプログラムを作ってみる。今回も、外部ライブラリ PygamePillow(PIL)を利用する。

プログラム "firework.py" を実行してみてほしい。左図のように背景画像に打ち上げ花火があがる。ウィンドウの右上×ボタンをクリックするとプログラムは終了する。

解説:打ち上げ花火のサンプル・プログラム

firework.py
# 初期値  ===================================================================
# アニメーションのフレームレート
fps = 10

# 背景画像ファイル
bgfilename = 'img20241020-182730r.jpg'
初期値は、雪を降らせるプログラムと同様で、フレームレートと背景画像ファイルをあらかじめ代入しておく。ここでは圧縮ファイルに同梱の画像ファイル名を代入してある。
firework.py
class star:
	"""星クラス
	
	Attributes:
		rect(pygame.Rect): 星を描画する領域
		vx(int):	X方向の速度
		vy(int):	Y方向の速度
		list(list):	星オブジェクトのリスト
	
	Note:
		花火の星の1つ1つをオブジェクト化する.
	"""
	list = []

	def __init__(self, x0, y0, vx, vy, lifeTime, color):
		"""コンストラクタ
		
		Args:
			x0(int):	玉が破裂したときのX座標
			y0(int):	玉が破裂したときのY座標
			vx(int):	X方向の初速
			vy(int):	Y方向の初速
			lifeTime(float):	寿命(0.1秒単位)
			color(str):	星の色
		
	"""
		self.x0 = x0
		self.y0 = y0
		self.vx = vx
		self.vy = vy
		self.lifeTime = lifeTime
		self.color = color
		self.counter = 0

		# 星を描画する矩形領域
		HR = 1
		self.rect = pygame.Rect(x0 - HR, y0 - HR, HR * 2, HR * 2)

	def move(self):
		"""寿命になるまで星を動かす
		
		"""
		si = 1		# 加速度(速度の増分)
		if self.counter < self.lifeTime:
			self.rect.x += self.vx
			self.rect.y += self.vy
			self.counter += 1
			self.vy += si

		# 寿命になったら星を消す
		else:
			self.delete()

	def delete(self):
		"""星を消す
		
		"""
		star.list.remove(self)
ここでは、打ち上げ花火の本体を(ball)、玉が破裂して飛び出す小玉を(star)と呼ぶ。は外側の親星 (おやぼし) と内側の芯星 (しんぼし) の二重構造になっているものとする。雪を降らせるプログラムと同じで、打ち上げる1つ1つのをオブジェクト化し、さらに玉から飛び出すの1つ1つをオブジェクトに見立ててプログラムを作っていく。
実際の打ち上げ花火では星が3次元の広がりを見せるが、ここでは計算を簡単にするために2次元で計算していく。

まず星のクラス star だが、コンストラクタとして、星が飛び出す原点となる座標(玉が破裂した位置)と、星の初速、星が消えるまでの寿命と星の色を渡す。そして、星を描画する矩形領域を rect に保管しておく。
メソッド move では、rect の座標を動かすことで、星が広がっていく様子をアニメーションする。寿命になったらメソッド delete を呼び出して星を消す。星はオブジェクトとしてリスト list に格納されているので、このリストから消したい星を remove するだけで描画しなくなる。
firework.py
class ball(star):
	"""玉クラス
	
	Attributes:
		StarColors(list): 星の色リスト
	
	Note:
		花火の玉をオブジェクト化する.
	"""
	# 星の色
	StarColors = ["#FF0000", "#E0FFFF", "#FF33FF", "#FF99FF", "#FFFFCC", "#FFCC00", "#FF00FF", "#8A2BE2", "#FFFFFF", "#FF99FF"]

	def setNumber(self, num):
		"""発射する星の数を設定する
		
		Args:
			num(int):	発射する星の数
		
	"""
		self.num = num

	def randomAngle(self):
		"""玉から見た星の角度をランダムに返す
		
		Note:
			花火の広がり方を変化させる。
		
	"""
		return math.pi * random.randint(-10, 10) / (float(self.num) * 10.0)

	def delete(self):
		"""玉を消す
		
	"""
		# 星を発射するためのパラメタをランダムに算出する
		r0 = random.randint(13, 17)
		v0 = random.randint(18, 22)
		nc0 = random.randint(0, len(self.StarColors) - 1)
		nc1 = random.randint(1, len(self.StarColors) - 2)
		x, y = self.rect.center
		# 親星(外側)を発射する
		self.fireStar(self.num, x, y, r0, v0, self.StarColors[nc0], self.vx, self.vy)
		# 芯星(内側)を発射する
		self.fireStar(self.num // 2, x, y, r0 / 3, v0 / 3, self.StarColors[(nc0 + nc1) % len(self.StarColors)], self.vx, self.vy)
		# 玉を消す
		super().delete()

	def fireStar(self, num, x0, y0, r, v, color, vx0, vy0):
		"""星を発射する
		
		Args:
			num(int):	発射する星の数
			x0(int):	玉が破裂したときのX座標
			y0(int):	玉が破裂したときのY座標
			r(int):		玉が破裂した点からの距離
			v(int):		星の速度
			color(int):	星の色
			vx0(int):	玉が破裂した時のX方向の速度
			vy0(int):	玉が破裂した時のY方向の速度
			color(str):	花色
		
	"""
		# Z軸に割り当てる角度
		dEta = 2.0 * math.pi / float(self.num)
		# Z軸との角度
		eta = self.randomAngle()
		for i in range(int(self.num / 2)):
			dxy = math.sin(eta)
			n = math.floor(dxy * num)
			# Y軸と星との割り当て角度
			dTheta = 2 * math.pi / n if n else 2 * math.pi
			# X軸と星との角度
			theta = self.randomAngle()

			if i % 2:
				theta += dTheta * 0.5

			for j in range(n):
				# XY位置成分
				dx = math.cos(theta) * dxy
				dy = math.sin(theta) * dxy
				# XY速度成分
				vx = v * dx + vx0
				vy = v * dy + vy0
				# 寿命
				lifeTime = random.randint(8, 15)
				# 星リストに加える
				star.list.append(star(x0 + r * dx, y0 + r * dy, vx, vy, lifeTime, color))
				# 次の星の角度
				theta += dTheta
			# 次のZ軸角度
			eta += dEta
次に玉を表すクラス ball を見ていこう。ball は星のクラス star を継承する。つまり、玉は星と同じように運動し、寿命がクルト消滅するオブジェクトと見なす。
描画できる玉や星の色は、炎色反応としてありそうな色をリスト StarColors に代入しておく。ここからランダムに選んで星の色を決める。
メソッド setNumber は星の数を設定する。
メソッド randomAngle は、星が同心円状に展開することのないように“揺らぎ”を与える角度を計算する。

メソッド deletestar.delete を継承しているので玉を消す作用があるが、加えて、星の速度や色をランダムに決めて、親星(外側)と芯星(内側)を発射する。

メソッド fireStar は星の発射を担当する。玉が破裂したときの、1つ1つの星の位置や速度成分にランダムな揺らぎを加える。
firework.py
def shootball(width, height):
	"""花火(玉)を一発打ち上げる
	
	Args:
		width(int):		画面の幅
		height(int):	画面の高さ
	"""
	# 玉の打ち上げ座標
	x0 = random.randint(int(width / 4), int(width / 4) * 3)
	y0 = height - 5
	# 玉の速度
	vx = random.randint(-1, 1)
	vy = random.randint(-33, -31)
	# 玉の色
	color = "#CC6600"
	# 玉の寿命
	lifeTime = -vy - random.randint(3, 5)

	bl = ball(x0, y0, vx, vy, lifeTime, color)
	bl.setNumber(20)
	star.list.append(bl)
関数 shootball は花火(玉)を一発打ち上げる。玉の移動が背景画像の中に収まるように、あからじめ取り出した背景画像の幅と高さを引数として渡す。ここから打ち上げ座標、速度などを計算し、先ほど定義した ball に渡す。
firework.py
# メイン・プログラム =======================================================
# 画像の幅、高さを取得する
image = Image.open(bgfilename)
width, height = image.size

# Pygameを初期化
pygame.init()

# アニメーションウィンドウのサイズを指定
screen = pygame.display.set_mode((width, height))
pygame.display.set_caption("Fireworks")

# 背景画像を読み込み
backgroundImage = pygame.image.load(bgfilename)

# メイン・ループ
running = True
while running:
	for event in pygame.event.get():
		if event.type == pygame.QUIT:
			running = False

	# 背景を描画
	screen.blit(backgroundImage, (0, 0))

	# 花火を動かす
	for st in star.list:
		st.move()
		pygame.draw.ellipse(screen, pygame.Color(st.color), st.rect)

	# 一定確率で親玉を打ち上げる
	if random.randint(0, 100) > 90:
		shootball(width, height)

	# 画面を更新する
	pygame.display.flip()

	# フレームレートを指定する
	pygame.time.Clock().tick(fps)

# Pygameを終了する
pygame.quit()
メイン・プログラムは、雪を降らせるプログラムとほぼ同じなので説明は割愛する。
雪や花火のように物体(キャラクター)を動かすアニメーションの場合、運動方程式を真似て、1つ1つのオブジェクトをリスト(や配列)の中で動かしてやればいい。風(流体力学)や重力(運動力学)を忠実にプログラムしてやれば、よりリアルなアニメーションになるだろう。

ちなみに、3Dアニメーションソフト「Blender」においてスクリプトとして Python が利用できるようになっているのは、こうした物理演算を書きやすいプログラミング言語だからだろう。もし Blendar を利用している方がいたら、オブジェクトの運動のさせ方を少し勉強してみてほしい。

練習問題

次回予告

Python はデータ処理が得意で、ビッグデータの解析処理や機械学習に利用されることが多い。処理するデータとして、「7.2 ファイル入力」ではCSVファイルを紹介したが、次回からは、より複雑で大規模なデータを扱えるよう、PythonSQL を使わずにデータベース処理を行うための基礎を学んでいく。データベースとは何かという概念は追い追い説明していくとして、まずは、「7.2 ファイル入力」で使ったCSVファイルをデータベースに登録することからはじめる。

コラム:スプライト

スーパーマリオ
1983年(昭和58年)7月に任天堂が発売した家庭用ゲーム機「ファミリーコンピュータ」(ファミコン)は、1画面中に64個のスプライトを表示することができる。
スプライトというのは、たとえば左図のマリオのように背景に対して動きのあるオブジェクトのことである。
スプライトはファミコンの専用機能ではなく、多くの家庭用ゲーム機やパソコン、アーケードゲーム機が同じような仕組みを持っており、背景とスプライトを別々に動かすことができる。
本編のプログラムで言えば、花火の星を動かすのに使った pygame.Rectスプライトに相当すると考えてほしい。また、Pygameは pygame.sprite.Sprite というスプライトクラスを備えており、これを使って様々なオブジェクトを動かすことができる。

ファミコンのスプライトの大きさは1個あたり8×8ドットで、マリオは16×16ドット、つまり4個のスプライトで構成されている。マリオはキノコを食べると大きくなるのだが、このとき、縦方向は64ドットになるが、横は64トットになっていない。というのは、ファミコンのハードウェアの制約でスプライトを水平方向に同時8個までしか並べることができなかったからだ。横64ドットだとスプライトを8個分消費してしまい、同じ高さに他のキャラを配置できなくなってしまう。

もちろん pygame.Rect にはそうした制約はないから、好きなだけスプライトを動かすことができる。マルチプラットフォームなので、LinuxやmacOSでも同じ画面を表示することができる。
ゲームプログラムを作ってみたいという方は、ぜひ活用してほしい。

参考サイト

(この項おわり)
header