js_of_ocamlでsnake作ってみた
OCamlでcanvasと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を触るミュータブル・手続き的なところとゲームロジックをしっかり分離できたのはよかった。この方法でいろいろやってみたい。
ちなみに今回の実装で一番時間がかかったのは:
js_of_ocamlで少しだけ複雑なことしようとしたらまったく表示されず、30分くらい試行錯誤して結局htmlが
— zehnpaard (@zehnpaard) February 28, 2021
<script type=“text/javscript”...
になってたことを発見した
これのデバッグだった・・・