bitterharvest’s diary

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

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

5.用語の説明

説明に入る前にまず言葉の定義をしておく。ゲームの中で登場する人や物を登場者と呼ぶ。その振舞いは信号関数により定義されている。ゲームはある時間間隔で進行する。一つの時間間隔をサイクルと呼ぶ。登場者はあるサイクルを始める前にある状態を有する。サイクルの終了後に登場者が有する状態は、登場者の信号関数と開始時の状態によるものとする。ただし、登場者は入力デバイス(キーボード、マウス)からの入力や他の登場者との干渉によって影響を受けることがあるものとする。この影響をイベントと呼ぶ。登場人物の状態の変化を求めることがゲームであるとする。

ゲームをプログラム化するにあたって、プログラムの中で使用する言葉も定義する。登場者には、振舞いを規定する信号関数とサイクルの開始時や終了時の状態があるが、登場者といったときは振舞いを指すこととする。具体的には信号関数で定められたものとする。登場者状態といったときは、登場者の状態を表すものとする。

ゲームでは、通常、複数の登場者が登場するが、いくつかの登場者は同じ振舞いをしてもよいものとする。即ち、同じ、信号関数を有してもよいものとする。但し、状態は同じでなくてもよいものとする。例えば、説明している「簡単なシューティング・ゲーム」には、二つの石と一匹の鳥がいる。石は同じ振舞いをするが、位置は異なる。

登場者や登場者状態は、次のサイクルに移るとき、そのすべてを引き渡すことになるが、ここでは、連想リストによく似ている識別リスト(Identity List)をもちいる。登場者の識別リストは、識別番号と登場者を対にしたリストである(さらに付加的な情報として、次に割当て可能な識別番号を有している。割当て番号は昇順である)。登場者状態の識別リストは、同様に、識別番号と登場者状態を対としたリストである。一般に、型aに属するものの集まりを識別リストにしたものをIL aと表す。

6.核構成部routeの処理

前回の記事で、一つのサイクルの処理を行うcoreの部分については説明した。ここでは、coreの構成要素について説明を行う。まず、routeである。routeの機能は、前のサイクルからの登場者状態の識別リストを受け取り、登場者ごとにサイクル開始時の状態を得て、このサイクル終了時の登場者の状態を作り出せるように用意することである。

プログラムは次のようになっている。

route :: (Input, IL ObjOutput) -> IL sf -> IL (ObjEvents, sf)
route (input, oos) objs = mapIL routeAux objs
  where
    hs = hits (assocsIL (fmap ooState oos)) 
    routeAux (k, obj) = (ObjEvents
      { oeInput = input,
        oeLogic = if k `elem` hs then Event () else NoEvent
      }, obj)

routeの型シグネチャからみていく。最初の入力(Input, IL ObjOutput)は前のサイクルで発生した状況を示す。Inputは入力デバイス(キーボードやマウス)からのイベントである。IL ObjOutputはこのサイクル開始時点での登場者状態の識別リストである。状態の中には、後で説明するが、それらの位置や速度、消去要求などが含まれている。

二番目の入力IL sfは(振舞いを表した)登場者の識別リストである。出力はイベントと登場者をタプルにしそれに識別番号を与え、それを識別リストにして送出している。
Routeの主要部分はmapIL routeAux objsである。一般に、mapIL f IL aは型aの識別リストを型faの識別リストに変える。従って、このプログラムではaは登場者objであるので、routAuxにより登場者の部分をイベントと登場者のタプルに変える。このタプルの集まりに対して作られた識別リストが出力される。

イベントには二種類あり、一つは入力デバイスからのもの、もう一つは他の登場者との干渉によるものである。このプログラムでは衝突である。衝突はk `elem` hsによって検知される。ここで、hsは衝突を起こした登場者の識別番号のリストである。従って、ここでの登場者の識別番号kがこのリストhsに含まれているかを調べることによって

衝突を起こした登場者のリストは次のhitsという関数により得る。

