1 ポイント 投稿者 GN⁺ 2023-10-09 | 1件のコメント | WhatsAppで共有
  • Flappy Dirdは、macOS Finderのファイル一覧を画面として、ファイルの選択と開く操作を入力として利用してFlappy Birdを動かす実験的な実装
  • 核となるアイデアは、Finderでのみ更新されるディレクトリのDate Last Openedの値をボタンのように読み取り、シンボリックリンクのファイル名を変えて画面を描画することから始まった
  • 単純なファイル名変更によるレンダリングは遅く、画面のティアリングも激しかったため、AppleScriptでFinderウィンドウの対象ディレクトリを切り替えるダブルバッファリング方式へ改善
  • ダブルクリック入力は秒単位のタイムスタンプ精度の限界により2 FPS以上では不安定で、その後Finderの選択中の項目を読む方式へ移行
  • 最終的な構成ではAppleScriptがループを回し、Pythonがゲーム状態とレンダリングを担当し、4 FPSで動作するが、入力の取りこぼしやFinder特有の制約は残っている

Finderをゲーム画面として使う

  • Flappy Dirdは、GitHubリポジトリでコードを確認して自分で実行できるFinderベースのゲーム
  • ゲームには使い方、ハイスコア追跡、スクロールするバナー広告が含まれる
  • ユーザーはダブルクリックでゲームを開始し、Finderウィンドウ内の任意のファイルを選択して鳥をジャンプさせる
  • 実行速度は毎秒4フレーム程度で、これより大幅に速く動かすのは難しく、入力もときどき取りこぼす

Date Last Openedをボタンのように使う

  • 出発点は、Finderにディレクトリ用のDate Last Openedフィールドがあることだった
  • 観察された挙動は3つあった
    • cdでディレクトリに入ってもタイムスタンプは更新されない
    • Finderでディレクトリのシンボリックリンクをダブルクリックするとタイムスタンプが更新される
    • mdlsでその値を秒単位の精度で読める
  • この性質を利用してFinder内にボタンを作れた
    • dirディレクトリ内に、dirを指すbuttonシンボリックリンクを作る
    • 開始時にdirの最終オープン時刻を読む
    • 繰り返しタイムスタンプをポーリングし、値が変わったら入力として処理する
    • Finderの位置を変えずにbuttonを開いてdirの最終オープン時刻を変える

絵文字ファイル名で15x15画面を描く

  • Finderの標準フォントではASCIIアートを揃えるのが難しかったが、ファイル名に入れた絵文字が一定幅で表示されることを活用した
  • プロトタイプでは、dirの中に再びdirを指す15個の下位シンボリックリンクを作り、各リンク名を絵文字の行に変えて画面を構成した
  • レンダリング関数はbird_y_pospipe_locationsframeの値を受け取り、15x15の絵文字グリッドを生成する
  • 毎フレーム、ディレクトリ内のすべてのシンボリックリンク名を絵文字グリッドの各行へ変更する
  • FinderがDate Modified基準で並べ替えるように、名前変更の順序を調整するハックも必要だった
  • この方式は動作したが非常に遅く、画面のティアリングも激しかった

AppleScriptとダブルバッファリング

  • 画面のティアリングを減らすためFinderのファイル一覧を更新する方法を探し、AppleScriptコマンドtell application "Finder" to tell front window to update every itemを使った
  • このコマンドは多少役立ったがティアリングは残り、その後AppleScriptでFinderを直接制御する方向につながった
  • ダブルバッファリング候補はいくつかあった
    • シンボリックリンクでディレクトリ内容を原子的に入れ替える方式は、Finderでシンボリックリンク先ディレクトリの内容を展開できず不向きだった
    • 互いを指す2つのディレクトリを使う方式はティアリングを減らせるが、ユーザーがクリックしたときにしかフレームを進められずゲームには向かなかった
    • Finderが表示するディレクトリを変えつつ、"最終オープン時刻"は変えない方法が必要だった
  • 解決策は、AppleScriptのtell application "Finder" to set target of front Finder window to ("PATH" POSIX file)でFinderウィンドウの対象ディレクトリを切り替える方式だった
  • この方法でダブルバッファリングを実装すると、ゲームは1 FPSでも滑らかに動作したが、それでも速度は遅かった

