前言

我是 Pyxel 的作者。感謝大家一直以來使用復古遊戲引擎 Pyxel

黃金週大家過得如何呢?難得放假,今天想來寫一點 Pyxel 背後的事情。
(如果你還想知道 Pyxel 到底是什麼,請參考這篇文章。)

經常會有使用 Pyxel 的朋友,或是聽過 Pyxel 的人問我以下這些問題:

「Pyxel 是用 Python 寫的吧?」
「要讓它在各種環境跑起來,不會很麻煩嗎?」
「Pyxel 為什麼能在 Web 瀏覽器裡跑 Python?」
「那些復古畫面是怎麼做出來的?」
「要怎麼重現 Famicom 的聲音?」

其實,這些問題要用一句話回答並不容易。

Pyxel 是以「輕鬆快樂地做遊戲程式」為核心概念的引擎。對外表現得簡單、好懂,但在背後,為了實現復古的畫面與聲音,其實偷偷做了很多事情,所以才會有這些相關疑問。

因此,本文會一邊回答常見問題,一邊介紹 Pyxel 背後運作的機制。希望大家閱讀時能覺得「原來是這樣啊」,看得開心。

這次要談的主題有以下 5 個:

  • Pyxel 是用什麼寫成的
  • 如何在各種環境中執行
  • Pyxel 在 Web 瀏覽器中運作的機制
  • 如何繪製復古風格的圖像
  • 如何播放類似 Famicom 的音效

那麼,我們依序來看吧。

Pyxel 是用什麼寫成的

大家常問「Pyxel 是用 Python 寫的吧?」但答案其實是「表面看起來像 Python,但裡面 99% 是 Rust」。

大家 import pyxel 之後接觸到的 API 確實是 Python,但引擎本體是用 Rust 寫的。

Rust 端的實作分成兩個模組(Rust 裡稱為 crate):

  • pyxel-core:用 Rust 寫的引擎本體(繪圖、音效、輸入等)
  • pyxel-binding:讓 Python 能呼叫 pyxel-core 的薄型包裝層(連接層)

連接 Python 與 Rust 的工具

Python 原本就有能呼叫其他語言程式碼的機制。PyO3 是讓 Rust 更容易使用這套機制的函式庫,而 maturin 則是把 Rust 程式碼打包成稱為「wheel」的 Python 套件格式的工具。有了這兩者,Rust 寫的引擎就能透過 pip install pyxel 輕鬆安裝。

順帶一提,住在首爾的 maturin 作者,也在 Pyxel 的建置相關工作上幫了我很多次。甚至有一次在年底發現 bug,對方還在元旦期間一起協助解決。真的非常感謝。

採用 Rust 的理由

Pyxel 裡面,像是以像素為單位的繪圖、以及以單一取樣點為單位的音訊計算,都是以高頻率(30〜60fps)反覆執行的。要穩定地實作這類處理,Rust 是很合適的語言。

  • 穩定可靠,比較容易壓住會突然當掉的 bug
  • 執行速度可與 C、C++ 相媲美
  • 只要在 Cargo.toml 寫上需要的函式庫(crate)就能導入
  • 容易為各種作業系統與 CPU 建置

不過 Rust 因為很重視安全性,所以對於像遊戲這種「想到就想馬上寫」的程式來說,偶爾會顯得有點拘束。因此 Pyxel 採取分工:引擎本體用 Rust,而寫遊戲的一側用 Python。

另外,Pyxel 一開始其實是用 C++ 寫的。後來因為想要更好的記憶體管理安全性、更容易支援多平台、以及更好用的生態系,所以整個重寫成 Rust。重寫當然相當辛苦,但也因此換來現在穩定且支援廣泛的成果。

另外,雖然也可以用 Rust 版的 crate 直接用 Rust 寫 Pyxel 應用,但如果照一般 Pyxel 的寫法風格去做,會被 Rust 的規則綁得很死,所以平常還是建議從 Python 端使用。

