wxHaskell (その4)

Haskellでゲーム計画もいよいよ大詰めである。
これまでHaskellではGUIアプリを作ったことが無かったため
(もっとも、日本でHaskellGUIアプリを作ったという話を聞いたことが無いけど)
その道程は困難を極めたが、これまでにひとつづつその障壁を取り除いてきた。
最初は私も本当にまともにゲームが組めるか不安でならなかったのだが、
ようやく行けそうだ、というような感覚が湧いてきた。


今回はゲームを実装する上で重要な入出力処理、
リアルタイムキー入力及び画像出力、さらにスピードコントロールを考える。

  • キー入力

wxHaskwllにはWin32APIで言うところのGetKeyState()みたいなものが無い。
どうやってキー入力を取るのかといえば、ウインドウにイベントハンドラ
追加して、それで処理するのである。
しかし、やはりリアルタイムなゲームを設計するにあたって、
イベントで処理はいやなのだ。適当にループまわして、そのループから
キーの押下状態を知りたいのである。


wxHaskellのローレベルな部分へのアクセスであるWXCoreモジュールにも
それに相当しそうなものは無い。
しかし、とりあえずこっちのほうにはKeyUpイベントと
KeyDownイベントがあったので(Coreじゃないほうには無い)
それらを使って何とかすることにした。

windowOnKeyDown :: Window w -> (EventKey -> IO ()) -> IO ()
windowOnKeyUp   :: Window w -> (EventKey -> IO ()) -> IO ()

それぞれ↑の様な型を持っている。指定したハンドラが追加される。
ハンドラにはEventKey型のデータとして押された/放されたキーが渡される。


このそれぞれで現在押されているキーを管理することができるので、
とりあえずそのようなものを実装することにした。

 -- キーボード処理

data GameKey =
  GKUp | GKDown | GKLeft | GKRight | GKRotate
  deriving (Eq,Show,Enum)

data KeyState =
  Pushed | Pushing | Released | Releasing
  deriving (Eq,Show)

isPressed Pushed  = True
isPressed Pushing = True
isPressed _       = False

type KeyProc = GameKey -> KeyState

使うデータを適当に定義する。
キー状態はゲーム中で用いやすいように

  • 押された瞬間
  • 押されている
  • 放された瞬間
  • 放されている

の4つを持つことにする。

main :: IO ()
main = start $ do
  kd  <- varCreate ([ ],[ ],[ ])
  ...

  windowOnKeyDown f $ kbdDown kd
  windowOnKeyUp   f $ kbdUp   kd
  ...

データの共有のためにVarな変数を作る。

    kbdDown kd ek = kbdUpdt kd $ \(e,s,l) ->
      let k = keyCvt ek
          ns = s `union` k
          ne = e `union` k
      in if ns==s then (e,s,l) else (ne,ns,l \\ k)
    kbdUp   kd ek = kbdUpdt kd $ \(e,s,l) ->
      let k = keyCvt ek in (e \\ k,s \\ k,l `union` k)

    keyCvt ek = case keyKey ek of
      KeyUp       -> [GKUp]
      KeyDown     -> [GKDown]
      KeyLeft     -> [GKLeft]
      KeyRight    -> [GKRight]
      KeyChar 'Z' -> [GKRotate]
      _           -> []
    
    kbdUpdt kd f = varUpdate kd f >> return ()

kbdDownとkbdUpの処理は上のとおり。
(e,s,l) にて、eがちょうど押されたキーのリスト、
sが押されているキーのリスト、lがちょうど放されたキーのリストとしている。
上記コードだけではちょうど押されたキーのリストと
ちょうど放されたキーのリストがずっとそのままになってしまう。
毎フレーム

    updateKey (_,s,_) = ([ ],s,[ ])

により、クリアを行う。キーデータは

    keyProc (e,s,l) k
      | k `elem` e = Pushed
      | k `elem` s = Pushing
      | k `elem` l = Released
      | otherwise = Releasing

