2012年2月17日金曜日

2D迷路データとその解を自動生成する(6):迷路の解を用いてゴール・敵・アイテムの配置を決める

このエントリでは、迷路の解を用いてゴール・敵・アイテムの配置を決める方法のヒントを示します。

これまでに、「2D迷路データとその解を自動生成する(4):「ポテンシャル法」で迷路を解いてみる」「2D迷路データとその解を自動生成する(5):「ポテンシャル法」で迷路を解くための実装」のエントリを通じて、次のようなデータを得られることについて述べてきました。

  • stepMap 迷路上のそれぞれの地点がスタート地点から何歩で到達できる場所なのかを示す2次元配列
  • stepList スタート地点からの同じ距離ごとに、迷路内地点のリストをまとめた2次元配列
  • pathStepTotal この迷路上に「道」のセルがいくつあるかを数える変数

迷路のゴール地点は、スタート地点から到達するのに、なるべく多くの歩数がかかるような場所にあるべきです。 そこで、stepListの配列の一番最後の要素を使うのです。これは、その迷路の中で、スタート地点から一番遠い地点を格納した配列になっています。 つまり、stepListの配列の一番最後の要素の配列のうち、いずれかの要素を選んでゴール地点とすれば良いと考えられます。 …あとは、ゴール地点の座標の周囲について、マップチップの内容を置き換える and/or 別のスプライトを重ねて置くことで、 いかにも「ゴールだ!」と分かるような見た目に演出してやればOKです。

なお、ここまでは説明の便宜上、「スタート地点」と、そこからの「同じ距離ごとの迷路内地点」という言い方をしてきました。 でも、これはようするに、迷路上の任意の地点Aから、任意の地点Bまでについて、最短距離での道のりの歩数を調べる方法を実現しているのです。 そう考えれば、この方法を応用して、いろいろなしくみを実現できることがわかります。

たとえば、ゲームスタート時に、敵キャラが主人公キャラのすぐ近くに居て、いきなりダメージを喰らってしまう!というのはアンフェアです。 そういうことがないように、敵キャラの初期位置は、主人公キャラから「一定以上の距離」になるようにするべきです。 このためには、stepListの配列で最初のいくつかの要素を除外したものから、敵を配置する場所の候補を作り出せば良いということになります。

また、アイテムを配置するときに、「扉」と「その鍵」を一定以上離れた場所に配置したいといったような場合、 ゲームプレイ中に、主人公キャラを追いかける形で、迷路の最短距離を賢く確実に移動する敵キャラを作りたい場合にも、 同じような方法を用いることができるでしょう。

ちなみに、現在のThe Flame Knightでは、ゴール地点の配置、敵キャラ・落ちているアイテムの配置をするときに、スタート地点からの距離を用いています。

2D迷路データとその解を自動生成する(5):「ポテンシャル法」で迷路を解くための実装

先に「2D迷路データとその解を自動生成する(2):「穴掘り法」による迷路データ生成」のエントリで説明したMazeクラスに対して、_initAnswerメソッドを追加します。

このメソッドを実行することで、

  • stepMap 迷路上のそれぞれの地点がスタート地点から何歩で到達できる場所なのかを示す2次元配列
  • stepList スタート地点からの同じ距離ごとに、迷路内地点のリストをまとめた2次元配列
  • pathStepTotal この迷路上に「道」のセルがいくつあるかを数える変数

というようなインスタンス変数が作成され、利用できるようになります。

_initAnswerのコードは以下のとおりです。

