Freeモナドでゲームを作ろう!第1回: Gameモナドの基本

連載目次

free-gameを使う

さて、みなさん、free-gameはインストールしましたか?まだの方も、もうインストールした方も、本日free-gameを更新したのでcabal update && cabal install free-gameしましょう。 2013/5/6: free-game 0.9.3をリリースし、内容も改訂しました。必ず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が表示された場合、カレントディレクトリにダウンロードしたフォントがあるかどうかもう一度確認してください。

f:id:fumiexcel:20130110163026p:plain

このプログラムの肝は、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

f:id:fumiexcel:20130110195629p:plain

入力を行う

これだけでは、ゲームを表現するのに不十分です――「入力」ができないからです。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を標榜するだけある