Pyxelに入門してみた
記事にしてみました。
なぜPyxelを選んだか
私は普段、ゲーム開発にはGodot Engineを使っているのですが、ゲームのプロトタイピング、趣味性全開な小さいプロジェクトなどで活用するために、ファンタジーコンソールを使ってみたいと考えていました[1]。
選定にあたって、個人的に外せないのは以下の要素でした。
- NES(ファミコン)くらいの解像度が欲しかった
- 開発が活発(OSSか否かに関わらず)
- 馴染みのある言語、もしくは使ってみたい言語が使える
- 単体実行可能ファイルが書き出せる
- macOSでも開発ができる
これらを満たすのがPyxelでした。
PICO-8やTIC-80は解像度が物足りなく、Pixel Vision 8は候補としてかなり魅力的でしたが、2021年の時点で開発がストップしていました[2]。
簡単なスクリプトを書いてみる
環境も構築できて、サンプルも実行できたので、今度は簡単なスクリプトを書いてみます。
pyxel.init()
__init__()
内にpyxel.init()
を書くことで、ゲームの設定を指定できます。
ドキュメントにはinit(width, height, [title], [fps], [quit_key], [display_scale], [capture_scale], [capture_sec])
とありますので、ひとまず(quit_key
を抜かして)display_scale
までの5つの引数を設定してみることにします。
draw()
また、draw()
には描画関連の処理を書くことができます。
ここにはpyxel.cls()
を使って背景色、そしてテキストを描画できるpyxel.text()
を使い、経過フレーム数を表示することにします。
ドキュメントを見るとtext(x, y, s, col)
となっているので、pyxel.frame_count
で経過フレーム数を取得し、文字列にキャストしてから第3引数に設定します。
import pyxel
class App:
def __init__(self):
pyxel.init(256, 256, title="Frame Counter", fps=30, display_scale=2)
pyxel.run(self.draw)
def draw(self):
pyxel.cls(2)
pyxel.text(8, 8, str(pyxel.frame_count), 7)
App()
実行結果 / 一秒間に30ずつカウントアップされる数字が表示されます
1文字ずつテキストを表示する
クラス変数に、現在表示されている文字数を保持することで、テキストを1文字ずつ表示することもできます。
import pyxel
class App:
# 現在表示している文字の位置を保持するクラス変数
_text_index = 0
def __init__(self):
pyxel.init(256, 256, title="Tick Text", fps=4, display_scale=2)
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(1)
self.tick_text()
# テキストを1文字ずつ表示するための関数
def tick_text(self):
text = "Hello Pyxel"
# _text_indexが、文字列を全て表示し切ったら、テキストをそのまま表示
if self._text_index > len(text):
pyxel.text(8, 8, text, 7)
return
# 文字列から現在の_text_indexの値までを切り出して表示
pyxel.text(8, 8, text[:self._text_index], 7)
self._text_index += 1
App()
Pyxel APIにない図形を描画する
pyxel APIは矩形や円を描くような関数がありますが、基本的には最低限の機能しかありません。
例えば複雑な図形を描画するのは、画面の任意の箇所にドットを描画するpset()
を駆使することになります。
以下のコードでは円弧を描画してみます。
import math
import pyxel
class App:
def __init__(self):
pyxel.init(256, 256, title="Hello Pyxel", fps=30, display_scale=2)
pyxel.run(self.draw)
def draw(self):
pyxel.cls(1)
draw_arc(32, 32, 32, 0, 90, 7) # 自作関数で円弧を描画
pyxel.circ(128, 32, 32, 8) # pyxelの機能で円を描画
def draw_arc(x0, y0, r, start_angle, end_angle, color):
for theta in range(start_angle, end_angle):
x = x0 + r * math.cos(math.radians(theta))
y = y0 + r * math.sin(math.radians(theta))
pyxel.pset(int(x), int(y), color)
App()
左が円弧、右が円。ちょっと円弧のカービングが汚い
複数のファイルに亘ってコードを書く
import
を使うことで、他のファイルのコードを読み込むことができます。
例えば以下のようなファイル構成だった場合、
app.py # メインのファイル
dialog.py # ダイアログを表示するためのコード
以下のようなimport
文で読み込めます。
import pyxel
import dialog
dither()
を試してみる
pyxel 2.0のcls()
の塗りつぶしやclip()
の切り抜きなどを除いた、大体のグラフィックを半透明にするようです。
pyxel.rect(0, 0, 32, 32, 1).dither(0.2)
のように他の描画用の関数と組み合わせることはできません。
以下のようにdither()
を複数回書いて、ディザの値をリセットすることで、要素ごとの半透明のかかり具合を調整できます。
パターン1
dither()
の後に書かれた要素が、dither()
の影響を受けます。
def draw(self):
pyxel.cls(0)
pyxel.dither(1.0)
pyxel.rect(32, 0, 32, 32, 1) # 不透明になる
pyxel.dither(0.2)
pyxel.rect(0, 0, 32, 32, 2) # 半透明になる
描画結果
パターン2
dither()
が1回しか指定されていない場合は、dither()
の位置に関わらず、全てが半透明になります。
def draw(self):
pyxel.cls(0)
pyxel.rect(32, 0, 32, 32, 1) # 半透明になる
pyxel.dither(0.2)
pyxel.rect(0, 0, 32, 32, 2) # 半透明になる
描画結果
パターン3
dither()
で半透明を指定した後にcls()
を書くと、普通に塗りつぶされる。
def draw(self):
pyxel.cls(0)
pyxel.dither(0.2)
pyxel.rect(0, 0, 32, 32, 2) # 半透明になる
pyxel.cls(3) # 塗りつぶされる
描画結果
パターン4
dither()
で半透明を指定した後にclip()
を書くと、普通に切り抜かれる。
def draw(self):
pyxel.cls(0)
pyxel.dither(1.0)
pyxel.rect(32, 0, 32, 32, 1) # 不透明になる
pyxel.dither(0.2)
pyxel.rect(0, 0, 32, 32, 2) # 半透明になる
pyxel.clip(16, 16, 32, 16) # 切り抜かれる
描画結果