PICO-8ユーザーによる、Pyxelの開発環境と周辺ツールの話

## Python初心者、PICO-8ユーザーです。
趣味として、ファンタジーゲームコンソール「[PICO-8](https://www.lexaloffle.com/pico-8.php)」で[ゲームを作っています](https://www.lexaloffle.com/bbs/?uid=40142)。

https://www.lexaloffle.com/pico-8.php

<img src="https://www.lexaloffle.com/media/40142/cybersanta2023_2.gif" alt="CyberSanta2023" width="40%"> <img src="https://www.lexaloffle.com/media/40142/space_lancers_0.gif" alt="Space Lancers" width="40%"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29093/9541ed7b-8dd4-adb0-ea9d-cea92be288bb.gif" width="40%"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29093/24c8e979-c6fd-35e1-abc5-3964dd50f205.gif" width="40%">

2024年の夏頃、Pythonをやってみようと思い立ち、[Microsoftの「Introduction to Python」](https://vscodeedu.com/courses/intro-to-python)を教材に、ChatGPT先生の補助を受けながら基本的な学習を始めました。
理解を深めるには何かを作ってみるのが一番なので、ある程度学習が進んだところで、趣味（ゲーム制作）と実益（実装経験）を兼ねて、Pythonでプログラムを書き、かつPICO-8の経験が活かせそうなPyxelにも挑戦してみることにしました。

執筆時点では、PICO-8で作ったものをPyxelに移植しながら理解を進めて、とりあえず動くゲームを一つ作ってみたという程度の経験です。

https://github.com/kadoyan/python_Playground/

ゲームはこちら

https://kadoyan.github.io/sarananda/sarananda.html

BGMは[@frenchbread](https://x.com/frenchbread1222)さんがご厚意で提供してくださいました。
ありがとうございます。

![SARANANDA 沙羅何蛇](https://private-user-images.githubusercontent.com/678802/371818176-de0c0fa7-b4a9-4eec-8c3f-d758e4cdad10.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMyODQ1ODQsIm5iZiI6MTczMzI4NDI4NCwicGF0aCI6Ii82Nzg4MDIvMzcxODE4MTc2LWRlMGMwZmE3LWI0YTktNGVlYy04YzNmLWQ3NThlNGNkYWQxMC5naWY_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQxMjA0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MTIwNFQwMzUxMjRaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1lYzY5OTRlNWJlODAwMjlkZGM4ZTI0MzY2MDBiMGU1NTM5YWJkZDRhYmQwODQ1NTIxOTFjOGE2MDFiY2JjZTY4JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.HNcyvbpiCDT0G0agiuwkHDFz72MNzhYoRjcjfEfR7_o)

ゼロからのスタートでしたが、X（旧Twitter）に[制作過程や疑問をポスト](https://x.com/search?q=from%3Akadoyan+pyxel)したところ、開発者の[@kitao](https://x.com/kitao)さんはじめ、いろいろと助言をいただきました。この場を借りてお礼申し上げます。

## Pyxelの開発環境を用意した

### PICO-8の場合
PICO-8は「ファンタジーゲームコンソール」という特徴から、開発からプログラムの実行まで、すべてPICO-8アプリケーション内で完結します。
外部ツールを使うこともできますが、コマンドライン、コーディング、スプライト描画、マップ作成、SE・音楽作成まで、一つのウィンドウ（アプリケーション）内にあり、いずれもコンパクトながら高機能で、だいぶ欲を出したゲームを作っても、およそ不足する機能はありません。
自分も、基本的にPICO-8内の機能だけでゲームを作っています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29093/ab1fb6ee-9c3f-a699-2cc3-ce49f948b20b.png" width="40%" alt="PICO-8 コード画面"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29093/0188005f-cbe3-86ed-549e-6f915f7c2061.png" width="40%" alt="PICO-8 スプライト画面"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29093/55d7cd5e-1a49-e5e5-d0a4-a0b53743f031.png" width="40%" alt="PICO-8 サウンド画面"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29093/a3adbbb3-4102-5c19-d3c6-aeb61ab9334b.png" width="40%" alt="PICO-8 音楽画面">


### Pyxelの場合
PyxelはPythonのライブラリなので、開発環境を自分で用意する必要があります。
とりあえず始めるだけであれば、Pythonの実行環境となんらかのテキストエディタがあれば問題ありません。
画像などは、Pyxel用のリソースエディタ（グラフィック、サウンド、マップ編集ができる）で用意することができます。

https://docs.anaconda.com/miniconda/

自分の場合、Pythonの基本的な学習ができればいいと考えていたので、minicondaをMacにインストールしました。
Pyxelでの開発も問題なくできています。

## エディタ: VS Code
普段から仕事でも利用していて、先にPythonの学習でも使った[Visual Studio Code](https://code.visualstudio.com/)をそのまま使っています。
Pythonの学習とPyxelでの開発にあたって追加したプラグインは次のものです。

https://marketplace.visualstudio.com/items?itemName=ms-python.python

https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy

https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring

https://marketplace.visualstudio.com/items?itemName=KevinRose.vsc-python-indent

他に何かおすすめがあれば教えてください。

## グラフィックツール: Aseprite
Pyxelには、`pyxel edit ファイル名.pyxres`で立ち上がる[リソースエディタ](https://github.com/kitao/pyxel/blob/main/docs/README.ja.md#%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E4%BD%9C%E6%88%90%E6%96%B9%E6%B3%95)があります。

簡単なものを作る分には不足がないのですが、PICO-8にくらべると解像度やリソースの上限が大きいこともあって、欲を出し始めると意外に早く限界を感じてしまいます。

https://kitao.github.io/pyxel/wasm/examples/image_editor.html

自分の場合、付属のイメージエディタが手に馴染まなかったので、早々に[Aseprite](https://www.aseprite.org/)を使うことにしました。

https://www.aseprite.org/

ドット絵描きに特化したグラフィックツールで、特にアニメーション作成に向いています。
作ったアニメーションをスプライトシートとして絵を並べて書き出す機能があり、Pyxelのコードからそれを読み込んで利用することができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29093/1e57ff1c-8a63-642f-3fda-d553681c574b.png" width="40%" alt="aseprite"> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29093/291971a9-68a7-2d9b-0e8f-db12a400fc68.png" width="40%" alt="aseprite">

## マップツール: Tiled Map Editor
上記のゲーム「沙羅何蛇」は、そもそも背景がスクロールしておらず（擬似スクロール）、タイルマップを単純に貼り付けるだけしか使っていなかったので付属のエディタで問題なかったのですが、

![pyxel-20241209-013254.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29093/6f1ee3b9-0417-8e0d-b379-ee22a1d0af2b.gif)
こういう「キャラが裏に隠れるマップ」というのを実現しようと思い、[サンプル](https://kitao.github.io/pyxel/wasm/examples/15_tiled_map_file.html)を見ながら考えてみたところ、このエディタでは結構面倒なことをしなければならないのでは、ということに気づきました。

https://kitao.github.io/pyxel/wasm/examples/15_tiled_map_file.html

PICO-8は、各スプライトに8bit分のフラグがあり、マップを描画するmap()の引数にフラグを指定すると、そのフラグのマップチップのみを描画することができます。
たとえば、フラグ0が立っているスプライトはキャラが後ろに隠れる背景チップとする、と決めておくと、キャラを描画した後にフラグ0のマップチップのみのマップを描画することで、レイヤーのような機能がなくても重なりを表現できます。

ところが、Pyxelには、レイヤーや、PICO-8のようなマップチップの属性を設定する機能はないようです。
（マップチップの種類に関しては、`pyxel.Tilemap.pget()`で、イメージバンクのどのセルの画像が使われているかは取得できます）
そのため、複雑な重なりを表現するためには、下になる部分と上になる部分を別で作って重ね合わせるような、何らかの工夫が必要になりそうでした。

https://kitao.github.io/pyxel/wasm/examples/tilemap_editor.html

そこで見つけたのが、[Tiled](https://www.mapeditor.org/)マップエディタです。

https://www.mapeditor.org/

![スクリーンショット 2024-12-09 17.26.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29093/537a722b-91f6-9721-c6cb-3c3521ce0e0a.png)

ランダム塗りつぶしや地形の作成など、タイルマップを作ることに特化したエディタです（まだ触り始めたばかりなのでよくわかってません）。
エディタとしての機能もさることながら、

- レイヤー機能があり
- 保存したファイルをそのままPyxelに読み込んで、レイヤーを利用することができる

という、先の不足を補って余りある素敵ツール。
これは今後マスターしたいアプリケーションです。

## サウンドツール: Pyxel Tracker
リソースエディタにもサウンド制作ツールはあるのですが、ノートを画面上に位置で置いていくスタイルです。

https://kitao.github.io/pyxel/wasm/examples/sound_editor.html

SEならまったく問題ないのですが、音楽を入力しようとするとどうしても手数が多くなり、時間がかかります。
自分の場合、昔、ファミリーベーシックやMSXでMMLを使って音楽の打ち込みをしていた経験があり、PICO-8もCDEFGABでの入力モードがあるため、ノートを置いていくよりも打ち込みする方が手に馴染んでいます。
（今後、MMLでの入力ができるようになるとのことなので期待します）

音楽については、[@frenchbread](https://x.com/frenchbread1222)さんが作成された、Pyxel Trackerというツールがとても使いやすいので、現時点ではこちらをお勧めします。

https://github.com/shiromofufactory/pyxel-tracker

入力はMML感覚、音色のプリセットも豊富なので、Pyxelのサウンド機能を十分に活用できると思います。
データをjsonで出力してPyxelに読み込ませれば、ゲーム内で再生することができます。

画面は次のような感じ。

https://x.com/kadoyan/status/1864305811394318411

ファミコンの音ですねぇ。すばらしい。

## スプライトとマップの実装例
https://kitao.github.io/pyxel/wasm/examples/15_tiled_map_file.html

このサンプルを見ながら、自分でもあらためて、Asepriteで描いたキャラクターとTiledで作ったタイルマップデータ、それぞれの読み込みと表示の実装にチャレンジしました。

https://kitao.github.io/pyxel/wasm/launcher/?play=kadoyan.pyxel_background_collision.background_collision&gamepad=enabled

コードはこちら

https://github.com/kadoyan/pyxel_background_collision

### 外部データの読み込み
```py:background_collision.py
class App:
    def __init__(self):
        pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, fps=60, display_scale=3, title="Maze")
        # リソースファイルを読み込むけど、イメージバンクもタイルマップも読み込まない
        pyxel.load("background_collision.pyxres", True, True)
        # マップチップをイメージバンク0に
        pyxel.images[0] = pyxel.Image.from_image(
            "assets/background.png", incl_colors=False
        )
        # キャラクターをイメージバンク1に
        pyxel.images[1] = pyxel.Image.from_image(
            "assets/characters.png", incl_colors=False
        )
        # Tiledで作ったマップデータを読み込む
        for index in range(4):
            pyxel.tilemaps[index] = pyxel.Tilemap.from_tmx("assets/test.tmx", index)
```
まず、サウンド（今回はありませんが）とカスタムパレットファイルを読み込むため、リソースファイル（.pyxres）をロードしています。ただし、オプションをTrueにすることで、イメージバンクとタイルマップはリソースファイルから読み込みません。

>load(filename, [excl_images], [excl_tilemaps], [excl_sounds], [excl_musics])
リソースファイル (.pyxres) を読み込みます。オプションにTrueを指定すると、そのリソースは読み込まれません。また、同名のパレットファイル (.pyxpal) がリソースファイルと同じ場所に存在する場合は、パレットの表示色も変更されます。
https://github.com/kitao/pyxel/blob/main/docs/README.ja.md#%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9

次に、マップチップの画像（background.png）をイメージバンク0に、キャラクターの画像（character.png）をイメージバンク1に読み込んでいます。
1枚にまとめてしまえばいいのですが、今回は絵を描く都合で分けました。
タイルマップ用のマップチップ画像は、外部からマップデータを読み込む場合、どうやらどのイメージバンクの画像を使うか指定できないようなので、イメージバンク0に読み込む必要があるようです。

最後に、Tiledで作ったファイル（.tmx）をタイルマップとして読み込みます。
Tiled上で一番下のレイヤーが`pyxel.timemaps[0]`、以降上に向かって順にインデックスされているようです。

実装を試すにあたって、Pyxelのサンプルを参考に、

1. 衝突判定用のレイヤー
1. キャラの上に乗るレイヤー
1. キャラの後ろに隠れるレイヤー（デザインの都合で最背面の上に乗せたいパーツ）
1. 最背面

という4枚を作ってみました。
2と3の間にキャラクターを描画することで重なりを表現します。
読み込みは、上記の通り下が先になるので、最背面が0、衝突判定が3です。

### マップデータを使う
```py:lib/map.pyimport pyxel
from .global_values import GlobalValues

class DrawMap:
    def __init__(self, x=0, y=0, tm=0, u=0, v=0, w=256, h=212, c=0):
        self.x = x
        self.y = y
        self.tm = tm
        self.u = u
        self.v = v
        self.w = w
        self.h = h
        self.c = c
        # 座標などを共有するためのシングルトンクラス
        self.global_values = GlobalValues()
        
    def update(self):
        # 表示座標を反映
        self.u = self.global_values.screen_position["x"]
        self.v = self.global_values.screen_position["y"]
    
    def draw(self):
        pyxel.bltm(self.x, self.y, self.tm, self.u, self.v, self.w, self.h, self.c)
```
現時点ではあまり意味はないと思いますが、マップ表示用のラッパークラスを作りました。
それを使って、

```py:background_collision.pyからマップ部分のみ抜粋
    def __init__(self):
        # 最背面
        self.background = DrawMap(0, 0, 0, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0)
        # 背面だけどデザイン上、最背面の上に乗せるもの
        self.overwrap = DrawMap(0, 0, 1, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0)
        # キャラクターの上に被さるもの
        self.foreground = DrawMap(0, 0, 2, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0)
        # 衝突判定用
        self.collision = DrawMap(0, 0, 3, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0)
    
    def update(self):
        self.background.update()
        self.foreground.update()
        self.overwrap.update()
        self.collision.update()
    
    def draw(self):
        self.background.draw()
        self.overwrap.draw()
        self.foreground.draw()
```
レイヤーごとにインスタンスを作り、衝突判定用以外のレイヤーを描画します。
読み込んだ.tmxファイルはPyxelがタイルマップとして呼び出せるようにしてくれているので、普通に`pyxel.bltm()`でマップ番号を指定すれば引き出すことができます。

外部画像の読み込みなどは、わかってしまえばシンプルな作りでした。
外部ツールを使うことで、がんばり次第で高度なアニメーションやマップが実現できそうです。

## Pyxel Trackerの実装例
上記サンプルにはないですが、Pyxel Trackerで書き出した音楽データを使う場合のサンプルです。
```python:読み込みのサンプル
class App:
    def __init__(self):
        # pyxel初期化
        pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, fps=60, display_scale=3, title="Sarananda")
        with open(f"./assets/music.json", "rt") as f:
            self.music = json.loads(f.read())
        
        # リソースファイル（.pyxres）に設定してあるSEに番号が被らないよう、番号を指定して読み込み
        for ch, sound in enumerate(self.music):
            set_channel = ch + 16
            pyxel.sounds[set_channel].set(*sound)
```
Pyxel Trackerから書き出したjsonファイル（music.json）を初期化時に読み込み、`pyxel.sounds`サウンドクラスに配置します。jsonはそのままリストに変換されているので、

>`set(notes, tones, volumes, effects, speed)`

に対して`pyxel.sounds[set_channel].set(*sound)`でアンパックするだけでOKです。
なお、上記の例は、読み込んだリソースファイルに設定済みのサウンドがある想定です。それらを上書きしないよう、インデックスをずらして（例の場合は`+ 16`）登録しています。

```python:再生のサンプル
    def update(self):
        # BGM再生
        if pyxel.play_pos(0) is None:
            for ch, sound in enumerate(self.music):
                set_channel = ch + 16
                pyxel.play(ch, set_channel, loop=True)
```
音楽が再生されていない場合`pyxel.play_pos(0) is None`、先に登録したサウンドを各チャンネルで再生します。

自分は作曲ができないので、音楽をどう確保するかが今後の課題です。
こまった…。

## 最後に
Pyxelのリソースエディタに不自由を感じて周辺ツールを導入してみました。
読み込みで少しつまずいて試行錯誤したものの、わかってしまえば実に使い勝手のいい連携プレイができました。

Pyxelは、PICO-8のある意味閉じた環境に比べると、制限があるとはいえ、使えるリソースは実質無限と言えます。
しかしそれでも、UnityやUEみたいな超リッチ環境よりははるかにキャップがある制限された世界であることは間違いありません。
その中でどこまで作り込むか、ぜひみなさんにもチャレンジしてほしいし、自分もしていきたいと思います。

ちなみに、PICO-8界には、明確な制限のある仕様を骨の髄までしゃぶり尽くした結果、あの環境でどうして動くのか理解を超えたゲームを作る「ウィザード」たちがたくさんいます。
ぜひこっちの更にミニマムな世界も覗いてみてください。

https://www.lexaloffle.com/bbs/?cat=7

楽しいゲーム開発ライフを。

https://x.com/Sora_Sakurai/status/1863500854390620500