Как реализовать коллизию с Netwire (5.0.1)

Я пытаюсь смоделировать движущиеся объекты с помощью Netwire и хотел бы знать рекомендуемый способ реализации чего-то вроде отскока мяча от стены. Я столкнулся с несколькими возможными способами сделать это, и мне нужна помощь, чтобы заставить их работать.

Код движения выглядит так:

type Pos = Float
type Vel = Float

data Collision = Collision | NoCollision
           deriving (Show)

motion :: (HasTime t s, MonadFix m) => Pos -> Vel -> Wire s Collision m a Pos
motion x0 v0 = proc _ -> do
             rec
                 v <- vel <<< delay 0 -< x
                 x <- pos x0 -< v
             returnA -< x

pos :: (HasTime t s, MonadFix m) => Pos -> Wire s Collision m Vel Pos
pos x0 = integral x0

main :: IO ()
main = testWire clockSession_ (motion 0 5)

Каков рекомендуемый способ сделать стрелку скорости, которая вызывает отскок в определенной позиции, например, x = 20?

Я видел три разных способа, которыми я мог бы это сделать:

  • Функция netwire --> кажется самой простой. У меня есть прототип, использующий эту функцию, но я не знаю, как сделать новую стрелку скорости на основе скорости во время столкновения, я могу использовать только фиксированное значение, которое бесполезно, если объект может ускоряться.

    vel :: (HasTime t s, MonadFix m) => Wire s Collision m Pos Vel
    vel = pure 5 . unless (>20) --> pure (-5)
    
  • Использование Event и switch в Netwire. Я не понимаю, как это использовать.

  • Использование функции (|||), доступной стрелкам в целом.

Первые два кажутся лучшими вариантами, но я не знаю, как их реализовать.

Я видел другие подобные вопросы, но несовместимость между разными версиями netwire сделала ответы бесполезными для меня.


person user668074    schedule 04.08.2016    source источник
comment
Может оказаться полезным взглянуть на этот код: github.com/crockeo/netwire-pong   -  person ErikR    schedule 04.08.2016
comment
@ErikR, спасибо за ссылку. Мне нужно время, чтобы понять, как это работает, но мое первое впечатление, что это не так лаконично, как я надеялся.   -  person user668074    schedule 04.08.2016


Ответы (1)


Отказ от ответственности: я не могу комментировать то, что «рекомендуется», но я могу показать способ, который делает то, что вы хотите сделать.

Я хочу описать два метода:
Первый – это использование проводов с отслеживанием состояния, и он очень похож на это немного устаревшее руководство 2013 года, но оно основано на Netwire 5.0.2.
Второй – использование проводов без сохранения состояния. Поскольку они не имеют состояния, им необходимо возвращать свои предыдущие значения, что делает типы ввода проводов и окончательную комбинацию проводов немного более запутанными. В остальном они довольно похожи.

Основные типы, задействованные в обоих примерах:

type Collision = Bool
type Velocity = Float
type Position = Float

с сохранением состояния

Вы можете смоделировать свою проблему с помощью двух проводов (с сохранением состояния), которые затем объединяются.

Один провод моделирует постоянную скорость и меняет направление при столкновении. (Упрощенный) тип этого — Wire s e m Collision Velocity, т. е. вход — если произошло столкновение, а вывод — текущая скорость.

Другой моделирует положение и обрабатывает коллизии. (Упрощенный) тип этого — Wire s e m Velocity (Position, Collision), т. е. он берет скорость, вычисляет новую позицию и возвращает ее, если произошло столкновение.

Наконец, скорость подается на провод положения, а результат столкновения возвращается обратно на провод скорости.

Давайте посмотрим на детали скоростной проволоки:

-- stateful fixed velocity wire that switches direction when collision occurs
velocity :: Velocity -> Wire s e m Collision Velocity
velocity vel = mkPureN $ \collision ->
  let nextVel = if collision then negate vel else vel
  in (Right nextVel, velocity nextVel)

mkPureN создает проводник с состоянием, который зависит только от ввода и собственного состояния (не от монады или времени). Состояние - это текущая скорость, а следующая скорость инвертируется, если в качестве входных данных передается Collision=True. Возвращаемое значение представляет собой пару значения скорости и нового провода с новым состоянием.

Для позиции уже недостаточно использовать провод integral напрямую. Нам нужна расширенная, «ограниченная» версия integral, которая гарантирует, что значение остается ниже верхней границы и больше 0, и возвращает информацию, если такое столкновение произошло.

