Arantium Maestum

プログラミング、囲碁、読書の話題

js_of_ocamlでsnake作ってみた

OCamlcanvasとLwtを使ってsnakeゲームを実装して、js_of_ocaml + duneでコンパイルしてみた:

js_of_ocaml snake - compile to JS with command `dune build ./main.bc.js` · GitHub

ポイントとしては

  • ゲームロジックを比較的ステートレスな形でgame.mlに記述(Randomを使っているので完全にステートレスではない・・・)
  • js_of_ocamlやブラウザ機能、Lwtなどはmain.mlのみで利用

という分離。まあこれはOCamlじゃなくても当然だとは思うが。

ゲームロジック

game.mlでは

  • ゲームの状態を表す型を定義する
  • 状態遷移のロジックを「イミュータブルなゲーム状態データから次のゲーム状態データへの変換関数」として書く

という比較的OCamlらしい書き方を心がけたつもり。カリカリにパフォーマンスが大事なゲームだとこういうスタイルを貫くのは難しくなるんだろうけど、軽いブラウザゲームくらいだったらこの方式でやっていけるのではないか。

個人的には

let remove_tail state = 
  let rec remove_last = function
  | [] -> []
  | [_] -> []
  | x::xs -> x :: (remove_last xs) in
  {state with body=remove_last state.body}

が見ていて少し辛くなる。リストの最後の要素を除くって毎回O(n)・・・。気になるならdequeでも使うところで、調べたところ有名なOkasakiのPurely Functional Data Structuresにイミュータブルなdequeの実装が載っているらしい。今度調べてみたい。

しかしそもそもn(snake本体の長さ)が大きくならないので今回はこれでよしとする。

UIとゲームループ

main.mlのキモは(* game loop & event handlers *)の部分。

ゲームループは

let rec game_loop canvas_context =
  game_state := Game.next_gamestate !game_state !direction;
  draw_game_state canvas_context;
  Lwt_js.sleep 0.1 >>= (fun () -> game_loop canvas_context)

で、ゲームステートを更新・描画してから0.1秒スリープしてループする。ここではミュータブルな変数(ref型)を使っている。いろんなハンドラが触るのでミュータブルの方が都合がいい、気がしている。

ハンドラについては今回は

canvas##.onclick 
Html.window##.onkeydown
Html.window##.onload

といったものを使っている。ここらへんの使い勝手はよくも悪くもJavaScriptと同じようなもの((js "main")のように値をOCaml->JSへと変換する箇所が出てくるぶん、少しJSより使いづらいかも)。

こういうUI的な部分はそのままJSで書いてしまって、イミュータブル&静的型が効きやすいロジック部分だけOCamlで書いてjs_of_ocamlコンパイルする、というやり方もありかもしれない。

まとめ

ブラウザ、DOMを触るミュータブル・手続き的なところとゲームロジックをしっかり分離できたのはよかった。この方法でいろいろやってみたい。

ちなみに今回の実装で一番時間がかかったのは:

これのデバッグだった・・・