경로를 계산하는 방법에 대한 튜토리얼을 제공하겠습니다.

Introduction

경로 탐색은 경로를 계산하는 것입니다. 그러나 이에는 여러 가지 변형이 있습니다. 가장 기본적인 경로는 한 점 A에서 다른 점 B로 이동하는 것이지만, 다른 경로 유형에는 예를 들어 후보 지점 목록의 가장 가까운 지점으로의 경로를 계산하는 경로 유형이 있고, 무작위 방향으로 경로를 계산하는 경로 유형도 있습니다.

또한, 내장된 이동 스크립트를 사용할 수도 있으며, 이 경우에는 경로 탐색 요청을 처리하려고 할 것입니다. 또는 사용자 정의 이동 스크립트를 작성하려고 할 수도 있으며, 이 경우에는 경로 탐색 요청을 직접 처리해야 합니다. 게다가, 어떤 이동 스크립트도 사용하지 않고 경로를 계산할 수도 있습니다. 예를 들어, 어떤 이동도 발생하지 않은 채로 플레이어에게 미리 경로를 표시하려고 할 수 있습니다.

이 페이지에서는 여러 가지 다양한 방법으로 경로를 계산하는 방법에 대해 다룰 것입니다.

Using a built-in movement script

내장된 이동 스크립트 중 하나를 사용하는 경우, 에이전트는 주기적으로 경로를 완전히 자동으로 다시 계산합니다. 해야 할 일은 단순히  ai.destination 속성을 대상 지점으로 설정하거나  AIDestinationSetter 를 사용하여 기존 게임 오브젝트를 향해 이동하도록 만드는 것입니다. 에이전트가 경로를 얼마나 자주 다시 계산하는지는 이동 스크립트의 일부인  AutoRepathPolicy 의 속성을 사용하여 제어할 수 있습니다.

void PointAndClick (IAstarAI ai) {
    // 마우스 버튼이 눌렸는지 확인합니다.
    if (Input.GetMouseButton(0)) {
        var cam = Camera.main;
        // 레이캐스트가 모든 콜라이더에 충돌하도록 설정합니다.
        LayerMask mask = -1;
        // 커서에서 레이를 발사하여 세계에서 어디에 충돌하는지 확인합니다.
        if (Physics.Raycast(cam.ScreenPointToRay(Input.mousePosition), out var hit, Mathf.Infinity, mask)) {
            // AI의 목적지를 레이가 충돌한 지점으로 설정합니다.
            ai.destination = hit.point;
        }
    }
}

 

경우에 따라서는 ai.SearchPath 를 사용하여 에이전트가 즉시 경로를 다시 계산하도록 강제할 수 있습니다.

경로 탐색 요청에 대해 더 직접적인 제어를 원하는 경우 ai.SetPath 메서드를 사용하여 경로 탐색 요청 또는 이미 계산된 경로를 에이전트에 할당할 수 있습니다. 그러나 이렇게 할 경우 자동 경로 재계산을 비활성화해야 합니다. 그렇지 않으면 에이전트가 설정한 경로를 곧바로 자신의 경로로 덮어씁니다.

// 자동 경로 재계산을 비활성화합니다.
ai.canSearch = false;
var pointToAvoid = enemy.position;
// AI를 적으로부터 피하도록 설정합니다.
// 경로는 약 20개의 월드 단위일 것입니다 (1개의 월드 단위를 이동하는 기본 비용은 1000입니다).
var path = FleePath.Construct(ai.position, pointToAvoid, 1000 * 20);
ai.SetPath(path);
IAstarAI.SetPathIAstarAI.SearchPath 을 확인하십시오.

모든 경로 유형을 시연한 Path Types 를 참조하십시오.

 

Using the Seeker component

자체 이동 스크립트를 작성하거나 다른 이유로 경로를 직접 계산해야 하는 경우, Seeker 구성 요소를 사용하여 경로를 계산할 수 있습니다. Seeker 구성 요소는 모든 GameObject에 연결할 수 있으며 해당 GameObject에 대한 경로 탐색 요청을 처리합니다. 경로를 요청하기 쉽게 만드는 편리한 메서드 모음이 있으며, 다양한 경로 탐색 설정과  path modifiers를 처리할 수도 있습니다.

