人工知能に関する断創録

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

パーティー

今までずっとプレイヤーは1人だったので仲間を増やしてパーティを組みます。説明を読む前にスクリプトを動かしてパーティの各プレイヤーの動きがどうなっているか観察してみることをおすすめします。観察のポイントは、先頭ではないプレイヤーは1人前にいるプレイヤーがいる位置に移動する点です。どんなにくねくね移動しようがそうなってます。

pyrpg25.zip
f:id:aidiary:20100807223715p:plain

パーティを組む

パーティを表すPartyクラスを作りましたが、まず使い方から見てみます。main()です。

    party = Party()  # パーティ
    # プレイヤーを複数作成
    player1 = Player("swordman_female", (3,5), DOWN, True, party)
    player2 = Player("elf_female2", (3,4), DOWN, False, party)
    player3 = Player("priestess", (3,3), DOWN, False, party)
    player4 = Player("magician_female", (3,2), DOWN, False, party)
    # パーティへキャラクターを追加
    party.add(player1)
    party.add(player2)
    party.add(player3)
    party.add(player4)

partyオブジェクトを作ったあとに4人のプレイヤーを作成してadd()でパーティへ追加しているだけですね。Playerクラスのコンストラクタに新しく2つ引数を追加しています。最後の2つです。

class Player(Character):
    """プレイヤークラス"""
    def __init__(self, name, pos, dir, leader, party):
        Character.__init__(self, name, pos, dir, False, None)
        self.leader = leader
        self.party = party

leaderとpartyですね。先頭にいるキャラクターのみleader=Trueとなります(なりますというかそうしてください!)。また、partyはPartyオブジェクトへの参照です。この2つがなぜ必要かというとマップ間移動などプレイヤーが上に乗ったときに発動するイベントは先頭のプレイヤー(leader)にのみ適用したいからです。Playerのupdate()を見てみると

    if not self.leader: return  # リーダーでなければイベントは無視

という一文があります。リーダー以外は移動イベントに乗っても移動しません。partyがなぜ必要かというとリーダーが移動イベントに乗ったときリーダーだけでなくパーティー内の全員が移動先マップに移動しないとダメだからです。同じくPlayerのupdate()に

    for player in self.party.member:
        player.set_pos(dest_x, dest_y, DOWN)  # プレイヤーを移動先座標へ
        player.moving = False

という文があります。partyのメンバー全員を移動先座標にセットしています。ここら辺は微妙で移動イベントは先頭プレイヤーのみ発動したいけれど毒沼はパーティ全員バシバシしたいですね。どうしようか・・・

Partyクラスの詳細

次にPartyクラスの詳細を見ていきます。

class Party:
    def __init__(self):
        # Partyのメンバーリスト
        self.member = []
    def add(self, player):
        """Partyにplayerを追加"""
        self.member.append(player)
    def update(self, map):
        # Party全員を更新
        for player in self.member:
            player.update(map)
        # 移動中でないときにキー入力があったらParty全員を移動開始
        if not self.member[0].moving:
            pressed_keys = pygame.key.get_pressed()
            if pressed_keys[K_DOWN]:
                # 先頭キャラは移動できなくても向きは変える
                self.member[0].direction = DOWN
                # 先頭キャラが移動できれば
                if map.is_movable(self.member[0].x, self.member[0].y+1):
                    # 後ろにいる仲間から1つ前の仲間の位置へ移動開始
                    for i in range(len(self.member)-1,0,-1):
                        self.member[i].move_to(self.member[i-1].x,self.member[i-1].y)
                    # 先頭キャラを最後に移動開始
                    self.member[0].move_to(self.member[0].x,self.member[0].y+1)
            elif pressed_keys[K_LEFT]:
                self.member[0].direction = LEFT
                if map.is_movable(self.member[0].x-1, self.member[0].y):
                    for i in range(len(self.member)-1,0,-1):
                        self.member[i].move_to(self.member[i-1].x,self.member[i-1].y)
                    self.member[0].move_to(self.member[0].x-1,self.member[0].y)
            elif pressed_keys[K_RIGHT]:
                self.member[0].direction = RIGHT
                if map.is_movable(self.member[0].x+1, self.member[0].y):
                    for i in range(len(self.member)-1,0,-1):
                        self.member[i].move_to(self.member[i-1].x,self.member[i-1].y)
                    self.member[0].move_to(self.member[0].x+1,self.member[0].y)
            elif pressed_keys[K_UP]:
                self.member[0].direction = UP
                if map.is_movable(self.member[0].x, self.member[0].y-1):
                    for i in range(len(self.member)-1,0,-1):
                        self.member[i].move_to(self.member[i-1].x,self.member[i-1].y)
                    self.member[0].move_to(self.member[0].x,self.member[0].y-1)
    def draw(self, screen, offset):
        # Partyの全員を描画
        # 重なったとき先頭キャラが表示されるように後ろの人から描画
        for player in self.member[::-1]:
            player.draw(screen, offset)

