でシューティングを読む(1)

Monadiusが話題に上りましたが、何が書いてあるかわけが分からないという人が多いです。
自分もその中の一人、、、。


少し、Haskellで出来ているシューティングゲームを読みながら解説っぽいことでもしていこうかと思います。
というわけで、作者の方が依然作っていたモナディックシューティング Shu-thing のソースから読んでいこうと思います。
http://www.geocities.jp/takascience/index.html
ソースはここからダウンロードです。

構造から見ていきましょう。

まず、
import文が続きます。モジュールの読み込みを行っています。

import Graphics.UI.GLUT hiding (position)
import Graphics.Rendering.OpenGL.GLU
import Control.Exception
import Control.Monad
import System.Exit
import Prelude hiding (catch)
import Data.IORef
import Data.List
import Data.Maybe
import System.Process 

hiding (position) とか hiding (catch)が謎で(たぶん、使わんってことかと)あること以外は、なんか知らないけど、

  • GLUT,GLU(OpenGLまわり)
  • Exception(例外)
  • Monad(謎のモナド)
  • Exit(多分プログラムが終わる時でも使うんしょ)
  • Prelude(基本的な奴)
  • IORef(ポインタみたいなの。状態保存しておく変数っぽいことをやるために使う)
  • List(リスト操作用の関数とか入ってるんじゃないの)
  • Maybe(なんかしらんけど、モナドの説明に出てくる簡単なモナドらしい)
  • Process(プロセス周りのことでもするんだろう)


って予想が出来ます。
で、定義されてる物をざっと書いて見ます。


