Flappy Dird: Flappy Bird im macOS Finder umgesetzt
(eieio.games)- Flappy Dird ist eine experimentelle Umsetzung, die Flappy Bird ausführt, indem sie die Dateiliste des macOS Finder als Bildschirm sowie Dateiauswahl- und Öffnen-Aktionen als Eingabe nutzt
- Die Kernidee geht davon aus, den nur im Finder aktualisierten Wert Date Last Opened eines Verzeichnisses wie einen Button auszulesen und den Bildschirm durch Umbenennen von Symlink-Dateinamen zu zeichnen
- Einfaches Rendering per Dateinamenänderung war langsam und verursachte starkes Screen Tearing; verbessert wurde es durch Double Buffering, bei dem per AppleScript das Zielverzeichnis des Finder-Fensters gewechselt wird
- Eingaben per Doppelklick waren wegen der auf Sekunden begrenzten Zeitstempelauflösung oberhalb von 2 FPS unzuverlässig; später wurde auf das Auslesen der ausgewählten Elemente im Finder umgestellt
- In der finalen Architektur läuft die Schleife in AppleScript, während Python Spielzustand und Rendering übernimmt; sie arbeitet mit 4 FPS, doch verpasste Eingaben und Finder-typische Einschränkungen bleiben bestehen
Den Finder als Spielbildschirm nutzen
- Flappy Dird ist ein Finder-basiertes Spiel, dessen Code im GitHub-Repository eingesehen und selbst ausgeführt werden kann
- Das Spiel enthält eine Anleitung, Highscore-Tracking und scrollende Bannerwerbung
- Nutzer starten das Spiel per Doppelklick und lassen den Vogel springen, indem sie im Finder-Fenster eine beliebige Datei auswählen
- Die Ausführungsgeschwindigkeit liegt bei etwa 4 Frames pro Sekunde; deutlich schneller zu laufen ist schwierig, und Eingaben gehen gelegentlich verloren
Date Last Opened wie einen Button verwenden
- Ausgangspunkt war die Tatsache, dass der Finder für Verzeichnisse ein Feld Date Last Opened besitzt
- Beobachtet wurden drei Verhaltensweisen
- Wenn man per
cdin ein Verzeichnis wechselt, wird der Zeitstempel nicht aktualisiert - Wenn man im Finder einen Symlink auf ein Verzeichnis doppelklickt, wird der Zeitstempel aktualisiert
- Mit
mdlslässt sich dieser Wert mit sekundengenauer Präzision auslesen
- Wenn man per
- Diese Eigenschaft ließ sich nutzen, um im Finder einen Button zu bauen
- Im Verzeichnis
dirwird ein Symlinkbuttonangelegt, der aufdirzeigt - Beim Start wird die letzte Öffnungszeit von
dirgelesen - Der Zeitstempel wird wiederholt abgefragt; ändert sich der Wert, wird das als Eingabe verarbeitet
- Ohne die Finder-Position zu ändern, wird
buttongeöffnet, um die letzte Öffnungszeit vondirzu verändern
- Im Verzeichnis
Einen 15x15-Bildschirm mit Emoji-Dateinamen zeichnen
- ASCII-Art mit der Standardschrift des Finders passgenau auszurichten war schwierig; genutzt wurde stattdessen, dass Emojis in Dateinamen mit gleichmäßiger Breite dargestellt werden
- Der Prototyp legte in
dir15 untergeordnete Symlinks an, die wiederum aufdirzeigten, und änderte die Namen dieser Links in Emoji-Zeilen, um den Bildschirm aufzubauen - Die Rendering-Funktion nimmt
bird_y_pos,pipe_locationsundframeentgegen und erzeugt ein 15x15-Emoji-Raster - In jedem Frame werden die Namen aller Symlinks im Verzeichnis in die jeweiligen Zeilen des Emoji-Rasters geändert
- Außerdem war ein Hack nötig, der die Reihenfolge der Umbenennungen anpasste, damit der Finder nach
Date Modifiedsortiert - Dieser Ansatz funktionierte, war aber sehr langsam und verursachte starkes Screen Tearing
AppleScript und Double Buffering
- Auf der Suche nach einer Möglichkeit, die Finder-Dateiliste zu aktualisieren und Screen Tearing zu reduzieren, wurde der AppleScript-Befehl
tell application "Finder" to tell front window to update every itemverwendet - Dieser Befehl half etwas, doch das Tearing blieb; anschließend führte der Weg dazu, den Finder direkt per AppleScript zu steuern
- Es gab mehrere Kandidaten für Double Buffering
- Ein Ansatz, den Verzeichnisinhalt atomar über Symlinks auszutauschen, passte nicht, weil der Finder den Inhalt von Symlink-Verzeichnissen nicht aufklappen kann
- Ein Ansatz mit zwei Verzeichnissen, die gegenseitig aufeinander zeigen, konnte Tearing reduzieren, passte aber nicht zum Spiel, weil Frames nur weiterlaufen konnten, wenn der Nutzer klickte
- Benötigt wurde eine Methode, die das vom Finder angezeigte Verzeichnis ändert, ohne die „letzte Öffnungszeit“ zu verändern
- Die Lösung war, das Zielverzeichnis des Finder-Fensters per AppleScript mit
tell application "Finder" to set target of front Finder window to ("PATH" POSIX file)zu wechseln - Nachdem Double Buffering auf diese Weise umgesetzt war, lief das Spiel bei 1 FPS flüssig, war aber weiterhin langsam
Wechsel der Eingabemethode: vom Doppelklick zur Dateiauswahl
- Doppelklick-Eingaben stießen oberhalb von 2 FPS an Grenzen, weil die Zeitstempelauflösung nur sekundenweise war
- Da sich per AppleScript die ausgewählten Elemente im Finder-Fenster auslesen lassen, wurde die Eingabe auf „beliebige Datei auswählen“ umgestellt
- Die neue Spielschleife hatte folgende Struktur
- Warten, bis der Nutzer doppelklickt
- In jedem Frame per AppleScript prüfen, ob im aktuellen Fenster eine Datei ausgewählt ist
- Wenn eine Auswahl vorhanden ist oder sich die letzte Öffnungszeit geändert hat, springt der Vogel
- Per AppleScript das vom Finder angezeigte Verzeichnis wechseln
- Für die verbleibende Zeit warten, um die Ziel-Framerate einzuhalten
- Dieser Ansatz erreichte ungefähr 2 FPS, aber ein AppleScript-Aufruf dauerte etwa 0,2 Sekunden, was hohe Fixkosten verursachte
- Selbst bei 2 FPS konnte eine Eingabe verpasst werden, wenn der Nutzer nach der Eingabeprüfung eine Datei auswählte
Die Hauptschleife nach AppleScript verlagern
- Profiling zeigte, dass ein AppleScript, das die Zahl
1ins Log schreibt und beendet wird, etwa 0,14 Sekunden benötigte - Später wurde korrigiert, dass diese Messung keine faire Messung der AppleScript-Startzeit war; gemessen an einem leeren AppleScript könnten es eher etwa 0,06 Sekunden sein
- Um die Startkosten von AppleScript pro Frame zu vermeiden, wurde die Hauptschleife nach AppleScript verlagert, während Python Spiellogik und Rendering übernahm
- Die Grundstruktur sieht wie folgt aus
- Mit
game.py awaitauf den Doppelklick des Nutzers zum Start warten - Mit
game.py start-framedie Startzeit des Frames aufzeichnen - Die Anzahl der ausgewählten Finder-Elemente auslesen, an
game.py tickübergeben und den Frame rendern - Den von
tickvorbereiteten Verzeichnisnamen entgegennehmen und das Ziel des Finder-Fensters auf dieses Verzeichnis setzen - Mit
game.py sleepdie Ziel-Framerate einhalten und wiederholen, solange der Spieler nicht verloren hat
- Mit
- Später konnten
start-frameundsleepzusammengeführt werden, und eine Schleife für den Neustart nach dem Tod wurde ergänzt
Detailarbeiten, die zur Fertigstellung des Spiels nötig waren
- Der Spielzustand wird in einer Datei
state.jsonangelegt und in jedem Frame gelesen und geschrieben - Da die verwendeten Textzeichen schmaler als Emojis waren, wurden viele hartcodierte Abstände verwendet, um die Ausrichtung passend zu machen
- Der scrollende Bannertext am oberen Rand war besonders knifflig; dafür wurde eine Funktion
read_n_ad_charserstellt, die abschätzt, wie viele Zeichen auf einmal angezeigt werden sollen - Statt das Arbeitsverzeichnis an AppleScript zu übergeben, werden in einem Python-Aufruf
first-time-setupTemplate-Variablen ersetzt - Weil AppleScript die Python-Ausgabe verschluckte, wurden Logs per Append in eine Datei geschrieben und später mit
catgeprüft - Um das Spielgefühl zu verbessern, springt der Vogel eine Zeile weiter nach oben, wenn in zwei aufeinanderfolgenden Frames getippt wird
Einschränkungen eines Finder-Spiels ohne Engine
- Der Prototyp umfasste etwa 90 Zeilen, der finale Code etwa 550 Zeilen, von denen ungefähr ein Drittel Boilerplate oder Konstanten sind
- Ohne Game Engine den Frame-Zustand in einem kleinen 2D-Array zu halten, machte es leicht, das gesamte Spiel im Kopf zu behalten
- 4 FPS und eingeschränkte Eingaben sind große Beschränkungen, doch es scheint möglich, mit derselben Methode auch andere Spiele zu bauen
- Auch die Idee, Tetris im Finder zu bauen, kam auf und bleibt als schwieriges, aber möglicherweise machbares Beispiel stehen
1 Kommentare
Hacker-News-Kommentare
Ich war sprachlos, so beeindruckt war ich; und obwohl ich mich selbst für jemanden halte, der outside the box denkt, hätte ich vor dem Lesen dieses Artikels wohl gesagt, dass das unmöglich sei.
Es ist wirklich schön zu sehen, wie jemand bei einem Projekt, das er als Wochenend-Challenge für sich selbst gebaut hat, austestet, ob es tatsächlich machbar ist; der Text war außerdem sehr unterhaltsam zu lesen.
Bob merkt nicht, dass er Flappy Bird auf MacOS laufen gelassen hat, als er am Freitagnachmittag hastig Jacke und Rucksack schnappt, um mit dem Team auf ein Bier zu gehen.
Über das lange Wochenende mit Feiertag verbraucht sich die Schreib-Lebensdauer der Laptop-SSD mit 4 Frames pro Sekunde, und als Bob am Dienstag zurückkommt, stellt er fest, dass die SSD nicht mehr funktioniert.
Mir gefällt die Variante „Das ist ein Bild. Ich weiß jetzt nicht mehr, wie man Tweets einbettet“ sogar deutlich besser.
Ich frage mich, ob OP das Startgeschwindigkeitsproblem von AppleScript lösen könnte, indem er es in JavaScript neu schreibt.
Seit einigen Jahren unterstützen das Standard-Framework und das Kommandozeilentool
osascriptAppleScript und JavaScript nahezu gleichwertig, und in JavaScript steckt im$-Objekt ziemlich viel.Um AppleScript-Events nativ aus Python aufzurufen, könnte man auch py-appscript ausprobieren.
https://github.com/hhas/appscript/tree/master/py-appscript
Ich habe nur die Ruby-Version von Appscript verwendet, aber wenn die Python-Version genauso funktioniert, wirkt sie wie eine ideale Wahl, bei der AppleScript selbst überflüssig werden könnte.
Wenn du solche Spiele an Orten, an denen sie nicht sein sollten, magst, gibt es noch absurdere Projekte.
Fontemon: ein Spiel in einer Schriftart https://www.coderelay.io/fontemon.html
Dungeons & Directories: ein Textadventure im Dateibrowser https://wheybags.com/dungeons_and_directories/ — das habe ich gemacht
Mit dem Font-Spiel habe ich auch ein wenig herumgespielt, und ich habe Hexagone[1] gebaut, um zu sehen, wie es sich anfühlt, eine Schriftart zu schreiben. Ich war mir danach zwar nicht sicher, welches Spiel ich bauen sollte, aber es macht Spaß, dadurch solche Inspiration zu bekommen.
[1] http://eieio.games/nonsense/hexagone-converting-hex-to-rgb-w...
https://github.com/facundoolano/rpg-cli
Ich mag die Haltung „Das geht noch besser“, die sich durch den ganzen Artikel zieht, wirklich sehr. Ich frage mich, wie es wäre, wenn alle Entwickler so eine Einstellung hätten.
Wenn das natürlich alle täten, würde nie etwas fertig, weil es immer noch besser geht; manchmal braucht man jemanden, der sagt: „Das ist gut genug.“
Bei solchen Hobbyprojekten ist es leichter, bei „das geht noch besser“ zu bleiben. Bei der Arbeit überlegt man normalerweise, ob der Nutzen, noch einen Tag zum Feinschliff zu investieren, größer ist als alles andere, was wir tun wollen.
Aber bei Flappy Dird wollte ich weitermachen, bis ich entweder mit dem Projekt zufrieden war oder glaubte, dass ich das Spiel nicht weiter treiben kann.
Vom Titel her dachte ich zuerst, es würde darum gehen, in der Icon-Ansicht eines Finder-Fensters per AppleScript Icons wie Sprites zu bewegen.
Dass es tatsächlich in der Listenansicht umgesetzt ist und anfangs über das Polling des „zuletzt geöffnet“-Datums eines Verzeichnisses funktionierte, hat mich überrascht; das ist definitiv ein kreativer Ansatz.
Ich liebe den Geist dieses Artikels, kleine Spiele an jeden nur möglichen Ort zu bringen.
Er erinnert mich an das Fortune Teller fish-Taskleisten-Widget, das man früher in GNOME nutzen konnte.
Ganz unabhängig davon ließ mich die Großschreibung „MacOS Finder“ zuerst an eine Erweiterung für MacOS 8 oder 9 denken, aber das tatsächliche Ergebnis war viel unterhaltsamer.
Die nächste Challenge: im Finder Game of Life ausführen und darin einen Mac emulieren, auf dem Finder läuft.
Man erstellt zunächst Ordner entsprechend dem Muster und sortiert sie per Rechtsklick, sodass sie im Raster liegen. Danach liest ein Skript die Ordnerpositionen aus der DS_Store-Datei und aktualisiert sie auf die neuen Positionen für den nächsten Frame oder erstellt und löscht bei Bedarf Ordner.