入力方式の変更: ダブルクリックからファイル選択へ

  • ダブルクリック入力は、タイムスタンプ精度が秒単位のため2 FPS以上では限界があった
  • AppleScriptでFinderウィンドウの選択中の項目を読めるため、入力を「任意のファイル選択」に変更した
  • 新しいゲームループは次の構成だった
    • ユーザーがダブルクリックするまで待機する
    • 毎フレームAppleScriptで現在のウィンドウでファイルが選択されているか確認する
    • 選択があるか、最終オープン時刻が変わっていれば鳥をジャンプさせる
    • AppleScriptでFinderが表示するディレクトリを切り替える
    • 目標フレームレートに合わせるため残り時間だけ待機する
  • この方式で2 FPSにはある程度到達したが、AppleScript呼び出しに約0.2秒かかり固定コストが大きかった
  • 2 FPSでも、入力確認後にユーザーがファイルを選ぶと入力を取りこぼす可能性があった

メインループをAppleScriptへ移す

  • プロファイリングでは、数字1をログに出して終了するAppleScriptが約0.14秒かかることが示された
  • その後、この測定は公正なAppleScript起動時間の測定ではなく、空のAppleScript基準では約0.06秒に近い可能性があるという訂正が付いた
  • 毎フレームAppleScriptの起動コストを払わないよう、メインループをAppleScriptへ移し、Pythonはゲームロジックとレンダリングを担当するようにした
  • 基本構成は次の通り
    • game.py awaitでユーザーのダブルクリック開始を待つ
    • game.py start-frameでフレーム開始時刻を記録する
    • Finderの選択項目数を読み取り、game.py tickへ渡してフレームをレンダリングする
    • tickが準備したディレクトリ名を受け取り、Finderウィンドウの対象をそのディレクトリへ切り替える
    • game.py sleepで目標フレームレートに合わせ、プレイヤーが負けていなければ繰り返す
  • その後、start-framesleepは統合でき、死亡後の再スタート用ループも追加された

ゲーム完成に必要だった細かな作業

  • ゲーム状態はstate.jsonファイルを作って毎フレーム読み書きする
  • テキストは使用文字の幅が絵文字より狭いため、整列を合わせるためにハードコードされた空白を多用した
  • 上部のスクロールバナーテキストは特に厄介で、一度に何文字表示できるかを推定するread_n_ad_chars関数を作った
  • 作業ディレクトリをAppleScriptに渡す代わりに、first-time-setupのPython呼び出しでテンプレート変数を置換した
  • AppleScriptがPythonの出力を飲み込んでしまうため、ログはファイルに追記して後でcatで確認した
  • 連続する2フレームでタップすると1行分さらに上へジャンプするようにして、ゲーム感覚を改善した

エンジンなしで作るFinderゲームの制約

  • プロトタイプは約90行で、最終コードは約550行だが、そのうち3分の1ほどはボイラープレートや定数
  • ゲームエンジンなしで小さな2D配列にフレーム状態を持たせる方式は、ゲーム全体を頭の中で把握しやすかった
  • 4 FPSと限られた入力は大きな制約だが、同じ方式で別のゲームを作ることも可能に思える
  • FinderでTetrisを作るというアイデアも出ており、難しいが不可能ではない例として残っている