などという関数をつくって取得することにする。
ゲームの処理にはこの辺が見えないようにこれに(e,s,l)なデータを部分適用
した関数を渡すことにする。要するに最初のほうで定義したKeyProcなものを
渡すということである。

  • 描画処理
  ...
  f <- frameFixed [text       := wndTitle
                  ,clientSize := wndSize
                  ,bgcolor    := black
                  ,visible    := False]
  dc <- clientDCCreate f
  ...

clientDCCreateによりDCが取得できる。(DCて、とか言わない!)
それをゲームの描画関数に渡してやることにする。

  • スピード調節

リアルタイムなゲームでは実行中FPSを維持する必要がある。
適当にウェイトを入れてやることにする。
というか、結局IOコマンドの羅列になるんだが…

elapseTime :: Integer -> IO (IO (Int,Bool))
elapseTime fps = do
  let frametime = picosec `div` fps
  tm <- getClockTime
  st <- varCreate ( (0,0,noTimeDiff),(1,tm))
  return $ do
    ( (bef,cur,fdt),(cnt,bt)) <- varGet st
    ct       <- getClockTime
    let dt   = diffClockTimes ct bt
        ndt  = diffClockTimes ct tm
        adj  = frametime*cnt - toPsec dt
        nc   = if cnt==fps then (1,ct) else (cnt+1,bt)
        (nbef,ncur) = if tdSec fdt /= tdSec ndt then (cur,0) else (bef,cur)
    if adj<0 then do
        varSet st ( (nbef,ncur,ndt),nc)
        return (bef,False)
      else do
        varSet st ( (nbef,ncur+1,ndt),nc)
        threadDelay $ fromInteger $ min 16666 $ adj `div` 1000000
        return (bef,True)
  where
    toPsec dt = toInteger (tdMin dt * 60 + tdSec dt) * picosec + tdPicosec dt
    picosec = 1000000000000

こんな感じ。
FPSを指定するとFPSを安定させるコマンドを返すようなコマンドである。
(というかこれ、C++用に作ったやつをほとんど書き写しなんだがなぁ…)
実行はIdleループを用いて、これを延々回し続けることによって行う。

  ...
  et <- elapseTime 60
  
  windowOnIdle    f $ do
    k   <- varGet kd
    st  <- varUpdate gs $ onProcess (keyProc k)
    varSet kd $ updateKey k
    (fps,draw) <- et
    when draw $ dcBuffer dc (rectFromSize wndSize) $ \dc -> do
      onDraw dc res st
      drawText dc (show fps) (pt 600 10) [color := white]
    return True
  ...

上記コードでetがFPSを安定させるコマンドになる。
ちなみに返す(Int,Bool)は現在FPS(描画した回数)と
次回描画処理を行うべきかどうかである。


上のコードのonProcessとonDrawがゲームの処理と描画処理になる。
それぞれ、

onProcess :: KeyProc -> GameState -> GameState
onDraw    :: DC a -> Resource -> GameState -> IO ()

の型を持つことにする。
ResourceとGameStateはゲームにあわせてお好みに定義する。


というわけで
このあたりで一通り雛形が完成した。説明が適当すぎたけど。
onProcessは毎秒ちょうどFPS回呼び出される。
onDrawは毎秒高々FPS回呼び出される。
onProcessからはなんとかIOをはずすことができた。
IO処理は一切できないのだが、それはonDrawのほうに全部行うことにした。


まぁ、なんというか、これも書いててHaskellの意味あるんかなぁとか
C++で作ってたころと何も変わらんやん?とか
なんとかちょっと悲しくなってきたのであるが、
最初はともかく動くものを作るのが先決であろう。
一通りできてからもっとエレガントな定義的で遅延を生かした
方法を考えることにする。


一応今回もファイルをアップ。
テトリスで使いそうなキーの入力テストプログラム。
http://fxp.infoseek.ne.jp/haskell/tatakidai.zip