[펌] A* 알고리즘 (A Star Algorithm )

|

출처 :  http://blog.naver.com/PostView.nhn?blogId=hermet&logNo=95405999



대학교 2학년, 홀로 어두운 방 안에서 만두로 끼니를 때우며 프로그래밍을 하던 시절...

당시 나도 울티마나 한번 따라 만들어본다고 했던 게임이 불현듯 떠올라, 소스를 훑어보다가 A* 알고리즘을 다시금 정리하게 됐습니다.

(뭥미... -_- )

 

A* 는 길찾기 알고리즘이며 적절한 비용을 요구한다는 점에서 유용하게 사용됩니다.

넓이 우선과 깊이 우선 탐색을 조합하고 목적지까지의 거리를 고려하여 최적의 비용을 찾아 가는 휴리스틱(Heuristic)한 기법을 이용하죠.

 

적절한 설명을 위해서 당시 게임을 참고 하겠습니다.

 



 

 자, 왼편이 실제 게임 화면이라고 가정했을 때, 오른편은 맵의 내부 구조를 시각화 한 것이라고 보시면 됩니다. 위 게임의 2D 맵은 격자들로 구성하여 지형 데이터를 관리하죠. (격자는 실제보다 크게 그렸습니다)  여기서 색깔이 칠해진 격자는 블록된 구역이라고 보시면 됩니다. 즉 장애물 등으로 캐릭터가 갈 수 없는 곳이죠. 응? 저 벽은 지나갈 수 없는 것 아닌가? 라고 생각하실 수도 있는데 원근감을 생각하시면 캐릭터는 벽 뒤쪽으로 지나갈 수가 있기 때문에 블록된 곳이 아닙니다.

 

자, 어쨌든... 여기서 임의의 위치로 캐릭터가 이동한다고 가정하고 A* 알고리즘이 어떤 순서로 길을 탐색하는지 보도록 합시다. (모든건 수작업이라는 사실... ㅜㅜ)



 


  

노란색 그리드가 목적지라고 가정했을 때, 캐릭터 위치로부터 이동 가능한 그리드를 탐색합니다. 즉, 넓이 우선 탐색을 하는 거죠. 그리고 이동 가능한 8방향에 대한 그리드 검색을 마친 후, 목적지에 가장 가까운 노드를 선택하게 됩니다. 여기서는 녹색화살표.

 

여기서 데이터는 다음과 같이 관리를 하게 됩니다. Open Node와 Close Node라고 하는 두 개의 링크드 리스트를 구축하게 되는데, Open Node는 앞으로 탐색을 시도할 여지가 있는 노드들의 후보 리스트로 볼 수 있습니다. 즉 한 노드는 8 방향에 대해서 탐색 여부를 시도해야 하는데, 이것을 아직 수행하지 않은 노드가 OpenNode 목록에 추가되는 것이고,  CloseNode는 현재 검색한 노드들 중 더 이상 탐색을 시도할 여지가 없는 노드 리스트 볼 수 있습니다.  그리고 OpenNode에 목록이 추가될 땐, 항상 목적지까지의 거리의 비용이 적은 순으로 정렬 상태를 유지하죠.

 

따라서 현재, OpenNode 리스트는 녹색을 비롯한 노란색 화살표의 노드들이 연결되어 있고  CloseNode 리스트는 출발점 노드 하나가 됩니다.  




 

 

다음 탐색을 시도합니다. 이번에는 이동 경로의 후보로 선택된 녹색 화살표를 중심으로 8방향을 탐색하게 됩니다. (즉, A*는 가장 가능성이 있는 노드들을 우선적으로 따라가며 탐색을 하므로 깊이 우선 탐색을 하게 되는 것이죠.)

 

OpenNode에는 새로 탐색한 노란색 화살표의 노드가 새로 추가되었으며

CloseNode에는 출발점과 옅은 녹색 화살표의 노드로 구성이 됩니다.

 

참고)

 노란색 화살표 - OpenNode에 새로 추가된 노드

 연한 노란색 화살표 - OpenNode에 기존에 추가된 노드

 녹색 화살표 - CloseNode에 새로 추가될 노드

 연한 녹색 화살표 - CloseNode에 기존에 추가된 노드



 

 


 

하지만  막다른 길이어서 다시 OpenNode의 후보로부터 탐색을 하게 됩니다. 하지만, 갈 수 있는 길은 바로 아래 한 칸 밖에 없겠지요.




 


 

자, 결국 다음 탐색 역시 모두 블럭되어 있거나 이미 탐색된 지역이기 때문에 OpenNode 리스트의 후보로 탐색을 다시 시도하게 됩니다. 결국 위와 같이 되는 것이지요.




 


 