1件のコメント

 
GN⁺ 2023-10-09
Hacker News のコメント
  • 言葉を失うほど驚いた。自分では既成概念にとらわれない人間だと思っていたのに、この記事を読む前だったら不可能だと言っていた気がする
    週末に自分への挑戦として作ったプロジェクトが、本当に可能なのかを突き詰めていく様子を見るのは本当に楽しいし、記事もとても面白く読めた

    • ソフトウェアでは、だから絶対に不可能とは言わない。必要なのは時間と創造力だけ
  • Bob が金曜の午後、ビールを飲みに行くチームについていくために慌ててジャケットとバックパックをつかみ、macOS 上で動く Flappy Birdを起動したままだと気づかなかった
    バンクホリデーを挟んだ週末の間、ノートPCのSSDの書き込み寿命が毎秒4フレームの速度で消費され、火曜日に戻ってきた Bob はSSDがもう動作しないのを目にする

    • 会社で Flappy Dird を走らせるような人が、本当にそんなに慌てる可能性があるのか気になる
    • それでもその程度なら、何年動かしてもドライブは壊れないと期待したい
  • 「これは画像です。もうツイートをどう埋め込めばいいのか分からないんですよね」というやり方のほうが、むしろずっと好み

    • こうすると記事が長く残る。埋め込みはいつでも消えたり壊れたりする可能性がある
    • Twitter をブロックしてからは、埋め込みの場所に何も表示されなかったり、空白領域の真ん中に絵文字だけがレンダリングされたりすることが多いので同意
  • OP が AppleScript の起動速度の問題を、JavaScriptで書き直して解決できるのか気になる
    数年前から標準フレームワークとコマンドラインツールの osascript は AppleScript と JavaScript をほぼ同等にサポートしていて、JavaScript では $ オブジェクトに入っているものがかなり多い

    • まったく知らなかったので試してみたい。5分の間に AppleScript 関連でとても有用なコメントを2つももらえて素晴らしい
  • Python から AppleScript イベントをネイティブに呼び出すなら、py-appscriptを使ってみるのもあり
    https://github.com/hhas/appscript/tree/master/py-appscript
    Ruby 版の Appscript しか使ったことはないが、Python 版も同じように動くなら、AppleScript 自体が不要になるくらい理想的な選択肢に見える

    • こういうものがあるか確認しようとすら思っていなかったので、ぜひ見てみる。ただ、コアループが AppleScript であることから来る楽しさも少しある
  • こういうあってはいけない場所にあるゲームが好きなら、もっと馬鹿げたプロジェクトもある
    Fontemon: フォントの中のゲーム https://www.coderelay.io/fontemon.html
    Dungeons & Directories: ファイルブラウザ内のテキストアドベンチャー https://wheybags.com/dungeons_and_directories/ — これは自分が作った

    • どちらもリンクありがとう。ファイルシステムを選んだところも面白いし、数週間後に Windows マシンに戻ったら見てみたい
      フォントゲームも少しいじってみたし、フォントを書くのがどんな感じかを知るために Hexagone[1] を作った。どんなゲームを作るかの確信は得られなかったけれど、こういう着想が湧くのは楽しい
      [1] http://eieio.games/nonsense/hexagone-converting-hex-to-rgb-w...
    • Defender of the Favicon もある: http://www.p01.org/defender_of_the_favicon/
    • 2つ目の実装方法が独創的。代わりに FUSE で実装することも検討したのか気になる
    • コマンドライン好き向けには rpg-cli もある
      https://github.com/facundoolano/rpg-cli
    • これはいったいどこの異界の魔法なのか分からない
  • 記事全体に流れる「もっと良くできる」という姿勢が本当にいい。すべての開発者がこういう心構えだったらどうだろうと思う
    もちろん全員がそうなら、常にもっと良くできてしまうので何も終わらないし、ときには「これで十分」と言ってくれる人が必要になる

    • その通りで、バランスを取るのは難しい
      こういう趣味プロジェクトでは「もっと良くできる」側を保つのがより簡単。仕事ではたいてい、さらに1日かけて磨き込む価値が、自分たちのやりたい他のすべてのことより大きいかを考えることになる
      でも Flappy Dird では、プロジェクトに満足するか、これ以上ゲームを押し進められないと信じるようになるまで続けたかった
  • タイトルを見たとき、最初は Finder ウィンドウのアイコン表示で、AppleScript を使ってアイコンをスプライトのように動かす方式だと思った
    実際にはリスト表示で実装されていて、最初はディレクトリの「最後に開いた日付」をポーリングする方式だったので驚いたし、確かに創造的なアプローチだ

  • 小さなゲームを可能な限りあらゆる場所に持ち込む、この記事の精神が本当に楽しい
    昔の GNOME で使えた Fortune Teller fish のタスクバーウィジェットを思い出す
    まったく別の話として、「MacOS Finder」という大文字表記を見て、最初は MacOS 8 や 9 用の拡張機能かと思ったが、実際の結果はそれよりずっと楽しかった

  • 次の挑戦は、Finder の中でライフゲームを実行し、その中で Finder を実行する Mac をエミュレートすること

    • MacOS エミュレーションは抜きにしてライフゲームだけなら、DS_Store ファイルで可能かもしれないのか気になる
      最初にフォルダをパターン通りに作って右クリックで整列すると、グリッド上に配置される。その後スクリプトが DS_Store ファイルからフォルダ位置を読み取り、次のフレームの新しい位置に更新したり、必要ならフォルダを作成・削除したりする方式