11
5

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 × PyTweening で動きを演出しよう!

11
Last updated at Posted at 2025-12-17

0.Prologue Easing / Tweening

 この記事では、Pyxelでイージングまたはトゥイーンを扱う方法を説明します。まずは、イージングとトゥイーンとは何ぞやというところから始めますね。
 Easing (イージング)とTweening (トゥイーン)は、主にアニメーションやモーショングラフィックス分野の用語で密接に関連しています。

  • Tweening (トゥイーン):アニメーションの中間フレームを自動生成する技術(補間)を指します。
  • Easing (イージング):そのトゥイーン(中間補間)の速度変化(緩急)を定義する機能を指します。
  • 両者とも「動きを滑らかに見せる」という意味では同義です。

 今後は、私が使い慣れているイージングで話を進めたいと思います。

主要なイージング30種類
 https://easings.net/ja
 (各イージングの上にマウスカーソルを置くと、そのイージングが確認できます)

1.PyTweenig

PyTweeningは、Pythonで実装されたイージング関数の集まりで、主要なイージング30種類に他の3種類を加えた計33種類の関数群になっています。(等速処理の Linear を入れると34種類になります)

また、PyTweeningは2つの方法でイージングを提供しています。

  • ease系関数
     0.0から始まり1.0で終わる時間経過を入力すると、線分の過程を0.0~1.0の値で出力します。
import pytweening as tween

# 0.0~1.0 の範囲で easeInQuad を適用
for time_progress in [0.0, 0.25, 0.5, 0.75, 1.0]:
    line_progress = tween.easeInQuad(time_progress)
    print(f"IN: {time_progress:.2f}  OUT: {line_progress}")
IN: 0.00  OUT: 0.0
IN: 0.25  OUT: 0.0625
IN: 0.50  OUT: 0.25
IN: 0.75  OUT: 0.5625
IN: 1.00  OUT: 1.0
  • iterEase系関数
     始点(startX, startY)と終点(endX, endY)、そして、間隔サイズ(intervalSize) [ 0.0~1.0 ] を入力すると、間隔サイズ毎のXY座標のジェネレータを出力します。
import pytweening as tween

# (0, 0)から(1, 100)へ4ステップで easeInQuad の値を生成
startX, startY = 0, 0
endX, endY = 1, 100
intervalSize = 0.25 # 1/4  [ 0.0~1.0 ]

iter_point_progress = tween.iterEaseInQuad(startX, startY, endX, endY, intervalSize)

print(f"{type(iter_point_progress)=}")
for point in iter_point_progress:
    print(point)
type(iter_point_progress)=<class 'generator'>
(0.0, 0.0)
(0.0625, 6.25)
(0.25, 25.0)
(0.5625, 56.25)
(1.0, 100.0)
  • この2種類の関数に共通して言えることは、始まりと終わり、そして時間の経過具合や間隔を指定するだけで、その間の中間フレームを生成してくれるところです。

2.Pyxel × PyTweening

(1) 簡単な例から

 PyxelPyTweeningを使うなら、Pyxelが2次元座標を取り扱うことが多いので、iterEase系関数がおススメです。
 例えば、30FPSで、始点(10, 50)から終点(210, 50)まで2秒で移動する円のイージングを考えてみましょう。間隔サイズは、60 frame (= 30 frame/sec * 2 sec) の逆数になります。

import pytweening as tween

# (10, 50)から(210, 50)へ60ステップで easeInQuad の値を生成
startX, startY = 10, 50
endX, endY = 210, 50
intervalSize = 1 / 60

iter_point_progress = tween.iterEaseInQuad(startX, startY, endX, endY, intervalSize)

print(f"{len(list(iter_point_progress))=}")
len(list(iter_point_progress))=61

 生成される座標の数(フレーム数)は61で、始点の座標に各移動後の座標60個を加えた計61個が出力されます。
 あとは、Pyxelで各フレーム毎にPyTweeningで求めた座標を参照して描画するだけになります。

import pyxel
import pytweening as tween

class App:
    def __init__(self):
        pyxel.init(220, 100, title="EaseInQuad Bullet Demo", fps=30)
        self.reset()
        pyxel.run(self.update, self.draw)

    def reset(self):
        startX, startY = 10, 50
        endX, endY = 210, 50
        intervalSize = 1 / 60 # 1 / (30FPS * 2sec)

        # iterEaseInQuad で座標列を生成
        self.points = list(tween.iterEaseInQuad(startX, startY, endX, endY, intervalSize))
        self.frame = 0

    def update(self):
        # スペースキーでリセット
        if pyxel.btnp(pyxel.KEY_SPACE):
            self.reset()

        # フレーム進行
        if self.frame < len(self.points) - 1:
            self.frame += 1

    def draw(self):
        pyxel.cls(0)
        x, y = self.points[self.frame]
        pyxel.circ(int(x), int(y), 4, 11)

        # UI表示
        pyxel.text(5, 5, "SPACE: Reset", 7)
        pyxel.text(5, 15, f"Frame: {self.frame}", 7)