最後提到的「容易為各種 OS 與 CPU 建置」,直接關係到 Pyxel 能在很多環境運作的原因。接下來就來看這部分。

如何在各種環境中執行

「Pyxel 能在那麼多環境跑起來,是怎麼做到的?」這也是常被問到的問題。Pyxel 的多平台支援,是透過設計上的職責分工,以及各種函式庫的組合,盡量有效率地實現的。

Pyxel 目前可在以下環境運作:

  • Windows / macOS / Linux 的桌面環境
  • Web 瀏覽器(包含 iOS 的 Safari、Android 的 Chrome)
  • Chrome OS
  • Raspberry Pi 與中國製掌機等 SBC(單板電腦)

活用 Rust 的 cross compile

能有效率地實現多平台支援,關鍵在於把引擎本體寫成 Rust。Rust 具備完善的 cross compile(從同一份原始碼建置成不同 OS 或 CPU 的版本)機制,只要像 cargo build --target ... 這樣指定一行,就能依照各個目標平台產出對應成果。

編譯出來的結果,會由前面提過的 maturin 整理成各平台的 wheel 套件。Windows 用、macOS(Intel / Apple Silicon)用、Linux 用等版本會一起建立,然後以一組套件的形式發布。當大家執行 pip install pyxel 時,系統會自動選擇符合當前環境的 wheel。

吸收不同 OS 差異的機制

不同作業系統之間的差異——例如視窗怎麼建立、鍵盤與滑鼠怎麼取得、聲音怎麼輸出——Pyxel 先套上一層共通操作規則(平台抽象層),實際功能則由久經使用的共通函式庫 SDL2 來實作。另外,在會大量執行像素級細部處理的部分,桌面平台使用 OpenGL,瀏覽器與小型電腦則使用 OpenGL ES。這些技術本身也都能在許多環境中運作,因此 Pyxel 就能達成「不同 OS 也用同一套程式碼運作」的狀態。

不過,各平台多少都有些微妙的差異,這也是很常見的事,我們也會在收到大家的 bug 回報後逐一修正。由於我不可能擁有所有執行環境,這裡其實也是 Pyxel 開發中最辛苦的部分之一。

在支援的平台中,Web 瀏覽器的支援尤其困難。接下來一章會更詳細說明。

Pyxel 在 Web 瀏覽器中運作的機制

「為什麼 Python 可以在瀏覽器裡跑?」這也是常被問到的問題。

瀏覽器本身沒有執行 Python 的機制,所以會覺得神奇也很正常。這裡登場的是 Pyodide 這個函式庫。Pyodide 是把 Python 執行環境(CPython)編譯成 WebAssembly(簡稱 WASM),讓 Python 能在瀏覽器內執行。

雖然聽到這裡會覺得「原來如此,這樣就能跑 Python 了」,但對 Pyxel 來說,Pyodide 能動還不夠,還需要在其上做一些額外設計。

Pyxel 核心也要轉成 WASM 一起送進去

如前所述,Pyxel 的引擎本體是 Rust。也就是說,要讓 Pyxel 在 Web 上運作,不只需要「讓 Python 能跑的機制」,還需要「讓 Rust 能跑的機制」。而且 Rust 核心引擎所依賴的 SDL2,也必須能在瀏覽器中執行。

因此,Pyxel 也把 Rust 寫的引擎本體(pyxel-core)編譯成 WASM,再送進瀏覽器。瀏覽器內由 Pyodide 執行的 Python,會呼叫這個 WASM 化的 Pyxel 核心來運作。

整理起來如下:

  • 瀏覽器:用 HTML / JS 開啟 Pyxel 頁面
  • Pyodide:在瀏覽器中執行 Python
  • pyxel-binding(Rust → WASM):讓 Python 能呼叫 pyxel-core 的薄型包裝層(連接層)
  • pyxel-core(Rust → WASM):引擎本體(內部使用 WASM 版 SDL2)

