ゲームプログラミングにおけるもっとも基本的なコンセプトは game loop (ゲームループ) と呼ばれているものです: プレイヤーから何かしらの入力があった場合、それを基に何らかの計算を行ってゲームの状態を更新し、変更が加えられた結果をプレイヤーに返す、という動作を繰り返すというものです。我々のゲームでは画面を更新するだけですが、サウンドや皮膚感覚 (haptic) などを含めたフィードバックを行うことも可能です。そしてこうしたループをプレイヤーがゲームを終了するまで続けるわけです。我々の場合では、プレイヤーへのフィードバックの度に pygame.display.flip() 関数を呼び出して画像を更新します。
初期化を行います。ウィンドウを構築し、スクリーン上に描画する要素の位置、色などをセットアップします。
ユーザーがゲームを終了するまでゲームループを実行し続けます。
ユーザーがゲームを終了したらウィンドウを閉じます。
import pygame
window_dimensions = 800, 600
screen = pygame.display.set_mode(window_dimensions)
player_quits = False
while not player_quits:
for event in pygame.event.get():
if event.type == pygame.QUIT:
player_quits = True
pygame.display.flip()
このコードは実際のゲームに関わることは何1つしていません。ただ、プレイヤーがウィンドウの「閉じる (Close)」ボタンを押すのを待ってループを終了させることだけです。もう少し興味深いものにするために画面上に小さな矩形を描画し、ユーザーが矢印キーを押した際に動かしてみましょう。
そのためには、画面上の初期位置に矩形を描画し、「矢印キーが押された」というイベントに反応する必要があります:
import pygame
window_dimensions = 800, 600
screen = pygame.display.set_mode(window_dimensions)
x = 100
y = 100
player_quits = False
while not player_quits:
for event in pygame.event.get():
if event.type == pygame.QUIT:
player_quits = True
pressed = pygame.key.get_pressed()
if pressed[pygame.K_UP]: y -= 4
if pressed[pygame.K_DOWN]: y += 4
if pressed[pygame.K_LEFT]: x -= 4
if pressed[pygame.K_RIGHT]: x += 4
screen.fill((0, 0, 0))
pygame.draw.rect(screen, (255, 255, 0), pygame.Rect(x, y, 20, 20))
pygame.display.flip()
自分でコードに少し手を入れて、矩形がウィンドウの外に消えて行ってしまわないようにしてみてください。
さて、矩形がウィンドウ内を自由に動き回ることができるようになりました。もしここで、円 (circle) も動き回るようにしたいとしたら?それに加えて、三角形も、ゲームのキャラクター画像も動き回るようにしたいとしたら...。想像するのは難しくないと思います。ゲームループを実現するコードの中は、それぞれの画像の動きを制御し表示するコードでギュウギュウ詰めになってしまうでしょう。この状況をオブジェクト指向アプローチで整頓・改善したいとしたらどうしたらよいのでしょうか?
import pygame
class Shape:
def __init__(self, x, y):
self.x = x
self.y = y
self.step = 4
def draw(self):
raise NotImplementedError()
def move(self, direction):
if direction == 'up':
self.y -= self.step
elif direction == 'down':
self.y += self.step
elif direction == 'left':
self.x -= self.step
elif direction == 'right':
self.x += self.step
class Square(Shape):
def draw(self):
pygame.draw.rect(
screen,
(255, 255, 0),
pygame.Rect(self.x, self.y, 20, 20)
)
class Circle(Shape):
def draw(self):
pygame.draw.circle(
screen,
(0, 255, 255),
(self.x, self.y),
10
)
if __name__ == '__main__':
window_dimensions = 800, 600
screen = pygame.display.set_mode(window_dimensions)
square = Square(100, 100)
obj = square
player_quits = False
while not player_quits:
for event in pygame.event.get():
if event.type == pygame.QUIT:
player_quits = True
pressed = pygame.key.get_pressed()
if pressed[pygame.K_UP]: obj.move('up')
if pressed[pygame.K_DOWN]: obj.move('down')
if pressed[pygame.K_LEFT]: obj.move('left')
if pressed[pygame.K_RIGHT]: obj.move('right')
screen.fill((0, 0, 0))
obj.draw()
pygame.display.flip()
この実装では円と矩形を描画できるようになっています。そこで、'C' キーが押された場合は円を、'S' キーが押されたら矩形を描画するにはどうしたらよいか考えてみてください。全てをゲームループの中で処理しようとせず、それぞれをオブジェクトとして保持しておくことで如何に簡単にそれを実現できるかが良く理解できると思います。
PyGame のドキュメントでキーボードのそれぞれのキーに割り当てられている定数を調べ、上のコード例を拡張してみてください (一例を次に示します):
import pygame
class Shape:
def __init__(self, x, y):
self.x = x
self.y = y
self.step = 4
def draw(self):
raise NotImplementedError()
def move(self, direction):
if direction == 'up':
self.y -= self.step
elif direction == 'down':
self.y += self.step
elif direction == 'left':
self.x -= self.step
elif direction == 'right':
self.x += self.step
@property
def pos(self):
return self.x, self.y
class Square(Shape):
def draw(self):
pygame.draw.rect(
screen,
(255, 255, 0),
pygame.Rect(self.x, self.y, 20, 20)
)
class Circle(Shape):
def draw(self):
pygame.draw.circle(
screen,
(0, 255, 255),
(self.x, self.y),
10
)
if __name__ == '__main__':
window_dimensions = 800, 600
screen = pygame.display.set_mode(window_dimensions)
circle = Circle(100, 100)
square = Square(100, 100)
obj = circle
player_quits = False
while not player_quits:
for event in pygame.event.get():
if event.type == pygame.QUIT:
player_quits = True
pressed = pygame.key.get_pressed()
if pressed[pygame.K_UP]: obj.move('up')
if pressed[pygame.K_DOWN]: obj.move('down')
if pressed[pygame.K_LEFT]: obj.move('left')
if pressed[pygame.K_RIGHT]: obj.move('right')
if pygame.key.get_mods() & pygame.KMOD_SHIFT:
if pressed[pygame.K_c] and (not isinstance(obj, Circle)):
circle.x, circle.y = obj.pos
circle.x += 10
circle.y += 10
obj = circle
elif pressed[pygame.K_s] and (not isinstance(obj, Square)):
square.x, square.y = obj.pos
square.x -= 10
square.y -= 10
obj = square
screen.fill((0, 0, 0))
obj.draw()
pygame.display.flip()
実行してみましょう。矩形オブジェクトが表示されているときに SHIFT + 'c' キーを押すと円オブジェクトに変化します。また、SHIFT + 's' キーで矩形オブジェクトが描画されます。
まだまだ改善できる余地はありますね。例えば、各図形クラスで発生する「移動」や「描画」といった操作は抽象化できるはずです。そうすれば、現在「どの図形」を処理しているのか、という情報を保持しておく必要がなくなります。つまり、「ある特定の図形」を念頭に置くのではなく「一般的な図形」として扱い、それに対して「自分自身を描画しろ」と指示するだけにしたいのです (or if it is a shape, to begin with, and not some image or even a frame of animation)。
明らかに、ポリモーフィズム (polymorphism) はこの問題に対する完璧な解決策ではありません。それは、新たなオブジェクトを作成する度にコードも更新し続ける必要があるからです。そして、大規模なゲームではこの状況が多くの場所で発生します。問題は「新しいタイプの作成」であって「すでに用意してあるタイプの利用」ではありません。
我々が取り掛かっている「図形変更」ゲームを拡張していくためのより良い方法を見つけ出すために、以下に挙げる「良いコード」であるための条件について考えてみてください。
メンテナンスが容易である
アップデートが容易である
拡張が容易である
何をしたいのかが明確である
「良いコード」であれば、数週間前に記述したものであってもほとんど「苦痛」を伴わずに再び作業に取り掛かれるはずです。最終目標は、「変更したいが怖くて手が出せない」コードをこうした「いつでも更新作業に取り掛かれる」コードに書き換えることです。
オブジェクトを作成するためのコードがシステム内のあちらこちらに散乱しているのではなく、共通のインターフェースのもとに纏まっているようにしたいのです。こうすれば、図形タイプを更新・拡張したい場合であっても、変更を加える必要のあるコードは「一カ所」だけです。そして、「新たな図形タイプの追加」という作業が最も頻繁に起こり得る可能性が高いわけですから、「コードの改善」を考えたときに最初に取り掛かるべき領域の1つであることに疑いの余地はありません。
このような「オブジェクト作成機能の集中化」を実現するための方法の1つはファクトリーパターン (factory pattern) の採用です。このパターンの実装には「純粋な」方法と「抽象的な」方法の2通りありますが、この章では「純粋な」方法を取り上げます。また、我々のゲームの「骨組み」にどのようにこのパターンを活かせばいいのか、についても考えます。
ここで、ファクトリーパターンについて考える前に、プロトタイプパターン (prototype pattern) とファクトリーパターンの明らかな違い、に触れておきたいと思います。プロトタイプパターンではサブクラス化は必要ありませんが、このパターンを機能させるための初期化操作 (initialize operation) が必要です。しかし、ファクトリーパターンでは初期化処理は不要ですが、サブクラス化が必要となります。
これらにはそれぞれに長所があり、状況によって使い分ける必要があります。この章を読み進めば、ある状況においてどちらのパターンを採用すべきなのか、しっかりとした判断を下せるようになるはずです。