How can I write a simple real-time game loop in pure Haskell?

879 views Asked by At

I've just started with Haskell and want to make a simple real-time game without installing additional libraries. I need to write a loop that scans for keyboard input but the game also has to run if there is no input. How do I do this?

1

There are 1 answers

0
Miloslav Číž On BEST ANSWER

this answer is being updated as I find new solutions

After hours of studying I came up with the following code:

    {- Simple game loop example. -}

import System.IO
import System.Timeout

inputTimeout = 50000           -- in microseconds
initialGameState = 100    
type GameState = Int           -- change to your own gamestate type here

nextGameState :: GameState -> Char -> GameState
nextGameState previousGameState inputChar =

  -- REPLACE THIS FUNCTION WITH YOUR GAME

  case inputChar of
    's' -> previousGameState + 1
    'a' -> previousGameState - 1
    _   -> previousGameState

loop :: GameState -> IO ()            -- game loop
loop gameState =
  do
    putStrLn (show gameState)
    hFlush stdout

    c <- timeout inputTimeout getChar -- wait for input, with timeout

    case c of
      -- no input given
      Nothing -> do loop gameState

      -- quit on 'q'
      Just 'q' -> do putStrLn "quitting"                     

      -- input was given
      Just input -> do loop (nextGameState gameState input)

main = 
  do
    hSetBuffering stdin NoBuffering   -- to read char without [enter]
    hSetBuffering stdout (BlockBuffering (Just 80000))  -- to reduce flickering, you can change the constant
    hSetEcho stdout False             -- turn off writing to console with keyboard
    loop initialGameState

A few notes:

  • This (not really) a game simply writes out the world state, which is just a number here, and increments/decrements it with 's' or 'a'. 'q' quits the program.
  • Obviously this is a very simple solution and won't be usable for more serious games. The downfalls are:
    • The code doesn't scan for the keyboard state but reads the standard input, which limits the input handling. You won't be able to read simultaneous keypresses and there will be repeat delay. You could fix this by maybe writing a script in another language that would handle the keyboard input in more sophisticated way and pass it via pipe to your program. On Unix-like systems you could also read the keyboard state from a file in /dev/...
    • You can expect each frame to take about inputTimeout microseconds, but not exactly. Very fast input could theoretically lower this, computation delays will increase this.
  • I'm a Haskell beginner, so feel free to improve on this and post here. I'm updating this answer.

updated code

In previous code, if the game nextGameState function took a significant time to compute, the input characters would pile up in stdin and the reaction of the program would be delayed. The following code solves this by always reading all characters from input and taking only the last one.

    {- Simple game loop example, v 2.0. -}

import System.IO
import Control.Concurrent

frameDelay = 10000             -- in microseconds
initialGameState = 100    
type GameState = Int           -- change to your own gamestate type here

nextGameState :: GameState -> Char -> GameState
nextGameState previousGameState inputChar =

  -- REPLACE THIS FUNCTION WITH YOUR GAME

  case inputChar of
    's' -> previousGameState + 1
    'a' -> previousGameState - 1
    _   -> previousGameState

getLastChar :: IO Char
getLastChar =
  do  
    isInput <- hWaitForInput stdin 1      -- wait for char for 1 ms

    if isInput
      then
        do
          c1 <- getChar
          c2 <- getLastChar

          if c2 == ' '
            then return c1
            else return c2

      else
        do
          return ' '

gameLoop :: GameState -> IO ()            -- game loop
gameLoop gameState =
  do
    threadDelay frameDelay
    putStrLn (show gameState)
    hFlush stdout

    c <- getLastChar

    case c of
      'x' -> do putStrLn "quitting"
      _   -> do gameLoop (nextGameState gameState c)

main = 
  do
    hSetBuffering stdin NoBuffering   -- to read char without [enter]
    hSetBuffering stdout (BlockBuffering (Just 80000))  -- to reduce flickering, you can change the constant
    hSetEcho stdout False             -- turn off writing to console with keyboard
    gameLoop initialGameState