_initAnswer: function(){

    var COLLISION = 0;
    var NOT_INITIALIZED = -1;

    // この迷路に「道」のセルがいくつあるかを数える変数
    this.pathStepTotal = 0;

    // stepMap として、迷路上のそれぞれの地点がスタート地点から何歩で到達できる場所なのかを示す2次元配列を作成する。
    // 迷路外周(最上段・最下段・左端・右端)と、迷路上の「壁」の部分に「COLLISON(衝突)」の値、
    // 迷路上の「道」の部分には「未初期化状態」の値を代入しておく。

    this.stepMap = new Array(this.mazeHeight);

    for(var i = 0; i < this.mazeHeight; i++){
        var answerRow = new Array(this.mazeWidth);
        for(var j = 0; j < this.mazeWidth; j++){
            answerRow[j] = this.hitTest(j,i)? COLLISION : NOT_INITIALIZED;
        }
        this.stepMap[i] = answerRow;
    }

    // スタート地点からの同じ距離ごとに、迷路内地点のリストをまとめた2次元配列を作成する。
    this.stepList = [];

    // スタート地点を設定する
    this.stepMap[this.sy][this.sx] = 1;

    var stepCount = 1; //調査する場所の、スタート地点からの歩数。最初は1歩目について調査。
    this.stepList[stepCount - 1] = [new Point(this.sx, this.sy)]; // 0歩目の場所は、スタート地点。

    while(true){

        var nextStepList = new Array(); //現在ステップ数の場所から到達できる、次のステップの地点をすべて格納するためのリスト

        for(var i=0; i < this.stepList[stepCount-1].length; i++){//(stepCount-1)歩目の場所のリストについて繰り返し。
            var p = this.stepList[stepCount-1][i]; // (stepCount-1)歩目の場所を、ここでの注目地点とする。
            var px = p.x;
            var py = p.y;
            //注目地点の「上」のセルが未初期状態かどうか
            if(this.stepMap[py-1][px] == NOT_INITIALIZED){
                this.stepMap[py-1][px] = stepCount+1;
                nextStepList.push(new Point(px, py-1));
            }
            //注目地点の「下」のセルが未初期状態かどうか
            if(this.stepMap[py+1][px] == NOT_INITIALIZED){
                this.stepMap[py+1][px] = stepCount+1;
                nextStepList.push(new Point(px, py+1));
            }
            //注目地点の「左」のセルが未初期状態かどうか
            if(this.stepMap[py][px-1] == NOT_INITIALIZED){
                this.stepMap[py][px-1] = stepCount+1;
                nextStepList.push(new Point(px-1, py));
            }
            //注目地点の「右」のセルが未初期状態かどうか
            if(this.stepMap[py][px+1] == NOT_INITIALIZED){
                this.stepMap[py][px+1] = stepCount+1;
                nextStepList.push(new Point(px+1, py));
            }
        }
        if(nextStepList.length == 0){
            //もうこれ以上遠くへは到達できない場合はループを脱出して終了!
            break;
        }else{
            // 次のステップ地点を格納したリストを、this.stepList配列のステップ数番目に格納。
            this.stepList[stepCount++] = nextStepList;
            // このステップでの道のセルの数を合計に加算
            this.pathStepTotal += nextStepList.length;
        }
    }
}

さらに、この_initAnswerで作成した stepMap, stepList の内容を確認するためのdebugメソッドを追加しておくことにします。 このメソッドを実行すると、「2D迷路データとその解を自動生成する(3):「ポテンシャル法」で迷路を解いてみる」のエントリ内での説明図のような内容を、コンソールに対して出力します。

    debug: function(){
     // 地図のコンソールに表示
     for(var i = 0; i < this.mazeData.length; i++){
         var line = "";
         for(var j = 1; j < this.mazeData[i].length - 1; j++){
          line += " "+String.fromCharCode(48+this.stepMap[i][j]);
         }
         console.log(line);
     }

     // 歩数ごとの地点リストをコンソールに表示
     for(var i = 0; i < this.stepList.length; i++){
         var line = ""+i+":\t"+String.fromCharCode(48+i)+"\t";
         for(var j = 0; j < this.stepList[i].length; j++){
          line += " "+this.stepList[i][j].toString();
         }
         console.log(line);
     }
    }

なお、大きな迷路についてdebugを実行すると、ASCII文字として表示できない場合が出てくると思いますが、面倒くさいのでチェックしてません!

2D迷路データとその解を自動生成する(4):「ポテンシャル法」で迷路を解いてみる

ここまでのエントリで、「どんな迷路を、どのようにつくるか」のうち、 (1)大きな構造の生成、(2)実際のMapに与えるdata生成 までを説明しました。 ここでは、(3)細部の構造の生成 について述べる前に、「どのように迷路の解をつくるか」について、具体的な例を示しながら解説をします。

次の図のような16x16の迷路があったとします。0が壁、1がスタート地点です。

この迷路について「ポテンシャル法(等高線法)」と呼ばれるアルゴリズムでの処理をした結果が次の図です。

  • 「0」は、「壁」を表しています。
  • 「0」以外の文字は、「道」を表しています。
  • 「1」が出発点で、出発点から最短で到達可能な歩数によって、ASCIIコード順にそれぞれの文字や記号が示されています。

