Функциональная реактивная 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#. Многие концепции должны применяться в вашем случае.

Может быть, вы захотите взглянуть на FsReactive.

Elm - это современная реализация FRP. Для моделирования динамических коллекций, которые распространены в играх, таких как Space Invaders, он содержит библиотеку Automaton, основанную на концепциях FRP со стрелками. Вы обязательно должны проверить это.

Другие вопросы по тегам