그리곤 다음 탐색 후보는 OpenNode 중에서 목적지와 가장 가까운 노드가 되므로 결국 출발점에서 바로 오른쪽 위치에 대해 탐색 시도를 하게 됩니다. 물론 그 노드는 이미 여덟 방향에 대해 탐색이 되었기 때문에 결국, 바로 CloseNode로 들어가게 되겠죠. 그리고 그 다음 후보는 바로 시작점의 바로 위 노드가 되며 역시 같은 이유로 CloseNode로 들어가며, 그 다음 후보는 맨 상단의 노드가 될 겁니다. (아래 그림)


 


 

물론 맨 상단 노드를 확장했을 땐, 새로 탐색한 노드들은 기존 OpenNode 후보보다 모두 멀기 때문에 다음 후보는 시작점 바로 오른쪽 하단 노드가 되는 것이지요.

 

결국 이런식으로 확장을 하다 보면, 최종적으로 목적지에 도착하게 될 겁니다.




 



 


 




 


 

 

그리고 이 흰색 화살표의 노드들을 역으로 추적하면 결국 최단거리가 나오는 것이지요. 물론 흰색 노드는 ClosedNode에 포함된 노드들이며 최종적으로 목적지에 도달했을 때의 노드를 역으로 추적하면 되는 겁니다.

 

어때요.. 참 쉽죠잉?  (아 힘들어.. 이거야 말로 충격과 공포 ㅡㅜ)

 

자, 이론은 이해하셨을 거라고 믿고... 이제 구현 한번 들어가 보도록 하겠습니다.

//노드에 대한 정의

typedef struct node_s {
        int degree;                        
  //현재 이 노드의 깊이 값. 즉, 자료 구조 트리에서 깊이 값.

        int distance;                        //이 노드로부터 목적지까지의 거리

        int value_factor;                   //평가치 값( degree + distance )

        int x, y;                           //이 노드의 위치 (그리드의 위치) 
        struct node_s* direct[8];       //확장 가능한 8방향에 대한 노드
        struct noder_s* prev_node;    //링크드 리스트 이전 노드 
        struct node_s* next_node;     //링크드 리스트 다음 노드 
} node_t;

 

//노드를 재정렬할 때 쓸 스택.

typedef struct node_stack_s {
       node_t*  node;                            //이 스택 위치에서 가리키고 있는 노드 
       struct node_stack_s*  next_node;    //이 스택의 다음 위치  
} node_stack_t;

 

참고로 평가치 값은 degree + distance로 구하는데 거리와 degree의 값의 단위는 해당 프로그램에 맞게 적절하게 정하시면 될 것 같습니다. (degree간의 값의 차이가 1이라고 본다면 인접한 노드들 간의 거리도 1로 지정하는게 적절할 것 같습니다.)

 

그리고,  OPEN_NODE와 CLOSE_NODE 그리고 스택을 위한 전역 데이터를 각각 선언합니다.

node_t *OPEN = NULL,  *CLOSED = NULL;

node_stack_t* STACK = NULL;

 

다음 함수들은 구현하기에 어렵지도 않고 중요하지도 않으니 간단한 주석 설명으로 대처 하였습니다.

//리스트와 스택을 초기화  또는 제거 해주는 함수

void init_astar()

{

    //OPEN노드를 순회하면서 각 노드를 제거  

    //CLOSE노드를 순회하면서 각 노드를 제거

    //스택을 순회하면서 각 스택에 존재하는 데이터 제거, 스택 공간 제거      

}

 

//해당 그리드로 이동 가능한지 판단하는 함수

bool is_available_grid( int x, int y )

{

      //캐릭터가 어떤 그리드로 이동하기 전에 그 그리드가 이동가능한지 여부를 판단하는 부분을 구현합니다.

      //상황에 따라 다르므로 역시 의사코드.   

      //이동 가능한 그리드면 TRUE

      //블럭된 것과 같이 이동 불가능한 그리드면 FALSE     

}

 

//해당 위치의 노드가 OPEN NODE에 존재하는 지 파악하는 함수

node_t* is_open( int x, int y )

{

       //OpenNode를 순회하면서 입력한 위치 값의 노드가 존재하는 지 파악.

       //존재하면 해당 노드를 반환

}

 

//해당 위치의 노드가 CLOSED NODE에 존재하는 지 파악하는 함수

node_t* is_closed( int x, int y )

{

       //ClosedNode를 순회하면서 입력한 위치 값의 노드가 존재하는지 파악.

       //존재하면 해당 노드를 반환

}

 

//해당 노드를 스택 공간에 삽입하는 함수

void push_into_stack( node_t* node )

{

       //스택 공간 할당

       //생성한 스택 공간에 삽입할 노드를 지정

       //기존 스택에 생성한 스택 공간을 연결 

}

 

//해당 노드를 스택 공간에 삽입하는 함수

node_t* pop_from_stack()

{

       //스택 공간에서 노드 데이터 꺼내옴

       //해당 스택 공간 삭제

       //노드 반환

}

 

