Open7

Pyxelに入門してみた

SLMNLLSLMNLL
SLMNLLSLMNLL

なぜPyxelを選んだか

私は普段、ゲーム開発にはGodot Engineを使っているのですが、ゲームのプロトタイピング、趣味性全開な小さいプロジェクトなどで活用するために、ファンタジーコンソールを使ってみたいと考えていました[1]

選定にあたって、個人的に外せないのは以下の要素でした。

  • NES(ファミコン)くらいの解像度が欲しかった
  • 開発が活発(OSSか否かに関わらず)
  • 馴染みのある言語、もしくは使ってみたい言語が使える
  • 単体実行可能ファイルが書き出せる
  • macOSでも開発ができる

これらを満たすのがPyxelでした。

PICO-8やTIC-80は解像度が物足りなく、Pixel Vision 8は候補としてかなり魅力的でしたが、2021年の時点で開発がストップしていました[2]

脚注
  1. Godot Engineはさまざまな機能が揃っていて非常に便利で、間違いなく一番好きなゲーム開発環境です。ただ、私の場合は制約がない分あれこれ詰め込んでしまいがちになり、いつの間にか小規模なプロジェクトじゃなくなってしまうのが悩みの種です。逆に、ファンタジーコンソールは制約が多く、ワンアイディアを形にするのにとても良いと考えています。 ↩︎

  2. ファンタジーコンソール自体、群雄割拠の時代を過ぎて今は廃れ気味なのか、プロジェクトがストップしているものが多く、今でも開発が続いているのはPICO-8、TIC-80、Pyxelと、その他だと、pikuseruくらいしか観測できませんでした。 ↩︎

SLMNLLSLMNLL

簡単なスクリプトを書いてみる

環境も構築できて、サンプルも実行できたので、今度は簡単なスクリプトを書いてみます。

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ずつカウントアップされる数字が表示されます

SLMNLLSLMNLL

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()

SLMNLLSLMNLL

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()


左が円弧、右が円。ちょっと円弧のカービングが汚い

SLMNLLSLMNLL

複数のファイルに亘ってコードを書く

importを使うことで、他のファイルのコードを読み込むことができます。

例えば以下のようなファイル構成だった場合、

app.py # メインのファイル
dialog.py # ダイアログを表示するためのコード

以下のようなimport文で読み込めます。

import pyxel
import dialog
SLMNLLSLMNLL

pyxel 2.0のdither()を試してみる

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) # 切り抜かれる


描画結果