bitterharvest’s diary

A Bitter Harvestは小説の題名。作者は豪州のPeter Yeldham。苦闘の末に勝ちえた偏見からの解放は命との引換になったという悲しい物語

ボールの衝突をFunctional Reactive Programmingで表現する(8)

9.壁でも跳ね返る二つのボール

9.1 プログラムの実行

完成したプログラムの実行結果を最初に見る。なかなか面白い動きをしていることが分かる。

9.2 壁で跳ね返った時の位置

前回の記事で一直線上を走る二つのボールの衝突についてのプログラムが完成した。今度は、両側に壁があり、二つのボールはお互いに正面衝突したり、追突したりするだけでなく、壁でも跳ね返るようにする。壁での跳ね返りの係数は1とする。即ち、壁にぶつかったときは、同じ速さだが反対方向の速度で跳ね返ってくる。

例によって、現時点\(t\)の後、\(dt\)間経過するまでに壁にぶつかったとし、次の時点\(t+dt\)でのボールの位置を求める。左側に衝突した場合と、右側にした場合では、少し異なるが、図で示すと次のようになる。
f:id:bitterharvest:20151005124347p:plain

壁の位置を\(X\)、ボールの現在の位置を\(x_0\)、速度を\(dx\)、半径を\(r\)とした時、\(dt\)間経過するまでに衝突したとする。次の時点\(t_0+dt\)での位置は、左側に衝突していれば\(2 \times X -(x_0 + dt \times dx) + 2 \times r\)となり、右側では\(2 \times X -(x_0 + dt \times dx) - 2 \times r\)となる。

9.3 壁からの跳ね返りを実装する

衝突を示すデータ型のCollisionに、ボール同士の衝突に加えて、壁との衝突を加えることにする。最後の二つが壁に衝突したことを示すブール値であるが、最初の方は最初のボールが、二番目の方は二番目のボールが壁に衝突したかどうかを示すブール値である。ここでは、どちらの壁に当たったかは示さない。

data Collision = Collision Bool Ball Ball Bool Bool deriving (Eq, Show, Read)

衝突を調べるためのWireであるcollisionには、それぞれのボールが壁に衝突したかどうかを示す記述を追加する。これは次のようになる。

collision :: (HasTime t s) => Wire s () IO (Ball, Ball) Collision
collision = proc (b1@(Ball name1 r1 (V2 vx1 vy1) (V2 x1 y1) (V2 dt1 dt1')), b2@(Ball name2 r2 (V2 vx2 vy2) (V2 x2 y2) (V2 dt2 dt2'))) -> do
    let nx1 = x1 + vx1 * dt1
        nx2 = x2 + vx2 * dt2
        h1  = nx1 - r1 < (- wall) || nx1 + r1 > wall
        h2  = nx2 - r2 < (- wall) || nx2 + r2 > wall
        makeCollision :: Bool -> Ball -> Ball -> Bool -> Bool -> Collision
        makeCollision b b1 b2 h1 h2 = Collision b b1 b2 h1 h2 
    if abs (nx1 - nx2) > r1 + r2 
      then returnA -< makeCollision False b1 b2 h1 h2
      else returnA -< makeCollision True  b1 b2 h1 h2

上のプログラムで、 h1 = nx1 - r1 < (- wall) || nx1 + r1 > wallは、最初のボールが壁に衝突するかどうかを判定する。h2の方は二番目に対してである。

collisionからのデータを受けて次の位置と速度を与えなければならないが、まず位置の方は、衝突が左側で起きた場合は半径の2倍を加算し、右側で起きた場合は減算することに注意して、次のように変更する。