main(エントリポイント)
exitLoop(プログラムを終了する)
initMatrix(OPENGLの初期化)
dispProc(表示時に呼ばれる)
data Scene(次に動かす関数?を入れる。)
openingProc(オープニング)
endingProc(エンディング)
mainProc(ゲームのメイン)
timerProc(タイマーごとに動くイベントハンドラみたいなの)
keyProc(キーボードが押された時に動くイベントハンドラみたいなの)
closeProc(ウィンドウが閉じた時に動くイベントハンドラみたいなの)
bosstime(ボスが出る時間)
bosstime2(ボスが出る時間2)
data GameObject(ゲーム用のデータを入れる)
data EnemySpec(敵の仕様)
updateObject(オブジェクトの更新)
watcher(謎)
renderGameObject(ゲームオブジェクトのレンダリング
data GameState(ゲームの状態)
initialGameState(ゲーム状態の初期化)
renderGameState(ゲーム状態をレンダリング
updateGameState(ゲーム状態の更新)
playerpos(プレイヤーの位置)
findplayer(プレイヤーを見つける)
findBoss(ボスを見つける)

isGameover(ゲームオーバー判定)
isClear(クリア判定)

type Point(位置情報)
data Shape(シェイプ情報)

+++(演算子定義)
-+-(演算子定義)
*++(演算子定義)
***(演算子定義)

distance2(差分2)
distance(差分2)
outofmap(マップの外)
draw(描画)
magnify(?)
translat(?)
koch(?)
sampleShape(?)


いきなりこれだけ見ると死ぬってことで、まず、オープニング部分だけに削ってコンパイルしてみましょう。


main(エントリポイント)
exitLoop(プログラムを終了する)
initMatrix(OPENGLの初期化)
dispProc(表示時に呼ばれる)
data Scene(次に動かす関数?を入れる。)
openingProc(オープニング)
timerProc(タイマーごとに動くイベントハンドラみたいなの)
keyProc(キーボードが押された時に動くイベントハンドラみたいなの)


今回はこれで動かしてみます。

import Graphics.UI.GLUT hiding (position)
import Graphics.Rendering.OpenGL.GLU
import Control.Exception
import Control.Monad
import System.Exit
import Prelude hiding (catch)
import Data.IORef
import Data.List
import Data.Maybe
import System.Process 

{-
 メイン関数(ここから始まる)
 TODO: $= の意味がわからない
 TODO: 例外処理がイマイチ分からない。
 -}
main = do

  -- 値  を持つ新しい IORef を作り keystate に入れます。
  keystate <- newIORef 
  -- 値 (openingProc keystate) を持つ新しい IORef を作り、cpに入れます。
  cp       <- newIORef (openingProc keystate)
  -- ウィンドウサイズを640x480にする
  initialWindowSize $= Size 640 480
  -- ディスプレイモードをRGBModeのダブルバッファに設定する。
  initialDisplayMode $= [RGBMode, DoubleBuffered]

  -- Shu-thing と言う名前でウィンドウを作成
  wnd <- createWindow "Shu-thing"

  -- 表示用のコールバック関数を設定
  displayCallback $= dispProc cp

  -- キーボード、マウスのコールバック設定
  keyboardMouseCallback $= Just (keyProc keystate)

  -- タイマーのコールバックを設定
  addTimerCallback 16 (timerProc (dispProc cp))

  -- メニューを設定
  attachMenu LeftButton (Menu [
    MenuEntry "&Exit" exitLoop])

  -- マトリックス初期化
  initMatrix

  -- メインループ
  mainLoop

  -- ウィンドウ削除
  destroyWindow wnd

  -- 例外をキャッチした場合は終了
  `catch` (\e -> return ())

{-
  ループを抜けるために例外を発生させる。
	a $ b c
	は
	a (b c)
	の意味である。
	exitLoop = throwIO $ ExitException ExitSuccess
	は
	exitLoop = throwIO (ExitException ExitSuccess)
	と同じ意味
	Haskell の場合
	a b c と書くと、aの関数の引数としてbとcが渡されるという意味になるので、
	bの関数にcを引数として渡してその結果をaに渡したい場合は
	a (b c) もしくは、a $ b c と書く必要がある。
 -}
exitLoop = throwIO $ ExitException ExitSuccess

{-
 マトリックス初期化
 -}
initMatrix = do
  viewport $= (Position 0 0,Size 640 480)
  matrixMode $= Projection
  loadIdentity
  perspective 30.0 (4/3) 600 1400
  lookAt (Vertex3 0 0 (927::Double)) (Vertex3 0 0 (0::Double)) (Vector3 0 1 (0::Double))

{-
 表示プロシージャ
 タイマーやrepaintで呼ばれる。
 IORefは要はポインタみたいなもんらしい。
 cp: 関数と状態が入っている変数
 たぶん、cpに入っている関数を実行して、次のcpを書き換えしているのだと思う。
 -}
dispProc cp = do
  -- IORef cp から実体を取り出す。
  m <- readIORef cp

  -- 実行して、値をnextに入れる
  NewScene next <- m

  -- IORef cp に値を入れる
  writeIORef cp next

{-
 Scene っていうデータ型を定義
 意味がわからん。
 -}
data Scene = NewScene (IO Scene)

{-
 オープニングシーン
 -}
openingProc :: IORef [Key] -> IO Scene
openingProc ks = do
  -- キー状態を読み込む
  keystate <- readIORef ks

  -- 画面クリア
  clear [ColorBuffer,DepthBuffer]

  -- モデルビューのモードにして
  matrixMode $= Modelview 0
  loadIdentity

  -- 色設定
  color $ Color3 (1.0::Double) 1.0 1.0

  -- 文字表示
  preservingMatrix $ do
    translate $ Vector3 (-250::Double) 0 0
    scale (0.8::Double) 0.8 0.8
    renderString Roman "shu-thing"

  -- 文字表示
  preservingMatrix $ do
    translate $ Vector3 (-180::Double) (-100) 0
    scale (0.4::Double) 0.4 0.4
    renderString Roman "Press Z key"

  -- バッファをスワップ
  swapBuffers

  return $ NewScene $ openingProc ks

{-
  時間ごとに呼ばれる
  m >> addTimerCallback 16 (timerProc m)
  は
  do
   m
   addTimerCallback 16 (timerProc m)
  と同じ意味。
 -}
timerProc m = m >> addTimerCallback 16 (timerProc m)


{-
 キー入力コールバック
 keystate : キー状態を保存する変数
 key : 押されたキーコード
 ks : Down or Up
 mod : ?
 pos : ?
 -}
keyProc keystate key ks mod pos =
  case (key,ks) of
    (Char 'q',_) -> exitLoop

    -- modifyIORef IORef の値を更新します。
    -- nub 重複を取り除く関数(module List) key
    -- .演算子は関数合成をする物で++[key]って関数とnubを合成している。
    -- keystateの中身はリストであるのだけど、そのリストに
    -- ++[key]したあとnubするということですな。
    (_,Down) -> modifyIORef keystate (nub . (++[key]))
    -- filter (/=key) で、keyでないものを残す関数を表すと
    (_,Up) -> modifyIORef keystate (filter (/=key))

とりあえず、これでコンパイルして動きます。
今日はここまで。
コメントないよりは読みやすいかなぁと思うけど、解説になってねーや。
mainで初期化して、イベントでdispProc呼ばれて、
その中で、Sceneに入っている関数を実行する。と関数がまた帰ってくるのでそれをSceneに保存する。
それの繰り返しです。
ここらへんは、逐次実行してるだけですねぇ。