Ich, ein Java-Imperativ-Programmierer, möchte verstehen, wie eine einfache Version von Space Invaders basierend auf den Entwurfsprinzipien der funktionalen Programmierung (insbesondere der referenziellen Transparenz) generiert wird. Jedes Mal, wenn ich versuche, an ein Design zu denken, verliere ich mich jedoch im Morast extremer Veränderlichkeit, der gleichen Veränderlichkeit, die von Puristen der funktionalen Programmierung gemieden wird.
Um die funktionale Programmierung zu erlernen, habe ich mich entschlossen, ein sehr einfaches interaktives 2D-Spiel, Space Invader (beachten Sie den fehlenden Plural), in Scala mithilfe der LWJGL zu erstellen . Hier sind die Anforderungen für das Grundspiel:
Das Benutzerschiff am unteren Bildschirmrand wurde mit den Tasten "A" und "D" nach links und rechts bewegt
Die von der Leertaste aktiv abgefeuerte Kugel des Benutzerschiffs wird mit einer minimalen Pause zwischen den Schüssen von 0,5 Sekunden aktiviert
Die Kugel eines außerirdischen Schiffes wurde direkt nach unten abgefeuert und durch eine zufällige Zeit von 0,5 bis 1,5 Sekunden zwischen den Schüssen aktiviert
Dinge, die absichtlich aus dem ursprünglichen Spiel weggelassen wurden, sind WxH-Aliens, abbaubare Verteidigungsbarrieren x3 und ein Hochgeschwindigkeits-Untertassenschiff am oberen Bildschirmrand.
Okay, jetzt zur eigentlichen Problemdomäne. Für mich sind alle deterministischen Teile offensichtlich. Es sind die nicht deterministischen Teile, die meine Fähigkeit zu blockieren scheinen, zu überlegen, wie ich mich nähern soll. Die deterministischen Teile sind die Flugbahn der Kugel, sobald sie existiert, die kontinuierliche Bewegung des Außerirdischen und die Explosion aufgrund eines Treffers auf einem (oder beiden) Schiff des Spielers oder dem Außerirdischen. Die nicht deterministischen Teile (für mich) behandeln den Strom von Benutzereingaben, das Abrufen eines zufälligen Werts zum Bestimmen von Alien-Geschossen und das Behandeln der Ausgabe (sowohl Grafik als auch Ton).
Ich kann (und habe) im Laufe der Jahre viele dieser Arten von Spieleentwicklungen durchführen. Alles war jedoch vom imperativen Paradigma. Und LWJGL bietet sogar eine sehr einfache Java-Version von Space Invaders (von denen ich mit Scala als Java ohne Semikolons zu Scala gewechselt bin).
Hier sind einige Links, die sich mit diesem Bereich befassen, von denen keiner direkt mit den Ideen so umgegangen zu sein scheint, wie es eine Person aus der Java / Imperative-Programmierung verstehen würde:
Es scheint, dass es einige Ideen in den Clojure / Lisp- und Haskell-Spielen gibt (mit Quelle). Leider bin ich nicht in der Lage, den Code in mentale Modelle zu lesen / zu interpretieren, die für mein vereinfachtes Java-Imperativhirn keinen Sinn ergeben.
Ich bin so begeistert von den Möglichkeiten, die FP bietet, dass ich nur die Multithread-Skalierbarkeitsfunktionen probieren kann. Ich habe das Gefühl, ich könnte herausfinden, wie etwas so Einfaches wie das Zeit + Ereignis + Zufallsmodell für Space Invader implementiert werden kann, indem die deterministischen und nicht deterministischen Teile in einem richtig entworfenen System getrennt werden, ohne dass es sich in eine Art fortgeschrittene mathematische Theorie verwandelt ;; dh Yampa, ich wäre bereit. Wenn das Erlernen des theoretischen Niveaus, das Yampa zu benötigen scheint, um einfache Spiele erfolgreich zu generieren, erforderlich ist, überwiegt der Aufwand für den Erwerb aller erforderlichen Schulungen und konzeptionellen Rahmenbedingungen mein Verständnis der Vorteile von FP bei weitem (zumindest für dieses stark vereinfachte Lernexperiment) ).
Alle Rückmeldungen, vorgeschlagenen Modelle und vorgeschlagenen Methoden zur Annäherung an den Problembereich (spezifischer als die von James Hague behandelten allgemeinen Aspekte) wären sehr willkommen.
quelle
Antworten:
Eine idiomatische Scala / LWJGL-Implementierung von Space Invaders würde nicht so sehr wie eine Haskell / OpenGL-Implementierung aussehen. Das Schreiben einer Haskell-Implementierung könnte meiner Meinung nach eine bessere Übung sein. Wenn Sie jedoch bei Scala bleiben möchten, finden Sie hier einige Ideen, wie Sie es in funktionalem Stil schreiben können.
Versuchen Sie, nur unveränderliche Objekte zu verwenden. Sie könnten ein
Game
Objekt haben, das aPlayer
, a enthältSet[Invader]
(verwenden Sie unbedingtimmutable.Set
) usw. Geben SiePlayer
einupdate(state: Game): Player
(es könnte auch dauerndepressedKeys: Set[Int]
usw.) und geben Sie den anderen Klassen ähnliche Methoden.Zufällig
scala.util.Random
ist nicht unveränderlich wie bei HaskellSystem.Random
, aber Sie könnten Ihren eigenen unveränderlichen Generator bauen. Dieser ist ineffizient, zeigt aber die Idee.Für die Eingabe und das Rendern von Tastatur / Maus führt kein Weg daran vorbei, unreine Funktionen aufzurufen. Sie sind auch in Haskell unrein, sie sind nur in
IO
usw. eingekapselt, so dass Ihre tatsächlichen Funktionsobjekte technisch rein sind (sie lesen oder schreiben selbst keinen Status, sie beschreiben Routinen, die dies tun, und das Laufzeitsystem führt diese Routinen aus). .Nur nicht setzen I / O - Code in Ihren unveränderlichen Objekten wie
Game
,Player
undInvader
. Sie könnenPlayer
einerender
Methode angeben, aber sie sollte so aussehenLeider passt dies nicht gut zu LWJGL, da es so zustandsbasiert ist, aber Sie können Ihre eigenen Abstraktionen darauf aufbauen. Sie könnten eine
ImmutableCanvas
Klasse haben, die eine AWT enthältCanvas
, und ihreblit
(und andere Methoden) könnten den Basiswert klonenCanvas
, an ihn übergebenDisplay.setParent
, dann das Rendern durchführen und die neue zurückgebenCanvas
(in Ihrem unveränderlichen Wrapper).Update : Hier ist ein Java-Code, der zeigt, wie ich das machen würde. (Ich hätte in Scala fast den gleichen Code geschrieben, außer dass ein unveränderlicher Satz eingebaut ist und einige für jede Schleife durch Karten oder Falten ersetzt werden könnten.) Ich habe einen Spieler gemacht, der sich bewegt und Kugeln abfeuert, aber ich fügte keine Feinde hinzu, da der Code bereits lang wurde. Ich habe fast alles kopiert - ich denke, das ist das wichtigste Konzept.
quelle
args
wenn der Code Argumente ignoriert. Entschuldigung für die unnötige Verwirrung.GameState
Kopien so teuer wären, obwohl mehrere pro Tick gemacht werden, da sie jeweils ~ 32 Bytes sind. Das Kopieren desImmutableSet
s kann jedoch teuer sein, wenn viele Kugeln gleichzeitig aktiv sind. Wir könnten durchImmutableSet
eine Baumstruktur ersetzenscala.collection.immutable.TreeSet
, um das Problem zu verringern.ImmutableImage
ist noch schlimmer, da es ein großes Raster kopiert, wenn es geändert wird. Es gibt einige Dinge, die wir tun könnten, um dieses Problem zu verringern, aber ich denke, es wäre am praktischsten, nur Rendering-Code im imperativen Stil zu schreiben (selbst Haskell-Programmierer tun dies normalerweise).Nun, Sie behindern Ihre Bemühungen durch die Verwendung von LWJGL - nichts dagegen, aber es wird nicht funktionierende Redewendungen auferlegen.
Ihre Forschung entspricht jedoch dem, was ich empfehlen würde. "Ereignisse" werden in der funktionalen Programmierung durch Konzepte wie funktionale reaktive Programmierung oder Datenflussprogrammierung gut unterstützt. Sie können Reactive , eine FRP-Bibliothek für Scala, ausprobieren, um festzustellen , ob sie Ihre Nebenwirkungen enthalten kann.
Nehmen Sie auch eine Seite aus Haskell heraus: Verwenden Sie Monaden, um Nebenwirkungen zu kapseln / zu isolieren. Siehe Staats- und E / A-Monaden.
quelle
Ja, IO ist nicht deterministisch und "alles über" Nebenwirkungen. In einer nicht reinen Funktionssprache wie Scala ist das kein Problem.
Sie können die Ausgabe eines Pseudozufallszahlengenerators als unendliche Folge (
Seq
in Scala) behandeln....
Wo sehen Sie insbesondere die Notwendigkeit der Veränderlichkeit? Wenn ich damit rechnen darf, könnten Sie sich vorstellen, dass Ihre Sprites eine Position im Raum haben, die sich im Laufe der Zeit ändert. Es kann nützlich sein, in einem solchen Kontext über "Reißverschlüsse" nachzudenken: http://scienceblogs.com/goodmath/2010/01/zippers_making_functional_upda.php
quelle