Haskellプログラマはゲームプログラミングに何を見たか?(実例編)

ええ……、2日ほど遅延評価の迷宮をさまよっていた。
Haskellは勉強すればするほどどんどん深みにはまっていくような気がする。
当初のもくろみは今回もそんなに外れてはいなかったが、
まだまだ工夫の余地がたくさんある。たくさん有りすぎて大変だ。
Haskellでコードを書いているとき結構迷いが生じるのだが(私だけだろうか…)、
実装するための方法がいっぱいあってどれを選択すれば良いのかわからない。
一般的なノウハウは無いのだろうか、はたまた例のごとく私が知らないだけなのか。


というわけで(どういうわけでだ…)、
習作として以前作ったテトリスを全編遅延ストリームで記述してみた。
正直いろいろうまくない点はあったが、収穫も色々有った。
まず概要は三日前?の通りで。

main = do
  ...
  sur  <- setVideoMode 640 480 32 [HWSURFACE,ANYFORMAT]
  imgs <- mapM loadBMP images
  seed <- newStdGen
  et   <- elapseTime 60
  getKeys et >>= process sur (seed,imgs) entryPoint
  ...

getKeysは遅延したキー状態のリスト、
processはそれを処理したものをentryPointに渡しつつ、
そのレスポンスを解釈する。
レスポンスは前回考察したコンビネータライブラリを用いて構築する。

process sur imgs entry dat = processResult sur $ entry imgs funcs where
  funcs = map mkKeyProc $ zip (:dat) dat

processResult :: Surface -> [Scr] -> IO ()
processResult _  = return ()
processResult bg (sc:scs) = do
  quit <- processEvents
  ks   <- getKeyState
  unless (quit || pushExit ks) $ do
    fillRect bg Nothing 0x000000
    drawScr bg sc
    flipSurface bg
    processResult bg scs

processEvents = do
  e <- pollEvent
  case e of Nothing -> return False
            Just QuitEvent -> return True
            _ -> processEvents

pushExit ks =
  SDLK_ESCAPE `elem` ks ||
  all (`elem` ks) [SDLK_LALT,SDLK_F4] ||
  all (`elem` ks) [SDLK_RALT,SDLK_F4]

entryPointがゲームの処理になる。

entryPoint (seed,imgs@[...]) fs = ...

前回のScrとかの実装は次のように行った。

 ----------------------------------------------
 -- graphics combinator library

type Scr = ScrState -> IO ()
type ScrState = (Surface,Point,Double)

drawScr :: Surface -> Scr -> IO ()
drawScr bg sc = execScr sc (bg,pt 0 0,1.0)

execScr :: Scr -> ScrState -> IO ()
execScr sc st = sc st

 -----

emptyScr :: Scr
emptyScr _ = return ()

image :: Surface -> (Maybe Rect) -> Scr
image sur rc (bg,pos,alp) = do
  setAlpha sur [SRCALPHA] (boundEnum (0,255) $ ceiling $ alp*255)
  blitSurface sur rc bg pos
  return ()
  where
    boundEnum (mini,maxi) = toEnum . max mini . min maxi

img :: Surface -> Scr
img sur = image sur Nothing

subImg :: Surface -> Rect -> Scr
subImg sur rc = image sur (Just rc)

trans :: Point -> Scr -> Scr
trans (Point dx dy) scr (bg,Point x y,alp) =
  scr (bg,Point (x+dx) (y+dy),alp)

alpha :: Double -> Scr -> Scr
alpha a scr (bg,pos,alp) =
  scr (bg,pos,alp*a)

infixr 4 <$$>
(<$$>) :: Scr -> Scr -> Scr
scr1 <$$> scr2 = \s -> do
  execScr scr1 s
  execScr scr2 s

scrs :: [Scr] -> Scr
scrs = foldl (<$$>) emptyScr

なんだか重なり合うイメージに対してα値を設定したときに
変な透け具合になるような気がするのだが、まぁ、よし…。


いよいよ遅延評価によるコルーチン使用の隠蔽の実例を挙げてみる。

  -- タイトルスクリーン
  opening = loop $ cycle pat where
    loop (al:als) (s:ss) = (trans posp (alpha al push) <$$> trans post tl) :
      case s GKRotate of
        Pushed -> game ss -- ゲームへ
        _      -> loop als ss

    pat = [0,1/60 .. 1] ++ replicate 1 30 ++ [1,1-1/60 .. 0] -- 明滅パターン
    posp = pt 220 300 -- push z key 位置
    post = pt 20 10   -- タイトル 位置

  tl   = (タイトルロゴ画像)
  push = (push z keyと書かれた画像)

これはタイトルスクリーン(画面の上半分ぐらいにロゴが表示されて、
下半分中央にpush z keyとかが明滅するような感じの)なのだが、
全体的にいい感じに書けたと思う。特にパターンの記述がすっきりと。


この時点でとりあえず問題に感じたのは
ゲーム処理部分からIOによる入力が行えないこと。
そのため今回は使用する画像などすべてを最初に読み込むようにした。
しかしまぁ、そのうちシーンレベルでの抽象化を行う予定なので
それで何とかなるだろう。


一応、作ったものをアップしてみる。
http://fxp.infoseek.ne.jp/haskell/HSDL/sample/hasdlis2.zip
ソースは切り貼りしまくったので汚いけど…