人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

キャラクターを追加

プレイヤー1人のさびしい世界でしたがついに王様

の登場です。ついでに王様の側近の大臣

と王様のまわりをぶらぶらしている護衛である兵士

も登場させましょう。今回はこの3人を追加します。プレイヤーを表すPlayerクラスを流用してCharacterクラスを作ることで簡単にキャラを増やせます。今回はオブジェクト指向の威力が存分に味わえます。

pyrpg11.zip
f:id:aidiary:20100731152130p:plain

キャラクターの作成

キャラクターを表すCharacterクラスの詳細を説明する前に具体的な使い方から説明します。キャラクターを作成しているのはmain()の下の部分です。

    # キャラクター作成
    king = Character("king", (2,1), DOWN, STOP)
    minister = Character("minister", (3,1), DOWN, MOVE)
    soldier = Character("soldier", (4,1), DOWN, MOVE)

Characterクラスのコンストラクタには4つの引数を渡します。1つめの引数はキャラクター名(画像ファイル名から拡張子を除いた名前)です。たとえば、王様だったら画像ファイル名king.pngの拡張子を除いたkingがキャラクター名になります。「すけさん」のような具体的な名前ではありません。2つめの引数はキャラクターの座標(マス単位)です。3つめの引数はキャラクターの向きです。DOWN, LEFT, RIGHT, UPのいずれかの値を取ります。4つめの引数は移動タイプです。STOPかMOVEという値を取ります。STOPの場合、キャラクターは動かず同じ場所に静止します。MOVEにするとランダムに動き回ります。王様は静止して大臣(minister)や兵士(soldier)は動き回るようにしてます。こんな感じでCharacterクラスというひな形を作っておくと簡単にキャラクターが増やせます。ここら辺はオブジェクト指向のすごいところです。

マップへの登録

次に作成したキャラクターをマップに追加します。ここら辺の仕様はだいぶ迷ったのですが、キャラクターの更新(update)や描画(draw)や衝突判定はマップ側で行うことにしました。たいていのキャラクターはマップ間移動ができず特定のマップに縛り付けられているからです。キャラクターをマップに登録しているのはmain()の

    # キャラクターをマップに追加
    map.add_chara(player)
    map.add_chara(king)
    map.add_chara(minister)
    map.add_chara(soldier)

です。ここでplayerもマップに登録している点に注意してください。プレイヤーはマップ間移動ができるのでどうしようか悩んだのですがとりあえずマップに登録することにしました。後で変更するかもしれません。次にMapクラスを見てみます。

class Map:
    images = [None] * 256  # マップチップ(番号->イメージ)
    def __init__(self, name):
        ...
        self.charas = []  # マップにいるキャラクターリスト
    def add_chara(self, chara):
        """キャラクターをマップに追加する"""
        self.charas.append(chara)
    def update(self):
        """マップの更新"""
        # マップにいるキャラクターの更新
        for chara in self.charas:
            chara.update(self)  # mapを渡す
    def draw(self, screen, offset):
        """マップを描画する"""
        offsetx, offsety = offset
        # マップの描画範囲を計算
        startx = offsetx / GS
        endx = startx + SCR_RECT.width/GS + 1
        starty = offsety / GS
        endy = starty + SCR_RECT.height/GS + 1
        # マップの描画
        for y in range(starty, endy):
            for x in range(startx, endx):
                # マップの範囲外はデフォルトイメージで描画
                # この条件がないとマップの端に行くとエラー発生
                if x < 0 or y < 0 or x > self.col-1 or y > self.row-1:
                    screen.blit(self.images[self.default],
                                (x*GS-offsetx,y*GS-offsety))
                else:
                    screen.blit(self.images[self.map[y][x]],
                                (x*GS-offsetx,y*GS-offsety))
        # このマップにいるキャラクターを描画
        for chara in self.charas:
            chara.draw(screen, offset)
    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 or self.map[y][x] == 4:
            # 水と山は移動できない
            return False
        # キャラクターと衝突しないか?
        for chara in self.charas:
            if chara.x == x and chara.y == y:
                return False
        return True

self.charasというリストにこのマップに登場するCharacterオブジェクトを登録します。先ほど使ったadd_chara()で self.charasにキャラクターを追加しています。次にupdate()とdraw()を見てください。self.charasに登録されたキャラクターの更新や描画を行っています。つまり、Mapのupdate()やdraw()を呼び出せば、マップ内のすべてのキャラクターのupdate()やdraw()は自動的に行われます。最後にis_movable()で海や岩山などのマップチップとの衝突を調べる処理に加えて、マップ内のキャラクターとも衝突しないか調べています。

この処理は少し甘く、2人のキャラクターが同じタイミングで同じマスに移動しようとすると衝突してしまいます。これを回避するには移動前に移動先のマスを予約しておくなど細かい処理が必要になります。

この変更にともないmain()側ではplayerのupdate()やdraw()は削除します。mapのupdate()やdraw()だけ呼べばよいわけです。

Characterクラスの詳細

最後にCharacterクラスの詳細を説明します。今まで使っていたPlayerクラスと機能はほぼ同じなのでPlayerクラスの大部分をCharacterクラスに移します。