1の出発点から1歩離れるごとに、数値〜文字のASCIIコードが大きいものになっていく様子が読み取れるでしょうか?

さらに、スタート地点からの距離ごとに、迷路内地点をリスト化してまとめたものを示しておきます。

0:  1 (3,8)
1:  2 (2,8) (4,8)
2:  3 (2,7) (5,8)
3:  4 (2,6) (6,8)
4:  5 (2,5) (6,9) (7,8)
5:  6 (2,4) (6,10) (7,7) (8,8)
6:  7 (2,3) (6,11) (5,10) (7,10) (7,6) (8,7) (8,9)
7:  8 (2,2) (6,12) (5,11) (4,10) (8,10) (7,5) (6,6) (8,6) (9,9)
8:  9 (3,2) (5,12) (7,12) (4,11) (3,10) (9,10) (7,4) (6,5) (5,6) (9,6) (10,9)
9:  : (4,2) (5,13) (4,12) (8,12) (2,10) (9,11) (10,10) (6,4) (8,4) (4,6) (10,6)
10: ; (4,3) (5,14) (3,12) (8,13) (9,12) (10,11) (9,4) (4,5) (11,6)
11: < (4,4) (4,14) (6,14) (2,12) (8,14) (9,13) (11,11) (10,4) (12,6)
12: = (3,14) (7,14) (2,13) (10,13) (12,11) (11,4) (12,5)
13: > (2,14) (11,13) (12,12) (12,4)
14: ? (12,13) (12,3)
15: @ (12,2)
16: A (11,2)
17: B (10,2)
18: C (9,2)
19: D (8,2)
20: E (7,2)
21: F (6,2)

この例では、出発点からいちばん遠いのは、Fの文字が置かれている(6,2)の点で、ここが21歩で到達するゴール地点だということになります。

こうしたデータ構造を作ってしまえば、「迷路を解く」のは、もう簡単です。 このゴールの地点から始めて、ASCIIコードを逆順に文字を辿れるような方向へと歩いていけば、最短距離での経路が求められるということになるわけです。

次のエントリで、こうしたアルゴリズムの実装例を示していきます。

2D迷路データとその解を自動生成する(3):迷路データからのマップデータ生成

ひきつづき、「どんな迷路を、どのようにつくるか」について、述べていきます。

(1)大きな構造の生成、(2)実際のMapに与えるdata生成、(3)細部の構造の生成、という 3段階のうち、「(2)実際のMapに与えるdata生成」についてを解説します。

以下、Worldというクラスを定義し、そのコンストラクタ内で一つ前のエントリで生成した迷路データをもとにして、enchant.jsのMapインスタンスに与える前景の地図データ・背景の地図データを作成するというプログラムを示していきます。

このWorldのインスタンスから、 「主人公キャラクタ開始位置」「地形衝突判定データ」「マップ前景データ」「マップ背景データ」などの それぞれの値が得られるものとします。

ここでは、Worldの実装として、 単純な「壁の最小の厚さと、道の最小の幅とが、基本的には同じ」であるような迷路マップを作ることを考え、 コンストラクタには、次のような引数を与えることにします。

  • mapWidth マップ全体の横幅のセル数
  • mapHeight マップ全体の高さのセル数
  • mapCellUnitSize 1セルがマップチップ何個分であるのかを表す個数
  • block_id 「岩」(通行不可能セル)を表すマップチップの値
  • wall_id 「壁」(通行可能セル)を表すマップチップの値
  • empty_id 「道」(通行可能セル)を表すマップチップの値

ようするに、迷路データのそれぞれのセルを、mapCellUnitSizeの分だけ単純に整数倍して引き伸ばした内容を迷路マップとして作っていこうというものです。 マップ全体の横幅・高さのセル数であるmapWidth, mapHeightを引数とし、迷路データの幅・高さのセル数については、内部的に算出するものとします。 また、地形の基本マップチップである「岩」「壁」「道」を表す値として、block_id, wall_id, empty_idの3種類の値を与えることにします。

この部分のプログラムは、特に複雑なアルゴリズムを用いているわけではなく、とってもかんたんです。

