Как реализовать коллизию с 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)
Каков рекомендуемый способ сделать стрелку скорости, которая вызывает отскок в определенной позиции, например, х =20?
Я видел три разных способа, которыми я мог бы сделать это:
Сеть
-->
функция, которая кажется самой простой. У меня есть прототип, использующий эту функцию, но я не знаю, как сделать новую стрелку скорости, основанную на скорости во время столкновения, я могу использовать только фиксированное значение, которое бесполезно, если объект может ускоряться.vel :: (HasTime t s, MonadFix m) => Wire s Collision m Pos Vel vel = pure 5 . unless (>20) --> pure (-5)
С использованием
Event
а такжеswitch
в сети. Я не понимаю, как это использовать.С использованием
(|||)
функция доступна для стрелок в целом.
Первые два кажутся лучшими вариантами, но я не знаю, как их реализовать.
Я видел другие подобные вопросы на этот счет, но несовместимость между различными версиями netwire сделала ответы бесполезными для меня.
1 ответ
Отказ от ответственности: я не могу комментировать то, что "рекомендуется", но я могу показать способ, который делает то, что вы хотите сделать.
Я хочу описать два метода:
Первый использует проводники с отслеживанием состояния и довольно похож на этот немного устаревший учебник 2013 года, но основан на Netwire 5.0.2.
Второе использование проводов без сохранения состояния. Поскольку они не имеют состояния, их необходимо вернуть назад к их предыдущим значениям, что делает типы входов и окончательную комбинацию проводов немного более запутанными. В остальном они очень похожи.
Основные типы, которые используются в обоих примерах:
type Collision = Bool
type Velocity = Float
type Position = Float
Stateful
Вы можете смоделировать вашу проблему с двумя (с состоянием) проводами, которые затем объединяются.
Один провод моделирует скорость, которая является постоянной, и меняет направление, когда происходит столкновение. (Упрощенный) тип этого 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)
Stateless
Вариант без состояния очень похож на вариант с состоянием, за исключением того, что состояние отклоняется от типа ввода проводов, а не является параметром для функций, которые создают провод.
Скорость проволоки поэтому принимает кортеж (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)