Я, императивный программист на Java, хотел бы понять, как создать простую версию Space Invaders, основанную на принципах проектирования функционального программирования (в частности, ссылочной прозрачности). Однако каждый раз, когда я пытаюсь придумать дизайн, я теряюсь в муре чрезвычайной изменчивости, той же изменчивости, которой избегают пуристы функционального программирования.
В качестве попытки освоить функциональное программирование я решил попытаться создать очень простую 2D интерактивную игру Space Invader (обратите внимание на отсутствие множественного числа) в Scala с использованием LWJGL . Вот требования к основной игре:
Пользовательский корабль внизу экрана перемещается влево и вправо с помощью клавиш «A» и «D» соответственно
Пуля пользовательского корабля, выпущенная прямо вверх, активируется пробелом с минимальной паузой между выстрелами, чтобы быть .5 секунд
Пуля инопланетного корабля, выпущенная прямо вниз, активируется случайным образом от 0,5 до 1,5 секунд между выстрелами
В оригинальной игре намеренно не учтены инопланетяне WxH, разрушаемые защитные барьеры х3, высокоскоростной корабль с блюдцем в верхней части экрана.
Хорошо, теперь к актуальной проблемной области. Для меня все детерминированные части очевидны. Это недетерминированные части, которые, кажется, блокируют мою способность думать, как приблизиться. Детерминированные части - это траектория полета пули, как только они существуют, непрерывное движение инопланетянина и взрыв из-за удара по одному или обоим корабля игрока или пришельца. Недетерминированные части (для меня) обрабатывают поток пользовательского ввода, обрабатывают выборку случайного значения для определения стрельбы инопланетными пулями и обработку вывода (как графики, так и звука).
Я могу сделать (и сделал) много такого типа разработки игр на протяжении многих лет. Однако все это было из императивной парадигмы. И LWJGL даже предоставляет очень простую Java-версию Space invaders (о которой я начал переходить на Scala, используя Scala как Java-без точек с запятой).
Вот некоторые ссылки, которые обсуждают эту область, о которой никто, похоже, не имел непосредственного отношения к идеям таким образом, чтобы человек, пришедший из Java / императивного программирования, мог понять:
Похоже, что есть некоторые идеи в играх Clojure / Lisp и Haskell (с исходным кодом). К сожалению, я не в состоянии читать / интерпретировать код в ментальных моделях, которые имеют какой-то смысл для моего простого мышления в Java.
Я так взволнован возможностями, которые предлагает FP, я могу просто попробовать возможности многопоточной масштабируемости. Я чувствую, что смог бы воплотить в жизнь что-то простое, например, модель время + событие + случайность для Space Invader, разделяя детерминированные и недетерминированные части в правильно спроектированной системе без превращения ее в нечто вроде продвинутой математической теории. ; то есть Ямпа, я бы поставил. Если изучение уровня теории Yampa, по-видимому, требует успешного создания простых игр, необходимо, тогда накладные расходы на приобретение всей необходимой учебной и концептуальной основы значительно перевесят мое понимание преимуществ FP (по крайней мере для этого упрощенного эксперимента по обучению). ).
Любая обратная связь, предлагаемые модели, предлагаемые методы подхода к проблемной области (более конкретные, чем общие положения, охватываемые Джеймсом Хейгом), будут высоко оценены.
источник
Ответы:
Идиоматическая реализация Space Invaders для Scala / LWJGL не будет выглядеть так же, как реализация на Haskell / OpenGL. Написание реализации на Haskell может быть лучшим упражнением, на мой взгляд. Но если вы хотите придерживаться Scala, вот несколько идей о том, как написать его в функциональном стиле.
Старайтесь использовать только неизменные объекты. Вы можете иметь
Game
объект , который держитPlayer
, АSet[Invader]
(не забудьте использоватьimmutable.Set
) и т.д. Дайте (он может также принимать и т.д.), и дать другим классам подобные методы.Player
update(state: Game): Player
depressedKeys: Set[Int]
Для случайности
scala.util.Random
не является неизменным, как у ХаскеллаSystem.Random
, но вы можете сделать свой собственный неизменный генератор. Этот неэффективен, но он демонстрирует идею.Для ввода с клавиатуры / мыши и рендеринга нет способа обойти нечистые функции. Они также нечисты в Haskell, они просто инкапсулированы
IO
и т. Д., Так что ваши действительные функциональные объекты технически чисты (они сами не читают и не записывают состояние, они описывают подпрограммы, которые выполняют, и система времени выполнения выполняет эти подпрограммы) ,Только не помещайте код ввода / вывода в ваши неизменяемые объекты, такие как
Game
,Player
иInvader
. Вы можете датьPlayer
наrender
метод, но он должен выглядетьК сожалению, это не очень подходит для LWJGL, так как он основан на состоянии, но вы можете построить свои собственные абстракции поверх него. У вас может быть
ImmutableCanvas
класс, содержащий AWTCanvas
, и егоblit
(и другие методы) могут клонировать базовый объектCanvas
, передавать егоDisplay.setParent
, затем выполнять рендеринг и возвращать новыйCanvas
(в вашей неизменяемой оболочке).Обновление : вот некоторый код Java, показывающий, как я поступил бы по этому поводу. (Я написал бы почти такой же код в Scala, за исключением того, что встроенный неизменяемый набор и несколько циклов for-each можно заменить картами или сгибами.) Я сделал игрока, который перемещается и запускает пули, но я не добавлял врагов, так как код уже становился длинным. Я сделал почти все, что копировал при записи - я думаю, что это самая важная концепция.
источник
args
чтобы код игнорировал аргументы. Извините за ненужную путаницу.GameState
копии будут такими дорогостоящими, даже если по нескольким тикам делаются по несколько, так как они ~ 32 байта каждая. Но копированиеImmutableSet
может быть дорогостоящим, если много пуль живы одновременно. Мы могли бы заменитьImmutableSet
на древовидную структуру,scala.collection.immutable.TreeSet
чтобы уменьшить проблему.ImmutableImage
что еще хуже, поскольку при модификации он копирует большой растр. Есть некоторые вещи, которые мы могли бы сделать, чтобы уменьшить эту проблему, но я думаю, что было бы наиболее практичным просто написать код рендеринга в императивном стиле (даже программисты на Haskell обычно делают это).Ну, вы сдерживаете свои усилия, используя LWJGL - ничего против этого, но это навязывает нефункциональные идиомы.
Однако ваше исследование соответствует тому, что я бы порекомендовал. «События» хорошо поддерживаются в функциональном программировании через такие понятия, как функциональное реактивное программирование или программирование потока данных. Вы можете попробовать Reactive , библиотеку FRP для Scala, чтобы узнать, может ли она содержать ваши побочные эффекты.
Кроме того, возьмите страницу из Haskell: используйте монады, чтобы инкапсулировать / изолировать побочные эффекты. Смотрите состояние и IO монады.
источник
Да, IO является недетерминированным и «все о» побочных эффектах. Это не проблема в не чистом функциональном языке, таком как Scala.
Вы можете рассматривать выход генератора псевдослучайных чисел как бесконечную последовательность (
Seq
в Scala)....
Где, в частности, вы видите необходимость изменчивости? Если я могу предвидеть, вы можете думать о своих спрайтах как о положении в пространстве, которое меняется со временем. Вам может быть полезно подумать о «молнии» в таком контексте: http://scienceblogs.com/goodmath/2010/01/zippers_making_functional_upda.php
источник