オブジェクト化
今回はPythonのオブジェクト指向機能を活用して、プレイヤーを表すPlayerクラス、マップを表すMapクラスを作ります。これにともない、プレイヤーに関連した変数や関数をPlayerクラス、マップに関連した変数や関数をMapクラスに移動させました。このように変数や関数をクラスとしてまとめておくとプログラムの構造が把握しやすくなり、拡張性も飛躍的に高まります。スクリプトの実行結果は前回とまったく変わりません。
Pythonのオブジェクト指向については解説していませんのでPythonの入門書を参考にしてください。
サンプルスクリプト
#!/usr/bin/env python # -*- coding: utf-8 -*- import pygame from pygame.locals import * import sys import os SCR_RECT = Rect(0, 0, 640, 480) GS = 32 DOWN,LEFT,RIGHT,UP = 0,1,2,3 def main(): pygame.init() screen = pygame.display.set_mode(SCR_RECT.size) pygame.display.set_caption(u"PyRPG 07 オブジェクト化") # マップチップをロード Map.images[0] = load_image("grass.png") # 草地 Map.images[1] = load_image("water.png") # 水 # マップとプレイヤー作成 map = Map() player = Player("player", (1,1), DOWN) clock = pygame.time.Clock() while True: clock.tick(60) player.update() map.draw(screen) player.draw(screen) pygame.display.update() for event in pygame.event.get(): if event.type == QUIT: sys.exit() if event.type == KEYDOWN and event.key == K_ESCAPE: sys.exit() # プレイヤーの移動処理 if event.type == KEYDOWN and event.key == K_DOWN: player.move(DOWN, map) if event.type == KEYDOWN and event.key == K_LEFT: player.move(LEFT, map) if event.type == KEYDOWN and event.key == K_RIGHT: player.move(RIGHT, map) if event.type == KEYDOWN and event.key == K_UP: player.move(UP, map) def load_image(filename, colorkey=None): filename = os.path.join("data", filename) try: image = pygame.image.load(filename) except pygame.error, message: print "Cannot load image:", filename raise SystemExit, message image = image.convert() if colorkey is not None: if colorkey is -1: colorkey = image.get_at((0,0)) image.set_colorkey(colorkey, RLEACCEL) return image def split_image(image): """128x128のキャラクターイメージを32x32の16枚のイメージに分割 分割したイメージを格納したリストを返す""" imageList = [] for i in range(0, 128, GS): for j in range(0, 128, GS): surface = pygame.Surface((GS,GS)) surface.blit(image, (0,0), (j,i,GS,GS)) surface.set_colorkey(surface.get_at((0,0)), RLEACCEL) surface.convert() imageList.append(surface) return imageList class Map: row,col = 15,20 # マップの行数、列数 images = [None] * 256 # マップチップ(番号->イメージ) # 固定マップ map = [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]] def draw(self, screen): """マップを描画する""" for r in range(self.row): for c in range(self.col): screen.blit(self.images[self.map[r][c]], (c*GS,r*GS)) def is_movable(self, x, y): """(x,y)は移動可能か?""" # マップ範囲内か? if x < 0 or x > self.col-1 or y < 0 or y > self.row-1: return False # マップチップは移動可能か? if self.map[y][x] == 1: # 水は移動できない return False return True class Player: animcycle = 24 # アニメーション速度 frame = 0 def __init__(self, name, pos, dir): self.name = name # プレイヤー名(ファイル名と同じ) self.images = split_image(load_image("%s.png" % name)) self.image = self.images[0] # 描画中のイメージ self.x, self.y = pos[0], pos[1] # 座標(単位:マス) self.rect = self.image.get_rect(topleft=(self.x*GS, self.y*GS)) self.direction = dir def update(self): # キャラクターアニメーション(frameに応じて描画イメージを切り替える) self.frame += 1 self.image = self.images[self.direction*4+self.frame/self.animcycle%4] def move(self, dir, map): """プレイヤーを移動""" if dir == DOWN: self.direction = DOWN if map.is_movable(self.x, self.y+1): self.y += 1 self.rect.top += GS elif dir == LEFT: self.direction = LEFT if map.is_movable(self.x-1, self.y): self.x -= 1 self.rect.left -= GS elif dir == RIGHT: self.direction = RIGHT if map.is_movable(self.x+1, self.y): self.x += 1 self.rect.left += GS elif dir == UP: self.direction = UP if map.is_movable(self.x, self.y-1): self.y -= 1 self.rect.top -= GS def draw(self, screen): screen.blit(self.image, self.rect) if __name__ == "__main__": main()
クラスとインスタンス
まず、オブジェクト指向の基本となるクラスとインスタンス(オブジェクト)の違いを説明します。下の図を見てください。
クラスはひな形のことでひな形からぽんぽんオブジェクトが作られるってイメージがあればよいと思います。たとえば、Playerクラスにはプレイヤーの情報として名前(name)、座標(x, y)、方向(direction)を持つことを定義するだけで具体的にどんな値を取るかはクラスからオブジェクトを作るときに決定します。上の図ではPlayerクラスから4つのPlayerオブジェクトを作っています。4つのPlayerオブジェクトはname, x, y, directionはそれぞれ別の値を持っています。このようにオブジェクトごとに別の値を取り得る変数はインスタンス変数と呼びます。一方、すべてのオブジェクトで共有する変数も用意できます。上の図ではanimcycleとかframeがそうです。このようなすべてのオブジェクトで共有される変数はクラス変数と呼びます。ちなみにインスタンスとオブジェクトはほとんど同じ意味です。
Mapクラス
class Map: row,col = 15,20 # マップの行数、列数 images = [None] * 256 # マップチップ(番号->イメージ) # 固定マップ map = [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]] def draw(self, screen): """マップを描画する""" for r in range(self.row): for c in range(self.col): screen.blit(self.images[self.map[r][c]], (c*GS,r*GS)) def is_movable(self, x, y): """(x,y)は移動可能か?""" # マップ範囲内か? if x < 0 or x > self.col-1 or y < 0 or y > self.row-1: return False # マップチップは移動可能か? if self.map[y][x] == 1: # 水は移動できない return False return True
マップに関する変数として行数、列数、マップチップイメージ、マップ配列を移動しました。マップチップイメージはすべてのマップで共通して使うものなのでクラス変数にしています。クラス変数っていうのはクラスから作られたすべてのオブジェクトが共有する変数のことでMap.imagesのように<クラス名>.<クラス変数>のようにアクセスします。マップの行数、列数、マップ配列は本来は各マップごとに違うはずですが、ここでは固定マップなのでクラス変数にしてしまっています。
次にマップに関する関数として描画するdraw()と移動可能かを調べるis_movable()をメソッドにしています。メソッドとはクラスに属する関数のことです。Pythonではメソッドの第1引数はselfと書くのが慣習です(selfにはメソッド呼び出しもとのオブジェクトが格納されます)。
このようにMapを独立したクラスにしておくと、フィールドマップ、お城のマップ、洞窟のマップというようにさまざまなマップをMapクラスのオブジェクト(インスタンス)として簡単に作り出せます。ただ今のままだとmapが固定なのでこういうことはできません。マップのロード(2008/6/1)でさまざまなマップを作る方法を紹介します。
# マップチップをロード Map.images[0] = load_image("grass.png") # 草地 Map.images[1] = load_image("water.png") # 水 # マップの作成 map = Map() # マップの描画 map.draw(screen)
マップオブジェクトを作成して使っているのはmain()関数です。クラス変数imagesへのアクセスはクラス名Mapを使い、draw()などのアクセスにはオブジェクトmapを通している点に注意してください。
Playerクラス
class Player: animcycle = 24 # アニメーション速度 frame = 0 def __init__(self, name, pos, dir): self.name = name # プレイヤー名(ファイル名と同じ) self.images = split_image(load_image("%s.png" % name)) self.image = self.images[0] # 描画中のイメージ self.x, self.y = pos[0], pos[1] # 座標(単位:マス) self.rect = self.image.get_rect(topleft=(self.x*GS, self.y*GS)) self.direction = dir def update(self): # キャラクターアニメーション(frameに応じて描画イメージを切り替える) self.frame += 1 self.image = self.images[self.direction*4+self.frame/self.animcycle%4] def move(self, dir, map): """プレイヤーを移動""" if dir == DOWN: self.direction = DOWN if map.is_movable(self.x, self.y+1): self.y += 1 self.rect.top += GS elif dir == LEFT: self.direction = LEFT if map.is_movable(self.x-1, self.y): self.x -= 1 self.rect.left -= GS elif dir == RIGHT: self.direction = RIGHT if map.is_movable(self.x+1, self.y): self.x += 1 self.rect.left += GS elif dir == UP: self.direction = UP if map.is_movable(self.x, self.y-1): self.y -= 1 self.rect.top -= GS def draw(self, screen): screen.blit(self.image, self.rect)
次にプレイヤーに関する変数と関数を抜き出してPlayerクラスを作ります。プレイヤーに関する変数としてプレイヤー名(画像ファイル名)、画像、位置、方向などをPlayerクラスに移しました。animcycleとframeはどのオブジェクトでも共有して使えるのでクラス変数にしています。一方、画像、位置、方向などは各オブジェクトが独立して持つ必要がある(プレイヤー4人のパーティを組んだとき画像、位置、方向などはみんな違いますよね?)のでインスタンス変数にしています。インスタンス変数とは各オブジェクトが独立して持つ変数のことです。インスタンス変数はself.<インスタンス変数>という形式でコンストラクタ(__init__()関数)で初期化するのが慣習です。実際は、コンストラクタで初期化する必要はないのですが、そうしておかないと何がインスタンス変数なのかわかりにくくなります。
self.rectはピクセル単位で表したプレイヤーの座標です。描画するdraw()関数で使います。またプレイヤーの移動用の関数move() を追加しています。move()にmapを渡す必要があるのはマップとの当たり判定(2008/5/26)でマップのis_movable()を使いたいからです。
Playerクラスからオブジェクトを作ったり、使ったりするのは下のように書きます。
player = Player("player", (1,1), DOWN) # プレイヤーを作成 player.update() # プレイヤーを更新 player.draw(screen) # プレイヤーを描画 player.move(DOWN, map) # プレイヤーを下に移動
その他の変更
Pygameの初期化、オブジェクトの作成、ゲームループなどは今までファイルのトップレベルにべた書きしていましたが、main()関数にまとめました。スクリプトを起動したときにmain()関数を呼び出すようにしています。
if __name__ == "__main__": main()