見出し画像

Pyxelで慣性スクロールしてみる

ここのところずっと、レトロゲームエンジンPyxelでなぜかスマホゲーム向けの実装をするという遊びにこだわっています。
本来相性がいいとは全然思えないのですけど。

「Pyxelでスマホゲーム」の場合の画面サイズは160x256(9:16)が最適かなと思っていますが、横幅160pxというサイズは、それなりに色々な表現ができるものの、たとえば16x16のマップチップを並べようとするとわずか横に10チップしか並べられないことになります。

そこで、スマホといえば慣性スクロール。
指をシャーっとやると画面がシャーっと動くアレです(絶望的語彙力)

実装ですが実は大して難しくありません。日本語で書くとこんな感じです。

  • タップ(マウスのクリックと同じ)開始したら速度の記録を開始

  • 指を外したら、その時点までの座標の移動量を時間で割、速度を確定する。たとえば5フレームの間にx座標が45、y座標が-10動いたとすると、速度はx方向に+9, y方向に-2、といった具合

  • あとはスクロールを開始しつつ、少しずつ減速して、ある程度小さくなったら止めるようにする

サンプルコードも掲載しておきます。
スクロールとは別に「このセルをタップした」というイベントも検出する必要があったり(このサンプルではタップしたセルに白い枠が表示されるようにしています)、マップの端で境界処理を入れたりで若干長くてなっていますが、処理自体はイージーであることは汲み取っていただけるのではと思います。

import pyxel as px

VISION_X = 10
VISION_Y = 16


class Screen:
    def __init__(self, screen):
        self.x = screen["x"] * 16
        self.y = screen["y"] * 16


class App:
    def __init__(self):
        px.init(VISION_X * 16, VISION_Y * 16, title="慣性スクロールデモ")
        px.load("dq1.pyxres")  # マップ表示用のアセット
        self.screen = Screen({"x": 45, "y": 40})
        self.is_drag = False
        self.saved_x = 0
        self.saved_y = 0
        self.selected_x = None
        self.selected_y = None
        self.vel_x = 0
        self.vel_y = 0
        self.vel_len = 0
        px.mouse(True)
        px.run(self.update, self.draw)

    # update
    def update(self):
        if px.btnp(px.KEY_Q):
            px.quit()
        x = px.mouse_x
        y = px.mouse_y
        scr = self.screen
        if px.btn(px.MOUSE_BUTTON_LEFT):
            if not self.is_drag:
                self.saved_x = x
                self.saved_y = y
                self.is_drag = True
                self.selected_x = None
                self.selected_y = None
                self.vel_len = 0
            else:
                self.vel_len += 1
        elif self.is_drag and self.vel_len > 0:
            self.vel_x = (self.saved_x - x) / self.vel_len
            self.vel_y = (self.saved_y - y) / self.vel_len
            self.is_drag = False
            # 移動量が小さい場合はスワップではないタップとみなす
            if self.vel_x**2 + self.vel_y**2 < 4:
                self.selected_x = (x + scr.x) // 16
                self.selected_y = (y + scr.y) // 16

        self.apply_boundary()
        self.apply_inertia()

    # 境界処理
    def apply_boundary(self):
        scr = self.screen
        self.vel_x = max(self.vel_x, -scr.x)
        self.vel_x = min(self.vel_x, (128 - VISION_X) * 16 - scr.x)
        self.vel_y = max(self.vel_y, -scr.y)
        self.vel_y = min(self.vel_y, (128 - VISION_Y) * 16 - scr.y)

    # 惰性を反映
    def apply_inertia(self):
        scr = self.screen
        scr.x += self.vel_x
        scr.y += self.vel_y
        self.vel_x *= 0.95
        self.vel_y *= 0.95
        # 速度が小さくなったら止める
        if abs(self.vel_x) < 1 / 16:
            self.vel_x = 0
        if abs(self.vel_y) < 1 / 16:
            self.vel_y = 0

    # draw
    def draw(self):
        px.cls(0)
        scr = self.screen
        # フィールド表示
        px.bltm(0, 0, 0, scr.x, scr.y, VISION_X * 16, VISION_Y * 16)
        # カーソル表示
        if not self.selected_x is None and not self.selected_y is None:
            cx = self.selected_x * 16 - scr.x
            cy = self.selected_y * 16 - scr.y
            px.rectb(cx, cy, 16, 16, 7)


app = App()

今のところ私自身はこれを使って実際にゲームを作る予定はないのですが、いろいろ応用は効きそうなので、よければどうぞどなたでも使ってやってください!

(もちろんPyxelでなくてもこの手法は使えますし、JSとかで自前で実装する場合も同じ考え方でいけそうですね。)

この記事が気に入ったらサポートをしてみませんか?