前面提到的「Python ↔ Rust」兩層結構,在 Web 世界裡,就直接換成了「Pyodide ↔ WASM」的形式。

與 Pyodide 團隊的合作

其實走到這一步花了很長時間,我們和 Pyodide 團隊一起試錯了大約一年。最困難的是確立「如何讓 Pyxel 依賴的 SDL2 在瀏覽器中運作,並且能被 Pyodide 當作 wheel 載入」。Pyodide 端也需要修改與驗證,幸好團隊成員都很樂意協助,這才讓現在的 Web 執行環境成形。

擴充 HTML,讓 Python 可以直接寫進去

另一個技巧是,Pyxel 擴充了 HTML,提供可直接在 HTML 中寫 Python 程式碼的自訂元素。只要寫像 <pyxel-...> 這類自訂標籤,瀏覽器就會把它當成 Pyxel 頁面來執行。

也就是說,「有 Pyodide 就能跑」只是入口而已,後面還結合了 Pyxel 核心的 WASM 化 + 與 Pyodide 的協作 + HTML 擴充,才讓 Pyxel 能在瀏覽器裡運作。

如何繪製復古風格的圖像

「那些復古風格的繪圖是怎麼實現的?」這也是常見問題。很多人會以為「只用 16 色、解析度又小,應該很簡單吧?」但其實為了讓畫面看起來簡潔,背後做了不少細節處理。

採用調色盤方式

Pyxel 的畫面中,每個像素存的不是「實際顏色」,而是「顏色編號(0〜15)」。每個編號對應哪個 RGB 顏色,則由另外準備的調色盤決定。

這種方式的好處是,只要更換調色盤,就能大幅改變整體視覺風格。例如換成 Famicom 配色的調色盤,就會變得很像 Famicom;換成 MSX 的調色盤,就會是 MSX 風格。也就是說,不用改程式碼,就能切換外觀。

### 直接讀寫 frame buffer

每次呼叫 pyxel.rect()pyxel.line()pyxel.blt() 這類繪圖 API 時,Pyxel 都會直接改寫內部的 frame buffer(存放整個畫面大小的顏色編號陣列)中的像素。

Pyxel 特別採用 不把繪圖邏輯全丟給 GPU,而是由 CPU 直接讀寫 frame buffer 的方式。跟現代化、以 GPU 渲染為主的引擎相比速度較保守,但換來的是更高的彈性。

例如:

  • pyxel.pget(x, y) 讀出單一像素的顏色
  • 將自己計算的結果逐像素塗上去
  • 把 frame buffer 的一部分複製到另一張圖片上進行加工

這些像過去 BASIC 時代那樣自由繪圖的方法,現在依然可以直接寫出來。

此外,內部也加入了以下機制:

  • 抖動(dither):用棋盤格把兩種顏色混合,看起來像中間色的古典技巧
  • 裁切(clip):只讓畫面中指定區域成為繪圖目標
  • 貼圖地圖(tilemap):把繪圖素材以方塊方式排列來繪製
  • 攝影機/調色盤切換:可即時改變繪圖位置與顏色的設定

只有顯示的最後一步才使用 GPU

不過,顯示到螢幕的 最後一步 還是會使用 GPU。最後完成的 frame buffer 會當作 texture 交給 GPU,再貼到畫面上,這樣就能在顯示時加入放大縮小或特殊效果。

透過這種「繪圖邏輯由 CPU 處理、顯示由 GPU 處理」的混合架構,同時保留了復古的手感,以及現代化顯示的彈性。

Pyxel 有個功能是可用 Alt(Option)+9 切換成類似映像管螢幕等特殊視覺表現,這是透過 OpenGL(OpenGL ES)的 shader 功能實作的。

如果把所有繪圖都全面改成 GPU 化,執行速度當然還能再提升。不過這樣一來,像 pgetfill 這類可自由讀寫像素的功能會消失,而且也會出現部分環境無法運作的問題,所以 GPU 化做到哪裡, همیشه 都是需要斟酌的地方。

