haskelltimegloss

Haskell Gloss: time variable stays constant


I'm trying to rotate a Picture in Haskell, using the current time as a value for a rotate function. I've got the following main function:

main :: IO ()
main = do
    args <- getArgs
    time <- round <$> getPOSIXTime
    let initial'        = initial time
    let (w, h, display) = chooseDisplay args
    let background      = black
    let fps             = 60
    play display background fps initial' (draw w h) eventHandler timeHandler

The triangle (=player) is stored inside a 'World' data type: module Model where

data World = World {
        -- all kinds of World properties --
        player           :: Picture
    }

Then I have a function initial which initializes the World, and a function playerBody which, given a rotation value, returns a picture of player:

initial :: Int -> World
initial _ = World{player = playerBody 0}

playerBody :: Float -> Picture
playerBody rot = Rotate rot $ color red $ Polygon[(-10, 100), (10, 100), (0, 125)]

The draw function is defined as follows:

draw :: Float -> Float -> World -> Picture
draw _ _ world = player world

It currently simply returns the playerPicture.

Now, in my timeHandler module, I want to use the time (given to timeHandler in the main function) to rotate player as follows:

timeHandler :: Float -> World -> World
timeHandler time = (\world -> world {player = playerBody time} )

This doesn't work. I replaced time with a constant value (in the timeHandler function), and that did rotate the Picture. So it seems like time is not being updated.. what am I doing wrong?


Solution

  • Naturally it doesn't work, timeHandler receives a number which, in practice, is close to the time delta since the previous frame - the docs say: "A function to step the world one iteration. It is passed the period of time (in seconds) needing to be advanced." - and presumably frame time is about constant, so naturally one would expect the output to be about constant (and a number very close to 0).

    You need to collect all the deltas and add them up. If you are just concerned with time since the start of your simulation, then you don't need getCurrentTime in main - just add the deltas. This means you must store the time in your state. If you do need to deal with real time, I suggest you stick with UTCTime or another abstraction which makes it clear what quantity you are manipulating:

    import Graphics.Gloss 
    import Graphics.Gloss.Interface.Pure.Game
    import Data.Time.Clock
    
    data World = World 
      { startTime 
      , time :: UTCTime 
      } 
    
    -- make explicit what units are being used
    seconds2DiffTime :: Float -> NominalDiffTime
    seconds2DiffTime = realToFrac 
    diffTime2seconds :: NominalDiffTime -> Float 
    diffTime2seconds = realToFrac 
    
    displayWorld World{time,startTime} = 
      Rotate rot $ color red $ Polygon[(-10, 100), (10, 100), (0, 125)]
        where rot = diffTime2seconds $ diffUTCTime time startTime 
    
    updateWorld dt w = w {time = addUTCTime (seconds2DiffTime dt) $ time w}
    
    main = do 
      t0 <- getCurrentTime
      let initialState = World { time = t0, startTime = t0 }
      play (InWindow "" (400,400) (400,400)) 
           white
           30 
           intialState 
           displayWorld 
           (const id) 
           updateWorld
    

    This gets you both the elapsed time since the start of the simulation, as well as the real clock time.

    Note that you should not place picture drawing code inside of timeHandler - this function could be called much more frequently than is needed to redraw the picture, resulting in a lot of extra work. Instead do your drawing in displayWorld as above.