Freeモナドでゲームを作ろう!第3回: Haskellは手続き型言語

※最新のfree-gameを想定しているので、古いバージョンを使っている方は今すぐアップグレード(cabal update && cabal install free-game)してください。

7週間以上開きましたが、第3回です。ここからはどんどんプログラムを組み上げていきます。

コードが長くなったのでGitHubに上げました。重要な所に絞り、どのようにしてプログラムを組むかを解説していきたいと思います。

当たり判定について(Collision.hs)

説明が面倒なので省略。円、線分、およびそれらの集合同士がどこで衝突しているかを計算することができます。

Prelude Data.Vect Collision> Primitive (Circle 5 (V2 0 0))
 `collisions` Primitive (LineSegment (V2 5 0) (V2 (-7) 2)) :: Maybe (V2 Float)
Just (V2 (-2.0) 2.0)

世界を作る(Types.hs)

Types.hsでは、世界に存在する物体の型と形状を定義しています。フィールド名が_で始まるような代数的データ型を定義し、makeLensesするというのは前回と変わりませんね。

data Pack = Pack
    { _packPos :: V2 Float
    , _packVel :: V2 Float
    }

makeLenses ''Pack

packShape :: Getter Pack Fig
packShape = packPos . to (Primitive . Circle 15)

前回定義したものに加えて、Getterなるものが出てきていますね。これは文字通りGetterで、Lensの特殊な場合です。to :: (s -> a) -> Getter s aによって関数からGetterを作ることができます。

また、LensやGetterなどはすべて(.)で合成できます。

(.) :: Lens' a b -> Lens' b c -> Lens' a c

メインの処理(main.hs)

プログラムは世界を状態とするStateモナドで統一しています。mainの処理はこれだけ。

main :: IO (Maybe ())
main = runGame def $ evalStateT gameMain $ (世界の初期状態)

そして、本体であるgameMainも非常にシンプルなものです。

gameMain :: TheGame ()
gameMain = do
    addPack $ Pack (V2 320 360) (V2 4 (-3))
    forM_ [60,140,220] $ \y -> do
        addBlock $ Block (V2 60 y)
        addBlock $ Block (V2 160 y)
        addBlock $ Block (V2 480 y)
        addBlock $ Block (V2 580 y)
    forever $ do
        updateBar
        updatePacks
        updateBlocks
        tick

そして、lensをふんだんに活用してupdateBar, updatePack, updateBlocksを実装するだけですね。

さりげなくfree-game 0.3.2.3の新機能、loadBitmaps :: FilePath -> Q [Dec]も登場しています。指定したディレクトリ内の画像をすべて読み込んで定義してくれる関数です。

Lensの技

彼には72通りの型があるから、何て呼べばいいのか…

確か、最初に会ったときは…

zoom :: Monad m => Lens' s t -> StateT t m a -> StateT s m a

そう、指定されたフィールド(Lens' s t)を状態とするアクションを実行する関数だ。

まぁ…いい関数だったよ。

これを使うと、いちいち対象を指定するLensを書かなくて済むので非常に便利です。コンテナのすべての要素に作用するLensであるeachと組み合わせることで、foreachのような処理が簡潔に書けます。

updateBlocks :: TheGame ()
updateBlocks = zoom (blocks . each)
    $ use blockPos >>= (`translate` fromBitmap _block_png)

Lensは一見難解ですが、use, .=, each, toの使い方さえ覚えておけば、あとは手続き型言語の要領で分かりやすいコードを作ることができます。また、以下のような演算子も覚えておくと便利です。

(<~) :: MonadState s m => Lens' s t -> m t -> m () -- l <~ mはm >>= (l .=)と同じ。
(%=) :: MonadState s m => Lens' s t -> (t -> t) -> m () -- 指定した関数を使って更新する。Lens版modify

まとめ

f:id:fumiexcel:20130311220935p:plain

無事それっぽいものができました。勿論、これで終わりではありません!次回からは、ゲームの本質的な部分を記述していきます。