2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pyxelでキャラクター書き換え背景アニメーション

Last updated at Posted at 2024-12-23

ファイナルファンタジー3の海のアニメーションを作ってみる

ファミコン版ファイナルファンタジー3(以下FF3)の海面は、波打つようにアニメーションしています。
(たとえば次の動画の1:29:39あたりから見られます)。

それを、Pyxelで真似して作ってみました。

pyxel-20241223-142136.gif

しくみ

もとのFF3のアニメーションを見てみると、一ラインごとにタイミングをずらして1pxずつ右に動いているように見えます。

bg_anime_figure1.png

左がもとになるキャラクター(Pyxelのイメージバンク)、右が表示した状態(Pyxelのタイルマップ)です。
水面にあたる左の赤枠の部分を何度もマップに配置して海を表現しています。
そのため、元になるイメージバンクの絵を動かしてしまえば、マップにある絵もすべて書き換わる算段です。
FF3も、元になるキャラクターのパターンを書き換えてアニメーションさせているはず。

コード

指定したイメージバンクの部分を一ラインずつずらして表示するためのクラスです。

self.tileshift = TileShift(0, 1, 40, 16, 1, 16, 16, True)
"""
self.tileshift = TileShift(
    イメージバンク番号,
    ずらす量(正で右、負で左),
    イメージバンクのX,
    イメージバンクのY,
    何フレームごとに動かすか,
    動かす部分の幅,
    動かす部分の高さ,
    同ずらし動作(Falseで全ライン同時シフト)
    )
"""
tile_shift.py
import pyxel

class TileShift:
    def __init__(self, bank, speed_h, u, v, timing, width=8, height=8, delay=False):
        """
        Shift an image cell on an image bank

        Args:
            bank (int): The number of the image bank
            speed_h (int): Scrolling direction (+:left, -:right)
            u (int): X position of the target image on the image bank
            v (int): Y position of the target image on the image bank
            timing (int): Scroll timing (per frame)
            width (int): tile width(px)
            height (int): tile height(px)
            delay: delay shift per lines (Boolean)
        """
        self.screen_ptr = pyxel.images[bank].data_ptr()
        self.speed_h = speed_h
        self.u = u
        self.v = v
        self.timing = timing
        self.width = width
        self.height = height
        self.count = 0
        self.delay = delay
        
    def update(self):
        if self.timing == 0 :
            self.timing = 1
        self.count = (self.count + 1) % self.height
        
        if pyxel.frame_count % self.timing == 0:
            if self.delay:
                y = self.count
                self.shift_line(y)
            else:
                for y in range(0, self.height):
                    self.shift_line(y)

    def shift_line(self, y):
        cell = self.u + (self.v + y) * 256
        source = self.screen_ptr[cell:cell + self.width]
        shifted = self.rotate(source, self.speed_h)
        self.screen_ptr[cell:cell + self.width] = shifted
        
    def rotate(self, list, n):
        if not list: # empty list
            return list
        length = len(list)
        n = n % length
        # n<0: left / n>0: right
        return list[-n:] + list[:-n]

pyxel.images[bank].data_ptr()で、{bank}番目のイメージバンクの色データをリストで取得できます。
データは一本につながっているので、

        cell = self.u + (self.v + y) * 256
        source = self.screen_ptr[cell:cell + self.width]

横の指定座標u + (縦の指定座標V + y座標) * イメージバンクの幅256で目的のデータのスタート部分を割り出した後、そこからwidth分データを取り出します。
それを、定義したrotate()で左右にデータをずらし、self.screen_ptr[cell:cell + self.width] = shiftedで、抜き出した部分に上書きしています。

ずらしありで動かすと、

self.tileshift = TileShift(0, 1, 40, 16, 1, 16, 16, True)

pyxel-20241223-142136.gif

ずらしなしで動かすと、

self.tileshift = TileShift(0, 1, 40, 16, 10, 16, 16, False)

pyxel-20241223-142227.gif

MSXなど昔のコンピュータ風にいうと、PCG書き換えという感じでしょうか。
うまく使うと、スクロールしていないのにスクロールしているように見える、というような演出が可能です。

余談

ツインファミコンで遊んでますが、いよいよクライマックスという段階のデータが消えました。
ファイナルファンタジーもうやめます。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?