代数データ型を使ったゲームのアクター
代数データ型を使ったゲームのアクター
ご挨拶
こんにちは、すっかり寒くなってインフルエンザにやられてしまったh_sakuraiです。 頭もやられて、ここ1週間ほどまったく進捗ありません。が、そろそろ復活したい所です。
昨日の記事ですが、@camloebaさん曰く
ありがとう!Windowsでも動くという情報がありました RT @no_maddo OCaml: merlinでcompilerのコードを補完できるようにする - (略)
とのことです。merlin便利そうですね。是非入れてみましょー。
ML Advent Calendar 2014の2日目の今日はOCamlでゲームを代数データ型を使ってゲームのキャラクターを作ってみます。
いくつか記事書こうと思うのでよろしくお願いします。
今日の話しは結論から先に書くとイマイチ良くないという結論になるのですけど、ネガティブキャンペーンをしようというわけではありません。そこはご理解ください。 白骨化した冒険者の日記があれば、次の冒険者は同じ過ちをしなくて済むってぇもんです。
はじめに
UnityではC#のコルーチン使えるのでコルーチンや継続を使った方が奇麗に書けます。でも、8bitのファミコン時代の時代からマルチスレッドでもないのにゲームのキャラクターは複数同時に動いていました。その辺の仕組みを関数型言語で書いてみます。
最初の例
最初は、配列のデータの中にキャラクターは存在しており、それが構造体のデータとして存在するようになったのでしょう。例えば左右に動くキャラクターがいた場合は、今は左に、今は右に動くと言った状態を持って動いていた事でしょう。
OCamlで書くと以下のようになります。
type state = left | right type actor = Enemy of state let move = function | Enemy(left) -> Printf.printf "left\n"; Enemy(right) | Enemy(right) -> Printf.printf "right\n"; Enemy(left) let _ = let enemy = Enemy(left) in let enemy = move enemy in let enemy = move enemy in let enemy = move enemy in enemy
stateが左に動くか右に動くかを表します。 actorはキャラクターの種類を表しています。 move関数で動きを記述します。 最後に動かしてみると、
left right left
と表示されます。
関数に処理を分割する
これはこれで良いのですが、状態でswitchやmatchを書くのって関数が長々として気持悪いので、関数として分けたくなるので、以下のように書き換えます。
type state = left | right type actor = Enemy of state let move e = match e with | Enemy(left) -> move_left e | Enemy(right) -> move_right e and move_left = function | Enemy(_) -> Printf.printf "left\n"; Enemy(right) and move_right = function | Enemy(_) -> Printf.printf "right\n"; Enemy(left) let _ = let enemy = Enemy(left) in let enemy = move enemy in let enemy = move enemy in let enemy = move enemy in enemy
うう、こういう場合は、C言語より長くなって嬉しくないですね。でもまぁ、こうなります。
関数で状態を持つ
状態をイチイチ定義して関数を定義するのは面倒ですね。 関数を状態として持たせましょう。
type actor = Enemy of actor -> actor let move = match e with | Enemy(m) -> m e let move_left = function | Enemy(_) -> Printf.printf "left\n"; Enemy(move_right) and move_right = function | Enemy(_) -> Printf.printf "right\n"; Enemy(move_left) let _ = let enemy = Enemy(move_left) in let enemy = move enemy in let enemy = move enemy in let enemy = move enemy in enemy
短く書けるようになりました。
親子関係とモジュール化
次は、さらに種類を増やして親子関係を持たせてみます。 各、アクターはモジュールに分けてみます。
open Printf type actor = | BG | Enemy of (actor -> actor) | Child of (actor -> actor) * actor | Parent of (actor -> actor) * (int->unit) * actor list ref module Actor = struct let move e = match e with | BG -> printf("BG\n"); e | Enemy(m) -> m(e) | Child(m,_) -> m(e) | Parent(m,_,_) -> m(e) end module BG = struct let new_() = BG end module Enemy = struct let rec new_() = Enemy(move1) and move1 e = match e with | Enemy(m) -> printf("enemy move1\n"); Enemy(move2) | _ -> assert false and move2 e = match e with | Enemy(m) -> printf("enemy move2\n"); Enemy(move1) | _ -> assert false end module Child = struct let rec new_ p = Child(move1, p) and move1 this = match this with | Child(m,(Parent(_,msg,_) as p)) -> printf("child move1\n"); msg(10); Child(move2,p) | _ -> assert false and move2 this = match this with | Child(m,(Parent(_,msg,_) as p)) -> printf("child move2\n"); msg(10); Child(move1,p) | _ -> assert false end module Parent = struct let rec new_ () = let childs = ref [] in let p = Parent(move, msg, childs) in childs := [Child.new_ p; Child.new_ p]; p and move this = match this with | Parent(m,_,childs) -> printf("parent\n"); childs := List.map(fun (child:actor)-> Actor.move(child) ) !childs; this | _ -> assert false and msg i = printf "parent msg %d\n" i end let _ = let e = BG.new_()in let _ = Actor.move(e) in let e = Enemy.new_()in let e = Actor.move(e) in let _ = Actor.move(e) in let p = Parent.new_() in let _ = Actor.move(p) in ()
親子関係を持たせたゲームの動きがそれなりにかけました。 でも色々と嬉しくありません。
そうご参照する為に、リファレンスを使わなくては行けなくなるあたり、残念です。
何故嬉しくないのか?
何故嬉しくないのかと言うと、ゲームのアクターは垂直分割したいのに、水平分割するのに便利な機能を使ったからです。残念ながら、このようなケースでは嬉しくないですね。ありがとうございました。
ファンクタを使えばどうなるとかあるのかもしれませんが、あまり良い結果は得られないような気がします。 レコードを使う手もありますが、ま、本命はobjectでしょう。
そしてオブジェクト指向へ
という事で、明日はオブジェクト指向を使ってみます。