hits :: [(ILKey, State)] -> [ILKey]
hits kooss = concat (hitsAux kooss)
  where
    hitsAux [] = []
    hitsAux ((k,oos):kooss) =
        [ [k, k'] | (k', oos') <- kooss, oos `hit` oos' ]
        ++ hitsAux kooss

    hit :: State -> State -> Bool
    hit state1 state2 = dis2 (position' state1) (position' state2) < 0.1
    dis2 (x1, y1) (x2, y2) = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)

上記のプログラムは、登場者のooState(位置と速度)のリストを受け取り、自身と、リストの中で自身より以降に来る登場者の間で衝突があるかどうかを調べ、衝突したものの識別番号をリストとして出力する。衝突したかの判定はhitで行う。hitでは、二つの登場者がある範囲の中に接近した時(プログラムでは距離の二乗が0.1より小さい時)衝突したものとみなす。

7.核構成部routeの処理

衝突した登場者には退場することもあり得る。このゲームでは、鳥に石が当たったとき鳥を退場させることとした。退場させられる登場者は、ooKillRequestというイベントを状態として有する。後で説明するが、ooKillRequestは、登場者の状態を表すものの一つで、これは登場者(信号関数)がこのサイクルでの状態の変化を登録するときに、イベントとして残す。従って、あるサイクルの中で、鳥と石の間で衝突が起こり、それによって、鳥の状態にooKillRequestというイベントが立った時、次のサイクルで、このイベントを知って鳥は退場させられる。退場の処理を行うのが、killAndSpawnである。これは次のようになっている。

killAndSpawn :: ((Input, IL ObjOutput), IL ObjOutput)
             -> Event (IL Object -> IL Object)
killAndSpawn (_, oos) =
  foldl (mergeBy (.)) noEvent events
  where
    events :: [Event (IL Object -> IL Object)]
    events = [ mergeBy (.)
                      (ooKillRequest oo `tag` (deleteIL k))
                      (fmap  (foldl (.) id . map insertIL_)
                             (ooSpawnRequests oo))
             | (k, oo) <- assocsIL oos ]

上記のプログラムで、撃ち落とされた鳥の退場は(ooKillRequest oo `tag` (deleteIL k))で行われる。他の部分は、今後の拡張を考えてキーボードからの信号に対応できるようにしてあるので、ここでは無視して欲しい。

8.モジュールProcessのプログラム

最後になるが、これまでのプログラムをmodule Processとしてまとめたものを掲載しておく。

{-# LANGUAGE Arrows #-}
module Process where

import FRP.Yampa
import IdentityList
import Types

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

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

route :: (Input, IL ObjOutput) -> IL sf -> IL (ObjEvents, sf)
route (input, oos) objs = mapIL routeAux objs
  where
    hs = hits (assocsIL (fmap ooState oos)) 
    routeAux (k, obj) = (ObjEvents
      { oeInput = input,
        oeLogic = if k `elem` hs then Event () else NoEvent
      }, obj)

hits :: [(ILKey, State)] -> [ILKey]
hits kooss = concat (hitsAux kooss)
  where
    hitsAux [] = []
    hitsAux ((k,oos):kooss) =
        [ [k, k'] | (k', oos') <- kooss, oos `hit` oos' ]
        ++ hitsAux kooss

    hit :: State -> State -> Bool
    hit state1 state2 = dis2 (position' state1) (position' state2) < 0.1
    dis2 (x1, y1) (x2, y2) = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)

killAndSpawn :: ((Input, IL ObjOutput), IL ObjOutput)
             -> Event (IL Object -> IL Object)
killAndSpawn (_, oos) =
  foldl (mergeBy (.)) noEvent events
  where
    events :: [Event (IL Object -> IL Object)]
    events = [ mergeBy (.)
                      (ooKillRequest oo `tag` (deleteIL k))
                      (fmap  (foldl (.) id . map insertIL_)
                             (ooSpawnRequests oo))
             | (k, oo) <- assocsIL oos ]