沖縄行ってきた🌴
先月、Okinawa.swiftという開発合宿に参加してきた。
せっかくなので前から触ってみたかった新しい領域に手を出してみることに。当初はApple Vision ProとRealityKitを使ったゲーム開発を考えていたけど、3日前から素振りを始めたところ、習熟する時間も、素材を用意する力量も、本番の開発時間も全く足りないなと判断して即座に断念。Embedded Swiftによる組み込みゲーム開発をしてみることにした。
実は僕はゲーム開発の本を書いてる程度にはゲーム開発をしていて、以前は毎年ゲーム開発のハッカソンに出ていたのも今や昔。
沖縄行ったらなぜかゲームができた
というわけでこんなゲームができた。

今更だけど #okinawa_swift で開発したゲーム。Embedded Swiftで書いてます pic.twitter.com/ANzvMIE2yR
— giginet (@giginet) December 4, 2025
短い作業時間でなんとか形になった!落ちてくる爆弾を大砲で撃ち落とすゲーム。Playdateはクランク(側面のハンドル)が特徴的なゲーム機なので回転のアクションを取り入れたかった。十字キーによる移動も付けたら、2軸の操作でとても忙しいゲームになった。
下準備はあれど、環境構築だけで、ゲームロジックや素材は全て合宿中に作った。なんとか形になって良かったー。
ちなみに今回の実装は全て公開しているので参考にどうぞ。
SwiftでPlaydateを開発しよう!
ここからはPlaydateとSwiftでどうやってゲームを作ったか、技術的なところを紹介したい。
Embedded Swiftの実例として、Swiftの公式ブログでもPlaydateというゲーム機で動くサンプルが紹介されている。
Playdateは開発環境付きの携帯ゲーム機。ほとんどのゲームはLuaで書かれているが、Swiftで書くという酔狂なこともできる。Macアプリで有名なPanic社が開発しているのが面白い。中の人は本当に好きでやっているんだろうなと熱を感じる。
Embedded Swiftのビルドの仕組みと開発環境
Embedded SwiftをPlaydateで動作させるためのサンプルリポジトリがSwift公式から提供されている。
基本的にはこのサンプルとドキュメントを読むとビルドはできるようになるが、一部情報が古いので適宜読みが必要。例えばドキュメントが出た当時はstableなSwiftでEmbeddedモードがサポートされていなかったが、今は最新の6.2 Toolchainを使えば良い*1。
サンプルはモノレポで複数のゲームをビルドする構成になっているので、まずゲーム単体のリポジトリに移行した。
ビルド環境は全く洗練されてなく、Makefileでswift-frontendに直接ビルドオプションを渡してオブジェクトファイルを生成する力業。ポイントは-enable-experimental-feature Embeddedオプションを付与すること。
Xcodeによるビルドはサポートされていないので、デバッグするときはShell Scriptからシミュレータを起動する。サンプルには2本のオマケゲームが付いているので、まずはこれらを読んで勘所を掴む。
Playdate APIの実装
サンプルゲームを一通り把握したら、今度は他のAPIを試してみる。
基本的にPlaydate SDKのブリッジをSwiftで書くだけ。UnsafePointerやOpaquePointerを操作する必要はあるが、ドキュメントを読めばそこまで難しくない。
var graphicsAPI: playdate_graphics { playdateAPI.graphics.unsafelyUnwrapped.pointee } public static func drawRotatedBitmap( bitmap: OpaquePointer, x: Int32, y: Int32, degrees: Float, centerx: Float, centery: Float, xscale: Float, yscale: Float ) { graphicsAPI.drawRotatedBitmap.unsafelyUnwrapped(bitmap, x, y, degrees, centerx, centery, xscale, yscale) }
可変長引数を受け取る関数などを扱う場合、一部C側にラッパーを生やす必要はある。
Playdate SDKでは、CのAPIドキュメントも提供されているので、あとはこれを見ながら、使いたいAPIを機械的にブリッジしていくだけだ。
全てのAPIを掌握したので、ここまで来るともはやゲーム開発環境としての攻略が完了したといっても良い。あとは作るだけ。
グラフィック - 生成AIと1bitドット絵のコラボレーション
swift-playdate-examplesのうち、1つめのLifeGameは直接ピクセルバッファを操作しているが、2つめのSwiftBreakはSprite APIを使っていてかなりモダン。実は2つめのサンプルの方がとっつきやすい。LifeGameから読み始めたので、これは骨が折れるぞと尻込みしたが、実際はpngまでそのまま貼れちゃうのでお手軽。
僕は絵が描けないのでChatGPTに1bitっぽい絵を描いてもらった。*2

ChatGPTはわりと絵が上手いが、単体ではプロンプトを試行錯誤しても正しいサイズや色で生成するのは困難だ。そのため、人間が手作業で1bit化したりアスペクト比を修正している。
macOSでドット絵を描くにはPixenというアプリを愛用している。二値化もこれでできる。
ついでに、簡単なグラについては自分で作画した。絵心がなくても白、黒、透過の3色だけだとわりとなんとかなる。

