2

I want to move an object in Haskell Gloss every frame a key is pressed, not just the one frame that the key is started being pressed. (Example: While 'w' key is pressed, accelerate object every frame)

Edit: I tried using the second parameter of EventKey but to no avail.

My code:

--TODO - Holding keys doesn't work yet
handleKeys :: Event -> AsteroidsGame -> AsteroidsGame
handleKeys (EventKey (Char char) _ _ _) game 
        | char == 'w' = move   0   1
        | char == 'a' = move (-1)  0
        | char == 's' = move   0 (-1)
        | char == 'd' = move   1   0
    where move x y = game {player = accelerateObject (player game) x y}
handleKeys _ game = game

accelerateObject :: Object -> Float -> Float -> Object
accelerateObject obj hor ver = obj {vel = (vx + hor, vy + ver)}
    where (vx, vy) = vel obj
The Coding Wombat
  • 805
  • 1
  • 10
  • 29
  • Just keep track of pressed keys in AsteroidGame? I presume the EventKey tell you wether the key was pressed or released. – typetetris Oct 18 '18 at 10:09
  • As @Krom says, check for `Down` events and `Up` events. Depending on your environment, you should be seeing repeating `Down` events until the key is released, which should show as an `Up` event. – Bob Dalgleish Oct 18 '18 at 13:26
  • @BobDalgleish I think the Down event only is triggered upon pressing the key, not again and again when it is already pressed. So it doesn't work. I do not know how to confirm that there aren't any more events, i.e. I don't know how to print some debug info in the middle of other code in Haskell – The Coding Wombat Oct 19 '18 at 10:21
  • You can use the `Debug.Trace` package to insert `trace` statements. Normally, the event system should issue an event when the key starts auto-repeating. Any text editor, for instance, will record each character when you hold the character key down. Check the documentation to see if something isn't filtering auto-repeating characters. – Bob Dalgleish Oct 19 '18 at 14:13
  • @BobDalgleish I can now confirm that there is only an event upon pressing the button, no more events while the button is pressed, so I still do not have a solution. – The Coding Wombat Nov 11 '18 at 17:03

1 Answers1

4

As OP correctly deduced, gloss gives you input events ("key was just pressed", "mouse was just moved"), rather than input state ("key is currently pressed", "mouse is at x,y"). There doesn't seem to be a built-in way to see input state on each frame, so we'll have to make our own workaround. Thankfully, this isn't too difficult!

For a simple working example, we'll make an incredibly fun "game" where you can watch a counter count upwards while the space bar is pressed. Riveting. This approach generalises to handling any key presses, so it'll be easy to extend to your case.

The first thing we need is our game state:

import qualified Data.Set as S

data World = World
    { keys :: S.Set Key
    , counter :: Int }

We keep track of our specific game state (in this case just a counter), as well as state for our workaround (a set of pressed keys).

Handling input events just involves either adding a key to our set of currently pressed keys or removing it:

handleInput :: Event -> World -> World
handleInput (EventKey k Down _ _) world = world { keys = S.insert k (keys world)}
handleInput (EventKey k Up _ _) world = world { keys = S.delete k (keys world)}
handleInput _ world = world -- Ignore non-keypresses for simplicity

This can easily be extended to handle eg. mouse movement, by changing our World type to keep track of the last known coordinates of the cursor, and setting it in this function whenever we see an EventMotion event.

Our frame-to-frame world update function then uses the input state to update the specific game state:

update :: Float -> World -> World
update _ world
    | S.member (SpecialKey KeySpace) (keys world) = world { counter = 1 + counter world }
    | otherwise = world { counter = 0 }

If the spacebar is currently pressed (S.member (SpecialKey KeySpace) (keys world)), increment the counter - otherwise, reset it to 0. We don't care about how much time as elapsed between frames so we ignore the float argument.

Finally we can render our game and play it:

render :: World -> Picture
render = color white . text . show . counter

main :: IO ()
main = play display black 30 initWorld render handleInput update
    where
        display = InWindow "test" (800, 600) (0, 0)
        initWorld = World S.empty 0
hnefatl
  • 5,860
  • 2
  • 27
  • 49