wxHaskell (その4)
Haskellでゲーム計画もいよいよ大詰めである。
これまでHaskellではGUIアプリを作ったことが無かったため
(もっとも、日本でHaskellでGUIアプリを作ったという話を聞いたことが無いけど)
その道程は困難を極めたが、これまでにひとつづつその障壁を取り除いてきた。
最初は私も本当にまともにゲームが組めるか不安でならなかったのだが、
ようやく行けそうだ、というような感覚が湧いてきた。
今回はゲームを実装する上で重要な入出力処理、
リアルタイムキー入力及び画像出力、さらにスピードコントロールを考える。
- キー入力
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