Какой может быть минимальный пример игры, написанной на Haskell?

Обновить через три месяца

У меня есть ответ ниже с использованием netwire-5.0.1 + sdl в структуре функционального реактивного программирования с использованием стрелок и стрелок Клейсли для ввода-вывода. Хотя это слишком просто, чтобы называться «игрой», оно должно быть очень сложным и расширяемым.

Исходный

Я только изучаю Haskell и пытаюсь сделать из него небольшую игру. Однако хотелось бы посмотреть, какой структурой может быть небольшая (каноническая) текстовая игра. Я также стараюсь, чтобы код был как можно более чистым. Сейчас я изо всех сил пытаюсь понять, как реализовать:

  1. Основной цикл. Здесь есть пример Как написать игровой цикл на Haskell ? но кажется, что принятый ответ не является хвостовым рекурсивным. Я не совсем уверен, имеет ли это значение. В моем понимании использование памяти будет расти, не так ли?
  2. Состояние перехода. Я думаю, что это имеет прямое отношение к первому. Я пробовал немного использовать State и что-то в http://www.gamedev.net/page/resources/_/technical/game-programming/haskell-game-object-design-or-how-functions-can-get-you-apples-r3204, но хотя отдельные компоненты могут работать и обновляться за конечные шаги, я не понимаю, как это можно использовать в бесконечном цикле.

Если возможно, я хотел бы увидеть минимальный пример, который в основном:

  1. Многократно просит игрока ввести что-нибудь
  2. Когда какое-то условие выполнено, измените состояние
  3. Когда встретится какое-то другое условие, выйдите
  4. Теоретически может работать бесконечно долго без потери памяти

У меня нет кода для отправки, потому что я не могу получить самые простые вещи. В любых других материалах / примерах, которые я нашел в сети, для управления событиями используются другие библиотеки, например SDL или GTK. Единственное, что я нашел полностью написано на Haskell, - это http://jpmoresmau.blogspot.com/2006/11/my-first-haskell-adventure-game.html, но он тоже не похож на хвостовую рекурсию в своем основном цикле (опять же, я не знаю, имеет ли это значение).

Или, возможно, Haskell не предназначен для подобных вещей? Или, возможно, мне следует поместить main в C?

Изменить 1

Поэтому я изменил небольшой пример в https://wiki.haskell.org/Simple_StateT_use и сделал его даже попроще (и это не соответствует моим критериям):

module Main where
import Control.Monad.State

main = do 
  putStrLn "I'm thinking of a number between 1 and 100, can you guess it?"
  guesses <- execStateT (guessSession answer) 0
  putStrLn $ "Success in " ++ (show guesses) ++ " tries."
  where
    answer = 10

guessSession :: Int -> StateT Int IO ()
guessSession answer =
    do gs <- lift getLine    -- get guess from user
       let g = read gs       -- convert to number
       modify (+1)           -- increment number of guesses
       case g of
         10 -> do lift $ putStrLn "Right"
         _ -> do lift $ putStrLn "Continue"
                 guessSession answer

Однако в конечном итоге это приведет к переполнению памяти. Я тестировал с

bash prompt$ yes 1 | ./Test-Game

и использование памяти начинает линейно расти.

Изменить 2

Хорошо, я нашел рекурсию Haskell и использование памяти и получил некоторое представление о «стеке» ... Так что что-то не так с моим методом тестирования?


person Carl Dong    schedule 18.06.2015    source источник
comment
Haskell - это не другой язык, который вы использовали. Вызов функций не использует стек. И IO в любом случае не является функцией. Вам нужны новые ментальные модели - те, которые у вас есть, сбивают вас с пути.   -  person Carl    schedule 18.06.2015
comment
Я бы не стал предлагать эту приключенческую игру в качестве модели. Код выглядит недостаточно структурированным и практически нечитаемым.   -  person dfeuer    schedule 18.06.2015
comment
Хвостовая рекурсия не так важна для поведения памяти в Haskell, как в языках с вызовом по значению. Более близкая ментальная модель того, что вам нужно, - это хвостовая рекурсия по модулю cons, менее распространенная схема, но фактическая операционная семантика Haskell основана на редукции графов. Лучший способ подумать об этом - это, вероятно, сократить количество выражений вручную, работая всегда снаружи внутрь.   -  person dfeuer    schedule 18.06.2015
comment
По сути, ваша игра представляет собой большой цикл while. Напишите тело цикла - функцию, реализующую логику игры - а все остальное тривиально. Вы, безусловно, должны использовать возможности StateT и других преобразователей монад. Скажем, ваша body функция имеет тип ExceptT String (StateT GameState IO) () - StateT для игровой логики, ExceptT для выхода, IO для ... ну, IO - тогда вся ваша игра будет Control.Monad.forever body.   -  person user2407038    schedule 18.06.2015
comment
Так что, даже если это не хвостовая рекурсия, мне не нужно беспокоиться о переполнении стека?   -  person Carl Dong    schedule 18.06.2015
comment
Но согласно wiki.haskell.org/Stack_overflow, кажется, что я все еще могу получить переполнение стека, если я не используйте хвостовую рекурсию. Как мне реализовать цикл while в Haskell?   -  person Carl Dong    schedule 18.06.2015
comment
@Carl Я тестировал его, и, по-видимому, цикл без хвостовой рекурсии стоит стека. Я думаю, что последний вызов функции (>>=) вместо guessSession, верно?   -  person Carl Dong    schedule 18.06.2015
comment
Выражения, оценка которых требует оценки пространства стека стоимости глубоко вложенного графа. Вы можете создавать их с хвостовой рекурсией или без нее. Вы также можете создавать выражения, которые не требуют оценки глубоко вложенных графов с хвостовой рекурсией или без нее. Дело в том, что это отвлекающий маневр. Код на Haskell работает не так, как вы думаете.   -  person Carl    schedule 18.06.2015
comment
Прочитав больше о том, как работает haskell, я теперь понимаю. Я также постараюсь сделать один, используя State, если возможно.   -  person Carl Dong    schedule 18.06.2015


Ответы (2)


Предисловие

После 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
comment
Неужели нет лучшего способа выяснить, какие клавиши не работают? Я ожидал, что такое будет в библиотеке. - person dfeuer; 02.10.2015
comment
Но поскольку я использую SDL, мне нужно сделать некоторые функции - person Carl Dong; 02.10.2015
comment
Похоже, что mkSW_ следует вероятно использовать accumE вместе с одним из переключателей от Control.Wire.Switch, но я никогда не использовал Netwire. В любом случае, хорошая работа, сводя все это вместе с объяснениями. - person dfeuer; 02.10.2015
comment
mkSW_ - это настраиваемая функция для построения проводов с отслеживанием состояния, а accumE работает с событиями. Но я не думаю, что мне нужны переключатели для этой простой игры. - person Carl Dong; 02.10.2015

Ваша проблема в том, что вы используете ленивую версию преобразователя StateT, которая создает массивный преобразователь из повторяющихся modifys (потому что они никогда не оцениваются полностью). Если вместо этого вы импортируете Control.Monad.State.Strict, он, вероятно, будет нормально работать без каких-либо переполнений.

person Fraser    schedule 18.06.2015
comment
Ленивая версия в целом менее удобна, поэтому следует использовать строгую, если нет веской причины использовать ленивую версию. Строгость исходит не от грязных уловок с seq, а от простого сопоставления с образцом. - person dfeuer; 18.06.2015