検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
当サイトのドメイン名は " getwebtips.net " です。
トップレベルドメインは .net であり、他の .com / .shop といったトップレベルドメインのサイトとは一切関係ありません。
practical_python_design_patterns

Practical Python Design Patterns - Python で学ぶデザインパターン: The Factory Pattern - Part. 2 「ゲームループ」の巻 投稿一覧へ戻る

Published 2022年5月25日10:23 by T.Tsuyoshi

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

The Game Loop
(ゲームループ)

ゲームプログラミングにおけるもっとも基本的なコンセプトは game loop (ゲームループ) と呼ばれているものです: プレイヤーから何かしらの入力があった場合、それを基に何らかの計算を行ってゲームの状態を更新し、変更が加えられた結果をプレイヤーに返す、という動作を繰り返すというものです。我々のゲームでは画面を更新するだけですが、サウンドや皮膚感覚 (haptic) などを含めたフィードバックを行うことも可能です。そしてこうしたループをプレイヤーがゲームを終了するまで続けるわけです。我々の場合では、プレイヤーへのフィードバックの度に pygame.display.flip() 関数を呼び出して画像を更新します。
ゲームの基本的な構造は次のようなものです:
初期化を行います。ウィンドウを構築し、スクリーン上に描画する要素の位置、色などをセットアップします。
ユーザーがゲームを終了するまでゲームループを実行し続けます。
ユーザーがゲームを終了したらウィンドウを閉じます。
コードで記述すると以下のようになるでしょう:
graphic_base.py
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)」ボタンを押すのを待ってループを終了させることだけです。もう少し興味深いものにするために画面上に小さな矩形を描画し、ユーザーが矢印キーを押した際に動かしてみましょう。
そのためには、画面上の初期位置に矩形を描画し、「矢印キーが押された」というイベントに反応する必要があります:
graphic_base.py
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) も動き回るようにしたいとしたら?それに加えて、三角形も、ゲームのキャラクター画像も動き回るようにしたいとしたら...。想像するのは難しくないと思います。ゲームループを実現するコードの中は、それぞれの画像の動きを制御し表示するコードでギュウギュウ詰めになってしまうでしょう。この状況をオブジェクト指向アプローチで整頓・改善したいとしたらどうしたらよいのでしょうか?
shape_game.py
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' キーが押されたら矩形を描画するにはどうしたらよいか考えてみてください。全てをゲームループの中で処理しようとせず、それぞれをオブジェクトとして保持しておくことで如何に簡単にそれを実現できるかが良く理解できると思います。
TIP
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() # ⑥
図形の現在の描画位置を簡単に取得するために新たな属性を property として追加しました。
SHIFT キーが押されているか確認します。
SHIFT キーが押されていて、かつ、'c' または 's' が押されており、なおかつ、現在描画している図形オブジェクトが書き換え対象のものではない、を判断しています。
円オブジェクトの描画位置は円の中心座標、矩形オブジェクトの描画位置は矩形の左上座標、という違いがあるため、その差を補正し、図形が入れ替わった瞬間に位置がずれないようにします。
描画するためのオブジェクトとしてセットします。
表示を変更する場合は必ず 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) が必要です。しかし、ファクトリーパターンでは初期化処理は不要ですが、サブクラス化が必要となります。
これらにはそれぞれに長所があり、状況によって使い分ける必要があります。この章を読み進めば、ある状況においてどちらのパターンを採用すべきなのか、しっかりとした判断を下せるようになるはずです。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)