代数データ型を使ったゲームのアクター

代数データ型を使ったゲームのアクター

ご挨拶

こんにちは、すっかり寒くなってインフルエンザにやられてしまった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でしょう。

そしてオブジェクト指向

という事で、明日はオブジェクト指向を使ってみます。