Pyxelは扱いやすいプログラム言語であるPythonと低解像度の2Dドット絵を用いてゲーム制作ができるので、ゲーム制作入門にうってつけのゲームエンジンだと私は考えていますが、「敷居の低さ」という意味ではサウンド面が1つのハードルになるかもしれません。
Pyxel版MMLの登場
2025年になってPyxelはMMLをサポートするようになり、今年のアドカレでもMMLを題材にしたテーマが複数投稿されています。
- 【Pyxel】SlackBot に MML → .wav 変換を仮実装してみた
- Pyxel を利用して MML を mp3 にして返すコンテナの実装をしてみた
- Pyxel の MMLで音色づくり、Pyxel MML Studioならスマホでも聞ける
これらの記事内でも紹介されていますが、Pyxel MML Studio ではWeb上で手軽に打ち込んで試すこともできるので、Pyxel MML StudioでMMl制作をして、できあがったらMMLをゲームに組み込む、といった手法で、以前より手軽に本格的な8ビット風サウンドを取り入れられるようになりました。
でもサウンドは作れないのでやっぱり素材使いたい
とはいえ、MMLも知らないしそもそも作曲なんて無理、まだ人に依頼するほどの段階でもないし... という方も当然いらっしゃると思います。
であれば、サウンドに関してはPyxelの機能にこだわらずオーディオ使っちゃいましょう!
オーディオなら素材もいろいろ手に入りますし、作曲ができる人に依頼もしやすいでしょう。
具体的には古からのPython用ゲームライブラリ 「pygame」のmixer機能だけ を使います。
ループ対応したmp3やoggのファイルを用意するだけで、BGMのループ再生、フェードイン・アウト、一時停止や再開などゲームでよく使う操作が手軽に実装できます。
ただ、残念ながら大きな欠点(制約)があります。
pygameはpyodide(pyxelでも利用している、pythonコードをWebで動かすライブラリ)に対応していないので、この記事で紹介するやり方で制作したゲームはWeb公開できません。
サンプル
実際にリンクから手軽に操作してもらえるサンプルを用意したかったのですが上記事情によりできないので、以下の動画で動作サンプルをご覧ください。
手元で動かす場合は、以下からコードをダウンロードしてmain.pyを実行してください。
実現方法
サウンドクラスを用意し、その中でpygame.mixerを使った処理を行います。
今回のような小さいデモであればメインプログラムに組み込んでもよいのですが、独立したソースにしておくとで、いろいろなプロジェクトに流用可能になります。
from pygame import mixer
class Sound:
def __init__(self, vol=0.6):
mixer.init()
self.file = None # 再生中のファイル名
self.pausing = False
self.set_volume(vol)
# 音量設定
def set_volume(self, vol):
self.vol = vol
mixer.music.set_volume(vol)
# BGM再生
def play(self, file, fade_time=300):
# まだ再生中かもしれないので止める
mixer.music.stop()
try:
mixer.music.load(file)
except Exception as e:
print(e)
return
self.file = file
self.pausing = False
mixer.music.play(loops=-1, fade_ms=fade_time)
# 停止
def stop(self, fade_time=300):
if fade_time:
mixer.music.fadeout(fade_time)
else:
mixer.music.stop()
# ポーズ/再開
def toggle_pause(self):
if not self.file:
return
if self.playing():
mixer.music.pause()
self.pausing = True
else:
mixer.music.unpause()
self.pausing = False
# 再生位置取得(ミリ秒:ループしても維持されます)
def get_pos(self):
return mixer.music.get_pos()
# 再生中かどうか(True/False)
def playing(self):
return mixer.music.get_busy()
続いてメインプログラムです。
キーボードで再生/停止、一時停止/再開、音量の上下ができる程度の簡単なコードにしています。
assets.pyxresをロードしていますが中身は空っぽで、単にパレット(スーファミっぽい雰囲気を出そうと思って背景をグラデーションにしました。この記事の趣旨とは関係ないです)をセットすることが目的です。
import pyxel as px
from sound import Sound
class App:
def __init__(self):
px.init(256, 224, "Pyxel-with-Audio サンプル") # スーファミっぽいサイズ
px.load("assets.pyxres") # パレット読み込み用
self.bdf = px.Font("umplus_j12r.bdf") # フォントファイル
self.sound = Sound()
px.run(self.update, self.draw)
def update(self):
if px.btnp(px.KEY_RETURN):
if self.sound.playing():
self.sound.stop()
else:
self.sound.play("sample.ogg")
if px.btnp(px.KEY_SPACE):
self.sound.toggle_pause()
if px.btnp(px.KEY_UP, 10, 2):
self.sound.set_volume(min(self.sound.vol + 0.05, 1.0))
if px.btnp(px.KEY_DOWN, 10, 2):
self.sound.set_volume(max(self.sound.vol - 0.05, 0.0))
if px.btn(px.KEY_ESCAPE):
px.quit()
def draw(self):
px.cls(0)
sound = self.sound
for y in range(224):
c = y // 7
px.line(0, y, 255, y, c)
self.draw_text(40, 34, "【BGM再生サンプル】")
self.draw_text(40, 66, "エンターキーで再生/停止")
self.draw_text(40, 82, "スペースキーで一時停止/再開")
self.draw_text(40, 98, "上下キーで音量調整")
playing = sound.playing()
state = "停止中"
if playing:
state = "再生中"
elif sound.pausing:
state = "一時停止中"
self.draw_text(40, 130, f"再生状態:{state}")
self.draw_text(40, 146, f"音量 :{int(sound.vol * 100)}")
if playing or sound.pausing:
self.draw_text(40, 162, f"ファイル:{sound.file}")
pos = sound.get_pos() / 1000
self.draw_text(40, 178, f"再生位置:{pos:.3f}秒")
def draw_text(self, x, y, t):
for i in range(2):
c = (0, 33)[i]
px.text(x, y + 1 - i, t, c, self.bdf)
App()
補足:Web版でオーディオ使いたい
pygame.mixerを使った方法ではWeb化できないと書きましたが、Web版ではオーディオを使うことができないわけではなくて、HTMLにタグを配置し、とPythonからJavascriptの関数を呼び出してaudioタグを鳴らす、という方法があります。(ちょいと面倒です。)
もしローカルとWeb両方で動かしたいなら、ローカルとWebで動作を切り分けて、ローカル動作時はpygame.mixer、Web動作時はaudioタグを使う、といった実装も可能ですね。
サウンド関連で悩んでいる人のヒントになったら幸いです。