Writing a movement script 참조

built-in movement scripts는 경로를 계산하기 위해 내부적으로 Seeker 구성 요소를 사용합니다. FollowerEntity 구성 요소는 예외입니다. FollowerEntity는 Unity의 Entity Component System을 내부적으로 사용하며, 이것은 전통적인 Unity 구성 요소와 잘 호환되지 않기 때문입니다.

참고: Seeker는 한 번에 하나의 경로 탐색 요청만 처리합니다. 이전 경로 요청이 완료되기 전에 새로운 경로를 요청하면 이전 경로 요청이 취소됩니다. 동시에 여러 경로를 계산하려면 Seeker를 건너뛰고 AstarPath 클래스를 직접 사용해야 합니다.
 
void Start () {
    //  이 GameObject에 연결된 seeker 구성 요소를 가져옵니다
    var seeker = GetComponent<Seeker>();

    // 현재 위치에서 앞으로 10 단위의 위치로 새 경로 요청을 예약합니다.
    // 경로가 계산되면, 취소되지 않은 한 OnPathComplete 메서드가 호출됩니다.
    seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, OnPathComplete);

    // 여기서 경로가 계산되는 것은 아닙니다.
    // 단지 계산을 위해 대기열에 추가된 것뿐입니다.
}

void OnPathComplete (Path path) {
    // 경로가 이제 계산되었습니다!

    if (path.error) {
        Debug.LogError("Path failed: " + path.errorLog);
        return;
    }

    // 사용 중인 경로 유형으로 경로를 캐스트합니다
    var abPath = path as ABPath;

    // 씬 뷰에 경로를 10초 동안 그립니다
    for (int i = 0; i < abPath.vectorPath.Count - 1; i++) {
        Debug.DrawLine(abPath.vectorPath[i], abPath.vectorPath[i+1], Color.red, 10);
    }
}

위의 코드는 GameObject의 현재 위치에서 앞으로 10 단위의 위치로 경로를 요청합니다. 경로가 계산되면 OnPathComplete 메서드가 경로를 인수로 호출됩니다. Seeker와 동일한 GameObject에 path modifiers를 추가하면 OnPathComplete 메서드가 호출되기 전에 경로에 적용됩니다.

자신의 씬에서 이를 시도해보세요. GameObject에 Seeker 구성 요소를 추가한 다음 위의 스크립트를 동일한 GameObject에 추가하세요. 경로의 복잡성 및 동시에 예약된 경로 수에 따라 경로가 계산되는 데 한 프레임 또는 두 프레임이 걸릴 수 있습니다.

경로 요청이 실패하는 경우 일반적인 Error messages목록과 해결 방법을 확인하십시오.

자주 하는 실수는 StartPath 호출 후에 경로가 이미 계산된 것으로 가정하는 것입니다. 그러나 이것은 잘못된 것입니다. StartPath 호출은 경로를 대기열에 넣기만 합니다. 이는 많은 단위가 동시에 경로를 계산할 때 FPS 하락을 피하기 위해 경로 계산을 여러 프레임에 걸쳐 분산하는 것이 바람직하기 때문입니다.  multithreading을 활성화한 경우 모든 경로가 별도의 스레드에서 비동기적으로 계산됩니다.

물론 즉시 경로를 계산해야 할 때도 있습니다. 그런 경우에는 Path.BlockUntilCalculated 메서드를 사용할 수 있습니다.

var path = seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, OnPathComplete);
path.BlockUntilCalculated();

// 이제 경로가 계산되었으며 OnPathComplete 콜백이 호출되었습니다.

 Path.WaitForPath 를 사용하여 코루틴에서 경로가 계산될 때까지 기다릴 수도 있습니다.

