Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9309

C#で経路探査アルゴリズムを作ってみた

$
0
0

完成図

446a81c014ab63b36030a7d4dcf371a6.png

はじめに

映画「メイズ・ランナー」を見ていたら、ふと経路探査アルゴリズムを作りたくなってしまいました。

2Dでブロックを描画できればよかったので、ぱっと思い浮かんだのはUnityエンジンでした。
しかし、
ab1630ae061f9592637996d50e095d3e.png
ご覧の通り、僕のPCにはもうUnityが入る隙がありませんでした。
そこで、致し方なくWinFormに描画することにしました。

描画方法

具体的には、WinFormにPictureBoxを配置してBitmapに描きます。

pictureBox1.Image=World.bitmap;

座標管理

今回は2Dで描画するので、各タイルのとりえる座標は x, yです。
これをより扱いやすくする為に2次元ベクトルとしてクラス化しました。

また、例えば2つのベクトルを計算したい時に、

Vector2v1=newVector2(1,1);Vector2v2=newVector2(1,1);
v1.x=v1.x+v2.x;v1.y=v1.y+v2.y;

とハードコーディングするのはしんどいですし、ミスも誘発しそうです。
ということで、演算子をオーバーロードしました。

Vector2result=newVector2();result=v1+v2;

完全なコードはGithubにもあります。
Github | Vector2.cs