물론 위의 의사코드 부분은 첨부파일에 완성되어 있으니 참고하시기 바랍니다.

 

자, 이제부터 좀 중요한 부분들을 보도록 하죠. 먼저 길찾기 시작 함수입니다.

 

//출발점 x,y, 목적지 x,y

node_t* find_path( int start_x, int start_y, int dest_x, int dest_y ) {

 

          node_t* present=NULL;          //출발점 노드

 

          //시작 위치 노드 추가. 여기서는 시작 위치가 목적지가 되겠습니다. 즉, 목적지에서 -> 출발지 방향으로 탐색. 

          //위의 그림설명에서 완성된 노드가 역으로 연결된 점을 확인하셨다면, 목적지->출발지로 하면 되겠죠.
          present = ( node_t* ) calloc( 1, sizeof(node_t) );        
          present->degree = 0; 
          present->distance=  pow( (dest_x- start_x ), 2 ) + pow( (dest_y - start_y ), 2 );
   
//진정한 의미에서는 루트를 씌워야

                                                                                                                           //겠지만 구지 안씌워도 문제될 건 없죠.
          present->value_factor = present->distance;        // distance + degree
        
  present->x= dest_x;                                          
          present->y= dest_y;

 

          OPEN=present;   

 

          node_t* best=NULL;              //best는 현재까지 최적의 경로에 대한 리스트입니다. 
          int count=0;                          //탐색 깊이 값을 카운팅 합니다.  무한 루프에 빠질 가능성을 제한할 용도이죠.

         

          //탐색 시작합니다. 길 찾기를 실패할 경우에 대비해서 루프문에 제한을 둘 필요가 있습니다.

          while(count< FINDPATH_LIMIT) {

 

                    //모든 노드에 대한 찾기를 수행했을 경우. 그럴 경우 아마 최적의 길을 찾았겠죠? 아마..

                    if(OPEN==NULL) {
                             return best;

                     }

 

                    best = OPEN;     //매 루프문을 돌면서 OpenNode의 후보로부터 탐색을 우선 시도합니다. 
                    OPEN = best->next_node;     //그리고 그 다음 노드가 OpenNode의 최상위 후보노드로 지정되며 
                    best->next_node = CLOSED;  //지금까지 구축된 최적의 노드를 이번에 탐색할 best노드와 연결함으로써

                                                              //현재까지 탐색된  최적의 길을 유지하게 됩니다. 
                    CLOSED=best;   //이 best노드는 이번 루프에서 탐색 시도되므로 close노드로 들어가게 되는거죠.        

 

                   //길찾기 실패

                    if(best==NULL) {
                            return NULL;   

                    }

          

                    //길찾기 성공 
                
   if(best->x == start_x && best->y == start_y )  
                             return best;

 

                    //현재 탐색 노드를 중심으로 8방향으로 확장. 
         
           if(make_child(best, start_x, start_y )==0 && count ==0) 
                            return NULL;

 

                    count++;             

            }          

            return best;

}

 

자 다음으로, 현재 탐색 노드를 확장하는 함수를 보도록 합시다.

//현재 탐색할 노드, 목적지 x, y,    반환값은 현 노드의 확장 여부

bool make_child(  node_t* node, int dest_x, int dest_y ) {

         
         bool extended = false;                 //하나라도 확장된 사실이 있으면 true
         int x = node->x;
         int y = node->y;

 

         //각 8방향에 대해  이동 가능한 지역에 대해서 자식 노드 생성 

         //왼편.
         
if( is_available_grid( x - 1, y ) ) {
                extend_child_node( node, x-1, y, dest_x, dest_y, LEFT_IDX );
                extended=1;
          }

 

         //그 외 7방향에 대해서는 해당 x, y값을 주어서 똑같이 호출하면 되겠습니다.

         //여기서는 생략.

         //오른편 x + 1, y

         //상단 x, y - 1

         //하단 x, y + 1

         //좌측 상단 x - 1, y - 1

         //우측 상단 x + 1, y - 1

         //좌측 하단 x - 1, y + 1

         //우측 하단 x + 1, y + 1

 

         return extened;

}

 

여기까지는 이해가 되셨길 합니다. 사실 위의 예에서 보여준 게임같은 경우, 대각선 이동은 인접한 두 그리드가 이동 가능할 때 허용되도록 컨셉을 잡는게 더 현실적일 겁니다. 따라서 대각선 판단은 인접한 두 노드가 이동 가능할 때 판단하면 될 듯 합니다. 그건 직접 수정 가능하시겠죠? :)

 

-인접한 두 노드의 블럭 상태로 대각선 이동 안됨-

 

 

자, 다음 함수를 보도록 합시다. 이번 함수는 노드를 확장하고 필요한 경우 리스트를 재정렬합니다.

 

//확장할 노드, 현재 노드 위치, 목적지 위치, 확장할 방향 인덱스

