Freeモナドでゲームを作ろう!第2回: 本当に動かす

連載目次

Haskellの力を最大限に引き出す

前回紹介したような関数の組み合わせでももちろんゲームは作れるのですが、今回は、さらにゲームを開発しやすくするための手法をいくつか紹介し、その例を実際に動かしてみます。

Vec2

2Dゲームを作る上でベクトルの演算は必須です。vectというライブラリは、二次元から四次元までの範囲でベクトルや行列の操作ができる、非常に便利なライブラリです。二次元に絞って主要な関数をいくつか紹介しておきます。 Edward Kmettさんから「linearを使った方がいい」といったアドバイスを頂いたので、free-game 0.9ではlinearを採用しました。零次元から四次元までの範囲でベクトルや行列の操作ができる、非常に便利なライブラリです。二次元に絞って主要な関数をいくつか紹介しておきます。

V2 :: a -> a -> V2 a -- コンストラクタ

zero :: V2 a -- 零ベクトル
negated :: V2 a -> V2 a -- 反転
(^+^) :: V2 a -> V2 a -> V2 a -- 加算
(^-^) :: V2 a -> V2 a -> V2 a -- 減算
(*^) :: a -> V2 a -> V2 a -- スカラー倍
(^*) :: V2 a -> a -> V2 a -- スカラー倍

norm :: V2 a -> a -- ベクトルのノルム(長さ)
dot :: V2 a -> V2 a -> a -- ベクトルの内積
normalize :: V2 a -> V2 a -- 方向が同じの単位ベクトル

distance :: V2 a -> V2 a -> V2 a -- 二つのベクトル間の距離を求める

Stateモナド

ご存知の方も多いと思いますが、Stateモナドは状態を扱うためのモナドで、計算の任意のタイミングで「内部状態」を変更することができます。ゲームに状態はつきものなので、そういったプログラムと相性が良いです。

Lens

Lensはlensというライブラリで定義されている型で、これを使うことで、データ型の要素の一つに着目して操作することが可能となります。まさにレンズのように!

実際に見てもらった方が早いでしょう。

{-# LANGUAGE ImplicitParams, TemplateHaskell #-}
import Graphics.UI.FreeGame
import Control.Monad.State
import Control.Lens

data Hoge = Hoge {
    _xOfHoge :: Float
    ,_yOfHoge :: Float
    ,_dxOfHoge :: Float
    ,_dyOfHoge :: Float
    }

makeLenses ''Hoge -- TemplateHaskellを用いて対応するLensを自動生成(!)

main = do
    bmp <- loadBitmapFromFile "HaskellLogoStyPreview-1.png"
    let ?pic = bmp
    _ <- runGame def $ runStateT hoge $ Hoge 80 80 3.7 1.1
    return ()

hoge :: (?pic :: Bitmap) => StateT Hoge Game ()
hoge = do
    x <- use xOfHoge
    y <- use yOfHoge
    dx <- use dxOfHoge
    dy <- use dyOfHoge

    translate (V2 x y) (fromBitmap ?pic)

    xOfHoge += dx
    yOfHoge += dy

    tick
    hoge

Q.これは手続き型言語ですか?

A.はい、関数型言語です

使い方はとっても簡単。

  • フィールド名が_で始まるようなデータ型Tを定義します
  • makeLenses ''Tします
  • Tを状態とするStateモナドの中で、
  • useで値を取得します
  • .=で値を代入します

これだけで手続き型言語の便利さを取り入れた強力なプログラミングができるようになります!すごい!

さっきのコードをVec2を使った形に直し、さらに壁に反射するようにしてみましょう。(pack.pngはhttp://botis.org/shared/pack.pngからダウンロードしてください)

{-# LANGUAGE ImplicitParams, TemplateHaskell #-}
import Graphics.UI.FreeGame
import Control.Monad.State
import Control.Applicative
import System.Random
import Control.Lens

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

makeLenses ''Pack

main = do
    bmp <- loadBitmapFromFile "pack.png"
    let ?picPack = bmp
        ?packSize = 30
    pack <- Pack <$> (V2 <$> randomRIO (?packSize, 640 - ?packSize) <*> randomRIO (?packSize, 480 - ?packSize))
                 <*> ((^*4) <$> unitV2 <$> randomRIO (0, 2*pi))

    runGame def $ runStateT updatePack pack
    return ()

updatePack :: (?picPack :: Bitmap, ?packSize :: Float) => StateT Pack Game ()
updatePack = do
    pos@(V2 x y) <- use packPos
    vel <- use packVel

    let collisions = [V2 0 y | x < ?packSize]
            ++ [V2 640 y | x > 640 - ?packSize]
            ++ [V2 x 0 | y < ?packSize]
            ++ [V2 x 480 | y > 480 - ?packSize]

    translate (V2 240 240) (fromBitmap ?picPack)
    
    packVel += sum [2 *^ n
        | t <- collisions
        ,let u = normalize (t ^-^ pos)
        ,let n = dot u vel *^ negated u]

    use packVel >>= (packPos +=)

さて、壁で反射する物体を見て、あるゲームを想像した方も多いと思いますが、この連載ではそれとは一味違うゲームを作っていきます。続きはまた次回。

まとめ

  • linearは超便利
  • lensは超便利