Flash Lite のガラケーゲームを Flutter で復刻する — SWF パーサーから自作してブラウザ公開するまで
| 開発記録 | ギリギリジャンパー
タグ: #Flutter #Dart #Flash #SWF #ゲーム開発 #Web
ガラケー向けに作った自作ゲーム 6 本が、Flash の終了で遊べないまま何年も眠っていました。SWF4 パーサーも ActionScript VM も描画も Flutter で自作して、公式サイトの /play/ で復刻しました。フォーマット解析から公開までの記録です。20 年前のゲームがブラウザで動きました。
Flash とともに眠っていたゲームたち
スマホより前、フィーチャーフォン(いわゆるガラケー)向けに Flash Lite 1.x でミニゲームを作っていました。「ギリギリジャンパー」シリーズなど、手元に 6 本残っています。Flash Player は 2020 年末でサポートが終了しました。当時の携帯実機ももうありません。SWF ファイルという「データ」はあるのに、それを再生する環境がどこにもない。そんな状態が何年も続いていました。
そこで、SWF ファイルを直接読み込んで再生するシミュレータを Flutter で自作しました。公式サイトの ゲームページ(/play/) で公開しています。ブラウザ上で、当時の携帯画面サイズのまま遊べます。
既存の Flash エミュレータを使う選択肢もありました。それでも自作にしたのは、理由が 3 つあります。ガラケー特有の「十字キー+テンキー」前提の操作 UI を画面上に再現したかったこと。以前から運用しているスコアランキングと連携させたかったこと。そして何より、SWF というフォーマットの中身を一度自分の手で理解してみたかったことです。
SWF4 は素直なフォーマットだった
Flash Lite 1.x が再生できるのは、おおむね Flash 4 世代(SWF4)のコンテンツです。四半世紀近く前に固まった仕様なので、もう変更されません。「枯れたフォーマット」は、実装対象としてとてもありがたい存在です。
構造も素直です。ファイルはヘッダとタグ列でできています。各タグは 16bit の値にタグコードと長さがパックされていて、上位 10bit がコード、下位 6bit が長さです。パーサーの中心部はこんなループになりました(抜粋・簡略化しています):
while (r.bytePosition < endPos) {
final tagCodeAndLength = r.readUI16();
final tagCode = tagCodeAndLength >> 6;
var length = tagCodeAndLength & 0x3F;
if (length == 0x3F) length = r.readUI32(); // ロングタグ
final body = r.readBytes(length);
final tr = SwfBitReader(body); // タグ本体用のリーダー
switch (tagCode) {
case TagCode.showFrame:
frames.add(current); // 1フレーム確定
current = SwfFrame();
case TagCode.doAction:
current.actions.add(body); // スクリプトは現在フレームへ
case TagCode.placeObject2:
current.ops.add(_parsePlaceObject2(tr));
case TagCode.defineShape:
_defineShape(tr, 1); // 素材は辞書へ
// ... DefineSprite / DefineEditText / DefineFont2 など
}
}
タグは大きく 2 系統に分かれます。DefineShape や DefineSprite のような「素材定義」タグは、ID をキーに辞書へ登録します。PlaceObject2 や ShowFrame のような「制御」タグは、フレーム列に積んでいきます。この整理さえできれば、タイムラインの再生は「各フレームの命令を順に適用して表示リストを更新する」だけです。
なお、音声系のタグは読み捨てています。今回のゲームは効果音がなくても遊べるものばかりでした。視覚の再現に全振りする割り切りです。対応範囲を「自分のゲーム 6 本が正しく動くこと」に絞る。これが短期間で公開まで持っていけた最大の要因です。
ActionScript は小さなスタックマシン
SWF4 の ActionScript は、コンパイル済みのバイトコードとして SWF に埋め込まれています。中身はスタックマシンです。Push、演算、変数の読み書き、GotoFrame といった 50 種類ほどの命令を、switch で分岐する小さなインタープリタを書きました。
ひとつ実戦的な工夫があります。1 回の実行あたりの命令数に上限(200 万命令)を設けました。20 年前の自分が書いたスクリプトを信頼しすぎない、という自衛策です(過去の自分は、だいたい何かやらかしています)。万一無限ループがあっても、ブラウザのタブごと固まることはありません。打ち切って先へ進みます。
twips の世界から Canvas へ
SWF の座標系は「twips」という単位です。1 ピクセル=20 twips。図形(DefineShape)は直線と 2 次ベジェ曲線のエッジレコードで表現されています。これを Flutter の Path に変換し、CustomPainter で描画します。
面白いのはフォントです。DefineFont2 タグには、使用するグリフの輪郭データそのものが埋め込まれています。だからフォントファイルが手元になくても、当時のビットマップ風フォントをそのまま再現できます。テキスト描画もグリフ輪郭からの自前描画です。
ゲームだけでなく「ガワ」も再現する
ガラケーのゲームは、十字キーとテンキーで遊ぶ前提で作られています。SWF 内部ではボタンのキー割り当てが CondKeyPress というキーコードで表現されています(左=1、右=2、決定=13、上=14、下=15、数字キー=48〜57…)。そこで画面下半分にガラケー筐体風のバーチャルキーパッドを描き、タップをこのキーコードに変換して配送します。長押しには 300ms 待ってから 100ms 間隔のオートリピートも付けました。PC では物理キーボードの矢印キーなどを、変換表で同じコードに写像しています。
もうひとつ地味に重要だったのが、フレームスケジューラです。Flutter の Ticker で経過時間を蓄積し、SWF ヘッダのフレームレートに合わせてフレームを進めます。ここで素朴に実装すると、ブラウザのタブを切り替えて戻ってきたときに「溜まった時間のぶん一気に進む」。放置しただけで即ゲームオーバーです。キャッチアップは最大 2 フレームまでとし、それ以上の遅れは捨てる設計にしました:
accumulator += delta;
var advanced = 0;
// キャッチアップは最大2フレーム(それ以上は破棄)
while (accumulator >= frameDuration && advanced < 2) {
stage.advanceFrame();
accumulator -= frameDuration;
advanced++;
}
if (accumulator > frameDuration * 2) {
accumulator = Duration.zero;
}
ゲームオーバーの検知には、タイムラインのフレームラベルを使っています。ルートタイムラインが特定のラベルに到達したら、Flash 側のスコア変数を VM から読み取ってランキング登録 UI を出す。そういう連携です。20 年前のコンテンツと今日のサーバー API が、フレームラベルひとつで握手している。ちょっと感慨深いです。
もちろんハマりどころもあった
すんなり動いたわけではありません。大小のバグ取りがありました。代表的なものを 2 つだけ挙げます。
- macOS の日本語ファイル名問題。日本語ファイル名のアセットが、Unicode 正規化の差(NFD/NFC)でアセットキーと一致しません。ロード画面のまま止まる現象に遭遇しました
- スクリプトとタイムラインの優先順位問題。ActionScript が動かしたスプライトを、タイムライン側の巻き戻し処理が上書きしてしまう。画面の演出が一瞬で消えるバグです。オープンソースの Flash エミュレータ Ruffle にも同種の対策フラグがあり、Flash 再実装の「あるある」だと後から知りました
どちらも一本の記事になる濃さです。別途詳しく書く予定です。
公開まわり
配置はサイト本体と同じサーバーの /play/ です。ゲーム自体は CanvasKit 描画なので、検索エンジンから見ると本文ゼロのページになります。だから noindex にして、サイト本体の SEO とは切り離しました。トップページのメニュー「ゲーム」から遊べます。PC では、ガラケーの画面比率に合わせた縦長のポップアップで開くようにしました。
まとめ
- 枯れたバイナリフォーマットは仕様がもう動かない。パーサー自作の題材として良質でした
- 「全機能対応」ではなく「手元の 6 本が動く」を完了条件にした。おかげでパーサー・VM・描画・UI まで含めて、実質 4 日ほどで公開に到達できました(Claude Code とのペアプログラミングです)
- 自作コンテンツは、再生環境ごと自分で持ってしまえば延命できる
20 年前のゲームがブラウザで動いた瞬間は、うれしかったです。当時の自分のスクリプトが、自分で書いた VM の上で走っている。よろしければ ゲームページ で遊んでみてください。ランキング登録もできます。