void extend_child_node( node_t* node, int cur_x, int cur_y, int dest_x, int dest_y, int cur_direct ) {

 

           node_t *old = NULL, *child = NULL;
           int i;
           int degree= node->degree+1;

 

            //확장할 노드가 OpenNode에 존재한다면, 
            if( old = Is_open( cur_x, cur_y ) ) {

 

                      node->direct[ cur_direct ] = old;
 

                      //그리고 노드 재연결 및 설정 . 참고로 노드 방향이 반대로 적용된다는 걸 고려할 것. 
                     if( degree < old->degree ) {
                             old->prev_node = node;
                             old->degree = degree;
                             old->value_factor = old->distance + old->degree;
                     }

 

            //확장할 노드가 CloseNode에 존재한다면,

            }else if( old = is_close( cur_x, cur_y ) ) {
  

                    node->direct[ cur_direct ] = old;
  

                    //어떤 경우에는 기존 CloseNode에 있는 노드의 degree가 더 적을 경우가 발생할 수도 있는데

                   //이 때 순서를 다시 설정             
                    if(degree<old->degree) {
                            old->prev_node = node;
                            old->degree = degree;
                            old->value_factor = old->distance + old->degree;
                            make_sort( old );
                    }

 

            //확장할 노드 생성 및 OpenNode 리스트에 추가

            }else {

                    if( ( child = ( node_t* ) calloc( 1, sizeof( node_t ) ) ) == NULL ) 
                                return;    //lack of memory

 

                    child->prev_node = node;
                    child->degree = degree;
                    child->distance =  ( cur_x - dest_x ) * ( cur_x - dest_x ) + ( cur_y - dest_y ) * ( cur_y - dest_y );
                    child->value_factor = child->distance + child->degree;
                    child->x = cur_x;
                    child->y = cur_y;

                    

                    Insert_node_( child );

 

                    node->direct[ cur_direct ] = child;
        

            }

}

 

이제 좀 머리가 아파오실 수도 있습니다. 본 내용과 소스만으로 이해가 되지 않는다면 직접 가상의 길을 그린 후에 이 알고리즘을 따라 직접 확장해 보시는걸 추천드립니다. 그러면 확실해지겠죠.

 

//재평가힐 노드의 기준

void make_sort( node_t* old ) {

 

            node_t *direct, *previous;
            int i;
            int degree = old->degree+1;

 

            for(i=0; i<8; i++) {

 

                     if( ( direct = old->direct[i] ) == NULL ) 
                              continue;

 

                    //순서 재정렬로 인한 연결된 노드들의 설정값이 얽힌경우 재설정을 해줍니다.

                    //자식 노들들을 위해 스택 공간을 이용합니다.

                    if( direct->degree > degree ) {
                                 direct->prev_node = old;
                                 direct->degree  = degree;
                                 direct->value_factor  = direct->distance + direct->degree;
                                  
                                  push_into_stack( direct );
                    }

           }

 

          //스택 공간을 이용하여 차례차레 노드들을 재 설정.

          while( STACK ) {

                    previous = pop_from_stack();

 

                    for( i=0; i<8; i++ ) {

 

                           if( ( direct = previous->direct[i]) == NULL ) 
                                     break; 

 

                           if( direct->degree > previous->degree + 1 ) {
                                   direct->prev_node = previous;
                                   direct->degree  = previous->degree + 1;
                                   direct->value_factor  = direct->distance + direct->degree;
    
                                   push_into_stack( direct );
                          }
                    }
         }

}

 

주석 설명이 다소 부진할 수 있을지도 모르겠습니다. 예제의 경우에 나타나지 않았던  예외적인 상황에 대한 처리부분이지요. 이 부분은 CloseNode의 순서가 바뀌면서 그에 따른 연결된 노드들을 재평가합니다.

 

 

마지막으로 노드를 OpenNode에 추가합니다.  주석 내용이 번잡하게 들릴 수도 있는데 결국은, 노드 삽입 시 목적지에 가까운 순서를 유지 한다는 점.

 

 //확장한 새 노드

 void Insert_node( node_t* present ) {
 
        node_t* old = NULL, *temp = NULL;

  

        if(OPEN == NULL) {
                OPEN = present;
                return;
        }

 

        temp=OPEN;

    

        //OpenNode에 있는 노드가 현재 자식 노드보다 평가치가 낮으면
        //OpenNode를 추적해서 현재 자식 노드보다 평가치가 높은 노드를 찾는다. 
        while(temp && (temp->value_factor < present->value_factor ) ) {
              old=temp;  
              temp=temp->next_node;
        }

 
         //낮은 평가치의 OpenNode 중 출발지에 가장 가까운 노드 -> 추가할 자식 노드 -> 높은 평가치의 OpenNode 중 출발지에 가

        //장 먼 노드
        if(old) {
                  present->next_node = temp;
                  old->next_node = present;
        }else {
                  present->next_node = temp;
                  OPEN = present;
        }

}

 