classVector2{////////////////////////////////////////////////////////////////////////// 2次元ベクトルにおける X座標とY座標を保持////////////////////////////////////////////////////////////////////////publicintx{get;set;}publicinty{get;set;}////////////////////////////////////////////////////////////////////////// コンストラクタ////////////////////////////////////////////////////////////////////////publicVector2(int_x_,int_y_){this.x=_x_;this.y=_y_;}////////////////////////////////////////////////////////////////////////// 加法演算子のオーバーロード////////////////////////////////////////////////////////////////////////publicstaticVector2operator+(Vector2n1,Vector2n2){returnnewVector2(n1.x+n2.x,n1.y+n2.y);}////////////////////////////////////////////////////////////////////////// 減法演算子のオーバーロード////////////////////////////////////////////////////////////////////////publicstaticVector2operator-(Vector2n1,Vector2n2){returnnewVector2(n1.x-n2.x,n1.y-n2.y);}////////////////////////////////////////////////////////////////////////// 乗算演算子のオーバーロード////////////////////////////////////////////////////////////////////////publicstaticVector2operator*(Vector2n1,Vector2n2){returnnewVector2(n1.x*n2.x,n1.y*n2.y);}////////////////////////////////////////////////////////////////////////// 2点の座標における距離を整数で取得////////////////////////////////////////////////////////////////////////publicstaticintDistance(Vector2v1,Vector2v2){return(int)Math.Sqrt(Math.Pow(v1.x-v2.x,2)+Math.Pow(v1.y-v2.y,2));}////////////////////////////////////////////////////////////////////////// 2点の座標における距離を小数点で取得////////////////////////////////////////////////////////////////////////publicstaticdoubleDistanceEx(Vector2v1,Vector2v2){returnMath.Sqrt(Math.Pow(v1.x-v2.x,2)+Math.Pow(v1.y-v2.y,2));}}

ワールド管理

思ったよりソースコードが巨大になってしまったので、簡易的に書きます。
完全なコード: Github | World.cs

タイルの保持

タイルは配列に保持しておきます。

privatestaticTileBlock[,]tileBlocks{get;set;}

イニシャライズ

1.コンテキストの作成

publicstaticvoidCreateContext(){graphics=Graphics.FromImage(GetBitmap());if(graphics!=null){LoggerForm.WriteSuccess("Context created.");}else{LoggerForm.WriteError("CreateContext() failed.");}}

2.マップの生成

タイルを保持するための配列のメモリ確保と
ビットマップ上におけるタイルサイズの定義を行います。
そして、空のタイルを敷き詰めておきます。

publicstaticvoidCreateMap(){tileBlocks=newTileBlock[MAX_COORD_X,MAX_COORD_Y];tileSizeX=(bitmap.Width)/MAX_COORD_X;tileSizeY=(bitmap.Height)/MAX_COORD_Y;for(intx=0;x<MAX_COORD_X;x++){for(inty=0;y<MAX_COORD_Y;y++){AddTile(newVector2(x,y),TileType.Walkable);}}LoggerForm.WriteSuccess("Map created.");}

タイルの管理

タイルには座標だけでなく探索する際に使用するデータやその他色々な値を格納しておきたかったので、
構造体と挙列型を用意しました。

完全なコード: GitHub | TileBlock.cs

データ構造:
Untitled Diagram (2).png

////////////////////////////////////////////////////////////////////////// タイル属性を示すEnum////////////////////////////////////////////////////////////////////////publicenumTileType:int{Walkable=1,Wall=2,StartTile=3,GoalTile=4,NullTile=5,AnalyzedTile=6}
////////////////////////////////////////////////////////////////////////// 探索に使用する属性を示すEnum////////////////////////////////////////////////////////////////////////publicenumAnalyzeAttributes:int{INull=0x00000000,IOpenedTile=0x00000030,IClosedTile=0x00000040,}
////////////////////////////////////////////////////////////////////////// 探索データ構造体////////////////////////////////////////////////////////////////////////publicstructAnalyzeData{publicintコスト;publicint推定コスト;publicintスコア;}
////////////////////////////////////////////////////////////////////////// タイル構造体////////////////////////////////////////////////////////////////////////privatestructTileStruct{publicVector2coordinate;publicTileTypetileType;publicAnalyzeAttributesattributes;publicAnalyzeDataanalyzeData;publicboolisAnalyzed;}

描画関連

タイルの描画位置

まず初めに、今回使用するビットマップのサイズ$BS$は360×360です。
また、タイルの最大座標$MS$は25×25です。

このときビットマップ上に於けるタイルの縦/横描画サイズ$S$は

S(x,y) = \frac{BS}{MS}

マップの周りの余白$w$は

w = \frac{BS-S(x,y)}{2}

タイルの位置(左上角)$L$、余白$w$は

L = S(x,y)+w

です。
これを関数で表すと

tileSizeX=bitmap.Width/MAX_COORD_X;tileSizeY=bitmap.Height/MAX_COORD_Y;publicstaticSizeGetTileSize(){returnnewSize(tileSizeX,tileSizeY);}publicstaticVector2GetWhiteSpace(){varx=(bitmap.Width-(GetTileSize().Width*MAX_COORD_X))/2;vary=(bitmap.Height-(GetTileSize().Height*MAX_COORD_Y))/2;returnnewVector2(x,y);}publicstaticPointGetRednerTileLocation(Vector2coord){Vector2drawCoord=newVector2(tileSizeX*coord.x,tileSizeY*coord.y);returnnewPoint(drawCoord.x+GetWhiteSpace().x,drawCoord.y+GetWhiteSpace().y);}

となります。

マウス座標からタイル座標を取得

ビットマップ上のマウス座標x,yからタイル座標x,yを算出します。

1x8a9-qoncl.gif

結果のタイル座標$cx,cy$は

\begin{align}
ax = x+w\\
ay = y+h
\end{align}
cxのとりえる値の範囲は x \leqq cx \leqq ax にあるタイル\\&&\\
cyのとりえる値の範囲は y \leqq cy \leqq ay にあるタイル

であると言えます。
これを関数で表すと

publicstaticVector2GetTileCoordByMouseCoord(Vector2coord){for(intx=0;x<MAX_COORD_X;x++){for(inty=0;y<MAX_COORD_Y;y++){vartileLocation=GetRednerTileLocation(newVector2(x,y));vartileSize=GetTileSize();varxX=tileLocation.X;varXx=tileLocation.X+tileSize.Width;varyY=tileLocation.Y;varYy=tileLocation.Y+tileSize.Height;if(xX<=coord.x&&Xx>=coord.x)// x ≦ cx ≦ax{if(yY<=coord.y&&Yy>=coord.y)// y ≦ cy ≦ay{returnnewVector2(x,y);}}}}returnnewVector2(0,0);}

となります。
コッチの方が美しいですね

if(xX<=coord.x&&coord.x<=Xx)// x ≦ cx ≦ax{if(yY<=coord.y&&coord.y<=Yy)// y ≦ cy ≦ay{returnnewVector2(x,y);}}

アルゴリズム

スタートからゴールへの方角を算出

////////////////////////////////////////////////////////////////////////// 方角を示すEnum////////////////////////////////////////////////////////////////////////publicenumArrowVector{NULL,UP,DOWN,RIGHT,LEFT,UP_RIGHT,UP_LEFT,DOWN_RIGHT,DOWN_LEFT,}publicstaticArrowVectorCalculateVector(Vector2start,Vector2goal){if(start.x==goal.x){if(start.y>goal.y){returnArrowVector.UP;}else{returnArrowVector.DOWN;}}if(start.y==goal.y){if(start.x>goal.x){returnArrowVector.LEFT;}else{returnArrowVector.RIGHT;}}if(start.x>goal.x&&start.y>goal.y)returnArrowVector.UP_LEFT;if(start.x<goal.x&&start.y>goal.y)returnArrowVector.UP_RIGHT;if(start.x<goal.x&&start.y<goal.y)returnArrowVector.DOWN_RIGHT;if(start.x>goal.x&&start.y<goal.y)returnArrowVector.DOWN_LEFT;returnArrowVector.NULL;}

進む方向を決める

可読性を求めていたらいつの間にかハードコーディングしてました。

データ処理フロー:
Untitled Diagram-Page-2.png

////////////////////////////////////////////////////////////////////////// 4方向から進むに最適なタイルを算出////////////////////////////////////////////////////////////////////////privatestaticTileBlockGetBestTile(TileBlockorigin,intcost){if(origin.GetTileType()==TileType.GoalTile||origin.GetAnalyzed()){returnorigin;}vargoalCoord=GetTileBlockByTileType(TileType.GoalTile).GetCoordinate();varoriginCoord=origin.GetCoordinate();varup=GetTileBlock(newVector2(originCoord.x,originCoord.y-1));varbottom=GetTileBlock(newVector2(originCoord.x,originCoord.y+1));varright=GetTileBlock(newVector2(originCoord.x+1,originCoord.y));varleft=GetTileBlock(newVector2(originCoord.x-1,originCoord.y));//ふるいにかけるif(up!=null&&up.GetTileType()!=TileType.Walkable&&up.GetTileType()!=TileType.GoalTile)up=null;if(bottom!=null&&bottom.GetTileType()!=TileType.Walkable&&bottom.GetTileType()!=TileType.GoalTile)bottom=null;if(right!=null&&right.GetTileType()!=TileType.Walkable&&right.GetTileType()!=TileType.GoalTile)right=null;if(left!=null&&left.GetTileType()!=TileType.Walkable&&left.GetTileType()!=TileType.GoalTile)left=null;//どれかが前のoriginだったらやめるif(up!=null&&up.GetAnalyzed())up=null;if(bottom!=null&&bottom.GetAnalyzed())bottom=null;if(right!=null&&right.GetAnalyzed())right=null;if(left!=null&&left.GetAnalyzed())left=null;//どれかがゴールだったらそこまで線を描画if(up!=null&&up.GetTileType()==TileType.GoalTile)DrawLineCenterTileToTile(origin.GetCoordinate(),up.GetCoordinate());if(bottom!=null&&bottom.GetTileType()==TileType.GoalTile)DrawLineCenterTileToTile(origin.GetCoordinate(),bottom.GetCoordinate());if(right!=null&&right.GetTileType()==TileType.GoalTile)DrawLineCenterTileToTile(origin.GetCoordinate(),right.GetCoordinate());if(left!=null&&left.GetTileType()==TileType.GoalTile)DrawLineCenterTileToTile(origin.GetCoordinate(),left.GetCoordinate());varup_hcost=0;varbottom_hcost=0;varright_hcost=0;varleft_hcost=0;//推定コストを計算if(up!=null)up_hcost=CalculateHeuristic(up.GetCoordinate(),goalCoord);if(bottom!=null)bottom_hcost=CalculateHeuristic(bottom.GetCoordinate(),goalCoord);if(right!=null)right_hcost=CalculateHeuristic(right.GetCoordinate(),goalCoord);if(left!=null)left_hcost=CalculateHeuristic(left.GetCoordinate(),goalCoord);//データをセットif(up!=null)up.SetAnalyzeData(cost,up_hcost);if(bottom!=null)bottom.SetAnalyzeData(cost,bottom_hcost);if(right!=null)right.SetAnalyzeData(cost,right_hcost);if(left!=null)left.SetAnalyzeData(cost,left_hcost);varup_score=0;varbottom_score=0;varright_score=0;varleft_score=0;if(up!=null)up_score=up.GetScore();if(bottom!=null)bottom_score=bottom.GetScore();if(right!=null)right_score=right.GetScore();if(left!=null)left_score=left.GetScore();varscores=newint[4];scores[0]=up_score;scores[1]=bottom_score;scores[2]=right_score;scores[3]=left_score;varhcosts=newint[4];hcosts[0]=up_hcost;hcosts[1]=bottom_hcost;hcosts[2]=right_hcost;hcosts[3]=left_hcost;vartiles=newTileBlock[4];tiles[0]=up;tiles[1]=bottom;tiles[2]=right;tiles[3]=left;varmin_score=int.MaxValue;varmin_cost=int.MaxValue;varmin_hcost=int.MaxValue;varmin_tile=origin;//一番スコアの低いものを探すfor(intm=0;m<4;m++){if(scores[m]==0)continue;if(scores[m]>min_score)continue;if(scores[m]==min_score&&cost>=min_cost)continue;min_score=scores[m];min_cost=cost;min_tile=tiles[m];min_hcost=hcosts[m];}//自身をCloseif(origin!=null){origin.Close();}if(min_tile.GetTileType()==TileType.Walkable){origin.SetAnalyzed(true);DrawLineCenterTileToTile(origin.GetCoordinate(),min_tile.GetCoordinate());}returnmin_tile;}////////////////////////////////////////////////////////////////////////// マップを探索////////////////////////////////////////////////////////////////////////publicstaticasyncvoidAnalyzeMap(){LoggerForm.WriteSuccess("探索開始");//各タイル座標に属性を付与SetTileAttributesToAll();varstartTile=GetTileBlockByTileType(TileType.StartTile);vargoalTile=GetTileBlockByTileType(TileType.GoalTile);if(startTile==null||goalTile==null){if(startTile==null){LoggerForm.WriteError("Start tile not found.");}elseif(goalTile==null){LoggerForm.WriteError("Goal tile not found.");}return;}varstartTileCoord=startTile.GetCoordinate();vargoalTileCoord=goalTile.GetCoordinate();vark=0;TileBlockbestTile=startTile;while(k<999){k++;if(bestTile==null||!IsTileBlockExists(bestTile.GetCoordinate())){LoggerForm.WriteError("Tile not found.");break;}elseif(bestTile.GetTileType()==TileType.GoalTile){LoggerForm.WriteSuccess("Goal found.");break;}else{bestTile=GetBestTile(bestTile,k);}awaitTask.Delay(10);}}

アルゴリズムについて

gceun-5mza3.gif
ご覧の通り、最短ルートは保証されてません。アルゴリズム(笑)です。
なぜこうなっているかというと、
7d8506b45d4dda150f4e932980f98440.png
進んでいる彼は左右上下1ブロックとスコアしか見えてません。
したがって、より良いスコアである右に進んでしまうのです。
しかしながら、全体で見れば左に進むのが明らかに最短ルートです。
こういった場合、左に進んでゴールに辿り着いた場合と
右に進んでゴールに辿り着いた場合の総タイル数を比較してルートを定める必要があります。
また、直線移動しかしません。行き止まりにも対応してません。

「最短コースは保証されていないが、最低限の分析でゴールに辿り着ける」
という側面で見れば、ダイクストラ法とAスター法のいいとこ取りだと思います。(言い訳)
また改めて時間があれば、最適化します。

といった感じで、中途半端ではありますが経路探査アルゴリズムを作ってみました。
ソースコードはGithubにあります。


Viewing all articles
Browse latest Browse all 9309