Функциональная реактивная F# - сохранение состояний в играх
Я студент, в настоящее время изучающий парадигму функциональной реактивности с использованием F#. Это принципиально новая точка зрения для меня. Вчера я узнал о создании простой игры в пинг-понг с использованием этой парадигмы. Идея, которую я понимаю до сих пор, такова: мы думаем о ценностях как о функциях времени. В чистом виде это без гражданства. Тем не менее, мне нужно запомнить положение мяча (или состояние). Поэтому я всегда передаю текущую позицию мяча в качестве параметра глобальной функции.
Если говорить о небольших, более сложных играх, таких как Space Invaders, у нас будет много состояний (положение инопланетян, текущее здоровье пришельцев, количество оставшихся бомб и т. Д.)
Есть ли элегантный / лучший способ решить эту проблему? Всегда ли мы храним состояния на верхнем уровне? Все ли текущие состояния должны быть заданы в качестве дополнительного входного аргумента глобальной функции?
Кто-нибудь может объяснить это, используя простой пример на F#? Большое спасибо.
5 ответов
Есть несколько способов сделать FRP, и это активная область исследований. То, что лучше всего, может во многом зависеть от деталей того, как вещи взаимодействуют друг с другом, и в будущем могут появиться новые и лучшие методы.
В общих чертах, идея состоит в том, чтобы иметь поведение, которое является функцией времени, вместо обычных ценностей (как вы сказали). Поведения могут быть определены с точки зрения других поведений, и могут быть определены для переключения между другими поведениями, когда происходят определенные события.
В вашем примере вам обычно не нужно запоминать положение мяча с помощью аргументов (но для некоторых видов FRP вы можете это сделать). Вместо этого вы можете просто иметь поведение:ballPos : time -> (float * float)
Это может иметь глобальную область действия, или для более крупной программы может быть лучше иметь локальную область действия со всеми ее использованием в этой области.
По мере того, как все усложняется, поведение будет определяться все более сложными способами, зависеть от других поведений и событий, включая рекурсивные зависимости, которые обрабатываются по-разному в разных средах FRP. В F# для рекурсивных зависимостей я бы ожидал, что вам понадобится let rec
включая все вовлеченные поведения. Тем не менее, они могут быть организованы в структуры - на верхнем уровне вы можете иметь:
type alienInfo = { pos : float*float; hp : float }
type playerInfo = { pos : float*float; bombs : int }
let rec aliens : time -> alienInfo array = // You might want laziness here.
let behaviours = [| for n in 1..numAliens ->
(alienPos player n, alienHP player n) |]
fun t -> [| for (posBeh, hpBeh) in behaviours ->
{pos=posBeh t; hp=hpBeh t} |] // You might want laziness here.
and player : time -> playerInfo = fun t ->
{ pos=playerPos aliens t; bombs=playerBombs aliens t}
И затем можно определить поведение для alienPos, alienHP с зависимостями от игрока и playerPos, playerBombs можно определить с зависимостями от пришельцев.
В любом случае, если вы сможете дать более подробную информацию о том, какой тип FRP вы используете, вам будет проще дать более конкретный совет. (А если вам нужен совет по какому типу - лично я бы порекомендовал прочитать: http://conal.net/papers/push-pull-frp/push-pull-frp.pdf)
У меня нет опыта реактивного программирования под F#, но проблема глобального состояния в чисто функциональных системах довольно распространена и имеет довольно элегантное решение: монады.
Хотя сами монады в основном используются в Haskell, основная концепция превратила их в F# в качестве выражений вычислений.
Идея состоит в том, что вы на самом деле не меняете состояния, а просто описываете переходы состояния, то есть как создавать новые состояния. Само состояние может быть полностью скрыто в программе. Используя специальный монадический синтаксис, вы можете писать чистые, но с сохранением состояния программ почти обязательно.
Принимая (модифицированную) реализацию из этого источника, State
Монада может выглядеть так
let (>>=) x f =
(fun s0 ->
let a,s = x s0
f a s)
let returnS a = (fun s -> a, s)
type StateBuilder() =
member m.Delay(f) = f()
member m.Bind(x, f) = x >>= f
member m.Return a = returnS a
member m.ReturnFrom(f) = f
let state = new StateBuilder()
let getState = (fun s -> s, s)
let setState s = (fun _ -> (),s)
let runState m s = m s |> fst
Итак, давайте приведем пример: мы хотим написать функцию, которая может записывать значения в журнал (просто список) во время работы. Поэтому мы определяем
let writeLog x = state {
let! oldLog = getState // Notice the ! for monadic computations (i.e. where the state is involved)
do! setState (oldLog @ [x]) // Set new state
return () // Just return (), we only update the state
}
В state
Теперь мы можем использовать это в императивном синтаксисе, не обрабатывая список журналов вручную.
let test = state {
let k = 42
do! writeLog k // It's just that - no log list we had to handle explicitly
let b = 2 * k
do! writeLog b
return "Blub"
}
let (result, finalState) = test [] // Run the stateful computation (starting with an empty list)
printfn "Result: %A\nState: %A" result finalState
Тем не менее, здесь все чисто функционально;)
Томас рассказал о реактивном программировании на F#. Многие концепции должны применяться в вашем случае.
Elm - это современная реализация FRP. Для моделирования динамических коллекций, которые распространены в играх, таких как Space Invaders, он содержит библиотеку Automaton, основанную на концепциях FRP со стрелками. Вы обязательно должны проверить это.