Freeモナドでゲームを作ろう!第1回: Gameモナドの基本
連載目次
free-gameを使う
さて、みなさん、free-gameはインストールしましたか?まだの
2013/5/6: free-game 0.9.3をリリースし、内容も改訂しました。必ずcabal update && cabal install free-gameしてください。型方も、もうインストールした方も、本日free-gameを更新したのでcabal update && cabal install free-game
しましょう。
free-gameは、Gameモナドという独自のモナドによってGUIの抽象化を実現しています。GameはFreeモナドによって生成されたモナドであり、元は単なるFunctorです(これが何を意味するかは、連載の中で明らかにしていきます)。
今回は、実際にプログラムを組みながら、どのようにしてfree-gameを扱うかを学んでいきましょう。
Hello, world!
まずはHello, world!ですね。最初に、https://github.com/fumieval/free-game/blob/master/examples/VL-PGothic-Regular.ttf からフォントをダウンロードしましょう。
helloworld.hs:
{-# LANGUAGE ImplicitParams, OverloadedStrings #-} import Graphics.UI.FreeGame main = do font <- loadFont "VL-PGothic-Regular.ttf" let ?font = font runGame def mainLoop mainLoop :: (?font :: Font) => Game a mainLoop = do translate (V2 40 240) $ colored black $ text ?font 30 "Hello, Free World!" tick mainLoop
$ ghc helloworld.hs
$ ./helloworld (Windowsの場合は単にhelloworld.exe)
このように表示されれば成功です。FreeType Errorが表示された場合、カレントディレクトリにダウンロードしたフォントがあるかどうかもう一度確認してください。
このプログラムの肝は、Gameモナドを実行するrunGame :: GUIParam -> Game a -> IO (Maybe a)
と、Picture型の値を画面に表示する関数、画面を更新するアクション、drawPicture :: Picture -> Game ()
です。tick :: Game ()
です。
Picture型の定義を見てみましょう。
*** この型は削除されました data Picture -- ビットマップ(後述)をPictureとして使う = BitmapPicture Bitmap -- 複数のPictureをまとめて一つのPictureにする。 | Pictures [Picture] -- IOモナドに入ったPicture(内部実装のためのもの)。 | IOPicture (IO Picture) -- Pictureを回転する。 | Rotate Float Picture -- Pictureを拡大・縮小する。 | Scale Vec2 Picture -- Pictureを移動する。 | Translate Vec2 Picture -- Pictureに色を付ける。 | Colored Color Picture
実はこれはglossにインスパイアされたものです。わかりやすいでしょう?*1
free-game 0.9では、Picture2DおよびFigure2Dという型クラスが定義されています。Gameモナドはこれらのインスタンスであるため、以下のような操作が利用できます。
class Picture2D p where -- ビットマップから生成する fromBitmap :: Bitmap -> p () -- 指定した角度だけ回転させる rotate :: Float -> p a -> p a -- 拡大・縮小する scale :: V2 Float -> p a -> p a -- 平行移動する translate :: V2 Float -> p a -> p a -- 色を付ける colored :: Color -> p a -> p a class Picture2D p => Figure2D p where -- 線 line :: [V2 Float] -> p () -- 多角形 polygon :: [V2 Float] -> p () -- 多角形(枠のみ) polygonOutline :: [V2 Float] -> p () -- 円 circle :: Float -> p () -- 円(枠のみ) circleOutline :: Float -> p () -- 太さを設定 thickness :: Float -> p a -> p a
V2
は、vectというライブラリで定義されている、二次元のベクトルを表すデータ型です。が、具体的な使い方はまた次回。
ビットマップはloadBitmapFromFile :: FilePath -> IO Bitmap
で読み込むことができます。http://www.haskell.org/wikiupload/4/4a/HaskellLogoStyPreview-1.pngを表示してみましょう(カレントディレクトリに保存してください)。当然ながら、loadBitmapFromFileはGameモナドの中では実行できないので注意してください。Gameモナドの中でIOアクションを使いたいときは、embedIO :: IO a -> Game a
を適用しましょう。
{-# LANGUAGE ImplicitParams #-} import Graphics.UI.FreeGame import System.Random main = do font <- loadFont "VL-PGothic-Regular.ttf" bmp <- loadBitmapFromFile "HaskellLogoStyPreview-1.png" let ?font = font let ?bmp = bmp runGame def mainLoop mainLoop :: (?bmp :: Bitmap, ?font :: Font) => Game a mainLoop = do translate (V2 40 240) $ colored black $ text ?font 30 "Hello, Free World!" translate (V2 240 480) $ rotate 45 -- 反時計回りに45°回転 $ colored red -- 赤色 $ text ?font 70 "真っ赤な誓いいいいいいいいいい" r <- randomness (-40, 40) translate (V2 (-40) (360 + r)) -- ランダムに振動させる $ scale (V2 0.7 1) -- x方向に0.7倍 $ colored blue -- 青色 $ text ?font 100 (replicate 20 'ド') translate (V2 300 300) $ colored magenta $ circle 40 -- 円を表示 translate (V2 240 80) $ fromBitmap ?bmp -- 読み込んだビットマップを(240,80)に表示 tick mainLoop
入力を行う
これだけでは、ゲームを表現するのに不十分です――「入力」ができないからです。free-gameでは、入力を行うためにKeyboardとMouseという二つのインターフェイスを提供しています。Gameモナドはこれらのインスタンスであるため、直に以下のアクションを実行できます。
-- | The class of types that can handle inputs of the keyboard. class Keyboard t where keyChar :: Char -> t Bool keySpecial :: SpecialKey -> t Bool -- | The class of types that can handle inputs of the mouse. class Mouse t where mousePosition :: t (V2 Float) mouseWheel :: t Int mouseButtonL :: t Bool mouseButtonM :: t Bool mouseButtonR :: t Bool
引数に指定するキーについては、http://hackage.haskell.org/packages/archive/free-game/0.9.1/doc/html/Graphics-UI-FreeGame-Base.html#t:SpecialKeyを参照してください。
例: Zキーを押している間だけカウントアップを行う。Escキーを押すとプログラムが終了する
{-# LANGUAGE ImplicitParams #-} import Graphics.UI.FreeGame import System.Random import Control.Monad main = do font <- loadFont "VL-PGothic-Regular.ttf" let ?font = font runGame def (mainLoop 0) mainLoop n = do translate (V2 40 120) $ colored green $ text ?font 20 $ show n translate (V2 40 240) $ colored black $ text ?font 20 "Hello, Free World!" key <- keySpecial KeyEsc -- Escキーの状態を取得 when key quit -- Trueならば終了 key <- keyChar 'Z' -- Zキーの状態を取得 tick mainLoop $ if key then succ n else n -- Trueのときだけ1増やす
mousePosition, mouseWheel, mouseButtonL, mouseButtonR, mouseButtonMを使うことで、位置、ホイール、ボタンの状態が取得できます。
例: カウンタがマウスカーソルの右上に表示され、左クリックするたびに値が1ずつ増える
{-# LANGUAGE ImplicitParams #-} import Graphics.UI.FreeGame import System.Random import Control.Monad main = do font <- loadFont "VL-PGothic-Regular.ttf" let ?font = font runGame def $ mainLoop (0, False) mainLoop (n, btn) = do pos <- mousePosition b <- mouseButtonL translate pos $ colored green $ text ?font 20 $ show n translate (V2 40 240) $ colored black $ text ?font 20 "Hello, Free World!" tick mainLoop (if not btn && b then n + 1 else n, b)
drawPicture、askInput、getMouseStateの使い方が分かりましたね。実はfree-gameの基本はこれで全部なのです!
ゲームを開発する準備は整いました。次回は、これらを踏まえて実際にゲームを作ってみましょう!
まとめ
- 基本はdrawPicture、getButtonState、getMouseStateの3つだけ
- embedIOがあるからIOだって使えるぞ
- runSimpleですべてが動きだす
Tips
今回の例では、GHC拡張のImplicitParamsを使っています。?のついた変数は、一旦束縛すればスコープ内で呼び出した関数からどこでも参照できるようになるので、フォントや画像データなどの不変なものを扱うのに適しています。
*1:生意気にもnice data typeを標榜するだけある