モナドのまほう 第2話『ゲームループとキー入力ができました』
* [第一話 画像が表示できました](/post/5321d8cebce7b87851f6/) ←前回 つづきです。 # 座標変換!:eyes: 前回は画像の読み込みと複数回の描画ができました。ここで**急にクォータービューっぽく表示してみたくなった**(思いつき)ので、そうしてみます! `translate`とか`scale`とか`rotate`とかそういう座標変換の関数をそういう感じに呼びます! アフィン変換について知らない人はゲームプログラミングの書籍なんかを読んでお勉強してみましょう! ```haskell translate { translateX: 1280.0 * 0.5 , translateY: 720.0 * 0.5 } context scale { scaleX: 1.0, scaleY: 0.6 } context rotate (45.0 / 180.0 * pi) context ``` これらの関数について、詳しくは[Canvas API](https://developer.mozilla.org/ja/docs/Web/HTML/Canvas)をどうぞ。 # `save`/`restore`!:cop: そして忘れてはいけないのが、コンテキストの`save`と`restore`。これを忘れるとゲームループが回るたびに座標変換が蓄積していってわけのわからないことになります。まあまだゲームループも実装していませんけど。もちろんPureScriptからでも問題なく`save`と`restore`を呼ぶことができます。 ```haskell save context pure unit -- do something restore context ``` でも、これじゃうっかり`restore`を書き忘れるかも……。そんなときは`withContext`っていう関数が用意されているので、これを使えば自動的に`save`と`restore`をしてくれるので楽ちん! ```haskell withContext context do pure unit -- do something ``` `save`のあとにこの一段インデントされた部分が実行され、それが終わると勝手に`restore`してくれます。それでは表示してみましょう。  きっったあああああああああ!:smiling_imp::smiling_imp::smiling_imp: クォータービュー:small_orange_diamond:だ! でも英語ではIsometric projectionっていうそうですよ! # ゲームループとキー入力!:point_left: 静止画を表示しただけではゲームにはなりません。アニメーション、すなわち画面を時間にそって変化させることが必要ですが、そのためにはいわゆる**ゲームループ**が必要です。今はとくにゲームライブラリを使っていないので、ゲームループも自作する必要がありますが、この部分はJavaScriptで書きました(血涙)。イベントハンドラを仕掛けたりと多少DOMをいじらなくてはならないのですが、PureScriptの生DOMライブラリは死ぬほど面倒くさくて、はっきり言って使わないほうがよっぽどマシです。でもあんまりガチなUIフレームワークを使うほどではないので、代わりにJavaScriptでゲームループ関連のコードを書いて、それを**外部関数インターフェイス**を使ってPureScriptから呼び出すことにします。 JavaScript側は適当にこんな感じに。PureScriptから呼び出されるJavaScriptコードはCommonJSモジュールとして書きます。呼び出されたらすぐに内容を実行するのではなく、いったん引数のない関数オブジェクトを返しているのが特徴です。PureScript側からもう一度その関数オブジェクトを呼び出すことで本体が実行されます。 ```js:JavaScript exports.gameloop = function(callback){ return function(){ function next(){ callback(); count += 1; window.requestAnimationFrame(next); } var count = 0; window.addEventListener("keydown", function(e){ if( ! keyTable.hasOwnProperty(e.keyCode)){ keyTable[e.keyCode] = count; } }); window.addEventListener("keyup", function(e){ delete keyTable[e.keyCode]; }); next(); }; }; ``` PureScript側はこんな感じ。 ```haskell:PureScript foreign import gameloop :: forall eff . Eff (gameloop :: GameLoop, dom :: DOM | eff) Unit → Eff (dom :: DOM | eff) Unit ``` この関数`gameloop`はこんな感じで呼び出します。 ```haskell gameloop do withContext context do translate { translateX: 1280.0 * 0.5 , translateY: 720.0 * 0.5 } context scale { scaleX: 1.0, scaleY: pitch / 90.0 } context rotate (45.0 / 180.0 * pi) context translate { translateX: 36.0 * -2.0 , translateY: 36.0 * -2.0 } context for (0 .. 9) \y → do for (0 .. 9) \x → do drawImage context grass (36.0 * toNumber x) (36.0 * toNumber y) pure unit ``` こうすると、`gameloop`からひとつインデントされた部分が`requestAnimationFrame`のタイミングで繰り返し実行されます。これで何度も再描画はされているのですが、何も表示が変わっていないのでアニメーションしているように見えません……:sob::sob::sob: # 変更可能な領域!:alien: さて、PureScriptは**純粋関数型プログラミング言語**であり、**すべての式が純粋**です。噂には聞いたことがあるかもしれませんが、次のようにして変数を直接書き換えて状態を変更することができません。 ```haskell let counter = 0 gameloop do counter = counter + 1 -- こういうことはできない! ``` なんてこった……:frowning: 状態を変えられないとか、純粋関数型な言語じゃゲームにならないじゃないか……もう駄目だ……お終いだ……:cold_sweat: いや! まだだ! まだ**`Ref`**がある! 使い方は簡単で、`newRef`で変更可能な状態を作り、`readRef`で状態の読み取り、`writeRef`や`modifyRef`で状態の書き換えができます。 ```haskell ref ← newRef 0 -- 可変な領域を作成! 初期値は0! gameloop do count ← readRef ref -- 領域から値の読み取り! writeRef ref (count + 1) -- 領域への値の書き込み ``` これで、ゲームループが回るたびにカウンターがひとつずつ増えていきます。状態を読み取る前に`readRef`を呼んで領域から現在の状態を取り出さなくてはならないのがちょっと面倒ですが、三匹の豚精神で乗り切ります。逆にいえば、DOMや外部のライブラリが持つ状態を除けば、**アプリケーション自身の状態といえるものはこの`ref`の状態だけ**であり、それ以外にはまったく状態を持たないのが保証されるので、アプリケーションの状態の管理がとても楽です。なんかゲームの状態がおかしくなったなあと思ったらこの`ref`の中だけ確認すればいいのです。 それでは、さっき`gameloop`関数と一緒に定義した`getKey`関数で、キーの状態を取得し、現在の状態に反映させます。それからその状態を使って画面を再描画します。試しに、変更されている状態を、座標変換の回転量に使いましょうか。実行させてみるとこうなります。  あああああああ! キー操作でくるくる回っているのに、静止画だから伝わらない! 詳しくは実際に動かしてみてください。 # 画像の読み込み(二回目)!:camera: 現状の方法だと、読み込む画像が増えるたびにネストがどんどん増えていってしまいます。 ```haskell main = do withImage "grass.png" \grass → do withImage "foo.png" \foo → do withImage "bar.png" \bar → do pure unit ``` これはキモいので、**非同期処理モナド**という謎の技術を使ってこれを平坦に書けるようにします(なお、PureScriptの非同期処理モナドについては、[こちら](/post/98598af2b0e9a6206ef3/)で筆者も紹介しています)。これを使って`loadImage`という関数を定義すると、こんなふうに書けるようになります。 ```haskell main = launchAff do grass ← loadImage "grass.png" foo ← loadImage "foo.png" bar ← loadImage "bar.png" pure unit ``` よし!:sun_with_face: これでネストが増えていかない!:cactus: え? なんでこういうふうに書けるのかって? そういう難しいことをあんまり気にしちゃだめです! # 全体像!:chicken: 今日のところはここまでにしておいてやる!:cop: キャンバスを取得するところとかもちょっとリファクタリングして、こんなかんじです(再現)。 ```haskell main = launchAff do grass ← loadImage "grass.png" canvas ← getCanvas "canvas" liftEff $ runST do context ← getContext2D canvas stateRef ← newSTRef (0 :: Int) gameloop do state ← readSTRef stateRef let zeroOrOne key = maybe 0 (const 1) <$> getKey key rx ← (-) <$> zeroOrOne 68 <*> zeroOrOne 65 writeSTRef stateRef (state + rx) clearRect context { x: 0.0, y: 0.0, w: 1280.0, h: 720.0 } withContext context do translate { translateX: 1280.0 * 0.5 , translateY: 720.0 * 0.5 } context scale { scaleX: 1.0, scaleY: 0.6 } context rotate (45.0 / 180.0 * pi) context for_ (0 .. (chunkSize - 1)) \y → do for_ (0 .. (chunkSize - 1)) \x → do drawImage context grass (36.0 * toNumber x) (36.0 * toNumber y) ``` # 次のお話:chicken: * [第三話 オンラインゲームになりました](/post/5d3f61339e84d2715f71/) ----
(この記事は同じ筆者が Qiita に投稿した記事の複製です。オリジナル記事)