本文へスキップ
PYTHON RETRO GAME ENGINE
自分だけのレトロゲームを作ろう
「Pyxel」入門ガイド_
ドット絵と8bitサウンドで動く、小さくて自由なゲーム作りの世界へ。インストールから公開まで、ステージを1つずつクリアする感覚で読み進められます。コピペで動く部品集12本つき。
- Pyxel 2.9.9 対応
- 動くコード 21本
- 旧API→新API 対応表
- AI共同開発テンプレート

Pyxel(ピクセル)は、プログラミング言語 Python で動く、無料のレトロゲームエンジンです。この記事は「読み終えたら自分のゲームが1本動いている」ことをゴールに、インストール、画面と色、入力、ドット絵、音、タイルマップ、当たり判定、そしてブラウザで遊べる形での公開までを順番に案内します。
今回の改訂では、現行のPyxel 2系の書き方にすべて書き直しました。ネット上の古いサンプルやAIチャットの回答には、今のPyxelではエラーになる書き方(caption= や colkey=-1 など)がよく混ざっています。どこが変わったのかも対応表にまとめたので、古い記事で動かなかった人の答え合わせにも使ってください。
Pyxelってなに?
Pyxelは、日本人エンジニアのkitaoさんが個人で開発を続けている、オープンソースのゲームエンジンです。設計図にあたるコードはGitHubで公開されていて、2026年10月時点でスターは1.7万を超えています。Python向けのゲームライブラリとしては世界的に見てもかなり人気のある部類です。
いちばんの特徴は、昔のゲーム機のような「制限」をわざと持たせていることです。使える色は16色、同時に鳴らせる音は4つ。できることが絞られているぶん、覚える命令が少なく、絵が苦手でもそれらしい見た目にまとまります。
画面が四角いドット(マス目)でできていて、色の数も少ない、昔のファミコンやゲームボーイのようなゲームのことです。今のなめらかなゲームとは違う、あたたかい雰囲気があります。
| 項目 | Pyxelの仕様 | ひとことで言うと |
|---|---|---|
| 色 | 16色のパレット | 色えらびで迷わない |
| 絵の置き場 | イメージバンク3枚(各256×256ドット) | キャラや背景をまとめて保管 |
| マップ | タイルマップ8枚(各256×256タイル、1タイル8×8ドット) | 広いステージも作れる |
| 音 | 同時再生4チャンネル、サウンド64個、ミュージック8曲 | BGM3音+効果音1音が定番 |
| 入力 | キーボード・マウス・ゲームパッド | スマホではバーチャルパッド |
| 動く場所 | Windows・macOS・Linux・Webブラウザ | 作ったものをURLで配れる |
| ライセンス | MITライセンス | 無料。商用利用もOK |
中身(エンジンの心臓部)は高速なRust言語で書かれていて、私たちはそれをPythonから呼び出すだけです。「書くのはやさしいPython、動くのは速いRust」という分業になっているので、初心者向けのわりに動作は軽快です。
Pyxelが選ばれる理由と、向かないこと
① 命令が少なく、名前が短い
「画面を消す」はcls、「四角を描く」はrect、「文字を出す」はtext。ほとんどの命令が1行で完結し、引数の並びも「位置→大きさ→色」でそろっています。この記事の最後にあるチートシートに収まる量を覚えれば、ひととおりのゲームが作れます。
② 制限があるから、完成までたどり着ける
ゲーム作りで挫折する一番の原因は「やれることが多すぎて終わらない」ことです。Pyxelは色も音も画面も小さいので、凝りようがありません。その結果、週末だけで1本完成させるという体験がしやすくなります。完成させた経験は、次の1本を作る一番の燃料になります。
③ 絵も音も、1つのコマンドで作れる
ドット絵・マップ・効果音・BGMを作る専用エディタが最初から付いてきます。別のお絵かきソフトや作曲ソフトを探して、書き出し形式で悩む必要がありません(詳しくはSTAGE 09)。
④ ブラウザで動くので、人に見せやすい
作ったゲームはHTMLファイル1つに変換でき、パソコンにもスマホにもPythonを入れてもらわずに遊んでもらえます。「家族や友だちにURLを送るだけ」で済むのは、続けるうえで想像以上に効きます(STAGE 15)。
⑤ お手本が最初から手元にある
インストールすると、ジャンプゲームやシューティング、アクションゲームなど約20本の公式サンプルも一緒に入ります。pyxel copy_examples と打つだけで手元にコピーでき、そのまま改造の材料にできます。
正直に書く「向かないこと」
- 高解像度のイラストや写真を使うゲーム:16色のドット絵が前提です。フルカラーの絵を見せたいならPygameやGodotが向いています
- 3Dゲーム:基本は2D専用です
- 日本語の長い文章を読ませるゲーム:標準フォントは英数字向けです。日本語は別途フォントファイルを読み込めば表示できますが、ひと手間かかります
- 物理演算やUI部品が欲しい場合:ボタンやスクロールバーのような部品はないので、自分で描いて作ります
逆に言えば、「小さなゲームを自分の手で最後まで作ってみたい」という目的には、これ以上ないほど合っています。
同じPythonゲーム制作の実例Arduino×Pygameのジャンプゲーム開発は、生成AIを使うとどこまで速くなるのか。
はじめよう:インストール

Pyxelは、PythonのパッケージとしてPyPIで配布されています。まだPythonを入れていない場合はPython公式サイトから先にインストールしましょう。Pyxel 2.9.9はPython 3.11以上が必要なので、古いPythonが入っている人は新しくしてください。Windowsではインストーラの最初の画面にある「Add python.exe to PATH」に必ずチェックを入れてください。ここを飛ばすと、あとで「pipが見つかりません」というエラーで止まります。
| OS | コマンド | メモ |
|---|---|---|
| Windows | pip install -U pyxel |
コマンドプロンプトかPowerShellで実行 |
| macOS | pip3 install -U pyxel |
ターミナルで実行 |
| Linux | pip3 install -U pyxel |
環境によってはSDL2関連のライブラリが別途必要 |
入ったかどうかは、次の2つで確かめます。バージョン番号と使い方の一覧が表示されれば成功です。
python -c "import pyxel; print(pyxel.VERSION)"
pyxel
続けて、公式サンプルを手元にコピーしておきましょう。今いるフォルダに pyxel_examples というフォルダができ、中の .py ファイルはすべてそのまま動かせます。
pyxel copy_examples
cd pyxel_examples
pyxel run 02_jump_game.py
コードを書くエディタに迷ったら、無料のVisual Studio Codeがおすすめです。Python拡張機能を入れると、pyxel. と打った時点で命令の候補と説明が出るようになります。Pyxelは型情報を同梱しているので、この補完がかなり正確です。venvなどの仮想環境を使う人は、環境を有効にしてからインストールしてください(venvの公式ガイド)。
最初の一歩:最小プログラムと実行コマンド

まずは「画面を開いて、文字をひとつ表示するだけ」の、いちばん小さなプログラムです。hello_pyxel.py という名前で保存してください。
import pyxel
def update():
# 毎フレーム呼ばれる「考える」場所(今回は何もしない)
pass
def draw():
# 毎フレーム呼ばれる「描く」場所
pyxel.cls(0) # 画面を色0(黒)で消す
pyxel.text(54, 56, "Hello, Pyxel!", 7) # (x, y, 文字, 色)
pyxel.init(160, 120, title="Hello Pyxel") # 幅, 高さ, タイトル
pyxel.run(update, draw)
pyxel.initで画面を用意し、pyxel.run(update, draw)を呼ぶと、そこから先はupdate → drawの順番が、ゲームを閉じるまで自動でくり返されます。終了は Esc キーです。実行のしかたは3通り
| コマンド | 動き | 使いどころ |
|---|---|---|
python hello_pyxel.py |
ふつうにPythonとして実行 | いつでも使える基本形 |
pyxel run hello_pyxel.py |
Pyxelのコマンド経由で実行 | pythonコマンドの名前がOSで違っても同じ書き方で済む |
pyxel watch . hello_pyxel.py |
フォルダを見張り、ファイルを保存するたびに自動で再実行 | 改造しながら試すときの本命 |
特に pyxel watch は覚えておく価値があります。数字を1つ変えて保存すると、その瞬間にゲームが立ち上がり直すので、「色を変えたらどう見えるか」「速さを変えたらどう感じるか」を数秒単位で試せます。ゲーム作りは調整の回数がそのまま面白さになるので、この往復の速さは大事です。
title="..." で指定します。以前は caption="..." という名前でしたが、今のPyxelでこれを書くと TypeError: init() got an unexpected keyword argument 'caption' で止まります。この記事の旧版にもこの書き方が残っていました。古いサンプルが動かない原因の第1位です。initで決められること
title……ウィンドウのタイトル(省略すると “Pyxel”)fps……1秒間に何回 update / draw を回すか(省略すると30)quit_key……終了キー(省略するとEsc)display_scale……画面を何倍に拡大して表示するか(省略すると自動)
画面・座標・16色のひみつ

