bitterharvest’s diary

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

Yampaでゲームを定式化する―Haskanoidの信号関数

1.Haskanoidでの信号関数

パックマンでは信号関数はオブジェクトと入力の対を得てオブジェクトの新しい状態を得ていた。Haskanoidでも原理は同様である。信号関数はオブジェクトごとに用意され、信号関数への入力は、キーボード、マウス、Wiiリモートプラスからの入力、オブジェクト同士の衝突、さらには他のオブジェクトとなっている。他のオブジェクトはパックマンの説明には含まれていなかったが、他のオブジェクトもこのオブジェクトに影響を及ぼすため入力に含まれる。信号関数の出力はレコードで、フィールド名はoutputObject,harakiriとなっている。従って、オブジェクトと発生したイベントが出力される。プログラムでは次のように定義されている。

type ObjectSF    = SF ObjectInput ObjectOutput
data ObjectInput = ObjectInput {
  userInut     :: Controller,
  collisions   ::[Collision],
  knownObjects ::[Object]
}
data ObjectOutput = ObjectOutput {
  outputObject :: Object,
  harakiri     :: Event ()
}

2.パドルの信号関数

最初にパドルの信号関数について説明する。その概略は次のようになっている。パドルの信号関数は、入力デバイス、衝突、他のオブジェクトを入力として受け、パドルの新しい状態を出力する。パドルの動作はボールを打つことと、入力デバイスからの制御によって新しい場所へと移すことである。このため、プログラムの中では、ボールを打ったかの判断を行うとともに、入力デバイスからの制御情報を受けて現在の位置と速度を計算する。これらの状況をパドルの新しい状態として出力する。

objPaddle :: ObjectSF
objPaddle = proc ( ObjectInput ci cs os) -> do
  let name  = “paddle”
  let isHit = inCollision name cs
  let p     = refPosPaddle ci
  v <- derivative -< p
  returnA -< livingObject $ Object {
                             objectName = name
                             objectPos  = p,
                             objectVel  = v,
                             …}

上記のプログラムだと過剰に反応する場合がある。そこで、Wiiリモートプラスでの過剰な反応を抑えたほうがパドルは自然に移動する。改良したプログラムではパドルの位置と速度を次のようになる。

objPaddle :: ObjectSF
objPaddle = proc ( ObjectInput ci cs os) -> do
  let name  = “paddle”
  let isHit = inCollision name cs
  rec
    let v = limitNorm (20.0 * ^ (refPosPaddle ci ^-^ p)) maxVNorm
    p <- (initPosPaddle ^+^) ^<< v
  returnA -< livingObject $ Object {
                             objectName = name
                             objectPos  = p,
                             objectVel  = v,
                            …}

3.ボールの信号関数

ボールの信号関数については、段階を追って次のように開発することにする。最初はボールを発射した後ボールが自由に落下する場合である。次はその改良版で、ボールがパドルで打てるようにした場合である。

ボールを発射する場面は両方に共通するが次のようになっている。ボールoはパドルの表面の中心に存在する。入力デバイスがクリックされるとボールとイベントとして現在のボールの位置が出力される。

objBall :: ObjectSF
objBall = switch followPaddleDetectLaunch $ \ p -> objBall
followPaddleDetectLaunch = proc oi ->
  o       <- followPaddle -< oi
  click <- edge -< controllerClick (userInput oi)
  returnA -< (o, click `tag` (objectPos (outputObject o)))

次に簡単な方のプログラムで、ボールが自由落下する場合を考えると次のようになる。

objBall :: ObjectSF
objBall = 
switch followPaddleDetectLaunch $ \ p ->
  switch (freeBall p initBall Vel &&& never) $ \ p -> objBall
freeBall p0 v0 = proc (ObjectInput ci cs os) -> do
  p <- (p0 ^+^) ^<< integral -< v0’
  return -< livingObject $ {…}
  where
    vo’ = limitNorm v0 maxVNorm

自由落下については前の記事を参照して欲しい。

自由落下するボールを、パドルで跳ね返すようにすると次のようになる。この場合、二つのケースが考えられる。一つは首尾よくパドルで打つことができた場合、もう一つは、打つことに失敗し、ボールが落ちてしまった場合である。関数bounceAroundDetectMiss pの中で、最初の行が成功した場合、次の行が失敗した場合である。

objBall :: ObjectSF
objBall = 
switch followPaddleDetectLaunch $ \ p ->
  switch (bounceAroundDetectMiss p) $ \ _ -> objBall
bounceAroundDetectMiss p = proc oi -> do
  o <- bouncingBall p initBallVel -< oi
  miss <- collisionWithBottom   -< collisions oi
  returnA -< (o, miss)

