Daydreaming in Brookline, MA

DL-3; マップをスクロールさせる

1 はじめに

Rogue+Wizライクなゲーム(仮称Daemon Lord)を作るプロジェクトの3回目です。今回はマップのスクロール画面表示を行います。

2 スクロール画面表示

スクロール画面の表示のために、いったん仮想画面を作ることにします。仮想画面は新旧2面持ち、前回描いた行と全く同じ内容の行は画面書き換えをしないようにすることで、画面全体の書き換え量を節約します。また、マップに重ねてメッセージウインドウ等を表示する場合に、実際の画面書き換えを何度も行わないようにして、画面書換量を減らすと同時に、画面のちらつきを防止します。

まずはスクロール画面制御用のクラスを定義します。

class Vscr:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.vscr0 = bytearray(b'M'*width*height)
        self.vscr1 = bytearray(b'N'*width*height)
        self.prev_vscr_view = memoryview(self.vscr0)
        self.cur_vscr_view = memoryview(self.vscr1)

width, heightは表示画面のサイズ=仮想画面のサイズです。vscr0/1が仮想画面の実体で、現在使用中の仮想画面ビュー(cur_vscr_view)と前回使った仮想画面ビュー(prev_vscr_view)をmemoryviewとして持ちます。スクロール画面表示が終わるたびに、これらのビューが指す仮想画面の実体を切り替えます。

2.1 メインメソッド

スクロール画面表示のメインメソッドはdisp_scrwin()です。ここでは主に、マップから仮想画面を作成し、仮想画面を実際に表示して、仮想画面のビューを切り替えています。

def disp_scrwin(self, party, floor):
    """
    Display scroll window main
    """
    start = time.time()
    self.draw_map(party, floor)
    self.display()
    self.cur_vscr_view, self.prev_vscr_view \
        = self.prev_vscr_view, self.cur_vscr_view
    delta = time.time() - start
    try:
        print(f"\033[{self.height-1};0H", end='')
        print(f"\n{party.x:03d}/{party.y:03d}, {delta:.5f}", flush=True)
    except:
        pass

2.2 マップから仮想画面の作成

マップデータを仮想画面にコピーするのはdraw_map()メソッドで行います。フロア全体のマップデータから、画面表示する部分だけを切り取っています。表示エリアがフロアからはみ出た部分は、いまのところ岩('#')と区別して'^'記号を表示しています。

表示する1行分をスライスして'^'で初期化しています。ここに対応するマップデータをスライスしてコピー、上書きします。l_left/l_rightの計算がmin()を使って若干面倒なことになっているのは、フロア外の表示部分を考慮しているためです。プレイヤーパーティーは常に画面の中心位置に'@'として表示します。

