Unityですごろくを作ってみる2

ayumegu(プログラマー)
よろしくお願いします。

はい、こんにちは〜。
今回は前回のすごろくに経路探索を組み込んで見ました。
やったことは

  • ゴールまでのマスの数の表示
  • 道が選択できるときに、近いほうの矢印を赤くしました。

サイコロの挙動が怪しいのは直してないです。(;w;)
どこがおかしいんだろな〜w

参考書籍

ActionScript3.0にアニメーションというオライリーの書籍です。
ActionScript3.0用のソースをちょっとUnityようにいじって、すごろく用にアレンジしています。
経路探索についてはゲーム開発者のためのAI入門という本(こちらもオライリー)にも記載があります。

今回の作成結果

すごろくのようなもの2

ゴールの場所を変更してもちゃんと計算されます。

経路探索のクラス

追加した経路探索用のクラスです。

Node.js
一つの探索したノードの情報を格納するクラス
gがこのノードまでの距離、hがこのノードからゴールまでの予測の距離、fにgとhを足した値が入ります。
parentはこのノードの前のノード
walkableは移動できるかどうかです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Node{
  var x:int;
  var y:int;
  var f:float;
  var g:float;
  var h:float;
  var walkable:boolean = true;
  var parent:Node;
  
  public function Node(x:int, y:int){
      this.x = x;
      this.y = y;
  }
}

Grid.js
検索するタイルの情報を保持するクラス

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class Grid{
  private var _startNode:Node;
  private var _endNode:Node;
  private var _nodes:Array;
  private var _numCols:int;
  private var _numRows:int;
  
  public function Grid(numCols:int, numRows:int){
      _numCols = numCols;
      _numRows = numRows;
      _nodes = new Array();
      for(var x:int = 0; x < _numCols; x++){
          for(var y:int = 0; y < _numRows; y++){
              _nodes[x + y *  _numCols] = new Node(x,y);
          }
      }
  }

  public function getNode(x:int, y:int):Node{
      return _nodes[x + y *  _numCols] as Node;
  }
  
  public function setEndNode(x:int, y:int):void{
      _endNode = getNode(x,y) as Node;
  }
  
  public function setStartNode(x:int, y:int):void{
      _startNode = getNode(x,y) as Node;
  }
  
  public function setWalkable(x:int, y:int, value:boolean):void{
      getNode(x,y).walkable = value;
  }
  
  public function get endNode():Node{
      return _endNode;
  }
  
  public function get numCols():int{
      return _numCols;
  }
  
  public function get numRows():int{
      return _numRows;
  }
  
  public function get startNode():Node{
      return _startNode;
  }

}

AStar.js
こちら実際に経路探索を行うクラス
heuristicはhを計算する方法です。細かいことは割愛w
pathにスタートからゴールまでのNodeのリストが格納されます

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
public class AStar{
  private var _open:Array;
  private var _closed:Array;
  private var _grid:Grid;
  private var _endNode:Node;
  private var _startNode:Node;
  private var _path:Array;
  private var _straightCost:float = 1.0;
  private var _diagCost:float = Mathf.Sqrt(2);
  private var _heuristic:Function = manhattan;    // マンハッタン法
  // private var _heuristic:Function = euclidian; // ユークリッド法
  // private var _heuristic:Function = diagonal;  // 対角線法
  private var _skewPathFlg:boolean; // 斜め移動許可
  
  public function AStar(){
  }
  
  public function findPath(grid:Grid, skewFlg:boolean){
      _grid = grid;
      _skewPathFlg = skewFlg;
      _open = new Array();
      _closed = new Array();
      _startNode = _grid.startNode;
      _endNode = _grid.endNode;
      _startNode.g = 0;
      _startNode.h = _heuristic(_startNode);
      _startNode.f = _startNode.g + _startNode.h;
      
      return search();
  }
  

  
  public function search():boolean{
      var node:Node = _startNode;
      while(node != _endNode){
          var startX:int = Mathf.Max(0, node.x -1);
          var endX:int = Mathf.Min(_grid.numCols - 1, node.x + 1);
          var startY:int = Mathf.Max(0, node.y - 1);
          var endY:int = Mathf.Min(_grid.numRows - 1, node.y + 1);

          for(var i:int = startX; i <= endX; i++){
              for(var j:int = startY; j <= endY; j++){
                  var test:Node= _grid.getNode(i,j);
                  if(test == node || !test.walkable){
                      continue;
                  }
                  
                  // 斜め移動禁止
                  if(!_skewPathFlg){
                      var dx:Number = node.x - test.x;
                      var dy:Number = node.y - test.y;
                      var num:float = Mathf.Sqrt(dx * dx + dy * dy) * _straightCost;
                      if(num > 1){
                          continue;
                      }
                  }
                  
                  var cost:Number = _straightCost;
                  if(!((node.x == test.x) || (node.y == test.y))){
                      cost = _diagCost;
                  }
                  var g:float = node.g + cost;
                  var h:float = _heuristic(test);
                  var f:float = g + h;
                  if(isOpen(test) || isClosed(test)){
                      if(test.f > f){
                          test.f = f;
                          test.g = g;
                          test.h = h;
                          test.parent = node;
                      }
                  }else{
                      test.f = f;
                      test.g = g;
                      test.h = h;
                      test.parent = node;
                      _open.push(test);
                  }
              }
          }
          _closed.push(node);
          if(_open.length == 0){
              Debug.Log("経路がないよ");
              return false;
          }
          BubbleSort(_open);
          node = _open.shift() as Node;
      }
      
      buildPath();
      return true;
  }
  