サウンド - レトロに見せかけて高級
音が出るとゲームの完成度が飛躍的に上がるので、なんとしてもサウンドは出したかった。Playdateは音周りもリッチ。
昨年のiOSDCでは、Playdateを使ってシーケンサを実装するという面白いトークをされていた方がいて、サウンドプログラミングの知識があれば内蔵音源で鳴らせそう。
チップチューンは最高なので、そのうち内蔵音源にも挑戦してみたいが、今回はサンプリング音源を鳴らすだけのFilePlayerという情緒も何もない方法を採用することにする。mp3も鳴らせるぞ!
サンプルアプリにはサウンドを鳴らす仕組みは付属していないので、C APIのbridgeをSwiftで書くことになる。
var soundAPI: playdate_sound { playdateAPI.sound.unsafelyUnwrapped.pointee } public enum Sound { } extension Sound { public enum FilePlayer { } } extension Sound.FilePlayer { private static var filePlayerAPI: playdate_sound_fileplayer { soundAPI.fileplayer.unsafelyUnwrapped.pointee } public static func newPlayer() -> OpaquePointer? { filePlayerAPI.newPlayer() } public static func loadIntoPlayer(_ filePlayer: OpaquePointer, _ path: StaticString) -> Int32 { filePlayerAPI.loadIntoPlayer(filePlayer, path.utf8Start) } public static func play(_ filePlayer: OpaquePointer, _ `repeat`: Int32) -> Int32 { filePlayerAPI.play(filePlayer, `repeat`) } }
効果音の作成は、jsfxr というインディーゲームデベロッパー御用達のチップチューン作成ツールを使うことにする。波形を弄る知識はあんまりないので、ランダムで適当に生成してそれをベースに細かいところを弄ってそのまま採用。

Vibe Game Development
自分でなんちゃってゲームエンジンを実装して、グラフィックや音素材まで揃えると、ゲームロジックについてはほとんどVibe Codingで解決した。
もっとも、Okinawa.swiftは合宿とは名ばかりで、会期中の実態はほとんど酒を飲み続けるとんでもないイベントだった。作業時間は6時間程度。こんな短い時間でゲームを作るのは至難の業だけど、Claude Codeが全てを解決してくれた。
1時まで酒を飲んで、桃鉄をやっている裏側でClaude Codeがゲームロジックを生成していた。これが新時代の開発合宿か。

タイトル画面やゲームオーバーといったシーケンス遷移、当たり判定やライフ制、爆風のアニメーションなど、ほとんどの表層的な部分は全部Claude Codeにより実装された。これがなければ短期間で形にするのは無理だった!
Playdateでのゲーム開発
一見するとレトロゲーム開発のように見えるが、開発環境としてはめちゃくちゃモダンだった。最初はアルファチャンネルとかあるんかな、ぐらいのことを気にしていたが、実態はSpriteなどの概念もありリッチ。pngもmp3も使えるし、シミュレーターもあるし、スペックも困ることないぐらい潤沢。それどころか単体でネットワークアクセスまでできちゃう。オンライン対戦も実装できそうなぐらい。
一度環境構築ができてしまえば、あとはゲームロジックの実装に集中できる。
今回は時間がなくて、直接CのAPIを叩いたりしていたが、上手くゲームエンジンとしてラップすれば、ほぼSpriteKitと同じような感じでゲーム開発ができそう。
ちなみに、Swift PackageとしてPlaydateKitというものもあるようだ。チラ見はしたが、自前で書けば十分そうだったので今回は採用を見送った。
モダンな言語で低級なことをする
組み込み開発にSwiftが使えるのは面白い。印象的だったのは、~Copyableや borrowing など、比較的最近登場した言語機能の活用だ。
CのAPIをラップしている関係上、生ポインタを引き回したり、適切にalloc/releaseしなくてはならない用途は多い。そこにSwiftのownershipは綺麗にハマる。
例えば、先ほどのプリミティブなサウンドAPIをラップしたAudioPlayerのような実装を考える。newPlayerはC側の構造体のポインタを返すが、それをstructで保持する。
struct AudioPlayer: ~Copyable { enum Error: Swift.Error { case initializeFailed } private let filePlayer: OpaquePointer init(file: StaticString) throws { guard let filePlayer = Sound.FilePlayer.newPlayer() else { throw Error.initializeFailed } self.filePlayer = filePlayer let _ = Sound.FilePlayer.loadIntoPlayer(self.filePlayer, file) } func play() -> Bool { Sound.FilePlayer.play(self.filePlayer, 0) != 0 } deinit { Sound.FilePlayer.freePlayer(filePlayer) } }
~Copyableになっていることで、内部のC側で生成したポインタを不要にコピー、移動してしまうことは防げるし、Swift側のライフサイクルによるdealloc時に自動的にメモリ解放も行われる。とても綺麗で安全。
let explosionPlayer = try AudioPlayer(file: "sounds/explosion") explosionPlayer.play()
他にもSwift 6.2から導入されたSpanを使えば、ピクセルバッファをより安全に操作できたり、いろいろ応用が利きそう。
ドット絵やチップチューン、レトロゲーム開発手法といった古代の技術と、Vibe Codingの手法やownershipのようなモダンなパラダイムが交差しているのは、なんとも総合格闘技という感じがしてとても面白い。
生成AI時代のゲーム開発はゲームが変わった!
以前は毎年出ていたゲーム開発のハッカソンも出場しなくなって久しいが、Vibe Codingの登場により完全に別世界になったと感じる。
コーディングエージェントで作らせて、テストプレイして、仕様変更して、といったフィードバックループが人間業では不可能だった早さで回るようになり、短期間でのゲームデザインの試行錯誤が可能になった。これにより、ゲーム自体を面白くしたり、レベルデザインを洗練させるという本来の創作に専念することができるように。その上、ハッカソンのような書き捨てのコードの生成にも相性が良い。まさにゲームチェンジャー。

今回は時間がなさ過ぎてそこまで行けなかったが、この辺のVibe感に脳を焼かれたくなってきたので、またゲーム開発をしてみても良いかも。
Okinawa.swift最高〜
開発も濃厚だったが、その他も大ボリュームの2泊3日だった。いつものメンバーでわいわいしながら修学旅行みたいだった。良い思い出ができました。
