初版 : 2026-04-13
🖥 基本・ウィンドウ制御 編
Pyxelで最初に触ることが多い「画面を出す」「サイズを決める」「FPSを変える」「フルスクリーンにする」といった、ウィンドウ制御まわりを逆引き形式でまとめます。
対象にする項目は次の7つです。
- 画面を表示したい
- 画面サイズを変更したい
- フレームレート(FPS)を変更したい
- 実行中に画面サイズを変えたい
- ウィンドウタイトルを変更したい
- フルスクリーン表示にしたい
- ヘッドレス(画面なし)で実行したい
まず最初に:Pyxelで画面を出すときの基本形
Pyxelでは、最初に pyxel.init() でアプリ全体の初期設定を行い、そのあと pyxel.run(update, draw) でメインループを開始するのが基本です。init() では画面サイズ、ウィンドウタイトル、FPS、終了キー、表示倍率、キャプチャ設定、ヘッドレス実行の有無などを指定できます。run() は毎フレーム update と draw を呼びます。
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120, title="My App", fps=30)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, "Hello, Pyxel", 7)
App()
このコードでやっていることは単純です。
-
pyxel.init(160, 120, ...)で「160×120ピクセルの仮想画面」を作る -
pyxel.run(...)で毎フレーム処理を回す -
draw()のたびに画面をクリアして文字を描く
ここでいう 160x120 は「ゲーム内部の画面サイズ」です。実際にモニタに表示されるウィンドウの見た目サイズは、後述する display_scale やフルスクリーンの有無でも変わります。pyxel.width と pyxel.height には現在の画面サイズが入ります。
1. 画面を表示したい
概要
画面を出す最小構成は、pyxel.init() と pyxel.run() の2つです。init() を呼ばずに run() だけ呼ぶことはできません。run() は「毎フレーム、更新処理と描画処理を呼ぶ関数」なので、Pyxelのアプリは基本的にこの形から始まります。
詳細解説
import pyxel
def update():
pass
def draw():
pyxel.cls(1)
pyxel.text(20, 20, "Window Test", 7)
pyxel.init(160, 120)
pyxel.run(update, draw)
このコードで実際に起きること
-
pyxel.init(160, 120)
→ 160×120の画面を持つPyxelアプリを初期化する -
pyxel.run(update, draw)
→ 以後、Pyxelが内部ループを開始し、毎フレームupdate()とdraw()を呼ぶ -
pyxel.cls(1)
→ 毎フレーム、背景色1で画面を塗りつぶす -
pyxel.text(...)
→ 文字を描く
初学者が混乱しやすい点
「画面サイズ」と「実際のウィンドウサイズ」は同じではない
160x120 は内部解像度です。実際の見た目の大きさは、自動スケーリングや display_scale の指定で変わります。つまり、小さいゲーム画面を拡大して表示する のがPyxelの基本です。display_scale を省略した場合は自動決定されます。
draw() は1回だけではない
draw() は毎フレーム呼ばれます。
そのため、例えば一度だけ描けば残るという考え方ではなく、毎フレーム、今の状態を描き直す という理解が必要です。これは run() の説明そのものに対応しています。
どういうときに使うか
- 最初の動作確認
- メニュー画面の土台作成
- ランチャーや簡易UIの雛形作成
- ゲーム本体の起動部分
ここまで理解できれば十分なこと
この段階では、次の3点が分かっていれば十分です。
- 画面を出すには
pyxel.init()が必要 - 毎フレーム処理は
pyxel.run(update, draw)で回る -
draw()では毎フレーム描き直す
それ以上の細かい挙動を詰めたくなったら、init() と run() のAPIリファレンスを見れば十分です。
2. 画面サイズを変更したい
概要
画面サイズは pyxel.init(width, height, ...) の width と height で指定します。ここで設定するサイズは「ゲーム内部で扱う画面サイズ」です。 pyxel.width と pyxel.height には現在のサイズが入ります。
詳細解説
import pyxel
class App:
def __init__(self):
pyxel.init(256, 144, title="256x144")
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, f"{pyxel.width}x{pyxel.height}", 7)
App()
画面サイズとは何か
例えば pyxel.init(256, 144) と書いた場合、
Pyxelの内部では「横256ピクセル、縦144ピクセルのスクリーン」が作られます。
その中にスプライトや文字や図形を描くことになります。
具体的に何が変わるのか
1. 描画可能な範囲が変わる
たとえば 160x120 なら x=159, y=119 あたりまでが見える範囲です。
320x180 にすると、より広い座標範囲をそのまま表示できます。これは width / height が画面そのもののサイズだからです。
2. 見た目の密度が変わる
同じ16×16スプライトでも、
- 160×120の画面では「大きく見えやすい」
- 320×180の画面では「相対的に小さく見えやすい」
という違いが出ます。
これはスプライトの大きさが変わるのではなく、画面全体に対する占有率が変わる からです。
3. UI設計の考え方が変わる
小さな画面では文字数を絞る必要があります。
大きな画面では一覧性は上がりますが、そのぶん座標管理も細かくなります。
どのサイズを選べばよいか
これは作るものによります。
Pyxelでは「レトロっぽい低解像度を拡大表示する」前提が扱いやすいので、たとえば次のような考え方が実用的です。
-
160x120
小さくて管理しやすい。試作向け -
256x144
16:9で扱いやすい。UIや横長画面に向く -
320x180
16:9で情報量を少し増やしたいときに便利 -
256x256
正方形寄りの画面や携帯機向けUI検討に向く
display_scale と混同しないこと
ここで変える width / height は内部画面サイズです。
一方、display_scale はその内部画面を何倍で表示するか に関わる値です。 display_scale は init() の引数として用意されており、省略時は自動決定されます。
例えば、
pyxel.init(160, 120, display_scale=4)
とすると、内部画面は160×120のままですが、見た目の表示倍率は4倍になります。
よくある勘違い
「画面サイズを大きくすれば、ドット絵が綺麗になる」
そうではありません。
画面サイズを大きくすると、単に内部座標の範囲が広がるだけです。
ドット絵そのものの解像感や見え方には、スプライト設計や表示倍率のほうが強く影響します。
「後で変えるのは難しい」
実際には後からでも変えられますが、座標配置やUIの見え方はかなり変わるので、プロジェクトの早い段階で基準解像度を決める ほうが楽です。
ここまで理解できれば十分なこと
-
width/heightは内部画面サイズ -
display_scaleは見た目の倍率 - 画面サイズを変えると、描ける範囲とUI密度が変わる
ここまで分かれば、細かい引数仕様は API リファレンスを見れば十分です。
3. フレームレート(FPS)を変更したい
概要
pyxel.init() の fps 引数でフレームレートを指定できます。デフォルトは30です。つまり、指定しない場合は毎秒30回 update() と draw() が回ります。
詳細解説
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120, fps=60, title="FPS 60")
self.x = 0
pyxel.run(self.update, self.draw)
def update(self):
self.x += 1
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, f"frame={pyxel.frame_count}", 7)
pyxel.text(self.x, 30, ">", 10)
App()
FPSとは何か
FPSは Frames Per Second の略で、
1秒あたり何回フレーム更新を行うかを意味します。
-
fps=30
1秒間に30回更新 -
fps=60
1秒間に60回更新
Pyxelの run() は毎フレーム update と draw を呼ぶので、FPSを変えるとロジックの進む速さの基準も変わります。
具体的に何が変わるのか
1. 動きの滑らかさ
一般に60FPSのほうが30FPSより滑らかに見えやすいです。
2. 処理コスト
FPSを上げるほど、1秒あたりの更新回数が増えるので処理負荷も増えやすくなります。
3. 1フレームあたりの意味
たとえば update() 内で x += 1 しているだけだと、
- 30FPSなら1秒で30進む
- 60FPSなら1秒で60進む
という違いになります。
つまり、フレーム単位で書いた処理はFPSに依存する ということです。
実務でどう考えるか
30FPS向き
- レトロゲーム風
- 軽いUI
- 組み込み機向け
- そこまで高い滑らかさが不要な場合
60FPS向き
- より滑らかな移動が欲しい場合
- 反応の良さを重視する場合
- 高FPS環境で見栄えを良くしたい場合
よくある失敗
移動速度をFPSと切り離して考えてしまう
例えば、
self.x += 2
のような処理は、FPSを30から60に変えると、見た目上ほぼ2倍の速度になります。
そのため、FPSを変えるなら「移動量」や「アニメーション間隔」も見直す必要があります。
アニメーションのフレーム間隔を固定しすぎる
pyxel.frame_count % 10 == 0 のようなコードは、FPSが変わると体感速度も変わります。
30FPSでは約0.33秒ごと、60FPSでは約0.16秒ごとになります。 frame_count は経過フレーム数です。
ここまで理解できれば十分なこと
- FPSは
init(..., fps=値)で指定する - デフォルトは30
- FPSを変えると、見た目だけでなくロジックの体感速度も変わる
これ以上の細かな設計は、init() の仕様と、自分のゲーム内ロジックの書き方をAPIリファレンスと実コードで照らして詰めるのがよいです。
4. 実行中に画面サイズを変えたい
概要
Pyxel 2.9.0 で resize(width, height) が追加され、実行中に画面サイズを変更できるようになりました。API上も「Change the screen size at runtime」と明記されています。
詳細解説
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120, title="Resize Demo")
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_1):
pyxel.resize(160, 120)
if pyxel.btnp(pyxel.KEY_2):
pyxel.resize(256, 144)
if pyxel.btnp(pyxel.KEY_3):
pyxel.resize(320, 180)
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, "1:160x120 2:256x144 3:320x180", 7)
pyxel.text(10, 30, f"current={pyxel.width}x{pyxel.height}", 10)
App()
何が便利なのか
これは単なるおまけ機能ではなく、次のような用途でかなり便利です。
- 設定画面から解像度を切り替えたい
- デバッグ中に複数解像度を試したい
- UIがどのサイズで崩れるか確認したい
- 端末ごとの見え方を簡単に比較したい
resize() で何が起きるのか
resize(256, 144) を呼ぶと、その時点から
pyxel.widthpyxel.height
が新しい値になり、以後の描画は新しい画面サイズ基準で行われます。これは API の説明に一致しています。
実務で注意すべきこと
1. UI座標を固定値だけで書いていると崩れる
例えば右下に表示したい要素を
pyxel.text(140, 110, "OK", 7)
のように固定で書いていると、サイズ変更後に位置関係が崩れます。
そのため、リサイズを前提にするなら
pyxel.text(pyxel.width - 20, pyxel.height - 10, "OK", 7)
のように、現在サイズを基準にした配置のほうが向いています。
2. カメラやマップ表示範囲も変わる
タイルマップやカメラ制御をしている場合、画面サイズが変わると「何タイル見えるか」が変わります。
そのため、単に resize() を呼ぶだけでなく、表示範囲を計算しているコードも見直す必要があります。
3. フォントやUI密度の印象も変わる
同じ12pxフォントでも、160×120では大きく感じ、320×180では小さく感じます。
解像度切り替えを正式機能にするなら、フォントサイズや余白も含めて設計したほうが見栄えが安定します。
どんな場面で使うとよいか
- 開発中の確認用
- オプションメニューの解像度切り替え
- 複数端末を意識したUI検証
- ランチャーやツールで表示モードを切り替えたい場合
ここまで理解できれば十分なこと
-
resize(width, height)で実行中変更できる - 追加されたのはPyxel 2.9.0
- リサイズに対応するなら、固定座標前提のUI設計を見直す必要がある
細かな相互作用は resize() と width / height 周辺のAPIリファレンスを見れば十分です。
5. ウィンドウタイトルを変更したい
概要
方法は2つあります。
-
pyxel.init(..., title="...")で最初から指定する - 実行中に
pyxel.title("...")を呼んで変更する
どちらも公式APIにあります。 init() の title のデフォルト値は "Pyxel" です。 title() は「Set the window title」と説明されています。
詳細解説
最初から設定する例
import pyxel
pyxel.init(160, 120, title="My Launcher")
pyxel.run(lambda: None, lambda: pyxel.cls(0))
実行中に変更する例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120, title="Initial Title")
self.counter = 0
pyxel.run(self.update, self.draw)
def update(self):
self.counter += 1
if self.counter == 90:
pyxel.title("Title Changed")
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, "Watching title...", 7)
App()
タイトル変更は何に使うのか
一見地味ですが、実用上は意外と便利です。
- アプリ名を分かりやすくしたい
- デバッグ中に状態を表示したい
- 起動中のモード名を出したい
- ランチャーで現在の画面を示したい
例えば開発中なら、
Map Editor - test_01Launcher - FavoritesGame - Pause
のように状態をタイトルバーに出しておくと、複数ウィンドウを扱うときに見分けやすくなります。
注意点
タイトルは機能そのものではない
タイトルは見た目上のラベルです。
ゲーム内ロジックには影響しません。
頻繁に変えすぎる必要はない
フレームごとにタイトルを変更するような使い方は、通常は実益が少ないです。
画面遷移時やモード変更時だけ変える程度が扱いやすいです。
ここまで理解できれば十分なこと
- 最初に決めるなら
init(..., title="...") - 後から変えるなら
pyxel.title("...") - デフォルトタイトルは
"Pyxel"
あとはAPIリファレンスでシグネチャだけ確認すれば十分です。
6. フルスクリーン表示にしたい
概要
pyxel.fullscreen(enabled) でフルスクリーンの有効・無効を切り替えられます。API上も enabled: bool を受け取る関数として公開されています。
詳細解説
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120, title="Fullscreen Demo")
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_F):
pyxel.fullscreen(True)
if pyxel.btnp(pyxel.KEY_G):
pyxel.fullscreen(False)
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, "F: fullscreen on", 7)
pyxel.text(10, 20, "G: fullscreen off", 7)
App()
フルスクリーンで何が起きるのか
フルスクリーンは「内部画面サイズが変わる」機能ではありません。
160x120 の内部画面を、画面全体に広げて表示するモードです。
つまり、解像度変更とフルスクリーン化は別の話 です。 APIでも fullscreen() と resize() は別関数として分かれています。
どういうときに便利か
- 実機っぽく全画面表示したい
- ランチャーやゲームを専用アプリっぽく見せたい
- 枠付きウィンドウを避けたい
- 小さい内部画面を大きく見せたい
実務上の考え方
フルスクリーンは「表示モード」
ゲームロジックや内部座標系はそのままです。
そのため、フルスクリーン対応をしたい場合はまず
- 内部画面サイズ
- 表示倍率
- UIの余白
- 画面端で切れないか
を考え、その上で「見せ方として全画面にする」と捉えると分かりやすいです。
リサイズとの組み合わせも可能
例えば設定画面で
- 160x120 / Windowed
- 256x144 / Windowed
- 256x144 / Fullscreen
のような選択肢を作ることも考えられます。
この場合、resize() と fullscreen() を組み合わせて制御します。 resize() は2.9.0追加、fullscreen() は現行APIで公開されています。
よくある勘違い
フルスクリーンにすれば内部解像度も上がる
上がりません。
フルスクリーンは表示形態の変更であって、内部の width / height 変更ではありません。
フルスクリーンだけ考えればいい
実際には、表示倍率・文字サイズ・UI密度もセットで考えたほうが完成度が上がります。
ここまで理解できれば十分なこと
-
pyxel.fullscreen(True/False)で切り替えられる - フルスクリーンは内部解像度変更とは別
-
resize()と併用すると設定画面が作りやすい
詳細は fullscreen() のAPI仕様を見れば十分です。
7. ヘッドレス(画面なし)で実行したい
概要
pyxel.init(..., headless=True) を使うと、ウィンドウを出さずに実行できます。 headless は 2.7.10 で init() に追加され、その後 2.8.5 でヘッドレス時の音声再生対応、2.8.7 で通常と同じフレームループに関する修正が入っています。つまり、比較的新しい機能で、実用性も段階的に改善されてきた機能です。
詳細解説
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120, headless=True)
self.count = 0
pyxel.run(self.update, self.draw)
def update(self):
self.count += 1
if self.count >= 60:
pyxel.quit()
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, "headless", 7)
App()
ヘッドレスとは何か
ヘッドレスは「画面を一切出さない」という意味です。
ただし「Pyxelの処理そのものを何もしない」という意味ではありません。
run() によるフレームループ自体は動くので、update() / draw() の構造でアプリを回せます。 2.8.7 で「通常モードと同じフレームループを回す修正」が入っていることからも、その意図が分かります。
どういうときに使うか
1. 自動テスト
例えば「60フレームだけ回して、変数の状態を確認する」など、画面なしでロジックだけ検証したい場合に向きます。
2. サーバ・CI・SSH環境での検証
画面を出せない環境でも、Pyxelのループ構造をそのまま使ってテストしやすくなります。
3. 生成処理
内部データ生成やフレーム進行を伴う処理を、表示なしで回したい場合に向きます。
draw() は不要なのか
APIとしては run(update, draw) の2つが必要です。
そのためヘッドレスでも draw() 自体は用意します。
ただし表示されないので、実際には空でも構いません。
def draw():
pass
注意点
「画面がないだけ」で、通常実行と完全に同じ用途ではない
ヘッドレスは便利ですが、表示確認や実際の見た目検証には当然向きません。
あくまで
- ロジック確認
- 自動実行
- テスト用途
が中心です。
比較的新しい機能
2.7.10 以降の機能なので、古いPyxelでは使えません。
また 2.8.5・2.8.7 でも関連修正が入っているため、使うなら比較的新しい版のほうが安心です。
ここまで理解できれば十分なこと
-
headless=Trueで画面なし実行できる - 比較的新しい機能
- テストや自動処理向け
-
run(update, draw)の構造自体はそのまま使う
より細かな運用は init() のAPIと変更履歴を見れば十分です。
まとめ
Pyxelのウィンドウ制御まわりは、実際には pyxel.init() を中心に整理すると理解しやすいです。 init() で内部画面サイズ・タイトル・FPS・終了キー・表示倍率・キャプチャ設定・ヘッドレス有無を決め、必要に応じて title()・fullscreen()・resize() を呼ぶ、という形です。 resize() は2.9.0追加、headless は2.7.10追加です。
整理するとこうなります。
-
画面を出す
pyxel.init()+pyxel.run() -
最初の画面サイズを決める
pyxel.init(width, height, ...) -
FPSを決める
pyxel.init(..., fps=60) -
実行中にサイズ変更する
pyxel.resize(width, height) -
タイトルを決める / 変える
pyxel.init(..., title="...")/pyxel.title("...") -
全画面表示にする
pyxel.fullscreen(True) -
画面なしで動かす
pyxel.init(..., headless=True)
このあたりまで理解できたら、次に見るべきはAPIリファレンスの init() / run() / title() / fullscreen() / resize() あたりです。そこから先は「機能の存在を知る」よりも、「自分のアプリにどう組み込むか」の段階に入ります。
🎨 描画(基本)編
Pyxelで最初に使うことが多い「描画の基本」を、逆引き形式でまとめます。
対象にする項目は次の7つです。
- 画面をクリアしたい
- 文字を表示したい
- 図形(線・四角・円)を描きたい
- 画面の一部だけ描きたい
- 色を変更したい(パレット操作)
- 透明色を使いたい
- 点滅やフェードをしたい
まず最初に:Pyxelの描画はどう考えればよいか
Pyxelの描画は、基本的に draw() の中で毎フレームやり直します。
つまり、「前の絵の上に必要なところだけ少しずつ描き足す」というより、そのフレームで見せたい状態を毎回描き直す と考えるのが分かりやすいです。描画に使う代表的なAPIとして、cls、pset、line、rect、rectb、circ、circb、elli、ellib、tri、trib、fill、clip、pal、dither、text が公式APIとして用意されています。
たとえばこのような形です。
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120, title="Draw Basics")
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.rect(20, 20, 40, 30, 8)
pyxel.text(20, 60, "Hello", 7)
App()
このコードでは、毎フレーム次の順番で画面を作っています。
-
cls(0)で画面全体を色0で塗る -
rect(...)で四角を描く -
text(...)で文字を描く
この「下地 → 図形 → 文字」のような順番は重要です。
後に描いたものほど手前に見えるからです。Pyxelの描画APIはこの前提で使うと理解しやすいです。
1. 画面をクリアしたい
概要
画面全体を1色で塗りつぶして、前のフレームの絵を消したいときは pyxel.cls(col) を使います。 cls は「画面を色 col でクリアする」APIです。
詳細解説
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(1) # 背景を色1でクリア
pyxel.text(10, 10, "Background color = 1", 7)
App()
cls() は何をしているのか
cls(1) は、「今の画面を全部、色1で塗り直す」という意味です。
これを毎フレーム最初に行うことで、前のフレームの描画結果を消してから、新しいフレームを描けます。 cls は screen に対する描画メソッドとして定義されています。
なぜ必要なのか
たとえばキャラクターを右に動かしたいのに cls() を使わないと、前の位置に描いた絵が残ってしまい、残像のように見えます。
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.x = 0
pyxel.run(self.update, self.draw)
def update(self):
self.x += 1
def draw(self):
pyxel.cls(0)
pyxel.rect(self.x, 50, 10, 10, 11)
App()
このコードでは、四角が移動しても前の位置は残りません。
それは cls(0) で毎フレーム背景を塗り直しているからです。 rect は指定位置に塗りつぶし矩形を描くAPIです。
どんな場面で使うか
- 背景色を毎フレーム設定したい
- シーン切り替えごとに背景を変えたい
- 残像を残さずにアニメーションしたい
- UIを毎回まっさらな状態から再描画したい
よくある勘違い
cls() を毎フレーム書くと無駄なのでは?
Pyxelの基本的な描画スタイルでは、毎フレーム cls() を呼ぶのが普通です。
毎回「今見せたい絵」を作り直すほうが、状態管理が分かりやすくなります。APIもこの使い方を前提にした構成です。
背景画像を使うなら cls() は不要?
背景画像を毎フレーム全面に描くなら、見た目上は cls() がなくても成立することはあります。
ただし、まず cls() で背景色を決め、その上に背景画像や図形を描くほうが、デバッグ時や一時的に描画を外したときに挙動が分かりやすいです。
ここまで理解できれば十分なこと
-
cls(col)は画面全体を1色で塗る - 毎フレーム最初に呼ぶことが多い
- 残像を消したいときに必須
細かい仕様は cls() のAPIリファレンスを見れば十分です。
2. 文字を表示したい
概要
文字表示には pyxel.text(x, y, s, col, font=None) を使います。
標準フォントでそのまま描けますし、必要ならカスタムフォントも使えます。 text は文字列 s を色 col で (x, y) に描き、font を省略すると標準フォントを使います。カスタムフォントは Font(filename, font_size) で作成し、.bdf / .otf / .ttf / .ttc を読み込めます。
詳細解説
標準フォントで描く
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, "SCORE: 012345", 7)
pyxel.text(10, 20, "HP: 3", 8)
App()
text() の各引数の意味
-
x, y
文字列の描き始め位置 -
s
表示したい文字列 -
col
文字色 -
font
省略時は標準フォント。指定するとカスタムフォント
カスタムフォントを使う
たとえば日本語を表示したい、フォントの見た目を変えたい、文字幅を測りたいといった場合はカスタムフォントが便利です。 Font.text_width(s) で文字列の表示幅も取得できます。
例:.ttf ファイルを使う場合の構成
project/
├── main.py
└── fonts/
└── PixelMplus12-Regular.ttf
import pyxel
class App:
def __init__(self):
pyxel.init(200, 120)
self.font = pyxel.Font("fonts/PixelMplus12-Regular.ttf", 12)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, "こんにちは", 7, font=self.font)
pyxel.text(10, 30, "設定メニュー", 10, font=self.font)
App()
実務でよくやること
UIラベル
STARTOPTIONSLOAD GAME
ステータス表示
SCOREHPTIME
メッセージ表示
- チュートリアル文
- システムメッセージ
- ノベル風テキスト
中央揃えしたいとき
標準フォントだけで厳密な文字幅を取りたいときは、文字数ベースでざっくり計算することもありますが、カスタムフォントなら text_width() を使うのが確実です。 Font.text_width() は文字列の表示幅をピクセル単位で返します。
msg = "GAME OVER"
x = (pyxel.width - len(msg) * 4) // 2 # 標準フォントでの簡易計算例
pyxel.text(x, 50, msg, 8)
カスタムフォントなら次のようにできます。
msg = "設定"
w = self.font.text_width(msg)
x = (pyxel.width - w) // 2
pyxel.text(x, 50, msg, 7, font=self.font)
よくある勘違い
text() は日本語も標準で全部出せる
標準フォントは基本的にレトロ用途の簡易フォントです。
日本語や凝ったフォント表現をしたいなら、.ttf や .otf などのカスタムフォントを使うほうが現実的です。カスタムフォント対応形式はAPIに明記されています。
テキストの背景色も同時に指定できる
text() 自体に背景色引数はありません。
背景が欲しいなら、先に rect() で背景の帯やウィンドウを描き、その上に text() を重ねます。
ここまで理解できれば十分なこと
- 文字表示は
text() - 標準フォントだけでも使える
-
.bdf/.otf/.ttf/.ttcの外部フォントも使える - 背景が必要なら図形と組み合わせる
細かい仕様やフォント計測は Font と text() のAPIリファレンスを見れば十分です。
3. 図形(線・四角・円)を描きたい
概要
Pyxelには基本図形用のAPIが最初から用意されています。
-
pset(x, y, col)
1ピクセル描く -
line(x1, y1, x2, y2, col)
線を描く -
rect(x, y, w, h, col)
塗りつぶし四角 -
rectb(x, y, w, h, col)
四角の枠線 -
circ(x, y, r, col)
塗りつぶし円 -
circb(x, y, r, col)
円の枠線 -
elli(x, y, w, h, col)
塗りつぶし楕円 -
ellib(x, y, w, h, col)
楕円の枠線 -
tri(x1, y1, x2, y2, x3, y3, col)
塗りつぶし三角 -
trib(...)
三角の枠線 -
fill(x, y, col)
同色領域の塗りつぶし
これらはすべて公式APIです。
詳細解説
線を描く
pyxel.line(10, 10, 100, 30, 7)
これは (10,10) から (100,30) まで、色7の線を引くという意味です。 line は開始座標と終了座標を指定するAPIです。
四角を描く
pyxel.rect(20, 20, 40, 30, 8)
pyxel.rectb(70, 20, 40, 30, 7)
-
rectは塗りつぶし -
rectbは枠線だけ
rect / rectb は (x, y) を左上として、幅 w、高さ h の矩形を描きます。
円を描く
pyxel.circ(40, 80, 15, 10)
pyxel.circb(90, 80, 15, 7)
-
circは塗りつぶし円 -
circbは円周だけ
こちらは (x, y) が中心で、r が半径です。 APIでも中心座標と半径で定義されています。
まとめて描く例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.pset(5, 5, 7)
pyxel.line(10, 10, 60, 10, 7)
pyxel.rect(10, 20, 30, 20, 8)
pyxel.rectb(50, 20, 30, 20, 7)
pyxel.circ(25, 70, 12, 9)
pyxel.circb(70, 70, 12, 7)
pyxel.elli(100, 20, 30, 18, 11)
pyxel.ellib(100, 50, 30, 18, 7)
pyxel.tri(120, 90, 140, 60, 150, 100, 13)
pyxel.trib(90, 90, 100, 70, 110, 100, 7)
App()
実務でどう使うか
UIの土台
- ボタン背景に
rect - 選択枠に
rectb - 注目演出に
line
簡易グラフやメーター
- HPバーを
rect - 枠を
rectb - 線グラフの一部を
line
仮素材・デバッグ表示
- 当たり判定枠を
rectb - 当たり判定中心点を
pset - 敵の視野や範囲を
circ
fill() は何に使うのか
fill(x, y, col) は、その座標からつながっている「同じ色の領域」を別色で塗りつぶします。
ペイントソフトのバケツ塗りに近い操作です。 APIでも「(x, y) と同じ色でつながっている領域を色 col で塗る」と説明されています。
ここまで理解できれば十分なこと
- 線・四角・円・楕円・三角は標準APIで描ける
- 塗りつぶし版と枠線版が分かれている
- UI、デバッグ、仮素材作りにかなり使える
細かな引数は図形系APIのリファレンスを見れば十分です。
4. 画面の一部だけ描きたい
概要
画面全体ではなく、一部分だけに描画を制限したいときは clip(x, y, w, h) を使います。
clip() を引数なしで呼ぶと、画面全体に戻ります。 clip は「描画領域を指定し、引数なしで全画面に戻す」APIです。
詳細解説
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
# まず枠を描く
pyxel.rectb(20, 20, 60, 40, 7)
# この範囲の中にだけ描けるようにする
pyxel.clip(20, 20, 60, 40)
# はみ出す大きな図形を描く
pyxel.rect(0, 0, 100, 60, 8)
pyxel.circ(70, 40, 30, 10)
# クリップ解除
pyxel.clip()
pyxel.text(10, 70, "Only inside the box", 7)
App()
何が起きているのか
このコードでは、clip(20, 20, 60, 40) を呼んだあとに描いた図形は、
見た目上、その範囲の中に入っている部分だけ表示されます。
rect() や circ() 自体は大きく描かれていますが、画面に出るのはクリップ領域内だけ です。 APIでも「画面の描画領域を (x, y) から幅 w、高さ h で設定する」とされています。
どういうときに使うのか
スクロールする一覧
設定項目一覧やファイル一覧の表示領域を限定したいときに便利です。
ウィンドウ内部だけ描画
ウィンドウ枠の外に文字や画像がはみ出ないようにしたいときに便利です。
ミニマップやサブビュー
画面の一角だけに別表示領域を作りたいときに使えます。
よくある実務例
スクロールリスト
pyxel.rectb(10, 10, 100, 60, 7)
pyxel.clip(10, 10, 100, 60)
for i in range(20):
pyxel.text(14, 14 + i * 10 - scroll_y, f"ITEM {i}", 7)
pyxel.clip()
この例では、項目数が多くても枠の外には表示されません。
clip() を戻し忘れるとどうなるか
意外とよくあるミスですが、その後の描画も全部クリップされたままになります。
そのため、使い終わったら必ず pyxel.clip() を呼んで解除する癖をつけると安全です。 APIでも引数なしで全画面に戻ることが説明されています。
ここまで理解できれば十分なこと
-
clip(x, y, w, h)で描画範囲を制限できる - 枠付きウィンドウやスクロール領域に便利
-
clip()で必ず解除する
細かい挙動は clip() のAPIリファレンスを見れば十分です。
5. 色を変更したい(パレット操作)
概要
Pyxelは16色パレット前提で動きます。
色番号そのものを指定して描くのが基本ですが、描画時に「この色番号を別の色番号として扱う」こともできます。それが pal(col1, col2) です。 pal() は「描画時に色 col1 を col2 に置き換える」APIで、引数なしで元に戻せます。Pyxelの仕様として16色パレットが案内されています。
詳細解説
普通の色指定
pyxel.rect(10, 10, 30, 20, 8) # 色8の四角
pyxel.text(10, 40, "RED?", 8) # 色8の文字
この場合、描画色はそのまま色8です。
パレット置換を使う
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
# 左:通常
pyxel.rect(10, 20, 30, 20, 8)
pyxel.text(10, 45, "normal", 7)
# 右:描画時だけ 8 -> 11 に置換
pyxel.pal(8, 11)
pyxel.rect(70, 20, 30, 20, 8)
pyxel.pal()
pyxel.text(70, 45, "8 -> 11", 7)
App()
何が便利なのか
これは「元の絵データは変えずに、見た目だけ差し替える」ための機能です。
具体例
- キャラの色違いを作る
- 選択中だけ色味を変える
- ダメージ中だけ赤っぽくする
- 夜ステージ風に全体色を変える
実務的な考え方
画像を複製しなくてよい
たとえば、同じスプライトを
- 通常色
- 毒状態
- 氷状態
で表示したいとき、画像を3枚用意しなくても、描画時だけ pal() で見た目を変えられます。 APIは「描画時の置換」であり、画像データ自体を書き換えるわけではありません。
効果が残るので戻し忘れに注意
pal(8, 11) のあとに pal() を呼ばないと、その後の描画でも置換が続きます。
これは便利でもあり、バグの原因にもなりやすい点です。
パレットそのものの表示色を変えたい場合
Pyxelには DEFAULT_COLORS や色定数もありますが、通常のゲーム開発でまず使うのは pal() による置換です。色定数として COLOR_BLACK などが用意されていることもAPIに含まれています。
ここまで理解できれば十分なこと
- Pyxelは16色前提
- 普通は色番号で描く
-
pal(col1, col2)で描画時だけ置換できる - 使い終わったら
pal()で戻す
細かな活用は pal() と色定数のAPIリファレンスを見れば十分です。
6. 透明色を使いたい
概要
透明色は主に blt() や bltm() の colkey 引数で指定します。
colkey に指定した色番号は「描かない色」として扱われます。 blt / bltm はどちらも colkey を受け取り、「transparent color」として使う仕様です。
詳細解説
まず前提:透明色はどこで使うのか
図形APIの rect() や circ() に透明色という考え方はありません。
透明色が特に重要になるのは、画像を貼るとき です。
つまり、スプライトやアイコンを背景の上に重ねるときです。 blt() は画像バンクまたは Image からコピーして描くAPIです。
例:背景色0を透明扱いにしてスプライトを描く
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
# 画像バンク0に簡単な絵を作る
pyxel.images[0].set(0, 0, [
"00033000",
"00333300",
"033cc330",
"03cccc30",
"03cccc30",
"033cc330",
"00333300",
"00033000",
])
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(1)
# 背景の飾り
pyxel.rect(30, 30, 60, 40, 8)
# 色0を透明として描画
pyxel.blt(50, 40, 0, 0, 0, 8, 8, colkey=0)
App()
このコードで起きていること
-
images[0].set(...)で、色番号の文字列から簡単な絵を作っている -
blt(..., colkey=0)で、その絵を描くとき、色0の部分は描かない - そのため、背景の四角が透けて見える
Image.set() は各文字を16進数の色番号として解釈するAPIです。 blt() の colkey は「その色を透明として扱う」と公式に説明されています。
外部画像ファイルを使う場合
たとえば .png を読み込んで使うこともできます。 Image.load() は .png / .gif / .jpeg を読み込めます。
例:ファイル構成
project/
├── main.py
└── assets/
└── player.png
pyxel.images[0].load(0, 0, "assets/player.png")
pyxel.blt(40, 40, 0, 0, 0, 16, 16, colkey=0)
この場合、player.png の中で色0に相当する部分を透明扱いにできます。
「背景色だけ抜いてキャラ本体を重ねる」用途でよく使います。 Image.load() の対応形式は API に明記されています。
bltm() でも透明色が使える
タイルマップ描画でも colkey が使えます。
これは特定色を抜きながらマップを重ねたいときに便利です。 bltm() も colkey による透明色指定をサポートしています。
よくある勘違い
透明色を指定しなくても背景色は勝手に透ける
透けません。
透明にしたい色は、colkey として明示的に指定する必要があります。 APIでも colkey を省略した場合は「透明なし」です。
透明色はアルファチャンネルと同じ
同じではありません。
colkey は「この色番号なら描かない」という単純な仕組みです。
PNGの一般的な半透明アルファとは考え方が違います。
ここまで理解できれば十分なこと
- 透明色は主に
blt()/bltm()で使う -
colkey=色番号で指定する - その色の部分だけ描かれない
-
.png/.gif/.jpeg読み込み時にも活用できる
さらに詳しい画像運用は Image.load()、blt()、bltm() のAPIリファレンスを見ると十分です。
7. 点滅やフェードをしたい
概要
やり方は大きく2つあります。
-
点滅
表示する/しない、あるいは色を切り替える -
フェード
pal()やdither()を使って徐々に見え方を変える
Pyxelには dither(alpha) があり、0.0-1.0 の範囲で擬似透過をかけられます。 APIでも dither() は「pseudo-transparency」を適用するものと説明されています。
詳細解説
点滅:一番簡単な方法
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
if pyxel.frame_count % 20 < 10:
pyxel.text(40, 50, "PRESS START", 7)
App()
何が起きているのか
pyxel.frame_count は経過フレーム数です。
この例では、20フレームごとに区切って、その前半だけ文字を描いています。
つまり、
- 10フレーム表示
- 10フレーム非表示
を繰り返しているので、点滅に見えます。 frame_count は現行Pyxelの変数として公開されています。
色切り替えで点滅させる
if pyxel.frame_count % 20 < 10:
col = 7
else:
col = 8
pyxel.text(40, 50, "WARNING", col)
こちらは消すのではなく、色を切り替えて強調しています。
フェード:dither() を使う
import pyxel
import math
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
alpha = (math.sin(pyxel.frame_count * 0.1) + 1) / 2 # 0.0〜1.0
pyxel.dither(alpha)
pyxel.rect(40, 40, 80, 40, 7)
pyxel.dither(1.0)
App()
dither() は何をしているのか
dither(alpha) は、本物のアルファブレンドではなく、擬似的な透明感 を作る機能です。
たとえば alpha=0.5 なら、ドットの間引きで半透明っぽく見せます。 APIでも「pseudo-transparency」と書かれています。
このため、レトロゲーム風の見た目と相性が良いです。
フェードアウトに使う例
fade = max(0.0, 1.0 - self.timer / 60)
pyxel.dither(fade)
pyxel.rect(0, 0, pyxel.width, pyxel.height, 0)
pyxel.dither(1.0)
このように、黒い矩形を dither() 付きで全面に描くと、
徐々に暗くなる「フェードアウト」っぽい表現ができます。
pal() で色を変えてフェードっぽく見せる
本格的な階調変化ではありませんが、パレット置換を段階的に変えると、
- 明るい色 → 暗い色
- 鮮やかな色 → グレー寄り
のようにして「暗転」や「状態異常演出」のような見た目を作れます。 pal() は描画時置換なので、画像を作り直さずに見え方を変える用途に向いています。
どんな場面で使うのか
点滅
- PRESS START
- カーソル点滅
- 危険状態の警告
- 無敵時間の演出
フェード
- シーン切り替え
- ダメージ時の画面演出
- 画面暗転
- UIの出現・消失
よくある注意点
dither() の設定を戻し忘れる
dither() も状態として残るので、使い終わったら pyxel.dither(1.0) などで戻しておくと安全です。 API上、alpha=1.0 が完全不透明、0.0 が透明です。
点滅周期が速すぎる
frame_count % 4 などだと速すぎて見づらいことがあります。
30FPS環境なら、10〜20フレーム単位くらいから試すと見やすいです。PyxelのデフォルトFPSは30です。
ここまで理解できれば十分なこと
- 点滅は「描く/描かない」または色切り替えで作れる
- フェードは
dither()が便利 -
pal()を使った色変化演出もある - どちらも描画状態を戻し忘れないことが大事
これ以上の細かい調整は dither()、pal()、frame_count をAPIリファレンスで確認すれば十分です。
まとめ
Pyxelの基本描画は、かなり少ないAPIで組み立てられます。
まず覚えるべきなのは次のあたりです。
-
画面全体を塗る
cls(col) -
文字を描く
text(x, y, s, col, font=None) -
図形を描く
pset,line,rect,rectb,circ,circb,elli,ellib,tri,trib,fill -
一部だけ描く
clip(x, y, w, h) -
色の見え方を変える
pal(col1, col2) -
透明色を使う
blt(..., colkey=0)/bltm(..., colkey=0) -
点滅やフェードをする
frame_count、pal()、dither(alpha)
ここまで理解できたら、次に見るべきは text()、図形系API、clip()、pal()、dither()、blt() のAPIリファレンスです。そこから先は、「存在を知る段階」ではなく、「自分のUIやゲーム画面にどう組み込むか」を詰める段階に入ります。
🧩 画像・スプライト編
Pyxelで画像やスプライトを扱うときに最初に知りたくなる項目を、逆引き形式でまとめます。
対象にする項目は次の7つです。
- スプライトを表示したい
- 画像の一部を切り出して表示したい
- 画像を反転(左右・上下)したい
- スプライトをアニメーションさせたい
- 画像を回転・拡大縮小したい(疑似的に)
- 画像を重ねて表示したい
- 複数キャラを効率よく管理したい
まず最初に:Pyxelで画像を表示するときの基本
Pyxelで画像を表示するときに中心になるのは pyxel.blt() です。
blt() は「画像バンクのどの位置から、何ピクセル分を切り出して、画面のどこに描くか」を指定する関数です。
画像バンクは 0〜2 が使え、あるいは Image インスタンスを直接渡すこともできます。さらに、colkey で透明色、rotate で回転、scale で拡大縮小ができます。 w と h を負数にすると反転です。
基本形は次の通りです。
pyxel.blt(x, y, img, u, v, w, h, colkey=None, rotate=0, scale=1)
各引数の意味はこうです。
-
x, y
画面のどこに描くか -
img
画像バンク番号(0〜2)またはImageインスタンス -
u, v
画像のどこから切り出すか -
w, h
切り出す幅と高さ -
colkey
透明にしたい色番号 -
rotate
回転角度(度数) -
scale
拡大縮小倍率
つまり、Pyxelのスプライト表示は「画像ファイルそのものをそのまま貼る」というより、画像の中の一部分を必要に応じて切り出して描く という考え方です。APIの説明でも blt() は「画像バンクの (u, v) から (w, h) の領域を (x, y) にコピーする」と定義されています。
1. スプライトを表示したい
概要
スプライトを表示したいときは pyxel.blt() を使います。
「スプライト」という言い方をしていますが、PyxelのAPIとしてはあくまで画像の一部分を画面へコピーして描く処理です。
詳細解説
まずは一番単純な例です。
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
# 画像バンク0の左上に 8x8 の簡単な絵を作る
pyxel.images[0].set(0, 0, [
"00033000",
"00333300",
"033cc330",
"03cccc30",
"03cccc30",
"033cc330",
"00333300",
"00033000",
])
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(1)
pyxel.blt(40, 30, 0, 0, 0, 8, 8, colkey=0)
App()
このコードで何をしているのか
pyxel.images[0].set(...)
画像バンク0に、8×8ピクセルの絵を直接書き込んでいます。
Image.set() は各文字を16進数の色番号として解釈します。
たとえば "0" は色0、"3" は色3、"c" は色12 です。APIの例でも ["0123", "4567", "89ab", "cdef"] のような書き方が示されています。
pyxel.blt(40, 30, 0, 0, 0, 8, 8, colkey=0)
これは、
- 画面の
(40, 30)に - 画像バンク
0の -
(0, 0)から - 幅8、高さ8の領域を描く
- 色
0は透明にする
という意味です。blt() の colkey は透明色を指定する引数です。
外部画像ファイルを使いたい場合
たとえば .png ファイルを使いたい場合は、Image.load() を使います。
読み込める形式は .png / .gif / .jpeg です。
例:ファイル構成
project/
├── main.py
└── assets/
└── player.png
読み込みと表示
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.images[0].load(0, 0, "assets/player.png")
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.blt(40, 30, 0, 0, 0, 16, 16, colkey=0)
App()
実務でどう考えるか
スプライト表示は「ゲーム画面にキャラクターを描く」だけでなく、次のような用途でも使います。
- キャラクター
- 敵
- アイテム
- ボタンアイコン
- カーソル
- 顔グラフィック
つまり、「動くキャラ専用」ではなく、画面に画像を出したい場面のほとんどで使います。
ここまで理解できれば十分なこと
- スプライト表示の中心は
blt() - 画像は
Image.set()でも.png/.gif/.jpeg読み込みでも用意できる - 透明色は
colkey - スプライトとは「画像の一部を画面へコピーして描くこと」
細かい引数仕様は blt() と Image.load() のAPIリファレンスを見れば十分です。
2. 画像の一部を切り出して表示したい
概要
これは blt() の最も典型的な使い方です。
u, v, w, h を使って、1枚の画像の中から必要な部分だけ切り出して描きます。 APIでも blt() は「画像バンクの (u, v) から (w, h) の領域をコピーする」と説明されています。
詳細解説
どういうときに必要か
たとえば、1枚の画像に複数の絵が並んでいることがあります。
[主人公待機][主人公歩行1][主人公歩行2][敵][アイテム][UI]
このような画像を一般に「スプライトシート」的に使います。
その中の必要な部分だけを blt() で切り出して表示します。
例:16x16のキャラを切り出す
pyxel.blt(20, 20, 0, 0, 0, 16, 16, colkey=0)
これは、
- 画像バンク0の
- 左上
(0,0)から -
16x16を切り出して - 画面
(20,20)に表示
という意味です。
例:同じ画像の隣の絵を表示
pyxel.blt(40, 20, 0, 16, 0, 16, 16, colkey=0)
今度は u=16, v=0 なので、画像の「右に16ピクセルずれた場所」から切り出します。
つまり、1枚の画像の中の別のスプライトを描いています。
外部ファイルを使う場合のイメージ
ファイル構成
project/
├── main.py
└── assets/
└── spritesheet.png
読み込み
pyxel.images[0].load(0, 0, "assets/spritesheet.png")
切り出し表示
# 1コマ目
pyxel.blt(20, 20, 0, 0, 0, 16, 16, colkey=0)
# 2コマ目
pyxel.blt(40, 20, 0, 16, 0, 16, 16, colkey=0)
# 3コマ目
pyxel.blt(60, 20, 0, 32, 0, 16, 16, colkey=0)
なぜ1枚にまとめるのか
ここはかなり重要です。
1枚にまとめる理由は次の通りです。
1. 管理しやすい
キャラごと・動作ごとに .png を大量に分けるより、1枚にまとめたほうが探しやすいです。
2. 描画指示が分かりやすい
「このキャラの歩行2は (32,0) から 16x16」のように整理できます。
3. アニメーションと相性が良い
連番のように横に並べておくと、u をずらすだけでアニメーションできます。
よくある失敗
スプライトごとに画像ファイルを分けすぎる
たとえば
player_idle.png
player_walk1.png
player_walk2.png
player_walk3.png
のようにすると、小規模なら動きますが、キャラ数や動作数が増えると管理が煩雑になりやすいです。
Pyxelの blt() はもともと「画像の一部を切り出す」設計なので、1枚にまとめる運用のほうが自然です。
ここまで理解できれば十分なこと
-
blt()は画像の一部切り出し表示が基本 -
u, v, w, hが切り出し範囲 -
.png/.gif/.jpegを読み込んで使える - スプライトを1枚にまとめると管理しやすい
細かな設計は blt() と Image.load() のAPIを見れば十分です。
3. 画像を反転(左右・上下)したい
概要
Pyxelでは blt() の w または h を負数にすると反転できます。
APIでも「Negative w/h flips the image」と明記されています。つまり、
-
wを負にする → 左右反転 -
hを負にする → 上下反転
です。
詳細解説
左右反転の例
# 通常
pyxel.blt(20, 20, 0, 0, 0, 16, 16, colkey=0)
# 左右反転
pyxel.blt(50, 20, 0, 0, 0, -16, 16, colkey=0)
上下反転の例
# 上下反転
pyxel.blt(80, 20, 0, 0, 0, 16, -16, colkey=0)
左右・上下の両方を反転
pyxel.blt(110, 20, 0, 0, 0, -16, -16, colkey=0)
何が便利なのか
これはかなり実用的です。
たとえば横向きのキャラ画像が1枚しかなくても、左右反転で「左向き」「右向き」の両方を表示できます。
具体例
- 右向きの歩行グラしか作っていない
→ 左向きはw=-16 - 敵の向きで見た目を変えたい
→ 同じ画像を反転して使う - ミラー演出をしたい
→ 反転描画で対応
よくある実務パターン
if facing_right:
pyxel.blt(x, y, 0, 0, 0, 16, 16, colkey=0)
else:
pyxel.blt(x, y, 0, 0, 0, -16, 16, colkey=0)
このように「向きのフラグ」に応じて w を切り替えるだけで、左右反転ができます。
わざわざ左向き画像を用意しなくていいのか
多くのケースでは不要です。
ドット絵で左右ほぼ対称に扱えるキャラなら、1枚だけ作って反転表示したほうが効率的です。
ただし、左右で完全に見た目が違う場合、たとえば片側だけ武器を持っている、服の模様が左右非対称などの場合は、別画像を用意したほうが自然なこともあります。
ここまで理解できれば十分なこと
-
wを負にすると左右反転 -
hを負にすると上下反転 - 同じ画像を使い回せるので管理が楽
- 向きだけ変えたいときに特に便利
これ以上の細かな挙動は blt() のAPIリファレンスを見れば十分です。
4. スプライトをアニメーションさせたい
概要
最も基本的なやり方は、スプライトシートの切り出し位置を時間に応じて変える ことです。
Pyxel自体に「アニメーション専用クラス」があるわけではなく、blt() の u, v をフレームごとに変えて実現するのが基本です。 blt() が画像の任意領域を描くAPIであることから、この運用が自然です。
詳細解説
一番単純な例
たとえば横に3コマ並んだ歩行アニメを使うとします。
[歩行1][歩行2][歩行3]
各コマが16x16なら、切り出し位置はこうなります。
- 1コマ目:
u=0 - 2コマ目:
u=16 - 3コマ目:
u=32
コード例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.images[0].load(0, 0, "assets/player_walk.png")
self.x = 40
pyxel.run(self.update, self.draw)
def update(self):
self.x += 1
def draw(self):
pyxel.cls(0)
frame = (pyxel.frame_count // 6) % 3
u = frame * 16
pyxel.blt(self.x, 40, 0, u, 0, 16, 16, colkey=0)
App()
このコードで何をしているのか
pyxel.frame_count
経過フレーム数です。毎フレーム増えていきます。
(pyxel.frame_count // 6) % 3
これは、
- 6フレームごとに1コマ進める
- 3コマでループする
という意味です。
つまり、
- 0〜5フレーム → 1コマ目
- 6〜11フレーム → 2コマ目
- 12〜17フレーム → 3コマ目
- 18〜23フレーム → また1コマ目
のように繰り返します。
止まっているときと歩いているときで切り替えたい
実際のゲームでは、入力がないときは待機モーション、移動中だけ歩行アニメにしたいことが多いです。
if moving:
frame = (pyxel.frame_count // 6) % 3
else:
frame = 0
このようにすると、止まっているときは1コマ目で固定、動いているときだけアニメーションします。
外部ファイル例
ファイル構成
project/
├── main.py
└── assets/
└── player_walk.png
画像イメージ
player_walk.png の中に、16x16 の歩行コマが横に3つ並んでいる前提です。
よくある失敗
1. アニメ速度が速すぎる
frame = pyxel.frame_count % 3 のようにすると、毎フレーム変わって速すぎることがあります。
そのため // 4 や // 6 のように速度を落とすことが多いです。
2. アニメコマ数をコードに埋め込みすぎる
小規模ならよいのですが、キャラが増えると「何コマあるか」「1コマの幅はいくつか」をデータとして持ったほうが管理しやすくなります。これは後述の「複数キャラを効率よく管理したい」に繋がります。
ここまで理解できれば十分なこと
- アニメーションは
u/vを時間で変えて作る -
frame_countと//、%の組み合わせが基本 - 歩行・待機などは状態で切り替える
- 1枚の画像に複数コマを並べると扱いやすい
これ以上の細かな制御は、自分のゲーム構造に合わせて blt() と frame_count を使って詰めれば十分です。APIとしてはここが本体です。
5. 画像を回転・拡大縮小したい(疑似的に)
概要
Pyxelでは blt() と bltm() に rotate と scale があり、画像やタイルマップを回転・拡大縮小できます。
このオプションは Pyxel 2.2.0 で追加されました。 APIでも rotate は角度(度数)、scale は倍率として定義されており、どちらもコピー領域の中心を基準に適用されます。
詳細解説
回転の例
pyxel.blt(60, 40, 0, 0, 0, 16, 16, colkey=0, rotate=45)
これは、16x16の画像を45度回転して描きます。
拡大の例
pyxel.blt(60, 40, 0, 0, 0, 16, 16, colkey=0, scale=2)
これは、元が16x16でも、見た目としては2倍に拡大して描きます。
回転+拡大を同時に使う
pyxel.blt(60, 40, 0, 0, 0, 16, 16, colkey=0, rotate=30, scale=1.5)
動かしてみる例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.images[0].load(0, 0, "assets/icon.png")
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
angle = pyxel.frame_count * 3
scale = 1 + (pyxel.frame_count % 20) / 20
pyxel.blt(80, 60, 0, 0, 0, 16, 16, colkey=0, rotate=angle, scale=scale)
App()
何に使うのか
回転
- コインや魔法陣の演出
- 矢印やポインタ
- エフェクト系の飾り
拡大縮小
- 出現アニメーション
- ボタン選択時の強調
- 取得アイテムのポップ感
「疑似的に」と書いている理由
これは3Dエンジンのような本格変形ではなく、2D画像のコピー時に回転と倍率をかける 機能です。
ただし、UI演出や軽いアクション演出にはかなり便利です。
また、タイルマップ側でも bltm() に同じ rotate / scale があるので、背景や一部領域を動的に変形することもできます。
注意点
元画像の中心を基準に回る
APIの説明では、回転・倍率は「copy region の center を基準にする」とされています。
そのため、「左上が基準で回る」のではなく、切り出した領域の中心を軸にして回る と理解するとズレにくいです。
ドット絵らしい見え方が崩れることもある
回転や拡大を使うと、ドット絵本来のカチッとした見え方とは少し変わることがあります。
そのため、常用するというより「ここぞという演出」に使うのが向いています。
ここまで理解できれば十分なこと
-
blt()/bltm()はrotateとscaleに対応 - 回転角は度数
- 倍率は
1が等倍 - 中心基準で変形される
- 演出用途に向いている
細かい挙動は blt() / bltm() のAPIリファレンスを見れば十分です。
6. 画像を重ねて表示したい
概要
Pyxelの描画は、後に描いたものほど手前に見える というシンプルな仕組みです。
そのため、画像を重ねたい場合は「描く順番」を意識します。 blt() は他の描画APIと同様に、その時点で画面へ描く処理なので、順序がそのまま重なり順になります。
詳細解説
一番簡単な例
pyxel.blt(30, 30, 0, 0, 0, 16, 16, colkey=0) # 背景側
pyxel.blt(34, 34, 0, 16, 0, 16, 16, colkey=0) # 手前側
この場合、2回目に描いたほうが上に見えます。
どういう場面で使うのか
1. キャラ+影
先に影を描いて、その上にキャラを描く
pyxel.blt(x+2, y+8, 0, 32, 0, 16, 8, colkey=0)
pyxel.blt(x, y, 0, 0, 0, 16, 16, colkey=0)
2. キャラ+装備
本体スプライトの上に武器や帽子だけ重ねる
3. 顔グラ+表情パーツ
ベースの顔に、目や口だけ別スプライトを重ねる
4. UI背景+アイコン+文字
ボタンの背景 → アイコン → 文字の順で描く
透明色が重要になる
画像を重ねるとき、上の画像の背景まで全部描かれてしまうと、下の画像が見えなくなります。
そのため、colkey で背景色を透明にして、必要な部分だけ重ねることが重要です。 colkey は透明色指定のための引数です。
実例:影と本体を重ねる
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
# 本体
pyxel.images[0].set(0, 0, [
"00033000",
"00333300",
"033cc330",
"03cccc30",
"03cccc30",
"033cc330",
"00333300",
"00033000",
])
# 影
pyxel.images[0].set(16, 0, [
"00000000",
"00000000",
"00000000",
"00055500",
"00555550",
"00055500",
"00000000",
"00000000",
])
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(1)
pyxel.blt(50, 50, 0, 16, 0, 8, 8, colkey=0) # 影
pyxel.blt(50, 46, 0, 0, 0, 8, 8, colkey=0) # 本体
App()
実務で大事な考え方
「1枚の完成画像」を用意するより、必要に応じてパーツを重ねたほうが、差分表現がしやすいです。
たとえば、
- 同じ本体に帽子だけ変える
- 同じ顔に口だけ変える
- 装備だけ差し替える
といったことが簡単になります。
ここまで理解できれば十分なこと
- 重ね順は描画順で決まる
- 後に描いたほうが手前
-
colkeyで透明色を使うと重ねやすい - 影、装備、表情差分などに向いている
細かな描画順設計は、自分の画面構成に応じて blt() の順番を整理すれば十分です。APIとしては単純です。
7. 複数キャラを効率よく管理したい
概要
ここは「Pyxel専用の特別機能がある」というより、blt() を呼ぶためのデータ構造を整理する のがポイントです。
キャラが1体なら直接 pyxel.blt(...) を書いてもよいですが、複数キャラになると、位置・向き・現在フレーム・使う画像領域をまとめて持ったほうが圧倒的に管理しやすくなります。Pyxelの blt() が「画像のどこを描くか」を細かく指定するAPIだからこそ、この整理が効きます。
詳細解説
ダメになりやすい書き方
pyxel.blt(player_x, player_y, 0, 0, 0, 16, 16, colkey=0)
pyxel.blt(enemy1_x, enemy1_y, 0, 16, 0, 16, 16, colkey=0)
pyxel.blt(enemy2_x, enemy2_y, 0, 16, 0, 16, 16, colkey=0)
pyxel.blt(npc_x, npc_y, 0, 32, 0, 16, 16, colkey=0)
これでも動きますが、キャラが増えると同じようなコードが大量に並びます。
さらに、向きやアニメーションを入れ始めると管理が急に難しくなります。
おすすめの考え方
キャラ1体を、たとえば次のような情報のまとまりとして持ちます。
-
x, y
位置 -
u, v
画像上の基準位置 -
w, h
スプライトサイズ -
frame
現在アニメコマ -
facing
向き -
kind
プレイヤー / 敵 / NPC など
例:辞書で持つ
characters = [
{"x": 20, "y": 40, "u": 0, "v": 0, "w": 16, "h": 16, "frame": 0, "facing": 1},
{"x": 60, "y": 40, "u": 48, "v": 0, "w": 16, "h": 16, "frame": 0, "facing": -1},
]
描画処理
for ch in characters:
u = ch["u"] + ch["frame"] * ch["w"]
w = ch["w"] if ch["facing"] == 1 else -ch["w"]
pyxel.blt(ch["x"], ch["y"], 0, u, ch["v"], w, ch["h"], colkey=0)
この書き方の何がよいのか
1. キャラを増やしやすい
characters に要素を追加するだけで増やせます。
2. 同じ描画ロジックを使い回せる
プレイヤーも敵もNPCも、基本的に同じ blt() の流れで描けます。
3. 向きやアニメを共通化しやすい
frame や facing をデータとして持つので、描画側の条件分岐が減ります。
もう少し実践的な例:クラスで管理
import pyxel
class Character:
def __init__(self, x, y, u, v, w=16, h=16):
self.x = x
self.y = y
self.u = u
self.v = v
self.w = w
self.h = h
self.frame = 0
self.facing = 1
def draw(self):
u = self.u + self.frame * self.w
w = self.w if self.facing == 1 else -self.w
pyxel.blt(self.x, self.y, 0, u, self.v, w, self.h, colkey=0)
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.images[0].load(0, 0, "assets/chars.png")
self.characters = [
Character(20, 40, 0, 0),
Character(60, 40, 48, 0),
Character(100, 40, 96, 0),
]
pyxel.run(self.update, self.draw)
def update(self):
for i, ch in enumerate(self.characters):
ch.frame = (pyxel.frame_count // 8 + i) % 3
def draw(self):
pyxel.cls(0)
for ch in self.characters:
ch.draw()
App()
何が効率化されるのか
この書き方にすると、
- 画像の切り出し位置
- アニメフレーム
- 反転
- 座標
をキャラごとに持てます。
つまり「どのキャラも同じ描画処理だが、持っているデータが違うだけ」という状態にできます。
これが複数キャラ管理の基本です。
実務でさらに整理したくなるもの
キャラ数が増えると、さらに次のような情報も持ちたくなります。
- 当たり判定サイズ
- 移動速度
- 状態(待機、歩行、攻撃、被弾)
- レイヤー順(奥・手前)
- 使用しているスプライトシート名
ここまで来たら、もう「Pyxelの画像表示の使い方」は理解できています。
あとはゲーム側の設計問題なので、ここから先は API リファレンスより、自分の設計方針を詰める段階です。
ここまで理解できれば十分なこと
- 複数キャラ管理の本質は
blt()を呼ぶためのデータ整理 - 位置、画像位置、アニメフレーム、向きをキャラごとに持つ
- 辞書でもクラスでもよい
- 描画ロジックを共通化するとかなり楽になる
Pyxel固有のAPIとしては blt() を理解できていれば十分です。そこから先は設計の話です。
まとめ
Pyxelの画像・スプライトまわりは、実質的には blt() を中心に理解すると整理しやすいです。
blt() は「画像の一部を切り出して、透明色・反転・回転・拡大縮小つきで画面に描く」ための関数です。
画像データは Image.set() で直接書いても、Image.load() で .png / .gif / .jpeg を読み込んでも構いません。 rotate / scale は 2.2.0 で追加されました。
整理するとこうです。
-
スプライトを表示する
blt() -
画像の一部を切り出して表示する
u, v, w, hを指定する -
左右・上下反転する
w/hを負数にする -
アニメーションさせる
u/vをフレームで変える -
回転・拡大縮小する
rotate/scale -
画像を重ねる
描画順を意識する -
複数キャラを管理する
blt()用のデータを構造化する
ここまで理解できたら、次に見るべきは blt()、Image.set()、Image.load() のAPIリファレンスです。そこから先は「機能の存在を知る段階」ではなく、「自分のプロジェクトでどう整理して使うか」の段階に入ります。
🗺 マップ・タイル編
Pyxelでタイルマップを使って地形や背景を作るときに、最初に知りたくなる内容を逆引き形式でまとめます。
対象にする項目は次の6つです。
- タイルマップを表示したい
- マップをスクロールさせたい
- マップをエディタで作りたい
- マップに当たり判定を付けたい
- マップとキャラの衝突判定をしたい
- 大きなマップを分割して管理したい
まず最初に:Pyxelのタイルマップとは何か
Pyxelのタイルマップは、「8x8ピクセルの小さなタイルを並べて、大きな地形や背景を表現する仕組み」です。
画面に直接1ピクセルずつ背景を描くのではなく、“このマスには草タイル”“このマスには壁タイル” のように、タイル単位でマップを持ちます。Pyxelの bltm() は、タイルマップのある範囲を画面へ描く関数で、各タイルは (image_tx, image_ty) という「画像バンク内のタイル座標」で表現されます。
ここで重要なのは、Pyxelのタイルマップは「色の二次元配列」ではなく、画像バンクのどの8x8タイルを置くかの二次元配列 だということです。
つまり、タイルマップに入っているのは「草の色」ではなく「画像バンク上の草タイルの位置」です。Tilemap.pget() は (image_tx, image_ty) を返し、Tilemap.pset() も (image_tx, image_ty) を受け取ります。
1. タイルマップを表示したい
概要
タイルマップ表示の中心になるのは pyxel.bltm() です。
bltm() は「タイルマップのどこから、何タイル分を、画面のどこに描くか」を指定する関数です。API上は、tm にタイルマップ番号 0-7 または Tilemap インスタンスを指定し、u, v, w, h でタイルマップ上の領域を選びます。各タイルは8x8ピクセルです。
詳細解説
一番小さい例はこうです。
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
# 画像バンク0に 8x8 のタイルをいくつか作る
pyxel.images[0].set(0, 0, [
"11111111",
"12222221",
"12222221",
"12222221",
"12222221",
"12222221",
"12222221",
"11111111",
])
pyxel.images[0].set(8, 0, [
"33333333",
"34444443",
"34444443",
"34444443",
"34444443",
"34444443",
"34444443",
"33333333",
])
# タイルマップ0にタイルを並べる
pyxel.tilemaps[0].set(0, 0, [
"0000 0100 0000 0100",
"0100 0000 0100 0000",
"0000 0100 0000 0100",
"0100 0000 0100 0000",
])
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.bltm(20, 20, 0, 0, 0, 4, 4)
App()
このコードで何をしているのか
pyxel.images[0].set(...)
画像バンク0に、8x8タイルを2種類作っています。
1つ目は画像バンク0の (0,0) タイル、2つ目は (1,0) タイルです。
Pyxelでは画像バンク上のタイル位置を (image_tx, image_ty) で扱うので、
8ピクセル右にずらして set(8, 0, ...) すると、画像上では隣のタイルになります。画像データは Image.set() で16進数の色インデックス文字列として書き込めます。
pyxel.tilemaps[0].set(...)
タイルマップ0の (0,0) から、4x4マス分のタイルを設定しています。
ここで書いている "0000" や "0100" は、4桁の16進数で1タイルを表す文字列 です。
前半2桁が image_tx、後半2桁が image_ty です。たとえば "0100" は (1,0)、 "0000" は (0,0) を意味します。Pyxelの Tilemap.set() の説明にも「各タイルは4桁の16進値で (image_tx, image_ty) を表し、スペース区切り」とあります。
pyxel.bltm(20, 20, 0, 0, 0, 4, 4)
これは、
- 画面
(20,20)に - タイルマップ
0の -
(0,0)から - 幅4タイル、高さ4タイル分
を描く、という意味です。bltm() の w / h はピクセル数ではなく、タイルマップ領域の幅・高さ ですが、実際には「タイルを8x8として展開して描く」ので、見た目上は 32x32 ピクセル分が表示されます。各タイルは8x8ピクセルです。
タイルマップはどんなときに向いているか
- 地形つきのアクションゲーム
- RPGのフィールドやダンジョン
- 背景が繰り返しパターンになる画面
- ランチャーやUIの背景パネル
- ブロックを並べるパズル系の盤面
まず理解すべきこと
タイルマップを使い始めるときに、まず頭の中で分けるべきものは2つです。
1. 画像バンク
「草」「壁」「水」などのタイルの絵そのもの
2. タイルマップ
「どこに草を置くか」「どこに壁を置くか」の並び情報
この2つを分けて考えられるようになると、Pyxelのマップ処理がかなり分かりやすくなります。
ここまで理解できれば、あとは bltm() と Tilemap.set() のAPIリファレンスを見れば十分です。
2. マップをスクロールさせたい
概要
やり方はシンプルで、bltm() で描くタイルマップ上の開始位置 u, v を変えます。
つまり、「画面を動かす」のではなく、タイルマップのどの位置を見せるかを変える という考え方です。
Pyxel公式の 10_platformer.py でも scroll_x を持ち、タイルや敵の出現位置をスクロールに合わせて管理しています。
詳細解説
一番単純な横スクロール
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.load("assets/map_sample.pyxres")
self.scroll_x = 0
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btn(pyxel.KEY_RIGHT):
self.scroll_x += 1
if pyxel.btn(pyxel.KEY_LEFT):
self.scroll_x -= 1
def draw(self):
pyxel.cls(0)
pyxel.bltm(0, 0, 0, self.scroll_x, 0, 20, 15)
App()
このコードで何をしているのか
self.scroll_x
「タイルマップ上のどこから描き始めるか」を表す値です。
pyxel.bltm(0, 0, 0, self.scroll_x, 0, 20, 15)
画面左上 (0,0) に、タイルマップ0の self.scroll_x 列目から 20x15 タイル分を描いています。
ここで重要なのは、self.scroll_x の単位は タイル単位 だということです。
タイルマップは 8x8 タイルを並べる仕組みなので、u=1 にすると、マップ表示が8ピクセルぶん右へ進んだのと同じ見た目になります。 bltm() の u, v はタイルマップ上の座標で、各タイルは8x8ピクセルです。
ピクセル単位で滑らかにスクロールしたい場合
タイル単位だけだと、8ピクセル刻みでしか動かないように感じることがあります。
その場合は、タイルマップの描画に加えて camera() を使う設計もありますが、
PyxelのタイルマップAPIの基本理解としては、まず 「タイル位置をずらして見せる」 ことを押さえるのが先です。
さらに細かなスクロール制御は、画面の描画オフセットやキャラの描画位置と組み合わせて設計する段階になります。Pyxelには描画オフセットを変える camera() もあります。
公式サンプルの考え方
10_platformer.py では scroll_x を持ち、プレイヤーが特定位置を超えると scroll_x を進めています。
そのうえで、敵の生成やプレイヤー位置の補正も scroll_x を基準に行っています。
つまり、スクロールは単なる見た目だけでなく、「今どこまでマップを進んだか」 を表すゲーム内部の基準にもなります。
実務で大事なこと
1. スクロール量とキャラ位置は分けて考える
- キャラの世界座標
- 画面上の見え方
- タイルマップの表示開始位置
この3つを混ぜると混乱しやすいです。
2. 端まで行ったら止める
マップ幅が有限なら、scroll_x は
- 0未満にしない
- マップの終端を超えない
ように制限する必要があります。
ここまで理解できれば十分なこと
- スクロールは
bltm()のu, vを変えることで実現できる -
u, vはタイル単位 - スクロールは見た目だけでなくゲーム進行の基準にもなる
ここまで分かれば、細かな制御は bltm() とサンプルコードを見れば十分です。
3. マップをエディタで作りたい
概要
Pyxelには Pyxel Editor があり、その中に Tilemap Editor があります。
Pyxel Editor は任意のリソースファイル名で起動でき、作成したリソースファイルは load() で読み込めます。
Wikiでは、Tilemap Editor は「image banks の画像をタイル状に並べて tilemaps を編集するモード」と説明されています。
詳細解説
Pyxel Editorで作る方法
エディタ起動
pyxeleditor my_game.pyxres
Wikiの説明では、このように 任意のリソースファイル名 で起動します。
作られたリソースファイルは .pyxres として保存され、Pythonコード側では pyxel.load("my_game.pyxres") で読み込めます。 APIでは load() は .pyxres を読む関数として定義されています。
Python側で読み込む
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.load("my_game.pyxres")
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.bltm(0, 0, 0, 0, 0, 20, 15)
App()
実際の作業の流れ
1. Image Editorでタイル画像を作る
まず、草・壁・水などの8x8タイルを画像バンク側に作ります。
2. Tilemap Editorで並べる
作ったタイルを、実際のマップとして並べます。
3. .pyxres として保存する
エディタの内容はリソースファイルに保存されます。
4. Pythonから pyxel.load("xxx.pyxres") で読む
コード側では bltm() で表示します。
外部ファイルの例
ファイル構成
project/
├── main.py
└── my_game.pyxres
読み込み例
pyxel.load("my_game.pyxres")
TMXファイルを使う方法もある
Pyxelは Tilemap.from_tmx(filename, layer) と Tilemap.load(x, y, filename, layer) に対応しており、Tiled の .tmx ファイル を読み込めます。
公式サンプル 15_tiled_map_file.py では、.png のタイルセット画像と .tmx のマップファイルを読み込み、Tilemap.from_tmx("assets/urban_rpg.tmx", i) で複数レイヤーを作っています。さらにコメントには、Tiled側では
- tile layout format を CSV
- tileset image を Embed in map
- tile size を 8x8
にする必要があると書かれています。
ファイル構成の例
project/
├── main.py
└── assets/
├── urban_rpg.png
└── urban_rpg.tmx
公式サンプルに近い読み込み例
pyxel.images[0] = pyxel.Image.from_image("assets/urban_rpg.png", include_colors=True)
pyxel.tilemaps[0] = pyxel.Tilemap.from_tmx("assets/urban_rpg.tmx", 0)
pyxel.tilemaps[1] = pyxel.Tilemap.from_tmx("assets/urban_rpg.tmx", 1)
どちらを選べばよいか
Pyxel Editor(.pyxres)
- Pyxelの標準的な流れ
- 小〜中規模で扱いやすい
- Pyxelの画像・音・タイルを一括管理しやすい
Tiled(.tmx)
- 外部マップエディタを使いたい
- 複数レイヤー前提で作りたい
- 既存のTiledワークフローを使いたい
ここまで理解できれば十分なこと
- Pyxel Editorで
.pyxresを作ってpyxel.load()で読める - Tilemap Editor は画像バンクのタイルを並べるモード
- Tiled の
.tmxも読める -
.tmxを使うなら公式サンプルの条件に合わせる
ここまで分かれば、あとは load() / Tilemap.from_tmx() / Tilemap.load() のAPIと、実際のエディタ操作を見れば十分です。
4. マップに当たり判定を付けたい
概要
ここで大事なのは、「タイルマップに当たり判定を付ける」とは、
タイルそのものに“壁かどうか”の意味を与えること だという点です。
Pyxelのタイルマップは、もともと (image_tx, image_ty) の並びにすぎません。
APIが「このタイルは壁」「このタイルは床」と自動判断してくれるわけではありません。
そのため、自分で“どのタイルを壁扱いするか”を決める 必要があります。 Tilemap.pget() はタイル座標から (image_tx, image_ty) を返します。
詳細解説
一番基本的な考え方
たとえば、
-
(0, 0)は草 -
(1, 0)は床 -
(4, 0)以降は壁
のように、自分でルールを決めます。
公式サンプルの例
10_platformer.py では、次のようにしています。
-
get_tile(tile_x, tile_y)でpyxel.tilemaps[0].pget(tile_x, tile_y)を読む -
is_wall(x, y)の中で、ピクセル座標を// 8してタイル座標に変換 -
tile == TILE_FLOOR or tile[0] >= WALL_TILE_Xなら壁扱い
つまり、「画像タイルの x 座標が4以上なら壁」といったルールをコード側で定義しています。
同じ考え方で書く例
def get_tile(tile_x, tile_y):
return pyxel.tilemaps[0].pget(tile_x, tile_y)
def is_wall_pixel(px, py):
tile = get_tile(px // 8, py // 8)
return tile[0] >= 4
何をしているのか
px // 8, py // 8
ピクセル座標をタイル座標に変換しています。
Pyxelのタイルは8x8なので、ピクセル位置を8で割ると「何マス目か」が分かります。各タイルは8x8ピクセルです。
tile[0] >= 4
pget() が返す (image_tx, image_ty) のうち、画像上のタイルx座標を見て、壁かどうかを判定しています。
別の考え方もできる
たとえば「壁タイル一覧」を作る方法もあります。
WALL_TILES = [(4, 0), (5, 0), (6, 0), (7, 0)]
def is_wall_pixel(px, py):
tile = pyxel.tilemaps[0].pget(px // 8, py // 8)
return tile in WALL_TILES
こちらのほうが、どのタイルが壁なのかが明示的で分かりやすい場合もあります。
実務ではどう決めるか
小規模
tile[0] >= 4 のように、並びルールで決める
中規模以上
WALL_TILES や LADDER_TILES などの一覧で管理する
さらに複雑
- 壁
- 床
- 水
- ダメージ床
- はしご
- 一方通行床
のように種類ごとの集合を作る
ここで理解すべきこと
「マップに当たり判定を付ける」とは、
タイルマップの見た目に意味を与えること です。
PyxelのタイルマップAPIそのものは、あくまで「どのタイルが置かれているか」を返すだけです。
そこに「歩ける」「ぶつかる」「登れる」といった意味を与えるのはアプリ側の仕事です。
ここまで理解できれば、APIリファレンスより先に「自分のゲームでどのタイルを何扱いにするか」を決める段階です。APIとしては pget() が分かれば十分です。
5. マップとキャラの衝突判定をしたい
概要
ここは前項との違いが重要です。
-
前項
どのタイルを壁扱いにするかを決める -
この項
実際にキャラを動かしたとき、壁にめり込まないようにする
Pyxelには Tilemap.collide() があり、
「ピクセル矩形 (x, y, w, h) が (dx, dy) だけ動こうとしたとき、壁タイル一覧にぶつかるなら移動量を調整する」処理を行えます。APIの説明でも、pixel movement (dx, dy) を適用した後の衝突を解決し、調整後の (dx, dy) を返す とされています。
詳細解説
一番重要な例
公式サンプル 10_platformer.py では、次のように使っています。
def push_back(x, y, dx, dy):
walls = WALL_TILES_WITH_FLOOR if dy > 0 else WALL_TILES
dx, dy = pyxel.tilemaps[0].collide(x, y, 8, 8, dx, dy, walls)
return x + dx, y + dy
これは、
- キャラの現在位置
(x, y) - キャラのサイズ
(8, 8) - 今フレームの移動量
(dx, dy) - 壁扱いするタイル一覧
walls
を渡し、ぶつかるなら移動量を削って、最終的な新座標を返しています。
同じ考え方の最小例
import pyxel
WALL_TILES = [(4, 0), (5, 0), (6, 0), (7, 0)]
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.load("assets/map_test.pyxres")
self.x = 16
self.y = 16
pyxel.run(self.update, self.draw)
def update(self):
dx = 0
dy = 0
if pyxel.btn(pyxel.KEY_LEFT):
dx = -1
if pyxel.btn(pyxel.KEY_RIGHT):
dx = 1
if pyxel.btn(pyxel.KEY_UP):
dy = -1
if pyxel.btn(pyxel.KEY_DOWN):
dy = 1
dx, dy = pyxel.tilemaps[0].collide(self.x, self.y, 8, 8, dx, dy, WALL_TILES)
self.x += dx
self.y += dy
def draw(self):
pyxel.cls(0)
pyxel.bltm(0, 0, 0, 0, 0, 20, 15)
pyxel.rect(self.x, self.y, 8, 8, 8)
App()
このコードで何をしているのか
self.x, self.y
キャラの左上座標です。
8, 8
キャラの当たり判定サイズです。
ここでは8x8の四角キャラだと仮定しています。
collide(...)
「この8x8の四角が (dx, dy) だけ動こうとしたら、壁に当たるか」を計算し、
当たるなら dx / dy を修正して返します。APIの説明通りです。
pget() で自前判定する方法との違い
pget() 方式
- 自分で細かく判定を書く
- 柔軟
- ただし複雑になりやすい
collide() 方式
- 壁リストを渡すだけで衝突解決までやってくれる
- 基本的な壁衝突にはかなり便利
じゃあ pget() は不要か?
不要ではありません。
たとえば
- はしごに触れたら登れる
- 水タイルに入ったら速度が落ちる
- ダメージ床ならHPが減る
のような「衝突というより意味判定」は pget() で見ることが多いです。
collide() は主に「壁として押し返す」用途に向いています。
ここまで理解できれば十分なこと
-
Tilemap.collide()は壁衝突解決用 - 壁タイル一覧を渡して使う
- 返ってくるのは修正済みの
dx, dy - タイルの意味判定は
pget()と併用することが多い
ここまで分かれば、あとは collide() のAPIリファレンスと公式サンプルを見れば十分です。
6. 大きなマップを分割して管理したい
概要
やり方はいくつかありますが、Pyxelで現実的なのは次の3パターンです。
- タイルマップを複数使い分ける
.tmxのレイヤーや複数ファイルに分ける- 必要な範囲だけ描く・必要なタイミングで読み込む
Pyxelは tilemaps[0]〜[7] まで使えるので、1つの巨大マップをいくつかの区画に分けて持つことができます。
また Tilemap.from_tmx() / Tilemap.load() で .tmx から読み込めるので、外部ファイル単位でエリア分割する運用もできます。
詳細解説
パターン1:タイルマップ番号を分ける
たとえば、
-
tilemaps[0]= 村 -
tilemaps[1]= 洞窟 -
tilemaps[2]= 城内
のように分ける方法です。
current_map = 0
def draw():
pyxel.cls(0)
pyxel.bltm(0, 0, current_map, 0, 0, 20, 15)
これは一番分かりやすいです。
マップ切り替えも current_map を変えるだけです。
パターン2:.tmx や .pyxres をエリアごとに分ける
例:.tmx を分ける
project/
├── main.py
└── maps/
├── town.tmx
├── cave.tmx
└── castle.tmx
pyxel.tilemaps[0] = pyxel.Tilemap.from_tmx("maps/town.tmx", 0)
pyxel.tilemaps[1] = pyxel.Tilemap.from_tmx("maps/cave.tmx", 0)
pyxel.tilemaps[2] = pyxel.Tilemap.from_tmx("maps/castle.tmx", 0)
例:.pyxres を分ける
project/
├── main.py
└── resources/
├── town.pyxres
├── cave.pyxres
└── castle.pyxres
必要な場面で pyxel.load("resources/town.pyxres") のように読み分ける方法もあります。
load() は .pyxres を読み込むAPIです。
パターン3:大きな1枚マップの一部だけ描く
巨大なマップを1つのタイルマップに持ったままでも、bltm() で必要な範囲しか描かなければ、画面に出る量は制御できます。
これは「管理上は1枚、表示上は一部だけ」という考え方です。
スクロール処理そのものがこの発想です。 bltm() はタイルマップ上の任意領域だけを描けます。
どの方法を選ぶべきか
シンプルな画面切り替え型
村 → フィールド → ダンジョン のような切り替えなら、タイルマップ番号やファイルを分けるのが分かりやすいです。
ひと続きの広いフィールド
横スクロールや見下ろし型の広いマップなら、1枚の大きなマップを必要範囲だけ描くほうが自然です。
制作を分担したい
エリアごとに .tmx / .pyxres を分けたほうが管理しやすいです。
実務で大事なこと
1. マップデータと遷移情報は分ける
「どのマップを表示するか」と「どこからどこへ遷移するか」は別の情報です。
2. キャラの出現位置もセットで持つ
マップ切り替え時には、
- 次のマップ番号
- 出現座標
- 進入方向
も一緒に管理したくなります。
3. 分割しすぎても管理が面倒になる
小さく分ければ良いわけではなく、「制作しやすい単位」「切り替えやすい単位」で区切るのが実用的です。
ここまで理解できれば十分なこと
- 大きなマップは複数タイルマップや複数ファイルに分けられる
-
.pyxresと.tmxの両方で運用できる -
bltm()は必要範囲だけ描ける - 分割は表示だけでなく制作・保守の都合でも考える
ここまで分かれば、APIリファレンスより先に「自分のゲームではどの単位でマップを区切るか」を設計する段階です。Pyxel側の機能としては tilemaps[]、load()、Tilemap.from_tmx()、bltm() を押さえれば十分です。
まとめ
Pyxelのマップ・タイルまわりは、実質的には次の4つを押さえるとかなり整理できます。
-
画像バンク
タイルの見た目を置く場所 -
タイルマップ
どのタイルをどこに置くかを並べたもの -
bltm()
タイルマップを画面へ描く -
pget()/collide()
タイルの意味判定や衝突解決に使う
整理するとこうです。
-
タイルマップを表示する
bltm() -
マップをスクロールする
bltm()のu, vを変える -
エディタでマップを作る
pyxeleditor xxx.pyxresまたは.tmx -
マップに当たり判定を付ける
pget()でタイルの意味を自分で決める -
キャラとマップを衝突させる
collide()で移動量を調整する -
大きなマップを分割して管理する
tilemaps[]、.pyxres、.tmx、部分描画を使い分ける
ここまで理解できたら、次に見るべきは bltm()、Tilemap.set()、Tilemap.pget()、Tilemap.collide()、load()、Tilemap.from_tmx() のAPIリファレンスです。そこから先は「Pyxelで何ができるか」を知る段階ではなく、「自分のゲームでマップをどう設計するか」を詰める段階に入ります。
🎮 入力(操作)編
Pyxelで操作入力を扱うときに最初に知りたくなる内容を、逆引き形式でまとめます。
対象にする項目は次の7つです。
- キーボード入力を取りたい
- キーが押された瞬間を検知したい
- キー長押しを検知したい
- ゲームパッド入力を使いたい
- 複数キーの同時入力を扱いたい
- カーソル操作を作りたい
- メニュー操作を作りたい
まず最初に:Pyxelの入力はどう考えればよいか
Pyxelの入力は、基本的に update() の中で毎フレーム確認します。
つまり「イベントが飛んできたら受け取る」というより、今このフレームでそのキーがどういう状態かを毎回見る という考え方です。
Pyxelでまず覚えるべき入力APIは次の4つです。
-
pyxel.btn(key)
押している間ずっとTrue -
pyxel.btnp(key, hold=0, repeat=0)
押された瞬間だけTrue
ただしholdとrepeatを指定すると長押しリピートにも使える -
pyxel.btnr(key)
離した瞬間だけTrue -
pyxel.btnv(key)
アナログ値を取得する。ゲームパッドのスティックやトリガー向け
Pyxelには KEY_LEFT や KEY_Z のようなキーボード定数だけでなく、GAMEPAD1_BUTTON_A、GAMEPAD1_BUTTON_DPAD_UP、GAMEPAD1_AXIS_LEFTX などのゲームパッド定数も用意されています。つまり、入力の読み方は共通で、違うのは使うキー定数だけ です。
1. キーボード入力を取りたい
概要
一番基本の入力取得は pyxel.btn() です。
btn() は「そのキーが今このフレームで押されているかどうか」を返します。
押している間は毎フレーム True になります。APIでも btn(key) は「押されていれば True」と説明されています。
詳細解説
一番小さい例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.x = 40
self.y = 40
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btn(pyxel.KEY_LEFT):
self.x -= 1
if pyxel.btn(pyxel.KEY_RIGHT):
self.x += 1
if pyxel.btn(pyxel.KEY_UP):
self.y -= 1
if pyxel.btn(pyxel.KEY_DOWN):
self.y += 1
def draw(self):
pyxel.cls(0)
pyxel.rect(self.x, self.y, 8, 8, 8)
App()
このコードで何をしているのか
この例では、KEY_LEFT、KEY_RIGHT、KEY_UP、KEY_DOWN が押されている間だけ座標を変えています。
たとえば左キーを3フレーム押し続ければ、self.x -= 1 が3回実行されるので、キャラは3ピクセル左に動きます。
ここで重要なのは、btn() は「押した瞬間」ではなく、押している間ずっと反応する という点です。
どういうときに使うか
btn() は、押している間ずっと動き続けてほしい操作に向いています。
- キャラの移動
- 長押しでのスクロール
- 充電攻撃の溜め
- カメラ移動
- 音量調整バーの連続操作
よくある勘違い
btn() を決定ボタンに使うと連打になる
たとえばメニューの「決定」を btn(KEY_Z) で処理すると、押している数フレームの間に何度も決定扱いされることがあります。
決定・選択・シーン切り替えのような「1回だけ発生してほしい処理」には btnp() のほうが向いています。 btnp() は「そのフレームで押されたら True」と定義されています。
どのキー定数を使えばいいか分からない
Pyxelには KEY_A から KEY_Z、矢印キー、数字キー、スペース、エンター、シフト、Ctrl など非常に多くの定数があります。
特殊なキーを使いたくなったら、定数一覧をAPI定義で見ればだいたい見つかります。
ここまで理解できれば十分なこと
- キーボード入力の基本は
btn() - 押している間ずっと
True - 連続移動や押しっぱなし操作に向いている
ここまで分かれば、細かいキー定数はAPIリファレンスを見れば十分です。
2. キーが押された瞬間を検知したい
概要
押された瞬間だけ反応させたいときは pyxel.btnp() を使います。
btnp() は「そのフレームで押されたら True」を返します。
つまり、押しっぱなしでも毎フレーム反応するのではなく、押し始めた最初の1フレームだけ反応 します。APIの説明でも btnp() は「pressed in that frame」となっています。
詳細解説
一番小さい例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.color = 8
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_SPACE):
self.color = 10 if self.color == 8 else 8
def draw(self):
pyxel.cls(0)
pyxel.rect(60, 40, 40, 20, self.color)
pyxel.text(45, 80, "PRESS SPACE", 7)
App()
このコードで何をしているのか
スペースキーを押した瞬間だけ色を切り替えています。
押し続けても色は高速で点滅せず、1回の押下につき1回だけ切り替わります。
これが btn() との一番大きな違いです。
どういうときに使うか
btnp() は「1回押したら1回だけ起きてほしい処理」に向いています。
- 決定
- キャンセル
- ジャンプ開始
- 攻撃開始
- メニュー選択
- シーン切り替え
- ウィンドウを開く / 閉じる
btn() と btnp() の違いを比べる
btn() の場合
if pyxel.btn(pyxel.KEY_Z):
selected += 1
これだと、Zキーを押している数フレームの間ずっと selected += 1 が走るので、
カーソルが一瞬で何項目も進んでしまうことがあります。
btnp() の場合
if pyxel.btnp(pyxel.KEY_Z):
selected += 1
こちらなら、押した瞬間の1回だけ進みます。
btnr() との違い
似たものに btnr() がありますが、こちらは「離した瞬間」です。
たとえば「ボタンを離したら弾を撃つ」のような特殊な仕様に向いています。
普通の決定操作なら btnp() のほうが自然です。APIでは btnr() は released in that frame と説明されています。
ここまで理解できれば十分なこと
- 押された瞬間を見たいなら
btnp() - 決定やジャンプ開始などに向いている
- 押しっぱなし操作には
btn()のほうが向いている
これ以上の細かい仕様は btnp() と btnr() のAPIリファレンスを見れば十分です。
3. キー長押しを検知したい
概要
長押しには2つの考え方があります。
-
押している間ずっと反応させる
→btn() -
最初に1回反応して、少し待ってから一定間隔で連続反応させる
→btnp(key, hold, repeat)
Pyxelの btnp() は hold と repeat を持っていて、
「何フレーム押し続けたらリピートを始めるか」「何フレーム間隔で繰り返すか」を指定できます。
APIでも、hold フレーム保持後に repeat フレームごとに True を返すと説明されています。
詳細解説
一番実用的な例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.index = 0
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_DOWN, hold=15, repeat=3):
self.index += 1
if pyxel.btnp(pyxel.KEY_UP, hold=15, repeat=3):
self.index -= 1
self.index = max(0, min(self.index, 9))
def draw(self):
pyxel.cls(0)
for i in range(10):
col = 10 if i == self.index else 7
pyxel.text(20, 20 + i * 8, f"ITEM {i}", col)
App()
このコードで何が起きるのか
最初の1回
キーを押した瞬間に1回反応します。
その後
15フレーム押し続けたあと、3フレームごとに反応し続けます。
これはPCやゲーム機のメニューでよくある、
- まず1つ動く
- 少し待つ
- その後は高速で連続移動する
という挙動です。
長押しメニュー操作を作るときに非常に使いやすいです。API仕様そのものがこの用途に合っています。
btn() だけではダメなのか
ダメではありませんが、使い分けが大事です。
btn() の長押し
if pyxel.btn(pyxel.KEY_DOWN):
self.index += 1
これだと押している毎フレーム反応するので、30FPSなら1秒で30回動きます。
普通のメニューでは速すぎることが多いです。
btnp(..., hold, repeat) の長押し
if pyxel.btnp(pyxel.KEY_DOWN, hold=15, repeat=3):
self.index += 1
こちらは人間が扱いやすい「長押しリピート」になります。
どういうときに使うか
- メニューの上下移動
- 数値設定の増減
- リストスクロール
- カーソル移動
- キーリピートが欲しいUI全般
設定値の目安
30FPS前提なら、次のような値がよく使いやすいです。
hold=10〜20repeat=2〜5
たとえば hold=15, repeat=3 はかなり扱いやすい部類です。
ただし最適値はUIの種類やテンポ感によるので、最終的には実機で触って決めるのがよいです。PyxelのデフォルトFPSは30です。
ここまで理解できれば十分なこと
- 長押しそのものは
btn()でも分かる - メニュー用の長押しリピートは
btnp(key, hold, repeat)が便利 -
holdは待ち時間、repeatは繰り返し間隔
細かい調整は btnp() のAPIリファレンスを見れば十分です。
4. ゲームパッド入力を使いたい
概要
Pyxelはゲームパッド用の定数を持っており、
キーボードと同じ btn() / btnp() / btnr() / btnv() で扱えます。
たとえば GAMEPAD1_BUTTON_A、GAMEPAD1_BUTTON_B、GAMEPAD1_BUTTON_DPAD_UP、GAMEPAD1_AXIS_LEFTX などが用意されています。さらに GAMEPAD2〜GAMEPAD4 までの定数もあります。
詳細解説
一番基本の例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.x = 40
self.y = 40
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_LEFT):
self.x -= 1
if pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_RIGHT):
self.x += 1
if pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_UP):
self.y -= 1
if pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_DOWN):
self.y += 1
if pyxel.btnp(pyxel.GAMEPAD1_BUTTON_A):
self.y -= 8
def draw(self):
pyxel.cls(0)
pyxel.rect(self.x, self.y, 8, 8, 8)
pyxel.text(10, 10, "DPAD: MOVE / A: JUMP", 7)
App()
キーボードとの違いは何か
読み方はほとんど同じです。
違うのは、KEY_LEFT などの代わりに GAMEPAD1_BUTTON_DPAD_LEFT のような定数を使う点だけです。
つまり、「入力APIを覚え直す」というより、使うキーコードが変わるだけ と考えると分かりやすいです。APIの構造上も同じ btn() 系関数を使います。
アナログスティックを使いたい場合
アナログ入力は btnv() を使います。
btnv(key) は「ゲームパッドの軸の値」などを返します。
APIでは btnv() は「analog value of the specified key」と説明されています。
例
lx = pyxel.btnv(pyxel.GAMEPAD1_AXIS_LEFTX)
ly = pyxel.btnv(pyxel.GAMEPAD1_AXIS_LEFTY)
if lx < -10000:
self.x -= 1
if lx > 10000:
self.x += 1
if ly < -10000:
self.y -= 1
if ly > 10000:
self.y += 1
このコードで何をしているのか
スティックの値はアナログなので、
「左に少し傾いている」「大きく傾いている」のような連続値として取得されます。
そのため、多くの場合は
- ある閾値より小さい / 大きいか
- どれくらい傾いているか
を自分で判断して使います。
実務でよくやること
キーボードとゲームパッド両対応
同じ操作に対して、複数の入力を許可することが多いです。
if pyxel.btn(pyxel.KEY_LEFT) or pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_LEFT):
self.x -= 1
こうしておくと、キーボードでもゲームパッドでも動きます。
ここまで理解できれば十分なこと
- ゲームパッドも
btn()/btnp()/btnr()/btnv()で扱う - ボタン入力はキーボードとほぼ同じ
- アナログ入力は
btnv() -
GAMEPAD1からGAMEPAD4まで定数がある
細かいボタン定数や軸定数はAPIリファレンスを見れば十分です。
5. 複数キーの同時入力を扱いたい
概要
Pyxelは各キーの状態を個別に取れるので、
複数キー同時押しは単に複数の btn() や btnp() を組み合わせれば実現できます。
また、Pyxelには input_keys という「そのフレームで入力されたキー一覧」もありますが、通常のゲーム操作では btn() 系の組み合わせで十分なことが多いです。API定義にも input_keys が current frame のキー一覧としてあります。
詳細解説
一番基本の例
if pyxel.btn(pyxel.KEY_LEFT) and pyxel.btn(pyxel.KEY_UP):
self.x -= 1
self.y -= 1
これは左上への斜め移動です。
Shift + 方向キーのような例
speed = 1
if pyxel.btn(pyxel.KEY_SHIFT):
speed = 2
if pyxel.btn(pyxel.KEY_RIGHT):
self.x += speed
このようにすると、Shiftを押している間だけ高速移動できます。
ゲームでよくある使い方
- 左 + 上
→ 斜め移動 - Shift + 方向キー
→ ダッシュ - 下 + 決定
→ しゃがみ攻撃 - ガード + 方向キー
→ 特殊アクション
優先順位を決める必要がある場合もある
同時入力をそのまま全部受けると都合が悪いこともあります。
たとえば左右を同時に押したら、
- 何もしない
- 直前に押した方向を優先する
- 左を優先する
など、ゲームごとに仕様を決める必要があります。
例:左右同時なら停止
left = pyxel.btn(pyxel.KEY_LEFT)
right = pyxel.btn(pyxel.KEY_RIGHT)
if left and not right:
self.x -= 1
elif right and not left:
self.x += 1
input_keys はどういうときに使うか
input_keys は「そのフレームで入力されたキーの一覧」です。
通常の移動処理よりも、キー入力履歴を見たい場合や、複数キー入力の記録を取りたい場合に向きます。
ただし一般的なゲーム操作では btn() / btnp() のほうが分かりやすいことが多いです。 API定義でも current frame の input key list とされています。
ここまで理解できれば十分なこと
- 同時入力は
btn()/btnp()を組み合わせればよい - 斜め移動、ダッシュ、複合操作に使える
- 矛盾する入力が来たときは優先順位を決める
さらに特殊な入力履歴が必要なら input_keys を見れば十分です。
6. カーソル操作を作りたい
概要
カーソル操作の本質は、
「今どの項目を選んでいるか」を表す変数を1つ持ち、それを上下左右入力で変える ことです。
Pyxel専用のカーソル機能があるわけではなく、btnp() や btnp(..., hold, repeat) で選択位置を更新します。
つまり、入力APIをどう使うかというより、選択状態をどう管理するか が重要です。Pyxelの btnp() が長押しリピートに対応しているため、カーソル操作と相性が良いです。
詳細解説
縦メニューの一番基本
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.items = ["START", "OPTION", "LOAD", "QUIT"]
self.index = 0
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_DOWN, hold=15, repeat=3):
self.index += 1
if pyxel.btnp(pyxel.KEY_UP, hold=15, repeat=3):
self.index -= 1
self.index = max(0, min(self.index, len(self.items) - 1))
def draw(self):
pyxel.cls(0)
for i, item in enumerate(self.items):
prefix = ">" if i == self.index else " "
col = 10 if i == self.index else 7
pyxel.text(30, 30 + i * 10, f"{prefix} {item}", col)
App()
このコードで何をしているのか
self.index
今どの項目を選んでいるかを表す番号です。
0なら1番上、1なら2番目、という意味になります。
btnp(KEY_DOWN, hold=15, repeat=3)
下キーを押した瞬間に1つ下へ行き、長押しすると少し待ってから高速に下へ進みます。
カーソル操作ではこの挙動がとても自然です。Pyxelの btnp() の仕様そのものです。
max() / min()
範囲外へ出ないようにしています。
これがないと、最後の項目のさらに下へ進んでしまいます。
2列のカーソル操作にしたい場合
たとえば 2x2 のボタン配置なら、row と col を分けて持つ方法が分かりやすいです。
self.row = 0
self.col = 0
if pyxel.btnp(pyxel.KEY_LEFT):
self.col -= 1
if pyxel.btnp(pyxel.KEY_RIGHT):
self.col += 1
if pyxel.btnp(pyxel.KEY_UP):
self.row -= 1
if pyxel.btnp(pyxel.KEY_DOWN):
self.row += 1
画面上のカーソルを動かす場合
選択番号だけでなく、実際に矢印や枠を描いてもよいです。
cursor_y = 30 + self.index * 10
pyxel.text(20, cursor_y, ">", 10)
あるいは rectb() で選択枠を描く方法もあります。
つまり、カーソルというより「選択中の見せ方」を自分で作るという感覚です。
ここまで理解できれば十分なこと
- カーソル操作は「選択番号」を動かす処理
-
btnp()と長押しリピートが便利 - 範囲外へ出ないように制限する
- 見た目は
>や枠線で自由に作れる
ここまで分かれば、APIよりもUI設計の話になります。入力APIとしては btnp() が分かっていれば十分です。
7. メニュー操作を作りたい
概要
メニュー操作は、実際には次の3要素の組み合わせです。
-
選択位置を動かす
上下左右キーやゲームパッドでカーソルを移動する -
決定する
btnp()で選んだ項目を実行する -
戻る / キャンセルする
別のキーで前の状態へ戻す
つまり、メニュー操作は新しい特別な機能ではなく、
ここまで説明した カーソル操作 + 決定入力 + キャンセル入力 の組み合わせです。
Pyxelの入力APIだけで十分作れます。
詳細解説
一番基本のメニュー例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.items = ["START", "OPTION", "QUIT"]
self.index = 0
self.message = ""
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_DOWN, hold=15, repeat=3):
self.index = min(self.index + 1, len(self.items) - 1)
if pyxel.btnp(pyxel.KEY_UP, hold=15, repeat=3):
self.index = max(self.index - 1, 0)
if pyxel.btnp(pyxel.KEY_Z):
self.message = f"SELECT: {self.items[self.index]}"
if pyxel.btnp(pyxel.KEY_X):
self.message = "CANCEL"
def draw(self):
pyxel.cls(0)
pyxel.text(20, 10, "MAIN MENU", 7)
for i, item in enumerate(self.items):
col = 10 if i == self.index else 7
prefix = ">" if i == self.index else " "
pyxel.text(20, 30 + i * 10, f"{prefix} {item}", col)
pyxel.text(20, 90, self.message, 8)
pyxel.text(20, 105, "Z: OK / X: CANCEL", 5)
App()
このコードで何をしているのか
上下で選択位置を変える
これはカーソル操作そのものです。
KEY_Z で決定
今選んでいる項目の文字列を self.message に入れています。
実際のゲームではここで、
- シーンを切り替える
- 設定画面へ移る
- ゲーム開始フラグを立てる
などを行います。
KEY_X でキャンセル
前の画面へ戻る、メニューを閉じる、選択をやり直す、といった処理に使えます。
ゲームパッド対応版にするなら
入力部分だけこう変えれば十分です。
if pyxel.btnp(pyxel.GAMEPAD1_BUTTON_DPAD_DOWN, hold=15, repeat=3):
self.index = min(self.index + 1, len(self.items) - 1)
if pyxel.btnp(pyxel.GAMEPAD1_BUTTON_DPAD_UP, hold=15, repeat=3):
self.index = max(self.index - 1, 0)
if pyxel.btnp(pyxel.GAMEPAD1_BUTTON_A):
self.message = f"SELECT: {self.items[self.index]}"
if pyxel.btnp(pyxel.GAMEPAD1_BUTTON_B):
self.message = "CANCEL"
Pyxelではキーボードとゲームパッドで入力APIが共通なので、
メニュー設計そのものはそのままで、使う定数だけ差し替えればよいです。ゲームパッド定数は GAMEPAD1_BUTTON_A、GAMEPAD1_BUTTON_B、GAMEPAD1_BUTTON_DPAD_UP などが用意されています。
実務でよくある拡張
ループ移動
最後の項目で下を押したら最初に戻る、という仕様です。
if pyxel.btnp(pyxel.KEY_DOWN, hold=15, repeat=3):
self.index = (self.index + 1) % len(self.items)
無効項目を飛ばす
まだ選べない項目をスキップする仕様です。
サブメニュー
state = "main"、state = "option" のように画面状態を持ち、
決定で別状態へ切り替えます。
ここまで理解できれば十分なこと
- メニュー操作はカーソル移動 + 決定 + キャンセル
- 入力APIとしては
btnp()が中心 - 長押しリピートを使うと操作感がよい
- キーボード対応もゲームパッド対応も同じ設計で作れる
ここまで理解できたら、あとはAPIリファレンスよりも「自分のメニューをどういう画面遷移にしたいか」を詰める段階です。Pyxelの入力APIとしては十分理解できています。
まとめ
Pyxelの入力まわりは、実質的には次の4つを押さえるとかなり整理できます。
-
押している間を見る
btn() -
押した瞬間を見る
btnp() -
離した瞬間を見る
btnr() -
アナログ値を見る
btnv()
整理するとこうです。
-
キーボード入力を取る
btn(KEY_...) -
押した瞬間を取る
btnp(KEY_...) -
長押しを扱う
btnp(KEY_..., hold, repeat)またはbtn() -
ゲームパッドを使う
btn(GAMEPAD1_BUTTON_...)/btnv(GAMEPAD1_AXIS_...) -
同時入力を扱う
複数のbtn()/btnp()を組み合わせる -
カーソル操作を作る
選択番号を上下左右で変える -
メニュー操作を作る
カーソル移動 + 決定 + キャンセルを組み合わせる
ここまで理解できたら、次に見るべきは btn()、btnp()、btnr()、btnv() のAPIリファレンスと、使いたいキー定数・ゲームパッド定数の一覧です。そこから先は「Pyxelで何ができるか」を知る段階ではなく、「自分のゲームやUIでどういう操作感にするか」を詰める段階に入ります。
🔊 サウンド・音楽編
Pyxelで音を扱うときに最初に知りたくなる内容を、逆引き形式でまとめます。
対象にする項目は次の7つです。
- 効果音を鳴らしたい
- BGMを流したい
- BGMをループさせたい
- 音量を調整したい
- 複数の音を同時に鳴らしたい
- 音を途中で止めたい
- 自動でBGMを生成したい
- ogg や wav や mp3 の音楽ファイルを読み込ませたい
まず最初に:Pyxelの音はどういう構造か
Pyxelの音まわりを理解するには、まず Sound と Music の違いを押さえるのが重要です。Pyxel には sounds[0]〜sounds[63] と musics[0]〜musics[7] があり、さらに再生用として channels[0]〜channels[3] の4チャンネルがあります。Pyxel全体として 4 sound channels を持つことも公式に案内されています。
Soundとは何か
Sound は「1つの効果音」や「1フレーズ分の音」です。
具体的には、次の情報を持っています。
-
notes
音程 -
tones
音色 -
volumes
音量 -
effects
スライド、ビブラート、フェードなど -
speed
再生速度
APIでは Sound がこれらの配列を持ち、set()、set_notes()、set_tones()、set_volumes()、set_effects() などで設定できると定義されています。たとえば notes は 0-59、休符は -1、tones は Triangle/Square/Pulse/Noise、volumes は 0-7 です。
Musicとは何か
Music は「複数チャンネルの Sound を順番に並べた曲」です。
API上では Music.seqs は「複数チャンネルにまたがる Sound 番号列の二次元リスト」とされています。つまり、BGM は 複数の Sound をチャンネルごとに並べて再生する仕組み です。 musics[0].set([0,1], [], [3]) のような例もAPIにあります。
再生は何で行うか
-
pyxel.play(ch, snd, ...)
1チャンネルに Sound もしくは Sound列を流す -
pyxel.playm(msc, ...)
Music を流す -
pyxel.stop(ch)/pyxel.stop()
1チャンネルまたは全チャンネル停止 -
pyxel.play_pos(ch)
再生位置取得
API上、play() は Sound番号だけでなく、Sound番号のリスト、Sound インスタンス、Sound のリスト、MML文字列 まで受け取れます。 playm() は Music番号を受け取り、どちらも sec と loop を持ちます。 play() にはさらに resume もあります。 :contentReference[oaicite:4]{index=4}
一番大事な理解
Pyxelの音は、ざっくり言うと次のように考えると分かりやすいです。
-
効果音
Soundを作ってplay()で鳴らす -
BGM
Musicを作ってplaym()で鳴らす
もしくは複数チャンネルにplay()で分けて流す -
音量や停止
channels[ch].gain、stop() -
自動生成BGM
gen_bgm()
ここまで分かれば、あとは項目ごとの使い方を覚えれば十分です。
細かな仕様や最新の挙動は API リファレンスと変更履歴を見れば足ります。Pyxel 2.4.0 ではサウンドエンジン更新、play / playm の秒指定化、MML文字列再生などが入っています。 :contentReference[oaicite:5]{index=5}
1. 効果音を鳴らしたい
概要
効果音の基本は Sound を作って pyxel.play() する ことです。
play(ch, snd, sec=0, loop=False, resume=False) は、指定チャンネル ch に Sound を流します。 snd には Sound番号、番号リスト、Sound インスタンス、リスト、MML文字列を渡せます。 :contentReference[oaicite:6]{index=6}
詳細解説
一番小さい例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
# sounds[0] を「短い効果音」として作る
pyxel.sounds[0].set(
"c3e3g3c4", # notes
"p", # tones: pulse
"7", # volumes
"f", # effects: fadeout
20, # speed
)
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
pyxel.play(0, 0) # channel 0 で sound 0 を再生
def draw(self):
pyxel.cls(0)
pyxel.text(20, 40, "PRESS Z TO PLAY SE", 7)
App()
このコードで何をしているのか
pyxel.sounds[0].set(...)
Sound番号0に音を定義しています。
APIでは set(notes, tones, volumes, effects, speed) でまとめて設定できます。 tones は T/S/P/N、volumes は 0-7、effects は N/S/V/F 系で指定できます。公式サンプル 04_sound_api.py でも sounds[n].set(note, tone, volume, effect, speed) を使っています。
pyxel.play(0, 0)
これは「チャンネル0で Sound番号0を再生」という意味です。
効果音は普通、BGMとは別のチャンネルで鳴らすことが多いです。 play() はチャンネル番号 0-3 を取ります。
set() の中身をもっと具体的に見る
notes
音の並びです。
たとえば "c3e3g3c4" はド、ミ、ソ、ドです。
APIではノート文字列は CDEFGAB、# または -、オクターブ 0-4、休符 R を使え、空白は無視されます。例として g2b-2d3r rf3f3f3 も示されています。
tones
音色です。
Triangle、Square、Pulse、Noise が使えます。
04_sound_api.py でも説明表示として tone : [T]riangle [S]quare [P]ulse [N]oise と出しています。
volumes
音量です。
0-7 の文字列で指定します。 7 が大きく、0 は無音です。
effects
音の変化です。
APIでは None / Slide / Vibrato / FadeOut / HalfFadeOut / QuarterFadeOut が定義されています。 2.1.2 で Half-FadeOut と Quarter-FadeOut が追加されています。
speed
再生速度です。
APIでは 1 が最速で、数字が大きいほど遅くなります。
120 だと1ノートが1秒相当です。
実務でどう使うか
効果音は、だいたい次のように役割ごとに番号を決めておくと管理しやすいです。
-
sounds[0]= 決定音 -
sounds[1]= キャンセル音 -
sounds[2]= 被弾音 -
sounds[3]= アイテム取得音
こうしておくと、コード側で
pyxel.play(3, 2)
のように見たとき、「チャンネル3で被弾音を鳴らしている」と分かりやすくなります。
MML文字列を直接鳴らす方法もある
Pyxel 2.4.0 で play() に MML文字列対応 が追加されました。
そのため、Sound番号を経由せず、その場で短いフレーズを鳴らすこともできます。
pyxel.play(0, "t180 l8 cdefgab>c")
これは試作時には便利ですが、実務では Sound番号に整理したほうが再利用しやすいことが多いです。
ここまで理解できれば十分なこと
- 効果音は
sounds[n]を作ってplay()で鳴らす -
set()ではnotes / tones / volumes / effects / speedを決める - 役割ごとに Sound番号を割り当てると管理しやすい
- MML文字列を直接鳴らすこともできる
細かな音作りは Sound のAPIリファレンスと 04_sound_api.py のサンプルを見れば十分です。
2. BGMを流したい
概要
BGMを流す方法は大きく2つあります。
pyxel.playm()で Music を流す- 複数チャンネルに
pyxel.play()で Sound列を流して、自分でBGMとして扱う
Pyxelには musics[0]〜musics[7] があり、playm(msc, sec=0, loop=False) で再生できます。
一方で、公式サンプル 04_sound_api.py では playm() の説明も出しつつ、実際のデモ再生は play(0,[0,1],loop=True)、play(1,[2,3],loop=True)、play(2,4,loop=True) のように、複数チャンネルへ分けて流しています。
詳細解説
方法1:Musicを使う
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.sounds[0].set("c2e2g2c3", "p", "6", "n", 20)
pyxel.sounds[1].set("g1g1g1g1", "s", "4", "n", 20)
pyxel.sounds[2].set("c1r c1r c1r c1r", "n", "6", "f", 20)
# music 0 を作る
pyxel.musics[0].set(
[0, 0, 0, 0], # channel 0
[1, 1, 1, 1], # channel 1
[2, 2, 2, 2], # channel 2
[], # channel 3 は未使用
)
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
pyxel.playm(0)
def draw(self):
pyxel.cls(0)
pyxel.text(30, 40, "PRESS Z TO PLAY BGM", 7)
App()
このコードで何をしているのか
pyxel.musics[0].set(...)
Music番号0に、各チャンネルで鳴らすSound列を登録しています。
APIでは Music.set(*seqs) で、各チャンネルの Sound番号リストを渡します。空リストは未使用チャンネルです。例として musics[0].set([0,1], [], [3]) も示されています。
pyxel.playm(0)
Music番号0を再生します。
playm() は Music再生専用です。
方法2:play() をチャンネルごとに使う
これは公式サンプル 04_sound_api.py に近い考え方です。
各チャンネルを明示的に使い分けるので、構造が分かりやすいです。
# melody
pyxel.play(0, [0, 1], loop=True)
# bass
pyxel.play(1, [2, 3], loop=True)
# drums
pyxel.play(2, 4, loop=True)
どちらを選ぶべきか
playm() が向いている場面
- 曲としてまとまった単位で扱いたい
- タイトル曲、通常ステージ曲、ボス曲のように管理したい
- 停止・再生を
Music番号基準で行いたい
play() を分ける方法が向いている場面
- チャンネルごとのON/OFFをしたい
- ドラムだけ切る、ベースだけ切る、メロディだけ再生するなどを試したい
- 構造理解やデバッグをしやすくしたい
実務でのおすすめ
まずは playm() を覚えると、「BGMを流す」という目的には最短です。
その後、必要なら play() でのチャンネル分解再生も覚える、という順番が理解しやすいです。
ここまで理解できれば十分なこと
- BGMは
playm()が基本 -
Musicは複数チャンネルの Sound列 - 必要なら
play()を複数チャンネルに分けてBGM扱いしてもよい
ここまで分かれば、細かい構築は Music.set()、playm()、04_sound_api.py を見れば十分です。
3. BGMをループさせたい
概要
play() も playm() も loop 引数を持っており、True にするとループ再生になります。
API定義でも両方に loop: bool = False があり、04_sound_api.py でも pyxel.play(..., loop=True) が使われています。
詳細解説
Musicをループさせる例
pyxel.playm(0, loop=True)
Sound列をループさせる例
pyxel.play(0, [0, 1], loop=True)
pyxel.play(1, [2, 3], loop=True)
pyxel.play(2, 4, loop=True)
何がループするのか
playm(0, loop=True)
Music番号0に登録された各チャンネルの Sound列全体が最後まで行ったあと、先頭から繰り返されます。API上、Music は各チャンネルのSound列です。
play(0, [0, 1], loop=True)
チャンネル0で、Sound0 → Sound1 → Sound0 → Sound1 ... と繰り返します。
snd にリストを渡せるのは play() の仕様です。
BGMループで実務上大事なこと
1. 曲の終端が自然につながるように作る
ループ再生は、技術的には loop=True で済みます。
ただし音楽として自然かどうかは別です。
最後のSoundの終わりから、最初のSoundに戻ったときに違和感がないように、曲構成を考える必要があります。
2. ドラムやベースの長さをそろえる
複数チャンネルに分けて play() する場合、
チャンネルごとのSound列の長さや総再生時間がずれていると、ループのたびにズレた印象になることがあります。
Music を使うか、各チャンネルの構成をそろえると扱いやすいです。
3. 停止と再開も考える
タイトル画面 → ゲーム画面 → ポーズ画面 のようにシーンが分かれるなら、
ループ再生の開始位置や停止タイミングも設計に入ってきます。
sec も使える
play() と playm() は sec を持っているので、
曲の途中から再生することもできます。これは Pyxel 2.4.0 で tick ではなく 秒ベース に更新されました。
ただし、まずは通常の loop=True だけ使えれば十分です。
ここまで理解できれば十分なこと
- ループは
loop=True -
play()でもplaym()でも使える - 技術的なループと、音楽的に自然なループは別問題
- まずは
playm(0, loop=True)を覚えれば十分
より細かな挙動は play() / playm() のAPIリファレンスを見れば十分です。
4. 音量を調整したい
概要
Pyxelではチャンネル単位の音量調整として channels[ch].gain が使えます。
APIでは Channel.gain は「The gain (volume) of the channel. Defaults to 0.125.」と定義されています。
つまり、全体共通のマスター音量というより、各チャンネルごとの音量を調整する設計 です。
詳細解説
一番単純な例
# channel 0 の音量を小さくする
pyxel.channels[0].gain = 0.05
# channel 1 の音量を大きめにする
pyxel.channels[1].gain = 0.2
実際に使う例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.sounds[0].set("c3e3g3c4", "p", "7", "f", 20)
self.volume = 0.125
pyxel.channels[0].gain = self.volume
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_UP, hold=15, repeat=3):
self.volume = min(self.volume + 0.01, 0.5)
pyxel.channels[0].gain = self.volume
if pyxel.btnp(pyxel.KEY_DOWN, hold=15, repeat=3):
self.volume = max(self.volume - 0.01, 0.0)
pyxel.channels[0].gain = self.volume
if pyxel.btnp(pyxel.KEY_Z):
pyxel.play(0, 0)
def draw(self):
pyxel.cls(0)
pyxel.text(20, 20, f"GAIN: {self.volume:.3f}", 7)
pyxel.text(20, 40, "UP/DOWN: volume", 7)
pyxel.text(20, 50, "Z: play sound", 7)
App()
volumes と gain の違い
ここは混同しやすいので分けて考えると分かりやすいです。
Sound.volumes
音そのもののデータに含まれる音量変化です。
各ノートごとの強さを 0-7 で持ちます。Sound設計の一部です。
Channel.gain
再生チャンネル全体にかかる音量です。
ミキサーのフェーダーに近いイメージです。
つまり、
-
volumes
音色データ内の細かい抑揚 -
gain
チャンネル全体の最終音量
と考えると整理しやすいです。
実務でどう使うか
効果音とBGMで別々に調整
たとえば、
- ch0 = BGMメロディ
- ch1 = BGMベース
- ch2 = ドラム
- ch3 = 効果音
のようにしておけば、BGM側だけ全体を下げたり、効果音だけ大きめにしたりしやすいです。
オプション画面の音量設定
設定値を 0.0〜0.2 程度で持ち、
シーン開始時に各チャンネルへ反映すると実装しやすいです。
ここまで理解できれば十分なこと
- 音量調整は
channels[ch].gain - Soundデータ内の
volumesとは別物 - BGMと効果音でチャンネルを分けると音量設計しやすい
細かなバランス調整は Channel.gain と自分のチャンネル設計を見れば十分です。
5. 複数の音を同時に鳴らしたい
概要
Pyxelは 4チャンネル あるので、
別のチャンネルを使えば複数の音を同時再生できます。
これは Pyxel の基本仕様のひとつです。公式リポジトリでも 4 sound channels と説明されています。
詳細解説
一番単純な例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.sounds[0].set("c3e3g3", "p", "7", "f", 20) # 効果音A
pyxel.sounds[1].set("c2r c2r", "n", "6", "f", 20) # 効果音B
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
pyxel.play(0, 0)
if pyxel.btnp(pyxel.KEY_X):
pyxel.play(1, 1)
def draw(self):
pyxel.cls(0)
pyxel.text(20, 30, "Z: SOUND A (CH0)", 7)
pyxel.text(20, 40, "X: SOUND B (CH1)", 7)
App()
何が起きているのか
- Zを押すとチャンネル0でSound0
- Xを押すとチャンネル1でSound1
が鳴ります。
同じタイミングで押せば、別チャンネルなので同時に鳴ります。
同じチャンネルに流すとどうなるか
同じチャンネルに新しい音を流すと、そのチャンネルの再生内容は上書きされます。
だから「同時に鳴らしたい」なら、チャンネルを分ける必要がある と考えると分かりやすいです。
APIとして play(ch, ...) がチャンネル指定であることからも、この設計です。
BGM + 効果音も同じ考え方
たとえば
- ch0 = BGMメロディ
- ch1 = BGMベース
- ch2 = ドラム
- ch3 = 効果音
のように決めておけば、BGMを鳴らしながら効果音を重ねられます。
04_sound_api.py でも、複数チャンネルを分けて再生しています。
resume が便利な場面もある
play() には resume があります。
これは「短い効果音を鳴らしたあと、前の再生に戻る」用途に使えます。
resume は 2.1.0 で追加されました。APIでも resume: Resume previous sound after playback ends と説明されています。
たとえば、
pyxel.play(0, 5, resume=True)
のようにすると、チャンネル0で短い音を鳴らしたあと、前の再生へ戻すことを狙えます。
実務でのチャンネル設計例
小規模
- ch0 = BGM
- ch1 = 効果音
- ch2 = 予備
- ch3 = 予備
しっかり作る場合
- ch0 = メロディ
- ch1 = ベース
- ch2 = ドラム / 和音
- ch3 = 効果音専用
ここまで理解できれば十分なこと
- 同時再生はチャンネルを分ける
- Pyxelは4チャンネル
- 同じチャンネルでは上書きされる
- BGMと効果音でチャンネルを分けると設計しやすい
ここまで分かれば、あとは play() とチャンネル設計を詰めれば十分です。
6. 音を途中で止めたい
概要
停止は pyxel.stop(ch) または pyxel.stop() です。
APIでは、引数ありならそのチャンネルだけ、引数なしなら全チャンネル停止です。
Channel.stop() もありますが、まずは pyxel.stop() を覚えると分かりやすいです。
詳細解説
一番小さい例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.sounds[0].set("c3e3g3c4 c4g3e3c3", "p", "7", "n", 20)
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
pyxel.play(0, 0, loop=True)
if pyxel.btnp(pyxel.KEY_X):
pyxel.stop(0)
def draw(self):
pyxel.cls(0)
pyxel.text(20, 30, "Z: PLAY LOOP", 7)
pyxel.text(20, 40, "X: STOP CH0", 7)
App()
全部止めたいとき
pyxel.stop()
これは、すべてのチャンネル再生を止めます。
たとえばシーン切り替えやタイトルへ戻るときに便利です。APIでも「Call without arguments to stop playback of all channels」と明記されています。
実務でどう使うか
1. シーン切り替え
タイトルBGMを止めて、ゲーム画面のBGMへ切り替える
pyxel.stop()
pyxel.playm(1, loop=True)
2. ポーズ
ゲーム中の音を止める、あるいは一部だけ止める
3. 効果音の重複防止
同じ効果音が重なりすぎる前に、特定チャンネルを止めてから流し直す
再生中かどうか見たいとき
pyxel.play_pos(ch) を使うと、そのチャンネルの再生位置を (sound_no, sec) で取得できます。
止まっていれば None です。 API定義そのままです。
pos = pyxel.play_pos(0)
if pos is None:
print("stopped")
else:
sound_no, sec = pos
print(sound_no, sec)
ここまで理解できれば十分なこと
- 停止は
stop(ch)またはstop() - チャンネル単位にも全体にも止められる
- 再生状態確認は
play_pos()
ここまで分かれば、APIリファレンス以上に複雑なことはほとんどありません。
7. 自動でBGMを生成したい
概要
Pyxelには gen_bgm(preset, transp, instr, seed, play=False) があります。
これはアルゴリズムで BGM用のMML文字列リスト を生成する関数です。
APIでは preset (0-7)、transp (-5 to +5)、instr (0-3)、seed を取り、play=True ならその場で再生もできます。戻り値は List of MML strings です。 changelog では 2.3.5 で 8bit BGM generator の更新が入っています。
詳細解説
一番小さい例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.mmls = pyxel.gen_bgm(0, 0, 3, 12345, play=False)
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
# 生成したMMLを各チャンネルで再生
for ch, mml in enumerate(self.mmls):
pyxel.play(ch, mml, loop=True)
def draw(self):
pyxel.cls(0)
pyxel.text(20, 20, "PRESS Z TO PLAY AUTO BGM", 7)
for i, mml in enumerate(self.mmls):
pyxel.text(10, 40 + i * 10, f"CH{i}: {mml[:20]}...", 6)
App()
このコードで何をしているのか
pyxel.gen_bgm(0, 0, 3, 12345, play=False)
自動生成BGMを作っています。
戻り値は、各チャンネル用のMML文字列リストです。APIどおりです。
for ch, mml in enumerate(self.mmls): pyxel.play(ch, mml, loop=True)
play() は MML文字列にも対応しているので、生成結果をそのまま各チャンネルへ流せます。
MML文字列対応は 2.4.0 で追加されています。
gen_bgm() の引数を具体的に見る
preset
曲調の大枠です。
APIでは次のように定義されています。
-
0-1
title / departure(中速) -
2-3
town / peaceful(低速) -
4-5
field / adventure(中速) -
6-7
battle / crisis(高速)
transp
移調です。
半音単位で -5〜+5 を指定します。
同じ曲調でも高さを変えられます。
instr
編成です。APIでは次の4種類です。
-
0
melody + reverb + bass(3ch) -
1
melody + bass + drums(3ch) -
2
melody + sub + bass(3ch) -
3
melody + sub + bass + drums(4ch)
seed
乱数シードです。
同じ値を使えば同じ生成結果になります。
試作時にはかなり便利です。
play=True を使う方法もある
pyxel.gen_bgm(4, 0, 1, 9999, play=True)
これなら生成と同時に再生されます。
まず雰囲気を試したいときには便利です。
ただしゲームに組み込むなら、一度戻り値を受けておいたほうが管理しやすいことが多いです。APIで play があることが明記されています。
実務での使い道
- 仮BGMをすぐ用意したい
- タイトルやフィールドの雰囲気曲を素早く試したい
- 作曲前のプロトタイピング
- ランダム性のあるゲームでBGMにも変化を付けたい
生成結果を保存したい場合
Pyxelには Sound.save(filename, sec, ffmpeg=False) と Music.save(filename, sec, ffmpeg=False) があり、指定秒数の .wav を保存できます。
必要なら ffmpeg=True で MP4 作成も可能ですが FFmpeg が必要です。APIで Output WAV file name と明記されています。
例:保存ファイル構成
project/
├── main.py
└── outputs/
└── bgm_preview.wav
ただし gen_bgm() の戻り値はMML文字列リストなので、保存するなら play() / playm() で組み立てたうえで Sound / Music に落とし込むか、再生内容を別途設計する必要があります。
まずは「生成してその場で使う」理解で十分です。
ここまで理解できれば十分なこと
-
gen_bgm()は自動でBGM用MMLを生成する - 戻り値はチャンネルごとのMML文字列リスト
-
preset、transp、instr、seedで雰囲気を調整できる - 試作・仮BGM用途でかなり便利
細かな活用は gen_bgm() のAPIと、自分のプロジェクトでの扱い方を詰めれば十分です。
8. ogg や wav や mp3 の音楽ファイルを読み込ませたい
できます
外部音楽ファイルを使いたい場合は、pyxel.sounds[n].pcm("ファイル名") を使います。
そのあと、pyxel.play(ch, n, loop=True) のようにして再生します。
詳細解説
一番小さい例
ファイル構成
project/
├── main.py
└── sounds/
├── bgm.ogg
├── bgm.wav
└── bgm.mp3
コード例1:OGGを再生する
import pyxel
class App:
def __init__(self):
pyxel.init(220, 120)
pyxel.sounds[0].pcm("sounds/bgm.ogg")
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
pyxel.play(0, 0, loop=True)
if pyxel.btnp(pyxel.KEY_X):
pyxel.stop(0)
def draw(self):
pyxel.cls(0)
pyxel.text(20, 30, "Z: play OGG", 7)
pyxel.text(20, 40, "X: stop", 7)
App()
コード例2:MP3を再生する
import pyxel
class App:
def __init__(self):
pyxel.init(220, 120)
pyxel.sounds[0].pcm("sounds/bgm.mp3")
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
pyxel.play(0, 0, loop=True)
if pyxel.btnp(pyxel.KEY_X):
pyxel.stop(0)
def draw(self):
pyxel.cls(0)
pyxel.text(20, 30, "Z: play MP3", 7)
pyxel.text(20, 40, "X: stop", 7)
App()
コード例3:WAVを再生する
import pyxel
class App:
def __init__(self):
pyxel.init(220, 120)
pyxel.sounds[0].pcm("sounds/bgm.wav")
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
pyxel.play(0, 0, loop=True)
if pyxel.btnp(pyxel.KEY_X):
pyxel.stop(0)
def draw(self):
pyxel.cls(0)
pyxel.text(20, 30, "Z: play WAV", 7)
pyxel.text(20, 40, "X: stop", 7)
App()
2. このコードで何をしているのか
pyxel.sounds[0].pcm("sounds/bgm.mp3")
これは、Sound番号0を「通常のノート列Sound」ではなく、
外部音声ファイルを再生するSound として使う、という意味です。
ここで重要なのは、ファイルを直接 play() に渡しているわけではない ことです。
手順としては次の2段階です。
-
pcm()で Sound に読み込む -
play()で再生する
つまり、
pyxel.sounds[0].pcm("sounds/bgm.mp3")
pyxel.play(0, 0, loop=True)
は、
- Sound 0 に
bgm.mp3を登録 - Channel 0 で Sound 0 を再生
という意味です。
pyxel.play(0, 0, loop=True)
これは、
- 再生チャンネル 0
- Sound番号 0
- ループ再生あり
という指定です。
Pyxelは複数チャンネルを持っているので、
外部ファイル再生であっても、どのチャンネルで鳴らすか という考え方は通常のSound再生と同じです。
pyxel.stop(0)
これはチャンネル0だけ停止します。
つまり、BGMを止めたいときは
pyxel.stop(0)
で十分です。
全部止めたいなら
pyxel.stop()
でも構いません。
3. ドキュメントには ogg / wav しか書かれていないのに、mp3 が再生できるのはどう理解すべきか
ここはかなり大事です。
実務上の理解
今回の前提として、
- 公式ドキュメントには mp3 の明記がない
- しかし 実際には mp3 も ogg と同じ方法で再生可能
という状況です。
この場合、記事としては次のように理解しておくのが自然です。
1. まず「実際に使える」ことを優先してよい
実動作として再生できるなら、実用上は使えます。
2. ただし「ドキュメント明記済みの保証」と「実際に動く」は別
たとえば今後のバージョン差や環境差を考えると、
- 明記されている形式
- 実際に再生できる形式
は必ずしも同じとは限りません。
3. 記事ではその違いを書いておくと親切
つまり、
ドキュメント上は主に ogg / wav の説明だが、実際には mp3 も同様に使える
と書いておくと、読者が混乱しにくくなります。
4. 実務ではどの形式を選ぶべきか
まず結論
実務では次のように考えると分かりやすいです。
-
既に mp3 素材がある
→ そのまま使ってよい -
新しく用意する
→ ogg も有力 -
短い効果音
→ wav も扱いやすい -
Pyxelで外部音源BGMを使いたい
→ mp3 / ogg / wav のいずれでも実用可能
形式ごとの考え方
.mp3
- よくある音楽素材形式
- 手持ち素材が多いことが多い
- 実際に再生できるなら、そのまま使える
.ogg
- ゲーム用途でよく使われることが多い
- ループBGM用途と相性がよい場面がある
- Pyxel文脈でも扱いやすい
.wav
- 非圧縮でサイズが大きくなりやすい
- 効果音や短い素材向き
- デコードまわりのトラブルが少ないことがある
5. BGMとして使うときの実用的な構成
一番わかりやすい構成
ファイル構成
project/
├── main.py
└── sounds/
├── title_bgm.mp3
├── field_bgm.ogg
├── battle_bgm.ogg
└── click.wav
コード例
import pyxel
class App:
def __init__(self):
pyxel.init(240, 140)
pyxel.sounds[0].pcm("sounds/title_bgm.mp3")
pyxel.sounds[1].pcm("sounds/field_bgm.ogg")
pyxel.sounds[2].pcm("sounds/battle_bgm.ogg")
pyxel.sounds[3].pcm("sounds/click.wav")
self.mode = "title"
pyxel.run(self.update, self.draw)
def play_bgm_for_mode(self):
pyxel.stop()
if self.mode == "title":
pyxel.play(0, 0, loop=True)
elif self.mode == "field":
pyxel.play(0, 1, loop=True)
elif self.mode == "battle":
pyxel.play(0, 2, loop=True)
def update(self):
if pyxel.btnp(pyxel.KEY_1):
self.mode = "title"
self.play_bgm_for_mode()
if pyxel.btnp(pyxel.KEY_2):
self.mode = "field"
self.play_bgm_for_mode()
if pyxel.btnp(pyxel.KEY_3):
self.mode = "battle"
self.play_bgm_for_mode()
if pyxel.btnp(pyxel.KEY_Z):
pyxel.play(3, 3) # 効果音
def draw(self):
pyxel.cls(0)
pyxel.text(20, 20, f"mode: {self.mode}", 7)
pyxel.text(20, 50, "1: title BGM", 7)
pyxel.text(20, 60, "2: field BGM", 7)
pyxel.text(20, 70, "3: battle BGM", 7)
pyxel.text(20, 90, "Z: click SE", 10)
App()
6. この構成の何がよいのか
BGMと効果音を番号で整理できる
-
sounds[0]= タイトルBGM -
sounds[1]= フィールドBGM -
sounds[2]= 戦闘BGM -
sounds[3]= クリック音
のように、役割が分かりやすくなります。
再生方法が統一される
読み込むファイル形式が mp3 / ogg / wav で違っていても、
再生時は全部
pyxel.play(channel, sound_no, loop=...)
で統一できます。
つまり、形式の違いは pcm() の時点で吸収し、再生は全部同じ流れにする
という設計にすると扱いやすいです。
7. 配布時に何を一緒に持たせるべきか
外部音声ファイルを使うなら、そのファイルも配布物に含める必要があります。
たとえば次のコードを使っているなら、
pyxel.sounds[0].pcm("sounds/title_bgm.mp3")
配布時には
sounds/title_bgm.mp3
が必要です。
例:配布対象ディレクトリ
my_game/
├── main.py
└── sounds/
├── title_bgm.mp3
├── field_bgm.ogg
└── click.wav
このように、実行時に参照される相対パスが成立する形でまとめておく 必要があります。
これは画像やフォントと同じ考え方です。
8. どういう方針で採用するとよいか
一番おすすめの考え方
1. すでに持っている素材を優先する
mp3 が既にあるなら、そのまま使えるならそれでよいです。
2. 形式ごとの再生方法を統一する
どの形式でも、
pcm()play()stop()
で扱う、という流れを共通化すると管理しやすいです。
3. コード内では「役割」で管理する
形式ではなく、
- タイトルBGM
- フィールドBGM
- 効果音
のように、役割で番号を管理すると後で差し替えしやすいです。
まとめ
Pyxelのサウンド・音楽まわりは、実質的には次の構造を押さえるとかなり整理できます。
-
Sound
効果音や短いフレーズ -
Music
複数チャンネルの Sound列からなる曲 -
Channel
実際に再生する4つの音声チャンネル -
再生API
play()/playm()/stop()/play_pos()
整理するとこうです。
-
効果音を鳴らす
sounds[n].set(...)→play(ch, n) -
BGMを流す
musics[n].set(...)→playm(n)
またはplay()を複数チャンネルに分けて使う -
BGMをループさせる
loop=True -
音量を調整する
channels[ch].gain -
複数の音を同時に鳴らす
別チャンネルを使う -
途中で止める
stop(ch)/stop() -
自動でBGMを生成する
gen_bgm(...)
ここまで理解できたら、次に見るべきは Sound、Music、Channel、play()、playm()、stop()、gen_bgm() のAPIリファレンスと 04_sound_api.py のサンプルです。そこから先は「Pyxelで何ができるか」を知る段階ではなく、「自分のゲームの音設計をどうするか」を詰める段階に入ります。
📝 テキスト・フォント編
Pyxelで文字やフォントを扱うときに最初に知りたくなる内容を、逆引き形式でまとめます。
対象にする項目は次の7つです。
- 日本語を表示したい
- フォントを変更したい
- 文字サイズを変えたい
- 等幅フォントで表示したい
- 複数行テキストを表示したい
- テキストを中央揃えにしたい
- 長文テキストをスクロールさせたい
まず最初に:Pyxelのテキスト表示はどう考えればよいか
Pyxelで文字を表示するときの基本は pyxel.text(x, y, s, col, font=None) です。
この font を省略すると標準フォントで描かれ、pyxel.Font(...) で作ったフォントを渡すと、そのフォントで描画されます。 Font は .bdf / .otf / .ttf / .ttc を読み込め、text_width() で文字列の表示幅を取得できます。
まずは最小構成です。
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, "HELLO", 7)
App()
この段階では、次の2つを分けて考えると理解しやすいです。
1. 「文字を出す」こと自体
これは pyxel.text() の役目です。
座標、文字列、色、必要ならフォントを指定して描きます。
2. 「どういう見た目で出すか」
これはフォント選びや文字幅計測、改行、中央揃えなどの設計の話です。
Pyxel自体はそこまで高機能な文書レイアウトエンジンではなく、必要な表示ルールを自分で組み立てていく 形です。 Font.text_width() が用意されているのは、そのためです。
1. 日本語を表示したい
概要
現在のPyxelでは、TTF/OTFフォント描画に対応しているので、日本語表示ができます。
これは Pyxel 2.6.0 で追加された機能です。公式サンプル 13_custom_font.py でも、.ttf フォントを使って「気軽に楽しく」「プログラミング!」のような日本語を描画しています。
詳細解説
一番小さい例
ファイル構成
project/
├── main.py
└── fonts/
└── PixelMplus12-Regular.ttf
コード
import pyxel
class App:
def __init__(self):
pyxel.init(200, 120)
self.font = pyxel.Font("fonts/PixelMplus12-Regular.ttf", 12)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.text(10, 20, "こんにちは", 7, self.font)
pyxel.text(10, 40, "設定メニュー", 10, self.font)
App()
このコードで何をしているのか
pyxel.Font("fonts/PixelMplus12-Regular.ttf", 12)
TTFフォントファイルからフォントオブジェクトを作っています。
Font(filename, font_size=10) は、BDF/OTF/TTF/TTC を受け取ります。BDFだけは font_size を使いません。
pyxel.text(..., self.font)
text() の第5引数にフォントを渡して、そのフォントで描いています。
標準フォントのままでは日本語全般を扱う用途には向きにくいので、日本語表示したい場合はこの形が基本になります。公式サンプルでも .ttf を Font で作って text() に渡しています。
どういう場面で使うか
- 日本語メニュー
- 設定画面
- ノベルゲーム
- 説明文
- ランチャーUI
- ログ表示
よくあるつまずき
1. フォントファイルのパスが違う
たとえば fonts/PixelMplus12-Regular.ttf と書いたのに、実際の置き場所が違うと読み込めません。
最初は必ず、スクリプトから見た相対パスが合っているか確認したほうが安全です。
2. 使いたい文字がフォントに入っていない
TTFだからといって、すべての文字が必ず入っているわけではありません。
表示したい記号や漢字が空白になったら、フォント側の収録文字も疑う必要があります。
3. 日本語用フォントはサイズが大きくなりやすい
これはPyxelというより日本語フォント一般の特徴です。
そのため、配布サイズを気にするなら「必要文字だけに絞ったフォント」を使う設計も現実的です。
ただし、まずは通常の .ttf / .otf を読み込んで動かすところから始めれば十分です。Pyxelが読み込める形式自体は .bdf / .otf / .ttf / .ttc です。
ここまで理解できれば十分なこと
- 日本語表示は
Font+text()でできる - フォント形式は
.ttf/.otf/.ttc/.bdf - 日本語表示の中心は「適切なフォントファイルを用意すること」
ここまで分かれば、細かな文字品質やフォント選定はAPIより実フォント運用の話になります。Pyxel側としては Font と text() が分かれば十分です。
2. フォントを変更したい
概要
フォント変更の基本は、pyxel.Font(...) で別フォントを読み込み、それを text() に渡すことです。
Pyxelは Font クラスで .bdf / .otf / .ttf / .ttc を読み込めます。公式サンプル 13_custom_font.py では、.bdf と .ttf の両方を同時に使っています。
詳細解説
例:2種類のフォントを使い分ける
ファイル構成
project/
├── main.py
└── fonts/
├── umplus_j10r.bdf
└── PixelMplus12-Regular.ttf
コード
import pyxel
class App:
def __init__(self):
pyxel.init(220, 120)
self.font_small = pyxel.Font("fonts/umplus_j10r.bdf")
self.font_large = pyxel.Font("fonts/PixelMplus12-Regular.ttf", 12)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, "TITLE", 8, self.font_small)
pyxel.text(10, 40, "設定を開く", 7, self.font_large)
App()
このコードで何をしているのか
.bdf と .ttf を別々に読み込んでいる
Pyxelは両方とも Font で扱えます。
ただし BDF は固定サイズのビットマップフォントなので、font_size は使いません。
これは API の Font 説明に明記されています。
text() ごとにフォントを変えている
text() は呼び出しごとに別フォントを指定できます。
そのため、
- タイトルは見出し用フォント
- 本文は読みやすいフォント
- 補足は小さなフォント
のような使い分けができます。
フォント変更は何のためにやるのか
1. 見た目を変えるため
雰囲気づくりです。
レトロ感の強いBDFを使うか、読みやすいTTFを使うかで印象がかなり変わります。
2. 用途に合わせるため
たとえば、
- タイトル
→ 装飾性のあるフォント - 本文
→ 読みやすいフォント - 数値表示
→ 等幅で揃うフォント
のように、用途ごとに向いたフォントがあります。
3. 日本語対応のため
英数字しかない標準フォントでは足りないので、日本語対応フォントを読み込む必要があります。
TTF/OTF対応は 2.6.0 で追加されました。
公式サンプルに近い例
13_custom_font.py では、
font10 = pyxel.Font("assets/umplus_j10r.bdf")font12 = pyxel.Font("assets/PixelMplus12-Regular.ttf", 12)
のように2種類読み込み、さらに font10.text_width(s) で幅を測っています。
単に描くだけでなく「表示幅を計算して装飾を合わせる」ところまで含めて、かなり参考になります。
ここまで理解できれば十分なこと
- フォント変更は
Font(...)を作ってtext()に渡すだけ -
.bdf/.otf/.ttf/.ttcが使える - 用途ごとに複数フォントを併用できる
ここまで分かれば、詳細は Font API と公式サンプルを見れば十分です。
3. 文字サイズを変えたい
概要
文字サイズの考え方は、使うフォント形式によって違います。
-
.ttf/.otf/.ttc
Font(filename, font_size)でサイズ指定できる -
.bdf
フォント自体が固定サイズなので、font_sizeは使わない
これは Font のAPI説明に明記されています。BDFでは font_size は not used です。
詳細解説
例:サイズ違いのTTFを使う
ファイル構成
project/
├── main.py
└── fonts/
└── PixelMplus12-Regular.ttf
コード
import pyxel
class App:
def __init__(self):
pyxel.init(220, 140)
self.font_10 = pyxel.Font("fonts/PixelMplus12-Regular.ttf", 10)
self.font_16 = pyxel.Font("fonts/PixelMplus12-Regular.ttf", 16)
self.font_24 = pyxel.Font("fonts/PixelMplus12-Regular.ttf", 24)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, "小さい文字", 7, self.font_10)
pyxel.text(10, 40, "普通の文字", 10, self.font_16)
pyxel.text(10, 80, "大きい文字", 8, self.font_24)
App()
このコードで何をしているのか
同じ .ttf でも、サイズ違いの Font を別々に作っています。
Pyxelでは「後から text() 側でサイズだけ変える」のではなく、サイズごとに Font オブジェクトを作って使い分ける のが基本です。 Font.__init__(filename, font_size) の形からもそれが分かります。
どういうときに必要か
- タイトルを大きくしたい
- 本文だけ小さくしたい
- 補助情報を小さめにしたい
- 日本語UIで見出しと本文を分けたい
BDFフォントではどうなるか
BDFはビットマップフォントなので、サイズはフォントデータ側で決まっています。
そのため、BDFで文字サイズを変えたいなら「別サイズのBDFファイルを用意する」考え方になります。
Pyxel APIでも BDF に対しては font_size が使われないとされています。
例:別サイズBDFを使い分けるイメージ
project/
├── main.py
└── fonts/
├── myfont_8.bdf
└── myfont_12.bdf
font8 = pyxel.Font("fonts/myfont_8.bdf")
font12 = pyxel.Font("fonts/myfont_12.bdf")
実務での考え方
TTF/OTF向き
- 日本語UI
- サイズ違いを柔軟に使いたい
- 中央揃えや見出し設計をしやすくしたい
BDF向き
- ドット絵らしい固定ピクセル感を重視したい
- レトロUIらしい見た目を保ちたい
- サイズを厳密に管理したい
ここまで理解できれば十分なこと
- TTF/OTF/TTC は
font_sizeでサイズ指定できる - BDFは固定サイズ
- サイズ違いを使いたいなら Font を複数作る
細かなサイズ感の調整は API というより実画面での見た目確認が重要です。Pyxel側としては Font が分かれば十分です。
4. 等幅フォントで表示したい
概要
Pyxelそのものに「等幅モード切り替え」のようなAPIがあるわけではありません。
等幅表示したい場合は、最初から等幅フォントを選んで読み込む のが基本です。
そのうえで text_width() を使えば、表示幅も測れます。 Font.text_width() は「文字列の表示幅をピクセルで返す」APIです。
詳細解説
なぜ等幅フォントが必要なのか
等幅フォントは、すべての文字が同じ幅で並ぶフォントです。
そのため、次のような用途で特に便利です。
- ファイル一覧
- スコア表
- 設定値の列揃え
- デバッグ表示
- ログ表示
- 表形式のUI
つまり、「位置を揃えて見せたい」場合に向いています。
例:等幅っぽい一覧表示
ファイル構成
project/
├── main.py
└── fonts/
└── MyMonoFont.ttf
コード
import pyxel
class App:
def __init__(self):
pyxel.init(240, 140)
self.font = pyxel.Font("fonts/MyMonoFont.ttf", 12)
self.rows = [
("HP", "003"),
("MP", "015"),
("GOLD", "1240"),
]
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
for i, (name, value) in enumerate(self.rows):
y = 20 + i * 18
pyxel.text(20, y, f"{name:<8} {value:>4}", 7, self.font)
App()
このコードの考え方
Python側で文字列整形をして、等幅フォントで描いています。
フォント自体が等幅なら、HP と GOLD のような長さの違う文字列でも列が揃って見えやすくなります。
Pyxelで等幅が便利な場面
1. 数字の桁揃え
score = 123
pyxel.text(10, 10, f"SCORE {score:06}", 7, self.font)
2. デバッグ情報
pyxel.text(10, 20, f"X={player_x:03} Y={player_y:03}", 7, self.font)
3. ファイル一覧・ROM一覧
名前、サイズ、更新日などを並べるときに見やすくなります。
等幅でないフォントだと何が起きるか
プロポーショナルフォントでは文字ごとに幅が違うので、
桁が揃っているように見せたつもりでも、見た目の列はズレることがあります。
そのため、表っぽい表示は等幅フォントとかなり相性が良いです。
text_width() はどう役立つか
等幅フォントでも、文字列全体の幅を取りたい場面があります。
たとえば、右寄せ・中央揃え・枠幅の自動計算などです。
s = "SAVE SLOT 01"
w = self.font.text_width(s)
pyxel.rect(20, 20, w + 8, 16, 1)
pyxel.text(24, 24, s, 7, self.font)
公式サンプル 13_custom_font.py でも font10.text_width(s) を使って線の長さを決めています。
ここまで理解できれば十分なこと
- 等幅表示したいなら等幅フォントを選ぶ
- Pyxelの専用フラグではなく、フォント選択で実現する
-
text_width()を組み合わせるとUIを作りやすい
ここまで分かれば、あとはフォント選定の問題です。Pyxel側としては Font と text_width() が分かれば十分です。
5. 複数行テキストを表示したい
概要
複数行表示には大きく2つの方法があります。
- 改行入り文字列をそのまま
text()に渡す - 自分で行ごとに分けて
yをずらして描く
Pyxel 2.5.1 では、カスタムフォント描画に改行対応が追加されています。
そのため、現在のPyxelでは改行文字を含む文字列も扱いやすくなっています。
詳細解説
方法1:改行入り文字列を使う
import pyxel
class App:
def __init__(self):
pyxel.init(200, 120)
self.font = pyxel.Font("fonts/PixelMplus12-Regular.ttf", 12)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, "1行目\n2行目\n3行目", 7, self.font)
App()
方法2:自分で行ごとに描く
こちらのほうが、行間や色替えを細かく制御しやすいです。
lines = ["1行目", "2行目", "3行目"]
for i, line in enumerate(lines):
pyxel.text(10, 10 + i * 16, line, 7, self.font)
どちらを使うべきか
改行文字列向き
- 単純な説明文
- 改行位置が固定
- 一括で出したい
行ごと描画向き
- 行ごとに色を変えたい
- 行間を調整したい
- 一部だけ点滅させたい
- スクロール表示へ発展させたい
外部テキストを使う場合
Pyxel自体に「.txt をそのまま読み込んで表示する専用API」はありません。
ただし、Pythonとして普通にテキストファイルを読んで、その内容を text() に渡せば十分です。
ファイル構成
project/
├── main.py
└── texts/
└── intro.txt
intro.txt の例
ようこそ
Pyxelの世界へ
コード
with open("texts/intro.txt", "r", encoding="utf-8") as f:
text_data = f.read()
pyxel.text(10, 10, text_data, 7, self.font)
実務での考え方
複数行テキストは、最初はただの説明文でも、あとで
- 自動折り返し
- スクロール
- 選択肢表示
- 行ごとの演出
へ発展しやすいです。
そのため、簡単な説明文以外は「行リストとして持つ」設計のほうが拡張しやすいことが多いです。
ここまで理解できれば十分なこと
- 複数行は改行文字でも行リストでも描ける
- カスタムフォントの改行対応は 2.5.1 で入っている
- 細かい制御をしたいなら行ごと描画が向いている
ここまで分かれば、あとは表示設計の話になります。Pyxelの機能としては十分理解できています。
6. テキストを中央揃えにしたい
概要
Pyxelには「中央揃え専用関数」はありません。
その代わり、Font.text_width() で文字列幅を測り、自分で x を計算して中央へ置きます。
これは公式サンプル 13_custom_font.py の text_width() 利用例と同じ発想です。
詳細解説
一番基本の例
import pyxel
class App:
def __init__(self):
pyxel.init(200, 120)
self.font = pyxel.Font("fonts/PixelMplus12-Regular.ttf", 12)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
s = "中央揃えテキスト"
w = self.font.text_width(s)
x = (pyxel.width - w) // 2
pyxel.text(x, 50, s, 7, self.font)
App()
このコードで何をしているのか
w = self.font.text_width(s)
その文字列が、今のフォントで何ピクセル幅になるかを取っています。
APIで text_width() は「Return the display width of the string s in this font」と定義されています。
x = (pyxel.width - w) // 2
画面幅から文字列幅を引いて、その半分だけ左側余白にする計算です。
これで中央揃えになります。
複数行を中央揃えしたい場合
各行ごとに幅が違うので、1行ずつ測って中央位置を出します。
lines = ["1行目", "少し長い2行目", "3行目"]
for i, line in enumerate(lines):
w = self.font.text_width(line)
x = (pyxel.width - w) // 2
pyxel.text(x, 20 + i * 16, line, 7, self.font)
標準フォントではどうするか
標準フォントには Font オブジェクトがないので、厳密な幅取得用メソッドは使いません。
その場合は、
- 文字数からざっくり計算する
- あらかじめ固定幅前提で設計する
という簡易法を取ることがあります。
s = "GAME OVER"
x = (pyxel.width - len(s) * 4) // 2
pyxel.text(x, 50, s, 7)
ただし、標準フォントの幅を完全に一般化して扱うより、
中央揃えをしっかりやりたい場面では Font を使ったほうが確実です。
実務でどう使うか
- タイトル画面の見出し
- ダイアログのタイトル
- ゲームオーバー表示
- セクション見出し
- 説明文の中央配置
ここまで理解できれば十分なこと
- 中央揃えは
text_width()で幅を測って自分でxを計算する - 複数行は各行ごとに計算する
- 標準フォントよりカスタムフォントのほうがやりやすい
ここまで分かれば、APIとしては Font.text_width() が理解できていれば十分です。
7. 長文テキストをスクロールさせたい
概要
Pyxelに「長文スクロール専用ウィジェット」はありません。
そのため、考え方としては 複数行テキスト + 表示開始位置の制御 + 必要なら clip() で表示範囲制限 を組み合わせます。
clip() は描画範囲を制限するAPIで、引数なしで全画面に戻せます。これは長文スクロール表示と非常に相性が良いです。
詳細解説
一番実用的な例
ファイル構成
project/
├── main.py
└── texts/
└── story.txt
story.txt の例
これは1行目です。
これは2行目です。
これは3行目です。
これは4行目です。
これは5行目です。
これは6行目です。
コード
import pyxel
class App:
def __init__(self):
pyxel.init(220, 140)
self.font = pyxel.Font("fonts/PixelMplus12-Regular.ttf", 12)
with open("texts/story.txt", "r", encoding="utf-8") as f:
self.lines = f.read().splitlines()
self.scroll_y = 0
self.line_height = 16
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_DOWN, hold=15, repeat=3):
self.scroll_y += self.line_height
if pyxel.btnp(pyxel.KEY_UP, hold=15, repeat=3):
self.scroll_y -= self.line_height
self.scroll_y = max(0, min(self.scroll_y, max(0, len(self.lines) * self.line_height - 80)))
def draw(self):
pyxel.cls(0)
# 表示領域の枠
pyxel.rectb(10, 10, 200, 80, 7)
# この中だけ表示
pyxel.clip(10, 10, 200, 80)
for i, line in enumerate(self.lines):
y = 12 + i * self.line_height - self.scroll_y
pyxel.text(14, y, line, 7, self.font)
# クリップ解除
pyxel.clip()
pyxel.text(10, 105, "UP/DOWN: scroll", 5, self.font)
App()
このコードで何をしているのか
self.lines
長文を1行ずつ分けたリストです。
長文表示では「まず行の配列にする」のが扱いやすいです。
self.scroll_y
何ピクセルぶん上へスクロールしたかを表します。
表示位置をずらす基準です。
pyxel.clip(10, 10, 200, 80)
この範囲の中だけ描くようにしています。
そのため、長文がはみ出しても枠外には見えません。 clip() は描画範囲制限用です。
y = 12 + i * self.line_height - self.scroll_y
行ごとの基準位置から、スクロール量だけ引いています。
これで見た目上、テキストが上下に動きます。
実務での応用
1. ノベルゲーム風のログ
過去の会話をスクロール閲覧する
2. 設定説明
長めのヘルプ文章をウィンドウ内で読ませる
3. クレジット
エンドロールのように自動で上へ流す
self.scroll_y += 1
のようにすれば自動スクロールになります。
自動折り返しはどうするか
Pyxelに高機能な文章レイアウトエンジンがあるわけではないので、
折り返ししたい場合は、
- 事前に改行済みのテキストを用意する
-
text_width()を使って自前で折り返し処理を書く
という方向になります。
まずは「行に分ける」「スクロールする」だけ理解できれば十分です。
より高度な折り返しは、そこから先のアプリ設計の段階です。 text_width() があるので、自前折り返しの基礎は作れます。
ここまで理解できれば十分なこと
- 長文スクロールは「行リスト + scroll_y + clip()」で作る
-
.txtを読んでsplitlines()すれば使いやすい - 自動折り返しは自前実装の領域
- まずは表示範囲制限とスクロールの考え方を押さえれば十分
ここまで分かれば、Pyxelの機能理解としては十分で、あとはUI設計の話になります。
まとめ
Pyxelのテキスト・フォントまわりは、実質的には次の3つを押さえるとかなり整理できます。
-
文字を描く
text() -
フォントを作る
Font(...) -
文字幅を測る
text_width()
整理するとこうです。
-
日本語を表示する
Font("xxx.ttf", size)やFont("xxx.otf", size)をtext()に渡す -
フォントを変更する
.bdf/.otf/.ttf/.ttcを読み込んで使い分ける -
文字サイズを変える
TTF/OTF/TTC はfont_size、BDF は固定サイズ -
等幅フォントで表示する
等幅フォントを選んでtext_width()と組み合わせる -
複数行テキストを表示する
改行文字または行リストで描く -
中央揃えする
text_width()で幅を測ってxを計算する -
長文をスクロールさせる
行リスト +scroll_y+clip()
ここまで理解できたら、次に見るべきは Font、text()、text_width()、clip() のAPIリファレンスと、公式サンプル 13_custom_font.py です。そこから先は「Pyxelで何ができるか」を知る段階ではなく、「自分のUIで文字をどう見せるか」を詰める段階に入ります。
📦 リソース管理編
Pyxelで画像・音・マップなどの素材をどう管理するかを、逆引き形式でまとめます。
対象にする項目は次の6つです。
- 画像・音・マップをまとめて管理したい
- リソースファイルを読み込みたい
- リソースを外部ファイルとして分離したい
- エディタで作った素材を使いたい
- プロジェクト構成を整理したい
- 素材だけ差し替えたい
まず最初に:Pyxelの「リソース管理」とは何か
Pyxelでいうリソース管理とは、コードとは別に、画像・タイルマップ・サウンド・Musicなどの素材をまとめて持ち、それを必要なときに読み込んで使うことです。Pyxelには .pyxres という専用のリソースファイルがあり、pyxel.load() で読み込み、pyxel.save() で保存できます。API上も load(filename, exclude_images=False, exclude_tilemaps=False, exclude_sounds=False, exclude_musics=False) と save(...) が用意されており、画像・タイルマップ・サウンド・Musicをまとめて扱う前提になっています。
この仕組みを使うと、たとえばコード側は「画像バンク0のこの位置を描く」「タイルマップ0を表示する」「Sound 3を鳴らす」とだけ書き、実際の画像やマップや音の中身は .pyxres 側に持たせられます。
つまり、ゲームの動作を書く場所 と ゲームに使う素材を持つ場所 を分けられます。これはPyxelのAPI設計そのものに沿った使い方です。
1. 画像・音・マップをまとめて管理したい
概要
Pyxelでは、画像・タイルマップ・サウンド・Musicを1つの .pyxres ファイルにまとめて保存できます。
これは「ゲーム本体のコード」と「ゲームで使う素材」を分けて管理したいときに非常に便利です。pyxel.load() と pyxel.save() は、どちらも .pyxres を対象にしており、必要に応じて画像・タイルマップ・サウンド・Musicの読み書きを個別に除外できます。
詳細解説
どういう意味で「まとめて管理」なのか
ここで言う「まとめて管理」とは、単にファイルを1つにするだけではありません。
より具体的には、次のように役割を分けることです。
コード側が持つもの
- プレイヤーをどう動かすか
- 敵がどう動くか
- どの場面でどの画像を表示するか
- どのタイミングでどの音を鳴らすか
リソース側が持つもの
- スプライト画像
- タイルマップ
- 効果音
- BGMの素材
つまり、「どう動くか」はコード、
「何を表示・再生するか」はリソース
という分担です。これは images、tilemaps、sounds、musics を別々のAPIで持ちながら、それを .pyxres で一括保存・読込できるPyxelの設計と一致しています。 :contentReference[oaicite:4]{index=4}
具体例
たとえば、次のような構成を考えます。
project/
├── main.py
└── game.pyxres
この場合、
-
main.py
プレイヤー操作、シーン切り替え、当たり判定などを書く -
game.pyxres
画像、タイルマップ、Sound、Musicを入れる
という役割分担になります。
コード側では pyxel.load("game.pyxres") を呼ぶだけで、画像バンク・タイルマップ・音素材がまとめて読み込まれます。APIの load() はまさにそのための関数です。
何が嬉しいのか
1. 素材を触ってもコードを壊しにくい
画像やマップを変えたいだけなのに、コードの中の数値や配列まで触る必要がなくなります。
2. 作業場所が分かれる
コード修正と素材修正が分離されるので、何をどこで直すべきか判断しやすくなります。
3. 後から見返したときに理解しやすい
特に規模が大きくなるほど、「これはゲームロジック」「これは素材データ」という分離は保守のしやすさに直結します。
ここまで理解できれば十分なこと
-
.pyxresは画像・タイルマップ・サウンド・Musicをまとめるファイル -
load()/save()で扱う - コードと素材を分けるための仕組み
ここまで分かれば、細かな内部形式より先に「自分のプロジェクトをどう分けるか」を考える段階です。Pyxel側の機能としては load() と save() が理解できていれば十分です。
2. リソースファイルを読み込みたい
概要
.pyxres を読み込むには pyxel.load("ファイル名.pyxres") を使います。
API上も load(filename, exclude_images=False, exclude_tilemaps=False, exclude_sounds=False, exclude_musics=False) と定義されており、リソースファイル .pyxres を読み込む関数だと明記されています。さらに、同名の .pyxpal が存在すれば、パレット表示色も一緒に更新されます。
詳細解説
一番小さい例
ファイル構成
project/
├── main.py
└── game.pyxres
コード
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.load("game.pyxres")
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
# 画像バンク0の左上16x16を表示
pyxel.blt(20, 20, 0, 0, 0, 16, 16, colkey=0)
# タイルマップ0を表示
pyxel.bltm(60, 20, 0, 0, 0, 8, 8)
# テキストだけコード側で描く
pyxel.text(20, 90, "resource loaded", 7)
App()
このコードで何をしているのか
pyxel.load("game.pyxres")
この1行で、game.pyxres の中に保存されている画像・タイルマップ・サウンド・Musicがメモリ上へ読み込まれます。
その後、pyxel.images[0] や pyxel.tilemaps[0] や pyxel.sounds[0] などが使える状態になります。API上、load() は .pyxres の読み込み関数です。
pyxel.blt(...)
読み込まれた画像バンク0の一部を描いています。
画像バンクの内容自体は .pyxres 側にあるので、コードには「どこを描くか」だけが書かれています。blt() は画像バンク 0-2 または Image から指定領域を描くAPIです。
pyxel.bltm(...)
読み込まれたタイルマップ0を描いています。
タイルマップの並びそのものは .pyxres 側にあるため、コード側は表示命令だけで済みます。 bltm() はタイルマップ 0-7 を描くAPIです。
一部だけ読み込みたくない場合
たとえば「画像とタイルマップだけ読み込み、音は除外したい」場合は、exclude_* を使います。
pyxel.load(
"game.pyxres",
exclude_sounds=True,
exclude_musics=True,
)
これは、画像とタイルマップだけを使いたいツールや、起動時の素材読み込みを段階的に分けたい場面で便利です。APIでも、画像・タイルマップ・Sound・Musicを個別に除外できると説明されています。
.pyxpal も知っておくとよい
Pyxelには .pyxpal というパレットファイルもあり、load_pal() / save_pal() で扱えます。
さらに、load("game.pyxres") 時には 同名の .pyxpal があれば表示色も更新される とAPIに書かれています。
つまり、
game.pyxres
game.pyxpal
のように置いておけば、リソース本体と色設定をセットで運用できます。
ここまで理解できれば十分なこと
-
.pyxresの読み込みはpyxel.load() - 必要なら
exclude_imagesなどで一部を除外できる - 同名の
.pyxpalがあるとパレットも連動する
ここまで分かれば、APIリファレンス以上に難しいことはあまりありません。あとは「どのタイミングで何を読み込むか」という設計の話です。
3. リソースを外部ファイルとして分離したい
概要
これはPyxelのリソース運用で最も重要な考え方の1つです。
「外部ファイルとして分離する」とは、画像やマップや音のデータを Pythonコードの中に直接ベタ書きせず、.pyxres や .png や .tmx などの外部ファイルに分ける ことです。Pyxelは .pyxres の一括運用に加え、画像は .png / .gif / .jpeg、タイルマップは .tmx も扱えます。
詳細解説
分離しないとどうなりやすいか
たとえば、コードの中に画像をこう書くことはできます。
pyxel.images[0].set(0, 0, [
"00033000",
"00333300",
"033cc330",
"03cccc30",
"03cccc30",
"033cc330",
"00333300",
"00033000",
])
これは小さな試作では便利ですが、画像が増えたり、マップが大きくなったり、音数が増えたりすると、コードファイルが「ゲームロジック」と「素材データ」の両方を抱えることになります。
その結果、どこを直せば何が変わるのかが分かりにくくなりやすいです。Image.set() は便利ですが、あくまで小規模な直接定義向けです。
分離する場合の代表例
例1:Pyxel標準の .pyxres にまとめる
project/
├── main.py
└── game.pyxres
例2:画像だけ外部 .png にする
project/
├── main.py
└── assets/
└── player.png
pyxel.images[0].load(0, 0, "assets/player.png")
Image.load() は .png / .gif / .jpeg を読み込めます。
例3:タイルマップを .tmx にする
project/
├── main.py
└── maps/
└── field.tmx
pyxel.tilemaps[0] = pyxel.Tilemap.from_tmx("maps/field.tmx", 0)
Tilemap.from_tmx() は .tmx ファイルからタイルマップを作るAPIです。
何が具体的に楽になるのか
1. 素材だけ直せる
画像を差し替えたいとき、Pythonコードを触らずに済みます。
2. 再利用しやすい
同じ .pyxres や .png を別のコードからも使いやすくなります。
3. 役割が明確になる
-
main.py= ロジック -
assets/*.png= 画像 -
maps/*.tmx= マップ -
game.pyxres= Pyxel素材集
のように、役割がファイル単位で見えるようになります。
どこまで分離すべきか
小規模な試作
全部 .pyxres にまとめるだけでも十分実用的です。
中規模以上
- 共通画像は
.png - エリアマップは
.tmx - 効果音や小物素材は
.pyxres
のように、運用しやすい単位で分けることもあります。
ここまで理解できれば十分なこと
- 分離とは「素材をコード外へ出すこと」
- Pyxel標準なら
.pyxres - 必要に応じて
.pngや.tmxも使える - 分離の目的は保守性と見通しの向上
ここまで分かれば、APIリファレンスよりも「自分の規模ならどこまで分けるべきか」を考える段階です。
4. エディタで作った素材を使いたい
概要
Pyxelにはエディタがあり、その前提に合わせて作った素材は .pyxres に保存し、pyxel.load() でそのまま読み込めます。
Pyxelの変更履歴でも、エディタマニュアルやリソース関連ドキュメントの追加、画像・タイルマップ・サウンドエディタまわりの修正が継続して行われています。リソースファイル形式のドキュメントも 2.8.3 / 2.8.4 で追加されています。
詳細解説
一番基本の流れ
- Pyxel Editor で画像・タイルマップ・サウンドを作る
-
.pyxresとして保存する - Pythonコードで
pyxel.load("xxx.pyxres")する -
images[]/tilemaps[]/sounds[]/musics[]を使う
この流れが、Pyxelの一番自然な素材運用です。 load() と save() が .pyxres を対象にしていることからも、それが公式想定フローだと分かります。
具体例
ファイル構成
project/
├── main.py
└── ui_assets.pyxres
コード
import pyxel
class App:
def __init__(self):
pyxel.init(200, 120)
pyxel.load("ui_assets.pyxres")
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
pyxel.play(3, 0) # エディタで作った効果音0を鳴らす
def draw(self):
pyxel.cls(0)
# エディタで作った画像バンク0の左上16x16を表示
pyxel.blt(20, 20, 0, 0, 0, 16, 16, colkey=0)
# エディタで作ったタイルマップ0を表示
pyxel.bltm(60, 20, 0, 0, 0, 8, 8)
pyxel.text(20, 100, "PRESS Z", 7)
App()
このコードのポイント
画像を使う
エディタで作ったスプライトは、画像バンクに入っています。
コード側では blt() で取り出して使います。 blt() は画像バンク0〜2の一部を描くAPIです。
タイルマップを使う
エディタで並べたマップはタイルマップに入っています。
コード側では bltm() で表示します。 bltm() はタイルマップ0〜7の一部を描くAPIです。
サウンドを使う
エディタで作ったSoundは sounds[n] に入っているので、play(ch, n) で鳴らせます。 play() はSound番号を受け取れます。
何が便利なのか
1. 視覚的に素材を作れる
マップや画像を、コードではなく見た目で確認しながら作れます。
2. コードが素材定義だらけにならない
画像を Image.set() で何十個も書く必要がなくなります。
3. 試行錯誤が速い
見た目の微調整、タイル並び替え、効果音の作り直しがしやすくなります。
ここまで理解できれば十分なこと
- エディタで作った素材は
.pyxresに保存してload()で使う - 画像は
blt()、タイルマップはbltm()、音はplay()/playm() - エディタ連携の本質は「コードから素材定義を分離すること」
ここまで分かれば、後は実際にエディタで触ってみる段階です。APIとしては load() が分かっていれば十分です。
5. プロジェクト構成を整理したい
概要
これはPyxel専用APIの話というより、Pyxelのリソース運用に合ったディレクトリ構成を作る という話です。
ただし、Pyxelが .pyxres、.pyxpal、.png、.gif、.jpeg、.tmx などを扱えることが分かっていると、役割ごとの整理がかなりしやすくなります。
詳細解説
一番単純な構成
project/
├── main.py
└── game.pyxres
これは最小構成として非常に分かりやすいです。
小さなゲームや試作なら、これだけでも十分実用的です。
少し整理した構成
project/
├── main.py
├── resources/
│ ├── game.pyxres
│ └── game.pyxpal
└── fonts/
└── PixelMplus12-Regular.ttf
この構成では、
-
resources/
Pyxel用の素材 -
fonts/
外部フォント
という整理になります。
.pyxpal は同名 .pyxres と組み合わせて使いやすいので、同じ場所に置いておくと分かりやすいです。
さらに分ける構成
project/
├── main.py
├── resources/
│ ├── common.pyxres
│ ├── title.pyxres
│ └── stage1.pyxres
├── images/
│ ├── logo.png
│ └── portrait.jpeg
├── maps/
│ ├── town.tmx
│ └── cave.tmx
└── fonts/
├── PixelMplus12-Regular.ttf
└── umplus_j10r.bdf
このようにすると、
- 共有素材は
common.pyxres - タイトル専用素材は
title.pyxres - ステージ専用素材は
stage1.pyxres - 外部画像は
images/ - 外部マップは
maps/ - フォントは
fonts/
というように、かなり見通しが良くなります。
どのくらい分けるべきか
小規模
分けすぎないほうが分かりやすいです。
まずは main.py + game.pyxres だけでも十分です。
中規模
「共通素材」と「画面ごとの素材」を分けると保守しやすくなります。
画面数・マップ数が多い場合
エリアごと、シーンごと、用途ごとに分けたほうが後で楽です。
実務で意識するとよいこと
1. 読み込み単位と保存単位を一致させる
たとえばタイトル画面を開いたら title.pyxres を読み込む、というように、
「いつ使うか」と「どこに置くか」を合わせると構造が自然になります。
2. 共通素材は1か所に寄せる
同じアイコンや共通ウィンドウ素材が複数 .pyxres に散ると、修正漏れが起きやすいです。
3. コードファイルを素材倉庫にしない
大きな画像やマップ定義をPythonコードへ直接書き込むのは、試作以外では避けたほうが見通しが保ちやすいです。
ここまで理解できれば十分なこと
- プロジェクト構成は「何をどこで使うか」に合わせて整理する
- 小規模なら単純でよい
- 規模が大きくなるほど
.pyxresや.tmxを分けたほうが楽
ここまで分かれば、APIリファレンスよりも設計の話になります。Pyxel側の拡張子と読み込みAPIが分かっていれば十分です。
6. 素材だけ差し替えたい
概要
これもリソース管理の大きな利点です。
コード側で「画像バンク0のこの位置を使う」「タイルマップ0を表示する」「Sound 2 を鳴らす」という約束を守っていれば、中身の画像や音だけを差し替えても、コードはそのままで済みます。
これは .pyxres でも .png / .tmx でも同じ考え方です。Pyxelの描画・再生APIは番号や座標を参照するだけなので、素材の中身を入れ替える余地があります。
詳細解説
.pyxres を差し替える例
構成A
project/
├── main.py
└── theme_dark.pyxres
構成B
project/
├── main.py
└── theme_light.pyxres
コード
import pyxel
USE_DARK_THEME = True
class App:
def __init__(self):
pyxel.init(200, 120)
if USE_DARK_THEME:
pyxel.load("theme_dark.pyxres")
else:
pyxel.load("theme_light.pyxres")
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.blt(20, 20, 0, 0, 0, 16, 16, colkey=0)
pyxel.bltm(60, 20, 0, 0, 0, 8, 8)
pyxel.text(20, 100, "theme test", 7)
App()
この場合、コードが使う画像位置やタイルマップ番号が同じなら、リソースファイルだけ入れ替えて見た目を変えられます。
.png を差し替える例
ファイル構成
project/
├── main.py
└── assets/
├── player_blue.png
└── player_red.png
コード
filename = "assets/player_blue.png"
pyxel.images[0].load(0, 0, filename)
ここで filename を player_red.png に変えるだけで、コードの描画側は同じまま見た目を変えられます。
Image.load() は .png / .gif / .jpeg に対応しています。
差し替えを成立させるための大事な条件
1. 参照位置を合わせる
たとえばコードが
pyxel.blt(20, 20, 0, 32, 0, 16, 16, colkey=0)
としているなら、新しい素材でも (32,0) の位置に同じ意味のスプライトがある必要があります。
2. 番号の役割を固定する
たとえば、
-
sounds[0]= 決定音 -
sounds[1]= キャンセル音 -
tilemaps[0]= メイン背景
のように決めておくと、差し替えてもコードが壊れにくいです。
3. 素材側の約束を崩さない
画像の並び順や、Sound番号の意味が変わると、コード側も修正が必要になります。
差し替え可能な設計とは、コードと素材の間に「この番号・位置はこの意味」という契約を作ること です。
どういう場面で便利か
- テーマ差し替え
- スキン差し替え
- 季節イベント用UI差し替え
- キャラ色違い差し替え
- 効果音だけ別案に交換
ここまで理解できれば十分なこと
- 素材差し替えは、コードと素材の役割分離が前提
-
.pyxresでも.pngでもできる - 差し替え可能にするには「参照位置・番号の約束」を守る必要がある
ここまで分かれば、あとは設計の問題です。PyxelのAPIとしては、load()、Image.load()、blt()、bltm()、play() が理解できていれば十分です。
まとめ
Pyxelのリソース管理は、「コードと素材を分けて管理する仕組み」 として理解するとかなり整理しやすいです。
その中心にあるのが .pyxres と pyxel.load() / pyxel.save() です。画像・タイルマップ・サウンド・Musicをまとめて読み書きでき、必要なら .pyxpal も組み合わせられます。さらに、用途に応じて .png / .gif / .jpeg / .tmx も使えます。
整理するとこうです。
-
画像・音・マップをまとめて管理する
.pyxresにまとめる -
リソースファイルを読み込む
pyxel.load("xxx.pyxres") -
リソースを外部ファイルとして分離する
.pyxres、.png、.tmxなどを使う -
エディタで作った素材を使う
エディタで保存した.pyxresをload()する -
プロジェクト構成を整理する
用途ごとにresources/、maps/、fonts/などへ分ける -
素材だけ差し替える
参照位置と番号の約束を守ったまま中身を交換する
ここまで理解できたら、次に見るべきは load()、save()、load_pal()、save_pal()、Image.load()、Tilemap.from_tmx() のAPIリファレンスです。そこから先は「Pyxelで何ができるか」を知る段階ではなく、「自分のプロジェクトをどう整理するか」を詰める段階に入ります。
🧠 ゲームロジック編
Pyxelでゲームらしい動きを作るときに最初に知りたくなる内容を、逆引き形式でまとめます。
対象にする項目は次の7つです。
- キャラクターを移動させたい
- 重力やジャンプを実装したい
- 当たり判定を作りたい
- 敵を動かしたい(AIっぽい動き)
- スコアや体力を管理したい
- シーン(画面)を切り替えたい
- 一時停止機能を作りたい
まず最初に:Pyxelでいう「ゲームロジック」とは何か
Pyxelでは、ゲームロジックの中心は update() にあります。
pyxel.run(update, draw) を呼ぶと、Pyxelは毎フレーム update() を呼んで状態を更新し、そのあと draw() を呼んで画面を描きます。つまり、
-
update()
入力を読む、座標を変える、速度を変える、シーンを切り替える -
draw()
今の状態を画面に見せる
という役割分担になります。これは run() のAPI説明そのものです。
この構造を理解すると、ゲームロジックはすべて「変数の更新」として考えられるようになります。
たとえば、
- プレイヤーが右へ進む
→x += 1 - ジャンプする
→vy = -4 - 敵が折り返す
→dir *= -1 - HPが減る
→hp -= 1 - ポーズ画面に入る
→scene = "pause"
のように、最終的には「どの変数をどう変えるか」の話になります。Pyxelはそのための入力取得や描画APIを持っている、という理解が一番実用的です。
1. キャラクターを移動させたい
概要
一番基本の移動は、座標を変数で持ち、入力に応じてその値を更新する ことです。
キーボードやゲームパッド入力は btn() や btnp() で取得できます。btn() は押している間ずっと True、btnp() は押されたフレームだけ True です。
詳細解説
一番小さい例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.x = 40
self.y = 40
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btn(pyxel.KEY_LEFT):
self.x -= 1
if pyxel.btn(pyxel.KEY_RIGHT):
self.x += 1
if pyxel.btn(pyxel.KEY_UP):
self.y -= 1
if pyxel.btn(pyxel.KEY_DOWN):
self.y += 1
def draw(self):
pyxel.cls(0)
pyxel.rect(self.x, self.y, 8, 8, 8)
App()
このコードで何をしているのか
self.x, self.y
プレイヤーの位置です。
「今どこにいるか」を数値で持っています。
pyxel.btn(pyxel.KEY_LEFT)
左キーが押されている間だけ True になります。
そのため、押しているフレームのたびに self.x -= 1 が実行されます。 btn() は「押されていれば True」を返すAPIです。
pyxel.rect(self.x, self.y, 8, 8, 8)
プレイヤーを8x8の四角として描いています。
ここでは画像ではなく図形で代用していますが、実際には blt() でスプライトを描いても構いません。 rect() は塗りつぶし矩形、blt() は画像の一部を描くAPIです。
なぜこの考え方が基本なのか
ゲームの移動は、見た目上は「キャラが動いている」ように見えますが、内部的には 座標が変わっているだけ です。
この理解ができると、後で
- 速度を持たせる
- 加速度を持たせる
- マップ衝突を入れる
- カメラ追従を入れる
といった拡張がしやすくなります。
実務でよくやる形
速度変数を挟む
直接 x += 1 と書く代わりに、速度を変数で持つことが多いです。
self.vx = 0
if pyxel.btn(pyxel.KEY_LEFT):
self.vx = -1
elif pyxel.btn(pyxel.KEY_RIGHT):
self.vx = 1
else:
self.vx = 0
self.x += self.vx
こうしておくと、後でダッシュやノックバックを入れやすくなります。
ここまで理解できれば十分なこと
- 移動の基本は「位置変数を更新すること」
- 入力取得は
btn()が基本 - 見た目は
rect()でもblt()でもよい - 最初は
x,yを直接動かせれば十分
ここまで分かれば、後はゲームごとの移動感の調整です。PyxelのAPIとしては btn() と描画APIが理解できていれば十分です。
2. 重力やジャンプを実装したい
概要
重力やジャンプは、縦方向の速度 vy を持つ ことで実装するのが基本です。
考え方としては、
- 毎フレーム、重力で
vyを増やす - ジャンプした瞬間に
vyを負の値にする - その
vyをyに足す
という流れです。
Pyxelに「重力」専用APIがあるわけではなく、毎フレーム変数を更新して作ります。これは run(update, draw) の仕組みと相性が良い考え方です。
詳細解説
一番小さい例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.x = 40
self.y = 80
self.vy = 0
self.on_ground = False
pyxel.run(self.update, self.draw)
def update(self):
# ジャンプ
if pyxel.btnp(pyxel.KEY_Z) and self.on_ground:
self.vy = -4
self.on_ground = False
# 重力
self.vy += 0.2
# 位置更新
self.y += self.vy
# 地面判定
ground_y = 100
if self.y >= ground_y:
self.y = ground_y
self.vy = 0
self.on_ground = True
def draw(self):
pyxel.cls(0)
pyxel.rect(self.x, self.y, 8, 8, 8)
pyxel.line(0, 108, 160, 108, 7)
App()
このコードで何をしているのか
self.vy
縦方向の速度です。
上へ飛ぶときは負、下へ落ちるときは正、と決めています。
self.vy += 0.2
毎フレーム、下向きへ少しずつ速度を増やしています。
これが重力です。
if pyxel.btnp(pyxel.KEY_Z) and self.on_ground:
ジャンプボタンを押した瞬間だけ反応しています。
押しっぱなしだと連続ジャンプになりやすいので、ジャンプ開始は btnp() が向いています。 btnp() は「そのフレームで押されたら True」です。
self.vy = -4
上向きの初速を与えています。
これで一度上に動き始め、その後、重力で徐々に落ちてきます。
ここで大事な考え方
ジャンプは「上へ移動する特別な処理」ではなく、
一瞬だけ上向きの速度を与える処理 と考えると理解しやすいです。
つまり、
- ジャンプボタンを押す
→vy = -4 - その後は重力で
vyが増える - 結果として上がってから落ちる
という流れです。
実務でよく追加するもの
落下速度の上限
self.vy = min(self.vy + 0.2, 4)
これを入れると、落下速度が無限に増えなくなります。
多段ジャンプ防止
on_ground を使うのはそのためです。
床にいるときだけジャンプできるようにしています。
可変ジャンプ
ジャンプボタンを短く押したときは低く、長く押したときは高くしたい場合は、
ボタンを押している間だけ上昇を少し延ばす設計もあります。
ここまで理解できれば十分なこと
- 重力は
vyを毎フレーム増やすこと - ジャンプは
vyに負の初速を与えること - 地面に着いたら
vy=0とon_ground=True - ジャンプ開始は
btnp()が向いている
ここまで分かれば、後はゲームごとのジャンプ感の調整です。PyxelのAPIとしては btnp() と基本描画が理解できていれば十分です。
3. 当たり判定を作りたい
概要
一番基本の当たり判定は、矩形同士が重なっているか を判定する方法です。
Pyxelに「すべての汎用当たり判定を自動でやってくれる高レベルAPI」があるわけではないので、
まずは自分で関数を作るのが分かりやすいです。
ただし、タイルマップとの衝突については Tilemap.collide() があり、これは後の項目で扱います。 Tilemap.collide() はタイル壁との衝突解決用です。
詳細解説
一番基本の矩形判定
def is_hit(ax, ay, aw, ah, bx, by, bw, bh):
return (
ax < bx + bw and
ax + aw > bx and
ay < by + bh and
ay + ah > by
)
これは「Aの四角」と「Bの四角」が重なっているかを返す関数です。
使い方の例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.player_x = 20
self.player_y = 40
self.enemy_x = 80
self.enemy_y = 40
self.hit = False
pyxel.run(self.update, self.draw)
def is_hit(self, ax, ay, aw, ah, bx, by, bw, bh):
return (
ax < bx + bw and
ax + aw > bx and
ay < by + ah and
ay + ah > by
)
def update(self):
if pyxel.btn(pyxel.KEY_RIGHT):
self.player_x += 1
if pyxel.btn(pyxel.KEY_LEFT):
self.player_x -= 1
self.hit = self.is_hit(
self.player_x, self.player_y, 8, 8,
self.enemy_x, self.enemy_y, 8, 8
)
def draw(self):
pyxel.cls(0)
pyxel.rect(self.player_x, self.player_y, 8, 8, 8)
pyxel.rect(self.enemy_x, self.enemy_y, 8, 8, 10)
if self.hit:
pyxel.text(20, 20, "HIT!", 7)
App()
このコードで何をしているのか
- プレイヤーも敵も 8x8 の四角とみなしている
- 毎フレーム、座標が重なっているかを調べている
- 重なっていたら
self.hit = True
なぜこの方法が基本なのか
ゲーム内の見た目はキャラの形をしていても、
内部では「当たり判定は四角」で処理することが多いです。
理由は単純で、速くて分かりやすく、調整もしやすいからです。
実務でよくやる工夫
見た目より小さい当たり判定を使う
たとえば16x16のキャラ画像でも、当たり判定を 12x12 にすることがあります。
見た目どおりピッタリにすると、触れていないように見えるのに当たる、という違和感が出ることがあるからです。
攻撃判定と本体判定を分ける
- 本体判定
ダメージを受ける範囲 - 攻撃判定
武器や弾が当たる範囲
を別々の四角にすることも多いです。
ここまで理解できれば十分なこと
- 一番基本の当たり判定は矩形同士の重なり判定
- 見た目と当たり判定は同じでなくてもよい
- ゲームごとに少し小さめに作ることが多い
ここまで分かれば、APIリファレンスよりもゲーム設計の話になります。Pyxel側としては通常の座標管理ができていれば十分です。
4. 敵を動かしたい(AIっぽい動き)
概要
ここで言う「AIっぽい動き」は、まずは 一定ルールで自動的に動くこと と考えると分かりやすいです。
PyxelにAI専用APIはありません。
敵もプレイヤーと同じく、位置・速度・状態を変数で持って、update() の中で毎フレーム更新します。これは run(update, draw) の構造そのものです。
詳細解説
一番小さい例:左右往復
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.enemy_x = 40
self.enemy_y = 60
self.enemy_dir = 1
pyxel.run(self.update, self.draw)
def update(self):
self.enemy_x += self.enemy_dir
if self.enemy_x <= 20:
self.enemy_dir = 1
if self.enemy_x >= 120:
self.enemy_dir = -1
def draw(self):
pyxel.cls(0)
pyxel.rect(self.enemy_x, self.enemy_y, 8, 8, 10)
App()
このコードで何をしているのか
self.enemy_dir
敵の進行方向です。
右へ進むときは 1、左へ進むときは -1 としています。
self.enemy_x += self.enemy_dir
毎フレーム1ピクセルずつ移動しています。
端に着いたら反転
一定範囲の端に来たら enemy_dir を反転しています。
このような単純ルールでも、十分「敵が動いている」感じになります。
もう少しAIっぽくする例
プレイヤーに近づく
if self.player_x < self.enemy_x:
self.enemy_x -= 1
elif self.player_x > self.enemy_x:
self.enemy_x += 1
これは敵がプレイヤーの方向へ寄ってくる動きです。
かなり単純ですが、プレイヤー追跡の基本になります。
一定間隔で方向転換する
if pyxel.frame_count % 60 == 0:
self.enemy_dir *= -1
frame_count を使うと、時間ベースの周期行動が作れます。
frame_count はPyxelが持つ経過フレーム数です。
状態を持つ
self.enemy_state = "patrol"
のようにして、
-
patrol
徘徊 -
chase
追跡 -
attack
攻撃
といった状態ごとに処理を分けると、かなりゲームらしくなります。
実務で大事なこと
敵のAIを難しく考えすぎる必要はありません。
最初は次のようなルールだけで十分です。
- 一定範囲を往復する
- プレイヤーが近いと追いかける
- 当たったら止まる
- 一定時間ごとに向きを変える
これだけでも「考えて動いている」ように見えることが多いです。
ここまで理解できれば十分なこと
- 敵も位置・速度・状態を変数で持つ
- AIの最初の一歩は「一定ルールで自動的に動くこと」
- 往復、追跡、周期行動だけでも十分ゲームらしくなる
ここまで分かれば、後はルール設計の問題です。Pyxel側のAPIとしては run()、入力不要の座標更新、必要なら frame_count が分かっていれば十分です。
5. スコアや体力を管理したい
概要
スコアや体力は、基本的には 数値変数として持って更新し、text() で表示する だけです。
Pyxelに「スコア管理専用クラス」や「HPゲージ専用クラス」があるわけではありません。
だからこそ、最初はシンプルに数値として扱うのが分かりやすいです。
表示には text() が使えます。
詳細解説
一番小さい例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.score = 0
self.hp = 5
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
self.score += 10
if pyxel.btnp(pyxel.KEY_X):
self.hp -= 1
if self.hp < 0:
self.hp = 0
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, f"SCORE: {self.score}", 7)
pyxel.text(10, 20, f"HP: {self.hp}", 8)
pyxel.text(10, 40, "Z: score up / X: damage", 5)
App()
このコードで何をしているのか
self.score
スコアです。
アイテム取得や敵撃破のたびに増やすことになります。
self.hp
体力です。
ダメージを受けたら減らし、0未満にならないようにしています。
pyxel.text(...)
数値を文字列として画面に表示しています。
Pyxelの text() は標準フォントでもカスタムフォントでも使えます。
実務でよくやる工夫
最大値も持つ
self.hp = 5
self.max_hp = 5
こうしておくと、回復処理やHPバー表示がしやすくなります。
スコアは桁を揃える
pyxel.text(10, 10, f"SCORE: {self.score:06}", 7)
こうすると 000120 のように固定桁で表示できます。
スコア表示は等幅フォントと相性が良いです。
ゲージ表示にする
体力は文字だけでなく、矩形でバー表示にすることも多いです。
pyxel.rect(10, 30, self.hp * 10, 6, 8)
pyxel.rectb(10, 30, self.max_hp * 10, 6, 7)
状態変数としての考え方が大事
スコアやHPは単なる数字ですが、
ゲームロジックでは
- 0になったらゲームオーバー
- 一定値を超えたらクリア
- HPが少ないと演出を変える
のように、シーンや演出の条件 にもなります。
つまり、ただ表示するだけでなく「ゲーム進行の判定材料」として使う意識が大事です。
ここまで理解できれば十分なこと
- スコアやHPは数値変数で管理する
-
text()やrect()で表示できる - 最大値やしきい値を持つとゲーム進行に使いやすい
ここまで分かれば、APIというよりゲーム設計の問題です。Pyxel側としては text() や図形描画が使えれば十分です。
6. シーン(画面)を切り替えたい
概要
シーン切り替えの基本は、今どの画面にいるかを表す変数を1つ持つこと です。
たとえば self.scene = "title" のようにして、update() と draw() の中でその値によって処理を分けます。
Pyxelに「シーンマネージャー専用API」があるわけではないので、これが一番自然です。
必要なら reset() でアプリ全体を最初からやり直すこともできます。 reset() は Pyxelアプリを先頭から再起動するAPIです。
詳細解説
一番基本の例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.scene = "title"
pyxel.run(self.update, self.draw)
def update(self):
if self.scene == "title":
if pyxel.btnp(pyxel.KEY_Z):
self.scene = "game"
elif self.scene == "game":
if pyxel.btnp(pyxel.KEY_X):
self.scene = "gameover"
elif self.scene == "gameover":
if pyxel.btnp(pyxel.KEY_Z):
self.scene = "title"
def draw(self):
pyxel.cls(0)
if self.scene == "title":
pyxel.text(50, 40, "TITLE", 7)
pyxel.text(30, 60, "Z: START", 7)
elif self.scene == "game":
pyxel.text(55, 40, "GAME", 10)
pyxel.text(20, 60, "X: GAME OVER", 7)
elif self.scene == "gameover":
pyxel.text(40, 40, "GAME OVER", 8)
pyxel.text(25, 60, "Z: BACK TO TITLE", 7)
App()
このコードで何をしているのか
self.scene
今どの画面かを表しています。
この例では
titlegamegameover
の3状態です。
update() で切り替える
入力に応じて、self.scene の値を変更しています。
draw() で描き分ける
今の scene に応じて、表示内容を切り替えています。
なぜこのやり方がよいのか
シーン切り替えは本質的には「状態の切り替え」です。
だから、
- タイトル画面
- プレイ画面
- ゲームオーバー
- ポーズ画面
- 設定画面
を、全部「状態値の違い」として扱うと整理しやすくなります。
実務でよくやる形
定数化する
SCENE_TITLE = 0
SCENE_GAME = 1
SCENE_GAMEOVER = 2
のように定数で管理すると、タイプミスが減ります。
クラスごとに分ける
規模が大きくなると、
TitleSceneGameScenePauseScene
のようにクラスを分けることもあります。
ただし最初は scene 変数1つで十分です。
reset() の使いどころ
「全部最初からやり直す」でよいゲームなら、
タイトルへ戻るときに pyxel.reset() を使う考え方もあります。APIで reset() はアプリを最初から再起動すると説明されています。
ここまで理解できれば十分なこと
- シーン切り替えの基本は「今の画面を表す変数」を持つこと
-
update()で切り替え、draw()で描き分ける - 規模が大きくなるまではシンプルな状態変数で十分
ここまで分かれば、APIリファレンスよりも設計の話になります。Pyxel側の機能としては run() と必要なら reset() が分かっていれば十分です。
7. 一時停止機能を作りたい
概要
一時停止も、考え方としてはシーン切り替えとほぼ同じです。
つまり、「今はポーズ中かどうか」を表す変数を持ち、ポーズ中はゲームの更新を止める だけです。
Pyxelに「pause()」専用APIがあるわけではないので、ロジック側で更新停止を作ります。 run() は毎フレーム update と draw を呼ぶだけなので、update 内でどこまで処理するかは自分で決める構造です。
詳細解説
一番基本の例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.x = 20
self.paused = False
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_P):
self.paused = not self.paused
if self.paused:
return
self.x += 1
def draw(self):
pyxel.cls(0)
pyxel.rect(self.x, 50, 8, 8, 8)
if self.paused:
pyxel.text(50, 20, "PAUSE", 7)
App()
このコードで何をしているのか
self.paused
今ポーズ中かどうかです。
if pyxel.btnp(pyxel.KEY_P):
Pキーを押した瞬間だけポーズ状態を反転しています。
ポーズのON/OFFは1回だけ切り替わればよいので btnp() が向いています。
if self.paused: return
ポーズ中は update() の途中で止めています。
そのため、self.x += 1 が実行されず、ゲームが進みません。
何を止めて、何を止めないかを決める
ここが一時停止の実装で一番大事です。
たとえばポーズ中に、
- プレイヤー移動は止める
- 敵移動も止める
- スコア更新も止める
- でもポーズメニューの入力は受けたい
ということが起こります。
そのため、実務では
- ゲーム本編の更新
- ポーズメニューの更新
を分けて考えることが多いです。
実務での形
def update(self):
if pyxel.btnp(pyxel.KEY_P):
self.paused = not self.paused
if self.paused:
self.update_pause_menu()
else:
self.update_game()
この形にすると、
- ポーズ中はゲーム本編が止まる
- でもメニュー操作だけは動く
という自然な作りになります。
シーン方式でもよい
一時停止を
scene = "game"scene = "pause"
のように、独立したシーンとして扱ってもよいです。
ポーズも「画面状態の1つ」と考えると、シーン切り替えと統一しやすくなります。
音も止めたい場合
一時停止時にBGMや効果音も止めたいなら、
必要に応じて pyxel.stop() やチャンネル単位の停止を使います。
stop(ch) / stop() はチャンネルまたは全体停止のAPIです。
ここまで理解できれば十分なこと
- 一時停止は「ポーズ状態の変数」を持てば作れる
- ポーズ中はゲーム本編の更新を止める
- ただしポーズメニュー入力は別で動かすことが多い
- シーン方式で扱ってもよい
ここまで分かれば、APIリファレンスよりも設計の話になります。Pyxel側としては btnp() と必要なら stop() が分かっていれば十分です。
まとめ
Pyxelのゲームロジックまわりは、実質的には 「状態変数を毎フレームどう更新するか」 に集約されます。
Pyxelは run(update, draw) で毎フレームのロジック更新と描画更新を呼び、入力取得には btn() / btnp() / btnr()、描画には rect() / blt() / bltm()、タイル衝突には Tilemap.collide()、アプリ制御には quit() / reset() などを用意しています。つまり、移動・重力・当たり判定・敵AI・シーン管理・一時停止は、これらをどう組み合わせるかの問題です。
整理するとこうです。
-
キャラクターを移動させる
位置変数を入力に応じて更新する -
重力やジャンプを実装する
縦速度vyを持ち、重力で増やし、ジャンプ時に負の初速を与える -
当たり判定を作る
まずは矩形同士の重なり判定から始める -
敵を動かす
位置・方向・状態を持ち、一定ルールで更新する -
スコアや体力を管理する
数値変数として持ち、text()や図形で表示する -
シーンを切り替える
今の画面を表す状態変数を持つ -
一時停止を作る
ポーズ状態を持ち、ゲーム本編の更新を止める
ここまで理解できたら、次に見るべきは run()、btn() / btnp() / btnr()、blt() / bltm()、Tilemap.collide()、quit() / reset() のAPIリファレンスです。そこから先は「Pyxelで何ができるか」を知る段階ではなく、「自分のゲームをどう設計するか」を詰める段階に入ります。
🎥 カメラ・演出編
Pyxelで画面の見せ方を工夫したいときに最初に知りたくなる内容を、逆引き形式でまとめます。
対象にする項目は次の6つです。
- 画面をスクロールさせたい
- カメラをキャラに追従させたい
- 画面を揺らしたい
- フェードイン・フェードアウトしたい
- 点滅や点灯エフェクトを作りたい
- 疑似3D表現をしたい
まず最初に:Pyxelの「カメラ」とは何か
Pyxelのカメラは、3Dエンジンのような複雑なものではなく、「これから描くものを何ピクセルずらして見せるか」 を決める仕組みです。
中心になるのは pyxel.camera(x, y) で、これは以後の描画を (-x, -y) だけシフトします。引数なしで camera() を呼ぶと、オフセットは (0, 0) に戻ります。つまり、画面スクロールや追従カメラの基本は、ワールド座標のまま描いて、最後にカメラで見せ方だけずらす という発想です。
この考え方を先に理解しておくと、
- スクロール
- 追従カメラ
- 画面揺れ
- 演出用のズレ
が全部つながって見えるようになります。
逆に、キャラの座標そのものを「画面座標」と「世界座標」で混ぜてしまうと、設計が急に分かりにくくなります。Pyxelの camera() は「描画オフセット」を変える関数なので、ロジック座標と表示座標を分ける前提と相性が良いです。
1. 画面をスクロールさせたい
概要
一番基本的な方法は pyxel.camera(x, y) です。
たとえば camera(40, 0) を呼ぶと、その後に描くものは全体として左に40ピクセルずれて見えます。API上も「All subsequent drawing operations will be shifted by (-x, -y)」と説明されています。
詳細解説
一番小さい例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.scroll_x = 0
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btn(pyxel.KEY_RIGHT):
self.scroll_x += 1
if pyxel.btn(pyxel.KEY_LEFT):
self.scroll_x -= 1
def draw(self):
pyxel.cls(0)
# カメラを横に動かす
pyxel.camera(self.scroll_x, 0)
# ワールド座標のまま並べる
for i in range(20):
x = i * 20
pyxel.rect(x, 40, 16, 16, 8)
pyxel.text(x, 60, str(i), 7)
# カメラ解除
pyxel.camera()
App()
このコードで何をしているのか
self.scroll_x
「画面がどこまで右へ進んだか」を表す値です。
ここではワールドそのものを動かしているのではなく、見せる位置を変えるための基準値 として使っています。
pyxel.camera(self.scroll_x, 0)
この時点から後に描くものは、すべて (-self.scroll_x, 0) だけずれて表示されます。
たとえば self.scroll_x = 20 なら、本来 x=100 に描くものは、見た目上 x=80 に出ます。これは API の説明そのものです。
pyxel.camera()
引数なしで呼ぶとカメラがリセットされます。
これを忘れると、その後のUIや固定表示までスクロールしてしまいます。APIでも引数なしで (0, 0) に戻すと説明されています。
何が便利なのか
この方法を使うと、背景や地形や敵を「世界座標」に置いたまま、画面側だけをスライドできます。
つまり、
- キャラはワールドの
x=320 - 敵はワールドの
x=500 - 木はワールドの
x=640
のように、そのまま自然に置いておけます。
スクロールのために全オブジェクトの座標を書き換える必要がないので、設計がかなり楽になります。camera() が「描画オフセット」であることから、この使い方が自然です。
タイルマップでも同じ考え方で使える
bltm() でマップを描いている場合も、先に camera() を設定してから bltm() を呼べば、マップ全体がスクロールして見えます。
camera() は「以後の描画操作全部」に効くので、図形、画像、文字、タイルマップをまとめてスクロールできます。APIの説明でも「all subsequent drawing operations」となっています。
ここまで理解できれば十分なこと
- スクロールは
camera(x, y)で作れる - オブジェクトの座標そのものを動かすのではなく、見せ方をずらす
- 描画後は
camera()で戻す
ここまで分かれば、細かな設計は camera() のAPIリファレンスを見れば十分です。
2. カメラをキャラに追従させたい
概要
追従カメラは、考え方としては単純です。
「カメラ座標 = キャラ座標 - 画面中央」 にするだけです。
Pyxelには自動追従専用APIはありませんが、camera() が描画オフセットなので、自分で追従ロジックを書けば十分作れます。
詳細解説
一番基本の例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.player_x = 80
self.player_y = 60
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btn(pyxel.KEY_LEFT):
self.player_x -= 2
if pyxel.btn(pyxel.KEY_RIGHT):
self.player_x += 2
if pyxel.btn(pyxel.KEY_UP):
self.player_y -= 2
if pyxel.btn(pyxel.KEY_DOWN):
self.player_y += 2
def draw(self):
pyxel.cls(0)
cam_x = self.player_x - pyxel.width // 2
cam_y = self.player_y - pyxel.height // 2
pyxel.camera(cam_x, cam_y)
# ワールド
for i in range(20):
pyxel.rect(i * 32, 80, 16, 16, 11)
# プレイヤー
pyxel.rect(self.player_x, self.player_y, 8, 8, 8)
pyxel.camera()
App()
このコードで何をしているのか
cam_x = self.player_x - pyxel.width // 2
プレイヤーを画面の中央付近に見せるため、カメラの基準位置をプレイヤー座標から逆算しています。
camera(cam_x, cam_y) は以後の描画を (-cam_x, -cam_y) ずらすので、結果としてプレイヤーが中央に寄って見えます。 camera() の仕様そのものです。
そのままだと何が起きるか
この方法だけだと、マップの端でもカメラが中央追従を続けるため、
左端・右端・上端・下端で「何もない外側」が見えてしまうことがあります。
実際のゲームでは、カメラ座標をマップ範囲に収めることが多いです。
端を超えないようにする例
cam_x = self.player_x - pyxel.width // 2
cam_y = self.player_y - pyxel.height // 2
cam_x = max(0, min(cam_x, map_width - pyxel.width))
cam_y = max(0, min(cam_y, map_height - pyxel.height))
こうしておくと、マップの外側を見せずに済みます。
少し遅れて追従させたい場合
常にピタッと中央に固定すると、追従が機械的で硬く見えることがあります。
その場合は、カメラを少しずつ近づけるようにします。
self.cam_x += (target_x - self.cam_x) * 0.1
self.cam_y += (target_y - self.cam_y) * 0.1
pyxel.camera(self.cam_x, self.cam_y)
こうすると、いわゆる「ぬるっと追従するカメラ」になります。
これはPyxel専用機能ではなく、camera() に渡す値をどう作るかの問題です。 camera() は単純なオフセット設定なので、こうした補間と相性が良いです。
ここまで理解できれば十分なこと
- 追従カメラは
camera()に渡す値をキャラ座標から計算して作る - 画面中央基準が最初の定番
- マップ端で止める制限を入れると実用的
- 補間を入れると自然に見える
ここまで分かれば、APIよりもゲーム側設計の話です。Pyxel側としては camera() が分かっていれば十分です。
3. 画面を揺らしたい
概要
画面揺れは、camera() に一時的なランダムオフセットや振動オフセットを足すだけで作れます。
Pyxelに「shake()」のような専用関数はありませんが、camera() が描画オフセットなので、少しだけズレたカメラ位置を毎フレーム与える ことで十分作れます。
詳細解説
一番基本の例
import pyxel
import random
class App:
def __init__(self):
pyxel.init(160, 120)
self.shake_timer = 0
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
self.shake_timer = 20
if self.shake_timer > 0:
self.shake_timer -= 1
def draw(self):
pyxel.cls(0)
shake_x = 0
shake_y = 0
if self.shake_timer > 0:
shake_x = random.randint(-2, 2)
shake_y = random.randint(-2, 2)
pyxel.camera(shake_x, shake_y)
pyxel.rect(40, 40, 80, 40, 8)
pyxel.text(50, 55, "SHAKE DEMO", 7)
pyxel.camera()
App()
このコードで何をしているのか
self.shake_timer
あと何フレーム揺らすか、という残り時間です。
shake_x, shake_y
揺れている間だけ、毎フレームランダムなズレを作っています。
pyxel.camera(shake_x, shake_y)
見た目上、画面全体が少しずつ揺れて見えます。
camera() は「その後の描画全部」に効くので、背景・敵・プレイヤー・UI以外のワールド描画をまとめて揺らせます。
より自然な揺れにしたい場合
完全ランダムだと、細かくブルブルするだけに見えることがあります。
その場合は、左右に大きく、縦は少しだけ、のように重みを付けるとそれらしくなります。
shake_x = random.randint(-4, 4)
shake_y = random.randint(-1, 1)
カメラ追従と組み合わせる場合
追従カメラがあるなら、そこに揺れオフセットを足します。
pyxel.camera(cam_x + shake_x, cam_y + shake_y)
この形にすると、通常はキャラ追従しつつ、ダメージ時だけ揺れる、という実装がしやすいです。
UIまで揺らしたくない場合
ワールド描画だけ揺らして、UIは固定したいことが多いです。
その場合は、
-
camera()を揺れ付きで設定してワールドを描く -
camera()をリセット - UIを描く
という順番にします。 camera() はその後の描画に効くので、解除後に描いたものは揺れません。
ここまで理解できれば十分なこと
- 画面揺れは
camera()のオフセットを一時的にズラして作る - 追従カメラがあるなら加算する
- UIを揺らしたくないなら
camera()を戻してから描く
ここまで分かれば、APIより演出設計の話です。Pyxel側としては camera() が理解できていれば十分です。
4. フェードイン・フェードアウトしたい
概要
Pyxelでは dither(alpha) を使うと、疑似透過 によるフェード表現ができます。
APIでは alpha を 0.0-1.0 で指定し、0.0 が透明、1.0 が不透明です。
これは本物のアルファブレンドではなく、ドットの間引きによる擬似透過です。
詳細解説
フェードアウトの基本例
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
self.fade = 0.0
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btn(pyxel.KEY_Z):
self.fade = min(self.fade + 0.02, 1.0)
if pyxel.btn(pyxel.KEY_X):
self.fade = max(self.fade - 0.02, 0.0)
def draw(self):
pyxel.cls(1)
pyxel.text(20, 20, "Z: fade out / X: fade in", 7)
pyxel.rect(40, 50, 80, 20, 8)
# 黒い板を疑似透過で重ねる
pyxel.dither(self.fade)
pyxel.rect(0, 0, pyxel.width, pyxel.height, 0)
pyxel.dither(1.0)
App()
このコードで何をしているのか
self.fade
0.0〜1.0 の値で、暗転の進み具合を表しています。
pyxel.dither(self.fade)
以後の描画に疑似透過をかけます。
0.0 なら見えず、1.0 なら完全に見えます。APIの説明そのものです。
pyxel.rect(0, 0, pyxel.width, pyxel.height, 0)
画面全体を黒で覆っています。
その黒板を dither() 付きで描くことで、徐々に暗くなる見た目を作っています。
pyxel.dither(1.0)
状態を戻しています。
これを忘れると、その後の描画まで半透明っぽくなります。 dither() は「以後の描画」に効く状態なので、戻すのが安全です。
フェードインにするには
考え方は逆です。
開始時に fade = 1.0 にして、毎フレーム少しずつ減らしていけば、黒から画面が見えてくるフェードインになります。
self.fade = max(self.fade - 0.02, 0.0)
シーン切り替えでよくやる流れ
- 通常描画
- フェードアウトを進める
- 真っ黒になったらシーン切り替え
- 新シーン側でフェードインする
この流れが一番実用的です。
ここまで理解できれば十分なこと
- フェードは
dither()+ 全画面矩形で作れる -
0.0は透明、1.0は不透明 - 状態を戻すために最後に
dither(1.0)を入れると安全
これ以上の細かな調整は dither() のAPIリファレンスを見れば十分です。
5. 点滅や点灯エフェクトを作りたい
概要
点滅や点灯は、基本的には次の2系統です。
- 表示する / しないを切り替える
- 色や明るさを切り替える
Pyxelには frame_count があり、経過フレームを基準に点滅周期を作れます。
また pal() で色置換、dither() で明滅っぽい見せ方もできます。 pal() は描画時の色置換、dither() は疑似透過です。
詳細解説
一番簡単な点滅
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
if pyxel.frame_count % 20 < 10:
pyxel.text(40, 50, "PRESS START", 7)
何が起きているのか
20フレームを1周期として、その前半10フレームだけ描いています。
PyxelのデフォルトFPSは30なので、この例では「約0.33秒表示 → 約0.33秒消灯」の繰り返しになります。 frame_count は現在フレーム数で、PyxelのデフォルトFPSは30です。
色を切り替える点滅
if pyxel.frame_count % 20 < 10:
col = 7
else:
col = 10
pyxel.text(40, 50, "WARNING", col)
これは消すのではなく、色を切り替えて強調しています。
pal() を使った点灯感
画像やスプライトなら、描画時だけ色を明るい色へ置き換えて「光っているように見せる」ことができます。
pyxel.pal(8, 10)
pyxel.rect(40, 40, 30, 20, 8)
pyxel.pal()
pal(col1, col2) は、描画時に色 col1 を col2 に置換します。引数なしで初期状態へ戻ります。
dither() を使った点灯感
完全に消すのではなく、半透明っぽく見せたいなら dither() が便利です。
alpha = 0.5 if pyxel.frame_count % 20 < 10 else 1.0
pyxel.dither(alpha)
pyxel.rect(40, 40, 30, 20, 10)
pyxel.dither(1.0)
これで、ふわっと明滅しているような見え方を作れます。
どんな場面で使うか
- PRESS START
- カーソル点滅
- 警告表示
- 無敵時間演出
- 電飾っぽいUI
- ランプの点灯表現
ここまで理解できれば十分なこと
- 点滅は
frame_countで周期を作る - 消す / 色を変える / 疑似透過を変える、の3方向がある
-
pal()やdither()を使うと見た目の幅が広がる
ここまで分かれば、あとは演出デザインの話です。Pyxel側としては frame_count、pal()、dither() が分かっていれば十分です。
6. 疑似3D表現をしたい
概要
Pyxelには blt3d() と bltm3d() があり、画像やタイルマップを perspective projection で描けます。
これは Pyxel 2.7.8 で追加された機能で、changelog でも “Added blt3d and bltm3d for pseudo-3D perspective rendering” と説明されています。APIでは、pos=(x, y, z) をカメラ位置、rot=(rot_x, rot_y, rot_z) を回転、fov を視野角として受け取ります。
詳細解説
blt3d() の基本
blt3d() は画像バンクまたは Image を、指定した画面矩形へ遠近投影して描きます。
pyxel.blt3d(
20, 20, 120, 80, # 画面上の表示領域
0, # image bank 0
pos=(0, 0, 10), # カメラ位置
rot=(60, 0, 0), # 回転
fov=60, # 視野角
colkey=0, # 透明色
)
APIでは、pos の x, y は2D座標系に対応し、z は高さ、rot_x は縦方向、rot_y は横方向、rot_z は傾きです。
bltm3d() の基本
bltm3d() はタイルマップ版です。
地面や背景マップを「斜め上から見下ろしたように」表示するのに向いています。
pyxel.bltm3d(
0, 20, 160, 100, # 画面上の表示領域
0, # tilemap 0
pos=(40, 40, 20), # カメラ位置
rot=(70, 0, 0), # 見下ろし角度
fov=60,
colkey=0,
)
APIでは bltm3d() も blt3d() と同じく pos、rot、fov、colkey を持ち、対象が tilemap になるだけです。
どういう見た目になるのか
これは「本格的な3Dポリゴン」ではありません。
2D画像やタイルマップを、遠近感のある平面として見せる 機能です。
そのため、次のような用途に向いています。
- Mode7風の地面表現
- 見下ろし疑似3Dマップ
- 回転する床や背景
- タイトル画面の演出
- 遠近感のあるUI背景
changelog でも “pseudo-3D perspective rendering” と表現されており、本格3Dエンジンではなく演出用の3D風機能だと分かります。
一番重要な考え方
blt3d() / bltm3d() は、「画像やタイルをそのまま3Dモデル化する」のではなく、
1枚の面をカメラから見たように変形して描く 機能です。
そのため、3Dゲームエンジンのように物体ごとの奥行き管理や衝突判定をするものではありません。
まずは「平面を傾けて見せるためのAPI」と理解すると混乱しにくいです。API説明も画面矩形への perspective projection です。
ここまで理解できれば十分なこと
-
blt3d()は画像、bltm3d()はタイルマップを遠近投影で描く -
posがカメラ位置、rotが回転、fovが視野角 - 用途は本格3Dではなく「3D風演出」
- Pyxel 2.7.8 で追加された比較的新しい機能
ここまで分かれば、細かな数値調整は API リファレンスと実際の試行で十分です。
まとめ
Pyxelのカメラ・演出まわりは、実質的には次の4つを押さえるとかなり整理できます。
-
描画オフセットを変える
camera() -
疑似透過をかける
dither() -
色の見え方を変える
pal() -
遠近投影で描く
blt3d()/bltm3d()
整理するとこうです。
-
画面をスクロールする
camera(x, y)で描画全体をずらす -
キャラ追従カメラを作る
キャラ座標からcamera()の値を計算する -
画面を揺らす
camera()に一時的な揺れオフセットを足す -
フェードイン・アウトする
dither()と全画面矩形を組み合わせる -
点滅や点灯エフェクトを作る
frame_count、pal()、dither()を使う -
疑似3D表現をする
blt3d()/bltm3d()を使う
ここまで理解できたら、次に見るべきは camera()、dither()、pal()、blt3d()、bltm3d() のAPIリファレンスと changelog です。そこから先は「Pyxelで何ができるか」を知る段階ではなく、「自分のゲームでどう見せたいか」を詰める段階に入ります。
🖱 マウス・UI編
Pyxelでマウスを使ったUIを作るときに最初に知りたくなる内容を、逆引き形式でまとめます。
対象にする項目は次の6つです。
- マウス入力を取りたい
- クリック判定を作りたい
- ボタンUIを作りたい
- スライダーUIを作りたい
- リストやメニューUIを作りたい
- フォーカス(選択状態)を管理したい
まず最初に:Pyxelのマウス入力はどう考えればよいか
Pyxelのマウス入力は、考え方としてはキーボード入力とほぼ同じです。
違うのは「キーコード」の代わりに、
pyxel.mouse_xpyxel.mouse_ypyxel.mouse_wheelpyxel.MOUSE_BUTTON_LEFTpyxel.MOUSE_BUTTON_RIGHTpyxel.MOUSE_BUTTON_MIDDLE
などを使う点です。
ボタンの押下状態そのものは btn() / btnp() / btnr() で取れます。
つまり、マウス入力とは「座標」と「ボタン状態」の組み合わせです。これが分かると、クリック判定、ボタンUI、スライダーUI、リスト選択などが全部同じ原理で作れるようになります。 (raw.githubusercontent.com)
1. マウス入力を取りたい
概要
Pyxelでは、現在のマウスカーソル位置を pyxel.mouse_x と pyxel.mouse_y で取得できます。
マウスホイールは pyxel.mouse_wheel です。
また、カーソルの表示・非表示は pyxel.mouse(True / False) で切り替えられます。 API定義では mouse_x / mouse_y / mouse_wheel が入力状態として公開され、mouse(visible) は「カーソルを表示/非表示にする。隠しても座標更新は続く」と説明されています。 (raw.githubusercontent.com)
詳細解説
一番小さい例
import pyxel
class App:
def __init__(self):
pyxel.init(200, 120)
pyxel.mouse(True)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.text(10, 10, f"mouse_x: {pyxel.mouse_x}", 7)
pyxel.text(10, 20, f"mouse_y: {pyxel.mouse_y}", 7)
pyxel.text(10, 30, f"wheel: {pyxel.mouse_wheel}", 7)
App()
このコードで何をしているのか
pyxel.mouse_x, pyxel.mouse_y
今このフレームでのマウスカーソル座標です。
画面内のどこを指しているかをそのまま使えます。Pyxelの入力変数として公開されています。 (raw.githubusercontent.com)
pyxel.mouse_wheel
ホイールの現在値です。
スクロール量として使えます。こちらも入力変数として公開されています。 (raw.githubusercontent.com)
pyxel.mouse(True)
カーソル表示を有効にしています。
ゲームやツールによっては False にして独自カーソルを描くこともできます。APIでも「隠してもカーソル位置は更新され続ける」と説明されています。 (raw.githubusercontent.com)
どういうときに使うか
- ボタンUI
- スライダーUI
- ウィンドウ内の選択
- リストスクロール
- エディタ風の操作
- 独自カーソルの描画
実務で大事なこと
マウスUIは、結局のところ「今カーソルがどの矩形の上にあるか」を見る処理の集まりです。
つまり、mouse_x / mouse_y が取れれば、ボタンもスライダーもリスト選択も作れます。
ここが理解できると、専用UIライブラリがなくてもかなりのことができます。Pyxelは mouse_x / mouse_y と図形描画APIを持っているので、この方向の実装がしやすいです。 (raw.githubusercontent.com)
ここまで理解できれば十分なこと
- マウス座標は
mouse_x,mouse_y - ホイールは
mouse_wheel - カーソル表示切替は
mouse(True/False)
ここまで分かれば、細かなことはAPIリファレンスを見れば十分です。 (raw.githubusercontent.com)
2. クリック判定を作りたい
概要
クリック判定の基本は、次の2段階です。
- マウス座標が対象の矩形内にあるか調べる
- そのフレームで左ボタンが押されたか調べる
ボタン押下自体は btn() / btnp() / btnr() で取れます。
マウスボタンもキーボードと同じ入力APIで扱うので、クリック判定は「矩形判定 + btnp(pyxel.MOUSE_BUTTON_LEFT)」の組み合わせで作るのが基本です。 APIでは MOUSE_BUTTON_LEFT が定数として公開され、btnp() は「そのフレームで押されたら True」と定義されています。 (raw.githubusercontent.com)
詳細解説
一番基本の例
import pyxel
class App:
def __init__(self):
pyxel.init(200, 120)
pyxel.mouse(True)
self.message = "not clicked"
pyxel.run(self.update, self.draw)
def is_inside(self, x, y, w, h):
return x <= pyxel.mouse_x < x + w and y <= pyxel.mouse_y < y + h
def update(self):
if self.is_inside(50, 40, 80, 20) and pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
self.message = "clicked!"
def draw(self):
pyxel.cls(0)
pyxel.rectb(50, 40, 80, 20, 7)
pyxel.text(75, 47, "BUTTON", 7)
pyxel.text(50, 80, self.message, 10)
App()
このコードで何をしているのか
is_inside(x, y, w, h)
指定した矩形の中にマウスカーソルが入っているかを判定しています。
マウスUIの大半はこの判定を土台にします。
pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT)
左クリックされた瞬間だけ True になります。
押しっぱなしで連打扱いにならないので、「クリック1回」を作るには btnp() が自然です。 APIでも btnp() は押されたフレームだけ真で、MOUSE_BUTTON_LEFT はマウス左ボタン定数です。 (raw.githubusercontent.com)
btn() ではダメなのか
ダメではありませんが、用途が違います。
btnp() が向いている場面
- ボタンを押したときの1回だけの反応
- 決定
- ウィンドウを開く
- 項目選択
btn() が向いている場面
- ドラッグ中の継続判定
- スライダーをつまんで動かす
- 押している間ずっと変化するUI
右クリックや中クリックもできる
同じ要領で、
pyxel.MOUSE_BUTTON_RIGHTpyxel.MOUSE_BUTTON_MIDDLEpyxel.MOUSE_BUTTON_X1pyxel.MOUSE_BUTTON_X2
も使えます。これらはすべて定数として定義されています。 (raw.githubusercontent.com)
ここまで理解できれば十分なこと
- クリック判定は「矩形内か」+「
btnp(MOUSE_BUTTON_LEFT)」 - 1回だけ反応させたいなら
btnp() - ドラッグ系は
btn()も使う
ここまで分かれば、あとはUIごとの形へ落とし込むだけです。 (raw.githubusercontent.com)
3. ボタンUIを作りたい
概要
ボタンUIの本質は、クリック判定に「見た目の状態」を足すことです。
つまり、最低限必要なのは次の3状態です。
- 通常状態
- ホバー状態(マウスが乗っている)
- クリック状態(押した瞬間)
Pyxelには専用ボタン部品はありませんが、rect() / rectb() / text() とマウス判定を組み合わせれば十分作れます。図形描画と mouse_x / mouse_y / btnp() があれば成立します。 (raw.githubusercontent.com)
詳細解説
基本的なボタン例
import pyxel
class App:
def __init__(self):
pyxel.init(220, 140)
pyxel.mouse(True)
self.message = "waiting..."
pyxel.run(self.update, self.draw)
def is_inside(self, x, y, w, h):
return x <= pyxel.mouse_x < x + w and y <= pyxel.mouse_y < y + h
def update(self):
if self.is_inside(60, 40, 100, 24) and pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
self.message = "START clicked"
def draw(self):
pyxel.cls(0)
hover = self.is_inside(60, 40, 100, 24)
bg = 5 if hover else 1
fg = 10 if hover else 7
pyxel.rect(60, 40, 100, 24, bg)
pyxel.rectb(60, 40, 100, 24, fg)
pyxel.text(95, 48, "START", fg)
pyxel.text(20, 100, self.message, 7)
App()
このコードで何をしているのか
hover = self.is_inside(...)
マウスがボタン上にあるかどうかを見ています。
UIではこれを「ホバー状態」と考えると分かりやすいです。
bg と fg を変える
ホバー中だけ背景色や枠色を変えています。
これだけで「ボタンの上にマウスがある」というフィードバックになります。 Pyxel は 16色パレット前提なので、色差だけでも十分見せ方を作れます。 (raw.githubusercontent.com)
クリック時にメッセージ変更
実際のアプリなら、ここでシーン切り替えや設定変更を行います。
ボタンUIで大事なこと
1. 見た目の反応があること
クリックできる部品なら、ホバー時に色や枠を変えたほうが分かりやすいです。
2. テキスト位置を決めること
単に左上へ文字を書くのではなく、ボタンの中央や少し内側へ置くと見やすくなります。
中央揃えしたいなら Font.text_width() を使う設計も有効です。 Font.text_width() は文字列の表示幅を返します。 (raw.githubusercontent.com)
3. 入力と描画を分けること
update() でクリック判定、draw() で見た目を描く、という分担を守るとUIが整理しやすいです。
ここまで理解できれば十分なこと
- ボタンUIはクリック判定 + ホバー見た目
-
rect()/rectb()/text()で作れる - ホバー色を変えるだけでもかなり分かりやすくなる
ここまで分かれば、細かな見た目は自分のデザイン次第です。 (raw.githubusercontent.com)
4. スライダーUIを作りたい
概要
スライダーUIの本質は、横棒のどの位置にツマミがあるかを数値へ変換することです。
クリックだけでなく、押したまま動かす「ドラッグ」を使うので、btn() が活躍します。
Pyxelには専用スライダー部品はありませんが、マウス座標と図形描画だけで十分作れます。 btn() は押している間ずっと真なので、ドラッグ系UIに向いています。 (raw.githubusercontent.com)
詳細解説
一番基本の例
import pyxel
class App:
def __init__(self):
pyxel.init(240, 140)
pyxel.mouse(True)
self.value = 50 # 0〜100
self.dragging = False
pyxel.run(self.update, self.draw)
def is_inside(self, x, y, w, h):
return x <= pyxel.mouse_x < x + w and y <= pyxel.mouse_y < y + h
def update_slider(self, x, y, w):
relative = pyxel.mouse_x - x
relative = max(0, min(relative, w))
self.value = int(relative / w * 100)
def update(self):
bar_x, bar_y, bar_w = 40, 60, 120
knob_x = bar_x + int(bar_w * self.value / 100) - 4
if self.is_inside(knob_x, bar_y - 4, 8, 16) and pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
self.dragging = True
if not pyxel.btn(pyxel.MOUSE_BUTTON_LEFT):
self.dragging = False
if self.dragging:
self.update_slider(bar_x, bar_y, bar_w)
def draw(self):
pyxel.cls(0)
bar_x, bar_y, bar_w = 40, 60, 120
knob_x = bar_x + int(bar_w * self.value / 100) - 4
pyxel.text(40, 20, f"VALUE: {self.value}", 7)
pyxel.rect(bar_x, bar_y, bar_w, 4, 5)
pyxel.rect(knob_x, bar_y - 4, 8, 12, 10)
App()
このコードで何をしているのか
self.value
スライダーの値です。
ここでは 0〜100 の整数としています。
self.dragging
今ドラッグ中かどうかを表すフラグです。
ツマミをクリックした瞬間に True にし、ボタンを離したら False に戻します。
update_slider()
マウス座標をバー上の相対位置へ変換し、その割合から値を求めています。
何が重要なのか
スライダーは「UI部品」に見えますが、本質的には 値と位置の相互変換 です。
- 値 → ツマミ位置
- ツマミ位置 → 値
この2つができれば成立します。
応用例
- 音量(0〜100)
- 明るさ(0〜10)
- スクロール位置
- 設定値調整
- ゲーム内パラメータ変更
ホイールでも動かしたい場合
mouse_wheel を使えば、ホイールで値を変えるUIも作れます。
mouse_wheel は現在フレームのホイール値です。 (raw.githubusercontent.com)
self.value += pyxel.mouse_wheel
self.value = max(0, min(self.value, 100))
ここまで理解できれば十分なこと
- スライダーは「値 ↔ 位置」の変換
- ドラッグには
btn()が向いている - マウスボタンを離したらドラッグ終了にする
ここまで分かれば、見た目や操作感の微調整はUI設計の話です。 (raw.githubusercontent.com)
5. リストやメニューUIを作りたい
概要
リストやメニューUIは、基本的には 複数の矩形要素を縦または横に並べて、それぞれにホバー・クリック判定を付ける だけです。
キーボード版のメニューと違って、マウスUIでは「今どの項目の上にカーソルがあるか」がそのままフォーカス候補になります。Pyxelでは各項目を rect() / text() で描き、mouse_x / mouse_y で当たり判定すれば十分作れます。 (raw.githubusercontent.com)
詳細解説
一番基本の縦リスト
import pyxel
class App:
def __init__(self):
pyxel.init(220, 160)
pyxel.mouse(True)
self.items = ["START", "OPTION", "LOAD", "QUIT"]
self.hover_index = -1
self.selected = "none"
pyxel.run(self.update, self.draw)
def is_inside(self, x, y, w, h):
return x <= pyxel.mouse_x < x + w and y <= pyxel.mouse_y < y + h
def update(self):
self.hover_index = -1
for i, item in enumerate(self.items):
x, y, w, h = 40, 30 + i * 24, 120, 20
if self.is_inside(x, y, w, h):
self.hover_index = i
if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
self.selected = item
def draw(self):
pyxel.cls(0)
for i, item in enumerate(self.items):
x, y, w, h = 40, 30 + i * 24, 120, 20
hover = (i == self.hover_index)
bg = 5 if hover else 1
fg = 10 if hover else 7
pyxel.rect(x, y, w, h, bg)
pyxel.rectb(x, y, w, h, fg)
pyxel.text(x + 8, y + 6, item, fg)
pyxel.text(40, 140, f"SELECTED: {self.selected}", 7)
App()
このコードで何をしているのか
self.hover_index
今マウスが乗っている項目番号です。
ホバー中の色変更や説明表示に使います。
self.selected
実際にクリックして選ばれた項目です。
ホバーと選択は別の状態だ、と分けて考えるのが大事です。
clip() と組み合わせるとスクロールリストも作れる
項目数が多い場合は、clip() で表示範囲を限定し、スクロール量を持てば長い一覧にも対応できます。
clip() は描画範囲制限用です。 (raw.githubusercontent.com)
例の考え方
-
itemsをたくさん持つ -
scroll_yを持つ y = base_y + i * item_height - scroll_y-
clip()で枠内だけ描く -
mouse_wheelでscroll_yを変える
これで、ファイル一覧や設定一覧のようなUIへ発展できます。
ここまで理解できれば十分なこと
- リストやメニューは「複数のボタンの集合」
- ホバー状態と選択状態を分けると整理しやすい
-
clip()とmouse_wheelで長い一覧にもできる
ここまで分かれば、あとは項目数やレイアウトに応じた設計の話になります。 (raw.githubusercontent.com)
6. フォーカス(選択状態)を管理したい
概要
フォーカスとは、「今どのUI要素が操作対象として選ばれているか」を表す状態です。
マウスUIでは、少なくとも次の3つを区別すると整理しやすいです。
-
hover
マウスが乗っている -
active / pressed
今押している -
selected / focused
今選択中として保持されている
Pyxelには専用フォーカスマネージャはありませんが、状態変数を自分で持てば十分です。
入力APIが単純なので、むしろ自分で定義したほうが分かりやすいです。 btn() / btnp() / btnr() とマウス座標があれば、こうした状態管理はすべて自前で組み立てられます。 (raw.githubusercontent.com)
詳細解説
一番小さい例
import pyxel
class App:
def __init__(self):
pyxel.init(240, 160)
pyxel.mouse(True)
self.items = ["A", "B", "C"]
self.hover_index = -1
self.focus_index = 0
self.active_index = -1
pyxel.run(self.update, self.draw)
def is_inside(self, x, y, w, h):
return x <= pyxel.mouse_x < x + w and y <= pyxel.mouse_y < y + h
def update(self):
self.hover_index = -1
for i, item in enumerate(self.items):
x, y, w, h = 40, 30 + i * 30, 120, 24
if self.is_inside(x, y, w, h):
self.hover_index = i
if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
self.active_index = i
self.focus_index = i
if pyxel.btnr(pyxel.MOUSE_BUTTON_LEFT):
self.active_index = -1
def draw(self):
pyxel.cls(0)
for i, item in enumerate(self.items):
x, y, w, h = 40, 30 + i * 30, 120, 24
bg = 1
fg = 7
if i == self.focus_index:
bg = 5
if i == self.hover_index:
fg = 10
if i == self.active_index:
bg = 8
pyxel.rect(x, y, w, h, bg)
pyxel.rectb(x, y, w, h, fg)
pyxel.text(x + 10, y + 8, item, fg)
App()
このコードで何をしているのか
hover_index
今マウスが乗っている項目です。
カーソル移動に応じて毎フレーム変わります。
focus_index
今「選択状態として保持されている」項目です。
クリックして別項目を選ぶまで維持されます。
active_index
押している最中だけの項目です。
マウスボタンを離したら解除します。
ここでは btnr(MOUSE_BUTTON_LEFT) を使っています。 APIでは btnr() は離されたフレームで真です。 (raw.githubusercontent.com)
なぜ分ける必要があるのか
この3つを全部ごちゃ混ぜにすると、
「マウスが乗っただけなのに選択された」「押してる間だけしか選択が残らない」といった不自然なUIになりやすいです。
hover
見た目の反応用
active
押している最中の演出用
focus
選択中・確定中の保持用
このように役割を分けると、あとからキーボード操作やゲームパッド操作を足すときにも整理しやすくなります。
キーボード・ゲームパッドとの統合も可能
たとえばマウスだけでなく、上下キーやゲームパッドDPADでも focus_index を動かすようにすれば、
1つのUIを複数操作系で使えるようになります。 btn() / btnp() はキーボード・ゲームパッド・マウスで共通です。 (raw.githubusercontent.com)
ここまで理解できれば十分なこと
- フォーカス管理は状態変数を分けて持つのがコツ
-
hover、active、focusを分けると自然なUIになりやすい - マウスだけでなくキーボードUIにも発展しやすい
ここまで分かれば、あとはUI設計の話です。Pyxel側としては入力APIが分かっていれば十分です。 (raw.githubusercontent.com)
まとめ
Pyxelのマウス・UIまわりは、実質的には次の4つを押さえるとかなり整理できます。
-
マウス位置を取る
mouse_x,mouse_y,mouse_wheel -
マウスボタンを取る
btn(),btnp(),btnr()+MOUSE_BUTTON_* -
描画する
rect(),rectb(),text(),clip() -
状態を持つ
hover / active / selected / focus などを変数で管理する
整理するとこうです。
-
マウス入力を取る
mouse_x,mouse_y,mouse_wheel,mouse(True/False) -
クリック判定を作る
矩形判定 +btnp(MOUSE_BUTTON_LEFT) -
ボタンUIを作る
ホバー色変更 + クリック処理 -
スライダーUIを作る
ドラッグ中フラグ + 値↔位置変換 -
リストやメニューUIを作る
項目ごとの矩形判定 + hover/selected管理 -
フォーカスを管理する
hover、active、focusを分けて持つ
ここまで理解できたら、次に見るべきは mouse_x / mouse_y / mouse_wheel、mouse()、btn() / btnp() / btnr()、clip() のAPIリファレンスです。そこから先は「Pyxelで何ができるか」を知る段階ではなく、「自分のUIをどう操作させたいか」を詰める段階に入ります。 (raw.githubusercontent.com)
💾 データ保存・設定編
Pyxelで「ゲームの状態」や「設定」を保存したいときに最初に知りたくなる内容を、逆引き形式でまとめます。
対象にする項目は次の5つです。
- 設定を保存したい
- セーブデータを作りたい
- JSONなどでデータを保存したい
- 前回の状態を復元したい
- ランキングや履歴を保存したい
まず最初に:Pyxelで「保存」と言うと何を指すのか
ここはかなり重要です。
Pyxelには pyxel.load() / pyxel.save() があり、.pyxres を読み書きできます。
ただし、これは 画像・タイルマップ・Sound・Musicなどのリソースをまとめて扱う機能 です。
ゲームの「設定ファイル」や「セーブデータ」をそのまま保存する専用機能として使うものではありません。Pyxelの公開API定義でも load() / save() は .pyxres 用で、画像・タイルマップ・Sound・Musicの除外指定を持っています。
そのため、実際のゲームやツールでは次のように分けて考えるのが自然です。
Pyxel側で扱うもの
- 画像
- タイルマップ
- サウンド
- Music
- パレット
Python側で保存するもの
- 設定値
- セーブデータ
- 所持金やレベル
- 進行状況
- ランキング
- 履歴
- 前回開いていた画面
- 音量や表示オプション
JSONは Python標準の json.dump() / json.load() で扱え、pathlib.Path はパス操作やファイル入出力、ディレクトリ作成に向いています。
つまり、Pyxelのゲームデータ保存は「Pyxelの描画・音機能」+「Python標準のファイル保存」 で作る、という理解が一番実用的です。
1. 設定を保存したい
概要
設定保存で一番扱いやすいのは JSON です。
たとえば、
- BGM音量
- 効果音音量
- フルスクリーンのON/OFF
- 言語設定
- ボタン配置
- テーマ色
- ソート順
のような「名前付きの値」は、辞書にしてJSONへ保存すると分かりやすいです。Pythonの json.dump() はPythonオブジェクトをJSONとしてファイルへ保存し、json.load() はJSONファイルをPythonオブジェクトへ戻します。
詳細解説
一番基本の例
ファイル構成
project/
├── main.py
└── save/
└── settings.json
コード
import json
from pathlib import Path
import pyxel
SETTINGS_PATH = Path("save/settings.json")
DEFAULT_SETTINGS = {
"bgm_volume": 0.12,
"se_volume": 0.18,
"fullscreen": False,
"language": "ja",
}
def load_settings():
if not SETTINGS_PATH.exists():
return DEFAULT_SETTINGS.copy()
with SETTINGS_PATH.open("r", encoding="utf-8") as f:
data = json.load(f)
settings = DEFAULT_SETTINGS.copy()
settings.update(data)
return settings
def save_settings(settings):
SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
with SETTINGS_PATH.open("w", encoding="utf-8") as f:
json.dump(settings, f, ensure_ascii=False, indent=2)
class App:
def __init__(self):
pyxel.init(220, 120)
self.settings = load_settings()
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
self.settings["fullscreen"] = not self.settings["fullscreen"]
save_settings(self.settings)
def draw(self):
pyxel.cls(0)
pyxel.text(20, 20, f"fullscreen: {self.settings['fullscreen']}", 7)
pyxel.text(20, 40, "Z: toggle and save", 10)
App()
このコードで何をしているのか
SETTINGS_PATH = Path("save/settings.json")
保存先を pathlib.Path で表しています。
Path はファイルパスをオブジェクトとして扱う標準機能で、OSごとの差を吸収しやすく、exists()、open()、mkdir() などをまとめて使えます。 (Python documentation)
DEFAULT_SETTINGS
初回起動時や読み込み失敗時の基準値です。
設定保存では、保存ファイルがなくても動く初期値 を必ず用意しておくと安全です。
load_settings()
settings.json が存在すれば読み込み、なければデフォルト設定を返します。
json.load(f) はファイルからJSONを読み込む標準的な方法です。 (Python documentation)
save_settings()
設定ディレクトリを作ってから保存しています。
Path.mkdir(parents=True, exist_ok=True) は、必要なら親ディレクトリごと作り、既に存在してもエラーにしない書き方です。
json.dump(..., ensure_ascii=False, indent=2) にしているので、日本語も読みやすい形で保存されます。 JSONは文字列キーを持つ辞書との相性が良く、ensure_ascii=False を使うと非ASCII文字をそのまま書けます。 (Python documentation)
実務で大事なこと
1. 設定とセーブデータは分ける
設定は「ゲーム全体の好み」、セーブデータは「プレイ進行」です。
一緒のファイルに入れることもできますが、分けたほうが見通しがよいです。
2. デフォルト値を必ず持つ
設定ファイルが壊れたり、一部キーが欠けたりしても起動できるように、
DEFAULT_SETTINGS.copy() を土台にして update() で上書きする形が扱いやすいです。
3. JSONはキーが常に文字列になる
JSONでは辞書のキーは常に文字列です。
そのため、タプルや数値をキーにした辞書をそのまま保存する設計は避けたほうが安全です。PythonのJSONドキュメントでも、JSON化の際にキーは文字列へ強制変換されると説明されています。 (Python documentation)
ここまで理解できれば十分なこと
- 設定保存は JSON が扱いやすい
-
Pathで保存先を管理し、json.dump()/json.load()を使う - 初期値を必ず用意する
- 設定ファイルは
settings.jsonのように分けると見通しがよい
ここまで分かれば、細かいJSONの引数やパス操作は Python の json / pathlib のリファレンスを見れば十分です。 (Python documentation)
2. セーブデータを作りたい
概要
セーブデータも基本は JSON で十分です。
ただし、設定ファイルと違って、セーブデータは ゲーム進行を復元するために必要な最小限の状態 を保存するのが重要です。
詳細解説
たとえばRPGなら、
- プレイヤー座標
- レベル
- HP
- 所持金
- 所持アイテム
- 進行フラグ
- 現在マップ名
などを保存します。
一番基本の例
ファイル構成
project/
├── main.py
└── save/
└── slot1.json
コード
import json
from pathlib import Path
import pyxel
SAVE_PATH = Path("save/slot1.json")
def save_game(data):
SAVE_PATH.parent.mkdir(parents=True, exist_ok=True)
with SAVE_PATH.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def load_game():
if not SAVE_PATH.exists():
return None
with SAVE_PATH.open("r", encoding="utf-8") as f:
return json.load(f)
class App:
def __init__(self):
pyxel.init(220, 140)
self.player_x = 40
self.player_y = 40
self.level = 1
self.gold = 100
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btn(pyxel.KEY_LEFT):
self.player_x -= 1
if pyxel.btn(pyxel.KEY_RIGHT):
self.player_x += 1
if pyxel.btn(pyxel.KEY_UP):
self.player_y -= 1
if pyxel.btn(pyxel.KEY_DOWN):
self.player_y += 1
if pyxel.btnp(pyxel.KEY_S):
save_game({
"player_x": self.player_x,
"player_y": self.player_y,
"level": self.level,
"gold": self.gold,
})
if pyxel.btnp(pyxel.KEY_L):
data = load_game()
if data:
self.player_x = data["player_x"]
self.player_y = data["player_y"]
self.level = data["level"]
self.gold = data["gold"]
def draw(self):
pyxel.cls(0)
pyxel.rect(self.player_x, self.player_y, 8, 8, 8)
pyxel.text(10, 10, f"LV {self.level}", 7)
pyxel.text(10, 20, f"GOLD {self.gold}", 7)
pyxel.text(10, 120, "S: save / L: load", 10)
App()
このコードで何をしているのか
save_game({...})
今のゲーム状態を辞書にして保存しています。
セーブデータの本質は「あとで復元するための状態のスナップショット」です。
load_game()
JSONを読み込み、その内容でゲーム状態を戻しています。
何を保存すべきか
ここはかなり重要です。
セーブデータには「再開に必要なもの」だけを保存するのが基本です。
保存向き
- 数値
- 文字列
- 真偽値
- リスト
- 辞書
- 進行フラグ
- 現在位置
- 所持品一覧
そのまま保存しないほうがよいもの
- Pyxelの
ImageやTilemapオブジェクト -
SoundやMusicオブジェクト - 関数やクラスインスタンスそのもの
- 一時的な描画状態
JSONはリスト・辞書・数値・文字列・真偽値などの保存に向いていますが、任意のクラスインスタンスをそのまま直列化するには追加処理が必要です。Pythonのチュートリアルでも、単純なシリアライズはリストや辞書向きだと説明されています。 (Python documentation)
実務で大事なこと
1. セーブデータのバージョンを持つ
将来項目を増やしたくなったときに備えて、最初から version を入れておくと安全です。
{
"version": 1,
"player_x": 40,
"player_y": 40
}
2. 「保存対象」を明確にする
ゲーム全体の全変数を保存するのではなく、
どの変数が復元に必要か を明示的に選ぶほうが保守しやすいです。
3. スロット分けしやすい名前にする
slot1.json, slot2.json, slot3.json のようにすると、
複数セーブへ発展しやすいです。
ここまで理解できれば十分なこと
- セーブデータもJSONで作れる
- 保存するのは「復元に必要な状態」
- オブジェクトそのものではなく、辞書へ落として保存する
- スロット分けやバージョン管理を意識すると後で楽
ここまで分かれば、あとはゲームごとの状態設計の話になります。JSONの使い方としては十分です。 (Python documentation)
3. JSONなどでデータを保存したい
概要
Pyxel専用ではなく、Python標準の json を使います。
json.dump(obj, f) でファイルへ保存し、json.load(f) で読み込みます。
これは設定・セーブ・ランキング・履歴など、ほとんどの軽量データ保存にそのまま使えます。Pythonのチュートリアルでも json.dump(x, f) / json.load(f) の基本例が示されています。 (Python documentation)
詳細解説
JSONが向いている理由
1. 人間が読める
保存ファイルをテキストエディタで確認できます。
2. デバッグしやすい
壊れたとき、何が入っているか見やすいです。
3. Python標準で使える
追加ライブラリがいりません。
一番基本の書き方
import json
from pathlib import Path
path = Path("save/data.json")
path.parent.mkdir(parents=True, exist_ok=True)
data = {
"name": "player1",
"level": 5,
"items": ["potion", "key"],
}
with path.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
with path.open("r", encoding="utf-8") as f:
loaded = json.load(f)
このコードで何をしているのか
path.parent.mkdir(parents=True, exist_ok=True)
保存先のディレクトリを作っています。
存在しなくても作れるようにしているので、安全に保存できます。Path.mkdir() は exist_ok をサポートします。 (Python documentation)
json.dump(..., ensure_ascii=False, indent=2)
-
ensure_ascii=False
日本語を\uXXXXではなくそのまま書く -
indent=2
見やすく整形する
日本語が入る設定ファイルやセーブデータでは、この2つを付けるとかなり扱いやすいです。PythonのJSONドキュメントでは dump() / dumps() の各引数が説明されています。 (Python documentation)
JSON以外の選択肢はあるか
あります。
ただし、Pyxelのゲームやツールで最初に使うなら JSON が最も扱いやすいです。
たとえば、
-
.txt
単純だが構造化しづらい -
.csv
表形式には向くが階層データに弱い - 独自形式
自由だが保守しづらい
という違いがあります。
JSONで注意すべきこと
辞書のキーは文字列になる
JSONでは辞書のキーは常に文字列です。
そのため、たとえば (x, y) タプルをキーにした辞書は、そのまま保存設計にしないほうが安全です。PythonのJSONドキュメントにも、キーは文字列へ変換されるとあります。 (Python documentation)
オブジェクトはそのまま保存しづらい
クラスインスタンスをそのまま保存するのではなく、辞書へ変換して保存するのが基本です。Pythonのチュートリアルでも、任意のクラスをJSONにするには追加の工夫が必要だと説明されています。 (Python documentation)
ここまで理解できれば十分なこと
- JSON保存は
json.dump()/json.load() -
Pathと組み合わせると使いやすい -
ensure_ascii=Falseとindent=2が実用的 - 辞書キーやクラスインスタンスには注意が必要
ここまで分かれば、細かいJSON仕様は Python の json リファレンスを見れば十分です。 (Python documentation)
4. 前回の状態を復元したい
概要
「前回の状態を復元する」とは、
アプリ終了時に必要な状態を保存し、次回起動時に読み戻すことです。
これは設定保存やセーブデータと同じ仕組みで作れますが、保存対象が少し違います。
詳細解説
たとえば復元したいものとしては、
- 前回開いていた画面
- 最後に選んでいたメニュー位置
- 最後に見ていたリストのスクロール位置
- 最後に開いていたカテゴリ
- 前回の音量設定
などがあります。
一番基本の例
ファイル構成
project/
├── main.py
└── save/
└── session.json
コード
import json
from pathlib import Path
import pyxel
SESSION_PATH = Path("save/session.json")
def load_session():
if not SESSION_PATH.exists():
return {
"menu_index": 0,
"scroll_y": 0,
"last_screen": "main_menu",
}
with SESSION_PATH.open("r", encoding="utf-8") as f:
return json.load(f)
def save_session(data):
SESSION_PATH.parent.mkdir(parents=True, exist_ok=True)
with SESSION_PATH.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
class App:
def __init__(self):
pyxel.init(220, 140)
session = load_session()
self.menu_index = session["menu_index"]
self.scroll_y = session["scroll_y"]
self.last_screen = session["last_screen"]
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_DOWN, hold=15, repeat=3):
self.menu_index = min(self.menu_index + 1, 4)
if pyxel.btnp(pyxel.KEY_UP, hold=15, repeat=3):
self.menu_index = max(self.menu_index - 1, 0)
if pyxel.btnp(pyxel.KEY_S):
save_session({
"menu_index": self.menu_index,
"scroll_y": self.scroll_y,
"last_screen": self.last_screen,
})
def draw(self):
pyxel.cls(0)
pyxel.text(20, 20, f"menu_index: {self.menu_index}", 7)
pyxel.text(20, 40, f"last_screen: {self.last_screen}", 7)
pyxel.text(20, 100, "S: save session", 10)
App()
このコードで何をしているのか
session.json
これは「プレイデータ」ではなく、アプリの前回状態を戻すためのファイルです。
menu_index
最後に選んでいた項目番号を保存しています。
last_screen
どの画面にいたかを保存しています。
起動時にこれを見て、最初に開く画面を変えることもできます。
どういうときに便利か
- ランチャーで前回のカテゴリを開きたい
- 設定画面の最後の位置から再開したい
- 長い一覧のスクロール位置を覚えたい
- ノベルゲームのログ閲覧位置を覚えたい
実務での考え方
セーブデータと分ける
前回状態は「快適さ」のための保存であり、
ゲーム進行そのものとは別のことが多いです。
そのため session.json などで分けると見通しがよいです。
保存頻度を考える
終了時だけ保存するのか、選択が変わるたびに保存するのかで設計が変わります。
小規模なら、変わったタイミングで都度保存でも十分です。
ここまで理解できれば十分なこと
- 前回状態の復元も JSON 保存で作れる
- 保存するのは「次回起動時に戻したいUI状態」
- セーブデータと分けると整理しやすい
ここまで分かれば、後はどの状態を残したいかという設計の話です。 (Python documentation)
5. ランキングや履歴を保存したい
概要
ランキングや履歴も、JSONのリストで持つのが簡単です。
たとえば、
- ハイスコア上位10件
- 最近プレイした順
- 最近開いたファイル履歴
- 最近使った設定履歴
のようなものは、辞書の配列 として保存すると扱いやすいです。
JSONはリストと辞書の組み合わせ保存に向いています。 (Python documentation)
詳細解説
一番基本のランキング例
ファイル構成
project/
├── main.py
└── save/
└── ranking.json
コード
import json
from pathlib import Path
import pyxel
RANKING_PATH = Path("save/ranking.json")
def load_ranking():
if not RANKING_PATH.exists():
return []
with RANKING_PATH.open("r", encoding="utf-8") as f:
return json.load(f)
def save_ranking(ranking):
RANKING_PATH.parent.mkdir(parents=True, exist_ok=True)
with RANKING_PATH.open("w", encoding="utf-8") as f:
json.dump(ranking, f, ensure_ascii=False, indent=2)
def add_score(name, score):
ranking = load_ranking()
ranking.append({"name": name, "score": score})
ranking.sort(key=lambda x: x["score"], reverse=True)
ranking = ranking[:10]
save_ranking(ranking)
class App:
def __init__(self):
pyxel.init(220, 160)
self.score = 0
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
self.score += 100
if pyxel.btnp(pyxel.KEY_S):
add_score("PLAYER", self.score)
def draw(self):
pyxel.cls(0)
pyxel.text(20, 20, f"SCORE: {self.score}", 7)
pyxel.text(20, 40, "Z: add 100", 7)
pyxel.text(20, 50, "S: save to ranking", 10)
ranking = load_ranking()
pyxel.text(20, 80, "RANKING", 8)
for i, row in enumerate(ranking):
pyxel.text(20, 95 + i * 10, f"{i+1}. {row['name']} {row['score']}", 7)
App()
このコードで何をしているのか
ranking.append({"name": name, "score": score})
ランキングを「辞書の配列」として持っています。
JSONのリスト構造と相性がよい形です。
ranking.sort(...)
スコア順に並べ替えています。
ranking = ranking[:10]
上位10件だけ残しています。
履歴保存も同じ考え方で作れる
たとえば「最近使ったファイル履歴」なら、次のようにできます。
ファイル構成
project/
├── main.py
└── save/
└── history.json
データ例
[
"roms/mario.nes",
"roms/zelda.nes",
"roms/ff3.nes"
]
追加処理の例
def add_history(path_str):
history = load_history()
if path_str in history:
history.remove(path_str)
history.insert(0, path_str)
history = history[:20]
save_history(history)
何が重要なのか
ランキングや履歴では、並び順ルール と 最大件数 を決めることが大切です。
ランキング
- 高い順
- 上位10件だけ保存
履歴
- 新しい順
- 重複は消して先頭へ
- 20件まで
このルールを決めるだけで、かなり使いやすくなります。
ここまで理解できれば十分なこと
- ランキングや履歴は JSON のリストで保存しやすい
- 辞書の配列、または文字列配列が扱いやすい
- 並び順と最大件数を最初に決めると保守しやすい
ここまで分かれば、あとは自分のアプリの用途に合わせて項目を決めるだけです。JSONの基本理解としては十分です。 (Python documentation)
まとめ
Pyxelのデータ保存まわりは、Pyxelのリソース保存 と Python標準のデータ保存 を分けて考えると整理しやすいです。
.pyxres は画像・タイルマップ・Sound・Music用で、設定やセーブデータには Python の json と pathlib を使うのが実用的です。JSONは dump() / load() で扱え、Path は保存先の管理やディレクトリ作成に向いています。
整理するとこうです。
-
設定を保存する
settings.jsonに辞書で保存する -
セーブデータを作る
slot1.jsonなどにゲーム進行を保存する -
JSONなどで保存する
json.dump()/json.load()を使う -
前回状態を復元する
session.jsonにUI状態や前回位置を保存する -
ランキングや履歴を保存する
JSONのリストで管理する
ここまで理解できたら、次に見るべきは Python の json と pathlib のリファレンス、それと自分のゲームで「何を保存対象にするか」の設計です。そこから先は、Pyxelの機能を知る段階ではなく、アプリの状態管理をどう整理するかを詰める段階に入ります。 (Python documentation)
🌐 配布・実行編
Pyxelで作ったアプリを他の人に渡したり、Webで公開したりするときに最初に知りたくなる内容を、逆引き形式でまとめます。
対象にする項目は次の5つです。
- アプリを配布したい
- exe化したい
- Webで動かしたい
- HTMLとして公開したい
- 軽量に配布したい
まず最初に:Pyxelの配布はどう考えればよいか
Pyxelでアプリを配布するときは、まず .pyxapp という「Pyxelアプリファイル」 を作るのが基本です。
これは Pyxel が公式に用意している配布用フォーマットで、pyxel package APP_DIR STARTUP_SCRIPT_FILE で作成します。
作成した .pyxapp は pyxel play PYXEL_APP_FILE で実行できます。さらに、その .pyxapp を pyxel app2exe や pyxel app2html で別形式へ変換できます。
つまり、配布の流れは次のように考えると整理しやすいです。
-
開発中のアプリ本体
Pythonコード、フォント、画像、.pyxresなど -
配布用にまとめる
pyxel packageで.pyxappを作る -
配布形態を選ぶ
-
.pyxappのまま配る -
app2exeで実行ファイルにする -
app2htmlでHTMLにする - Web Launcher でGitHub公開する
-
この「まず .pyxapp を作る」という考え方を押さえると、
Pyxelの配布・実行まわりがかなり分かりやすくなります。公式README/PyPIでもこの流れで案内されています。
1. アプリを配布したい
概要
Pyxelでは、まず .pyxapp を作って配布単位にするのが基本です。
.pyxapp は Pyxel 専用のアプリ配布ファイルで、クロスプラットフォーム向けの配布形式として案内されています。作成コマンドは次の通りです。
詳細解説
pyxel package APP_DIR STARTUP_SCRIPT_FILE
一番基本の例
ファイル構成(開発中)
my_game/
├── main.py
├── game.pyxres
├── fonts/
│ └── PixelMplus12-Regular.ttf
└── modules/
└── helper.py
この場合、main.py が起動スクリプトなら、たとえば次のようにします。
pyxel package my_game main.py
すると、my_game.pyxapp のような配布用アプリファイルが作られます。
README / PyPI の説明では、追加モジュールやリソースが必要ならアプリディレクトリに置く とされています。つまり、配布に必要なものは APP_DIR 側へまとめておくのが基本です。
このコマンドで何をしているのか
APP_DIR
配布対象のディレクトリです。
ここに入っているコードやリソースが、Pyxelアプリとしてまとめられます。
STARTUP_SCRIPT_FILE
最初に実行するPythonファイルです。
たとえば main.py を起点に起動するなら、それを指定します。
.pyxapp はどう実行するのか
作った .pyxapp は、Pyxelが入っている環境なら次のように実行できます。
pyxel play my_game.pyxapp
つまり、.pyxapp は「Pyxelアプリのまとまった1ファイル」ですが、
実行側にPyxelランタイムがある前提の形式です。
メタデータも付けられる
起動スクリプトの先頭に、次のようなコメントを書くとメタデータを埋め込めます。
title と author が必須で、desc、site、license、version なども指定できます。これは PyPI の配布説明に記載されています。
# title: My Pyxel Game
# author: Your Name
# desc: A small retro action game
# site: https://example.com
# license: MIT
# version: 1.0
何が便利なのか
1. 配布単位を1つにまとめやすい
開発中は複数ファイルでも、配布時は .pyxapp を中心に考えられます。
2. exe化やHTML化の土台になる
後述する app2exe、app2html の入力として使えます。公式説明でも「.pyxapp は app2exe / app2html へ変換できる」と案内されています。
3. 配布工程が整理しやすい
「開発用フォルダ」と「配布用ファイル」が分かれるので、構造が見やすくなります。
ここまで理解できれば十分なこと
- 配布の基本は
.pyxapp - 作るには
pyxel package APP_DIR STARTUP_SCRIPT_FILE - 実行には
pyxel play file.pyxapp - 配布に必要なリソースは
APP_DIRに入れておく
ここまで分かれば、詳細は pyxel package / pyxel play の公式説明を見れば十分です。
2. exe化したい
概要
Pyxelでは、作成した .pyxapp を pyxel app2exe で実行ファイルへ変換できます。
公式の配布説明では、.pyxapp は pyxel app2exe または pyxel app2html で変換できると案内されています。 changelog にも app2exe 修正履歴があり、継続的にメンテナンスされている機能だと分かります。
詳細解説
基本の流れ
まず .pyxapp を作ります。
pyxel package my_game main.py
そのあと、作った .pyxapp を app2exe に渡します。
pyxel app2exe my_game.pyxapp
どういう意味で「exe化」なのか
ここで言う exe 化は、「相手に Pyxel コマンドを直接叩かせずに、アプリとして起動しやすくする」方向の配布です。
ただし、公式の説明では「.pyxapp を executable に変換できる」とだけ案内されており、
実際にどのOSでどの形式名になるか、どこまで自己完結するかは、利用環境で確認したほうが安全です。 changelog で app2exe 修正が何度も入っていることからも、環境差には注意したほうがよいです。
実務での考え方
1. まず .pyxapp を正常に動かす
いきなり exe 化に進むより、先に
pyxel play my_game.pyxapp
で正常動作を確認するほうが安全です。
.pyxapp で問題があると、exe化しても当然そのまま問題を引き継ぎます。 .pyxapp は pyxel play で実行できるのが公式フローです。
2. 配布相手のOSを意識する
Windows向けに配るのか、macOS向けに配るのかで、受け取る側の期待は変わります。
Pyxel公式が app2exe を用意しているとはいえ、配布前には対象OSで起動確認したほうが安心です。
3. 追加ファイルが必要か確認する
フォント .ttf や設定の初期JSONなど、起動時に参照する外部ファイルがあれば、
それらが APP_DIR に含まれていて .pyxapp へ入っているかを確認します。
公式説明でも「リソースや追加モジュールが必要ならアプリディレクトリに置く」とされています。
ここまで理解できれば十分なこと
- exe化は
pyxel app2exe - その前提として
.pyxappを作る - まず
.pyxappの動作確認をする - 実際の配布前に対象OSで動作確認するのが安全
ここまで分かれば、あとは実際の環境で出力結果を確認する段階です。公式機能としては app2exe の存在が分かっていれば十分です。
3. Webで動かしたい
概要
PyxelをWebで動かす方法として、公式FAQとWikiでは主に次の3つが案内されています。
詳細解説
- Pyxel Web Launcher
app2htmlでHTML化- Pyxelカスタムタグ入りHTMLを書く
このうち、一番手軽なのは Web Launcher と app2html です。
方法1:Pyxel Web Launcher を使う
Pyxel Wiki では、GitHubリポジトリを指定してブラウザで実行する方法 が案内されています。
これは「GitHubに公開したアプリを、特別な事前準備なしでWebブラウザから動かす」ための方法として説明されています。
つまり、ローカルでHTMLを書かなくても、公開済みGitHubリポジトリを入口にしてWeb実行へ持っていけるのが特徴です。
どういうときに向いているか
- GitHubでコード公開している
- まず手早くブラウザ実行を試したい
- 配布というより、手軽な共有をしたい
方法2:app2html を使う
これが「配布物としてHTMLを持ちたい」場合の基本です。
Wikiでは、.pyxapp を app2html で HTML ファイルへ変換できる と説明されています。
さらに、「作成されたHTMLファイルはサーバー不要で単独実行できる」と明記されています。
ここで理解すべきこと
「Webで動かす」と言っても、やりたいことは2種類あります。
1. とにかくブラウザで遊べればよい
→ Web Launcher が手軽
2. 自分で配布用HTMLを持ちたい
→ app2html が向いている
実務での考え方
GitHub公開前提なら Web Launcher は便利
READMEとあわせて公開すれば、すぐブラウザ実行まで持っていきやすいです。
公式Wikiでも「GitHubリポジトリを指定する方法」が最も手軽な方法として紹介されています。
きちんと成果物を持ちたいなら app2html
自分でHTMLファイルを持てるので、配布や保管の感覚が分かりやすいです。
ここまで理解できれば十分なこと
- Web公開方法は公式に複数ある
- 手軽なのは Web Launcher
- 成果物として持ちたいなら
app2html - FAQ でも Web Launcher / app2html / カスタムタグの3方式が案内されている
ここまで分かれば、詳細は公式FAQとWikiを見れば十分です。
4. HTMLとして公開したい
概要
Pyxelでは、.pyxapp を pyxel app2html でHTMLファイルへ変換できます。
公式の配布説明とWeb Wikiの両方で案内されている方法です。
Wikiでは「作成されたHTMLファイルはサーバー不要で単独実行できる」とも書かれています。
詳細解説
基本の流れ
まず .pyxapp を作ります。
pyxel package my_game main.py
次に HTML 化します。
pyxel app2html my_game.pyxapp
どういう成果物になるのか
ここでできるHTMLは、Pyxelアプリをブラウザで実行するためのファイルです。
Wikiの説明では、このHTMLはサーバー不要で単独実行できる とされています。
つまり、少なくとも「簡単な共有」や「成果物として1つ持つ」という用途に向いています。
ファイル構成の例
開発中
my_game/
├── main.py
├── game.pyxres
└── fonts/
└── PixelMplus12-Regular.ttf
配布工程
pyxel package my_game main.py
pyxel app2html my_game.pyxapp
配布後のイメージ
dist/
└── my_game.html
HTML公開はどういうときに向いているか
- インストール不要で配りたい
- GitHub Pages のような静的公開に乗せたい
- デモ版や体験版を簡単に共有したい
- ブログ記事やポートフォリオに埋め込みたい
カスタムHTMLを書く方法もある
Wikiでは、Pyxelカスタムタグを使ったHTML作成 も第3の方法として紹介されています。
これはよりカスタマイズしたい場合の手段ですが、最初は app2html のほうが理解しやすいです。
FAQ でも「Web Launcher / app2html / カスタムタグ」の3方式が案内されています。
実務での考え方
まずは app2html からで十分
HTML公開の入口としては最も分かりやすいです。
配布前にブラウザで確認する
ローカルで一度開いて、入力や表示に問題がないか見たほうが安全です。
スマホ対応も意識する
Wikiでは Pyxel Web は PC だけでなく、対応ブラウザのスマートフォンやタブレットでも動くと説明されています。
ただし、操作感はアプリ内容によって変わるので、実機確認したほうがよいです。
ここまで理解できれば十分なこと
- HTML化は
pyxel app2html - 前提として
.pyxappが必要 - 作成されたHTMLは単独実行できると公式Wikiで案内されている
- 最初は
app2htmlを覚えれば十分
ここまで分かれば、詳細は Web Wiki を見れば十分です。
5. 軽量に配布したい
概要
ただし、ここでの「軽量化」は Pyxel 専用の魔法のコマンドがあるというより、
何を含め、何を含めないかを整理すること が中心になります。
公式の pyxel package 説明でも「必要なリソースや追加モジュールはアプリディレクトリへ置く」とされているので、裏を返せば、不要なものを入れないことがそのまま軽量化 になります。
詳細解説
まず何が重くなりやすいか
Pyxelアプリ本体そのものより、外部リソースのほうがサイズを押し上げやすいことがあります。たとえば、
- 日本語用
.ttf/.otf - 大きな
.png - 未使用の
.pyxres - 使っていない音素材
- テスト用データ
- READMEやスクリーンショットをそのまま配布フォルダへ入れている
などです。
一番基本の考え方
1. APP_DIR に本当に必要なものだけ置く
pyxel package APP_DIR STARTUP_SCRIPT_FILE は、そのアプリディレクトリをもとに配布物を作ります。
だからこそ、APP_DIR に不要物を入れないことが最初の軽量化です。
例:軽量化前
my_game/
├── main.py
├── test_main.py
├── draft_notes.txt
├── screenshots/
│ ├── title.png
│ └── boss.png
├── big_unused_font.ttf
├── game.pyxres
└── old_game_backup.pyxres
例:軽量化後
my_game/
├── main.py
├── game.pyxres
└── fonts/
└── ui_font.ttf
2. フォントを見直す
日本語フォントは特に大きくなりやすいです。
そのため、
- 本当に必要なフォントだけ入れる
- 1種類に絞る
- サイズ違いを何個も別ファイルで持ちすぎない
といった見直しが有効です。
3. リソースを分けすぎない
細かく分けること自体は悪くありませんが、
配布時に複数の .pyxres や .png を全部持たせると、未使用分まで含んでしまうことがあります。
使わないテーマ、使わないステージ、古い試作素材などは除外したほうが軽くなります。
4. HTML配布でも不要物を持たせない
app2html を使う場合でも、元になる .pyxapp 側に不要物が多ければ、そのぶん成果物へ影響しやすいです。
つまり、HTML化の前段階で整理するのが基本です。 app2html は .pyxapp を入力にするためです。
5. Web共有だけなら Web Launcher も検討する
「成果物ファイルを最小限にしたい」という意味では、
GitHub公開 + Web Launcher のほうが運用しやすいこともあります。
これは「ローカル配布物を軽量化する」というより、「配布形態そのものを軽くする」発想です。
公式Wikiでも、GitHubリポジトリを指定するWeb Launcherが最も手軽な方法として紹介されています。
実務での軽量化チェックリスト
- 配布ディレクトリに未使用ファイルが入っていないか
- 巨大なフォントを複数入れていないか
- 古い
.pyxresやバックアップが残っていないか - テスト用画像やメモを含めていないか
- 配布方法が本当に exe / HTML / Web Launcher のどれに向いているか
ここまで理解できれば十分なこと
- 軽量配布の本質は「不要物を入れないこと」
-
pyxel packageの入力ディレクトリを整理するのが最初 - フォントと未使用リソースがサイズを押し上げやすい
- 配布形態そのものも見直す価値がある
ここまで分かれば、あとは自分のプロジェクトの中身を棚卸しする段階です。Pyxel側の公式配布フローが理解できていれば十分です。
まとめ
Pyxelの配布・実行まわりは、まず .pyxapp を作り、それをどの形で配るか選ぶ と考えると整理しやすいです。
公式には、pyxel package で .pyxapp を作り、pyxel play で実行し、必要に応じて pyxel app2exe や pyxel app2html へ変換する流れが案内されています。
Web公開については FAQ / Wiki で Web Launcher / app2html / カスタムタグ の3方式が紹介されています。
整理するとこうです。
-
アプリを配布する
pyxel package APP_DIR STARTUP_SCRIPT_FILE -
exe化する
.pyxappをpyxel app2exe -
Webで動かす
Web Launcher またはapp2html -
HTMLとして公開する
.pyxappをpyxel app2html -
軽量に配布する
配布ディレクトリを整理して不要物を入れない
ここまで理解できたら、次に見るべきは Pyxel の公式 README / FAQ / Web Wiki と、実際の package / play / app2exe / app2html の挙動確認です。そこから先は「Pyxelで何ができるか」を知る段階ではなく、「自分はどの配布形態を選ぶか」を詰める段階に入ります。
⚙️ 環境・応用編
Pyxelを「普通のPC以外も含めて安定して動かしたい」ときに最初に知りたくなる内容を、逆引き形式でまとめます。
対象にする項目は次の5つです。
- Linuxや組み込み環境で動かしたい
- SDL設定を調整したい
- 音が出ない問題を解決したい
- 画面が出ない問題を解決したい
- パフォーマンスを改善したい
まず最初に:Pyxelの環境依存は何が原因で起きるのか
PyxelはPython製のレトロゲームエンジンですが、実際に画面表示や音声出力を行う部分では SDL2 系の仕組みに強く依存しています。Pyxelの公式リポジトリでは Linux 向け改善が継続して行われており、たとえば KMSDRM video driver の追加、Waylandまわりのマウス修正、Wayland native driver preference などが changelog に載っています。つまり、Pyxelそのものよりも、どの SDL ビデオ/音声ドライバで動いているか が表示や音の成否を大きく左右します。 :contentReference[oaicite:0]{index=0}
SDL 側の公式FAQでは、Linux では SDL_VIDEODRIVER で x11 / wayland / kmsdrm などのビデオドライバを選べ、SDL_AUDIODRIVER で pulseaudio / pipewire / alsa / jack などの音声ドライバを選べると説明されています。つまり、Pyxelの環境調整とは多くの場合、SDLのドライバ選択を適切にすること です。
この章で理解してほしい一番大事なことは、問題を「Pyxelが悪い」「Linuxが悪い」とひとまとめにしないことです。実際には、
- Pyxelのバージョン
- SDL2の対応状況
- X11 / Wayland / KMSDRM のどれで動いているか
- PulseAudio / PipeWire / ALSA のどれを使っているか
の組み合わせで結果が変わります。Pyxelの changelog に Linux / Wayland / KMSDRM の個別修正が多数あること自体が、それを示しています。
1. Linuxや組み込み環境で動かしたい
概要
できますが、普通のデスクトップ環境と、組み込みLinux環境では前提がかなり違います。Pyxel は現行版でも Linux をサポートしており、GitHub の最新リリースは Pyxel 2.9.0(2026-04-10時点) です。さらに 2.7.3 では embedded Linux devices 向けの KMSDRM video driver が追加されています。つまり、Pyxel側も「組み込みLinuxで使われること」を意識して改善が入っています。
詳細解説
まず押さえるべき実行パターン
Linux系で Pyxel を動かすとき、実際の表示経路はざっくり次のどれかになります。
- X11
- Wayland
- KMSDRM
- まれに framebuffer 系や仮想環境経由
SDL公式FAQでは Linux の代表的なビデオドライバとして x11、wayland、kmsdrm が案内されています。Pyxel changelog でも Wayland と KMSDRM に関する修正が継続しているため、現実的にもこの3系統を意識しておけば十分です。 :contentReference[oaicite:4]{index=4}
どの環境に向いているか
X11
一番「普通のLinuxデスクトップ」寄りです。
ウィンドウを出して確認しやすく、一般的なPC環境ではまずここから試すのが自然です。SDL公式FAQでも Linux のデフォルト例として X11 が挙げられています。 :contentReference[oaicite:5]{index=5}
Wayland
最近のLinuxデスクトップでよく使われます。
Pyxel でも 2.7.4〜2.7.7 にかけて、Wayland や XWayland fallback 上でのマウス修正、Wayland native driver preference などが入っています。つまり、今はかなり現実的な選択肢ですが、古いPyxelやSDL環境では差が出やすい領域です。 :contentReference[oaicite:6]{index=6}
KMSDRM
ディスプレイサーバなしで、DRM/KMS を使って直接表示する系統です。
組み込みLinux、携帯ゲーム機、シングルボードコンピュータ系ではここが重要です。Pyxel 2.7.3 で KMSDRM video driver が追加されたので、こうした環境での実用性が以前より高くなっています。SDL公式FAQでも Linux のビデオドライバの1つとして kmsdrm が案内されています。 :contentReference[oaicite:7]{index=7}
実行時の基本例
X11で試す例
SDL_VIDEODRIVER=x11 python main.py
Waylandで試す例
SDL_VIDEODRIVER=wayland python main.py
KMSDRMで試す例
SDL_VIDEODRIVER=kmsdrm python main.py
これらの環境変数指定は、SDL公式FAQの「特定のビデオドライバを選ぶ方法」に基づくものです。Pyxel独自ではなく、SDL側の標準的な切り替え方法です。 (SDL3)
組み込み環境で特に意識したいこと
1. ウィンドウマネージャ前提ではない
組み込み環境では、X11/Waylandのようなデスクトップ前提ではなく、KMSDRMで直接出す構成が現実的です。Pyxelが 2.7.3 で KMSDRM を追加したのは、まさにそのような利用を見据えた改善だと考えられます。
2. 音声経路もPCと違うことが多い
Desktop Linux では PulseAudio / PipeWire が多いですが、組み込みでは ALSA 直叩きのほうが素直な場合があります。SDL公式FAQでも Linux のオーディオドライバとして pulseaudio / pulse / alsa / pipewire / jack などが列挙されています。 (SDL3)
3. まずは「どのドライバで通るか」を見る
組み込み環境では、最初から完璧な設定を当てにいくより、
- 画面は出るか
- 音は出るか
- 入力は取れるか
を切り分けて確認するほうが早いです。Pyxel changelog にも Linux / Wayland / KMSDRM 周辺の個別修正が多いため、1つの前提で全部解決すると考えないほうが安全です。
ここまで理解できれば十分なこと
- Linux / 組み込み環境では SDL ドライバの選択が重要
- 実際の主要候補は
x11/wayland/kmsdrm - Pyxel 2.7.3 以降は KMSDRM を意識した改善が入っている
- まずはどの経路で表示・音が通るかを切り分ける
ここまで分かれば、次は API リファレンスよりも 実際の環境変数を変えて挙動確認する段階 です。Pyxel側の最新 changelog と SDL FAQ を見ながら詰めれば十分です。
2. SDL設定を調整したい
概要
PyxelはSDL2ベースで動くため、SDLの環境変数で挙動を切り替えられます。
SDL公式FAQでは、ビデオドライバは SDL_VIDEODRIVER、音声ドライバは SDL_AUDIODRIVER で選べると明記されています。さらに、利用可能な候補として Linux では x11、wayland、kmsdrm、音声では pulseaudio、pulse、alsa、pipewire、jack などが案内されています。 (SDL3)
詳細解説
一番よく使う環境変数
ビデオドライバを固定したい
SDL_VIDEODRIVER=x11 python main.py
SDL_VIDEODRIVER=wayland python main.py
SDL_VIDEODRIVER=kmsdrm python main.py
音声ドライバを固定したい
SDL_AUDIODRIVER=pulseaudio python main.py
SDL_AUDIODRIVER=pipewire python main.py
SDL_AUDIODRIVER=alsa python main.py
これらはすべて SDL 公式FAQ に載っている使い方です。つまり、Pyxel独自の秘密設定ではなく、SDL標準の切り替え方法です。 (SDL3)
シェルで一時的に試す形が便利
Linuxでは、まず一時的に環境変数を付けて実行するのが安全です。
SDL_VIDEODRIVER=wayland SDL_AUDIODRIVER=pipewire python main.py
これなら、設定ファイルを書き換えずに挙動比較できます。
環境依存問題は「この組み合わせなら動く」を見つけることが先なので、まずは一時指定が向いています。SDL公式FAQの趣旨とも一致します。 (SDL3)
よくある実践パターン
デスクトップLinuxで画面が出ないとき
まず x11 と wayland を切り替えて試します。
SDL_VIDEODRIVER=x11 python main.py
SDL_VIDEODRIVER=wayland python main.py
Pyxel changelog では Wayland まわりの修正が 2.7.4〜2.7.7 に複数入っているため、Wayland系で問題があるなら Pyxelの版差も疑う価値があります。
組み込みLinuxで画面が出ないとき
kmsdrm を試します。
SDL_VIDEODRIVER=kmsdrm python main.py
KMSDRM は Pyxel 2.7.3 で追加されたビデオドライバなので、古いPyxelでは使えない前提で考える必要があります。
音が出ないとき
音声経路を1つずつ試します。
SDL_AUDIODRIVER=pipewire python main.py
SDL_AUDIODRIVER=pulseaudio python main.py
SDL_AUDIODRIVER=alsa python main.py
SDL FAQ によれば、Linux ではこれらが標準的な候補です。 (SDL3)
SDL側のライブラリ依存もある
SDLの Linux README では、ビルドや利用に関連するライブラリとして ALSA、PulseAudio、PipeWire、X11、Mesa GLES などが挙げられています。
つまり、単に環境変数だけでなく、そのドライバを支えるライブラリが環境にあるか も実際には効いてきます。SDL Linux README には alsa-lib-devel、pulseaudio-libs-devel、pipewire-devel、libX11-devel、mesa-libGLES-devel などの例が載っています。 (SDL3)
ここまで理解できれば十分なこと
- SDL設定調整の本体は環境変数
- まず見るべきは
SDL_VIDEODRIVERとSDL_AUDIODRIVER - 一時指定で挙動比較するのが実用的
- ドライバだけでなく関連ライブラリの有無も影響する
ここまで分かれば、次は API リファレンスよりも 実際に候補を切り替えて試す段階 です。SDL FAQ と Pyxel changelog を見ながら進めれば十分です。 (SDL3)
3. 音が出ない問題を解決したい
概要
ただし、ここは「1つの正解を当てる」より、音声経路を切り分ける のが大事です。
SDL公式FAQでは、Linux の音声ドライバ候補として pulseaudio / pulse / alsa / jack / pipewire などが案内されています。つまり、「音が出ない」という現象の中身は実際には、
- Pyxelが悪い
- SDLが悪い
- 選ばれた音声ドライバが合っていない
- 環境に該当ドライバの前提がない
のどれかであることが多いです。 (SDL3)
詳細解説
まず試すべきこと
1. 音声ドライバを明示する
SDL_AUDIODRIVER=pipewire python main.py
SDL_AUDIODRIVER=pulseaudio python main.py
SDL_AUDIODRIVER=alsa python main.py
これで音が出る組み合わせがあるか確認します。
SDL FAQ が公式に挙げている候補なので、この切り分けはかなり基本です。 (SDL3)
2. 最小のPyxel音を鳴らす
自分のアプリ全体で確認する前に、最小の Sound 再生だけでテストします。
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120)
pyxel.sounds[0].set("c3e3g3c4", "p", "7", "f", 20)
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btnp(pyxel.KEY_Z):
pyxel.play(0, 0)
def draw(self):
pyxel.cls(0)
pyxel.text(20, 40, "PRESS Z TO TEST SOUND", 7)
App()
これで鳴らないなら、BGM構成以前の問題です。
つまり「自分のサウンドデータが悪い」のか「環境が悪い」のかを分けられます。Pyxelの play() と sounds[].set() は最小テストに向いています。
Linuxデスクトップでよくある考え方
PipeWire環境
最近のLinuxデスクトップでは PipeWire が多いので、まず SDL_AUDIODRIVER=pipewire を試す価値があります。SDL FAQ にも PipeWire は Linux オーディオドライバの1つとして載っています。 (SDL3)
PulseAudio互換環境
PipeWire 上でも PulseAudio 互換層で動くことがあるため、pulseaudio / pulse が通る場合もあります。SDL FAQ でも両方が候補にあります。 (SDL3)
組み込み系
PulseAudioやPipeWireを介さず、alsa のほうが素直な場合があります。SDL FAQ でも ALSA は Linux 音声ドライバの代表候補です。 (SDL3)
SDL Linux README から見えること
SDL Linux README では、ALSA、PulseAudio、PipeWire、JACK などへの対応がビルド依存として案内されています。
つまり、環境変数だけでなく、そのドライバを使うためのライブラリが存在するかも影響します。
逆に言えば、「環境変数を切り替えても全部ダメ」なら、SDL側のビルド・同梱・リンク状況まで疑う段階です。 (SDL3)
具体的な確認順
- 最小Sound再生コードで確認
SDL_AUDIODRIVER=pipewire-
SDL_AUDIODRIVER=pulseaudioまたはpulse SDL_AUDIODRIVER=alsa- それでもダメなら SDL 側の環境やライブラリ依存を確認
この順番にすると、「アプリ設計の問題」と「環境問題」を切り分けやすいです。SDL FAQ と Linux README の情報に沿った現実的な順です。 (SDL3)
ここまで理解できれば十分なこと
- 音が出ない問題はまず音声ドライバ切り替えで切り分ける
-
pipewire/pulseaudio/alsaが主要候補 - 最小Sound再生コードでアプリ側問題と環境問題を分ける
- 全部ダメなら SDL ライブラリ依存も疑う
ここまで分かれば、あとは実環境での検証段階です。Pyxel API 以上に SDL 環境の確認が重要になります。 (SDL3)
4. 画面が出ない問題を解決したい
概要
こちらも音と同じで、まずは ビデオドライバの切り分け が最優先です。
SDL公式FAQでは Linux のビデオドライバ候補として x11、wayland、kmsdrm が案内されています。Pyxel changelog でも、
- KMSDRM 追加
- Wayland マウス修正
- Wayland native driver preference
などが複数入っているため、画面が出ない場合は表示経路と版差の両方を疑うのが自然です。 (SDL3)
詳細解説
まず試すべきこと
デスクトップLinuxなら X11 / Wayland を切り替える
SDL_VIDEODRIVER=x11 python main.py
SDL_VIDEODRIVER=wayland python main.py
Waylandまわりは Pyxel 2.7.4〜2.7.7 で複数の不具合修正が入っているので、
もし Wayland 系でおかしいなら、古いPyxelを使っていないかも確認したほうがよいです。
組み込みLinuxなら KMSDRM を試す
SDL_VIDEODRIVER=kmsdrm python main.py
KMSDRM は Pyxel 2.7.3 で追加されたので、この機能を前提にするなら版も重要です。SDL FAQ にも Linux ビデオドライバの1つとして kmsdrm が載っています。
最小描画コードで切り分ける
アプリ全体で確認する前に、最小の画面描画だけでテストします。
import pyxel
class App:
def __init__(self):
pyxel.init(160, 120, title="Display Test")
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(1)
pyxel.text(20, 40, "DISPLAY TEST", 7)
pyxel.rect(40, 60, 40, 20, 8)
App()
これで表示されないなら、タイルマップや画像ロード以前の問題です。
つまり、まずはビデオドライバかSDL環境を疑うべきだと切り分けられます。Pyxelの init() と基本描画だけで最小検証できます。
どんな現象があるか
1. まったくウィンドウが出ない
この場合は、ビデオドライバの初期化失敗やSDL経路の不一致を疑います。
まず SDL_VIDEODRIVER の切り替えです。SDL FAQ の使い方そのものです。 (SDL3)
2. ウィンドウは出るが入力や描画がおかしい
Wayland系なら Pyxelの版差を疑う価値があります。
changelog に Wayland とマウス修正が複数あるため、最新版寄りのほうが有利です。
3. 組み込み機で真っ黒
KMSDRM系の経路を試す価値があります。Pyxelが embedded Linux devices 向けに KMSDRM を追加しているため、ここは公式に想定された方向です。
画面確認の現実的な順番
- 最小描画コードで確認
SDL_VIDEODRIVER=x11SDL_VIDEODRIVER=wayland- 組み込みなら
SDL_VIDEODRIVER=kmsdrm - Pyxelの版を確認し、Wayland/KMSDRM修正を含む新しめへ寄せる
この順番なら、アプリ側の複雑さに引きずられずに基礎表示経路を切り分けられます。 (SDL3)
ここまで理解できれば十分なこと
- 画面が出ない問題は
SDL_VIDEODRIVER切り替えで見る - 主要候補は
x11/wayland/kmsdrm - 最小描画コードでアプリ側問題と環境問題を分ける
- Wayland / KMSDRM は Pyxel の版差も効きやすい
ここまで分かれば、あとは実環境での検証です。Pyxel API 以上に SDL 経路と版確認が重要になります。 (SDL3)
5. パフォーマンスを改善したい
概要
ただし、ここで言うパフォーマンス改善は「特殊な高速化オプションを1個入れれば終わり」ではありません。
現実的には、
- Pyxelの新しい版を使う
- 描画量を見直す
- 不要なリソース処理を減らす
- 環境に合ったビデオドライバを選ぶ
の組み合わせです。Pyxel changelog では 2.7.0 に drawing performance optimization があり、bulk fills や overhead 削減が入っています。つまり、まず新しめの版に寄せること自体がパフォーマンス改善になります。
詳細解説
まずやるべきこと
1. Pyxelを新しめにする
GitHubのリリース一覧では、最新は 2.9.0(2026-04-10時点) です。
Linux / Wayland / KMSDRM / drawing performance に関する改善が 2.7.x 以降も継続しているので、古い版で悩むより新しい版で確認するほうが有利です。
2. 毎フレームやることを減らす
Pyxelは毎フレーム update() と draw() が回るので、
そこに重い処理を入れすぎるとすぐ遅くなります。
典型的には次を疑います。
- 毎フレームファイル読み込み
- 毎フレーム大量の文字列組み立て
- 毎フレーム不要なJSON読込
- 毎フレーム大きな画像変換
- 毎フレーム使わない描画まで全部している
これはPyxel固有というより、ゲームループ一般の話ですが、Pyxelは run(update, draw) でこの構造が明確なので効果が大きいです。
Pyxelで実践しやすい改善ポイント
リソースは起動時に読み込む
たとえば画像や .pyxres やフォントは、原則として __init__() で読み込んで、毎フレームは再利用します。
Pyxel API自体も load() や Font(...) を初期化時に使う流れと相性がよいです。
描画範囲を絞る
たとえば長いリストや長文表示では clip() を使って表示範囲を限定すると、UIの見通しだけでなく描画管理もしやすくなります。
もちろん劇的な高速化を約束するものではありませんが、「必要な見せ方だけをする」設計は結果的に軽くなりやすいです。 clip() は描画範囲制限用APIです。
不要な文字描画を減らす
テキスト描画は便利ですが、毎フレーム大量に組み立てて描くとコストが積み上がります。
特にデバッグ表示を大量に載せたまま本番確認すると、思ったより重く感じることがあります。Pyxelは text() を毎フレーム使える一方で、当然ながら描画量はゼロコストではありません。
画面サイズとFPSを見直す
init(width, height, fps=...) の値が大きいほど、一般には描画・更新負荷も上がりやすいです。
すべての環境でそうだと断定はできませんが、少なくとも 160x120 / 30fps と 320x180 / 60fps では後者のほうが負荷要因が増えやすいと考えるのが自然です。Pyxel APIでも width / height / fps は主要初期化パラメータです。
Linux環境での実践ポイント
合わないビデオドライバは遅くもなる
単に「出る / 出ない」だけでなく、表示経路が合っていないと動いても重いことがあります。
そのため、SDL_VIDEODRIVER=x11、wayland、kmsdrm を比較する価値があります。SDL公式FAQの候補一覧そのものが、この切り分けの前提です。 (SDL3)
SDL関連ライブラリ構成も効く
SDL Linux README では X11、GLES、ALSA、PulseAudio、PipeWire などの依存候補が案内されています。
つまり、環境によっては「動くが遅い」「動くが不安定」という形で差が出ることがあります。 (SDL3)
最低限のチェックリスト
- Pyxel は新しめの版か
- 毎フレームファイルI/Oしていないか
- 使わない描画を毎フレームしていないか
- 画面サイズやFPSが過剰でないか
- SDLドライバを比較したか
ここまで理解できれば十分なこと
- まず新しいPyxel版を使う
- 毎フレーム処理を見直す
- リソースは初期化時読み込みに寄せる
- Linuxでは SDL ドライバ比較も重要
ここまで分かれば、あとは自分のアプリのどこが重いかを具体的に見ていく段階です。Pyxel側の changelog と SDL の環境差を押さえていれば十分です。
まとめ
Pyxelの環境・応用まわりは、Pyxel本体の問題 と SDL / Linux環境の問題 を分けて考えると整理しやすいです。
Pyxelの changelog には Linux 向けの KMSDRM 追加、Wayland修正、描画性能改善が継続して載っており、SDL公式FAQではビデオドライバと音声ドライバの選び方が明示されています。つまり、トラブルシュートの基本は ドライバ切り替えによる切り分け です。
整理するとこうです。
-
Linuxや組み込み環境で動かす
x11/wayland/kmsdrmの前提を分けて考える -
SDL設定を調整する
SDL_VIDEODRIVERとSDL_AUDIODRIVERを使う -
音が出ない問題を切り分ける
pipewire/pulseaudio/alsaを順に試す -
画面が出ない問題を切り分ける
x11/wayland/kmsdrmを順に試す -
パフォーマンスを改善する
新しめのPyxelを使い、毎フレーム処理とドライバ選択を見直す
ここまで理解できたら、次に見るべきは Pyxel の changelog と SDL の FAQ / Linux README、それに自分の環境での実行結果です。そこから先は「Pyxelで何ができるか」を知る段階ではなく、「自分の環境でどの経路が最適か」を詰める段階に入ります。