如何播放類似 Famicom 的聲音

「要怎麼重現 Famicom 的聲音?」這也是常有人問的問題。

這不是把錄好的聲音直接播放,而是即時模擬 Famicom 的音色生成——也就是自行實作了所謂的軟體合成器(soft synth)。

重現 Famicom 的軟體合成器

Pyxel 的音效引擎中,每個聲道會結合以下要素來產生聲音:

  • 波形表:方波、三角波、雜訊、以及可由使用者自行編輯形狀的自訂波形
  • 包絡線(envelope):音量變化(起音/衰減/持續/釋放)
  • 顫音/滑音:讓音高擺動,或平滑改變音高的效果

這些都會在 Rust 端全部計算,將 4 個聲道的波形加總後從喇叭播放。也就是每秒精準計算出「聲道數 × 22,050 個取樣值」並準時播放。

透過即時編輯波形,以及可自由增加同時發聲數,實現了當時遊戲機沒有的自由度;在保留 Famicom 味道的同時,也能做出「我想要再多一點這種音色」的效果。

MML:用字串寫音樂的語言

在這之上,Pyxel 還實作了自家規格的 MML(Music Macro Language,音樂巨集語言)來描述音樂。

pyxel.sounds[0].mml("CDEFG")

像這樣只要傳入 CDEFG 的字串,就能播放 do re mi fa sol。速度、八度、音色等設定也都可以寫在 MML 裡,所以可以用一個字串定義整段旋律。

背後會由 Rust 端對字串逐字讀取,進行「這是音符 C」「這是四分音符長度」「這是提高八度的指令」這類的詞法分析與語法分析。也就是把語句拆解成意義單位的處理,用在音樂字串上了。

### 復古風格 × 當時沒有的編輯環境

把這些機制組合起來,就能在保有復古外觀與音感的同時,實現當時遊戲機所沒有的聲音編輯與播放自由度。

我們官方提供的工具 Pyxel MML Studio(可在瀏覽器中撰寫 MML、試聽與分享的工具),以及 frenchbread さん製作的 Pyxel Composer(可用 GUI 編輯音色與旋律的工具),內部也都是使用剛剛介紹的音效引擎來輸出聲音。

最近,frenchbread 做的自動作曲功能也以 gen_bgm 這個函式名稱被整合進 Pyxel。這是一種基於音樂理論動態產生 MML 的演算法,可以依照遊戲狀況即時生成旋律並播放。

表面上看起來只是播放「很陽春的復古音」,但背後其實是每個波形都逐一計算並混音,同時還塞進了當時不存在的編輯自由度的合成器實作。

結語

到這裡為止,我們大致看過 Pyxel 背後到底有哪些機制。

表面雖然簡單,但底下其實塞了 Rust + Python 的雙層架構、為了 Web 執行而與 Pyodide 協作、結合 CPU 與 GPU 的繪圖、自製的軟體合成器與 MML 等等,意外地充滿細節。如果你因此感受到「復古不代表設計一定簡單」,我會很開心。

目前我們正朝著新的主要版本 3.0 推進大型新功能的實作。完成後,預計能更輕鬆愉快地製作更有特色的遊戲,敬請期待。

Pyxel 是我個人持續開發的專案,大家的使用與回饋就是最大的鼓勵。如果你喜歡它,歡迎到 GitHub 按個星號

Pyxel 可以免費使用,也有免安裝的瀏覽器版本。還沒碰過的人,也歡迎從這裡輕鬆試玩看看。

如果你想更深入學習,也推薦官方書籍 《用遊戲學 Python!從 Pyxel 開始的復古遊戲程式設計》

那麼,祝大家有個愉快的 Pyxel 生活!


原文出處:https://qiita.com/kitao/items/5361d45554872a39da92


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝12   💬4   ❤️1
464
🥈
alicec
📝1   ❤️2
87
#4
我愛JS
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登