제 역량 부족으로 설명이 힘들군요. 이중 연결 리스트 2개와 스택이 서로 얽혀있어서 말로는 다소 이해하기 힘들 수도 있구요.

 

예제 소스를 활용하여 진행 노드 상황을 직접 표현하여 이해해보는 것이 명확할 듯 합니다. ㅎㅎ (이런 무책임한...)

 

 

자, 어쨌든 A*에 대한 설명은 여기서 마치렵니다. 사실 최적화 방안은 여러 존재합니다. 예를 들면 직선 이동 노드들에 대해선 생략을 한다던지, 맵에 대한 WayPoint 정보를 추가로 삽입하여 이를 토대로 길찾기를 수행한다든지.. 적어도 위 게임에서 문에 대한 위치 정보를 참고할 수만 있어도 매우 최적화된 탐색이 수행되겠지요.

 

또한 3D 공간에 대해서는 다소 데이터 자체가 매우 상이할 겁니다. (추가적인 정보는 Game Programming Gems 1권을 추천합니다.)

 

부족한 설명은 첨부 소스를 참고하여 보충하시기 바랍니다.

 

 


Trackback 0 And Comment 0

길찾기 알고리즘 소스들

|

https://github.com/qiao/PathFinding.js

Trackback 0 And Comment 0

[펌] 초보자를 위한 A* 알고리즘 (기초개념 설명 및 소스)

|

출처 : http://cozycoz.egloos.com/viewer/9748811



여러분이 A*알고리즘을 쉽게 이해할때초보자들에게는 복잡하게 느껴질수도 있습니다

 

 A*알고리즘을 해석한 글이 웹상에 많이 존재하지만 그것과 달리 이글은 진정한 초보자들을 위한 것입니다.

이 글은 A* 길찾기 알고리즘에 대해 완벽히 보여주도록 노력하지는 않습니다
그보다 앞으로 여러분이 좀 더 구체적인 글을 읽고 이해하는데 필요한 A*길찾기 알고리즘의 원리와 기본적인 것들을 설명하고 있습니다
 

마지막으로, 나는 이 논설의 마지막 부분에 C++ Blitz Basic 두가지 버전의 샘플프로그램 패키지의 링크를 넣어 놓았지만 
여러분은 어떤 컴퓨터 언어로든 A*알고리즘을 적용하는 것이 가능할 것입니다.

샘플프로그램에는 여러분이 A*가 동작하는 것을 볼수있도록 실행파일도 포함시켜 놓았습니다.

 

시작합시다


 


 

도입범위 찾기(The Search Area)


누군가가 A지점에서 B지점으로 가기를 원한다고 가정합시다
그리고 아래그림처럼 벽이 이 두 지점을 분리시켜놓고 있습니다.
 

녹색은 출발지점A, 빨간색은 도착지점B, 그리고 파란색으로 채워진 사각형은 양쪽사이에 있는 벽입니다.

 

 

먼저 알수 있는 것은 검색지역이 사각형 격자들(Grid)로 나누어져 있다는 것인데우리는 이 단순화된 검색지역 사용합니다우리가 첫 단계로 할 것은 길 찾기입니다.

 격자들로 지역을 단순화시킨  이런 방법은 우리의 검색지역을 단순한 2차원배열로 만들어줍니다.

각각의 아이템(시작지점,도착지점,,길 등등)은 사각형 격자위에 하나의 2차원배열로 묘사되는데갈수 있는곳갈수없는곳‘ 이런식으로 상태가 기록됩니다.

 우리가 A에서 B로 갈 수 있는 길은 어떠한 사각형들의 모양으로 나타나게 되는데사각형의 중앙에서 다음 사각형의 중앙으로의 이동을 목표에 도착할때까지 반복하면 길이 찾아지는 거죠.

이 중심점들을 노드 라고 부릅니다.

(검색지역을 사각형 뿐만 아니라 육각형직사각형등의 다른 모양으로 할 수 도 있습니다만 우리는 사각형 방식을 사용할 것입니다가장 단순하므로..) 

 

다음의 순서로 길 찾기를 합니다.

 

1. 지점A로부터 검색 된 사각형을 열린목록(open list)에 추가합니다열린목록은 쇼핑목록과 비슷한데지금은 목록상에 아이템이 하나지만 나중에 좀더 갖게 될 것입니다.

 

2. 시작점 근처에 붙어있고 지나갈 수 있는 모든 사각형들을 봅시다. (물 또는 다른 잘못된 지역들은 무시합니다.) 그리고 그 지나갈 수 있는 사각형들을 열린목록에 추가합니다추가된 각각의 사각형들에게 지점A를 부모사각형이라고 저장합니다 ( ‘부모사각형은 우리가 길을 거슬러 올라갈 때 중요합니다. 나중에 이것에 대해 확실하게 알게 될 것입니다.)

 