IEnumerator Start () {
    // 이 GameObject에 연결된 seeker 구성 요소를 가져옵니다
    var seeker = GetComponent<Seeker>();

    var path = seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, null);
    // 기다립니다... 이것은 경로의 복잡성에 따라 한 프레임 또는 두 프레임이 걸릴 수 있습니다
	// 기다리는 동안 게임의 나머지 부분은 계속 실행됩니다
    yield return StartCoroutine(path.WaitForPath());
    // 이제 경로가 계산되었습니다

    // 씬 뷰에 경로를 10초 동안 그립니다
    for (int i = 0; i < path.vectorPath.Count - 1; i++) {
        Debug.DrawLine(path.vectorPath[i], path.vectorPath[i+1], Color.red, 10);
    }
}

Seeker의 메서드 대신 자체 경로 객체를 만들 수도 있습니다. 이렇게 하면 경로를 계산하기 전에 경로 개체의 설정을 변경할 수 있습니다.

// 새 경로 객체를 생성합니다. 마지막 매개변수는 콜백 함수입니다
// 하지만 이것은 내부적으로 seeker에 의해 사용되므로 여기서는 null로 설정합니다
// 경로는 pooled paths를 사용할 수 있도록 정적 Construct 호출을 사용하여 생성됩니다.
// 이렇게 하면 자주 발생하는 GC 스파이크를 피할 수 있습니다.
var p = ABPath.Construct(transform.position, transform.position+transform.forward*10, null);

// 기본적으로 시작 및 종료 노드에 대한 가장 가까운 이동 가능한 노드를 검색합니다
// 그러나 예를 들어 턴 기반 게임에서는 가장 가까운 이동 가능한 노드를 검색하지 않고
// 대신 대상 지점이 이동할 수 없는 노드에 있으면 오류를 반환하도록 설정할 수 있습니다.
// NNConstraint를 None으로 설정하면 가장 가까운 이동 가능한 노드 검색이 비활성화됩니다.
p.nnConstraint = NNConstraint.None;

// 경로 요청을 예약하기 위해 Seeker에게 전송합니다
seeker.StartPath(p, OnPathComplete);

주의 깊게 보는 독자들은 OnPathComplete에 대한 대리자가 생성될 때마다 GC 할당이 발생하는 것을 알 수 있을 것입니다. 이것은 많지 않지만 누적됩니다. 지역 필드에 대리자를 캐싱하여 이 할당을 피할 수 있습니다.

protected OnPathDelegate onPathComplete;

void OnEnable () {
    onPathComplete = OnPathComplete;
}

public void SearchPath () {
   // 이제 GC 할당을 피하기 위해 직접적으로 OnPathComplete 대신에 onPathComplete 필드를 사용하세요.
    GetComponent<Seeker>().StartPath(transform.position, transform.position+transform.forward*10, onPathComplete);
}

void OnPathComplete (Path p) {
    // 이 구성 요소가 경로를 계산하는 동안 파괴되는 경우를 대비하여
    if (!this) return;
}

 

Seeker 구성 요소는 게임에서 단일 캐릭터의 경로 찾기 요청을 처리하기 위해 설계되었습니다. 이러한 이유로 한 번에 하나의 경로 요청만 처리합니다. 다른 경로가 이미 계산 중일 때 StartPath를 호출하면 이전 경로 계산이 중단되었다는 경고가 기록됩니다. 이전 경로 요청을 명시적으로 취소하려면 경고를 로깅하지 않고 Seeker.CancelCurrentPathRequest 를 호출할 수 있습니다.

여러 경로를 동시에 계산하려면 Seeker를 건너뛰어야 합니다. 대신 직접 AstarPath 클래스를 사용하는 것을 참조하세요.

Other types of paths

표준 경로 외에도  MultiTargetPath (Pro 기능)와 같은 다른 유형의 경로가 있습니다. 특히 MultiTargetPath는 Seeker에 특별한 기능이 있으므로 이들도 쉽게 예약할 수 있습니다.

