OCamlでオブジェクト指向を使ったゲームのアクター

この記事はML Advent Calendar 2014の3日目の記事です。

昨日は代数データ型を使ってみましたが、今日はオブジェクト指向を使ってみます。 オブジェクト指向自体の説明はもっと良い記事があると思うので、複数のキャラクターを同じリストに入れて動かしたり、親子関係を持たせて、動かすオブジェクトを書いてみます。

open Printf

class actor =
  object(this)
    val mutable move = fun()->()
    method move = move()
  end

まずは、actorクラスを書いてみます。moveというmutableな関数を持つメンバ変数とmoveメソッドを定義します。OCamlオブジェクト指向Rubyオブジェクト指向に近いので、メンバ変数は使えないんですよね。そんな事は知ってるって気もしますけど、、、。

type actor = <move: unit>

で、型をかいてみると、こんな感じになります。moveメソッドだけがあります。<>でオブジェクトは括るようです。

class enemy_a =
  object(this)
    val mutable move = fun()->()
    method move = move()
    method private move1() = printf("a1\n"); move <- this#move2
    method private move2() = printf("a2\n"); move <- this#move1
    method init = move <- this#move1; this
  end

敵を定義してみます。initメソッドで初期化して、move1を入れるというように書きました。 コンストラクタのような物の書き方があるのかないのか、わからなかったので、initメソッドを作りました。 これ、ヘーって思うかもしれないんですが、メソッドを関数として取り出す方法で1時間とか結構悩んだんです。 ちょっとググっても、見つからないし、、、。という事で、これを見ればもう悩まないはずです。

class enemy_b =
  object(this) 
    inherit actor
    method private move1() = printf("b1\n"); move <- this#move2
    method private move2() = printf("b2\n"); move <- this#move1
    method init = move <- this#move1;this
  end

inheritで継承を使う事も出来るんですね。 enemy_aより短く書けました。

type e_parent = < move : unit; init : e_parent; k :unit >
type e_child = < move : unit; init: e_child >

次にe_parentとe_childの型を作っておきます。 このオブジェクトの型も見慣れないので辛かった。 色んなメソッドだけを<>で括ったものが型になります。

class enemy_child(parent:e_parent) =
  object(this) 
    inherit actor
    method private move1() =
      printf("c2\n");
      parent#k
    method init =
      move <- this#move1;
      (this :> e_child)
  end

enemy_childクラスを定義します。enemy_childは親を受け取る形になります。initはe_childと明示する必要があって苦労しました。何処をどう、型合わせたら良いんじゃ!っていう。

class enemy_c =
  object(this) 
    inherit actor
    val mutable childs: e_child list = []
    method private move1() =
      printf("c1\n");
      List.iter(fun e->e#move) childs

    method k =
      printf("c1 k\n")

    method init =
      move <- this#move1;
      childs <- [
        (new enemy_child(this :> e_parent))#init ;
        (new enemy_child(this :> e_parent))#init
      ];
      (this :> e_parent)
  end

enemy_cという親のクラスを定義して、childs(childrenって書けよ)リストに子供を入れてみました。 この辺型合わせるのが、最初大変でした。相互参照させるの難しくありません?って感じでした。 何回も大変大変って書くなよw

let _ =
  let f =
    let a = new enemy_a in
    (fun()-> a#init)
  in
  (f())#move;
  
  let ls = [
    (new enemy_a#init :> actor);
    (new enemy_b#init :> actor);
    (new enemy_c#init :> actor)] in
  for i = 0 to 3-1 do
    List.iter(fun l-> l#move) ls; printf"--\n"
  done

で、リストに入れて動かせます。 とりあえず、OCamlJavaインターフェイスを書く感じで型を定義しておいて、その型でリストを作れば様々な型のオブジェクトを1つのリストに登録して使う事が出来ました。

でも、<>でオブジェクトの型を書かなくても、出来ないのでしょうか?と思って書いてみたのが次のプログラムです。できるんですね。

open Printf

(* 動くだけ *)
class actor =
  object(this) 
    method move = ()
  end

動くだけのクラスを定義します。

class actor_a =
  object(this) 
    method move = printf("move a\n")
  end

同じ型のクラスを書きます。型が同じ形なので1つにまとめられます。

(* 状態を持ったアクター *)
class actor_m =
  object(this) 
    val mutable move = fun()->()
    method move = move()
    method private move1() = ()
    method init = move<-this#move1;(this :> actor_m)
  end

次に初期か関数があるオブジェクトを作ってみます。

(* メッセージを受け取るアクター *)
class actor2 =
  object(this) 
    val mutable move = fun()->()
    method move = move()
    method private move1() = ()
    method msg = ()
    method init = (this :> actor2)
  end

actor2はmsgメソッドがあるアクターとします。微妙に違うクラス並んでるだけですけど。

(* 子供 *)
class actor2_c(parent:actor2) =
  object(this) 
    val mutable move = fun()->()
    method move = move()
    method init =
      move <- this#move1;
      this
    method private move1() =
      printf("child move1\n");
      parent#msg;
      move <- this#move2
    method private move2() =
      printf("child move2\n");
      parent#msg
    method msg =
      printf("child msg ok\n")
  end

(* 親 *)
class actor2_p =
  object(this) 
    val mutable move = fun()->()
    method move = move()
    val mutable childs: actor2 list = []
    method private move1() =
      printf("parent move\n");
      List.iter(fun e->e#move; e#msg) childs

    method msg =
      printf("parent msg ok\n")

    method init =
      move <- this#move1;
      childs <- [
        (new actor2_c(this:>actor2))#init ;
        (new actor2_c(this:>actor2))#init
      ];
      (this :> actor2)
  end

親子関係のあるオブジェクトはこんな感じで書けます。

let _ =
  let actors = [
    new actor_a;
    (((new actor2_p)#init) :> actor);
    new actor_a;
  ] in
  List.iter(fun a->a#move) actors

で、アクターを1つのリストにまとめて書く事が出来ました。特に型は書かなくても出来ましたね。 型は書いてないけど、インターフェイスになるようなclassを作れば型を作ったのと同じというような気持で書けます。 こんな風に、OCamlのオブジェクトは使えるんですねぇ。っという話でした。

明日は@master_qさんの「禅問答的に #ATS2 の型理論を説明してみたよ」です。