// 世界クラスの定義
var World = enchant.Class.create({
    /**
      @param {Number} mapWidth マップ全体の横幅のセル数
      @param {Number} mapHeight マップ全体の高さのセル数
      @param {Number} mapCellUnitSize 1セルがマップチップ何個分であるのかを表す個数
      @param {Number} block_id 「岩」(通行不可能セル)を表すマップチップの値
      @param {Number} wall_id 「壁」(通行可能セル)を表すマップチップの値
      @param {Number} empty_id 「道」(通行可能セル)を表すマップチップの値
    */
    initialize: function(mapWidth, mapHeight, mapCellUnitSize, mazeComplexity,
                                    block_id, wall_id, empty_id){

            //mazeの幅・高さを算出
     var mazeWidth = Math.floor(mapWidth/mapCellUnitSize);
     var mazeHeight = Math.floor(mapHeight/mapCellUnitSize);

            //mazeの生成
     var maze = new Maze(mazeWidth, mazeHeight,
                                        mazeComplexity,
                   startPoint.x, startPoint.y);

            //背景、前景、衝突判定データの初期化
     var bgMapData = new Array(mapHeight);
     var fgMapData = new Array(mapHeight);
     var collisionData = new Array(mapHeight);
     for(var i=0; i < mapHeight; i++){
         bgMapData[i] = new Array(mapWidth);
         fgMapData[i] = new Array(mapWidth);
         collisionData[i] = new Array(mapWidth);
     }

            // 背景・前景・衝突判定データの内容設定
     for(var i = 0; i < mapHeight; i++){
         for(var j = 0; j < mapWidth; j++){
                        // 迷路上の該当位置が「侵入不可能」セルであるかどうか
          var isCollid = maze.hitTest(Math.floor(j/mapCellUnitSize), Math.floor(i/mapCellUnitSize));

          if(isCollid){
              //該当位置が「侵入不可能」の場合は、壁ブロックを表示
              fgMapData[i][j] = block_id;
              bgMapData[i][j] = block_id;
              collisionData[i][j] = 1;
          }else{
              //該当位置が「侵入不可能」ではない場合は、「道」(床)を表示
              fgMapData[i][j] = empty_id;
              bgMapData[i][j] = empty_id;
              collisionData[i][j] = 0;
          }

          if(i % mapCellUnitSize == 0 && mapCellUnitSize <= i){
              var northIsCollid = maze.hitTest(Math.floor(j/mapCellUnitSize), Math.floor(i/mapCellUnitSize) - 1);
              if(! isCollid && northIsCollid){
               //上側が「侵入不可能」、該当位置が「侵入可能」の場合は、背景を「壁」に変更
               bgMapData[i][j] = wall_id;
              }
          }

          if(i % mapCellUnitSize == mapCellUnitSize - 1 && i < mapHeight - mapCellUnitSize){
              var southIsCollid = maze.hitTest(Math.floor(j/mapCellUnitSize), Math.floor(i/mapCellUnitSize) + 1);
              if(! isCollid && southIsCollid){
               //下側が「侵入不可能」、該当位置が「侵入可能」の場合は、前景を「岩」に変更
               fgMapData[i][j] = block_id;
              }
          }
         }
     }

     //プレイヤー開始位置
     this.startPoint = new Point(maze.sx * mapCellUnitSize,
                                maze.sy * mapCellUnitSize);

     //迷路データ
     this.maze = maze;
     //地形衝突判定データ
     this.collisionData = collisionData;
     //マップ前景データ
     this.fgMapData = fgMapData;
     //マップ背景データ
     this.bgMapData = bgMapData;
    },
    ....略....
});

なおここでは、「斜め上からの見下ろし」を実現するために、 キャラクタの上半身が奥の「壁」に重なって表示されたり、 下半身が手前の「岩」に隠れたりするために、

  • 上側が「侵入不可能」、該当位置が「侵入可能」の場合は、マップ背景の該当位置セルの値を「壁」に変更する
  • 下側が「侵入不可能」、該当位置が「侵入可能」の場合は、マップ前景の該当位置セルの値を「岩」に変更する

という後処理を加えています。こうした内容の「マップ背景」「マップ前景」を、 次のような順番でステージに追加してやれば、「斜め上からの見下ろし」のような表現を実現できることになります。

  • マップ背景
  • キャラクタ、アイテム
  • 爆発などの効果の表示
  • マップ前景
  • スコアなどの表示

2D迷路データとその解を自動生成する(2):「穴掘り法」による迷路データ生成

「どんな迷路を、どのようにつくるか」について、述べていきます。