var endPoints = new Vector3[] {
    transform.position + Vector3.forward * 5,
    transform.position + Vector3.right * 10,
    transform.position + Vector3.back * 15
};
// endPoints가 Vector3[] 배열인 다중 대상 경로를 시작합니다.
// pathsForAll 매개변수는 각 대상 지점까지의 경로를 모두 검색해야 하는지,
// 아니면 어느 대상 지점까지의 최단 경로만 찾아야 하는지를 지정합니다.
var path = seeker.StartMultiTargetPath(transform.position, endPoints, pathsForAll: true, callback: null);
path.BlockUntilCalculated();

if (path.error) {
    Debug.LogError("Error calculating path: " + path.errorLog);
    return;
}

Debug.Log("The closest target was index " + path.chosenTarget);

// 모든 대상 지점으로 경로를 그립니다.
foreach (var subPath in path.vectorPaths) {
    for (int i = 0; i < subPath.Count - 1; i++) {
        Debug.DrawLine(subPath[i], subPath[i+1], Color.green, 10);
    }
}

// 가장 가까운 대상 지점까지의 경로를 그립니다.
for (int i = 0; i < path.vectorPath.Count - 1; i++) {
    Debug.DrawLine(path.vectorPath[i], path.vectorPath[i+1], Color.red, 10);
}
참고
MultiTargetPath는 A* Pathfinding Project Pro 기능입니다. 따라서 프로젝트의 무료 버전을 사용하여 위의 코드를 시도하면 오류가 발생합니다.

어떤 유형의 경로든 예약하는 일반적인 방법은

Path p = MyPathType.Construct (...);    // 여기서 MyPathType은 예를 들어 MultiTargetPath입니다.
seeker.StartPath(p, OnPathComplete);

Construct 메서드는 생성자 대신 사용됩니다. 이렇게 하면 경로 풀링이 더 쉽게 수행될 수 있습니다.

Path TypesPooling 을 참조하세요.
모든 경로 유형의 데모를 보려면 Path Types 를 확인하세요.

 

Using the AstarPath class directly

각 경로를 더욱 더 세밀하게 제어하려면 AstarPath 구성 요소를 직접 사용할 수 있습니다. 그러면 사용하는 주요 함수는  AstarPath.StartPath 입니다. 이 함수는 동시에 많은 경로를 계산하려는 경우 유용합니다. Seeker는 한 번에 하나의 활성 경로만 갖는 캐릭터용이며, 동시에 여러 경로를 요청하려고 하면 마지막 경로만 계산하고 나머지는 취소합니다.

Seeker는 내부적으로 AstarPath.StartPath 메서드를 사용하여 경로를 계산합니다.

참고
AstarPath.StartPath 를 사용하여 계산된 경로는 후처리되지 않습니다. 그러나 특정 Seeker에 연결된 수정자를 사용하여 경로를 후처리하려면 경로가 계산된 후에  Seeker.PostProcess 를 호출할 수 있습니다.
// 씬에 AstarPath 인스턴스가 있어야 합니다
if (AstarPath.active == null) return;

// 여러 경로를 비동기적으로 계산할 수 있습니다
for (int i = 0; i < 10; i++) {
    var path = ABPath.Construct(transform.position, transform.position+transform.forward*i*10, OnPathComplete);

    // AstarPath 구성 요소를 직접 사용하여 경로를 계산합니다
    AstarPath.StartPath(path);
}

 

Path pooling

새 경로 객체를 생성할 때 할당을 피하려면 경로 풀링을 사용할 수 있습니다. 위의 예제에서는 이를 건너뛰었지만, 가비지 수집을 최소화하는 것이 중요하다면 사용하는 것이 좋습니다. 내장된 이동 스크립트는 이미 내부적으로 경로 풀링을 사용합니다.

경로 풀링에 대해 더 자세히 알아보려면 다음을 참조하세요: Pooling .

'유니티 에셋 > A* Pathfinding project pro' 카테고리의 다른 글

Local Avoidance  (0) 2024.05.25
Using Modifiers  (0) 2024.05.24
Movement scripts > Writing a movement script  (0) 2024.05.24
Movement scripts  (0) 2024.05.23
Agent Movement  (0) 2024.05.23

+ Recent posts