Ну, вот примерный набросок того, что я бы сделал:
import Graphics.UI.SDL.Time (getTicks)
import Control.Concurrent (threadDelay)
type Frame = [[Char]]
type Animation = [Frame]
displayFrame :: Frame -> IO ()
displayFrame = mapM_ putStrLn
timeAction :: IO () -> IO Integer
timeAction act = do t <- getTicks
act
t' <- getTicks
return (fromIntegral $ t' - t)
addDelay :: Integer -> IO () -> IO ()
addDelay hz act = do dt <- timeAction act
let delay = calcDelay dt hz
threadDelay $ fromInteger delay
calcDelay dt hz = max (frame_usec - dt_usec) 0
where frame_usec = 1000000 `div` hz
dt_usec = dt * 1000
runFrames :: Integer -> Animation -> IO ()
runFrames hz frs = mapM_ (addDelay hz . displayFrame) frs
Очевидно, я использую SDL здесь исключительно для getTicks
, потому что это то, что я использовал раньше. Не стесняйтесь заменить его любой другой функцией, чтобы узнать текущее время.
Первый аргумент runFrames
- это, как следует из названия, частота кадров в герцах, то есть кадров в секунду. Функция runFrames
сначала преобразует каждый кадр в действие, которое его рисует, затем передает каждый из них функции addDelay
, которая проверяет время до и после выполнения действия, а затем спит до тех пор, пока не пройдет время кадра.
Мой собственный код выглядел бы несколько иначе, потому что у меня обычно был бы более сложный цикл, который выполнял бы другие вещи, например, опрос SDL для событий, выполнение фоновой обработки, передача данных на следующую итерацию и т. Д. Но основная идея та же самая.
Очевидно, что хорошая особенность этого подхода заключается в том, что, хотя он и остается достаточно простым, вы получаете постоянную частоту кадров, когда это возможно, с ясным средством указания целевой скорости.