bitterharvest’s diary

A Bitter Harvestは小説の題名。作者は豪州のPeter Yeldham。苦闘の末に勝ちえた偏見からの解放は命との引換になったという悲しい物語

Netwireでゲームを作成する(9)

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が押された時、どれも押されていないときの順になっていて、それらに対応して、速度が得られる。

次の記事では、これを用いた簡単なゲームを紹介する。