ここでは、(1)大きな構造の生成、(2)実際のMapに与えるdata生成、(3)細部の構造の生成、という 3段階のうち、まずは「(1)大きな構造の生成」についてを解説します。

準備のために、2D座標空間上の点を表すPointクラスを用意します。

//点を表すクラス
Point = enchant.Class.create({
    initialize: function(){
     this.x = 0;
     this.y = 0;
     switch(arguments.length){
     case 1:
         this.x = arguments[0].x;
         this.y = arguments[0].y;
         break;
     case 2:
         this.x = arguments[0];
         this.y = arguments[1];
         break;
     }
    },

    toString: function(){
     return "("+this.x+","+this.y+")";
    }
});

では、大きな構造としての(見た目以前の)迷路を生成・表現するMazeクラスを実装していきます。

ここでは、迷路ゲームを作る場合に良く用いられている、「穴掘り法」と呼ばれるアルゴリズムを基本にしています。 この開発にあたっては、Ishida So氏の「迷路自動生成アルゴリズム」のページを参考にさせていただきました。併せて参照されることをおすすめします。

Maze = enchant.Class.create({
    /**
      @param {Number} mazeWidth 迷路全体の横幅
      @param {Number} mazeHeight 迷路全体の高さ
     @param {Number} complexity 迷路内の分岐の多さ
   */
    initialize: function(mazeWidth, mazeHeight, complexity){
     this.mazeWidth = mazeWidth;
     this.mazeHeight = mazeHeight;
     this.sx = Math.floor(rand(mazeWidth-4))+2;//スタート地点のx位置、迷路の左右端にならないように調整
     this.sy = Math.floor(rand(mazeHeight-4))+2;//スタート地点のy位置、迷路の上下端にならないように調整

     this.mazeData = new Array(mazeHeight); //迷路データを格納する配列
     this.route = []; // 迷路上のすべての「道」を格納する配列

     this.empty = 0;
     this.block = 1;

     this._createMaze(complexity); //迷路データの生成
    },

   /** 
     指定されたx,y位置に「道」を掘る
   */
    _dig: function(x, y){
     this.route.push(new Point(x, y));
     this.mazeData[y][x] = this.empty;
    },

    _createMaze: function(complexity){

           // 迷路の配列を、迷路外周(最上段・最下段・左端・右端)は「道」とし、
           // それ以外はすべて「壁」として初期化する。
           // 基本的に、「壁」を掘ることで迷路を作るので、
           // 迷路外周を「道」にしておけば、迷路外まで掘らずに済むのでアルゴリズムを単純化できる。

     for(var i = 0; i < this.mazeHeight; i++){
         var data = (i == 0 || i == this.mazeHeight - 1)? this.empty : this.block;//迷路の最上段と最下段は常に「道」とする
         var mazeRow = new Array(this.mazeWidth);
         mazeRow[0] = this.empty; //迷路の左端は常に「道」とする
         mazeRow[this.mazeWidth-1] = this.empty; //迷路の右端は常に「道」とする
         for(var j = 1; j < this.mazeWidth - 1; j++){
          mazeRow[j] = data;
         }
         this.mazeData[i] = mazeRow;
     }
     
     var direction = new Array(4);

     //「道」を掘るスタート位置
     var sx = this.sx;
     var sy = this.sy;

     //はじめに、スタート位置を掘る
     this._dig(sx, sy);

     for(var j = 0; j < complexity; j++){ //迷路内の分岐の回数だけ繰り返し
         var start = this.route[rand(this.route.length)]; //すでに掘ってある場所から任意の場所を選んで分岐の開始点を設定。
         sx = start.x;
         sy = start.y;

         while(true){
                        //「道」がループしないような「掘れる方向」をチェック。
                       // 堀った道が別の道に突き抜けないように、2つ先まで調べる。
          direction[0] =// 「上」方向には掘れるか?
          this.mazeData[sy-1][sx] != this.empty && amp;
              this.mazeData[sy-2][sx] != this.empty ;
          direction[1] =// 「右」方向には掘れるか?
          this.mazeData[sy][sx+1] != this.empty &&
              this.mazeData[sy][sx+2] != this.empty ;
          direction[2] =// 「下」方向には掘れるか?
          this.mazeData[sy+1][sx] != this.empty &&
              this.mazeData[sy+2][sx] != this.empty;
          direction[3] =// 「左」方向には掘れるか?
          this.mazeData[sy][sx-1] != this.empty &&
              this.mazeData[sy][sx-2] != this.empty ;

                        //穴掘り可能な方向の候補を配列に格納
          var candidate = [];
          for(var i = 0; i < 4; i++){
              if(direction[i]){
               candidate.push(i);
              }
          }
          if(candidate.length == 0){
                           //これ以上掘れない場所なので、穴掘り作業を中断。今回の開始点を穴掘り開始点の候補から削除。
              delete this.route[start];
              break;
          }

                        // 穴掘り可能な方向からひとつの方向をランダムに選ぶ
          switch(candidate[rand(candidate.length)]){
          case 0://「上」に掘る
              this._dig(sx, sy-1);
              sy -= 1;
              break;
          case 1://「右」に掘る
              this._dig(sx+1, sy);
              sx += 1;
              break;
          case 2://「下」に掘る
              this._dig(sx, sy+1);
              sy += 1;
              break;
          case 3:// 「左」に掘る
              this._dig(sx-1, sy);
              sx -= 1;
              break;
          }
         }
     }

     // 迷路の外周を壁で埋め直す
     for(var i = 0; i < this.mazeHeight; i++){
         this.mazeData[i][0] = this.block;
         this.mazeData[i][this.mazeHeight - 1] = this.block;
     }
     for(var j = 1; j < this.mazeWidth - 1; j++){
         this.mazeData[0][j] = this.block;
         this.mazeData[this.mazeWidth-1][j] = this.block;
     }
    },

    /** そこに壁があるかどうかを調べる
      @param {Number} x 調べる場所のx位置
      @param {Number} y 調べる場所のy位置
    */
    hitTest: function(x, y){
     return (0 <= y && y < this.mazeData.length &&
                0 <= x && x < this.mazeData[y].length &&
                this.mazeData[y][x] != this.empty);
    },

    ....略.....
});