飛び跳ねるプログラムは次のようになる。これはスライドでの資料で、実際のプログラムは異なっているので、これを応用するときはプログラムの方を参照するとよい。

bouncingBall p0 v0 =
  switch ( moveFreelyDetBounce p0 v0 ) $ \(p’,v’) -> bouncingBall p’ v’
moveFreelyDetBounce p0 v0 =
  proc oi @ ( ObjectInput _ cs _ ) -> do
    o  <- freeBall p0 v0                 -< oi
    ev <- edgeJust <<< initially Nothing -< changedVelocity “ball” cs
  returnA -< ( o, fmap ( \v -> (objectPos (… o), v)) ev)

関数moveFreelyDetBounceの概略は次のとおりである。ObjectInputから衝突するものを得る。実際はパドルだがこれをoiとする。oiと衝突したときのボールoとする。ボールの速度の変位を得てこれをイベントとする。最後にボールoとこのイベントを返す。

4.ブロックの信号関数

ブロックは複数存在するが、それらはその座標によって識別される。それぞれの信号関数は次のようになっている。まず自身が衝突のリストの中に含まれているかを調べる。もし、そうであれば、自身のエネルギー(プルグラムではliveになっている)が負になった時は、消失させる。そのためのイベントはdeadである。

objBlockAt (x, y) (w, h)
  proc ( ObjectInput ci cs os) -> do
    let name = “blockat” ++ show (x, y) 
        isHit = inCollision name cs
    hit   <- edge               -< isHit
    lives <- accoumHoldBy (+) 3 -< (hit `tag` (-1))
    let isDead <= 0
    dead <- edge -< isDead
    returnA -< ObjectOutput (Object {…}) dead

ブロックはボールで打たれるとエネルギーが減少し、色が変化する(一回打たれると紫に、二回打たれると赤に、三回打たれると消滅する)。その様子はこの動画を参考にしてほしい。

5.プロセスの処理

ゲームの一サイクル、即ち、プロセスの処理はdpSwitchをベースに実現されている。

dpSwitch :: Functor col =>
  (forall sf . (a -> col sf -> col (b, sf)))
-> col (SF b c)
-> SF (a, col c) (Event d)
-> (col (SF b c) –> d -> SF a (col c))
-> SF a (col c)

dpSwitchは、パックマンの動作で説明した図と対比しながら分析するとわかりやすい。最初の関数(forall sf . (a -> col sf -> col (b, sf)))はルーティングの関数である。パックマンのルーティング部分に対応する。ここでは、入力デバイスと信号関数の集まりを得て、その対を出力する関数である。次のcol (SF b c)は前の関数への入力となるものである。これは、初期値として与えられる信号関数の集まりである。次のSF (a, col c) (Event d)はやはりルーティングの関数の入力となるものである。これは、イベントの発生源である。次の(col (SF b c) –> d -> SF a (col c))はスイッチさせる信号関数を生じさせる関数である。最後のSF a (col c)は出力である。

HaskanoidではdpSwitchを少し変更したdpSwitchBを利用している。それでは、Haskanoidでゲームの核となる部分がどのように実現されているかを説明する。

パックマンのところで説明したプロセスの部分は、HaskanoidではprocessMovementという関数で実現されている。

processMovement :: [ObjectSF] -> SF ObjectInput (IL ObjectOutput)
processMovement objs =
  dpSwitchB objs
             ( noEvent -> arr suicidalSect)
             ( \ sfs’  f -> processMovement’ (f sfs’))

dpSwitchBの中でobjsは信号関数である。次の二つの関数はdpSwitchに二つあった関数のうちで最後の関数に当る。最初の( noEvent -> arr suicidalSect)は、イベントが発生しないときは消失すべきオブジェクトの処理を行う関数である。また、次の( \ sfs’ f -> processMovement’ (f sfs’))は、イベントが発生したときは、それに対応したプロセス処理を行わさせる関数である。

プログラム全体はloopPre以下である。

loopPre ([],[],0) $
  adaptInput
  >>> processMovement objs
  >>> (arr elemsIL &&& detectCollisions)

ここでは、loopPreで初期設定を行う。次に、最初のサイクルに入るが、ここでは、入力を並び替えて、物理的な動作を処理し、新しい衝突を発見する。この後の処理は省いてあるが、出力を得て、衝突を次のサイクルのために入力として渡るようにする。

loopPre ([],[],0) $
  adaptInput
  >>> processMovement objs
  >>> (arr elemsIL &&& detectCollisions)