App()

(2) 複数のオブジェクトにイージング

 ゲームで使用するなら、複数のオブジェクトにイージング処理を行い、フレーム毎に表示できないと意味がありません。
 例えば、スペースキーを押すと弾丸が発射されるコードを考えてみましょう。各弾丸にイージングを行って、複数の弾丸飛び交う場面が期待できます。実行結果がこちらです。

 いい感じに弾幕ができたのではないでしょうか。因みに、画面右下の数字は弾丸の数で最大58個の弾丸を表示していました。
 スペースキーを押すと飛行体の左右から弾丸を発射しますが、左側は等速で、右側は減速系(初速は等速の弾より早く、だんだん減速していく)のイージング処理を行っています。

import pytweening as tween
# 弾丸のイージング
EASE_FUNC_L = tween.iterLinear # 等速
EASE_FUNC_R = tween.iterEaseOutQuad # 減速系

class Bullet:
    def __init__(self, ease_func, start_pos, end_pos, frames=FRAMES):
        self.start_x, self.start_y = start_pos
        self.end_x, self.end_y = end_pos
        self.frames = frames
        interval_size = 1.0 / max(1, frames)
        self.iterator = ease_func(self.start_x, self.start_y, self.end_x, self.end_y, interval_size)
        self.current = next(self.iterator, None)

class App:
    def __init__(self):
        self.objects = []
    
    def update(self):
        if pyxel.btnp(pyxel.KEY_SPACE, 30, 2): # 30フレーム押し続けたら連射モード
            start_pos_L = (self.x - 7, self.y)
            end_pos_L = (self.x - 7, -1)
            self.objects.append(
                Bullet(EASE_FUNC_L, start_pos_L, end_pos_L)
            )
            start_pos_R = (self.x + 7, self.y)
            end_pos_R = (self.x + 7, -1)
            self.objects.append(
                Bullet(EASE_FUNC_R, start_pos_R, end_pos_R)
            )

 スペースキーが押される度に弾丸オブジェクト( Bullet )を生成し、生成のタイミングでイージング処理された座標群が弾丸オブジェクト( Bullet )のiteratorプロパティ( self.itetator )に格納されます。この生成した弾丸オブジェクト( Bullet )は、Appオブジェクトのリスト( self.objects )に追加されます。
 次に、上記と同じ箇所で、弾丸オブジェクトのアップデートを行います。具体的には、弾丸オブジェクトの座標を提供するジェネレータを 組込み関数 next で至近の座標( self.current )を出力します。

class Bullet:
    def update(self):
        self.current = next(self.iterator, None)
    def is_finished(self):
        return self.current[1] < 0 # Y座標マイナスならTrue(表示終了)

class App:
    def update(self):
        ## 弾丸オブジェクトのアップデートと枠外弾丸は削除(メモリ開放)
        for obj in self.objects[:]:
            obj.update()
            if obj.is_finished():
                self.objects.remove(obj)

 また、アップデートの際、弾丸が画面より外なら、当該オブジェクトは弾丸のリスト( self.objects )から削除し、メモリから解放されます。
 あとは、App と Bullet の drawメソッド で弾丸を描画するだけになります。

class Bullet:
    def draw(self):
        if self.current is None:
            return
        pyxel.pset(self.current[0], self.current[1], 10)

class App:
    def draw(self):
        pyxel.cls(0)
        # 弾丸
        for obj in self.objects:
            obj.draw()

3.Epilogue

 始点と終点と使用するフレーム数が決まれば、その間の座標の計算はイージング関数がやってくれます。
 移動だけでなく、画像の大きさ(面積)や、Pyxelではできませんが画像の透過度などをスムーズに変化させたい時にもイージングは使えます。
 ぜひ、イージングで動きを演出してみませんか。

Appendix

コードの公開

自作したイージング作品の紹介

  • Pyxelではありませんが、今年の1月(2025年1月)に参加したgenuary( https://genuary.art/ )から私がイージングを使って作成した作品を紹介します。
     イージングは自作ライブラリ、データ処理にPandas、画像処理にPILを使用しています。
11
5
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
11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?