  // ソート
  private function BubbleSort(_nodeList:Array):Array{
      for(var i:int = 0; i < _nodeList.length; i++){
          for(var j:int = 0; j < _nodeList.length-1; j++) {
              if(Sort(_nodeList[j], _nodeList[j+1])){
                  var tmp:Node = _nodeList[j+1];
                  _nodeList[j+1] = _nodeList[j];
                  _nodeList[j] = tmp;
              }
          }
      }
      return _nodeList;
  }
  
  
  private function Sort(node1:Node, node2:Node){
      return node2.f - node1.f;
  }
  
  private function buildPath():void{
      _path = new Array();
      var node:Node = _endNode;
      _path.push(node);
      while(node != _startNode){
          node = node.parent;
          _path.unshift(node);
      }
  }
  
  public function get path():Array{
      return _path;
  }
  
  private function isOpen(node:Node):boolean{
      for(var i:int = 0; i < _open.length; i++){
          if(_open[i] == node){
              return true;
          }
      }
      return false;
  }
  
  private function isClosed(node:Node):boolean{
      for(var i:int = 0; i < _closed.length; i++){
          if(_closed[i] == node){
              return true;
          }
      }
      return false;
  }
  
  private function manhattan(node:Node):float{
      return Mathf.Abs(node.x - _endNode.x) * _straightCost +
      Mathf.Abs(node.y - _endNode.y) * _straightCost;
  }
  
  private function euclidian(node:Node):float{
      var dx:Number = node.x - _endNode.x;
      var dy:Number = node.y - _endNode.y;
      return Mathf.Sqrt(dx * dx + dy * dy) * _straightCost;
  }
  
  private function diagonal(node:Node):Number{
      var dx:int = Mathf.Abs(node.x - _endNode.x);
      var dy:int = Mathf.Abs(node.y - _endNode.y);
      var diag:int = Mathf.Min(dx, dy);
      var straight:int = dx + dy;
      return _diagCost * diag + _straightCost * (straight - 2 * diag);
  }
  
  public function get visited():Array{
      return _closed.concat(_open);
  }
}

使い方

使い方はまずタイルの初期化部分でGridデータを作成します。
引数は横のタイルの数と、縦のタイルの数です。

1
grid = new Grid(masuColSize,masuRowSize);

次にマスのループ内で各マスが移動可能かをセットします

1
grid.setWalkable(x,y,false);

そして、スタート位置、ゴール位置をセットします

1
2
grid.setStartNode(x, y);
grid.setEndNode(x, y);

これで初期設定完了

実際に経路探索する場合はこんな感じ

1
2
3
4
5
6
7
var astar:AStar = new AStar();
  if(astar.findPath(grid, false)){
      astarPath = astar.path;
      goalMoveText.GetComponent(TextMesh).text = "ゴールまで残り" + (astarPath.length-1) + "マス";
  }else{
      Debug.LogError("ゴールできません");
  }

AStarクラスのfindPathメソッドを呼ぶ。
引数はGridデータと斜め移動を許可するかどうかのフラグです。 すごろくの場合は斜め移動はないので指定できるようにしました。
すると、astar.pathに、現在地点からゴールまでのNodeのリストが入っています。
次のマスを取得したい場合はastarPath[1];です。
最初に現在のノードが入っているので、ゴールまでのマスの数は-1するのを忘れずに。
これを移動するたびに行えばOK!

今回の感想

前回のにそのまま経路探索組み込んだだけなのでかなりチープなすごろくですがまぁご愛嬌ということで。
経路探索を使えばマス目形のゲームで大助かりです。
たとえば

  • 今回のようなゴールへのマス数、最短経路を表示するようなすごろく
  • ダンジョンゲームで敵が追いかけてくる経路の計算
  • あるいはマスに好きに配置できる形のタワーディフェンスで、おけるかどうかのチェックや、敵の経路の計算

ぱっと思いつくので3つ。
ソースを変更すれば六角形のマスとかも同じ考え型でいけるはず。

では今回はこの辺で^^