3. 시작점A(녹색사각형)를 열린목록에서 빼고다시는 볼 필요가 없는 사각형들의 목록인 닫힌목록(closed list)에 추가합니다.

 

여기서여러분은 다음과 같은 그림을 얻을수 있습니다이 그림에서 중심에 있는 짙은 녹색 사각형은 출발사각형(시작점A)입니다이것은 하늘색 외곽선으로 되어있고닫힌목록에 추가되었음을 의미합니다그리고 인접한 모든 사각형들은 검색된 후 열린목록에 들어갑니다.

(이것들은 녹색 외곽선으로 되어있고 부모사각형인 시작점을 가리키고 있는 회색의 포인터를 가지고 있습니다)


 

다음으로우리는 열린목록에 있는 인접한 사각형중에 하나를 선택하고 앞에서 했던 처리를 아래에 설명된 방법으로 반복하게 됩니다.  좀더 많이 할 수도 있고 덜 할 수도 있겠죠.

그러면 어떤 사각형을 선택해야 할까요바로 가장 작은 F비용을 가진 것을 선택하는 것입니다.

(F비용에 관해서는 바로 밑에 나옵니다


 

길 기록(Path Scoring)

 

다음 방정식으로 사각형을 선택합니다.

F = G + H


F = 비용

G = 시작 A(녹색지점)로부터 새로운 사각형까지의 이동비용입니다길을 찾아갈 수록 G의 값은 커지겠죠.(A로부터 멀어지므로..)

H = 얻어진 사각형으로부터 최종목적지점 B(빨간지점)까지의 예상이동비용입니다.

( 값은 모든 장애물에 대해서는 고려하지 않고 현재 사각형에서 목적지점까지의 거기를 계산하는데 이때 대각선이동이 아닌 가로세로이동에 대한 비용으로 계산하는 것입니다장애물을 고려하지 않았으므로 이 비용이 최종이동비용과 같지 않을 확률이 크겠죠.)

 

우리가 찾고자 하는 길은 열린목록과 F비용이 작은 사각형을 선택하는것의 반복을 통해서 얻어집니다.

 

위에 설명된 것처럼, G는 출발점으로부터 하나씩 얻게 되는 사각형으로 이동하는데 드는 이동비용이죠이 예제에서 우리는 세로 또는 가로로 이동하는데 각각 10의 비용대각선이동에 대해서는 14의 비용을 할당 할 것입니다.

(정사각형이고 가로세로가 10이면 대각선의 길이는 이등변삼각형 길이비 루트2:1:1에 의해 루트2 *10 : 10 : 10이 되므로(루트2는 약1.414)  약 14의 값이 나옵니다)

컴퓨터에서 숫자를 사용하는 것은 그만큼 빠르기 때문인데 단순화 시킨 숫자를 사용하지 않으면 길찾기가 매우 느려진다는 것을 알게 될 것입니다.

 

사각형의 G비용을 알아내는 방법은 그 부모로부터 G비용을 얻어와서 부모로부터 직각으로 움직였냐 대각선으로 움직였냐에 따라서 10또는 14를 추가해 주면 되는 것이죠.

 

H비용은 그 사각형 위치의 변화에 따라 예측될 수 있습니다.

(우리가 사용하는 이 방법을 맨하탄방식(Manhattan method)이라고 부릅니다.)

 여러분은 현재사각형에서 목표사각형에 도달하기 까지의 대각선이동을 제외한 가로,세로로 이동한 총숫자를 계산할수 있고 거기에 10을 곱합니다이것이 바로 맨하탄방식인데 도시의 한쪽에서 다른 한쪽까지의 블록을 계산하는것과 같은 방식이기 때문에 명명 됐습니다여기서 여러분은 대각선으로 블록을 가로지를 수는 없겠죠위에서 말씀드렸듯이, H비용을 계산할 때 우리는 어떤 방해물도 무시합니다이 방식은 발견적방식이라고도 불리는데좀더 알기를 원하는 분은 여기(http://www.policyalmanac.org/games/heuristics.htm)에서 참고할 수 있을 것입니다.

 

G H를 더하므로써 F 가 계산됩니다.우리가 진행한 검색의 첫단계 결과를 아래 그림에서 볼수있습니다
F,G,H 
비용이 각각의 사각형에 표시되어 있는데시작점 우측에 있는 사각형안에 표시된것처럼, F는 좌상단에, G는 좌하단에그리고 H는 우하단에 표시됩니다.

 

 

녹색사각의 우측 사각형을 보면, F = G+H (40 = 10+30) 이죠이사각형은 부모사각형이 시작점이 됩니다. (그렇죠바로 아랫단계로 물려받았으니까요한번씩 사각형들이 퍼질때마다 자식들이 탄생한다고 보시면 되겠죠.)

G는 시작점과 새로운 사각형의 거리라고 했으니 10이죠. (한변은 10)

H는 장애물을 무시하고 가로세로 이동으로 목적지(빨간사각형)까지의 이동비용이므로 가로 3*10

이므로 30이죠. (G값이 14인 사각형들은 대각선 이동을 했기 때문인거 아시죠?)

이제 G, H값을 토대로 F값을 구하는 것이죠.


 

찾기 계속하기(Continuing the Search)

 

계속해서 검색을 해 나가기위해 우리는 단순히 열린목록에 있는 사각형들 중에서 가장 작은 F비용을 가지고 있는 사각형을 선택하고 그 선택된 사각형으로 다음의 과정을 진행하면 됩니다.

 

4. 선택된 사각형을 열린목록에서 빼고 닫힌목록에 추가합니다.

(부모가 되어 자식노드를 생성할 준비가 된거죠.)

 

5. 인접한 모든 사각형을 검색해서 닫힌목록에 있거나 갈수없는것(,,그밖에 장애물)들을 제외한 나머지 중에서 열린목록에 없는 사각형을 열린목록에 추가합니다.

 선택되었던 사각형을 열린목록에 추가된 새로운 사각형들의 부모로 만듭니다.

(선택되었던 사각형이란 4번에서 단힌목록에 추가된 그 사각형을 말합니다.

 새로운 사각형이란 5번에서 열린목록에 새롭게 추가된 사각형을 말하구요.)

 

6. 만약 인접한 사각형중에 이미 열린목록에 있는 사각형이 있다면 그 사각형으로 가는길이 더 좋은 길인지 확인하세요다시 말하면현재 선택된 사각형보다 G비용이 더 작은지를 검사하라는 것입니다만약 아니라면 아무것도 할 필요가 없지만 G비용이 더 작다면 선택되었던 사각형의 인접사각형들의 부모를 새로운 사각형으로 바꾸세요

(위쪽 그림에서 보면선택된 사각형으로 향하는 포인터들의 방향을 바꾸는 것을 말합니다.) 
마지막으로그 사각형의 F G를 다시 계산합니다.


이제 이 작업들을 직접 해봅시다.

최초에 9개의 사각형중에 우리는 시작사각형을 닫힌목록에 넣은후 열린목록에 8개의 사각형을 가지고 있었습니다이들중 F비용이 가장작은 것은 시작사각형에 오른쪽에 있는 사각형 입니다이 사각형은 다음 그림에서 하늘색 외곽선으로 표시되어 있습니다.

 

 

제일 먼저 , 이 사각형을 열린목록에서 빼고 닫힌목록에 넣습니다그리고 우리는 인접한 사각형들을 검색하게 되는데그 오른쪽에 있는 사각형은 벽이죠그래서 무시하고왼쪽에 있는 것은 시작점이고 이것은 닫힌목록에 있으므로 이것역시 무시합니다.

 

장애물 4개와 시작점을 제외하니 4개의 사각형이 남는데 이 사각형들은 이미 열린목록에 있습니다그래서 우리는 그것들을 이용한 길 중에 현재 이 사각형을 이용하는 것 보다 좋은 것이 있는지 G비용을 검색합니다녹색사각형의 우상단의 사각형을 봅시다이것의 G비용은 14인데만약 우리가 현재 선택된 사각형녹색점의 우측사각형)을 거쳐서 그곳으로 이동한다고 하면 G비용은 20일 것입니다(현재사각형이 G비용이 10이고 이것에서 수직으로 위쪽으로 한번 이동하였으므로 10을 더합니다.). G비용 20 14보다 크므로 좋은 길이 아니죠.

가로세로 움직이는 것 보다 대각선으로 한번 움직이는게 비용이 적게 들죠 ( 20 : 14)

 

여러분은 현재 선택된 사각형으로는 더 이상 최소비용의 길을 찾을수 없다는 걸 아실것입니다그래서 이제 인접한 사각형들에 주목합시다.

 

현재 열린목록에 있는 사각형은 7개입니다. (위 그림에서 녹색 외곽선인 사각형그중에 가장 작은 F비용을 가지고 있는 것을 하나 고르세요흥미롭게도 이 경우엔 두개의 사각형이 54 F비용을 가지고 있죠어떤 것을 사용할지는 중요한 문제가 아닙니다속도상의 목적으로는 둘중 열린목록에 더 늦게 추가된 것을 선택하는 것이 빠르겠죠

(각각 다른 두개의 A* 버전을 만들더라도 길이가 같은 각각 두개의 길을 찾게 될 것이기 때문에 F점수가 둘 다 같더라도 서로 다르게 처리해도 상관없습니다.)

 

그래서우리는 그냥 아래쪽에 있는 것을 선택합니다.


 

새로운 사각형이 선택되었으니 다시 인접한 사각형들을 검색 해야겠죠.여기서 우리는 바로 오른쪽에 사각형이 벽이라는 것을 발견하게 됩니다. (무시그 바로위도 똑같이 벽이기 때문에 이것도 역시 무시합니다그리고 벽 바로 아래 사각형(마지막 장애물의 아래 사각형)도 무시해야 합니다왜그럴까요사각형 자체가 움직인다고 생각하세요현재 선택된 사각형이 오른쪽 아래로 대각선 이동한다면 마지막 장애물의 대각선절반을 잘라야 이동이 가능하겠죠그러므로 마지막장애물의 아래사각형도 현재 검색에서 제외시키는 것입니다.

여러분은 일단 밑으로 내려가서 그다음 그사각형을 거쳐서 지나가야 합니다.

(이건 말그대로 어떻게 정의하느냐에 따라 틀린건데개발자 마음이겠죠여기선 그렇게 적용하도록 합니다)

 

이렇게 5개의 사각형이 검색대상에서 빠집니다현재사각형 아래에 있는 2개의 사각형은 열린목록에 없는것이므로 추가시킵니다그리고 현재 사각형은 이것들의 부모가 되겠죠?

그리고 바로 왼쪽에 있는 마지막 사각형은 현재사각형을 통해서 가는것보다 G비용이 적은지 검사를 해보면 아니라는 걸 알 수 있습니다.

 

우리는 목표사각형을 열린목록에 추가 할때까지 이 처리를 계속 반복합니다계속된 검색의 결과는 아래그림입니다.


 

시작점 밑으로 2번째 사각형의 부모사각형이 이전 그림과 변경되었다는 것을 주목하세요.

전에 이것은 G비용이 28이었고 오른쪽위에 사각형을 가리키고 있었습니다지금은 G비용은 20이고 위쪽 사각형을 가리키고 있는데이 일은 우리가 길찾기를 처리해 나가면서 어디선가에서 일어난 것입니다어딘가에서 G비용을 검사했고 여기서 더 비용이 적게드는 길을 찾아냈습니다그러므로써 부모가 바뀌것이고 G, F비용이 다시 계산되어진것이죠.

이런 식으로 목표지점으로 가는 가장 좋은 길을 찾아 검색을 하면서 바뀔 수 있는 사각형의 비용을 바꿔가면서 길을 찾을 것입니다.

 

최종적인 진짜 길은 빨간지점(목표지점)에서 부모사각형을 찾아가며 거꾸로 되짚어가면서 녹색지점(시작지점)까지 가면 됩니다이것이 가장 좋은 길입니다.

이것은 다음의 그림에서 볼수있습니다.

시작지점 A에서 목표지점 B까지 이동하는 것은 단순히 길을 따라있는 각각의 사각형의 중심(노드)에서 다음사각형의 중심으로 이동하면 되는 것입니다.

 


 

A* 방식의 요약(Summary of the A* Method)

 

마지막으로 위에서 설명한 A* 방식을 여기에서 정리해봅시다.

 

1. 시작사각형에서 검색된 인접사각형들을 열린목록에 넣습니다.

2. 다음의 과정들을 반복합니다.

   a) 열린목록에서 가장 낮은 F 비용을 찾아 현재사각형으로 선택합니다.

   b) 이것을 열린목록에서 꺼내 닫힌목록으로 넣습니다.

   c) 현재 사각형에 인접한 8 개의 사각형에 대해?

 만약 인접한사각형이 갈수없는 것 이거나 그것이 닫힌목록상에 있다면 무시그렇지 않은것은 다음을 계속합니다.

 만약 이것이 열린목록에 있지 않다면이것을 열린목록에 추가하고이 사각형의 부모를 현재 사각형으로 만듭니다사각형의 F,G,H 비용을 기록.

 만약 이것이 이미 열린목록에 있다면, G비용을 이용하여 이 사각형이 더 나은가 알아보고그것의 G비용이 더 작으면 그것이 더 나은 길이라는 것을 의미하므로 부모 사각형을 그 (G비용이 더 작은)사각형으로 바꿉니다그리고 그 사각형의 G,F비용을 다시 계산합니다만약 여러분의 열린목록을 F비용으로 정렬하고 있다면 바뀐것에 따라서 열린목록을 다시 정렬해야합니다.

   d) 그만해야 할 

● 길을 찾는 중 목표사각형을 열린목록에 추가하였을때,

● 열린목록이 비어있게 될 경우.

(이때는 목표사각형을 찾는데 실패한것인데 이 경우 길이 없는경우입니다.)

 

3. 길 저장하기.

 목표사각형으로부터 각각의 사각형의 부모사각형을 향하여 시작사각형에 도착할때까지 거슬러 올라갑니다
이것이 여러분이 찾는 길입니다.


소스


a star pathfinder v. 1.92.zip


Trackback 0 And Comment 0