Предисловие
После 3 месяцев копания на многочисленных веб-сайтах и опробования некоторых небольших проектов я, наконец, смог реализовать минималистичную игру (или нет?) Совершенно иным образом. Этот пример существует только для демонстрации одной возможной структуры игры, написанной на Haskell, и его следует легко расширить для обработки более сложной логики и игрового процесса.
Полный код и руководство доступны на https://github.com/carldong/HMovePad-Tutorial
Абстрактный
В этой мини-игре есть только один прямоугольник, который игрок может перемещать влево и вправо, нажимая клавиши «Влево» и «Вправо», и это вся игра.
Игра реализована с использованием netwire-5.0.1
, с SDL
обработкой графики. Если я правильно понял, архитектура полностью реактивная. Почти все реализовано композицией Arrow, и только одна функция представлена в IO
. Поэтому я ожидаю, что у читателя будет базовое понимание синтаксиса Arrow в Haskell, поскольку он широко используется.
Порядок реализации этой игры выбран, чтобы упростить отладку, а сама реализация выбрана так, чтобы максимально продемонстрировать различное использование netwire
.
Семантика непрерывного времени используется для ввода-вывода, но дискретные события используются для обработки игровых событий в игровой логике.
Настроить SDL
Самый первый шаг - убедиться, что SDL работает. Источник прост:
module Main where
import qualified Graphics.UI.SDL as SDL
main :: IO ()
main = do
SDL.init [SDL.InitEverything]
w <- SDL.setVideoMode 800 600 32 [SDL.SWSurface]
s <- SDL.createRGBSurfaceEndian [SDL.SWSurface] 800 600 32
SDL.fillRect s (Just testRect) (SDL.Pixel 0xFFFFFFFF)
SDL.blitSurface s (Nothing) w (Nothing)
SDL.flip w
testLoop
SDL.quit
where
testLoop = testLoop
testRect = SDL.Rect 350 500 100 50
Если все работает, в нижней части окна должен появиться белый прямоугольник. Обратите внимание, что нажатие x
не закроет окно. Он должен быть закрыт нажатием Ctrl + C или убийством.
Настройка выходных проводов
Поскольку мы не хотим реализовывать полностью до последнего шага и обнаруживать, что на экране ничего нельзя нарисовать, мы сначала выполняем часть вывода.
Нам понадобится синтаксис Arrows:
{-# LANGUAGE Arrows #-}
Также нам нужно импортировать кое-что:
import Prelude hiding ((.), id)
import Control.Wire
import Control.Arrow
import Control.Monad
import Data.Monoid
import qualified Graphics.UI.SDL as SDL
Нам нужно понять, как построить Kleisli Wires: Kleisli Arrow в Netwire 5?. Базовая структура интерактивной программы, использующей Kleisli Wires, показана в этом примере: Интерактивность консоли в Netwire?. Чтобы построить провод Клейсли из чего-либо типа a -> m b
, нам понадобятся:
mkKleisli :: (Monad m, Monoid e) => (a -> m b) -> Wire s e m a b
mkKleisli f = mkGen_ $ \a -> liftM Right $ f a
Затем, поскольку мне не удалось trace
работать с процессами Arrow, создается отладочная сеть для вывода объектов на консоль:
wDebug :: (Show a, Monoid e) => Wire s e IO a ()
wDebug = mkKleisli $ \a -> putStrLn $ show a
А теперь пора написать несколько функций, которые нужно перенести на провода. Для вывода нам нужна функция, которая возвращает SDL.Surface
с правильным прямоугольником, нарисованным с учетом координаты X площадки:
padSurf :: SDL.Surface
-> Int
-> IO SDL.Surface
padSurf surf x' = do
let rect' = SDL.Rect x' 500 100 50
clipRect <- SDL.getClipRect surf
SDL.fillRect surf (Just clipRect) (SDL.Pixel 0x00000000)
SDL.fillRect surf (Just rect') (SDL.Pixel 0xFFFFFFFF)
return surf
Будьте осторожны, эта функция выполняет деструктивные обновления. Пропущенная поверхность будет позже перенесена на поверхность окна.
Теперь у нас есть поверхность. Выходной провод тогда тривиален:
wTestOutput :: SDL.Surface -> Wire s () IO () SDL.Surface
wTestOutput surf = mkKleisli $ \_ -> testPad
where
testPad = padSurf surf 350
Затем мы соединяем провода и немного поиграем с ними:
gameWire :: SDL.Surface
-> Wire s () IO () SDL.Surface
gameWire w = proc _ -> do
finalSurf <- wTestOutput w -< ()
wDebug -< "Try a debug message"
returnA -< finalSurf
Наконец, меняем main
и правильно водим провода:
main :: IO ()
main = do
SDL.init [SDL.InitEverything]
w <- SDL.setVideoMode 800 600 32 [SDL.SWSurface]
s <- SDL.createRGBSurfaceEndian [SDL.SWSurface] 800 600 32
run w (countSession_ 1) $ gameWire w
SDL.quit
run ::SDL.Surface -> Session IO s -> Wire s () IO () SDL.Surface -> IO ()
run mainSurf s w = do
(ds, s') <- stepSession s
(eSrcSurf, w') <- stepWire w ds (Right ())
case eSrcSurf of
Right srcSurf -> do
SDL.blitSurface srcSurf (Nothing) mainSurf (Nothing)
SDL.flip mainSurf
SDL.delay 30
run mainSurf s' w'
_ -> return ()
Обратите внимание, что если хотите, вы также можете сделать еще один провод для обработки поверхности главного окна (и это проще и лучше, чем моя текущая реализация), но мне было слишком поздно и лень добавить это. Посмотрите интерактивный пример, который я упомянул выше, чтобы увидеть, насколько простым может быть run
(он может стать еще проще, если в этом примере используется ингибирование вместо quitWire
).
Когда программа запущена, ее внешний вид должен быть таким же, как и раньше.
Вот полный код:
{-|
01-OutputWires.hs: This step, the output wires are constructed first for
easy debugging
-}
{-# LANGUAGE Arrows #-}
module Main where
import Prelude hiding ((.), id)
import Control.Wire
import Control.Arrow
import Control.Monad
import Data.Monoid
import qualified Graphics.UI.SDL as SDL
{- Wire Utilities -}
-- | Make a Kleisli wire
mkKleisli :: (Monad m, Monoid e) => (a -> m b) -> Wire s e m a b
mkKleisli f = mkGen_ $ \a -> liftM Right $ f a
-- | The debug wire
wDebug :: (Show a, Monoid e) => Wire s e IO a ()
wDebug = mkKleisli $ \a -> putStrLn $ show a
{- Functions to be lifted -}
padSurf :: SDL.Surface
-- ^ Previous state of surface
-> Int
-- ^ X'
-- | New state
-> IO SDL.Surface
padSurf surf x' = do
let rect' = SDL.Rect x' 500 100 50
clipRect <- SDL.getClipRect surf
SDL.fillRect surf (Just clipRect) (SDL.Pixel 0x00000000)
SDL.fillRect surf (Just rect') (SDL.Pixel 0xFFFFFFFF)
return surf
{- Wires -}
wTestOutput :: SDL.Surface -> Wire s () IO () SDL.Surface
wTestOutput surf = mkKleisli $ \_ -> testPad
where
testPad = padSurf surf 350
-- | This is the main game wire
gameWire :: SDL.Surface
-- ^ The main surface (i.e. the window)
-> Wire s () IO () SDL.Surface
gameWire w = proc _ -> do
finalSurf <- wTestOutput w -< ()
wDebug -< "Try a debug message"
returnA -< finalSurf
main :: IO ()
main = do
SDL.init [SDL.InitEverything]
w <- SDL.setVideoMode 800 600 32 [SDL.SWSurface]
s <- SDL.createRGBSurfaceEndian [SDL.SWSurface] 800 600 32
run w (countSession_ 1) $ gameWire w
SDL.quit
run ::SDL.Surface -> Session IO s -> Wire s () IO () SDL.Surface -> IO ()
run mainSurf s w = do
(ds, s') <- stepSession s
(eSrcSurf, w') <- stepWire w ds (Right ())
case eSrcSurf of
Right srcSurf -> do
SDL.blitSurface srcSurf (Nothing) mainSurf (Nothing)
SDL.flip mainSurf
SDL.delay 30
run mainSurf s' w'
_ -> return ()
Входные провода
В этом разделе мы собираемся построить провода, по которым игрок вводит данные в программу.
Поскольку в логической части мы будем использовать дискретные события, нам понадобится тип данных для игровых событий:
data GameEvent = MoveR
| MoveL
| NoEvent
deriving (Show, Eq)
-- | Make it Monoid so that game events can be combined
-- (Only applicable in this "game"!)
instance Monoid GameEvent where
mempty = NoEvent
-- | Simultaneously moving left and right is just nothing
MoveR `mappend` MoveL = NoEvent
MoveL `mappend` MoveR = NoEvent
-- | NoEvent is the identity
NoEvent `mappend` x = x
x `mappend` NoEvent = x
x `mappend` y
-- | Make sure identical events return same events
| x == y = x
-- | Otherwise, no event
| otherwise = NoEvent
Как предполагается в комментарии, экземпляр Monoid
применяется только к этой конкретной игре, поскольку он имеет только две противоположные операции: левую и правую.
Сначала мы будем опрашивать события из SDL:
pollEvents :: [SDL.Event] -> IO (Either () ([SDL.Event]))
pollEvents es = do
e <- SDL.pollEvent
case e of
SDL.NoEvent -> return $ Right es
SDL.Quit -> return $ Left ()
_ -> pollEvents $ e:es
Очевидно, что эта функция опрашивает события из SDL в виде списка и запрещает получение события Quit
.
Затем нам нужно проверить, является ли событие событием клавиатуры:
isKeyEvent :: SDL.Event -> Bool
isKeyEvent (SDL.KeyDown k) = True
isKeyEvent (SDL.KeyUp k) = True
isKeyEvent _ = False
У нас будет список клавиш, которые в данный момент нажаты, и он должен обновляться при возникновении события клавиатуры. Короче говоря, когда клавиша не работает, вставьте ее в список, и наоборот:
keyStatus :: [SDL.Keysym] -> [SDL.Event] -> [SDL.Keysym]
keyStatus keysDown (e:es) =
case e of
-- | If a KeyDown is detected, add key to list
SDL.KeyDown k -> keyStatus (k:keysDown) es
-- | If a KeyUp is detected, remove key from list
SDL.KeyUp k -> keyStatus (filter (/= k) keysDown) es
_ -> keyStatus keysDown es
keyStatus keysDown [] = keysDown
Затем мы пишем функцию для преобразования события клавиатуры в игровое событие:
toGameEv :: SDL.Keysym -> GameEvent
toGameEv (SDL.Keysym SDL.SDLK_RIGHT _ _) = MoveR
toGameEv (SDL.Keysym SDL.SDLK_LEFT _ _) = MoveL
toGameEv _ = NoEvent
Мы сбрасываем игровые события и получаем одно событие (действительно, действительно, специфичное для игры!):
fireGameEv :: [SDL.Keysym] -> GameEvent
fireGameEv ks = foldl mappend NoEvent $ fmap toGameEv ks
Теперь можно приступить к изготовлению проволоки.
Во-первых, нам нужен провод, который опрашивает события:
wPollEvents :: Wire s () IO () [SDL.Event]
wPollEvents = mkGen_ $ \_ -> pollEvents []
Обратите внимание, что mkKleisli
создает провод, который не запрещает, но мы хотим запретить этот провод, поскольку программа должна завершиться, когда это необходимо. Поэтому здесь мы используем mkGen_
.
Затем нам нужно отфильтровать события. Во-первых, создайте вспомогательную функцию, которая создает провод непрерывного временного фильтра:
mkFW_ :: (Monad m, Monoid e) => (a -> Bool) -> Wire s e m [a] [a]
mkFW_ f = mkSF_ $ filter f
Используйте mkFW_
, чтобы создать фильтр:
wKeyEvents :: (Monad m, Monoid e) => Wire s e m [SDL.Event] [SDL.Event]
wKeyEvents = mkFW_ isKeyEvent
Затем нам понадобится еще одна удобная функция для создания проводника с отслеживанием состояния из функции с отслеживанием состояния типа b -> a -> b
:
mkSW_ :: (Monad m, Monoid e) => b -> (b->a->b) -> Wire s e m a b
mkSW_ b0 f = mkSFN $ g b0
where
g b0 a = let b1 = f b0 a in
(b1, mkSW_ b1 f)
Затем создайте провод с отслеживанием состояния, который запоминает все ключевые состояния:
wKeyStatus :: (Monad m, Monoid e) => Wire s e m [SDL.Event] [SDL.Keysym]
wKeyStatus = mkSW_ empty keyStatus
Последний кусок провода запускает игровое событие:
wFireGameEv :: (Monad m, Monoid e) => Wire s e m [SDL.Keysym] (GameEvent)
wFireGameEv = arr fireGameEv
Чтобы активно запускать дискретные события (события netwire), которые содержат игровые события, нам нужно немного взломать netwire (я думаю, что он все еще не завершен), поскольку он не предоставляет провод, который всегда запускает события:
always :: (Monad m, Monoid e) => Wire s e m a (Event a)
always = mkSFN $ \x -> (WE.Event x, always)
По сравнению с реализацией now
, разница только в never
и always
.
Наконец, большой провод, объединяющий все входные провода, указанные выше:
wGameInput :: Wire s () IO () (Event GameEvent)
wGameInput = proc _ -> do
ge <- wFireGameEv <<< wKeyStatus
<<< wKeyEvents <<< wPollEvents -< ()
e <- always -< ge
-- Debug!
case e of
WE.NoEvent -> wDebug -< "No Event?!!"
WE.Event g -> wDebug -< "Game Event: " ++ show g
-- End Debug
returnA -< e
В этом проводе также показан пример отладки.
Чтобы взаимодействовать с основной программой, измените gameWire
, чтобы использовать ввод:
gameWire w = proc _ -> do
ev <- wGameInput -< ()
finalSurf <- wTestOutput w -< ()
returnA -< finalSurf
Больше ничего менять не нужно. Что ж, интересно, не правда ли?
Когда программа запущена, консоль выдает много вывода, показывающего запущенные текущие игровые события. Попробуйте нажимать влево и вправо и их комбинации и посмотрите, ожидается ли такое поведение. Конечно, прямоугольник не двинется.
Вот огромный блок кода:
{-|
02-InputWires.hs: This step, input wires are constructed and
debugged by using wDebug
-}
{-# LANGUAGE Arrows #-}
module Main where
import Prelude hiding ((.), id)
import Control.Wire
import Control.Arrow
import Control.Monad
import Data.Monoid
import qualified Graphics.UI.SDL as SDL
import qualified Control.Wire.Unsafe.Event as WE
{- Data types -}
-- | The unified datatype of game events
data GameEvent = MoveR
| MoveL
| NoEvent
deriving (Show, Eq)
-- | Make it Monoid so that game events can be combined
-- (Only applicable in this "game"!)
instance Monoid GameEvent where
mempty = NoEvent
-- | Simultaneously moving left and right is just nothing
MoveR `mappend` MoveL = NoEvent
MoveL `mappend` MoveR = NoEvent
-- | NoEvent is the identity
NoEvent `mappend` x = x
x `mappend` NoEvent = x
x `mappend` y
-- | Make sure identical events return same events
| x == y = x
-- | Otherwise, no event
| otherwise = NoEvent
{- Wire Utilities -}
-- | Make a stateless filter wire
mkFW_ :: (Monad m, Monoid e) => (a -> Bool) -> Wire s e m [a] [a]
mkFW_ f = mkSF_ $ filter f
-- -- | Make a stateful wire from a chained stateful function and initial value
-- -- The function (a -> b -> a) takes in an old state /a/, and returns state
-- -- transition function (b -> a).
mkSW_ :: (Monad m, Monoid e) => b -> (b->a->b) -> Wire s e m a b
mkSW_ b0 f = mkSFN $ g b0
where
g b0 a = let b1 = f b0 a in
(b1, mkSW_ b1 f)
-- | Make a Kleisli wire
mkKleisli :: (Monad m, Monoid e) => (a -> m b) -> Wire s e m a b
mkKleisli f = mkGen_ $ \a -> liftM Right $ f a
-- | The debug wire
wDebug :: (Show a, Monoid e) => Wire s e IO a ()
wDebug = mkKleisli $ \a -> putStrLn $ show a
-- | The "always" wire
always :: (Monad m, Monoid e) => Wire s e m a (Event a)
always = mkSFN $ \x -> (WE.Event x, always)
{- Functions to be lifted -}
-- | This is the pad surface whose X coordinate can be updated
padSurf :: SDL.Surface
-- ^ Previous state of surface
-> Int
-- ^ X'
-- | New state
-> IO SDL.Surface
padSurf surf x' = do
let rect' = SDL.Rect x' 500 100 50
clipRect <- SDL.getClipRect surf
SDL.fillRect surf (Just clipRect) (SDL.Pixel 0x00000000)
SDL.fillRect surf (Just rect') (SDL.Pixel 0xFFFFFFFF)
return surf
-- | The function to poll events and add to a list of events
pollEvents :: [SDL.Event] -> IO (Either () ([SDL.Event]))
pollEvents es = do
e <- SDL.pollEvent
case e of
SDL.NoEvent -> return $ Right es
SDL.Quit -> return $ Left ()
_ -> pollEvents $ e:es
-- | Checks whether one SDL.Event is a keyboard event
isKeyEvent :: SDL.Event -> Bool
isKeyEvent (SDL.KeyDown k) = True
isKeyEvent (SDL.KeyUp k) = True
isKeyEvent _ = False
-- | The raw function to process key status from events
keyStatus :: [SDL.Keysym] -> [SDL.Event] -> [SDL.Keysym]
keyStatus keysDown (e:es) =
case e of
-- | If a KeyDown is detected, add key to list
SDL.KeyDown k -> keyStatus (k:keysDown) es
-- | If a KeyUp is detected, remove key from list
SDL.KeyUp k -> keyStatus (filter (/= k) keysDown) es
_ -> keyStatus keysDown es
-- | If all events are processed, return
keyStatus keysDown [] = keysDown
-- | Convert a SDL Keysym into "standard" game events
toGameEv :: SDL.Keysym -> GameEvent
toGameEv (SDL.Keysym SDL.SDLK_RIGHT _ _) = MoveR
toGameEv (SDL.Keysym SDL.SDLK_LEFT _ _) = MoveL
toGameEv _ = NoEvent
-- | Combine all game events to get one single firing
fireGameEv :: [SDL.Keysym] -> GameEvent
fireGameEv ks = foldl mappend NoEvent $ fmap toGameEv ks
{- Wires -}
-- | The Kleisli wire to poll events
wPollEvents :: Wire s () IO () [SDL.Event]
wPollEvents = mkGen_ $ \_ -> pollEvents []
-- | A stateless wire that filters out keyboard events
wKeyEvents :: (Monad m, Monoid e) => Wire s e m [SDL.Event] [SDL.Event]
wKeyEvents = mkFW_ isKeyEvent
-- | A stateful wire to keep track of key status
wKeyStatus :: (Monad m, Monoid e) => Wire s e m [SDL.Event] [SDL.Keysym]
wKeyStatus = mkSW_ empty keyStatus
-- | A wire to fire game events from SDL events
wFireGameEv :: (Monad m, Monoid e) => Wire s e m [SDL.Keysym] (GameEvent)
wFireGameEv = arr fireGameEv
-- | This is the connected wire for the entire game input
wGameInput :: Wire s () IO () (Event GameEvent)
wGameInput = proc _ -> do
ge <- wFireGameEv <<< wKeyStatus
<<< wKeyEvents <<< wPollEvents -< ()
e <- always -< ge
-- Debug!
case e of
WE.NoEvent -> wDebug -< "No Event?!!"
WE.Event g -> wDebug -< "Game Event: " ++ show g
-- End Debug
returnA -< e
-- | The wire to test output
wTestOutput :: SDL.Surface -> Wire s () IO () SDL.Surface
wTestOutput surf = mkKleisli $ \_ -> testPad
where
testPad = padSurf surf 350
-- | This is the main game wire
gameWire :: SDL.Surface
-- ^ The main surface (i.e. the window)
-> Wire s () IO () SDL.Surface
gameWire w = proc _ -> do
ev <- wGameInput -< ()
finalSurf <- wTestOutput w -< ()
returnA -< finalSurf
main :: IO ()
main = do
SDL.init [SDL.InitEverything]
w <- SDL.setVideoMode 800 600 32 [SDL.SWSurface]
s <- SDL.createRGBSurfaceEndian [SDL.SWSurface] 800 600 32
run w (countSession_ 1) $ gameWire w
SDL.quit
run ::SDL.Surface -> Session IO s -> Wire s () IO () SDL.Surface -> IO ()
run mainSurf s w = do
(ds, s') <- stepSession s
(eSrcSurf, w') <- stepWire w ds (Right ())
case eSrcSurf of
Right srcSurf -> do
SDL.blitSurface srcSurf (Nothing) mainSurf (Nothing)
SDL.flip mainSurf
SDL.delay 30
run mainSurf s' w'
_ -> return ()
Логика игры --- Наконец-то собрали все воедино!
Сначала мы пишем интегрирующую функцию положения X контактной площадки:
padDX :: Int -> GameEvent -> Int
padDX x0 e
| x > 700 = 700
| x < 0 = 0
| otherwise = x
where
x = x0 + go e
go MoveR = dx
go MoveL = -dx
go _ = 0
dx = 15
Я все жестко закодировал, но для этого минималистичного примера это не важно. Это должно быть просто.
Затем мы создаем провод, который представляет текущее положение контактной площадки:
wPadX :: (Monad m, Monoid e) => Wire s e m (Event GameEvent) Int
wPadX = accumE padDX 400 >>> hold
hold
содержит самое последнее значение потока дискретных событий.
Затем мы объединяем все логические элементы в большой логический провод:
wGameLogic :: Wire s () IO (Event GameEvent) Int
wGameLogic = proc ev -> do
x' <- wPadX -< ev
returnA -< x'
Поскольку у нас есть одно состояние о координате X, нам нужно изменить выходной провод:
wGameOutput :: SDL.Surface -> Wire s () IO Int SDL.Surface
wGameOutput surf = mkKleisli $ testPad
where
testPad = padSurf surf
Наконец, мы объединяем все в gameWire
:
gameWire w = proc _ -> do
ev <- wGameInput -< ()
x <- wGameLogic -< ev
finalSurf <- wGameOutput w -< x
returnA -< finalSurf
Ничего не нужно менять в main
и run
. Вау!
И вот оно! Запустите его, и вы сможете перемещать прямоугольник влево и вправо!
ГИГАНТНЫЙ блок кода (мне любопытно, сколько времени будет длиться программа на C ++, которая делает то же самое):
{-|
03-GameLogic.hs: The final product!
-}
{-# LANGUAGE Arrows #-}
module Main where
import Prelude hiding ((.), id)
import Control.Wire
import Control.Arrow
import Control.Monad
import Data.Monoid
import qualified Graphics.UI.SDL as SDL
import qualified Control.Wire.Unsafe.Event as WE
{- Data types -}
-- | The unified datatype of game events
data GameEvent = MoveR
| MoveL
| NoEvent
deriving (Show, Eq)
-- | Make it Monoid so that game events can be combined
-- (Only applicable in this "game"!)
instance Monoid GameEvent where
mempty = NoEvent
-- | Simultaneously moving left and right is just nothing
MoveR `mappend` MoveL = NoEvent
MoveL `mappend` MoveR = NoEvent
-- | NoEvent is the identity
NoEvent `mappend` x = x
x `mappend` NoEvent = x
x `mappend` y
-- | Make sure identical events return same events
| x == y = x
-- | Otherwise, no event
| otherwise = NoEvent
{- Wire Utilities -}
-- | Make a stateless filter wire
mkFW_ :: (Monad m, Monoid e) => (a -> Bool) -> Wire s e m [a] [a]
mkFW_ f = mkSF_ $ filter f
-- -- | Make a stateful wire from a chained stateful function and initial value
-- -- The function (a -> b -> a) takes in an old state /a/, and returns state
-- -- transition function (b -> a).
mkSW_ :: (Monad m, Monoid e) => b -> (b->a->b) -> Wire s e m a b
mkSW_ b0 f = mkSFN $ g b0
where
g b0 a = let b1 = f b0 a in
(b1, mkSW_ b1 f)
-- | Make a Kleisli wire
mkKleisli :: (Monad m, Monoid e) => (a -> m b) -> Wire s e m a b
mkKleisli f = mkGen_ $ \a -> liftM Right $ f a
-- | The debug wire
wDebug :: (Show a, Monoid e) => Wire s e IO a ()
wDebug = mkKleisli $ \a -> putStrLn $ show a
-- | The "always" wire
always :: (Monad m, Monoid e) => Wire s e m a (Event a)
always = mkSFN $ \x -> (WE.Event x, always)
{- Functions to be lifted -}
-- | This is the pad surface whose X coordinate can be updated
padSurf :: SDL.Surface
-- ^ Previous state of surface
-> Int
-- ^ X'
-- | New state
-> IO SDL.Surface
padSurf surf x' = do
let rect' = SDL.Rect x' 500 100 50
clipRect <- SDL.getClipRect surf
SDL.fillRect surf (Just clipRect) (SDL.Pixel 0x00000000)
SDL.fillRect surf (Just rect') (SDL.Pixel 0xFFFFFFFF)
return surf
-- | The function to poll events and add to a list of events
pollEvents :: [SDL.Event] -> IO (Either () ([SDL.Event]))
pollEvents es = do
e <- SDL.pollEvent
case e of
SDL.NoEvent -> return $ Right es
SDL.Quit -> return $ Left ()
_ -> pollEvents $ e:es
-- | Checks whether one SDL.Event is a keyboard event
isKeyEvent :: SDL.Event -> Bool
isKeyEvent (SDL.KeyDown k) = True
isKeyEvent (SDL.KeyUp k) = True
isKeyEvent _ = False
-- | The raw function to process key status from events
keyStatus :: [SDL.Keysym] -> [SDL.Event] -> [SDL.Keysym]
keyStatus keysDown (e:es) =
case e of
-- | If a KeyDown is detected, add key to list
SDL.KeyDown k -> keyStatus (k:keysDown) es
-- | If a KeyUp is detected, remove key from list
SDL.KeyUp k -> keyStatus (filter (/= k) keysDown) es
_ -> keyStatus keysDown es
-- | If all events are processed, return
keyStatus keysDown [] = keysDown
-- | Convert a SDL Keysym into "standard" game events
toGameEv :: SDL.Keysym -> GameEvent
toGameEv (SDL.Keysym SDL.SDLK_RIGHT _ _) = MoveR
toGameEv (SDL.Keysym SDL.SDLK_LEFT _ _) = MoveL
toGameEv _ = NoEvent
-- | Combine all game events to get one single firing
fireGameEv :: [SDL.Keysym] -> GameEvent
fireGameEv ks = foldl mappend NoEvent $ fmap toGameEv ks
-- | The integrator of X position of pad
padDX :: Int -> GameEvent -> Int
padDX x0 e
| x > 700 = 700
| x < 0 = 0
| otherwise = x
where
x = x0 + go e
go MoveR = dx
go MoveL = -dx
go _ = 0
dx = 15
{- Wires -}
-- | The Kleisli wire to poll events
wPollEvents :: Wire s () IO () [SDL.Event]
wPollEvents = mkGen_ $ \_ -> pollEvents []
-- | A stateless wire that filters out keyboard events
wKeyEvents :: (Monad m, Monoid e) => Wire s e m [SDL.Event] [SDL.Event]
wKeyEvents = mkFW_ isKeyEvent
-- | A stateful wire to keep track of key status
wKeyStatus :: (Monad m, Monoid e) => Wire s e m [SDL.Event] [SDL.Keysym]
wKeyStatus = mkSW_ empty keyStatus
-- | A wire to fire game events from SDL events
wFireGameEv :: (Monad m, Monoid e) => Wire s e m [SDL.Keysym] (GameEvent)
wFireGameEv = arr fireGameEv
-- | This is the connected wire for the entire game input
wGameInput :: Wire s () IO () (Event GameEvent)
wGameInput = proc _ -> do
ge <- wFireGameEv <<< wKeyStatus
<<< wKeyEvents <<< wPollEvents -< ()
e <- always -< ge
returnA -< e
-- | The stateful wire of X position of pad
wPadX :: (Monad m, Monoid e) => Wire s e m (Event GameEvent) Int
wPadX = accumE padDX 400 >>> hold
-- | This is the connected wire for the entire game logic
wGameLogic :: Wire s () IO (Event GameEvent) Int
wGameLogic = proc ev -> do
x' <- wPadX -< ev
returnA -< x'
-- | The wire of output
wGameOutput :: SDL.Surface -> Wire s () IO Int SDL.Surface
wGameOutput surf = mkKleisli $ testPad
where
testPad = padSurf surf
-- | This is the main game wire
gameWire :: SDL.Surface
-- ^ The main surface (i.e. the window)
-> Wire s () IO () SDL.Surface
gameWire w = proc _ -> do
ev <- wGameInput -< ()
x <- wGameLogic -< ev
finalSurf <- wGameOutput w -< x
returnA -< finalSurf
main :: IO ()
main = do
SDL.init [SDL.InitEverything]
w <- SDL.setVideoMode 800 600 32 [SDL.SWSurface]
s <- SDL.createRGBSurfaceEndian [SDL.SWSurface] 800 600 32
run w (countSession_ 1) $ gameWire w
SDL.quit
run ::SDL.Surface -> Session IO s -> Wire s () IO () SDL.Surface -> IO ()
run mainSurf s w = do
(ds, s') <- stepSession s
(eSrcSurf, w') <- stepWire w ds (Right ())
case eSrcSurf of
Right srcSurf -> do
SDL.blitSurface srcSurf (Nothing) mainSurf (Nothing)
SDL.flip mainSurf
SDL.delay 30
run mainSurf s' w'
_ -> return ()
person
Carl Dong
schedule
01.10.2015
IO
в любом случае не является функцией. Вам нужны новые ментальные модели - те, которые у вас есть, сбивают вас с пути. - person Carl   schedule 18.06.2015body
функция имеет типExceptT String (StateT GameState IO) ()
- StateT для игровой логики, ExceptT для выхода, IO для ... ну, IO - тогда вся ваша игра будетControl.Monad.forever body
. - person user2407038   schedule 18.06.2015(>>=)
вместоguessSession
, верно? - person Carl Dong   schedule 18.06.2015State
, если возможно. - person Carl Dong   schedule 18.06.2015