position :: (HasTime t s, Monad m) => (V2 Double) -> Wire s () m (V2 Double, String, Collision) (V2 Double, V2 Double)
position pInit = integralWith' collide' pInit
  where 
    collide' self (Collision b (Ball n1 r1 (V2 vx1 vy1) (V2 x1 y1) (V2 dt1 dt1')) (Ball n2 r2 (V2 vx2 vy2) (V2 x2 y2) (V2 dt2 dt2')) h1 h2) pos
            | h1 && self == n1 = if vx1 > 0 then V2 (2*wall - (x1+dt1*vx1) - 2 * r1) y1 else V2 (2*(-wall) - (x1+dt1*vx1) + 2 * r1) y1
            | h2 && self == n2 = if vx2 > 0 then V2 (2*wall - (x2+dt2*vx2) - 2 * r2) y2 else V2 (2*(-wall) - (x2+dt2*vx2) + 2 * r2) y2 
            | b  && self == n1 = if x1 > x2 then V2 (x2 + (r1 + r2)) y2 else V2 (x2 - (r1 + r2)) y2
            | b                = if x2 > x1 then V2 (x1 + (r1 + r2)) y1 else V2 (x1 - (r1 + r2)) y1
            | otherwise        = pos

速度の方は、壁に衝突したとき向きが逆になるだけなので、velocityにこれを加える。

velocity :: (HasTime t s, Monad m) => (V2 Double) ->  Wire s () m (String, Collision) (V2 Double) 
velocity vInit = constWith collide vInit
  where
    collide (self, (Collision b (Ball n1 r1 (V2 vx1 vy1) (V2 x1 y1) (V2 dt1 dt1')) (Ball n2 r2 (V2 vx2 vy2) (V2 x2 y2) (V2 dt2 dt2')) h1 h2))
            | h1 && self == n1  = V2 (-vx1) vy1
            | h2 && self == n2  = V2 (-vx2) vy2
            | b && self == n1   = V2 vx2 vy2
            | b                 = V2 vx1 vy1
            | self == n1        = V2 vx1 vy1
            | otherwise         = V2 vx2 vy2

constWith :: (Fractional a, HasTime t s)=> (w -> a) -> a -> Wire s e m w a
constWith correct = loop
  where
    loop x' = mkPure $ \ds w ->
      let dt = realToFrac (dtime ds)
          x  = correct w
      in x' `seq` (Right x', loop x)

9.4 プログラム全体

メインプログラムには壁の描写を加える。

{-# LANGUAGE Arrows #-}
-- module Main where

import Prelude hiding ((.),)
import Control.Wire
import Control.Monad.IO.Class
import FRP.Netwire
import Graphics.Rendering.OpenGL 
import Graphics.UI.GLFW 
import Data.IORef
import Linear.V2

import Ball
import Configure
import Collision

type Point = (Double, Double)
type Polygon = [Point]

renderPoint :: Point -> IO () 
renderPoint (x, y) = vertex $ Vertex2 (realToFrac x :: GLfloat) (realToFrac y :: GLfloat)

generatePointsForBall :: Ball -> Polygon 
generatePointsForBall (Ball name r (V2 vx vy) (V2 x y) (V2 dt dt')) = 
  map (\t -> (x+r*cos (t), y+r*sin (t))) [0,0.2..(2*pi)]

generatePointsForLeftWall :: Polygon 
generatePointsForLeftWall = 
  [ (-1, -1) 
  , (- wall, -1) 
  , (- wall, 1) 
  , (-1, 1) ] 

generatePointsForRightWall :: Polygon 
generatePointsForRightWall = 
  [ (1, 1) 
  , (wall, 1) 
  , (wall, -1) 
  , (1, -1) ] 

runNetwork :: (HasTime t s) => IORef Bool -> Session IO s -> Wire s e IO a (Ball, Ball) -> IO () 
runNetwork closedRef session wire = do 
  pollEvents 
  let color3f r g b = color $ Color3 r g (b :: GLfloat)
  closed <- readIORef closedRef 
  if closed 
    then return () 
    else do
      (st , session') <- stepSession session 
      (wt', wire' ) <- stepWire wire st $ Right undefined 
      case wt' of 
        Left _ -> return () 
        Right (b1,b2) -> do
          clear [ColorBuffer] 
          color3f 1.0 0.8 0.6
          renderPrimitive Polygon $ 
            mapM_ renderPoint $ generatePointsForBall b1
          color3f 0.8 0.2 0.2
          renderPrimitive Polygon $ 
            mapM_ renderPoint $ generatePointsForBall b2

          color3f 0.7 0.7 0.7
          renderPrimitive Polygon $ 
            mapM_ renderPoint $ generatePointsForLeftWall
          renderPrimitive Polygon $ 
            mapM_ renderPoint $ generatePointsForRightWall
          swapBuffers 
          runNetwork closedRef session' wire' 

simulation :: HasTime t s => Wire s () IO a (Ball, Ball)
simulation = proc _ -> do
    rec b1 <- ball "ball1" radius vInit1 pInit1 -< ("ball1", c)
        b2 <- ball "ball2" radius vInit2 pInit2 -< ("ball2", c)
        c <- collision -< (b1, b2) 
    returnA -< (b1, b2)

main :: IO () 
main = do
  initialize 
  openWindow (Size 640 640) [DisplayRGBBits 8 8 8, DisplayAlphaBits 8, DisplayDepthBits 24] Window
  closedRef <- newIORef False 
  windowCloseCallback $= do 
    writeIORef closedRef True 
    return True 
  runNetwork closedRef clockSession_ simulation
  closeWindow

ボールに関するプログラムは次のようになる。

{-# LANGUAGE Arrows #-}
module Ball (ball) where

import Prelude hiding ((.),)
import Control.Wire
import FRP.Netwire.Move
import Linear.V2
import Configure
import Collision

velocity :: (HasTime t s, Monad m) => (V2 Double) ->  Wire s () m (String, Collision) (V2 Double) 
velocity vInit = constWith collide vInit
  where
    collide (self, (Collision b (Ball n1 r1 (V2 vx1 vy1) (V2 x1 y1) (V2 dt1 dt1')) (Ball n2 r2 (V2 vx2 vy2) (V2 x2 y2) (V2 dt2 dt2')) h1 h2))
            | h1 && self == n1  = V2 (-vx1) vy1
            | h2 && self == n2  = V2 (-vx2) vy2
            | b && self == n1   = V2 vx2 vy2
            | b                 = V2 vx1 vy1
            | self == n1        = V2 vx1 vy1
            | otherwise         = V2 vx2 vy2

constWith :: (Fractional a, HasTime t s)=> (w -> a) -> a -> Wire s e m w a
constWith correct = loop
  where
    loop x' = mkPure $ \ds w ->
      let dt = realToFrac (dtime ds)
          x  = correct w
      in x' `seq` (Right x', loop x)


position :: (HasTime t s, Monad m) => (V2 Double) -> Wire s () m (V2 Double, String, Collision) (V2 Double, V2 Double)
position pInit = integralWith' collide' pInit
  where 
    collide' self (Collision b (Ball n1 r1 (V2 vx1 vy1) (V2 x1 y1) (V2 dt1 dt1')) (Ball n2 r2 (V2 vx2 vy2) (V2 x2 y2) (V2 dt2 dt2')) h1 h2) pos
            | h1 && self == n1 = if vx1 > 0 then V2 (2*wall - (x1+dt1*vx1) - 2 * r1) y1 else V2 (2*(-wall) - (x1+dt1*vx1) + 2 * r1) y1
            | h2 && self == n2 = if vx2 > 0 then V2 (2*wall - (x2+dt2*vx2) - 2 * r2) y2 else V2 (2*(-wall) - (x2+dt2*vx2) + 2 * r2) y2 
            | b  && self == n1 = if x1 > x2 then V2 (x2 + (r1 + r2)) y2 else V2 (x2 - (r1 + r2)) y2
            | b                = if x2 > x1 then V2 (x1 + (r1 + r2)) y1 else V2 (x1 - (r1 + r2)) y1
            | otherwise        = pos

integralWith' :: (Fractional a, HasTime t s) => (b -> w -> a ->  a) -> a -> Wire s e m (a, b, w) (a, a)
integralWith' correct = loop
  where
    loop x' = mkPure $ \ds (vel, n, c) ->
      let dt = realToFrac (dtime ds)
          x  = correct n c (x' + dt*vel)
      in x' `seq` (Right (x', dt), loop x)


ball :: (HasTime t s) => String -> Double -> (V2 Double) -> (V2 Double) -> Wire s () IO (String, Collision) Ball
ball name r v0 p0 = proc (self, collision) -> do
    vel <- velocity v0 -< (self, collision)
    (pos, dt) <- position p0 -< (vel, self, collision)
    returnA -< makeBall name vel pos dt
  where makeBall :: String -> V2 Double -> V2 Double -> V2 Double -> Ball
        makeBall n v p dt = Ball n r v p dt

衝突に関するプログラムは次のようになる。

{-# LANGUAGE Arrows #-}
module Collision (collision) where

import Prelude hiding ((.),)
import Control.Wire
import Control.Monad.IO.Class()
import FRP.Netwire()
import Linear.V2
import Configure


collision :: (HasTime t s) => Wire s () IO (Ball, Ball) Collision
collision = proc (b1@(Ball name1 r1 (V2 vx1 vy1) (V2 x1 y1) (V2 dt1 dt1')), b2@(Ball name2 r2 (V2 vx2 vy2) (V2 x2 y2) (V2 dt2 dt2'))) -> do
    let nx1 = x1 + vx1 * dt1
        nx2 = x2 + vx2 * dt2
        h1  = nx1 - r1 < (- wall) || nx1 + r1 > wall
        h2  = nx2 - r2 < (- wall) || nx2 + r2 > wall
        makeCollision :: Bool -> Ball -> Ball -> Bool -> Bool -> Collision
        makeCollision b b1 b2 h1 h2 = Collision b b1 b2 h1 h2 
    if abs (nx1 - nx2) > r1 + r2 
      then returnA -< makeCollision False b1 b2 h1 h2
      else returnA -< makeCollision True  b1 b2 h1 h2

データ型は初期値に関するプログラムは次のようになる。なお、データ型が複雑になってきたので、レコード構文に変更する。

{-# LANGUAGE Arrows #-}
module Configure where

import Linear.V2

-- one unit is equal to 10 meters
data Ball = Ball {name :: String,  semidiameter :: Double, speed :: V2 Double, location :: V2 Double, interval :: V2 Double } deriving (Eq, Show, Read)


radius :: Double
radius = 0.05

{-
pInit1 :: V2 Double
pInit1 = V2 (-0.5) 0

vInit1 :: V2 Double
vInit1 = V2 0.1 0

pInit2 :: V2 Double
pInit2 = V2 (-0.75) 0

vInit2 :: V2 Double
vInit2 = V2 0.2 0
-}

pInit1 :: V2 Double
pInit1 = V2 (0.5) 0

vInit1 :: V2 Double
vInit1 = V2 (-0.3) 0

pInit2 :: V2 Double
pInit2 = V2 (0.75) 0

vInit2 :: V2 Double
vInit2 = V2 (-0.4) 0

{-
pInit1 :: V2 Double
pInit1 = V2 (-0.75) 0

vInit1 :: V2 Double
vInit1 = V2 0.2 0

pInit2 :: V2 Double
pInit2 = V2 0.75 0

vInit2 :: V2 Double
vInit2 = V2 (-0.1) 0
-}

data Collision = Collision {colliding :: Bool, ball1 :: Ball, ball2 :: Ball, hitting1 :: Bool, hitting2 :: Bool} deriving (Eq, Show, Read)

wall = 0.9

9.5 やり残したこと

ボール同士の衝突と壁への衝突が同時に発生したときの処理はまだ含まれていない。