class Character:
    """一般キャラクタークラス"""
    speed = 4  # 1フレームの移動ピクセル数
    animcycle = 24  # アニメーション速度
    frame = 0
    # キャラクターイメージ(mainで初期化)
    # キャラクター名 -> 分割画像リストの辞書
    images = {}
    def __init__(self, name, pos, dir, movetype):
        self.name = name  # プレイヤー名(ファイル名と同じ)
        self.image = self.images[name][0]  # 描画中のイメージ
        self.x, self.y = pos[0], pos[1]  # 座標(単位:マス)
        self.rect = self.image.get_rect(topleft=(self.x*GS, self.y*GS))
        self.vx, self.vy = 0, 0  # 移動速度
        self.moving = False  # 移動中か?
        self.direction = dir  # 向き
        self.movetype = movetype  # 移動タイプ
    def update(self, map):
        """キャラクター状態を更新する。
        mapは移動可能かの判定に必要。"""
        # プレイヤーの移動処理
        if self.moving == True:
            # ピクセル移動中ならマスにきっちり収まるまで移動を続ける
            self.rect.move_ip(self.vx, self.vy)
            # マスにおさまったら移動完了
            if self.rect.left % GS == 0 and self.rect.top % GS == 0:
                self.moving = False
                self.x = self.rect.left / GS
                self.y = self.rect.top / GS
        elif self.movetype == MOVE and random.random() < PROB_MOVE:
            # 移動中でないならPROB_MOVEの確率でランダム移動開始
            self.direction = random.randint(0, 3)  # 0-3のいずれか
            if self.direction == DOWN:
                if map.is_movable(self.x, self.y+1):
                    self.vx, self.vy = 0, self.speed
                    self.moving = True
            elif self.direction == LEFT:
                if map.is_movable(self.x-1, self.y):
                    self.vx, self.vy = -self.speed, 0
                    self.moving = True
            elif self.direction == RIGHT:
                if map.is_movable(self.x+1, self.y):
                    self.vx, self.vy = self.speed, 0
                    self.moving = True
            elif self.direction == UP:
                if map.is_movable(self.x, self.y-1):
                    self.vx, self.vy = 0, -self.speed
                    self.moving = True
        # キャラクターアニメーション
        self.frame += 1
        self.image = self.images[self.name][self.direction*4+self.frame/self.animcycle%4]
    def draw(self, screen, offset):
        """オフセットを考慮してプレイヤーを描画"""
        offsetx, offsety = offset
        px = self.rect.topleft[0]
        py = self.rect.topleft[1]
        screen.blit(self.image, (px-offsetx, py-offsety))

Playerクラスと違うのは、移動タイプを表すself.movetypeと移動を開始する条件です。Playerクラスの移動開始はプレイヤーのキー入力で移動していたのに対し、Characterクラスは移動タイプがMOVEでPROB_MOVE以下の確率で自動的に移動を開始しています。

PlayerとCharacterはほとんど同じ機能を持つので継承という仕組みが使えます。ここでは、Characterのサブクラスとして Playerを定義します。Pythonでサブクラスを定義するにはクラス名の後に()で親クラス名を書き、コンストラクタ__init__()で親クラスのコンストラクタを呼び出します。

class Player(Character):
    """プレイヤークラス"""
    def __init__(self, name, pos, dir):
        Character.__init__(self, name, pos, dir, False)
    def update(self, map):
        """プレイヤー状態を更新する。
        mapは移動可能かの判定に必要。"""
        # プレイヤーの移動処理
        if self.moving == True:
            # ピクセル移動中ならマスにきっちり収まるまで移動を続ける
            self.rect.move_ip(self.vx, self.vy)
            # マスにおさまったら移動完了
            if self.rect.left % GS == 0 and self.rect.top % GS == 0:
                self.moving = False
                self.x = self.rect.left / GS
                self.y = self.rect.top / GS
                # TODO: ここに接触イベントのチェックを入れる
        else:
            # プレイヤーの場合、キー入力があったら移動を開始する
            pressed_keys = pygame.key.get_pressed()
            if pressed_keys[K_DOWN]:
                self.direction = DOWN  # 移動できるかに関係なく向きは変える
                if map.is_movable(self.x, self.y+1):
                    self.vx, self.vy = 0, self.speed
                    self.moving = True
            elif pressed_keys[K_LEFT]:
                self.direction = LEFT
                if map.is_movable(self.x-1, self.y):
                    self.vx, self.vy = -self.speed, 0
                    self.moving = True
            elif pressed_keys[K_RIGHT]:
                self.direction = RIGHT
                if map.is_movable(self.x+1, self.y):
                    self.vx, self.vy = self.speed, 0
                    self.moving = True
            elif pressed_keys[K_UP]:
                self.direction = UP
                if map.is_movable(self.x, self.y-1):
                    self.vx, self.vy = 0, -self.speed
                    self.moving = True
        # キャラクターアニメーション
        self.frame += 1
        self.image = self.images[self.name][self.direction*4+self.frame/self.animcycle%4]

PlayerはCharacterのサブクラスなのでCharacterで定義されたインスタンス変数やメソッドは自由に使えます。ただし、 update()だけは新たに定義してCharacterのupdate()をオーバーライドしています。Playerの場合、移動開始を自動移動ではなく、ユーザのキーボード入力にしたいからです。

もっとたくさんキャラクターを登録することもできます。ためしに追加してみてください。次回はキャラクターの追加をソースコードに直書きするのではなく、イベントファイルを用いて追加する方法を解説します。