読者です 読者をやめる 読者になる 読者になる

bitterharvest’s diary

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

YampaとOpenGL(GLUT)で簡単なシューティングゲームを作成する―プロセス(1)

1.プロセスの概略

Yampaを用いてのゲームは、reactimateを用いるが、reactimateは前にも記述した通り、init,input,output,processで構成されている。今回の記事では、この中のprocessについて説明する。ここで説明するゲームは極めて簡単なゲームで、石を二つ投げて飛んでいる鳥を撃ち落とすことである。

prosessは信号関数である。引数はゲームの登場者、出力は信号関数である。出力される信号関数は、キーボードなどからの入力inputを受け、新たな状態を有するゲームの登場者を出力する。具体的には、processは次のようになっている。

process :: IL Object -> SF Input (IL ObjOutput)
process objs0 = proc input -> do
  rec
    oos <- core objs0 -< (input, oos)
  returnA -< oos

これ以降の話を分かりやすくするために、ゲームプログラムの概略図(Meisinger作成)をもう一度掲載しておく。
f:id:bitterharvest:20141102101832p:plain

2.信号関数の復習

processの説明をする前に、信号関数を復習しておく。入力patを受けて、出力expを送出する信号関数は次の形式をとる。(ここでの記述はThe Yampa Arcadeの論文通りにしたが、pat1,pat2,..,patnがpatの一部であるかのような誤解を招くので、また、expも同じだが、本当は名前を変えたほうがよいと思う。)

proc pat -> do
  pat1 <- sfexp1 -< exp1
  pat2 <- sfexp2 -< exp2
  ......................
  patn <- sfexpn -< expn
  returnA -< exp

上記で、procはラムダ式でのλと同様の働きをする。pat,pat1,pat2,..,patnは信号変数で瞬間の信号値に束縛される。また、exp,exp1,exp2,..,expnは信号値である。sfexpiは信号関数でexpiを受けてpatiを送出する。(信号関数は内部状態を持たないので、patiはどのような時でもexpiが与えれれれば一意的に値が定まる。)また、ここで使われている矢印はPatersonのアロー記法である。

また、キーワードのrecは、単一または複数の定義のグループに対して定義され、記述順に変数の定義が解釈される式の中で利用され、再帰的な定義を許している。また、

let pat = exp

let pat = exp

の簡略記法である。
The Yampa Arcadeの論文には、次のような例が上がっている。

sf = proc (a,b) -> do
  c1 <- sf1 -< a
  c2 <- sf2 -< b
  c  <- sf3 -< (c1, c2)
  rec
    d <- sf4 -< (b, c, d)
  returnA -< (d, c)

上記のプログラムは、sfと名前の付けられた信号関数で、入力(a,b)を受けて出力(d,c)を送出する信号関数である。a,bはそれぞれ信号関数sf1,sf2によって信号値c1, c2を得る。また、信号値c1, c2のタプルから信号関数sf3によって信号値cを得る。次はrecになっているので、ここは再帰的に実行される。これは、dが入力側にも出力側にも表れていることに起因している。

recのところはなかなか分かりにくいのだが、次のようになっていると考えてほしい。sf4のところは複数の信号関数があって、それぞれが、(b, c, d)を受けて、dを送出しているものとする。複数の信号関数を一つ一つ処理してゆくのだが、最初の信号関数を処理するときはdはまだ存在していないので、この時は無として扱う。次の信号関数を処理するときは、最初の信号関数の処理で得たdとb, cを受けて新たなdを送出する。これを最後の信号関数まで行い、そこで得たものをこのプログラムの最終結果としてのdとする。

3.プロセスの処理

信号関数の働きがわかったところで、信号関数processの働きを調べることにする。processは上記で説明した信号関数とは異なり、引数にobjs0を有している。これはゲームの登場者である。このプログラムではゲームの登場者には識別名がつけられているので、ここでは識別名のリストが与えられるが、詳しい話は後で記述する。このゲームでの登場者は石二つと鳥がリストである。入力inputはキーボードやマウスから検知した信号であるが、このプログラムでは、これらを利用していないので無である。出力はoosとなっている。これはObjectOutputsを省略したもので、状態を有する登場者のリストである。ここでの状態にはkill(この登場者を破棄すること)がある。

processの主要な処理は、信号関数coreを処理することであるが、上の図からも分かる通り、coreは複数の信号関数から成り立っている。processの出力は、これら複数の信号関数からの出力を集めたものとなる。

4.核部分の処理

信号関数coreの部分は次のようになっている。

core :: IL Object -> SF (Input, IL ObjOutput) (IL ObjOutput)
core objs = dpSwitch route
                     objs
                     (arr killAndSpawn >>> notYet)
                     (\sfs' f -> core (f sfs'))

この信号関数の概略は次の通りである。信号関数の入力は、キーボードやマウスを検知して得られた信号inputと一つ前のサイクルで得られた状態を有する登場者のリストである。出力は、このサイクルでの処理を施した後の状態を有する登場者のリストである。従って、coreの主要な目的は、一サイクルゲームを進めることであるが、それはゲーム登場者の新たな状態を求めることである。その時、キーボードやマウスなどからゲームを左右するような信号が伝えられたときはそれに応じた処理を行う。

信号関数coreではdpSwitchが使われているので、ここでは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)))は、図でのrouteを正確に表現したものである。即ち、①キーボードやマウスからの信号とこのサイクルに入る直前の状態を有している登場者をaとして受け取る。また、②ゲームの登場者(これは信号関数で表されている)の集まりをcol sfとして受け取る。そして、③aからのイベント、即ち、入力デバイスからのイベントやこのサイクルで登場者間で発生した衝突などのイベントをbとし、それを受け取る登場者sfのタプルを出力する。これは、図ではrouteの右横に書かれたタプルのリストがそうである。

二番目の入力col (SF b c)は、ゲームでの登場者を表している。登場者のそれぞれは信号関数で表されているが、ここでの(登場者を表す)信号関数は入力をb(すなわち発生したイベント)とし、出力をcとしている。図では青いボックスで書かれたものである。

三番目の入力SF (a, col c) (Event d)は、イベントを発生する信号関数である。aからサイクルを進める中でイベントdが発生する信号関数である。ここで、cがcol cとなっているのは、イベントdにより複数の出力を持つ可能性があることに起因している。三番目の入力は図のkillAndSpawnに対応する。

四番目はイベントdが発生したときの信号関数を与える。もちろん、イベントが発生しなかったときは、最初に受取った信号関数を用いる。これらの信号関数は、登場者の新たな状態を表したものである。

coreを構成しているrouteやkillAndSpawnの中身は次の記事で紹介する。