Ja, bezwzględny programista Java, chciałbym zrozumieć, jak wygenerować prostą wersję Space Invaders w oparciu o zasady projektowania programowania funkcjonalnego (w szczególności przezroczystość referencyjną). Jednak za każdym razem, gdy próbuję wymyślić jakiś projekt, gubię się w bagnie ekstremalnej zmienności, tej samej zmienności, której unika funkcjonalny programista.
Próbując nauczyć się programowania funkcjonalnego, postanowiłem stworzyć w Scali bardzo prostą interaktywną grę 2D, Space Invader (zwróć uwagę na brak liczby mnogiej), używając LWJGL . Oto wymagania dotyczące podstawowej gry:
Użytkownik wysyłany na dole ekranu poruszał się w lewo i w prawo odpowiednio za pomocą klawiszy „A” i „D”
Kula statku użytkownika wystrzelona prosto w górę aktywowana spacją z minimalną przerwą między strzałami wynoszącą 0,5 sekundy
Kula statku kosmicznego wystrzelona prosto w dół aktywowana losowym czasem od 0,5 do 1,5 sekundy między strzałami
Rzeczy, które celowo pominięto w oryginalnej grze, to kosmici WxH, degradowalne bariery obronne x3, szybki statek spodka u góry ekranu.
OK, teraz do faktycznej domeny problemów. Dla mnie wszystkie części deterministyczne są oczywiste. To niedeterministyczne części wydają się blokować moją zdolność do zastanowienia się, jak podejść. Deterministyczne części to trajektoria pocisku, gdy już istnieją, ciągły ruch kosmity i eksplozja spowodowana trafieniem jednego (lub obu) statku gracza lub kosmity. Części niedeterministyczne (według mnie) obsługują strumień danych wejściowych użytkownika, pobierają losową wartość w celu określenia wystrzeliwania pocisków przez kosmitów i obsługi wyjścia (zarówno grafiki, jak i dźwięku).
Mogę robić (i robiłem) wiele tego rodzaju rozwoju gier na przestrzeni lat. Wszystko to jednak wynikało z paradygmatu imperatywu. A LWJGL zapewnia nawet bardzo prostą wersję Java najeźdźców kosmicznych (z których zacząłem przenosić się do Scali, używając Scali jako języka Java bez średników).
Oto kilka linków, które mówią o tym obszarze, z których żaden nie wydaje się bezpośrednio zajmować pomysłami w sposób zrozumiały dla osoby pochodzącej z programowania Java / Imperative:
Wygląda na to, że w grach Clojure / Lisp i Haskell są pewne pomysły (ze źródłem). Niestety, nie jestem w stanie odczytać / zinterpretować kodu w modelach mentalnych, które mają sens dla mojego prostego mózgu imperatywnego Java.
Jestem tak podekscytowany możliwościami oferowanymi przez FP, że po prostu próbuję możliwości wielowątkowej skalowalności. Wydaje mi się, że potrafiłem sobie wyobrazić, jak można wdrożyć coś tak prostego jak model czas + zdarzenie + losowość dla Space Invader, segregując deterministyczne i niedeterministyczne części w odpowiednio zaprojektowanym systemie, bez przekształcania się w coś, co wydaje się zaawansowaną teorią matematyczną ; tj. Yampa, byłbym ustawiony. Jeśli uczenie się poziomu teorii, który wydaje się wymagać od Yampa, aby z powodzeniem generować proste gry, jest konieczne, wówczas narzut związany z uzyskaniem wszystkich niezbędnych szkoleń i ram koncepcyjnych znacznie przewyższy moje rozumienie korzyści płynących z FP (przynajmniej w przypadku tego zbyt uproszczonego eksperymentu uczenia się ).
Wszelkie informacje zwrotne, proponowane modele, sugerowane metody podejścia do dziedziny problemowej (bardziej szczegółowe niż ogólne informacje objęte przez Jamesa Hague'a) byłyby bardzo mile widziane.
źródło
Odpowiedzi:
Idiomatyczna implementacja Scala / LWJGL Space Invaders nie wyglądałaby tak bardzo jak implementacja Haskell / OpenGL. Moim zdaniem, lepszym ćwiczeniem może być napisanie implementacji Haskell. Ale jeśli chcesz trzymać się Scali, oto kilka pomysłów, jak napisać ją w funkcjonalnym stylu.
Staraj się używać tylko niezmiennych obiektów. Możesz mieć
Game
obiekt, który zawiera aPlayer
, aSet[Invader]
(pamiętaj, aby użyćimmutable.Set
), itd. DajPlayer
anupdate(state: Game): Player
(może również zająćdepressedKeys: Set[Int]
itp.), I daj innym klasom podobne metody.Losowość
scala.util.Random
nie jest niezmienna jak w przypadku HaskellaSystem.Random
, ale można stworzyć własny, niezmienny generator. Ten jest nieefektywny, ale pokazuje pomysł.W przypadku wprowadzania i renderowania za pomocą klawiatury / myszy nie ma mowy o wywoływaniu nieczystych funkcji. Są one również nieczyste w Haskell, są po prostu zamknięte w
IO
itp., Dzięki czemu rzeczywiste obiekty funkcji są technicznie czyste (same nie czytają ani nie piszą stanu, opisują procedury, które je wykonują, a system wykonawczy wykonuje te procedury) .Po prostu nie umieścić I / O w swoim kod niezmiennych obiektów takich jak
Game
,Player
iInvader
. Można daćPlayer
sięrender
metody, ale to powinno wyglądaćNiestety nie pasuje to do LWJGL, ponieważ jest tak oparte na stanie, ale można na nim budować własne abstrakcje. Możesz mieć
ImmutableCanvas
klasę, która zawiera AWTCanvas
, a jejblit
(i inne metody) mogą sklonować bazęCanvas
, przekazać jąDisplay.setParent
, a następnie wykonać renderowanie i zwrócić nowąCanvas
(w niezmiennym opakowaniu).Aktualizacja : Oto kod Java pokazujący, jak bym sobie z tym poradził. (Napisałbym prawie taki sam kod w Scali, z wyjątkiem tego, że wbudowany jest niezmienny zestaw i kilka pętli dla każdej z nich można zastąpić mapami lub pasami.) Stworzyłem gracza, który porusza się i strzela pociskami, ale ja nie dodawał wrogów, ponieważ kod był już długi. Zrobiłem prawie wszystko, co napisałem na piśmie - myślę, że to najważniejsza koncepcja.
źródło
args
jeśli kod ignoruje argumenty. Przepraszam za niepotrzebne zamieszanie.GameState
kopie byłyby tak kosztowne, mimo że po jednym tiku wykonanych jest kilka, ponieważ każdy ma ~ 32 bajty. Ale kopiowanieImmutableSet
s może być kosztowne, jeśli wiele pocisków żyje w tym samym czasie. Możemy zastąpićImmutableSet
strukturę drzewa,scala.collection.immutable.TreeSet
aby zmniejszyć problem.ImmutableImage
jest jeszcze gorzej, ponieważ kopiuje duży raster po zmodyfikowaniu. Jest kilka rzeczy, które moglibyśmy zrobić, aby zmniejszyć ten problem, ale myślę, że najbardziej praktyczne byłoby napisanie kodu renderującego w trybie rozkazującym (nawet programiści Haskell zwykle to robią).Cóż, hamujesz swoje wysiłki, używając LWJGL - nic przeciwko temu, ale narzuci to niefunkcjonalne idiomy.
Twoje badania są jednak zgodne z tym, co zaleciłbym. „Zdarzenia” są dobrze wspierane w programowaniu funkcjonalnym poprzez pojęcia takie jak funkcjonalne programowanie reaktywne lub programowanie przepływu danych. Możesz wypróbować Reactive , bibliotekę FRP dla Scali, aby sprawdzić, czy może zawierać efekty uboczne.
Wyjmij też stronę z Haskell: używaj monad do zamykania / izolowania efektów ubocznych. Zobacz monady stanowe i we / wy.
źródło
Tak, IO jest niedeterministyczna i „wszystko o” skutkach ubocznych. Nie jest to problemem w nieczystym języku funkcjonalnym, takim jak Scala.
Możesz traktować wyjście generatora liczb pseudolosowych jako sekwencję nieskończoną (
Seq
w Scali)....
Gdzie w szczególności widzisz potrzebę zmienności? Jeśli mogę przewidzieć, możesz pomyśleć o swoich duszkach jako o pozycji w przestrzeni, która zmienia się w czasie. Przydatne może być myślenie o „zamkach błyskawicznych” w takim kontekście: http://scienceblogs.com/goodmath/2010/01/zippers_making_functional_upda.php
źródło