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?
How can I write a simple real-time game loop in pure Haskell?
Asked Answered
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
© 2022 - 2024 — McMap. All rights reserved.