PureScriptでゲームが簡単に作れるライブラリを作るぞ
経緯
PureScriptでゲームを作りたいな~って思って軽くラフ書いたのがこれ
名前はGlappleにした.理由: 適当
リポジトリは以下
現状
canvas描画を簡単に行うライブラリとなっている.ゲームを開始するにはrunGameM
関数にGameSpecM
型の値をぶちこむ
newtype GameSpecM sprite gameState input output = GameSpecM
{ fps :: Int
, canvasSpec ::
{ height :: Number
, width :: Number
}
, sprites :: Array (sprite /\ String)
, initGameState :: gameState
, render :: GlappleM gameState output (Picture sprite)
, handler :: Event input -> GlappleM gameState output Unit
}
GameSpecM
型の値を作るには次の値が必要である
-
fps
画面のfps -
canvasSpec
画面の大きさ -
sprites
画像を先読み込みする
ex:[Apple /\ "images/apple.png", Banana /\ "images/banana.png"]
これをすることで後述するPicture
でsprite
関数によって画像を表示出来る -
initGameState
ゲームの初期状態.任意の型を状態として持てるgameState
で多相化されている. -
render
ゲーム画面を表示する毎フレームの処理 -
handler
様々なイベントを処理する
render
, handler
がわかりにくいので詳しく
render
GlappleM gameState output
はモナドであって,中で使えるのはliftEffect
による副作用のある処理,現在のゲーム状態を取得するgetGameState
,現在のゲーム状態を更新するputGameState
,およびmodifyGameState
,ゲーム開始からの時間を取得するgetTotalTime
である.
またPicture sprite
はキャンバスに描く図形を表す.
empty
: 空の図形
sprite
: sprites
で読み込んだ画像を描画
text
: 文字列を描画
polygon
: 多角形を描画
etc...
などでPicture sprite
型の値を作れる.
例えば,回転するりんごを例にとる
rotatingApple :: forall o. GlappleM GameState o (Picture Sprite)
rotatingApple = do
timeMaybe <- getTotalTime
let
time = case timeMaybe of
Just (Milliseconds x) -> x
Nothing -> 0.0
pure $ rotate (2.0 * pi * time / 200.0) $ sprite Apple
getTotalTime
から得られる値はMaybe
でくくられたMilliseconds
の値.これはゲーム開始前のプリロードの段階で呼ばれたとき,Nothing
を返すため(ゲーム開始時からの時間を得るからゲーム開始前は定義されてない(普通に0.0
返してもいいかも知れない,わからず))
でなんやかんやしてtime
にgetTotalTime
の値をMaybe
を引き剥がし数値にしてぶちこむ.
sprite Apple
でりんごの画像を描画.それをrotate
で 2.0 * pi * time / 200.0
回転,という流れ
これをGameSpecM
のrender
に指定してやれば動く
handler
Event
様々なイベントを表す型.
data KeyState = KeyDown | KeyUp
data Event input = KeyEvent String KeyState | Update Milliseconds | Input input
KeyEvent
についてはわかりやすい.キーが押されたときに呼び出される.例えば"w"キーが押された時に内部状態を更新するハンドラはこう書ける
handler (KeyEvent "w" KeyDown) = {何かしらの処理}
handler _ = pure unit
Update
は毎フレーム描画の後に呼び出されるもの.
Milliseconds
は前回Update
からの経過時間を格納している.
わかりにくいのはInput
これは別項目で
runGameM
関数
runGameM
:: forall sprite gameState input output
. Ord sprite
=> GameSpecM sprite gameState input output
-> CanvasElement
-> (output -> Effect Unit)
-> Effect (GameId gameState input output)
runGame
にはGameSpec
型の値,キャンバス要素,そしてOutputハンドラ(後述)を打ち込めば勝手にゲームが始まる.そしてGameId
型の値が帰ってくる(後述)
input
とoutput
GameSpecM
の定義をみるとinput
とoutput
が多相化されている.これについて.
Input
ゲームを開始した後も,キャンバス外からのアクションをゲームに伝えたいときがある.
ex. キャンバス要素とは別にボタン要素を作って,そのボタンを押すとセーブが出来る,など.
この際使うのがInputの仕組みで,通信に用いる型がinput
である.また通信を発火させる関数はtell
である.次のような感じで使える.
# ゲーム側
data Input = SomeInput
handler (Input SomeInput) = {- 何かしらの処理 -}
...
# 外部
gameId <- runGameM gameSpec canvas outputHandler
tell gameId SomeInput --ここでゲームと通信
tell
に関数にgameId
をぶち込むと通信が出来る.わかりやすい.
Output
逆にゲームからの何かしらの出力を処理したいときがある.そのとき使うのがOutputの仕組み.通信を発火させる関数はraise
# ゲーム側
data Output = SomeOutput
handler {- 何かしら -} = do
...
raise SomeOutput --ここで外部と通信
...
# 外部
outputHandler SomeOutput = {- 何かしらの処理 -}
gameId <- runGameM gameSpec canvas outputHandler --ここでハンドラを渡す.
ゲームからSomeOutput
が渡されるたび{- 何かしらの処理 -}
を実行する
以上.
今後
GameState
1つのみでゲームを管理するのはこころもとない.(Webページのコンポーネント分けのように,ゲームの状態も細かく分割管理されるべき)
ここの仕組みを作りたい.
GameSpecC
という、レンダーとハンドラを多相化して色々受け付けるやつ作ってみたけどあまり便利じゃなさそう
コンポーネントシステムができた
GameId
はゲームそのもの表すような型であるとして,renderGame
という関数でGameId
をPicture
に変換できるようにした.これで生成したゲームのGameId
を他のゲームのrender
内で使うことが出来るので,階層化出来る
ゲームの生成場所は親ゲームのGlappleM
内
もちろんGameId
を使って通信もで
今後
一度作成したゲームを破棄出来るようにしたい
destroy
関数で子ゲームを破棄,destroyMe
で自分自身を破棄出来るようにした
しかし今後一切更新をしなくなるという意味であって,メモリ上から破棄されるというわけではない.
それぞれのゲームの実態はただの関数なので問題は無いかもしれない
今後
イベントを充実させる(現在キーボード入力しか見ていない)
経過秒数がMilliSecondで帰ってくるのウザイので変える
完
今後
一回5FPSまで落ちたのの原因を調べたいが再現性がない……
バグが治った
詳細↓
でも高階コンポーネントのノリで高階ゲームを作ろうとしたら詰んだ
詰んだ
原因はInputの型が直和のような形である事が好ましいから。例えば子ゲームのInputの型が
data Input = Hoge Int | Fuga Int
であったならば親ゲームは子ゲームにInput通信をするとき、Hoge
とFuga
から一つ選んで渡します。
しかしレコード型
type Input = {hoge :: Int, fuga :: Int}
のような形であるときはhogeとfuga両方に値を入れなければなりません。これはInputとして扱いづらいです
ところで高階ゲームを作るときは取れる子ゲームを型で絞り込みできます たとえば内部状態として
forall r. {foo :: Int | r}
という型をもつ子ゲームしか取れないとすればfooという内部状態を持つゲームなら全部取れるようになって便利です
こういう類の多相化はレコード型の方が便利ですが、前述の通りInputはレコード型であると意味を成しません 從って高階コンポーネントでInputの拡張というものが難しくなってしまいました。
row typeを上手く使えば解決できそう