【技術情報配信】pythonでゲーム作ってみましょうか
技術情報配信です。
これまで幾度かゲーム作成を勉強にと提案してきました。
(自分の好きなようにコード書くというのは楽しいものです)
「ゲーム作成は思っているほど敷居高くないですよ」ってのを伝えるべく、
今回はpythonでテトリスの基本部分を作成してみようかと思います。
(本格的なゲームとなるとvery敷居高いです)
作成にはpygameを使用します。
また、読み手を選ばないように基本「べた書き」します。
→クラスは二つだけ作成します。
あと、個人的な話ですがテトリスはこれまでに幾度もコーディングしてきましたので、
これまでと異なる手法で組みたいと思います。
では早速始めていきましょう。
(テトリスの仕様に関してはwiki見てください)
■ライブラリのインポートと定数定義
今回使用するライブラリをインポートします。
———————————
import pygame
import sys
import random
———————————
■定数定義
べた書きといえど定数の定義くらいはしておきます。
———————————
SCREEN_WIDTH = 300
SCREEN_HEIGHT = 600
FIELD_WIDTH = 10
FIELD_HEIGHT = 20
BLOCK_SIZE = 30
DROP_TIME = 500
FAST_DROP_TIME = 50
MOVE_TIME = 100
———————————
■ブロッククラスを定義
テトリスの基本要素となるブロックをクラス定義します。
色の管理と描画を提供。
———————————
class Block:
def __init__(self, color=(0, 0, 0)):
self.color = color
def draw(self, screen, x, y, block_size):
pygame.draw.rect(screen, self.color, pygame.Rect(x * block_size, y * block_size, block_size, block_size))
———————————
■アプリクラスの定義と初期化
アプリのベースとなるクラスを定義します。
今回定義するクラスは以上となります。
→イケてるコード書くのであればまだまだクラスを定義することになりますが、
今回はそこじゃないのでご了承ください。
———————————
class TetrisGame:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption(“Tetris”)
self.field = [[None for _ in range(FIELD_WIDTH)] for _ in range(FIELD_HEIGHT)]
self.minos = [
[[1, 1, 1, 1]], # I
[[1, 1], [1, 1]], # O
[[1, 1, 0], [0, 1, 1]], # Z
[[0, 1, 1], [1, 1, 0]], # S
[[1, 1, 1], [0, 1, 0]], # T
[[1, 1, 1], [1, 0, 0]], # L
[[1, 1, 1], [0, 0, 1]] # J
]
self.colors = [
(0, 255, 255), # I
(255, 255, 0), # O
(255, 0, 0), # Z
(0, 255, 0), # S
(255, 0, 255), # T
(255, 165, 0), # L
(0, 0, 255) # J
]
self.current_mino_index = random.randint(0, len(self.minos) – 1)
self.current_mino = self.minos[self.current_mino_index]
self.current_color = self.colors[self.current_mino_index]
self.mino_pos = [FIELD_WIDTH // 2 – len(self.current_mino[0]) // 2, 0]
self.last_drop_time = pygame.time.get_ticks()
self.last_move_time = pygame.time.get_ticks()
self.fast_drop = False
self.move_left = False
self.move_right = False
———————————
以下を行ってます。
・pygameライブラリの初期化
・ウィンドウサイズを定義
・テトリスで使用する7種のミノ(ブロック)と色を定義
・ゲームフィールドを2次元リストとして初期化
1要素1ブロックとします。
・ゲームで使用する変数を定義
ここからはパーツ(関数)を定義していきます。
■フィールド描画
ブロック描画を使用してフィールドを描画します。
———————————
def draw_field(self):
for y, row in enumerate(self.field):
for x, block in enumerate(row):
if block is not None:
block.draw(self.screen, x, y, BLOCK_SIZE)
———————————
■ミノ描画
ブロック描画を使用してミノを描画します。
———————————
def draw_mino(self):
for y, row in enumerate(self.current_mino):
for x, cell in enumerate(row):
if cell:
block = Block(color=self.current_color)
block.draw(self.screen, x + self.mino_pos[0], y + self.mino_pos[1], BLOCK_SIZE)
———————————
■ミノ回転
ミノを90度回転させます。
これまでは回転後のミノを定義する手法で実装してきたのですが、
今回はイキって回転後の形状を算出してみました。
———————————
def rotate(self, mino):
return [list(row) for row in zip(*mino[::-1])]
———————————
簡単に説明しますと、
mino[::-1]でリストを逆順とし、
zip(*mino[::-1])で列と行を入れ替えてます。
■衝突判定
指定した位置のミノがフィールド上のブロックに衝突しているかを判定します。
ミノを構成するブロックがフィールド外に位置する場合も衝突と判定する。
———————————
def check_collision(self, mino, offset):
off_x, off_y = offset
for y, row in enumerate(mino):
for x, cell in enumerate(row):
if cell:
new_x, new_y = x + off_x, y + off_y
if new_x < 0 or new_x >= FIELD_WIDTH or new_y >= FIELD_HEIGHT or self.field[new_y][new_x] is not None:
return True
return False
———————————
■ミノをフィールドへ配置(統合)
指定ミノをフィールドの指定位置に配置する。
ミノ接地時にフィールドへ展開するための処理です。
———————————
def merge(self):
off_x, off_y = self.mino_pos
for y, row in enumerate(self.current_mino):
for x, cell in enumerate(row):
if cell:
self.field[y + off_y][x + off_x] = Block(color=self.current_color)
———————————
■ライン消去
フィールド内のブロックで埋まっている行を削除します。
———————————
def clear_lines(self):
new_field = [row for row in self.field if any(block is None for block in row)]
lines_cleared = FIELD_HEIGHT – len(new_field)
new_field = [[None for _ in range(FIELD_WIDTH)] for _ in range(lines_cleared)] + new_field
self.field = new_field
return lines_cleared
———————————
以下の処理を行うことでfieldから対象となる行を削除しています。
1. fieldから空セルのある行を抽出して新fieldを生成
2. 元の行数と比較して削除行数を算出
3. 新fieldの先頭に削除行数分の要素を追加
■ミノ接地時の処理
ミノが接地した際の処理です。
———————————
def place_mino(self):
self.merge()
self.clear_lines()
self.current_mino_index = random.randint(0, len(self.minos) – 1)
self.current_mino = self.minos[self.current_mino_index]
self.current_color = self.colors[self.current_mino_index]
self.mino_pos = [FIELD_WIDTH // 2 – len(self.current_mino[0]) // 2, 0]
———————————
以下の処理を行ってます。
・ミノをフィールドへ展開
・展開した結果消える行を削除
・次のミノを生成
■イベントハンドラ
pygameが管理するイベントキューにあるイベントを処理します。
———————————
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
new_mino = self.rotate(self.current_mino)
if not self.check_collision(new_mino, self.mino_pos):
self.current_mino = new_mino
elif event.key == pygame.K_LEFT:
self.move_left = True
elif event.key == pygame.K_RIGHT:
self.move_right = True
elif event.key == pygame.K_DOWN:
self.fast_drop = True
elif event.type == pygame.KEYUP:
if event.key == pygame.K_LEFT:
self.move_left = False
elif event.key == pygame.K_RIGHT:
self.move_right = False
elif event.key == pygame.K_DOWN:
self.fast_drop = False
———————————
以下のイベントを処理してます。
・キー操作
UP:ミノ回転
DOWN:ミノ高速降下
LEFT:ミノ左移動
RIGHT:ミノ右移動
・ウィンドウを閉じる操作
アプリ終了
■ミノの自動落下
一定時間毎にミノを一段下に落下させます。
高速落下時は落下間隔を短縮して実現してます。
———————————
def drop_mino(self):
current_time = pygame.time.get_ticks()
interval = FAST_DROP_TIME if self.fast_drop else DROP_TIME
if current_time – self.last_drop_time > interval:
new_pos = [self.mino_pos[0], self.mino_pos[1] + 1]
if not self.check_collision(self.current_mino, new_pos):
self.mino_pos = new_pos
else:
self.place_mino()
self.last_drop_time = current_time
———————————
■ミノの横移動
一定時間ごとにミノを左右に移動させます。
左右のキー入力がある場合に処理します。
———————————
def move_mino(self):
current_time = pygame.time.get_ticks()
if current_time – self.last_move_time > MOVE_TIME:
if self.move_left:
new_pos = [self.mino_pos[0] – 1, self.mino_pos[1]]
if not self.check_collision(self.current_mino, new_pos):
self.mino_pos = new_pos
if self.move_right:
new_pos = [self.mino_pos[0] + 1, self.mino_pos[1]]
if not self.check_collision(self.current_mino, new_pos):
self.mino_pos = new_pos
self.last_move_time = current_time
———————————
■ゲームループ
メインとなるループ処理です。
ここまでで定義してきた関数を呼び出してます。
———————————
def run(self):
while True:
self.handle_events()
self.drop_mino()
self.move_mino()
# 画面消去(黒で塗りつぶす)
self.screen.fill((0, 0, 0))
# フィールドを描画
self.draw_field()
# ブロックを描画
self.draw_mino()
# 画面更新
pygame.display.flip()
———————————
■pythonのおまじない的なコード
———————————
if __name__ == “__main__”:
game = TetrisGame()
game.run()
———————————
簡単に説明しますと、
スクリプトが直接実行された場合にのみ、このブロック内のコードが実行されという仕組みです。
本スクリプトがモジュールとしてインポートされた時に実行されないようにしています。
これでとりあえずテトリスの基本動作まで完成です。*1
ここまでのコードをそのまま1ファイル(*.py)に纏めれば実行できます。*2
(”———————————“でくくっている個所がコードです。)
いかがだったでしょうか。
敷居は高くないということを感じ取ってもらえてたら幸いです。
指摘、質問等ありましたらご連絡ください。
(何なりと遠慮せずにどーぞ。)
*1) あまり動作確認してません。すみません。
*2) 実行にはpythonとpygameのインストールが必要となります。
以上。
