1

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?

Felix
  • 147
  • 2
  • 10
  • Not an answer: one thing to make absolutely clear is that the `time` in `main` and the `time` in `timeHandler` have nothing to do with each other -- in fact, they don't even have the same type. The former is a local value you got by executing `getPosixTime`, while the latter is a parameter in an independent function definition. They just happen to have the same name. – duplode Oct 22 '16 at 18:43
  • I'm aware of this @duplode ! However, according to the `play` function description, `timeHandler` should be given the time in seconds.. – Felix Oct 22 '16 at 18:51
  • 1
    Okay -- it's just that your phrasing in "given to timeHandler in the `main` function" confused me a bit. (You don't give the `time` to ` timeHandler` yourself; rather, Gloss picks it up by itself somewhere in the `play` implementation.) – duplode Oct 22 '16 at 19:05
  • In any case, if I am understanding [the docs](https://hackage.haskell.org/package/gloss-1.10.2.3/docs/Graphics-Gloss-Interface-Pure-Game.html#v:play) correctly, the expected behaviour for your program would be rotating the picture one degree per second (i.e. 1/60 of a degree per frame), right? – duplode Oct 22 '16 at 19:08
  • Exactly, that's what I'm expecting it to do. – Felix Oct 22 '16 at 19:09
  • What is `draw` doing? You should include all your code. Also this example doesn't seem minimal - `seed` is unused in `initial` so you should just remove it. Minimizing the example more may help you see the mistake. – user2407038 Oct 22 '16 at 19:09
  • You're right, I've updated my post including the draw function! – Felix Oct 22 '16 at 19:16

1 Answers1

3

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.

user2407038
  • 14,400
  • 3
  • 29
  • 42
  • Tanks for your answer! The only thing I'm a bit confused about: according to the docs of `play`, `updateWorld` should receive the time in seconds. In your code, you create your own time variable with `time <- getCurrentTime`. How is this being passed to `updateWorld`? – Felix Oct 22 '16 at 19:58
  • 1
    Where did you get that idea? I linked to the docs and I copy pasted the relevant bit - the step function **does not** get the time in seconds since the start of the simulation, or any other time, but the amount of time the simulation should be stepped (i.e. a *time delta*). In any case, the initial `startTime` and `time` declared in `main` are in the initial state - `play ... World{..} ...` - which is equivalent to `World{field1 = field1, field2 = field2, }` - this is `RecordWildCards`. – user2407038 Oct 22 '16 at 21:06
  • 2
    RecordWildCards can indeed be confusing if you are not familiar with it, and isn't relevant to the problem, so I got rid of it and used regular record syntax. – user2407038 Oct 22 '16 at 21:16
  • That makes sense. Keeping track of time in a state in a language like Haskell is only logical. Thanks! – Felix Oct 23 '16 at 10:18