読者です 読者をやめる 読者になる 読者になる

人工知能に関する断創録

人工知能、認知科学、心理学、ロボティクス、生物学などに興味を持っています。このブログでは人工知能のさまざまな分野について調査したことをまとめています。最近は、機械学習・Deep Learningに関する記事が多いです。



オブジェクト化

Pygame

今回はPythonのオブジェクト指向機能を活用して、プレイヤーを表すPlayerクラス、マップを表すMapクラスを作ります。これにともない、プレイヤーに関連した変数や関数をPlayerクラス、マップに関連した変数や関数をMapクラスに移動させました。このように変数や関数をクラスとしてまとめておくとプログラムの構造が把握しやすくなり、拡張性も飛躍的に高まります。スクリプトの実行結果は前回とまったく変わりません。

Pythonのオブジェクト指向については解説していませんのでPythonの入門書を参考にしてください。

pyrpg07.zip
f:id:aidiary:20100606104825p:plain

サンプルスクリプト

#!/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()

クラスとインスタンス

まず、オブジェクト指向の基本となるクラスとインスタンス(オブジェクト)の違いを説明します。下の図を見てください。

f:id:aidiary:20100606104823p:plain

クラスはひな形のことでひな形からぽんぽんオブジェクトが作られるってイメージがあればよいと思います。たとえば、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()