人工知能に関する断創録

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

ピクセルベーススクロール

今回は、ピクセル単位のスクロールを実装してよりドラクエっぽいスクロールを実現します。前回までのようなかくかくした移動ではなくなめらかな移動になります。イメージがわかない方はスクリプトを実行してみてください。ここまでできれば移動に関してはほぼ完了です。

pyrpg10.zip
f:id:aidiary:20100606112617p:plain

ピクセルベーススクロール

f:id:aidiary:20100606112618p:plain

まず、コードを見る前にタイルベーススクロールとピクセルベーススクロールの違いを説明します。上の図の四角形がマス(タイル)を表していてプレイヤーキャラがどのように移動するかを表しています。前回までのタイルベーススクロールの場合、一番上の状態から一番下の状態へいきなり移っていました。プレイヤーはマスの上にしか配置されずマスの途中にいることはありません。そのためかくかくした移動になってしまいます。一方、ピクセルベーススクロールの場合、いきなり移るのではなくマスの間を少しずつ移動する(ピクセル移動)のが特徴です。こうするとスムーズに移動したように見えます。これを実装する場合、プレイヤーのマス単位の座標とピクセル単位の座標をしっかり区別するのが重要です。

ゲームループの変更

まずゲームループの修正からみてみます。下は前回までのゲームループです。イベントハンドラで矢印キーが押されたらPlayerクラスの move()メソッドを呼び出して移動していました。move()にmapを渡していたのは移動の際に障害物(海、山、壁など)にあたるかチェックするためです。

    while True:
        clock.tick(60)
        player.update()
        offset = calc_offset(player)
        map.draw(screen, offset)
        player.draw(screen, offset)
        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)

下が今回修正したメインループです。イベントハンドラに書いていた移動関連の処理がごっそりなくなりました。どこにいったのかというとPlayer クラスのupdate()にまとめてあります。プレイヤーの移動に関する処理なのだからPlayerクラスに書いた方がオブジェクト指向っぽいよい書き方になります。update()にmapを渡すのは前回と同じく障害物にあたったか判定するためです。

    while True:
        clock.tick(60)
        player.update(map)
        offset = calc_offset(player)
        map.draw(screen, offset)
        player.draw(screen, offset)
        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()

ピクセル単位移動

    # グローバル変数
    speed = 4  # 1フレームの移動ピクセル数
    animcycle = 24  # アニメーション速度

    # インスタンス変数
    self.vx, self.vy = 0, 0  # 移動速度
    self.moving = 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
        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
        # キャラクターアニメーション
        # frameに応じて描画イメージを切り替える
        self.frame += 1
        self.image = self.images[self.direction*4+self.frame/self.animcycle%4]

次にPlayerクラスのupdate()を見てみます。移動処理をここに移したため前回よりは複雑になっています。ピクセルベース移動の基本的な考え方は

  1. 移動フラグ(self.moving)がオフ(False)のときに矢印キーを押したら移動フラグをオン(True)にする
  2. 移動方向に合わせて速度(self.vx, self.vy)をセットする
  3. 移動フラグがオンの状態では、4ピクセル(self.speed)ごと移動する
  4. プレイヤーの座標がマスにきっちりおさまったら移動フラグをオフ(False)にする

です。まず、矢印キーを押したところから見てみます。

        # キー入力があったら移動を開始する(速度をセットする)
        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

キー入力の検知はpygame.key.get_pressed()で行ってます。これは、キーイベント(2008/5/10)のpygame.keyモジュールを使う場合で紹介したのでそちらを参照してください。マップが移動できる場合に速度(self.vx, self.vy)をセットしています。ここでの速度は1フレームに移動するピクセル数で今回は4ピクセル(self.speed)としています。下移動の場合はY方向下向きの移動なので(0, self.speed)となります。最後に、移動フラグ(self.moving)をオンにします。self.movingをTrueにすると次の update()からピクセル移動が行われます。

        # プレイヤーの移動処理
        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

移動フラグ(self.moving)がオンのときは上の部分が実行されます。ここでは、先ほどセットした速度 (self.vx, self.vy) だけプレイヤーのピクセル単位の座標(self.rect)を動かしています。(self.x, self.y)はマス単位の座標なのに対し、self.rectはピクセル単位の座標であることに注意してください。

そして、きっちりマスにおさまった場合に移動フラグをオフにしています。マスにきっちりおさまったかどうかはプレイヤーのピクセル座標をマスのサイズ(GS)で割ったときにあまりが0になるかで調べられます。最後にプレイヤーのマス単位の座標 (self.x, self.y) を更新します。

実行してみるとわかりますが、スムーズな移動になって何か気持ちいいです。移動に関しては普通の2D RPGのレベルに到達したと思います。

ループマップ

龍彦さんにループマップのプログラムをもらいました。端っこに行くと逆側から出てきます。

def is_movable(self, x, y):
      # マップ範囲内にx,yを納める(上下左右マップをつなげる、ループさせる)
      x = x % self.col
      y = y % self.row
def draw(self, screen, offset):
      # 上下左右マップがつながったループ状態で描画
      screen.blit(self.images[self.map[y%self.row][x%self.col]],
                  x*GS-offsetx,y*GS-offsety))

滑らかスクロール

ekさんにさらに滑らかなスクロール方法を教えてもらいました。修正箇所はPlayerのupdate()です。マスにきっちり収まって止まったフレームでもキーが押されていたらそのまま移動を開始するようにしています。私のやり方だと移動を開始しないので少しかくかくした感じがしてました。

    def update(self, map):
        """プレイヤー状態を更新する。
        mapは移動可能かの判定に必要。"""
        # キャラクターアニメーション
        self.frame += 1
        self.image = self.images[self.name][self.direction*4+self.frame/self.animcycle%4]
        # プレイヤーの移動処理
        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: ここに接触イベントのチェックを入れる
                event = map.get_event(self.x, self.y)
                if isinstance(event, MoveEvent):  # MoveEventなら
                    dest_map = event.dest_map
                    dest_x = event.dest_x
                    dest_y = event.dest_y
                    map.create(dest_map)
                    # プレイヤーを移動先座標へ
                    self.set_pos(dest_x, dest_y, DOWN)
                    map.add_chara(self)  # マップに再登録
            else:
                return
        
            # プレイヤーの場合、キー入力があったら移動を開始する
        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