2D迷路データとその解を自動生成する(1):仕様策定

ひさびさの更新です。予告していた「2D迷路データを作成する」の件についてのエントリを連続投稿します。

ここでの論点は次の2つです。

  1. どんな迷路を、どのようにつくるか
  2. どのように迷路の解をつくるか

1つめの「どんな迷路を、どのようにつくるか」については、次のような仕様を考えました。

  • 今回作成しているゲーム The Flame Kinght は、単なる迷路脱出ゲームではなく、 迷路上で敵と遭遇した場合に、炎で攻撃して戦ったり、すり抜けて避けたりするような、 アクション要素を持ったゲームです。そのため、フィールド内には 大きな広間のような場所があったり、廊下のような場所があったりといったように、 大きな構造としては変化のあるマップが作られることが望ましいです。
  • 今回使用するマップチップはenchant.js組み込みで提供されている限られた種類の画像です。 そのため、ゲーム世界は基本的には「人工的に作られたダンジョン」とし、 小さな構造としては変化の少ないリアルっぽくないマップでも構わないものとします。
  • 斜め上から見下ろすような視界とし、キャラクタの上半身が奥の壁に重なって表示されたり、 下半身が手前の壁に隠れたりするようにしたいです。

そういうわけで、これを、「穴掘り法」と呼ばれるアルゴリズムを基本に手を加えつつ、 (1)大きな構造の生成(2)実際のMapに与えるdata生成、(3)細部の構造の生成、という 3段階に分けて、いろいろ作りながら試した結果を説明していくことにします。

次に、2つめの「どのように迷路の解をつくるか」です。

ここでは、単に複雑な迷路データを作るだけでなく、それと同時に、 この迷路上をスタート地点からゴール地点まで通り抜けるような経路データを作成する必要があるのです。 そうすれば、マップの自動生成時に、スタート地点から一番離れた場所にボスキャラやゴール地点を配置するなどの、 さまざまなニーズを満たすことができるはずです。このしくみを、 「ポテンシャル法」と呼ばれるアルゴリズムで実現する方法について、基本的な考え方具体的な実装に分けて説明していきます。

2012年2月5日日曜日

The Flame Knight 公開

このブログ上での報告が遅くなりましたが、 9leapにて「The Flame Knight」を公開しました。

The Flame Knight

もともとの「ARPGフレームワークのサンプル」と、そんなに変わっていないように見えるかもしれませんが...

本業の仕事と、週末は家族サービスで忙殺されており、ゲーム開発のための、まとまった時間がなかなか取れずにおります。まぁ、ぼちぼちとやっていくしか。