def draw_map(self, party, floor):
    """
    Copy map data to a virtual scroll window
    """
    floor_view = memoryview(floor.floor_data)
    cv = self.cur_vscr_view
    w = self.width
    for cy in range(self.height):
        cv[cy*w:(cy+1)*w] = b'^'*w  # fill with rocks
        my = party.y - self.height//2 + cy  # convert cy to floor_y
        if 0 <= my < floor.y_size:
            l_left = min(0, party.x-w//2) * -1
            l_right = min(w, floor.x_size - party.x + w//2)
            map_left = my*floor.x_size + party.x - w//2 + l_left
            map_right = map_left + l_right - l_left
            cv[cy*w+l_left:cy*w+l_right] = floor_view[map_left:map_right]
        if cy == self.height//2:
            cv[cy*w+w//2:cy*w+w//2+1] = b'@'

2.3 仮想画面を実際に表示

display()メソッドは、仮想画面を実際にprint()文を使ってターミナルに表示します。ロジックはstraight forwardで読みやすくなっていると思います。if文は前回描いた仮想画面と今回描く仮想画面の行同士を比較している部分です。1行まるまる同じであれば、描画をスキップします。

def display(self):
    """
    Actually print scroll window on the terminal
    """
    cv = self.cur_vscr_view
    w = self.width
    for y in range(self.height):
        slc = slice(y*w, (y+1)*w)
        if cv[slc] != self.prev_vscr_view[slc]:
            print(f"\033[{y};0H", end='')
            print(cv[slc].tobytes().decode(), end='')

3 プレイヤーパーティーの移動

プレイヤーパーティーは常に画面中心に表示するとしているため、パーティーの座標を変えると画面がスクロールします。

Rogueと同様のキーバインド、つまり'h', 'j', 'k', 'l'のキーでパーティー座標を移動させています。キーが押されるとパーティー座標を1ずつずらし、draw_map()関数を呼んでいます。

non-blockingなキー入力は初回で紹介したコードスニペットを修正して使っています。

def main():
    floor = generate_floor(1)
    party = Party(0, 0, 1)
    w, h = terminal_size()
    vscr = Vscr(w, h-1)
    vscr.disp_scrwin(party, floor)

    fd = sys.stdin.fileno()

    oattr = termios.tcgetattr(fd)
    nattr = oattr
    nattr[3] = nattr[3] & ~termios.ICANON & ~termios.ECHO
    termios.tcsetattr(fd, termios.TCSANOW, nattr)

    oflags = fcntl.fcntl(fd, fcntl.F_GETFL)
    fcntl.fcntl(fd, fcntl.F_SETFL, oflags | os.O_NONBLOCK)

    try:
        while True:
            try:
                c = sys.stdin.read(1)
                if c:
                    if c == 'h' and party.x > 0:
                        party.x -= 1
                    elif c == 'k' and party.y > 0:
                        party.y -= 1
                    elif c == 'j' and party.y < floor.y_size-1:
                        party.y += 1
                    elif c == 'l' and party.x < floor.x_size-1:
                        party.x += 1
                    elif c == '.':
                        pass
                    else:
                        pass
                else:
                    draw = False
                if draw:
                    vscr.disp_scrwin(party, floor)

            except IOError:
                pass
    finally:
        termios.tcsetattr(fd, termios.TCSAFLUSH, oattr)
        fcntl.fcntl(fd, fcntl.F_SETFL, oflags)


if __name__ == "__main__":
    main()

4 動かしてみる

動かしてみます。

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^######################............######^^^^^^^^^^^^
^^^^^^^^^^^^^#.......##############............######^^^^^^^^^^^^
^^^^^^^^^^^^^#.......##############............######^^^^^^^^^^^^
^^^^^^^^^^^^^#.......####.......#########+##+########^^^^^^^^^^^^
^^^^^^^^^^^^^#.......####.......######............###^^^^^^^^^^^^
^^^^^^^^^^^^^#.......####.......+....+............###^^^^^^^^^^^^
^^^^^^^^^^^^^####+#######.......######............###^^^^^^^^^^^^
^^^^^^^^^^^^^####.#######.......@#####............###^^^^^^^^^^^^
^^^^^^^^^^^^^####.##########+#############+##########^^^^^^^^^^^^
^^^^^^^^^^^^^####*....######.........#####+##########^^^^^^^^^^^^
^^^^^^^^^^^^^####*....*....+.........###.....########^^^^^^^^^^^^
^^^^^^^^^^^^^####*....######.........###.....+..+++##^^^^^^^^^^^^
^^^^^^^^^^^^^####+##*#######.........###.....###....#^^^^^^^^^^^^
^^^^^^^^^^^^^#......+#######.........###########....#^^^^^^^^^^^^
^^^^^^^^^^^^^#......+#######.........###########....#^^^^^^^^^^^^
^^^^^^^^^^^^^#......#################################^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
019/007, 0.00011

少し楽しいです。マップをスクロールして表示するだけでなかなかゲームらしくなりますね。Rogueぽいのはここまでで、これからはどんどんWizardry色が強くなっていく予定です。

5 次は、、

次は画面上にメッセージを表示できるようにします。 ではまた。