画面の大きさ
画面サイズは pyxel.init(幅, 高さ) で自由に決められます。レトロな雰囲気にしたいなら160×120あたりが扱いやすく、公式サンプルもこの前後のサイズが中心です。ドットが小さくても、ウィンドウは自動で拡大表示されるので見づらくはなりません。
大きな画面も指定できますが、広くするほど描く量が増え、ドット絵1枚あたりの存在感は薄くなります。最初の1本は、参考までに次の数字から選ぶと迷いません。
| サイズ | 雰囲気 | 向いているゲーム |
|---|---|---|
| 128×128 | 正方形。かなりレトロ | パズル、ミニゲーム |
| 160×120 | 横長。定番 | よけゲーム、シューティング、アクション |
| 256×192前後 | 情報量を増やせる | RPG風、文字が多いゲーム |
座標:左上が(0, 0)、yは下にいくほど大きい
算数のグラフと違い、yは下向きがプラスです。「上に動かす」は y -= 1。ここを逆に覚えていると、ジャンプしたつもりのキャラが地面にもぐっていきます。初心者がほぼ全員1回はやるミスなので、動きがおかしいときは真っ先に疑ってください。
もうひとつ、絵や四角の位置は「左上の角」で指定します(円だけは中心)。8×8のキャラを画面の右端ぴったりに置くなら、xは 160 - 8 = 152 です。
使える16色
色は名前ではなく0〜15の番号(カラーID)で指定します。pyxel.COLOR_RED のような定数も用意されているので、番号を覚えたくない人はそちらを使えます。
pyxel.DEFAULT_COLORS から取り出した実際の値です。1つのキャラに使う色は3色までにすると、まとまって見えます。「ベースの色」「影の色」「目立たせる色」の3つです。背景には1(紺)や5(濃い青)のような暗い色、キャラには10(黄)や11(黄緑)のような明るい色を選ぶと、動いているものがはっきり見えます。
パレットの色そのものを入れかえることもできます。たとえば pyxel.colors[1] = 0x102040 と書くと、1番の色だけが別の色になります。夕方のステージだけ全体を赤っぽくする、といった演出に使えます。
ゲームの心臓部:update と draw
Pyxelのゲームは、次の2つの関数を1秒間に30回くり返すことで動いています。
- update() — キー入力のチェックや位置の計算など、「頭で考える」部分
- draw() — 考えた結果を画面に映す「見た目」の部分
パラパラ漫画を思い浮かべてください。updateで「次のコマでは、キャラは2ドット右にいる」と決め、drawでそのコマを1枚描く。これを1秒に30枚めくると、動いて見えます。この仕組みはゲームループと呼ばれ、ほぼすべてのゲームエンジンに共通する考え方です。
守ると楽になる3つの約束
- drawの最初で画面を消す:
pyxel.cls(色)を忘れると、前のコマの絵が残って残像だらけになります - drawの中で数値を変えない:位置やスコアを変えるのはupdateだけ。こう決めておくと、バグを探す場所が半分になります
- 時間は「フレーム数」で数える:
pyxel.frame_countは起動からのコマ数です。fpsが30なら、30増えるごとに1秒。frame_count % 30 == 0で「1秒に1回」が作れます
場面(シーン)の切り替え
タイトル画面、プレイ中、ゲームオーバー。ゲームらしく見せるには、この3つの場面を切り替える必要があります。やり方は単純で、「今どの場面か」を覚えておく変数を1つ作り、updateとdrawの中で分岐するだけです。
TITLE, PLAY, OVER = 0, 1, 2
scene = TITLE
def update():
global scene
if scene == TITLE:
if pyxel.btnp(pyxel.KEY_SPACE):
scene = PLAY # スペースで開始
elif scene == PLAY:
... # ゲーム本体の処理
elif scene == OVER:
if pyxel.btnp(pyxel.KEY_SPACE):
scene = TITLE # タイトルに戻る
この形を先に作っておくと、「とりあえず遊べる」状態にいつでも戻れるので、作りかけで放置しにくくなります。完成形はSTAGE 12のよけゲームで確認できます。
関数で書くか、クラスで書くか
短いうちは、この記事の多くのサンプルのように関数と global で十分です。変数が10個を超えてきたら、class App: にまとめて self.x のように持たせると、global を書き並べる必要がなくなって見通しがよくなります。どちらで書いてもPyxelの動きは同じです。
操作をつくる:キー・マウス・ゲームパッド

入力を調べる命令は3つだけです。違いは「いつTrueになるか」です。
| 命令 | Trueになるタイミング | 向いている操作 |
|---|---|---|
pyxel.btn(key) |
押している間ずっと | 歩く、走る、ためる |
pyxel.btnp(key) |
押した瞬間の1フレームだけ | ジャンプ、決定、弾を1発 |
pyxel.btnr(key) |
離した瞬間の1フレームだけ | ため撃ちの発射、ドラッグの終わり |
次のコードは、矢印キー(またはゲームパッドの十字キー)で四角を動かし、スペースで色を変えます。
import pyxel
pyxel.init(160, 120, title="Move")
x, y = 76, 56
color = 11
def update():
global x, y, color
# 押している間ずっと反応(移動向き)
if pyxel.btn(pyxel.KEY_LEFT) or pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_LEFT):
x -= 2
if pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_RIGHT):
x += 2
if pyxel.btn(pyxel.KEY_UP) or pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_UP):
y -= 2
if pyxel.btn(pyxel.KEY_DOWN) or pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_DOWN):
y += 2
# 押した瞬間だけ反応(決定・ジャンプ向き)
if pyxel.btnp(pyxel.KEY_SPACE) or pyxel.btnp(pyxel.GAMEPAD1_BUTTON_A):
color = pyxel.rndi(8, 15)
# 画面の外に出ないようにする
x = max(0, min(x, pyxel.width - 8))
y = max(0, min(y, pyxel.height - 8))
def draw():
pyxel.cls(0)
pyxel.rect(x, y, 8, 8, color)
pyxel.text(4, 4, "ARROWS: MOVE SPACE: COLOR", 13)
pyxel.run(update, draw)
押しっぱなしで連射:btnpのholdとrepeat
pyxel.btnp(key, hold=10, repeat=5) と書くと、「押した瞬間に1回、10フレーム押し続けたらその後は5フレームごとに1回」Trueになります。シューティングの連射や、メニューのカーソル送りがこれ1行で作れます。
マウス
pyxel.mouse_x/pyxel.mouse_y……カーソルの位置(ゲーム画面の座標)pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT)……左クリックした瞬間pyxel.mouse(True)……カーソルを表示する。初期状態では非表示なので、マウスを使うゲームでは最初に呼びます
ゲームパッドとスマホ対応
ゲームパッドは pyxel.GAMEPAD1_BUTTON_A や pyxel.GAMEPAD1_BUTTON_DPAD_LEFT のように、キーと同じ感覚で調べられます。上のサンプルのように or でキーボードと両方に対応させておくのがおすすめです。というのも、ブラウザ版をスマホで開いたときに表示されるバーチャルパッドは、このゲームパッド入力として届くからです。最初から両対応で書いておけば、あとでスマホ対応に悩まずに済みます。
図形とドット絵を描いて動かす