-- bounded integral [0, bound]
pos :: HasTime t s => Position -> Position -> Wire s e m Velocity (Position, Collision)
pos bound x = mkPure $ \ds dx ->
  let dt = realToFrac (dtime ds)
      nextx' = x + dt*dx -- candidate
      (nextx, coll)
        | nextx' <= 0 && dx < 0     = (-nextx', True)
        | nextx' >= bound && dx > 0 = (bound - (nextx' - bound), True)
        | otherwise                 = (nextx', False)
  in (Right (nextx, coll), pos bound nextx)

mkPure похоже на mkPureN, но позволяет проводнику зависеть от времени.
dt — это разница во времени.
nextx' — это новая позиция, которую возвращает integral.
Следующие строки проверяют границы и вернуть новую позицию, если произошло столкновение и новый провод с новым состоянием.

Наконец, вы вводите их друг в друга, используя rec и delay. Полный пример:

{-# LANGUAGE Arrows #-}

module Main where

import Control.Monad.Fix
import Control.Wire
import FRP.Netwire

type Collision = Bool
type Velocity = Float
type Position = Float

-- bounded integral [0, bound]
pos :: HasTime t s => Position -> Position -> Wire s e m Velocity (Position, Collision)
pos bound x = mkPure $ \ds dx ->
  let dt = realToFrac (dtime ds)
      nextx' = x + dt*dx -- candidate
      (nextx, coll)
        | nextx' <= 0 && dx < 0     = (-nextx', True)
        | nextx' >= bound && dx > 0 = (bound - (nextx' - bound), True)
        | otherwise                 = (nextx', False)
  in (Right (nextx, coll), pos bound nextx)

-- stateful fixed velocity wire that switches direction when collision occurs
velocity :: Velocity -> Wire s e m Collision Velocity
velocity vel = mkPureN $ \collision ->
  let nextVel = if collision then negate vel else vel
  in (Right nextVel, velocity nextVel)

run :: (HasTime t s, MonadFix m) => Position -> Velocity -> Position -> Wire s () m a Position
run start vel bound = proc _ -> do
  rec
    v <- velocity vel <<< delay False -< collision
    (p, collision) <- pos bound start -< v
  returnA -< p

main :: IO ()
main = testWire clockSession_ (run 0 5 20)

без гражданства

Вариант без сохранения состояния очень похож на вариант с сохранением состояния, за исключением того, что состояние переходит к типу ввода проводов, а не является параметром функций, создающих проводник.

Таким образом, провод скорости принимает кортеж (Velocity, Collision) в качестве входных данных, и мы можем просто поднять функцию, чтобы создать его:

-- pure stateless independent from time
-- output velocity is input velocity potentially negated depending on collision
velocity :: Monad m => Wire s e m (Velocity, Collision) Velocity
velocity = arr $ \(vel, collision) -> if collision then -vel else vel

Вы также можете использовать функцию mkSF_ из Control.Wire.Core (и тогда избавиться от ограничения на Monad m).

pos становится

-- pure stateless but depending on time
-- output position is input position moved by input velocity (depending on timestep)
pos :: HasTime t s => Position -> Wire s e m (Position, Velocity) (Position, Collision)
pos bound = mkPure $ \ds (x,dx) ->
  let dt = realToFrac (dtime ds)
      nextx' = x + dt*dx -- candidate
      (nextx, coll)
        | nextx' <= 0 && dx < 0     = (-nextx', True)
        | nextx' >= bound && dx > 0 = (bound - (nextx' - bound), True)
        | otherwise                 = (nextx', False)
  in (Right (nextx, coll), pos bound)

Здесь нам по-прежнему нужно использовать mkPure, потому что нет функции, которую можно было бы использовать специально для проводов без сохранения состояния, которые зависят от времени.

Чтобы соединить два провода, мы теперь должны передать выход скорости в себя и положение, а из провода pos положение в себя и информацию о столкновении в провод скорости.

Но на самом деле с проводами без сохранения состояния вы также можете разделить части «интеграции» и «проверки границ» в проводе pos. Затем провод pos представляет собой Wire s e m (Position, Velocity) Position, который напрямую возвращает то, что nextx' указано выше, а провод boundedPos представляет собой Wire s e m (Position, Velocity) (Position, Collision), который получает новое положение из pos и скорости, а также применяет проверку границ. Таким образом, различные логические части будут хорошо разделены.

Полный пример:

{-# LANGUAGE Arrows #-}

module Main where

import Control.Monad.Fix
import Control.Wire
import FRP.Netwire

type Collision = Bool
type Velocity = Float
type Position = Float

-- pure stateless but depending on time
-- output position is input position moved by input velocity (depending on timestep)
pos :: HasTime t s => Wire s e m (Position, Velocity) Position
pos = mkPure $ \ds (x,dx) ->
  let dt = realToFrac (dtime ds)
  in (Right (x + dt*dx), pos)

-- pure stateless independent from time
-- input position is bounced off the bounds
boundedPos :: Monad m => Position -> Wire s e m (Position, Velocity) (Position, Collision)
boundedPos bound = arr $ \(x, dx) ->
  let (nextx, collision)
        | x <= 0 && dx < 0 = (-x, True)
        | x >= bound && dx > 0 = (bound - (x - bound), True)
        | otherwise          = (x, False)
  in (nextx, collision)

-- pure stateless independent from time
-- output velocity is input velocity potentially negated depending on collision
velocity :: Monad m => Wire s e m (Velocity, Collision) Velocity
velocity = arr $ \(vel, collision) -> if collision then -vel else vel

-- plug the wires into each other
run :: (HasTime t s, MonadFix m) => Position -> Velocity -> Position -> Wire s () m a Position
run start vel bound = proc _ -> do
  rec
    v <- velocity <<< delay (vel, False) -< (v, collision)
    lastPos <- delay start -< p'
    p <- pos -< (lastPos, v)
    (p', collision) <- boundedPos bound -< (p, v)
  returnA -< p'

main :: IO ()
main = testWire clockSession_ (run 0 5 20)
person E4z9    schedule 25.11.2016