Haskellでゲーム開発 - メインループとイベント操作
そろそろHaskellでゲーム開発なんか真面目にやっていこうかなぁとか思いつつ、SDLの勉強しています。
並行してやってる色んな事を総スルーしながら、やっていきます。気分屋なのでやることがコロコロ変わるのは悪いくせですね、はい。
というわけで、HaskellのSDLラッパーの使い方、色々調べてみたんですが、なかなか良い感じのチュートリアルに出会わず、結局Hackageの定義とにらめっこするのが一番手っ取り早いという結論に至りました。
SDLもOpenGLもはじめて使うので試行錯誤との戦いです。
っていうか、Haskell/SDL関連の記事書いてるみなさん、型定義書きましょうよ、マジで。型推論利くし、面倒なのは解るのですが、型が書かれてるだけで全然読みやすさ違いますから。
とにかく、今回書いたコードをペロンと貼っておきます。
まっさらな画面を表示して、その画面に対するイベントを標準出力に吐き出すだけのプログラムです。
リアルタイムでイベントを監視しているのを確認するために、だいたい0.1秒毎に起動してからの経過時間を1000分の1秒単位で出力してます。
コメントいっぱい書いたんで、後述の説明読むのが面倒な人はコードだけ眺めてやって下さい。
module Main where import Data.Word import qualified Graphics.UI.SDL as SDL --initしてsetVideoModeすればとりあえず画面が表示される --setVideoModeの戻り値はSurfaceとかいう構造体で --画面の情報なんかを保持しているらしい gameinit :: IO () gameinit = do SDL.init [SDL.InitEverything] SDL.setVideoMode 640 480 32 [] >>= print --メインループ処理 mainLoop :: Word32 -> IO () mainLoop ago = do SDL.delay 1 --1000分の1ミリ秒待機、これ入れないとCPU使用率が大変な事になる ev <- SDL.pollEvent --待機せずイベントを取得、waitEventとかだと処理が継続できない printEvent ev --イベントを出力 --isQuitがTrueを返すまで再帰呼出し if isQuit ev then return () else printTicks >>= mainLoop where --NoEventを除くイベントを出力 printEvent :: SDL.Event -> IO () printEvent SDL.NoEvent = return () printEvent ev = print ev --適当な間隔でgetTicksを出力 printTicks :: IO Word32 printTicks = do t <- SDL.getTicks if ago + 100 < t then print t >> return t else return ago --終了条件判定 isQuit :: SDL.Event -> Bool isQuit (SDL.KeyUp key) = SDL.symKey key == SDL.SDLK_ESCAPE --ESCキー押下 isQuit ev = ev == SDL.Quit --画面が閉じられる isQuit _ = False ------------------------------------------------- main :: IO () main = gameinit >> SDL.getTicks >>= mainLoop >> SDL.quit
まず、SDLの初期化はinit関数でやります。
*Main> :t SDL.init SDL.init :: [SDL.InitFlag] -> IO () *Main> :i SDL.InitFlag data SDL.InitFlag = SDL.InitTimer | SDL.InitAudio | SDL.InitVideo | SDL.InitCDROM | SDL.InitJoystick | SDL.InitNoParachute | SDL.InitEventthread | SDL.InitEverything -- Defined in Graphics.UI.SDL.General instance Bounded SDL.InitFlag -- Defined in Graphics.UI.SDL.General instance Eq SDL.InitFlag -- Defined in Graphics.UI.SDL.General instance Ord SDL.InitFlag -- Defined in Graphics.UI.SDL.General instance Read SDL.InitFlag -- Defined in Graphics.UI.SDL.General instance Show SDL.InitFlag -- Defined in Graphics.UI.SDL.General
起動時はぜーんぶ初期化してやりたいので、[SDL.InitEverything]を引数にしてやれば良いというワケですね。
で、setVideMode関数で画面を作ってやります。
*Main> :t SDL.setVideoMode SDL.setVideoMode :: Int -> Int -> Int -> [SDL.SurfaceFlag] -> IO SDL.Surface *Main> :i SDL.SurfaceFlag data SDL.SurfaceFlag = SDL.SWSurface | SDL.HWSurface | SDL.OpenGL | SDL.ASyncBlit | SDL.OpenGLBlit | SDL.Resizable | SDL.NoFrame | SDL.HWAccel | SDL.SrcColorKey | SDL.RLEAccel | SDL.SrcAlpha | SDL.PreAlloc | SDL.AnyFormat | SDL.HWPalette | SDL.DoubleBuf | SDL.Fullscreen -- Defined in Graphics.UI.SDL.Types instance Bounded SDL.SurfaceFlag -- Defined in Graphics.UI.SDL.Types instance Eq SDL.SurfaceFlag -- Defined in Graphics.UI.SDL.Types instance Ord SDL.SurfaceFlag -- Defined in Graphics.UI.SDL.Types instance Read SDL.SurfaceFlag -- Defined in Graphics.UI.SDL.Types instance Show SDL.SurfaceFlag -- Defined in Graphics.UI.SDL.Types
引数に渡すSurfaceFlag型には色々ありますが、今後必要になりそうなのはOpenGL、DoubleBuf、Fullscreenあたりでしょうか。
うち、Fullscreenだけちょっと試してみましたが、サクッと全画面表示してくれました。
すごくどうでも良いですが、終了条件をしっかり書いておかないと、Fullscreenにした時ににっちもさっちも行かなくなってしまうので注意。
そのせいで、途中まで書いてたこの記事が一旦クリアされてしまうという酷い目にあいました。
あ、あと戻り値のSurfaceは画面関係の情報を保持した構造体みたいです。
今回はprintして(メモリアドレスが出力されます)そのまま捨てちゃってますが、ちゃんと作るときはデータもち回してやったほうが良いんじゃないかと思います。
まだ用途とか良くわからんので、SDLに詳しい方良かったら教えてください。
画面が表示されたら、いよいよメインループとイベントの処理について考えていきます。
イベントの取得にはpollEvent、waitEvent、waitEventBlocking等といった関数が用意されています。
*Main> :t SDL.pollEvent SDL.pollEvent :: IO SDL.Event *Main> :t SDL.waitEvent SDL.waitEvent :: IO SDL.Event *Main> :t SDL.waitEventBlocking SDL.waitEventBlocking :: IO SDL.Event
三つとも型は一緒ですが、挙動は少しづつ違います。
よくあるHaskell/SDL関連の記事だと、waitEventを使ってたりするようですが、
waitEventやwaitEventBlockingはイベントキューが積まれるまで待機してしまうので、リアルタイム性のあるゲームを作るのには使えないです。
基本的には末尾再帰したメインループ関数内で、ゲームを処理しつつpollEvent関数でイベントを監視する感じになるかと思われますが、
安定して動作させるためには、時間を監視する必要があります。そこで必要になるのが、getTicks関数です。
*Main> :t SDL.getTicks SDL.getTicks :: IO Word32
この関数は、SDLが起動してからの時間を1000分の1秒単位で返します。
1000分の16秒程度待機してから描画すれば、大体60fps付近で安定すると考えれば良いでしょうか。
それから、単純に無限ループにしてしまうと、CPUが大変な事になってしまうので、適度に待機してやる必要があります。
そのために使えそうなのがdelay関数です。
*Main> :t SDL.delay SDL.delay :: Word32 -> IO ()
こいつは、引数で渡された時間だけ待機する関数です。これを入れてやるとCPU使用率がうんとマシになります。
ゲームの進行を管理する方法については、おそらく描画処理と計算処理は分断する事になると思われるので、StateTとかにするまでも無いんじゃないでしょうか。(むしろIOが入り乱れてややこしくなりそう)
あとは画像の処理についてアレコレ調べていけば、Haskellでゲームを作るのはそんなに大変では無いでしょう。