まずは図形だけで描く
絵を用意しなくても、図形の命令だけでゲームは作れます。試作の段階では四角や丸で十分です。面白さが確かめられてからドット絵に差しかえるほうが、遠回りに見えて早く完成します。
import pyxel
pyxel.init(160, 120, title="Shapes")
def update():
pass
def draw():
pyxel.cls(1)
pyxel.pset(10, 10, 7) # 点
pyxel.line(20, 10, 60, 30, 10) # 線
pyxel.rect(10, 40, 30, 20, 8) # 塗りつぶし四角
pyxel.rectb(50, 40, 30, 20, 9) # 枠だけの四角
pyxel.circ(110, 25, 12, 11) # 塗りつぶし円
pyxel.circb(140, 25, 12, 12) # 枠だけの円
pyxel.elli(95, 45, 50, 18, 14) # だ円
pyxel.tri(20, 110, 40, 75, 60, 110, 3) # 三角形
pyxel.text(75, 95, "x-> y is DOWN", 7) # 文字
pyxel.run(update, draw)
名前の最後に b が付くもの(rectb、circb)は「border=枠線だけ」という意味です。
イメージバンクという「絵の保管庫」
Pyxelには、絵をしまっておくイメージバンクが3枚あります(0〜2番、それぞれ256×256ドット)。1枚の大きな方眼紙に、キャラも敵もアイテムも並べて描いておき、使うときに必要な部分だけ切り取って画面に貼る、という使い方をします。
絵を入れる方法は3つあります。
- Pyxel Editorで描く(STAGE 09)……本格的に作るならこれ
- コードの中に文字で書く……下のサンプルの方法。ファイルが1つで済むので、人に渡したりAIに見せたりするときに便利
- 画像ファイルを読み込む……
pyxel.images[0].load(0, 0, "chara.png")。色は自動的に16色のうち近いものに置きかえられます
絵を画面に貼る:blt
import pyxel
pyxel.init(160, 120, title="Sprite")
# イメージバンク0の (0,0) に、8x8のドット絵を2コマぶん書き込む
# 1文字が1ドット。0〜9とa〜fがカラーID(0は透明として使う)
pyxel.images[0].set(0, 0, [
"00aaaa00",
"0aaaaaa0",
"aa7aa7aa",
"aa0aa0aa",
"aaaaaaaa",
"0a8888a0",
"00a00a00",
"0aa00aa0",
])
pyxel.images[0].set(8, 0, [
"00aaaa00",
"0aaaaaa0",
"aa7aa7aa",
"aa0aa0aa",
"aaaaaaaa",
"0a8888a0",
"0a0000a0",
"aa0000aa",
])
x = 76
direction = 1 # 1なら右向き、-1なら左向き
def update():
global x, direction
if pyxel.btn(pyxel.KEY_LEFT):
x -= 1
direction = -1
if pyxel.btn(pyxel.KEY_RIGHT):
x += 1
direction = 1
def draw():
pyxel.cls(5)
# 8フレームごとにコマを切り替える(0→8→0→8…)
u = (pyxel.frame_count // 8 % 2) * 8
# 幅をマイナスにすると左右反転。最後の0は「色0を透明に」
pyxel.blt(x, 56, 0, u, 0, 8 * direction, 8, 0)
# 回転と拡大もできる
pyxel.blt(120, 30, 0, 0, 0, 8, 8, 0, rotate=pyxel.frame_count * 4, scale=3)
pyxel.run(update, draw)
pyxel.blt(x, y, img, u, v, w, h, colkey) の引数は次のとおりです。
x, y……画面のどこに貼るか(左上の角)img……イメージバンクの番号(0〜2)u, v……イメージバンクのどこから切り取るかw, h……切り取る幅と高さ。マイナスにすると反転(wなら左右、hなら上下)colkey……透明にする色の番号。透明にしないなら書かずに省略
colkey に -1 を入れる」と書かれた記事がありますが、今のPyxelでは OverflowError になります。いらないときは引数ごと省略してください。また、イメージバンクは pyxel.images[0] と書くのが現行の書き方です。アニメーションの考え方
アニメーションは「何コマ目の絵を出すか」を時間で切り替えるだけです。サンプルの pyxel.frame_count // 8 % 2 は、「8フレームごとに0と1を行ったり来たりする数」を作っています。これに絵の幅(8)を掛ければ、切り取り位置 u が0→8→0→8と切り替わります。コマを4枚に増やしたいなら % 4 にするだけです。
音を鳴らす
音が1つ鳴るだけで、四角が動いているだけの画面が急に「ゲーム」になります。後回しにされがちですが、効果音は早めに入れるのがおすすめです。
Pyxelの音は3段がまえです。
- サウンド(0〜63番)……短いフレーズ。効果音や、曲の1パートぶん
- ミュージック(0〜7番)……サウンドを各チャンネルに並べて1曲にしたもの
- チャンネル(0〜3番)……同時に鳴らせる4本のレーン。同じチャンネルで新しい音を鳴らすと、前の音は止まります
import pyxel
pyxel.init(160, 120, title="Sound")
# sounds[番号].set(音階, 音色, 音量, エフェクト, 速さ)
pyxel.sounds[0].set("c3e3g3c4", "p", "6", "n", 8) # 決定音
pyxel.sounds[1].set("c3c2c1", "n", "742", "f", 6) # 爆発音
pyxel.sounds[2].set("e2g2c3e3 g2c3e3g3", "t", "5", "v", 20) # メロディ
pyxel.sounds[3].set("c1r c1r g0r g0r", "s", "4", "f", 20) # ベース
# musics[番号].set(チャンネル0の曲順, チャンネル1の曲順, ...)
pyxel.musics[0].set([2], [3])
pyxel.playm(0, loop=True) # BGMをループ再生
def update():
if pyxel.btnp(pyxel.KEY_Z):
pyxel.play(3, 0) # チャンネル3で決定音
if pyxel.btnp(pyxel.KEY_X):
pyxel.play(3, 1) # チャンネル3で爆発音
if pyxel.btnp(pyxel.KEY_S):
pyxel.stop() # すべて止める
def draw():
pyxel.cls(1)
pyxel.text(30, 50, "Z: COIN X: BOMB S: STOP", 7)
pyxel.run(update, draw)
setに渡す文字の読み方
| 引数 | 書き方 | 例 |
|---|---|---|
| 音階 | ドレミファソラシ=c d e f g a b、後ろにオクターブ0〜4。半音上げは#、下げは-、休みはr |
"c2e2g2 r" |
| 音色 | t=三角波(やわらかい)、s=矩形波(ピコピコ)、p=パルス波(するどい)、n=ノイズ(ザー) |
"p" |
| 音量 | 0〜7。数字を並べると1音ずつ変えられる | "7642" |
| エフェクト | n=なし、s=スライド、v=ビブラート、f=フェードアウト、h / q=短めのフェードアウト |
"f" |
| 速さ | 数字が小さいほど速い(120で1音=1秒) | 8 |
音色・音量・エフェクトが音階より短いときは、最初からくり返して使われます。"p" と1文字だけ書けば全部パルス波、という意味です。
うれしい音は音階を上げていく("c3e3g3c4")。残念な音は下げていく("c3g2e2c2")。爆発や衝突は音色を n(ノイズ)にして音量を下げていく。この3つの型だけで、たいていの効果音は作れます。
チャンネルの割りふり
「BGMにチャンネル0〜2、効果音にチャンネル3」と決めておくのが定番です。効果音をBGMと同じチャンネルで鳴らすと、その瞬間にメロディが途切れてしまいます。
なお、pyxel.play(0, "T120 L8 CDEFG") のように、MMLと呼ばれる楽譜用の書き方を文字列で直接渡すこともできます。作曲に慣れている人や、AIに曲を書いてもらうときはこちらが便利です。作曲が苦手な人向けの自動作曲機能はSTAGE 13の部品12で紹介します。
Pyxel Editorで創作

絵や音をコードに文字で書くのは、小さいうちは手軽ですが、凝りはじめるとつらくなります。そこで専用エディタの出番です。
pyxel edit my_game.pyxres
ファイル名を付けて起動すると、そのファイルが無ければ新しく作られ、あれば続きから編集できます。Ctrl+S(Macは Cmd+S)で保存です。
pyxel edit です。以前の pyxeleditor というコマンドは古いバージョンのものなので、見つからないと言われたらこちらを試してください。| タブ | できること | コツ |
|---|---|---|
| Image | ペン・四角・丸・塗りつぶしで、ドット絵を1マスずつ描く | 8×8か16×16のマスに合わせて描くと、あとで切り取り位置が計算しやすい |
| Tilemap | 描いた絵を8×8のタイルとして並べ、ステージを作る | 左上のタイル(0,0)は「何もない」用に空けておく |
| Sound | 鍵盤とピアノロールで効果音やメロディを作る | 再生しながら直せる。耳で確かめるのが一番早い |
| Music | 作ったサウンドをチャンネルごとに並べて、1曲に組み立てる | メロディ・伴奏・ベースの3チャンネルで十分それらしくなる |
作ったデータは .pyxres という1つのファイルにまとまります。ゲーム側では、pyxel.init のあとに1行足すだけで読み込めます。
pyxel.init(160, 120, title="My Game")
pyxel.load("my_game.pyxres") # 絵・マップ・音がまとめて読み込まれる
読み込んだあとは、これまでと同じ pyxel.blt や pyxel.play がそのまま使えます。つまり、最初はコード内の文字で絵を作り、あとからエディタ製の絵に差しかえても、ゲーム本体のコードはほとんど変わりません。
知っておくと得する録画ショートカット
ゲームの実行中、次のキーで画面を保存できます(Macは Alt の代わりに Option)。SNSやブログに載せるGIFが、追加のソフトなしで作れます。
- Alt+1 ……スクリーンショットをデスクトップに保存
- Alt+2 ……録画の開始位置をリセット
- Alt+3 ……直前の最大10秒をGIFアニメとしてデスクトップに保存
- Alt+0 ……処理の重さ(fpsや処理時間)を表示
- Alt+Enter ……フルスクリーン切り替え
マップでステージ作り
広いステージを作るときは、絵を1枚ずつ置くのではなくタイルマップを使います。8×8ドットの小さな絵(タイル)を、方眼紙のマス目に並べていくイメージです。同じレンガの絵を100回描く代わりに、「このマスはレンガ」という印を100個置くだけなので、データが小さく、直すのも簡単です。
タイルは「何列目・何行目」で指定する
タイルは、イメージバンクを8×8のマスに区切ったときの(列, 行)という2つの数字の組で表します。イメージバンクの左上のマスが (0, 0)、その右隣が (1, 0) です。
pyxel.tilemaps[0].pset(x, y, (1, 0))……マス(x, y)にタイル(1, 0)を置くpyxel.tilemaps[0].pget(x, y)……そのマスに何が置いてあるか調べるpyxel.bltm(x, y, tm, u, v, w, h, colkey)……タイルマップを画面に描く(u, v, w, h はドット単位)
カメラでスクロールさせる
画面より広いステージを見せるには pyxel.camera(x, y) を使います。これを呼ぶと、それ以降の描画がすべて「カメラがその位置にある」ものとしてずれて描かれます。キャラやマップの座標を自分で引き算する必要はありません。スコアなどの表示は、pyxel.camera() で元に戻してから描きます。
壁にぶつかる処理は、命令1つでできる
アクションゲームで一番むずかしいのが「床に立つ」「壁で止まる」処理です。今のPyxelには、これを計算してくれる collide という命令がタイルマップに用意されています。「この四角を(dx, dy)だけ動かしたい。壁はこのタイル」と伝えると、めり込まない範囲に直した移動量を返してくれます。
import pyxel
pyxel.init(160, 120, title="Tilemap")
# タイルの絵を用意する:イメージバンク0の(8,0)にレンガを描く
pyxel.images[0].set(8, 0, [
"44444444",
"49994999",
"49994999",
"44444444",
"99499949",
"99499949",
"44444444",
"22222222",
])
BRICK = (1, 0) # タイルは「イメージバンクの何マス目か」で指定する
tm = pyxel.tilemaps[0]
# 横40マスぶんのステージをコードで組み立てる
tm.rect(0, 13, 40, 2, BRICK) # 地面
tm.rect(8, 10, 4, 1, BRICK) # 浮き足場
tm.rect(16, 8, 5, 1, BRICK)
tm.rect(26, 11, 2, 2, BRICK) # 段差
px, py, vy = 16, 80, 0.0
on_ground = False
def update():
global px, py, vy, on_ground
dx = (pyxel.btn(pyxel.KEY_RIGHT) - pyxel.btn(pyxel.KEY_LEFT)) * 2
vy = min(vy + 0.5, 6) # 重力
if on_ground and pyxel.btnp(pyxel.KEY_SPACE):
vy = -6.5 # ジャンプ
dy = round(vy) # 1ドット単位で動かす
# 壁にめり込まない移動量を計算してもらう
mx, my = tm.collide(px, py, 8, 8, dx, dy, [BRICK])
on_ground = dy > 0 and my < dy # 下向きの動きが止められた=着地
if my != dy:
vy = 1 # 床や天井に当たったら速度をリセット
px = max(0, min(px + int(mx), 40 * 8 - 8))
py += int(my)
def draw():
pyxel.cls(6)
cam_x = max(0, min(px - 76, 40 * 8 - pyxel.width))
pyxel.camera(cam_x, 0) # カメラをプレイヤーに追従
pyxel.bltm(0, 0, 0, 0, 0, 40 * 8, 120, 0)
pyxel.rect(px, py, 8, 8, 8)
pyxel.camera() # UIはカメラの影響を受けない位置に
pyxel.text(4, 4, "ARROWS + SPACE", 1)
pyxel.run(update, draw)
60行ほどで、ジャンプして足場に乗れる横スクロールアクションの土台になっています。ステージの形は tm.rect(...) の行を足したり変えたりするだけで作りかえられます。
collide に渡す移動量は整数にしてから渡すのが安全です。0.5のような1ドット未満の移動はそのまま通ってしまい、床に半ドット沈んで「着地したり浮いたり」をくり返す原因になりました。サンプルで round(vy) としているのはそのためです。ぶつかったを判定する
壁との衝突は前のステージの collide に任せられますが、「敵に当たった」「アイテムを取った」のようなキャラどうしの当たり判定は自分で書きます。とはいえ、使う形は2つだけです。
def hit_rect(x1, y1, w1, h1, x2, y2, w2, h2):
"""四角どうしが重なっていたら True"""
return x1 < x2 + w2 and x2 < x1 + w1 and y1 < y2 + h2 and y2 < y1 + h1
def hit_circle(x1, y1, r1, x2, y2, r2):
"""円どうしが重なっていたら True(ルートを使わず二乗で比べる)"""
dx, dy = x1 - x2, y1 - y2
return dx * dx + dy * dy < (r1 + r2) ** 2
四角の判定はなぜこれで合っているのか
「重なっている」を直接考えるとややこしいので、逆から考えます。2つの四角が重なっていないのは、「相手が完全に右にある」「完全に左にある」「完全に上にある」「完全に下にある」のどれかのときだけです。この4つがどれも当てはまらなければ、重なっています。hit_rect の4つの条件は、それをそのまま式にしたものです。
どちらを使うか
- 四角:キャラ、ブロック、弾など、ほとんどの場面はこれで足ります
- 円:丸い弾や、「半径いくつ以内に入ったら反応する」ような範囲の判定に向いています
8×8のキャラなら、当たり判定は6×6くらいに小さくするのがコツです。角がかすっただけでやられると、遊ぶ人は「今のは当たってない!」と感じます。逆に、アイテムを取る判定は見た目より少し大きくすると、気持ちよく取れます。プレイヤーに不利な判定は小さく、有利な判定は大きく、と覚えてください。
敵や弾が何百個にも増えると、総当たりの判定は重くなってきます。ただ、小さなレトロゲームでそこまで増えることはまれです。最初から工夫しようとせず、重くなってからSTAGE 16の方法で確かめれば十分です。
実践!よけゲームを作る
ここまでの内容をすべて使って、「左右キーで動き、上から降ってくる敵をよけ続ける」ゲームを完成させます。タイトル画面、ドット絵、効果音、だんだん上がる難易度、ハイスコアまで入って約90行です。
import pyxel
TITLE, PLAY, OVER = 0, 1, 2
class App:
def __init__(self):
pyxel.init(160, 120, title="Avoid Game")
# 自機(8x8)と敵(8x8)のドット絵
pyxel.images[0].set(0, 0, [
"000bb000", "00bbbb00", "0bb77bb0", "bbb77bbb",
"bbbbbbbb", "0b3bb3b0", "0b0000b0", "bb0000bb",
])
pyxel.images[0].set(8, 0, [
"00888800", "08888880", "88788788", "88088088",
"88888888", "08822880", "00800800", "08000080",
])
pyxel.sounds[0].set("c3e3g3c4", "p", "5", "n", 6) # スタート音
pyxel.sounds[1].set("f2c2f1c1", "n", "7642", "f", 8) # やられ音
self.scene = TITLE
self.hi_score = 0
self.reset()
pyxel.run(self.update, self.draw)
def reset(self):
self.x = 76
self.enemies = [] # [x, y, 速さ] のリスト
self.score = 0
def update(self):
if self.scene == PLAY:
self.update_play()
elif pyxel.btnp(pyxel.KEY_SPACE):
# タイトル or ゲームオーバーからスタート
self.reset()
self.scene = PLAY
pyxel.play(0, 0)
def update_play(self):
if pyxel.btn(pyxel.KEY_LEFT):
self.x -= 2
if pyxel.btn(pyxel.KEY_RIGHT):
self.x += 2
self.x = max(0, min(self.x, pyxel.width - 8))
# スコアが上がるほど敵が出やすく、速くなる
level = 1 + self.score // 300
if pyxel.rndi(0, 99) < 2 + level:
self.enemies.append([pyxel.rndi(0, pyxel.width - 8), -8,
pyxel.rndf(1.0, 1.5 + level * 0.3)])
for e in self.enemies:
e[1] += e[2]
self.enemies = [e for e in self.enemies if e[1] < pyxel.height]
# 当たり判定は見た目より少し小さく(6x6)すると理不尽さが減る
for ex, ey, _ in self.enemies:
if abs(ex - self.x) < 6 and abs(ey - 104) < 6:
self.hi_score = max(self.hi_score, self.score)
self.scene = OVER
pyxel.play(0, 1)
return
self.score += 1
def draw(self):
pyxel.cls(1)
# 流れる星(背景)
for i in range(20):
pyxel.pset(i * 37 % 160, (i * 53 + pyxel.frame_count * (1 + i % 3)) % 120, 5)
if self.scene == TITLE:
pyxel.text(52, 44, "AVOID GAME", 10)
if pyxel.frame_count // 15 % 2:
pyxel.text(44, 70, "PRESS SPACE KEY", 7)
return
pyxel.blt(self.x, 104, 0, 0, 0, 8, 8, 0)
for ex, ey, _ in self.enemies:
pyxel.blt(ex, ey, 0, 8, 0, 8, 8, 0)
pyxel.text(4, 4, f"SCORE {self.score}", 7)
pyxel.text(100, 4, f"HI {self.hi_score}", 10)
if self.scene == OVER:
pyxel.text(62, 50, "GAME OVER", 8)
pyxel.text(40, 64, "SPACE KEY TO RETRY", 7)
App()
ブロックごとに読む
- __init__:画面を作り、絵と音を登録し、最後に
pyxel.runでループ開始。runを呼んだ行から先には進まないので、必ず最後に書きます - reset:1プレイぶんの変数を初期値に戻す関数。これを分けておくと、リトライが1行で済みます
- update:プレイ中だけゲームを進め、それ以外の場面ではスペースキーを待つ。STAGE 05の場面切り替えそのままです
- update_play:移動→敵を出す→敵を動かす→画面外の敵を消す→当たり判定→スコア加算、の順。消す処理を忘れると、見えない敵が無限に増えて重くなるので注意
- draw:背景→キャラ→文字の順に描く。あとに描いたものが手前に見えます
ここから自分のゲームにする:改造の順番
写して動かしただけでは、まだ「自分のゲーム」ではありません。次の順番で1つずつ変えていくと、無理なくオリジナルになります。1つ変えるたびに必ず動かして確かめるのがコツです。
コピペですぐ試せる部品集12選
ゲームでよく使う動きを、それぞれ1ファイルで完結して動く形にまとめました。どれも20〜40行ほどです。そのまま保存して実行でき、気に入ったら自分のゲームに移植してください。すべてPyxel 2.9.9で実行を確認しています。
壁ではね返るボール
動き
速度(vx, vy)を毎フレーム足し、壁に着いたら符号を反転させる。動くもの全般の基本形です。
コードを見る(22行)
import pyxel
pyxel.init(160, 120, title="Bounce")
x, y, vx, vy = 80.0, 60.0, 1.7, 1.3
def update():
global x, y, vx, vy
x += vx
y += vy
if x < 4 or x > pyxel.width - 4:
vx = -vx # 左右の壁で反転
if y < 4 or y > pyxel.height - 4:
vy = -vy # 上下の壁で反転
def draw():
pyxel.cls(0)
pyxel.circ(x, y, 4, 10)
pyxel.run(update, draw)
改造ポイント:vx・vy を大きくすると速くなります。ブロック崩しの出発点に。
流れる星空の背景
演出
速さの違う点を流すだけで奥行きが生まれます。シューティングの背景の定番。
コードを見る(21行)
import pyxel
pyxel.init(160, 120, title="Stars")
# [x, y, 速さ] を60個。速い星ほど手前にあるように見える
stars = [[pyxel.rndi(0, 159), pyxel.rndi(0, 119), pyxel.rndf(0.3, 2.5)] for _ in range(60)]
def update():
for s in stars:
s[1] += s[2]
if s[1] > pyxel.height:
s[0], s[1] = pyxel.rndi(0, 159), 0
def draw():
pyxel.cls(0)
for sx, sy, speed in stars:
pyxel.pset(sx, sy, 7 if speed > 1.8 else 13 if speed > 1 else 5)
pyxel.run(update, draw)
改造ポイント:星の数 60 と色の分け方を変えて、雪や雨にもできます。
重力つきジャンプ
動き
「上向きの速度を与える→毎フレーム重力を足す」だけで、放物線のジャンプになります。
コードを見る(25行)
import pyxel
pyxel.init(160, 120, title="Jump")
GROUND = 96
y, vy = float(GROUND), 0.0
def update():
global y, vy
if y >= GROUND and pyxel.btnp(pyxel.KEY_SPACE):
vy = -6.0 # 上向きの速度を一気に与える
vy += 0.4 # 毎フレーム重力で下向きに加速
y += vy
if y > GROUND: # 地面で止める
y, vy = float(GROUND), 0.0
def draw():
pyxel.cls(12)
pyxel.rect(0, GROUND + 8, 160, 16, 3)
pyxel.rect(76, y, 8, 8, 8)
pyxel.text(4, 4, "SPACE: JUMP", 1)
pyxel.run(update, draw)
改造ポイント:-6.0(ジャンプ力)と 0.4(重力)の組み合わせで操作感が大きく変わります。
弾を撃つ(押しっぱなしで連射)
動き
弾はリストで管理し、画面外に出たら捨てる。btnp の hold / repeat で連射にします。
コードを見る(28行)
import pyxel
pyxel.init(160, 120, title="Shot")
x = 76
bullets = [] # [x, y]
def update():
global x, bullets
x += (pyxel.btn(pyxel.KEY_RIGHT) - pyxel.btn(pyxel.KEY_LEFT)) * 2
x = max(0, min(x, 152))
# hold=8, repeat=6:押しっぱなしで6フレームごとに連射
if pyxel.btnp(pyxel.KEY_SPACE, hold=8, repeat=6):
bullets.append([x + 3, 100])
for b in bullets:
b[1] -= 4
bullets = [b for b in bullets if b[1] > -4] # 画面外の弾は捨てる
def draw():
pyxel.cls(0)
pyxel.tri(x, 112, x + 4, 104, x + 8, 112, 11)
for bx, by in bullets:
pyxel.rect(bx, by, 2, 4, 10)
pyxel.text(4, 4, f"BULLETS {len(bullets)}", 7)
pyxel.run(update, draw)
改造ポイント:repeat=6 を小さくすると連射が速く。弾を3方向に増やすのも簡単です。
クリックで弾けるパーティクル
演出
小さな点をたくさん飛ばして、寿命が尽きたら消す。爆発やキラキラの基本です。
コードを見る(29行)
import pyxel
pyxel.init(160, 120, title="Particles")
pyxel.mouse(True) # マウスカーソルを表示
parts = [] # [x, y, vx, vy, 寿命]
def update():
global parts
if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
for _ in range(24):
parts.append([pyxel.mouse_x, pyxel.mouse_y,
pyxel.rndf(-2, 2), pyxel.rndf(-3, 1), 30])
for p in parts:
p[0] += p[2]
p[1] += p[3]
p[3] += 0.15 # 重力
p[4] -= 1
parts = [p for p in parts if p[4] > 0]
def draw():
pyxel.cls(1)
for px, py, _, _, life in parts:
pyxel.pset(px, py, 10 if life > 20 else 9 if life > 10 else 4)
pyxel.text(4, 4, "CLICK!", 7)
pyxel.run(update, draw)
改造ポイント:個数 24、重力 0.15、色の変わり方を変えて花火や煙に。
カウントダウンタイマー
しくみ
frame_count の差で経過時間を測ります。制限時間つきゲームに。
コードを見る(25行)
import pyxel
FPS = 30
pyxel.init(160, 120, title="Timer", fps=FPS)
LIMIT = 10 * FPS # 10秒ぶんのフレーム数
start = 0
def update():
global start
if pyxel.btnp(pyxel.KEY_SPACE):
start = pyxel.frame_count # 押した時点から数え直す
def draw():
left = max(0, LIMIT - (pyxel.frame_count - start))
pyxel.cls(0)
pyxel.text(60, 44, f"TIME {left / FPS:4.1f}", 7 if left else 8)
pyxel.rect(20, 60, 120 * left // LIMIT, 6, 11)
pyxel.rectb(19, 59, 122, 8, 7)
if left == 0:
pyxel.text(62, 80, "TIME UP!", 8)
pyxel.run(update, draw)
改造ポイント:10 * FPS の10を変えれば制限時間が変わります。
画面ゆれ(シェイク)
演出
camera をランダムにずらすだけ。ダメージや着地の手ごたえが一気に増します。
コードを見る(25行)
import pyxel
pyxel.init(160, 120, title="Shake")
shake = 0
def update():
global shake
if pyxel.btnp(pyxel.KEY_SPACE):
shake = 12 # 12フレームだけ揺らす
shake = max(0, shake - 1)
def draw():
# 揺れが残っている間、描画位置をランダムにずらす
if shake:
pyxel.camera(pyxel.rndi(-shake // 3, shake // 3), pyxel.rndi(-shake // 3, shake // 3))
else:
pyxel.camera()
pyxel.cls(5)
pyxel.rect(40, 40, 80, 40, 9)
pyxel.text(52, 58, "SPACE: SHAKE", 0)
pyxel.run(update, draw)
改造ポイント:shake = 12 を大きくすると長く強くゆれます。やりすぎると酔うので控えめに。
1文字ずつ出るメッセージウィンドウ
しくみ
文字列を「何文字目まで見せるか」で切り取って表示します。RPGや説明画面に。
コードを見る(29行)
import pyxel
pyxel.init(160, 120, title="Message")
MESSAGES = ["HELLO, ADVENTURER.", "THIS IS A MESSAGE WINDOW.", "PRESS SPACE TO CONTINUE."]
page, shown = 0, 0
def update():
global page, shown
text = MESSAGES[page]
if pyxel.btnp(pyxel.KEY_SPACE):
if shown < len(text):
shown = len(text) # 途中なら全文表示
else:
page, shown = (page + 1) % len(MESSAGES), 0 # 次のページへ
elif pyxel.frame_count % 2 == 0:
shown = min(shown + 1, len(text)) # 2フレームに1文字ずつ
def draw():
pyxel.cls(3)
pyxel.rect(8, 80, 144, 32, 0)
pyxel.rectb(8, 80, 144, 32, 7)
pyxel.text(14, 88, MESSAGES[page][:shown], 7)
if shown == len(MESSAGES[page]) and pyxel.frame_count // 10 % 2:
pyxel.text(142, 102, "v", 10)
pyxel.run(update, draw)
改造ポイント:標準フォントは英数字向け。日本語で出したい場合はFAQのフォント読み込みを参照。
ハイスコアをファイルに保存
しくみ
user_data_dir で保存場所を用意し、JSONで読み書きします。
コードを見る(34行)
import json
import os
import pyxel
pyxel.init(160, 120, title="Save")
# OSごとの「アプリ用データ置き場」を作って、その場所を返してくれる
SAVE = os.path.join(pyxel.user_data_dir("yofukasi", "save_demo"), "save.json")
try:
with open(SAVE) as f:
best = json.load(f)["best"]
except (OSError, ValueError, KeyError):
best = 0 # 初回やファイルが壊れているときは0から
count = 0
def update():
global count, best
if pyxel.btnp(pyxel.KEY_SPACE):
count += 1
if count > best:
best = count
with open(SAVE, "w") as f:
json.dump({"best": best}, f)
def draw():
pyxel.cls(0)
pyxel.text(40, 50, f"COUNT {count} BEST {best}", 7)
pyxel.text(40, 64, "SPACE: +1 (AUTO SAVE)", 13)
pyxel.run(update, draw)
改造ポイント:保存する中身を増やせば、ステージ進行や設定も残せます(ブラウザ版では挙動が変わります)。
プレイヤーを追いかける敵
動き
相手との差を「長さ1の向き」に直してから速さを掛けると、ななめでも同じ速さで追ってきます。
コードを見る(25行)
import pyxel
pyxel.init(160, 120, title="Chase")
pyxel.mouse(True)
ex, ey = 10.0, 10.0
SPEED = 1.2
def update():
global ex, ey
dx, dy = pyxel.mouse_x - ex, pyxel.mouse_y - ey
dist = pyxel.sqrt(dx * dx + dy * dy)
if dist > SPEED:
# 向きだけ取り出して(長さ1にして)一定の速さで近づく
ex += dx / dist * SPEED
ey += dy / dist * SPEED
def draw():
pyxel.cls(0)
pyxel.circ(ex, ey, 4, 8)
pyxel.line(ex, ey, pyxel.mouse_x, pyxel.mouse_y, 5)
pyxel.run(update, draw)
改造ポイント:SPEED をプレイヤーより少し遅くするのが、逃げられる緊張感のコツ。
ダメージの点滅とフェード
演出
pal で色を一時的に置きかえ、dither で半透明ふうに描きます。
コードを見る(28行)
import pyxel
pyxel.init(160, 120, title="Flash & Fade")
flash = 0
def update():
global flash
if pyxel.btnp(pyxel.KEY_SPACE):
flash = 10
flash = max(0, flash - 1)
def draw():
pyxel.cls(1)
if flash and flash % 4 < 2:
pyxel.pal(8, 7) # 赤(8)を白(7)に置きかえて描く=ダメージ表現
pyxel.circ(50, 60, 16, 8)
pyxel.pal() # 置きかえを元に戻す(忘れやすい)
# ディザリングで半透明ふうのフェード(0.0=透明 〜 1.0=不透明)
pyxel.dither(abs(pyxel.sin(pyxel.frame_count * 3)))
pyxel.rect(90, 44, 40, 32, 10)
pyxel.dither(1.0) # こちらも元に戻す
pyxel.text(40, 100, "SPACE: DAMAGE FLASH", 7)
pyxel.run(update, draw)
改造ポイント:pal() と dither(1.0) で元に戻すのを忘れると、画面全体の色が変わります。
自動作曲でBGMを付ける
しくみ
gen_bgm は、場面のタイプと乱数の種からBGMを自動で作る命令です。作曲が苦手でも曲が付きます。
コードを見る(31行)
import pyxel
pyxel.init(160, 120, title="Auto BGM")
NAMES = ["TITLE", "DEPARTURE", "TOWN", "PEACEFUL", "FIELD", "ADVENTURE", "BATTLE", "CRISIS"]
preset, seed = 0, 1
def start():
# gen_bgm(プリセット0-7, 移調, 楽器構成0-3, 乱数の種, play=Trueで即再生)
pyxel.gen_bgm(preset, 0, 3, seed, play=True)
def update():
global preset, seed
if pyxel.btnp(pyxel.KEY_RIGHT):
preset = (preset + 1) % 8
start()
if pyxel.btnp(pyxel.KEY_SPACE):
seed += 1 # 種を変えると別のメロディになる
start()
def draw():
pyxel.cls(2)
pyxel.text(30, 46, f"PRESET {preset}: {NAMES[preset]}", 7)
pyxel.text(30, 58, f"SEED {seed}", 10)
pyxel.text(30, 78, "RIGHT: NEXT SPACE: NEW SEED", 13)
start()
pyxel.run(update, draw)
改造ポイント:気に入った preset と seed の数字をメモしておけば、いつでも同じ曲を再生できます。
応用:センサーとつないで「体で遊ぶ」ゲームにする

ここからは、YOFUKASI LABらしい寄り道です。PyxelはふつうのPythonプログラムなので、Pythonから読めるものなら何でもゲームの入力にできます。Arduinoにつないだセンサーの値をシリアル通信で受け取れば、握る・押す・傾けるといった体の動きで遊ぶゲームが作れます。
リハビリや運動の場面では、「あと10回」より「あと1面」のほうが続くことがあります。レトロゲームの見た目は情報量が少なく、何をすればいいかが一目でわかるので、こうした用途とも相性のよい組み合わせです。
Arduino側:値を1行ずつ送るだけ
void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println(analogRead(A0)); // 0〜1023 を1行ずつ送る
delay(20); // 1秒に約50回
}
Pyxel側:届いた数値をメーターにする
次のコードは、届いた値を横棒のメーターとして表示します。Arduinoがつながっていないときは、自動的にスペースキーで代用するモードになるので、センサーがなくても動きを確かめられます。
import pyxel
try:
import serial # pip install pyserial
ser = serial.Serial("COM3", 9600, timeout=0) # Macなら "/dev/tty.usbmodem..." など
except Exception:
ser = None # つながらなければキーボードで代用
pyxel.init(160, 120, title="Sensor Meter")
value = 0 # 0〜1023 を想定(ArduinoのanalogReadの範囲)
buf = ""
def update():
global value, buf
if ser:
# 届いているぶんだけ読み、改行ごとに数値として取り出す
buf += ser.read(ser.in_waiting or 0).decode(errors="ignore")
lines = buf.split("\n")
buf = lines.pop()
for s in lines:
if s.strip().isdigit():
value = int(s.strip())
else:
# センサーなしでも試せるように、スペースキーで増減させる
value += 30 if pyxel.btn(pyxel.KEY_SPACE) else -15
value = max(0, min(value, 1023))
def draw():
pyxel.cls(0)
w = value * 140 // 1023
col = 11 if value < 600 else 10 if value < 900 else 8
pyxel.rectb(9, 49, 142, 22, 7)
pyxel.rect(10, 50, w, 20, col)
pyxel.text(10, 36, f"VALUE {value}", 7)
pyxel.text(10, 80, "SERIAL" if ser else "KEYBOARD MODE (SPACE)", 13)
pyxel.run(update, draw)
COM3 の部分)は自分の環境に合わせる ②timeout=0 にして「待たずに読む」。ここで待ってしまうと、ゲーム全体がカクカクになります ③値は1行に1つ、数字だけを送る。単位や説明の文字を混ぜると、受け取る側の処理が一気にややこしくなります。このメーターの value を、よけゲームの自機の位置や、ジャンプの強さに置きかえれば、もうセンサー操作のゲームです。なお、シリアル通信を使ったゲームはブラウザ版(STAGE 15)では動かないので、パソコン上で実行してください。こうした用途で人の体に使う場合は、あくまで遊びや動機づけの道具として、無理のない範囲で試してください。
Arduino側のコードを基礎から【2026年版】AIを用いたArduinoのコードの書き方|基礎から応用まで全てがわかる初心者の手引き
完成したら公開しよう
ゲームは、人に遊んでもらって初めて完成します。Pyxelには配布用のコマンドがそろっていて、いちばん手軽なのはHTMLファイル1つにしてしまう方法です。
手順:フォルダにまとめて、2つのコマンドを打つ
まず、ゲームのファイル(main.py と、使っていれば .pyxres)を1つのフォルダに入れます。ここでは my_game フォルダとします。
pyxel package my_game my_game/main.py
pyxel app2html my_game.pyxapp
- 1行目で、フォルダの中身が
my_game.pyxappという1つのファイルにまとまります - 2行目で、それが
my_game.htmlに変換されます - できたHTMLをブラウザで開けば、そのまま遊べます
このHTMLファイルを、itch.ioのようなゲーム投稿サイトや、GitHub Pages、自分のブログのサーバーに置けば、URLを送るだけで遊んでもらえます。スマホで開くと画面にバーチャルパッドが表示されるので、STAGE 06で触れたようにゲームパッド入力にも対応させておきましょう。
| 配り方 | コマンド | 相手に必要なもの |
|---|---|---|
| HTML(おすすめ) | pyxel app2html ゲーム.pyxapp |
ブラウザとネット接続だけ |
| Pyxelアプリ | pyxel play ゲーム.pyxapp で遊ぶ |
Pyxelのインストール |
| 実行ファイル | pyxel app2exe ゲーム.pyxapp |
なし(作ったOSと同じOS向け。別途PyInstallerを使います) |
ほかの人の作品を見たい・自分の作品を見てもらいたいときは、公式のユーザー作品集がおすすめです。世界中のユーザーが作った作品が200本以上紹介されていて、「Pyxelでここまでできるのか」という目標が見つかります。
うまく動かない時は
エラーは「どこが違うか」を教えてくれる手がかりです。赤い文字が出たら、いちばん下の1行を読んでください。そこに原因の要約が書いてあります。Pyxelでよく出会うものを表にしました。
| 症状・エラー | よくある原因 | 直し方 |
|---|---|---|
unexpected keyword argument 'caption' |
古い書き方 | title= に変える |
OverflowError(bltの行) |
colkey に -1 を渡している |
引数を省略する |
| インストールで「対応するバージョンがない」と出る | Pythonが古い | Python 3.11以上を入れなおす |
ModuleNotFoundError: No module named 'pyxel' |
別のPythonに入れてしまった/仮想環境が有効でない | python -m pip install pyxel で、実行に使うPythonへ入れる |
pyxel コマンドが見つからない |
PATHが通っていない | python -m pyxel run ファイル.py を試す |
| ウィンドウが一瞬で閉じる/真っ黒 | pyxel.run を呼んでいない、またはdrawで何も描いていない |
最後の行に pyxel.run(update, draw) があるか確認 |
| 絵が残像のように残る | drawの最初で画面を消していない | pyxel.cls(0) を先頭に |
UnboundLocalError |
関数の中で外の変数を書きかえている | 関数の先頭に global 変数名 を書く。またはクラスにまとめる |
| キャラが上下逆に動く | yの向きの勘違い | 上へは y -= 、下へは y += |
| 絵のまわりに四角い色がつく | 透明色を指定していない | bltの最後に背景に使った色の番号を渡す |
| BGMが効果音で途切れる | 同じチャンネルで鳴らしている | 効果音はチャンネル3に分ける |
| 日本語が表示されない | 標準フォントは英数字向け | ローマ字にするか、フォントファイルを読み込む(FAQ参照) |
| だんだん重くなる | 画面外に出た弾や敵を消していない | リストから取りのぞく処理を入れる |
原因をしぼり込む4つの手
- 画面に数字を出す:
pyxel.text(0, 0, f"{x} {y}", 7)をdrawに1行足して、変数の中身を見ながら遊ぶ。動いているゲームではprintより読みやすい方法です - ゆっくり動かす:
pyxel.init(..., fps=5)にすると、1コマずつ目で追えます。当たり判定のズレを見つけるのに最適です - 当たり判定を見える化する:判定に使っている四角を
pyxel.rectbで描いてみる。「思っていた場所と違った」がすぐわかります - 重さを測る:実行中に Alt+0 で、updateとdrawにかかっている時間が表示されます。勘で直す前に、どちらが重いのかを見ましょう
それでも解決しないときは、エラーメッセージの全文と該当するコードをそのままAIチャットに貼るのが早道です。貼り方のコツはSTAGE 18で紹介します。
似ているツールと比較
ゲームを作る道具は他にもたくさんあります。優劣ではなく、「何を作りたいか」で選ぶための比較です。
| ツール | 言語 | 特徴 | 向いている人 |
|---|---|---|---|
| Pyxel | Python | 16色・4音の制限つき。絵と音のエディタ付き。無料。ブラウザ出力あり | Pythonで小さなゲームを最後まで作りたい |
| Pygame | Python | 制限なしの定番。フルカラー画像や好きなフォントが使える。素材づくりは別ソフトで | 見た目や規模を自由に決めたい |
| PICO-8 | Lua | 128×128・16色の仮想ゲーム機。有料。コード量にも上限があり、制限はPyxelより厳しめ | 制限そのものを楽しみたい |
| TIC-80 | Luaなど | PICO-8に近い無料の仮想ゲーム機。240×136・16色 | 無料で仮想ゲーム機を試したい |
| LÖVE | Lua | 制限の少ない2Dゲーム向けフレームワーク | Luaで本格的に2Dを作りたい |
| Scratch | ブロック | 文字を打たずに作れる。ブラウザだけで完結 | プログラミング自体が初めての小学生 |
PyxelとPygame、迷ったら
迷ったときのおすすめは、「1本目はPyxel、作りたいものがPyxelの枠に収まらなくなったらPygame」という順番です。ゲームループ、当たり判定、場面の切り替えといった考え方はどちらも同じなので、Pyxelで身につけたことはPygameでもそのまま使えます。逆に、Pygameから始めると「画像を用意する」「フォントを選ぶ」「画面サイズを決める」といった、ゲームの面白さとは別の決めごとが最初に並び、そこで手が止まりがちです。
Scratchを卒業して文字のプログラミングに進みたい、というお子さんにも、Pyxelはちょうどよい次の一歩になります。Pygameでの実装イメージを先に見ておきたい人は、以下の記事もあわせてどうぞ。
Pygameでの表現の幅を知る【コピペ可】色(カラー)、デザインの変化を自由自在に操る|VRレンズ風デジタルアート、Python、Pygame
AIと一緒に作ろう
ChatGPTやClaudeのようなAIチャットは、Pyxelでのゲーム作りの頼れる相棒になります。Pyxelは命令が少なく、1ファイルで完結しやすいので、AIとのやり取りに向いています。ただし、Pyxel特有の落とし穴が1つあります。
落とし穴:AIは古いPyxelの書き方を混ぜてくることがある
Pyxelはバージョン1から2にかけて命令の名前がいくつか変わりました。ネット上には古い書き方のサンプルが今もたくさん残っているため、AIの回答にもそれが混ざることがあります。もらったコードがエラーになったら、まず次の表と見比べてください。
| 古い書き方(今は動かない・非推奨) | 今の書き方 |
|---|---|
pyxel.init(160, 120, caption="...") |
pyxel.init(160, 120, title="...") |
pyxel.init(..., scale=4) |
pyxel.init(..., display_scale=4) |
pyxel.image(0) |
pyxel.images[0] |
pyxel.tilemap(0) / pyxel.sound(0) / pyxel.music(0) |
pyxel.tilemaps[0] / pyxel.sounds[0] / pyxel.musics[0] |
pyxel.blt(..., colkey=-1) |
colkey を省略する |
タイルを1つの数字で指定(pset(x, y, 3)) |
(列, 行) の組で指定(pset(x, y, (3, 0))) |
pyxeleditor コマンド |
pyxel edit |
pyxelpackager コマンド |
pyxel package と pyxel app2exe / app2html |
最初に貼る「前提プロンプト」
この問題は、会話の最初に前提を伝えておくだけでかなり防げます。下の文章をコピーして、最初のメッセージとして送ってください。
これからPythonのゲームエンジン「Pyxel」でゲームを作ります。
次の条件を守ってコードを書いてください。
・Pyxel 2系(2.9)の書き方を使う
- pyxel.init の引数は title(caption は使わない)
- 画像は pyxel.images[0]、音は pyxel.sounds[0] の書き方
- blt の colkey に -1 は使わない(不要なら省略)
- タイルは (列, 行) のタプルで指定
・1つの .py ファイルで完結させ、外部の画像や音のファイルは使わない
(絵は pyxel.images[0].set、音は pyxel.sounds[0].set で作る)
・画面サイズは 160x120
・コードには日本語のコメントを付ける
・私はプログラミング初心者です。変更したときは、
どこをなぜ変えたかを短く説明してください
場面別のお願いテンプレート
「次のコードに、取ると100点入るコインが上から降ってくる機能を足してください。今ある変数名や書き方はなるべく変えず、変更した部分がわかるようにしてください。(コード全文を貼る)」
「次のコードを実行したら、このエラーが出ました。原因と直し方を教えてください。Pyxelのバージョンは2.9.9です。(エラー全文とコード全文を貼る)」
「エラーは出ませんが、ジャンプしたあと地面をすり抜けて落ちていきます。期待している動きは地面の上で止まることです。原因として考えられることを可能性の高い順に挙げてください。」
「このコードの update 関数を、小学生にもわかるように1行ずつ説明してください。そのあと、私が理解できたか確かめるクイズを3問出してください。」
「Pyxelの images[0].set で使える形式で、8×8ドットのスライムの絵を作ってください。使う色は3色まで、0番は透明として使います。」
「160×120の画面、操作は左右とボタン1つだけ、という条件で、30秒で面白さが伝わるミニゲームの案を5つ出してください。それぞれ作る難しさも教えてください。」
うまくいく進め方:小さく頼んで、毎回動かす
- 一度に頼むのは1機能だけ:「敵とアイテムとボスとセーブ機能」をまとめて頼むと、動かなかったときにどこが原因かわからなくなります
- 動いたら保存してから次へ:
game_v1.py、game_v2.pyと名前を変えて残しておくと、壊れたときにすぐ戻れます - コードは毎回全文を貼る:AIは、あなたが手元で直した内容を知りません。食い違いを防ぐ一番確実な方法です
- 「期待した動き」と「実際の動き」をセットで伝える:「動きません」だけでは、AIも人間も答えようがありません
AIはとても頼りになりますが、まちがえることもあります。もらったコードは必ず自分で動かして、思った通りかたしかめましょう。そして、数字を1つ、自分の手で変えてみることをおすすめします。速さの 2 を 5 にしたら何が起きるか。それを自分で確かめた瞬間に、そのコードは「AIが書いたもの」から「自分がわかっているもの」に変わります。
Pyxelでよく使う命令チートシート
開発中に手元で見返せるよう、よく使う命令を1つの表にまとめました。ブックマークしておくと便利です。
| 分類 | 命令 | 役割 |
|---|---|---|
| 準備 | pyxel.init(w, h, title="", fps=30) |
画面を作る |
| 準備 | pyxel.run(update, draw) |
ゲームループを開始(最後に呼ぶ) |
| 準備 | pyxel.load("file.pyxres") |
エディタで作った素材を読み込む |
| 情報 | pyxel.width / pyxel.height |
画面の幅と高さ |
| 情報 | pyxel.frame_count |
起動からのフレーム数 |
| 描画 | pyxel.cls(col) |
画面を1色で消す |
| 描画 | pyxel.pset(x, y, col) |
点を打つ |
| 描画 | pyxel.line(x1, y1, x2, y2, col) |
線を引く |
| 描画 | pyxel.rect / rectb(x, y, w, h, col) |
四角(塗り/枠) |
| 描画 | pyxel.circ / circb(x, y, r, col) |
円(塗り/枠)。x, yは中心 |
| 描画 | pyxel.elli / ellib(x, y, w, h, col) |
だ円(塗り/枠) |
| 描画 | pyxel.tri / trib(x1, y1, x2, y2, x3, y3, col) |
三角形(塗り/枠) |
| 描画 | pyxel.text(x, y, s, col) |
文字(1文字は幅4×高さ6ドット) |
| 描画 | pyxel.blt(x, y, img, u, v, w, h, colkey) |
イメージバンクの絵を貼る |
| 描画 | pyxel.bltm(x, y, tm, u, v, w, h, colkey) |
タイルマップを描く |
| 描画 | pyxel.camera(x, y) / pyxel.camera() |
描画位置をずらす/戻す |
| 描画 | pyxel.pal(c1, c2) / pyxel.pal() |
色を置きかえて描く/戻す |
| 描画 | pyxel.dither(alpha) |
半透明ふうに描く(0.0〜1.0) |
| 入力 | pyxel.btn(key) |
押している間 True |
| 入力 | pyxel.btnp(key, hold, repeat) |
押した瞬間 True(連射も可) |
| 入力 | pyxel.btnr(key) |
離した瞬間 True |
| 入力 | pyxel.mouse_x / pyxel.mouse_y |
マウスの位置 |
| 入力 | pyxel.mouse(True) |
マウスカーソルを表示 |
| 音 | pyxel.sounds[n].set(notes, tones, volumes, effects, speed) |
サウンドを作る |
| 音 | pyxel.play(ch, snd, loop=False) |
サウンドを鳴らす |
| 音 | pyxel.playm(msc, loop=False) |
ミュージックを鳴らす |
| 音 | pyxel.stop() / pyxel.stop(ch) |
全部止める/チャンネルを止める |
| 素材 | pyxel.images[n].set(x, y, [文字列...]) |
コードでドット絵を作る |
| 素材 | pyxel.tilemaps[n].pset(x, y, (tx, ty)) |
タイルを置く |
| 素材 | pyxel.tilemaps[n].collide(x, y, w, h, dx, dy, walls) |
壁にめり込まない移動量を得る |
| 数学 | pyxel.rndi(a, b) / pyxel.rndf(a, b) |
a以上b以下の乱数(整数/小数) |
| 数学 | pyxel.sin(deg) / pyxel.cos(deg) |
三角関数。角度は度(0〜360) |
| 数学 | pyxel.clamp(x, lo, hi) |
xをlo〜hiの範囲におさめる |
ここに載せていない命令や細かい仕様は、公式ユーザーガイドで確認できます。VS Codeなら、命令にマウスを乗せるだけで説明が表示されるので、そちらも活用してください。
よくある質問
Pyxelはプログラミング未経験でも使えますか?
Python自体が初めての場合は、変数・if文・for文・関数の4つを先に軽くさわっておくと理解がスムーズです。Pyxelの命令自体はとてもシンプルなので、入門を一つ終えた程度の知識があれば、この記事の内容には十分ついていけます。わからない行はAIチャットに「この行を説明して」と聞きながら進めるのもおすすめです。
作ったゲームをスマホやブラウザで遊べるようにできますか?
できます。pyxel package でアプリ化し、pyxel app2html でHTMLファイルに変換すれば、ブラウザでそのまま遊べます。スマホではバーチャルパッドが表示されるので、ゲームパッド入力にも対応させておきましょう。手順はSTAGE 15を参照してください。
PyxelとPygame、結局どちらを選べばいいですか?
手早く1本完成させたい、レトロな見た目を楽しみたいならPyxel、解像度や色数の制限なく自由に作りたいならPygameが向いています。ゲームループや当たり判定の考え方は共通なので、Pyxelで覚えたことはPygameでも活かせます。STAGE 17の比較表もあわせてどうぞ。
無料で商用利用してもよいのでしょうか?
PyxelはMITライセンスで公開されており、商用・非商用を問わず無料で利用できます。ただし、ゲームに使う絵や音楽に第三者の素材を使う場合は、その素材側のライセンスを別途確認してください。
古い記事やAIが出したコードがエラーになります。なぜですか?
Pyxelはバージョン1から2にかけて、caption→title、pyxel.image(0)→pyxel.images[0] など、いくつかの書き方が変わりました。古い書き方のままだとエラーになることがあります。STAGE 18の対応表で、該当する箇所を書きかえてみてください。
日本語の文字を表示するにはどうすればいいですか?
標準のフォントは英数字と記号向けです。日本語を表示したい場合は、日本語のフォントファイル(BDF形式など)を用意して font = pyxel.Font("ファイル名.bdf") で読み込み、pyxel.text(x, y, "こんにちは", 7, font) のように最後の引数に渡します。フォントファイルにも利用条件があるので、配布するときはライセンスを確認してください。
自分で描いた画像や、持っている音声ファイルは使えますか?
画像は pyxel.images[0].load(0, 0, "ファイル名.png") で読み込めます。色はPyxelの16色のうち近いものに自動で置きかえられるので、写真のような画像は大きく印象が変わります。音についても、サウンドにWAVやOGGファイルを読み込ませる機能があります。
画面がカクついたり重くなったりするのはなぜですか?
一番多い原因は、画面外に出た弾や敵をリストから消しておらず、見えないところで数が増え続けていることです。次に多いのが、数が多い状態での総当たりの当たり判定です。実行中に Alt+0 を押すと処理時間が表示されるので、updateとdrawのどちらが重いかをまず確かめましょう。
何歳くらいから使えますか?
キーボードで英数字が打てて、Scratchなどで「順番に実行する」「もし〜なら」「くり返す」の感覚をつかんでいれば、小学校高学年くらいから大人と一緒に楽しめます。最初はこの記事のコードの数字を変えて遊ぶところから始めるのがおすすめです。
🏆 STAGE CLEAR
Pyxelは、Pythonの知識だけで、あの頃のゲームの手ざわりを再現できる、小さくて自由なゲームエンジンです。update と draw の2つを用意すればゲームが動き出し、絵も音もマップも同じ道具の中で作れて、最後はURL1つで人に遊んでもらえます。
制限が多いことは弱点ではなく、完成までたどり着くための近道です。まずは今日のよけゲームの数字を1つ変えるところから始めてください。
おすすめの進み方
- 1日目:インストールして、STAGE 03と12のコードを動かす
- 2日目:よけゲームの数字・絵・音を変えて、自分のゲームにする
- 3日目:STAGE 13の部品を2つ移植する(画面ゆれとセーブがおすすめ)
- 1週間後:HTMLにして、誰か1人に遊んでもらう
- その次:タイルマップのサンプルを土台に、アクションゲームに挑戦する
次に挑戦したい応用アイデア
- アクションゲーム:STAGE 10の土台に、敵とゴールを足す
- シューティング:部品04の弾と、よけゲームの敵を組み合わせる
- 落ち物パズル:タイルマップを盤面として使う
- ミニRPG:タイルマップ+部品08のメッセージウィンドウ
- 体で遊ぶゲーム:STAGE 14のセンサー入力で、握力や傾きを操作にする
AIという相棒を借りつつ、自分だけのドット絵と音で、世界に1つのレトロゲームを育てていきましょう。
コメント