12.キーボードからのイベント
前回、前々回の記事では時間に関するイベントについて記述したが、ここでは、キーボードを操作したときのイベントについて説明する。キーボードからのイベントの処理の仕方は、どのグラフィックスAPIを用いるかによって異なるが、ここでは、GLFWを用いた場合を説明する。
Netwire5.0ではイベントはnewtypeではなくdataによって新しいデータ型として定義されている。
data Event a = Event a | NoEvent deriving (Eq, Show)
また、イベントは型クラスFunctorに属している。
instance Fuctor Event where fmap f e = case e of Event a -> Event (f a) NoEvent -> NoEvent
それでは、キーボードからのイベントを考える。今、あるキーをkとすると、キーの状態はなにもされていないか、押されているかの二つの状態がある。そこで、キーが押されているときに出力を出し、なにもされていないときに抑制値を出すこととする。出力を()、抑制値を()とする。同じ記号で紛らわしいのだが、事情はもう少したつと分かる。
いま、キーボードが押された時のWire型の値に対して、次のように型シグネチャを定める。
isKeyDown :: Enum k => k - > Wire s () IO a ()
Wire型はWire s e m a bであったが、上記では、eとbが()となっている。eは(出力がない時の)抑制値の出力であり、bの方は出力があるときの値である。従って、最初(左側)の()はなにもされていないときの、最後(右側)の()はキーが押されているときのものである。
出力があるかないかわからないとき、常套的に用いるのが、Maybeである。この記事でも、イベントの説明をする時にMaybeを用いた。しかし、値がない時も、Nothingとは異なる値を出したいときに、Either型の値を用いる。Either型は、Either a bのように二つの型引数をとる。右側は出力がある場合で、左側は出力がなかった場合(MaybeでのNothingに相当)である。
そこで、isKeyDownの実装を行うと次のようになる。
isKeyDown :: Enum k => k - > Wire s () IO a () isKeyDown k = mkGen $ _ -> do input <- getKey k return $ case input of Press -> Right () -- 出力がある場合 Release -> Left () -- 出力がない場合
上記で、mkGen_はWire型の値を作成する構築子である。型シグネチャは次のようになっている。
mkGen_ :: Monad m => (a -> m (Either e b)) -> Wire s e m a b
mkGen_での型シグネチャでは、Eitherの前にmがついているので、ここでは、モナドにする必要がある。そこで、上記のプログラムは、()をモナドの単位元memptyに変える。これに伴って、()になっていた型変数e,bは、両方ともeとする。また、eがモノイド(大雑把にいうと二項演算子)なので、その指定も行う。
isKeyDown :: (Enum k, Monoid e) => k - > Wire s e IO a e isKeyDown k = mkGen $ _ -> do input <- getKey k return $ case input of Press -> Right mempty Release -> Left mempty
なお、これを用いるには以下のモジュールをインポートしておく。
import Prelude hiding ((.)) import Graphics.UI.GLFW import Control.Wire import FRP.Netwire
キーボードからイベントを入力するプログラムができたので、Aのキーが押されたら左の方に、Dの場合には右の方に、等速で動くようなプログラムは作成する。
isKeyDown :: (Enum k, Monoid e) => k -> Wire s e IO a e isKeyDown k = mkGen_ $ \_ -> do input <- getKey k return $ case input of Press -> Right mempty Release -> Left mempty speed :: Monoid e => Wire s e IO a Double speed = pure ( 0.0) . isKeyDown (CharKey 'A') . isKeyDown (CharKey 'D') <|> pure (-0.5) . isKeyDown (CharKey 'A') <|> pure ( 0.5) . isKeyDown (CharKey 'D') <|> pure ( 0.0) pos :: HasTime t s => Wire s () IO a Double pos = integral 0 . speed
上記のプログラムでpure (0.5), pure (-0.5)が、それぞれ、D,Aが押された時の速度である。<|>は選択で左から順番に操作し、条件が満たされたものが実行される。プログラムでは、A,Dが両方押された時、Aが押された時、Dが押された時、どれも押されていないときの順になっていて、それらに対応して、速度が得られる。
次の記事では、これを用いた簡単なゲームを紹介する。