まず、Partyにはパーティ内のメンバーを格納するリストmemberがあります。add()でPlayerオブジェクトを渡すとmemberに追加されます。これはさっき見ましたね。次にupdate()ですが、Partyのmember全員のupdate()を呼び出しています。次にキー入力があった場合、移動を開始しています。これは、前回までPlayerに合った処理ですがPartyに移しています。キー入力によってパーティ全員が移動するからです。下キーを押したときの移動処理を見てみます。

            if pressed_keys[K_DOWN]:
                # 先頭キャラは移動できなくても向きは変える
                self.member[0].direction = DOWN
                # 先頭キャラが移動できれば
                if map.is_movable(self.member[0].x, self.member[0].y+1):
                    # 後ろにいる仲間から1つ前の仲間の位置へ移動開始
                    for i in range(len(self.member)-1,0,-1):
                        self.member[i].move_to(self.member[i-1].x,self.member[i-1].y)
                    # 先頭キャラを最後に移動開始
                    self.member[0].move_to(self.member[0].x,self.member[0].y+1)

まず、先頭プレイヤー(leader)は移動できなくても方向は変えます。これは、1人のときと同じです。次に、先頭プレイヤーが移動できるなら、残りのメンバーも全部動かします。この残りのメンバーの動かし方が今回のポイントです。動かし方のポイントは、一番後ろにいるプレイヤーから順に1つ前にいるプレイヤーの座標へ移動することです。ただし、先頭にいるプレイヤーだけはキー入力した方向へ移動します。コード中のPlayer.move_to(x, y)は新しいメソッドですが、文字通り今いる座標から(x,y)へ移動するメソッドです。forループを見ると逆順になっています。つまり、4人プレイヤーのとき、iは3,2,1の順になります。i番目のメンバーはmove_toでi-1番目のメンバーがいる座標へ移動していることがわかります。つまり、

  • 3番目のメンバーは2番目のメンバーがいる座標へ
  • 2番目のメンバーは1番目のメンバーがいる座標へ
  • 1番目のメンバーは0番目のメンバー(leader)がいる座標へ移動します。最後に0番目のメンバーは、今までどおりキー入力した座標へ移動します。

これがパーティ移動のポイントです。

前にいるメンバーから移動しても実は変わりませんでした。これは、ピクセルベース移動しているためです。move_to()を実行しても移動を開始するだけで実際にプレイヤーの座標(x,y)はすぐには変わりません。しかし、タイルベース移動のようにmove_to()ですぐにプレイヤーの座標が変わる場合は後ろから動かさないとおかしくなります。

今回説明したパーティ移動のテクニックは別のゲームでも使えます。たとえば、Nibblesという蛇が移動する有名なゲームは同じテクニックを使います。あとで紹介予定です。

Playerの移動処理の修正

次にPlayerのupdate()を見てみます。

    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: ここに接触イベントのチェックを入れる
                if not self.leader: return  # リーダーでなければイベントは無視
                event = map.get_event(self.x, self.y)
                if isinstance(event, MoveEvent):  # MoveEventなら
                    sounds["step"].play()
                    dest_map = event.dest_map
                    dest_x = event.dest_x
                    dest_y = event.dest_y
                    map.create(dest_map)
                    for player in self.party.member:
                        # プレイヤーを移動先座標へ
                        player.set_pos(dest_x, dest_y, DOWN)
                        player.moving = False
        # キャラクターアニメーション
        self.frame += 1
        self.image = self.images[self.name][self.direction*4+self.frame/self.animcycle%4]
    def move_to(self, destx, desty):
        """現在位置から(destx,desty)への移動を開始"""
        dx = destx - self.x
        dy = desty - self.y
        # 向きを変える
        if dx == 1: self.direction = RIGHT
        elif dx == -1: self.direction = LEFT
        elif dy == -1: self.direction = UP
        elif dy == 1: self.direction = DOWN
        # 速度をセット
        self.vx, self.vy = dx*self.speed, dy*self.speed
        # 移動開始
        self.moving = True

今までキー入力をしたときに移動を開始する処理はPlayerのupdate()に書いていましたが、Partyに移したのできれいさっぱり消えています。今回追加したのはmove_to()です。これは、現在位置から引数で与えた (destx, desty) へ移動を開始する処理です。移動を開始するだけで実際の移動はupdate()でピクセル移動します。

プレイヤーの描画順序

最後にPartyの描画処理を見てみます。

    def draw(self, screen, offset):
        # Partyの全員を描画
        # 重なったとき先頭キャラが表示されるように後ろの人から描画
        for player in self.member[::-1]:
            player.draw(screen, offset)

ここでのポイントは、後ろにいるプレイヤーから描画することです。描画は重ね描きなのでプレイヤー同士が重なったとき一番前にいるプレイヤーが表示されるようにしたいからです。member[::-1]はPythonの書き方で逆順リストです。self.member[::-1]をself.memberに変えて前にいるプレイヤーから描画するとどうなるか確かめてみてください。