Contents

Introduction

참고: 이 튜토리얼을 진행하기 전에 Get Started 튜토리얼을 읽는 것이 좋습니다.

턴제 게임에서는 유닛이 이동할 수 있는 노드와 그 노드를 이동하는 데 드는 비용을 더 자세히 제어해야 할 때가 많습니다. 반면 대부분의 유닛이 한 번에 정지 상태에 있으므로 최대 경로 탐색 성능이 항상 필요한 것은 아닙니다.
모든 경로 요청 전에 그래프의 모든 노드에 대한 이동 비용 및 유사한 속성을 업데이트할 수 있지만, 이는 특히 깔끔한 해결책이 아니므로 이 패키지는 이를 수행할 수 있는 다른 방법을 제공합니다.
더 많은 제어가 필요한 경우 ITraversalProvider 섹션을 참조하십시오.

Blocking nodes

가장 일반적인 시나리오는 일부 유닛이 특정 노드를 통과하지 못하도록 차단하는 것입니다. 예를 들어, 턴제 게임에서는 유닛이 같은 타일에 서지 않도록 하는 경우가 많지만, 유닛 자체는 자신이 서 있는 타일에서 차단되지 않아야 합니다.

이 목적을 위해 BlockManager 컴포넌트와 그에 따른 SingleNodeBlocker 컴포넌트가 존재합니다. SingleNodeBlocker는 특정 노드를 쉽게 차단하기 위해 사용되며, 각 SingleNodeBlocker 컴포넌트는 정확히 하나의 노드를 차단합니다. BlockManager 컴포넌트는 모든 SingleNodeBlocker 컴포넌트를 추적하며, 경로에서 추가적으로 차단된 노드를 지정하는 데 사용할 수 있는 TraversalProvider 인스턴스를 생성할 수 있습니다.

참고: 이 기능의 별도 예제는 "Turnbased"라는 예제 장면에서 볼 수 있습니다. 이 예제 장면은 프로 기능에 의존하므로 무료 버전의 패키지에는 포함되어 있지 않습니다. 그러나 이 튜토리얼에서 언급된 기능은 프로 버전을 필요로 하지 않습니다.

이 튜토리얼에서는 새로운 장면과 몇 가지 스크립트를 만들어 API를 테스트해 보겠습니다.

새 장면을 만들고 프로젝트에 저장한 후 원하는 이름으로 "TurnBasedTest"로 이름을 지정합니다. 새로운 GameObject를 추가하고 이름을 "BlockManager"로 지정한 다음, 선택하고 BlockManager 컴포넌트를 추가합니다.

이제 구체를 추가하고 위치를 (1.5, 0, 2.5)에 배치합니다. 이렇게 하면 노드의 중심에 위치하게 됩니다. SingleNodeBlocker 스크립트를 추가하고 이전에 만든 BlockManager 컴포넌트를 SingleNodeBlocker 컴포넌트의 "manager" 필드에 할당합니다. 구체의 콜라이더를 삭제하는 것을 잊지 마십시오. 그렇지 않으면 구체가 장애물로 감지되므로 SingleNodeBlocker를 사용하여 차단하려는 것입니다. 새로운 GameObject를 만들고 이름을 "A*"로 지정한 다음, AstarPath 컴포넌트를 추가합니다(메뉴 바 -> Pathfinding -> AstarPath). 장면에 큰 평면을 추가하여 바닥 평면으로 사용할 수 있습니다. AstarPath 컴포넌트에 그리드 그래프를 추가하고 "Unwalkable when no ground" 설정을 false로 설정합니다. 이제 Scan 버튼을 클릭하면 작은 빈 그리드에 단일 구체가 있는 것을 볼 수 있습니다. 구체가 그래프의 어떤 노드도 차단하지 않는다는 점에 유의하십시오.


이제 SingleNodeBlocker가 실제로 위치를 차단하도록 해야 합니다. 새로운 C# 스크립트 "BlockerTest.cs"를 만들고 다음 코드를 추가합니다:

using UnityEngine;
using System.Collections;
using Pathfinding;

public class BlockerTest : MonoBehaviour {
    public void Start () {
        var blocker = GetComponent<SingleNodeBlocker>();

        blocker.BlockAtCurrentPosition();
    }
}

그 새로운 스크립트를 우리가 이전에 만든 구체에 추가합니다. 게임이 시작되면, 해당 스크립트는 SingleNodeBlocker 컴포넌트가 BlockManager에게 그 객체의 위치에 있는 노드를 점유하고 있다고 알리도록 합니다. 그러나 이것만으로는 충분하지 않습니다. 아직 어떤 경로도 노드가 차단되었다는 것을 알지 못합니다.

 

실제로 경로를 계산하려면 "BlockerPathTest.cs"라는 새로운 스크립트를 만들어야 하며, 이 스크립트에는 다음 코드를 포함해야 합니다:

using UnityEngine;
using System.Collections.Generic;
using Pathfinding;

public class BlockerPathTest : MonoBehaviour {
    public BlockManager blockManager;
    public List<SingleNodeBlocker> obstacles;
    public Transform target;

    BlockManager.TraversalProvider traversalProvider;

    public void Start () {
        // obstacles 배열에 있는 모든 SingleNodeBlocker에 의해 경로가 차단되어야 한다고 말하는 traversal provider를 생성합니다.
        traversalProvider = new BlockManager.TraversalProvider(blockManager, BlockManager.BlockMode.OnlySelector, obstacles);
    }

    public void Update () {
        // 새로운 Path 객체를 생성합니다.
        var path = ABPath.Construct(transform.position, target.position, null);

        // 경로가 특정 traversal provider를 사용하도록 만듭니다.
        path.traversalProvider = traversalProvider;

        // 경로를 동기적으로 계산합니다.
        AstarPath.StartPath(path);
        path.BlockUntilCalculated();

        if (path.error) {
            Debug.Log("No path was found");
        } else {
            Debug.Log("A path was found with " + path.vectorPath.Count + " nodes");

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

이 스크립트는 지정된 리스트에 있는 장애물을 피하기 위해 생성된 TraversalProvider를 사용하여 자신의 위치에서 대상의 위치까지의 경로를 매 프레임 계산합니다.

BlockManager.TraversalProvider에는 두 가지 모드가 있습니다: AllExceptSelector와 OnlySelector. AllExceptSelector 모드가 설정되면 지정된 리스트에 있는 노드를 제외한 모든 SingleNodeBlocker 컴포넌트에 의해 차단된 노드는 통행 불가능한 것으로 처리됩니다. 예를 들어, 특정 유닛이 자신을 제외한 모든 유닛을 피하도록 하거나, 아군은 피하고 적군은 피하지 않도록 하고 싶을 때 유용합니다. OnlySelector 모드가 설정되면 리스트에 있는 SingleNodeBlocker에 의해 차단된 모든 노드는 통행 불가능한 것으로 처리됩니다. 성능상의 이유로 선택기 리스트를 상대적으로 작게 유지하는 것이 좋습니다.

Pathfinding.BlockManager.BlockMode  참조

 

새로운 GameObject를 "Target"이라는 이름으로 만들고 예를 들어 위치를 (3.5, 0, 3.5)에 배치합니다. 또한 "Path Searcher"라는 이름의 GameObject를 만들고 새로운 BlockerPathTest 컴포넌트를 추가합니다. 객체를 위치 (-2.5, 0, 3.5)로 이동시키고 "Block Manager"와 "Target" 필드를 할당합니다. 이제 재생 버튼을 누르면 빨간색 선이 "Path Searcher"에서 "Target"으로 이어지는 것을 볼 수 있습니다. 이 선이 구체를 통과하는 것처럼 보일 것입니다. 이는 검색기의 "Obstacles" 리스트에 추가하지 않았기 때문입니다.

게임을 중지하고 구체의 SingleNodeBlocker 컴포넌트를 리스트에 추가한 다음 다시 재생 버튼을 누르면 이제 피하게 되는 것을 볼 수 있습니다.

 

이제 이것을 확장하여 예를 들어 여러 개의 빨간 구체와 여러 개의 파란 구체를 만들고, 하나는 파란 구체만 장애물로 간주하고 다른 하나는 빨간 구체만 장애물로 간주하는 두 검색기를 만들 수 있습니다. 아래 비디오에서 이를 확인할 수 있습니다.

 

ITraversalProvider

 

위 시스템은 간단한 경우에 적합하고 사용하기 쉽습니다. 그러나 더 복잡한 경우에는 ITraversalProvider 인터페이스를 사용하는 것이 좋습니다. 이에 대해서는  Agent-Specific Pathfinding에서 더 읽어볼 수 있습니다.

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

Manual Player Movement  (0) 2024.05.26
Circling a target  (0) 2024.05.26
Pathfinding in 2D > Pathfinding on tilemaps  (0) 2024.05.25
Pathfinding in 2D  (0) 2024.05.25
Agent-Specific Pathfinding  (0) 2024.05.25

이 페이지는 Unity의 타일맵을 사용하여 경로 탐색을 작동시키는 방법을 설명합니다.

타일맵은 Unity에서 2D 게임을 만들 때 매우 유용한 도구입니다. 이를 통해 격자 패턴 또는 육각형 격자와 같은 다른 규칙적인 패턴으로 스프라이트를 쉽게 효율적으로 렌더링할 수 있습니다.

타일맵을 사용할 때는 그리드 그래프(레이어형이 아닌) 또는 리캐스트 그래프를 사용하는 것이 좋습니다.

Tilemaps and Grid Graph


그리드 그래프와 함께 타일맵을 사용하는 것은 비교적 간단합니다. 씬에 타일맵을 만들면, 이 타일맵이 그리드 그래프의 인스펙터에 표시되어 그리드를 타일맵에 맞출 수 있습니다. 이는 수동으로 설정을 맞추는 것이 때때로 많은 삼각법을 필요로 하기 때문에 매우 유용합니다. 이 방법은 육각형 격자와 같은 더 특이한 모양에서도 작동합니다.


때로는 타일과 그리드 셀이 1대 1로 일치하지 않기를 원할 수 있습니다. 예를 들어, 더 세밀한 움직임을 허용하기 위해 타일당 3개의 그리드 셀을 사용할 수 있습니다. 이를 위해 먼저 위에서 설명한 대로 그리드를 타일맵에 맞춘 후, 노드 크기를 원래 값의 3분의 1로 조정하거나 게임에 가장 적합한 값으로 조정할 수 있습니다.

타일맵은 2D 물리를 사용하므로 "use 2D physics" 체크박스를 활성화하는 것이 좋습니다.

Use 2D Physics
Unity 2D 물리 API를 사용하십시오.

활성화된 경우 2D Physics API가 사용되며, 비활성화된 경우 3D Physics API가 사용됩니다.
이 설정은 콜라이더 타입을 3D 버전에서 해당하는 2D 버전으로 변경합니다. 예를 들어, 구형 콜라이더는 원형 콜라이더로 변경됩니다.
2D 물리가 사용될 때는 heightCheck 설정이 무시됩니다.

http://docs.unity3d.com/ScriptReference/Physics2D.html  참조
참조
이 설정은  Pathfinding.Graphs.Grid.GraphCollision.use2D 멤버에 해당합니다.


타일맵에 CompositeCollider2D를 사용하는 경우, Geometry Type을 Polygons로 설정해야 합니다. Outlines로 설정된 경우, 콜라이더가 그리드 그래프에 의해 제대로 감지되지 않을 수 있습니다.

Tilemaps and Recast Graph

리캐스트 그래프와 함께 타일맵을 사용하는 것도 비교적 간단합니다. 먼저 dimensions설정을 2D로 변경하여 그래프를 2D 모드로 전환해야 합니다.

Dimensions
3D 모드 또는 2D 모드를 사용할지 여부를 설정합니다.

DimensionMode  참조
참조
이 설정은 Pathfinding.RecastGraph.dimensionMode 멤버에 해당합니다.



그런 다음 타일맵에 TilemapCollider2D가 부착되어 있는지, 리캐스트 그래프 설정에서 rasterize colliders가 활성화되어 있는지, 마지막으로 타일맵의 레이어가 리캐스트 그래프 설정의  rasterization layer mask 에 포함되어 있는지 확인해야 합니다.
 cell size 를 조정하여 더 상세하거나 덜 상세한 그래프를 얻을 수 있습니다.

Graph Updates

런타임에 타일맵을 업데이트하는 경우, 그래프도 업데이트하고 싶을 수 있습니다. 이는 AstarPath.UpdateGraphs를 수정된 타일맵 부분의 경계를 사용하여 호출함으로써 수행할 수 있습니다. 일반 콜라이더로 그래프를 업데이트하는 것과 동일한 방식으로 작동합니다.

전체 맵의 대부분을 변경한 경우, AstarPath.active.Scan()을 호출하여 전체 그래프를 다시 계산할 수 있습니다.

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

Circling a target  (0) 2024.05.26
Utilities for turn-based games  (0) 2024.05.25
Pathfinding in 2D  (0) 2024.05.25
Agent-Specific Pathfinding  (0) 2024.05.25
Multiple agent types  (0) 2024.05.25

이 페이지는 2D 게임에서 경로 탐색을 작동시키는 방법을 보여줍니다.


Contents

 

Introduction

이 패키지에는 2D 설정을 보여주는 2개의 예제 장면이 포함되어 있습니다. 그 이름은 Example15_2D와 Example16_RVO 2D입니다.

 

2D 게임에서 하고자 하는 경로 탐색에는 여러 유형이 있습니다. 주로 두 가지 유형이 있습니다.

  • 탑다운 경로 탐색: 위에서 세계를 내려다보는 게임에서 사용됩니다. 위의 이미지가 이 경우에 해당합니다.
  • 플랫폼 경로 탐색: 예를 들어, 클래식 슈퍼 마리오와 유사합니다. 이 패키지는 현재 이 유형의 경로 탐색을 지원하지 않습니다. 포인트 그래프와 사용자 정의 연결을 생성하여 많은 것을 할 수 있지만, 기본적으로는 지원하지 않습니다.

이 비디오는 경로 탐색이 있는 간단한 2D 장면을 만드는 방법을 안내합니다.

 

 

Graph support

모든 그래프는 XY 평면에서 경로 탐색을 지원하며 2D 물리를 지원합니다.

Grid graphs

그리드 그래프와 계층화된 그리드 그래프는 XY 평면에서 작업하는 것을 잘 지원합니다. 실제로 그리드 그래프는 어느 방향으로든 회전할 수 있으며 여전히 작동합니다. 2D 게임의 경우 그리드 그래프에서 '2D' 토글을 체크하여 그래프가 XY 평면에 맞도록 회전시키는 것이 좋습니다. 2D 콜라이더를 사용하는 경우 'Use 2D Physics' 체크박스를 활성화하는 것도 필요합니다.

Point graphs

포인트 그래프는 XY 평면에서 경로 탐색을 지원합니다. 그리드 그래프와 마찬가지로 'Raycast' 설정이 활성화되어 있고 2D 콜라이더를 사용하는 경우 'Use 2D Physics' 체크박스를 활성화하는 것이 좋습니다.

Recast graphs

리캐스트 그래프는 XY 평면(및 다른 모든 방향)에서 작동하도록 회전할 수 있습니다. 2D 콜라이더를 사용하려면 dimensions 필드를 2D로 변경할 수 있습니다.

리캐스트 그래프는 일반 3D 메쉬, 3D 콜라이더 및 2D 콜라이더를 래스터화하는 것을 지원합니다. 현재 스프라이트를 메쉬로 래스터화하는 것은 지원하지 않습니다.

Navmesh graphs

네비메쉬 그래프도 마찬가지로 어느 방향으로든 회전하여 작동할 수 있습니다. 그러나 그래프의 'up' 방향이 원하는 'up' 방향에 해당하도록 주의해야 합니다. 예를 들어, XY 평면에서 네비메쉬 그래프를 사용하려면 회전 필드를 (-90,0,0)으로 설정해야 하며, Maya 또는 Blender에서 메쉬를 단순히 회전시킬 수는 없습니다.

Tilemaps

그리드 그래프와 리캐스트 그래프 모두 타일맵을 지원합니다. Pathfinding on tilemaps을 사용하는 방법을 알아보려면 타일맵에서의 경로 탐색을 참조하세요.

Graph updates

그래프 업데이트는 기본적으로 2D 게임과 3D 게임에서 동일하게 작동하지만, 몇 가지 유의해야 할 점이 있습니다.

Navmesh Cutting

리캐스트 및 네비메쉬 그래프를 업데이트하는 데 사용할 수 있는 네비메쉬 컷팅은 2D 및 다른 모든 방향에서 작동합니다.

참고
레거시 네비메쉬 컷 모드(사각형/원)를 사용하거나 사용자 정의 메쉬를 사용하는 경우, 네비메쉬 컷이 올바르게 회전되었는지 확인해야 합니다. 레거시 네비메쉬 컷은 그래프 표면에 투영되는 2D 윤곽으로 나타납니다. 레거시 네비메쉬 컷이 잘못 회전된 경우, 2D 윤곽이 단순히 단일 선으로 투영되어 그래프를 원하는 방식으로 컷팅하지 못할 수 있습니다(아래 이미지를 참조하십시오). 수평이 아닌 그래프에서 네비메쉬 컷팅을 사용할 때는 항상  useRotationAndScale 체크박스를 활성화해야 합니다.

 

Graph updates and 2D Colliders

3D 게임에서 그래프 업데이트를 수행하는 일반적인 방법은 다음과 같습니다:

var guo = new GraphUpdateObject(GetComponent<Collider>().bounds);
guo.updatePhysics = false;
guo.modifyWalkability = true;
guo.setWalkability = false;
AstarPath.active.UpdateGraphs(guo);

2D 게임의 경우, 코드를 작동시키기 위해 약간의 조정이 필요합니다. 그 이유는 2D 콜라이더가 무한히 얇아서 경계가 0 부피를 갖기 때문입니다. 그래프는 노드의 위치가 경계 안에 있는지를 확인하기 때문에, 무한히 얇은 경계 안에 어떤 노드도 들어갈 수 없어서 단일 노드도 업데이트되지 않습니다. 이를 해결하기 위해 Z 축을 따라 경계를 확장하여 모든 노드를 포함하도록 할 수 있습니다.

var bounds = GetComponent<Collider2D>().bounds;
// Z 축을 따라 경계를 확장하십시오.
bounds.Expand(Vector3.forward*1000);
var guo = new GraphUpdateObject(bounds);
// 객체의 일부 설정을 변경하십시오.
AstarPath.active.UpdateGraphs(guo);

 

Movement scripts

포함된 이동 스크립트는 다양한 그래프 방향을 지원합니다.

  •  AIPath  이동 스크립트는 그리드 그래프, 네비메쉬 그래프 및 리캐스트 그래프에 대해 어떤 그래프 방향에서도 이동을 지원하며, 자동으로 방향을 맞춥니다. 에이전트가 맵에서 떨어지지 않도록 '중력(gravity)' 설정을 비활성화하는 것이 좋습니다.
  • AILerp  이동 스크립트는 어떤 그래프 방향에서도 이동을 지원합니다. 많은 2D 게임에서는 +Z 축이 아닌 +Y 축을 에이전트의 전방 방향으로 설정하고 싶을 수 있습니다. 이 경우  AILerp.orientation 체크박스를 활성화할 수 있습니다.
  •  FollowerEntity  이동 스크립트는 어떤 그래프 방향에서도 이동을 지원하며, 그래프에 자동으로 방향을 맞춥니다. 에이전트가 맵에서 떨어지지 않도록 '중력(gravity)' 설정을 비활성화하는 것이 좋습니다.
  • RichAI  이동 스크립트는 현재 XZ 평면 이외의 다른 방향을 지원하지 않습니다. 그러나 이를 지원하기 위한 작업의 대부분이 이미 완료되었으며, 향후 업데이트에서 포함될 것으로 예상됩니다.

2D Movement on Point Graphs

포인트 그래프는 명확하게 정의된 상방향이 없기 때문에 AIPath 스크립트는 방향을 어떻게 맞춰야 하는지 알 수 없습니다. 사용하려는 방향을 알고 있는 경우, AIPath 컴포넌트를 서브클래싱하고 UpdateMovementPlane 메서드를 재정의하여 특정 방향을 사용하도록 강제할 수 있습니다.

class AIPathSub : AIPath {
    protected override void UpdateMovementPlane () {
        var graphRotation = new Vector3(-90, 0, 0);
        movementPlane = new SimpleMovementPlane(Quaternion.Euler(graphRotation));
    }
}

graphRotation 변수를 원하는 회전으로 변경할 수 있습니다.

Local avoidance

로컬 회피는 XY 평면에서 작동합니다. 해야 할 유일한 것은 RVOSimulator 컴포넌트의 movementPlane 설정을 XY로 변경하는 것입니다. "RVO 2D"라는 예제 장면을 살펴보면 설정 방법에 대한 예제를 볼 수 있습니다.

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

Utilities for turn-based games  (0) 2024.05.25
Pathfinding in 2D > Pathfinding on tilemaps  (0) 2024.05.25
Agent-Specific Pathfinding  (0) 2024.05.25
Multiple agent types  (0) 2024.05.25
Wandering AI Tutorial  (0) 2024.05.25

때로는 각 에이전트마다 경로 탐색을 맞춤화해야 할 필요가 있습니다.

예를 들어, 일부 에이전트가 물을 피하게 하거나, 일부 에이전트가 도로에서 걷는 것을 선호하게 할 수 있습니다. 일부 게임에서는 모든 에이전트에 대해 고유한 경로 탐색 규칙이 필요할 수도 있습니다.

이 페이지에서는 각 옵션의 장단점을 설명하며, 다양한 방법으로 이를 수행하는 방법을 안내합니다.

 

Alternatives

다음 몇 가지 옵션이 있습니다:

  • 태그 사용. 모든 노드는 0에서 31까지의 단일 태그를 할당받을 수 있습니다. 각 에이전트마다 특정 태그가 통행 가능해야 하는지, 또는 이를 통과하는 비용이 얼마나 될지 결정할 수 있습니다. 태그에 대해 더 알아보려면 여기를 참조하십시오: Working with tags . 이것은 경로 탐색을 맞춤화하는 매우 간단한 방법이지만, 할 수 있는 일에 제한이 있습니다.

  • 여러 그래프 사용. 다른 에이전트가 극적으로 다른 네비게이션 메쉬를 필요로 하는 경우, 여러 그래프를 사용하는 것이 좋은 옵션이 될 수 있습니다. 그러나 많은 그래프를 관리하는 것은 번거롭고 메모리와 성능에 부담이 될 수 있으므로, 소수의 그래프만 필요할 때에만 합리적입니다. 이것은 매우 견고한 옵션이며, 메모리와 성능을 약간 희생하여 최대한의 유연성을 제공합니다. 여러 그래프에 대해 더 알아보려면 여기를 참조하십시오: Multiple agent types.

  • 각 경로 탐색 요청에 대해 각 노드의 통행 가능성과 패널티를 개별적으로 완전히 제어하려면 ITraversalProvider인터페이스를 사용하십시오. 이 옵션은 매우 유연하지만 가장 복잡한 옵션으로, 코드를 작성해야 합니다. 자세한 내용은 아래를 참조하십시오.

 

ITraversalProvider 인터페이스

모든 경로는  traversalProvider를 할당받을 수 있으며, 이 인터페이스에는 재정의해야 하는 세 가지 메서드가 있습니다.

bool CanTraverse (Path path, GraphNode node);
bool CanTraverse (Path path, GraphNode from, GraphNode to);
uint GetTraversalCost (Path path, GraphNode node);

 

CanTraverse(path, node) 메서드는 유닛이 해당 노드를 통과할 수 있는지 여부에 따라 true 또는 false를 반환해야 합니다. CanTraverse(path, from, to) 메서드는 유닛이 from에서 to로의 연결을 통과할 수 있는 경우 true를 반환해야 합니다. 기본적으로 이 메서드는 CanTraverse(path, to)와 동일한 값을 반환하도록 전달됩니다. GetTraversalCost 메서드는 해당 노드를 통과하는 추가 비용을 반환해야 합니다. 기본적으로 태그나 패널티가 사용되지 않는 경우 통과 비용은 0입니다. 비용이 1000인 경우 대략 1 월드 유닛을 이동하는 비용에 해당합니다.

참고로, 기본 구현은 다음과 동일합니다.

public class MyCustomTraversalProvider : ITraversalProvider {
    public bool CanTraverse (Path path, GraphNode node) {
        // 노드가 통행 가능하고 'enabledTags' 비트마스크가 노드의 태그를 포함하는지 확인합니다.
        return node.Walkable && (path.enabledTags >> (int)node.Tag & 0x1) != 0;
        // 또는:
        // return DefaultITraversalProvider.CanTraverse(path, node);
    }

    public bool CanTraverse (Path path, GraphNode from, GraphNode to) {
        return CanTraverse(path, to);
    }
    public uint GetTraversalCost (Path path, GraphNode node) {
        // 통행 비용은 노드의 태그 패널티와 노드의 패널티의 합입니다.
        return path.GetTagPenalty((int)node.Tag) + node.Penalty;
        // 또는:
        // return DefaultITraversalProvider.GetTraversalCost(path, node);
    }

    // Unity 2021.3 및 이후 버전에서는 기본 구현(반환 값 true)을 사용할 수 있으므로 이 코드는 생략할 수 있습니다.
    public bool filterDiagonalGridConnections {
        get {
            return true;
        }
    }
}

사용자 정의 ITraversalProvider 를 구현하면 게임의 규칙에 맞게 패널티를 원하는 대로 변경할 수 있습니다. 성능상의 이유로 그리드 그래프는 통행 불가능한 노드에 대한 모든 연결을 제거하므로, 트래버설 프로바이더에서 "node.Walkable &&" 부분을 제거하더라도 그리드 그래프의 경로는 여전히 통행 불가능한 노드를 통과하지 않습니다. 다시 말해, 노드를 통행 불가능하게 만들 수는 있지만, 통행 불가능한 노드를 다시 통행 가능하게 만들 수는 없습니다.

경고
멀티스레딩이 활성화된 경우, ITraversalProvider의 메서드는 별도의 스레드에서 호출됩니다. 이는 Unity API의 어느 부분도 사용할 수 없다는 것을 의미합니다(수학 같은 것은 예외). 또한, 코드가 스레드 안전(thread safe)하도록 해야 합니다.

ITraversalProvider를 구현한 후에는 앞서 설명한 것처럼 이를 경로에 할당할 수 있습니다.
Seeker 컴포넌트에 설정하여, 해당 Seeker가 계산하는 모든 경로가 해당 트래버설 프로바이더를 사용하도록 할 수 있습니다:

seeker.traversalProvider = new MyCustomTraversalProvider();

또는 경로에 직접 설정하여:

var path = ABPath.Construct(...);
path.traversalProvider = new MyCustomTraversalProvider();

 FollowerEntity 컴포넌트를 사용하는 경우, 경로 탐색 설정에서 트래버설 프로바이더를 설정할 수 있습니다:

followerEntity.pathfindingSettings.traversalProvider = new MyCustomTraversalProvider();
참조
ITraversalProvider를 사용하는 또 다른 예시는 여기에서 확인할 수 있습니다: CircuitBoardExample.cs

 

Case Studies

다음은 몇 가지 일반적인 시나리오와 이를 해결하는 방법에 대한 사례 연구입니다.

Ships and peasants

중세 시대의 농민들이 섬에서 농사를 짓고 화물선이 바다를 항해하는 게임을 가정해봅시다. 농민은 육지에서 걸을 수 있어야 하지만 물에서는 걸을 수 없어야 합니다. 반면 배는 물에서 항해할 수 있어야 하지만 육지에서는 이동할 수 없어야 합니다.

이 경우, 여러 개의 그래프를 사용하는 것을 추천합니다. 하나는 육지를 위한 그래프, 다른 하나는 물을 위한 그래프입니다. 그런 다음 Seeker 인스펙터에서 적절한 그래프를 선택하여 에이전트가 사용할 그래프를 선택할 수 있습니다.

그 이유는 배와 농민이 공통된 표면을 이동할 수 없기 때문에, 이들을 위한 별도의 그래프를 갖는 것이 합리적입니다. 이렇게 하면 각 에이전트 유형에 맞게 그래프를 조정하기도 더 쉬워집니다. 예를 들어, 배가 해안에 너무 가까이 이동하는 것을 원치 않겠지만, 농민은 장애물에서 수 미터 떨어진 곳을 문제없이 걸을 수 있어야 합니다.

그리드 그래프를 사용하는 경우, 태그를 사용하여 농민을 섬에, 배를 물에 제한하는 방식으로 이 문제를 해결할 수도 있습니다. 이것은 가능하지만 설정하는 데 약간 더 번거로울 수 있습니다.

Turn based games

턴제 게임에서는 유닛이 같은 타일에 서지 못하도록 방지하고 싶어합니다. 하지만 현재 유닛이 서 있는 타일에는 서 있을 수 있어야 합니다. 턴제 게임에서는 종종 다른 유닛에 대해 서로 다른 이동 비용이 있으며, 이러한 규칙은 매우 복잡할 수 있습니다.

이것은  ITraversalProvider 인터페이스를 사용하여 이 로직을 구현하는 명확한 사례입니다. 다행히도, 이 경우를 위해 이미 구현된 편리한 도우미들이 있습니다.  Utilities for turn-based games 를 살펴보세요.

Multiple agent sizes

일부 게임에서는 매우 다양한 크기의 에이전트를 가지고 있습니다. 이 경우는 매우 흔하여 별도의 페이지가 있습니다! 여기에서 확인해보세요:  Multiple agent types.

Prefering roads

일부 게임에서는 유닛이 도로에서 걷는 것을 선호하게 하고 싶지만, 필요할 경우 다른 표면에서도 걸을 수 있도록 하고 싶습니다. 이는 태그를 사용하여 도로를 표시하고 태그 기반 패널티를 사용하여 유닛이 다른 표면을 피하려고 하도록 함으로써 가능합니다.

태그 패널티는 음수일 수 없으므로, 음수 패널티를 사용하여 유닛이 도로를 선호하게 만들 수는 없습니다. 대신, 도로가 아닌 모든 노드에 양수 패널티를 적용합니다.

이 기술은 그리드 그래프에서 가장 잘 작동합니다. 그리드 그래프에서 패널티를 사용하는 것은 예측 가능하고 설정하기 쉬우며 세분성이 좋기 때문입니다. 네비메쉬/리캐스트 그래프에서 패널티를 사용하는 Pathfinding은 본질적으로 덜 정밀합니다. 왜냐하면 노드가 더 크기 때문입니다.

이 페이지는 다양한 크기의 에이전트 유형을 처리하는 방법을 설명합니다.

다양한 크기의 에이전트를 가지고 있을 때, 이들은 보통 동일한 경로를 따라 목표 지점으로 이동할 수 없습니다. 다행히도 이 문제를 해결하는 것은 쉽습니다. 가장 쉬운 방법은 각 에이전트 유형에 대해 여러 개의 그래프를 만드는 것입니다. 다양한 유형의 에이전트가 많거나 연속적인 스펙트럼이 있는 경우, 그래프의 수가 많아지면 메모리 사용량이 증가하고 스캔 시간이 길어지므로 이를 그룹화하는 것이 좋습니다.  Seeker component에서는 Seeker가 사용할 수 있는 그래프를 설정할 수 있습니다.

참고
Seeker.graphMask
NNConstraint.graphMask

그리드 그래프의 경우 사용할 수 있는 추가적인 두 가지 방법이 있으며, 이는 그리드 그래프에 대한 다른 접근 방식 섹션에서 설명됩니다.

예시

다음과 같은 두 에이전트가 있다고 가정해 보겠습니다:


 AstarPath  인스펙터에서 우리는 두 개의 다른 그래프를 만들 수 있으며, 유일한 차이는 사용된 캐릭터 반경입니다(다른 매개변수도 필요에 따라 변경할 수 있습니다). 이 예에서는 리캐스트 그래프가 사용되었지만, 다른 그래프 유형으로도 쉽게 할 수 있습니다.


이 그래프들을 스캔할 때, 우리는 작은 에이전트를 위한 그래프는 파란색으로, 큰 에이전트를 위한 그래프는 보라색으로 된 결과를 얻게 됩니다.


그런 다음 Seeker 컴포넌트에서 각 에이전트가 사용할 그래프를 설정할 수 있습니다:

이제 두 에이전트 모두 자신들의 크기에 적합한 그래프를 사용하게 됩니다:

 

그리드 그래프에 대한 다른 접근 방식

그리드 그래프에서 대체 방법으로는 그래프가 가장 가까운 장애물까지의 거리와 상관없이 서로 다른 tags 를 생성하게 하는 방법이 있습니다. 이는 단 하나의 그래프만 필요로 합니다.

Pathfinding.GridGraph.erosionUseTags  참조

또한 매우 유연하지만 불행히도 계산 비용이 더 높은 다른 접근 방식도 있습니다. 경로 요청 시  ITraversalProvider 객체를 제공할 수 있는데 (이 튜토리얼도 참조:  ITraversalProvider ), 이를 통해 코드에서 정확히 어떤 노드를 통과할 수 없게 할지를 결정할 수 있습니다. 이를 사용하면 현재 노드가 통과 가능한지 확인하는 대신, 예를 들어 3x3 또는 5x5 정사각형 내의 모든 노드를 확인하는 사용자 정의 코드를 추가할 수 있습니다 (기본값은 1x1 정사각형과 동일).

아래 이미지에서는 3x3 정사각형을 사용한 예를 볼 수 있습니다. 장애물 바로 옆의 노드들이 걸을 수 있음에도 불구하고 통과하지 않으려는 것을 확인할 수 있습니다. 이는 3x3 정사각형 내의 노드들을 확인하기 때문입니다.

ITraversalProvider 접근 방식의 주요 장점은 노드를 통과할 수 없게 만드는 모든 요소들을 처리할 수 있다는 것입니다. 예를 들어, 에이전트의 움직임을 제한하기 위해 다른 태그를 사용할 수 있지만, 여러 그래프를 사용하더라도 다양한 에이전트가 태그 때문에 통과할 수 없는 영역까지 걸어갈 수 있습니다. 이 접근 방식을 사용하면, 태그가 있는 노드들이 통과할 수 없는 것으로 감지되기 때문에 에이전트의 크기가 매우 큰 경우에도 근처로 이동할 수 없습니다. 에이전트 주위의 NxN 노드들은 모두 통과 가능해야 합니다.

ITraversalProvider는 다음과 같이 구현할 수 있습니다:

class GridShapeTraversalProvider : ITraversalProvider {
    Int2[] shape;

    public static GridShapeTraversalProvider SquareShape (int width) {
        if ((width % 2) != 1) throw new System.ArgumentException("only odd widths are supported");
        var shape = new GridShapeTraversalProvider();
        shape.shape = new Int2[width*width];

        // 정수 좌표를 포함하는 배열을 생성합니다.
        int i = 0;
        for (int x = -width/2; x <= width/2; x++) {
            for (int z = -width/2; z <= width/2; z++) {
                shape.shape[i] = new Int2(x, z);
                i++;
            }
        }
        return shape;
    }

    public bool CanTraverse (Path path, GraphNode node) {
        GridNodeBase gridNode = node as GridNodeBase;

        // 그리드 노드가 아닌 경우 특별히 처리하지 않습니다.
        if (gridNode == null) return DefaultITraversalProvider.CanTraverse(path, node);
        int x0 = gridNode.XCoordinateInGrid;
        int z0 = gridNode.ZCoordinateInGrid;
        var grid = gridNode.Graph as GridGraph;

        // 현재 노드 주변의 모든 노드를 순회하며 해당 노드들이 모두 통과 가능한지 확인합니다.
        for (int i = 0; i < shape.Length; i++) {
            var inShapeNode = grid.GetNode(x0 + shape[i].x, z0 + shape[i].y);
            if (inShapeNode == null || !DefaultITraversalProvider.CanTraverse(path, inShapeNode)) return false;
        }
        return true;
    }

    public bool CanTraverse (Path path, GraphNode from, GraphNode to) {
        return CanTraverse(path, to);
    }

    public uint GetTraversalCost (Path path, GraphNode node) {
        // 기본 통과 비용을 사용합니다.
        // 선택적으로, 예를 들어, 도형 내부의 비용 평균을 계산하는 등으로 수정할 수 있습니다.
        return DefaultITraversalProvider.GetTraversalCost(path, node);
    }

    // Unity 2021.3 및 이후 버전에서는 기본 구현(참 값을 반환)이 사용되기 때문에 이 부분을 생략할 수 있습니다.
    public bool filterDiagonalGridConnections {
        get {
            return true;
        }
    }
}
주의
이 구현은 계층형 그리드 그래프가 아닌 그리드 그래프에서만 작동합니다.

 

그리고 다음과 같이 사용할 수 있습니다.

ABPath path = ABPath.Construct(currentPosition, destination, null);

path.traversalProvider = GridShapeTraversalProvider.SquareShape(3);

// 직접 이동 스크립트를 작성하는 경우
seeker.StartPath(path);

// 기존 이동 스크립트를 사용하는 경우 (ai.canSearch를 false로 설정할 수도 있음)

주의
두 가지 그리드 특정 접근 방식의 단점은 도달할 수 없는 목적지로 경로를 요청하려고 할 경우 경로가 완전히 실패하고, AI가 도달할 수 있는 가장 가까운 지점으로 이동하지 않는다는 점입니다. 경로를 계산하기 전에  calculatePartial 필드를 true로 설정하여 일부 경우에 이를 해결할 수 있습니다:

path.calculatePartial = true;

이렇게 하면 목표 노드가 ITraversalProvider에 따라 통과 가능한 노드인 경우, 도달할 수 있는 가장 가까운 노드로 이동하게 됩니다.

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

Pathfinding in 2D  (0) 2024.05.25
Agent-Specific Pathfinding  (0) 2024.05.25
Wandering AI Tutorial  (0) 2024.05.25
Local Avoidance > Custom Local Avoidance Movement Script  (0) 2024.05.25
Local Avoidance  (0) 2024.05.25

방황하는 AI를 만드는 방법에 대한 튜토리얼.

방황하는 AI를 만드는 방법에는 여러 가지가 있습니다. 이 방법들은 품질과 성능 면에서 차이가 있습니다. 명확한 최선의 해결책은 없으며, 각 방법은 게임에 따라 더 적합할 수 있습니다. 각 방법에 대해 장점과 단점을 나열했습니다.

  • 방법 1: 원 안의 무작위 지점
  • 방법 2: 전체 그래프에서 무작위 노드
  • 방법 3: RandomPath 유형
  • 방법 4: ConstantPath 유형
  • 방법 5: 너비 우선 탐색

방법 1: 원 안의 무작위 지점
이 방법은 캐릭터 주위의 특정 거리 내에서 무작위 지점을 선택하는 가장 간단한 방법입니다.

 

방법 1: 원 안의 무작위 지점

이동할 지점을 선택하는 가장 쉬운 방법은 캐릭터 주위의 특정 거리 내에서 무작위 지점을 선택하는 것입니다.

Vector3 PickRandomPoint () {
    var point = Random.insideUnitSphere * radius;

    point.y = 0;
    point += transform.position;
    return point;
}

포함된 이동 스크립트 중 하나를 사용하는 경우, 예를 들어 AI가 현재 경로의 끝에 도달할 때마다 목적지를 변경할 수 있습니다.

using UnityEngine;
using System.Collections;
using Pathfinding;

public class WanderingDestinationSetter : MonoBehaviour {
    public float radius = 20;

    IAstarAI ai;

    void Start () {
        ai = GetComponent<IAstarAI>();
    }

    Vector3 PickRandomPoint () {
        var point = Random.insideUnitSphere * radius;

        point.y = 0;
        point += ai.position;
        return point;
    }

    void Update () {
        // AI의 목적지를 업데이트합니다.
	// AI가 경로를 계산 중이 아니고
	// AI가 경로의 끝에 도달했거나 경로가 전혀 없을 때
        if (!ai.pathPending && (ai.reachedEndOfPath || !ai.hasPath)) {
            ai.destination = PickRandomPoint();
            ai.SearchPath();
        }
    }
}

위의 스크립트를 AIPath, RichAI, AILerp 등의 내장 이동 스크립트가 포함된 어떤 게임 오브젝트에든 부착할 수 있습니다.

참고
AI의 목적지를 설정하려고 시도하는 다른 컴포넌트( AIDestinationSetter 컴포넌트 등)가 부착되어 있지 않은지 확인하십시오.

 

 

 

참조
Pathfinding.IAstarAI
Random.insideUnitSphere

 

 

장점

  • 매우 빠름.
  • 간단한 코드.
  • 모든 그래프 유형에서 비교적 잘 작동함.

단점

  • 지점까지의 실제 거리를 무시하므로, 세계가 매우 개방적이지 않은 경우 매우 긴 경로를 생성할 수 있음(위의 비디오 참조).
  • 장애물 내부에서 생성된 지점은 그래프의 가장 가까운 지점으로 이동해야 하므로, 벽이나 장애물 근처의 지점으로 약간의 편향이 있음.

기타

  • 지점을 선택할 때 패널티를 무시함(그러나 지점까지 경로를 찾을 때는 패널티가 고려됨).

 

방법 2: 전체 그래프에서 무작위 노드

때로는 매우 간단한 것을 원할 때가 있습니다. 현재 위치 주위의 노드를 선택하는 대신, 그래프에서 무작위 노드를 선택하고 해당 노드의 위치로 이동하려고 시도할 수 있습니다.

GraphNode randomNode;

// 그리드 그래프의 경우
var grid = AstarPath.active.data.gridGraph;

randomNode = grid.nodes[Random.Range(0, grid.nodes.Length)];

// 포인트 그래프의 경우
var pointGraph = AstarPath.active.data.pointGraph;
randomNode = pointGraph.nodes[Random.Range(0, pointGraph.nodes.Length)];

// 모든 그래프 유형에서 작동하지만 훨씬 느림
var graph = AstarPath.active.data.graphs[0];
// 그래프의 모든 노드를 리스트에 추가
List<GraphNode> nodes = new List<GraphNode>();
graph.GetNodes((System.Action<GraphNode>)nodes.Add);
randomNode = nodes[Random.Range(0, nodes.Count)];

// 예를 들어 노드의 중심을 목적지로 사용
var destination1 = (Vector3)randomNode.position;
// 또는 노드 표면의 무작위 지점을 목적지로 사용
// 이는 노드가 큰 네비게이션 메시 기반 그래프에서 유용함
var destination2 = randomNode.RandomPointOnSurface();

 

 

사용 용도에 따라, 노드를 선택한 후 다음과 같은 체크를 수행할 수 있습니다:

  • 노드가 걷기 가능한지 확인 (  Pathfinding.GraphNode.Walkable 참조)
  • 노드가 AI로부터 도달 가능한지 확인 (  Pathfinding.PathUtilities.IsPathPossible 참조)
  • 게임에 관련된 다른 체크 사항. 이 체크 중 하나가 실패하면 새로운 무작위 노드를 선택하고 다시 체크를 수행할 수 있습니다. 유효한 노드가 매우 드물지 않은 한 유효한 노드를 빠르게 찾을 수 있을 것입니다.

장점

  • 매우 빠름 (위의 일반적인 접근 방식을 사용하지 않는 경우)
  • 간단한 코드

단점

  • 경로 길이에 대한 제어가 거의 없음

기타

  • 지점을 선택할 때 패널티를 무시함 (그러나 지점까지 경로를 찾을 때는 패널티가 고려됨).

 

방법 3: RandomPath 유형

RandomPath 유형은 특정 지점에서 다른 특정 지점으로의 경로를 계산하는 일반적인 경로와는 달리, 시작 지점에서부터 랜덤하게 멀어지는 경로를 계산하는 경로 유형입니다.

내장된 이동 스크립트 중 하나를 사용하고 있다면 ( Movement scripts 참조) 다음과 같은 작업을 수행할 수 있습니다:

// 에이전트의 내부 자동 경로 재계산을 비활성화합니다
ai.canSearch = false;
RandomPath path = RandomPath.Construct(transform.position, searchLength);
path.spread = spread;
// 에이전트에게 계산하고 따라갈 경로를 지정합니다
ai.SetPath(path);

 

사용자 정의 이동 스크립트를 사용하는 경우, 이를 수동으로 시커(Seeker)에 전달해야 합니다.

RandomPath path = RandomPath.Construct(transform.position, searchLength);

path.spread = spread;
seeker.StartPath(path);
Searching for paths  참조

 

 

장점

  • 한 번에 지점을 선택하고 경로를 계산함.
  • 고려되는 모든 노드가 동일한 확률로 선택되므로, 방법 1: 원 안의 무작위 지점과 달리 장애물 근처에 대한 편향이 없음.
  • 최소 경로 비용을 설정할 수 있음(searchLength).

 

단점

  • 긴 경로의 경우 속도가 느릴 수 있음.
  • 모든 노드가 동일한 확률로 선택되므로, 디테일이 많은 지역(더 많고 작은 노드가 있는 지역)에 대한 편향이 발생하여 navmesh/recast 그래프에서 잘 작동하지 않음.

 

기타

패널티를 고려함.

 

Pathfinding.RandomPath  참조

 

 

방법 4: ConstantPath 유형

ConstantPath 유형은 시작 지점에서 특정 값과 같거나 그 이하의 비용으로 도달할 수 있는 모든 노드를 찾는 경로 유형입니다. 이러한 모든 노드를 찾은 후, 해당 노드 표면에서 하나 이상의 무작위 지점을 선택할 수 있습니다.

ConstantPath path = ConstantPath.Construct(transform.position, searchLength);

AstarPath.StartPath(path);
path.BlockUntilCalculated();
var singleRandomPoint = PathUtilities.GetPointsOnNodes(path.allNodes, 1)[0];
var multipleRandomPoints = PathUtilities.GetPointsOnNodes(path.allNodes, 100);

 

내장된 이동 스크립트에 이를 적용하려면, 방법 1: 원 안의 무작위 지점에서 사용한 것과 동일한 접근 방식을 사용할 수 있습니다.

 

참조
Pathfinding.ConstantPath
Pathfinding.PathUtilities.GetPointsOnNodes

 

장점

  • 단일 경로 요청으로 여러 무작위 지점을 선택할 수 있음.
  • 노드를 선택할 확률은 해당 노드의 표면적에 비례함 (따라서 모든 노드가 동일한 크기를 갖는 경우, 예를 들어 그리드 그래프를 사용할 때, 고려되는 모든 노드는 동일한 확률로 선택됨). 방법 1: 원 안의 무작위 지점처럼 장애물 근처에 대한 편향이 없음.
  • 필요할 경우 노드의 사용자 정의 필터링이 가능함.
  • 방법 3: RandomPath 유형보다 보통 약간 더 빠름.
  • navmesh/recast 그래프에서 비교적 잘 작동함.

 

단점

  • 많은 노드를 검색해야 할 때 느릴 수 있음.

 

기타

  • 패널티를 고려함.

 

방법 5: 너비 우선 탐색

 breadth-first search은 시작 노드에서부터 하나의 노드씩 외부로 검색하는 간단한 탐색 방법입니다. 패널티나 다른 유형의 비용을 고려하지 않습니다. 예를 들어, 그리드 그래프에서 대각선으로 이동하는 것이 4개의 비대각선 연결 중 하나를 따라 이동하는 것과 동일한 비용을 가진다는 의미입니다.

사용 방법은 방법 4: ConstantPath 유형에서 사용된 것과 매우 유사합니다.

// 가장 가까운 걷기 가능한 노드 찾기
var startNode = AstarPath.active.GetNearest(transform.position, NNConstraint.Walkable).node;
var nodes = PathUtilities.BFS(startNode, nodeDistance);
var singleRandomPoint = PathUtilities.GetPointsOnNodes(nodes, 1)[0];
var multipleRandomPoints = PathUtilities.GetPointsOnNodes(nodes, 100);

내장된 이동 스크립트에 이를 적용하려면, 방법 1: 원 안의 무작위 지점에서 사용한 것과 동일한 접근 방식을 사용할 수 있습니다.

 

참조
Pathfinding.PathUtilities.BFS
Pathfinding.PathUtilities.GetPointsOnNodes

 

장점

  • 하나의 지점뿐만 아니라 여러 무작위 지점을 선택할 수 있음.
  • 노드를 선택할 확률은 해당 노드의 표면적에 비례함(따라서 모든 노드가 동일한 크기를 갖는 경우, 예를 들어 그리드 그래프를 사용할 때, 고려되는 모든 노드는 동일한 확률로 선택됨). 방법 1: 원 안의 무작위 지점처럼 장애물 근처에 대한 편향이 없음.
  • 필요할 경우 나중에 노드를 사용자 정의 필터링할 수 있음.
  • 경로 찾기 시스템으로의 왕복이 필요 없으므로 약간의 오버헤드를 줄일 수 있음.
  • 보통 방법 4: ConstantPath 유형과 방법 3: RandomPath 유형보다 약간 더 빠름.

단점

  • 많은 노드를 검색해야 할 때 느릴 수 있음.
  • 경로 찾기 스레드를 사용하지 않으므로, 이를 Unity 메인 스레드에서 쉽게 오프로드할 수 있는 기능을 잃게 됨.
  • navmesh/recast 그래프에서는 특히 잘 작동하지 않음. 노드의 크기와 세부 사항에 따라 경로 길이가 크게 달라지기 때문임(노드가 많고 작을수록 BFS가 멀리 도달하지 못함).

기타

  • 패널티나 다른 비용을 고려하지 않으며, 탐색된 노드의 수만 중요함.

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

Agent-Specific Pathfinding  (0) 2024.05.25
Multiple agent types  (0) 2024.05.25
Local Avoidance > Custom Local Avoidance Movement Script  (0) 2024.05.25
Local Avoidance  (0) 2024.05.25
Using Modifiers  (0) 2024.05.24

이 페이지에서는 로컬 회피를 기존 이동 스크립트에 통합하는 방법을 설명합니다.

작동 방식을 보기 위해 간단한 예제를 만들어 보겠습니다.
새로운 장면을 만들고, 큰 평면을 바닥으로 추가합니다 (위치 (0,0,0), 크기 (10,10,10)). 그런 다음 새로운 GameObject를 추가하고, 이름을 "Simulator"로 지정합니다. 이제 RVOSimulator 컴포넌트를 추가합니다. 이 컴포넌트는 Components -> Local Avoidance -> RVO Simulator에서 찾을 수 있습니다. 몇 가지 옵션이 있지만, 지금은 기본 설정으로 두어도 됩니다. 그러나 나중에 RVOSimulator의 클래스 문서를 읽는 것을 권장합니다. 성능은 이 몇 가지 설정에 매우 의존하기 때문입니다. 이 컴포넌트는 에이전트의 시뮬레이션을 처리하고, 추가할 동적 장애물을 저장합니다(나중에 더 자세히 설명합니다).

이제 AI가 이동하도록 하겠습니다. 매우 간단한 AI로, 기본적으로 앞으로 걸을 것입니다. 먼저 장면에 새로운 Cylinder를 추가합니다 (GameObject -> Create Other -> Cylinder). 장면의 어느 곳에든 높이 2 유닛의 실린더가 생성됩니다. 이전에 추가한 바닥 평면 위에 보이도록 배치합니다. 이 GameObject에 RVOController 컴포넌트를 추가합니다. 이 컴포넌트는 Components -> Local Avoidance -> RVO Controller에서 찾을 수 있습니다. 이 컴포넌트는 Unity Character Controller의 거의 드롭인 대체품으로 설계되었으므로, 캐릭터 컨트롤러를 사용해본 적이 있다면 익숙할 것입니다. 명백한 이유로 충돌 플래그와 같은 충돌 관련 기능은 지원하지 않지만, 매우 유사합니다. 우리의 실린더는 높이가 2 유닛이므로, RVOController의 height 변수를 2로 설정합니다.

이제 우리의 AI가 바닥에 서 있으므로, 그에게 무언가를 하도록 지시해봅시다. 좋아하는 텍스트 편집기를 열고 SimpleRVOAI.cs라는 스크립트를 만듭니다. 스크립트는 다음과 같아야 합니다:

using UnityEngine;
using System.Collections;
using Pathfinding.RVO;

public class SimpleRVOAI : MonoBehaviour {
    RVOController controller;

    // 초기화에 사용
    void Awake () {
        controller = GetComponent<RVOController>();
    }

    // 매 프레임마다 업데이트
    public void Update () {
        // 단지 멀리 떨어진 지점
        var targetPoint = transform.position + transform.forward * 100;

        // 원하는 속도 10과 최대 속도 12를 사용하여 이동할 원하는 지점을 설정
        controller.SetTarget(targetPoint, 10, 12, new Vector3(float.NaN, float.NaN, float.NaN));

        // 이 프레임 동안 얼마나 이동할지 계산
        // 이 정보는 이전 프레임의 이동 명령을 기반으로 함
        // 로컬 회피는 RVOSimulator 컴포넌트에 의해 정기적으로 전역에서 계산됨
        var delta = controller.CalculateMovementDelta(transform.position, Time.deltaTime);
        transform.position = transform.position + delta;
    }
}

이 코드는 Awake에서 RVOController를 가져오고 매 프레임마다 앞으로 멀리 떨어진 지점을 이동할 목표 지점으로 설정합니다. 로컬 회피 시스템에 초당 10 유닛의 속도로 이동하고 싶지만 필요하다면 초당 12 유닛의 속도로 이동할 수 있다고 알려줍니다. 이 스크립트를 실린더에 추가하고 재생 버튼을 누르면 일정한 속도로 앞으로 이동할 것입니다.

RVOController는 자체적으로 이동을 처리하지 않는다는 점에 유의하십시오. 일부 게임은 CharacterController를 사용할 수 있고, 일부는 transform.Translate를 사용할 수 있으며, 일부는 Rigidbody를 사용할 수 있으므로 실제로 캐릭터를 이동시키는 것은 이동 스크립트에 달려 있습니다. 이는 RVOController가 계산한 속도를 기반으로 합니다.

이제 재미있는 부분입니다. 실린더를 복제하여 첫 번째 실린더 앞에 일정 거리만큼 배치하고 서로 마주보도록 회전시킵니다. 재생 버튼을 누릅니다. 실린더들이 서로를 향해 이동하고, 충돌 직전에 서로를 피할 것입니다! 멋지지 않나요!

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

Multiple agent types  (0) 2024.05.25
Wandering AI Tutorial  (0) 2024.05.25
Local Avoidance  (0) 2024.05.25
Using Modifiers  (0) 2024.05.24
Searching for paths  (0) 2024.05.24

A*  Pathfinding Project에서 로컬 회피 기능을 사용하는 방법.

A*  Pathfinding Project에 포함된 로컬 회피 기능은 RVO(Reciprocal  Velocity Obstacles )를 기반으로 하며, 특히 ORCA 알고리즘을 사용합니다. 이는 예를 들어 주변을 이동하는 인간 에이전트에게 잘 맞습니다. 빠르게 속도를 변경할 수 없는 차량에는 적합하지 않습니다.

 

A* Pro 기능
이 기능은 A* Pathfinding Project의 Pro 버전에서만 사용할 수 있습니다. 이 함수/클래스/변수는 A* Pathfinding Project의 무료 버전에는 존재하지 않거나 기능이 제한될 수 있습니다.
Pro 버전은 여기에서 구매할 수 있습니다.

 

Overview

로컬 회피는 에이전트들이 서로를 피하도록 하며, 더 간단한 동적 장애물 회피에도 사용됩니다. 경로 탐색과는 달리, 로컬 회피는 이름 그대로 매우 로컬합니다. 에이전트와 가까운 장애물만을 고려하며, 에이전트의 글로벌 목표는 신경 쓰지 않습니다. 따라서 큰 장애물을 피할 수 없습니다.

반면에, 작은 동적 객체를 경로 탐색으로 처리하는 것은 비효율적일 수 있습니다. 작은 변화마다 경로를 재계산해야 하기 때문입니다. 로컬 회피는 이런 경우에 훨씬 더 적합합니다.

RVO 시스템은 두 부분으로 나뉩니다. 첫 번째는 핵심 시뮬레이션 코드로, GameObjects와 MonoBehaviours와 같은 Unity 특정 객체와는 완전히 독립적입니다. 이 핵심 코드는 모든 로컬 회피 에이전트의 시뮬레이션을 처리합니다.

두 번째 부분은 Unity 인터페이스입니다. 이 클래스들 중 많은 부분이 해당 핵심 클래스의 래퍼(wrapper)입니다. 예를 들어, RVOController 클래스는 Pathfinding.RVO.IAgent 인터페이스의 래퍼 클래스입니다.

Examples

로컬 회피 시스템이 어떻게 작동하는지 보여주는 세 개의 예제 장면이 포함되어 있습니다. 이 장면들은 

Assets/AstarPathfindingProject/ExampleScenes/Example11_RVO 및 

Assets/AstarPathfindingProject/ExampleScenes/Example16_RVO 2D에서 찾을 수 있습니다.

Integration

장면에는 RVOSimulator 컴포넌트가 정확히 하나 있어야 합니다. 다른 모든 컴포넌트는 이 컴포넌트를 자동으로 찾습니다.

에이전트 설정 방법은 사용하는 이동 스크립트에 따라 다릅니다.

FollowerEntity 이동 스크립트를 사용하는 경우, 이동 스크립트에서 Local Avoidance 체크박스를 활성화할 수 있습니다.
AIPath ,  RichAI 또는 사용자 정의 이동 스크립트를 사용하는 경우, 에이전트에  RVOController 컴포넌트를 추가해야 합니다.

참고
AILerp  이동 스크립트는 경로를 따라 보간(interpolate)하며 정확하게 따르도록 설계되었습니다. 따라서 경로에서 벗어나는 것이 의미가 없으므로 로컬 회피를 지원하지 않습니다.
참조
사용자 정의 이동 스크립트에서 로컬 회피를 사용하거나 로컬 회피가 어떻게 작동하는지 더 깊이 배우고 싶다면, 다음 페이지를 참조하세요: Custom Local Avoidance Movement Script .

 

이 두 가지 방법은 본질적으로 동일한 설정을 노출합니다. FollowerEntity 는 내부적으로 Entity Component System(ECS)을 사용하므로 RVOController 를 별도의 컴포넌트로 사용할 수 없습니다. 내부적으로는 둘 다 동일한 방식으로 시뮬레이션됩니다.

 

Radius

에이전트의 반경을 월드 유닛으로 나타냅니다.

참고
AIPath/RichAI/AILerp와 같은 이동 스크립트(  IAstarAI 인터페이스를 구현하는 모든 것)가 동일한 GameObject에 부착된 경우, 이 값은 해당 스크립트에 의해 제어됩니다.
참조
이 설정은  Pathfinding.RVO.RVOController.radius 멤버에 해당합니다.

 

 

Height

에이전트의 높이를 월드 유닛으로 나타냅니다.

참고
AIPath/RichAI/AILerp와 같은 이동 스크립트(  IAstarAI 인터페이스를 구현하는 모든 것)가 동일한 GameObject에 부착된 경우, 이 값은 해당 스크립트에 의해 제어됩니다.
참조
이 설정은 Pathfinding.RVO.RVOController.height 멤버에 해당합니다.

 

 

Center

이 게임 오브젝트의 피벗 포인트를 기준으로 한 에이전트의 중심입니다.

참고
AIPath/RichAI/AILerp와 같은 이동 스크립트( IAstarAI 인터페이스를 구현하는 모든 것)가 동일한 GameObject에 부착된 경우, 이 값은 해당 스크립트에 의해 제어됩니다.
참조
이 설정은 Pathfinding.RVO.RVOController.center 멤버에 해당합니다.



Agent Time Horizon
다른 에이전트와의 충돌을 예측하기 위해 미래를 얼마나 멀리 내다볼지 설정합니다 (초 단위).

참조: 이 설정은 Pathfinding.RVO.RVOController.obstacleTimeHorizon 멤버에 해당합니다.



Obstacle Time Horizon
장애물과의 충돌을 예측하기 위해 미래를 얼마나 멀리 내다볼지 설정합니다 (초 단위).

참조: 이 설정은  Pathfinding.RVO.RVOController.obstacleTimeHorizon 멤버에 해당합니다.



Max Neighbours
고려할 다른 에이전트의 최대 수를 설정합니다.

값이 작을수록 CPU 부하가 줄어들며, 값이 클수록 로컬 회피 품질이 향상될 수 있습니다.

참조: 이 설정은  Pathfinding.RVO.RVOController.maxNeighbours 멤버에 해당합니다.



Layer
이 에이전트의 회피 레이어를 지정합니다.

다른 에이전트의 collidesWith 마스크가 이 에이전트를 피할지 여부를 결정합니다.

참조: 이 설정은 Pathfinding.RVO.RVOController.layer 멤버에 해당합니다.



Collides with
이 에이전트가 피할 레이어 마스크를 지정합니다.

다음과 같이 설정할 수 있습니다: `CollidesWith = RVOLayer.DefaultAgent | RVOLayer.Layer3 | RVOLayer.Layer6 ...`

이것은 여러 팀이 있는 게임에서 매우 유용합니다. 예를 들어, 한 팀의 에이전트들은 서로를 피해야 하지만 적을 피하지 않기를 원할 수 있습니다.

이 필드는 이 에이전트가 피할 다른 에이전트에만 영향을 미치며, 다른 에이전트가 이 에이전트에 어떻게 반응하는지에는 영향을 미치지 않습니다.

참조:
Bitmask Tutorial
http://en.wikipedia.org/wiki/Mask_(computing)
참조: 이 설정은  Pathfinding.RVO.RVOController.collidesWith 멤버에 해당합니다.



Priority
다른 에이전트가 이 에이전트를 얼마나 강하게 피할지를 설정합니다.

일반적으로 0과 1 사이의 값을 가집니다. 비슷한 우선순위를 가진 에이전트들은 서로를 동일한 강도로 피합니다. 만약 에이전트가 자신보다 높은 우선순위를 가진 다른 에이전트를 보면, 그 에이전트를 더 강하게 피합니다. 극단적인 경우 (예: 이 에이전트의 우선순위가 0이고 다른 에이전트의 우선순위가 1인 경우) 그 다른 에이전트를 움직이는 장애물로 취급합니다. 반대로 에이전트가 자신보다 낮은 우선순위를 가진 다른 에이전트를 보면, 그 에이전트를 덜 피합니다.

일반적으로 이 에이전트의 회피 강도는 다음과 같습니다:

if this.priority > 0 or other.priority > 0:
    avoidanceStrength = other.priority / (this.priority + other.priority);
else:
    avoidanceStrength = 0.5

 

참조: 이 설정은 Pathfinding.RVO.RVOController.priority 멤버에 해당합니다.



Lock When Not Moving
원하는 속도가 거의 0일 때 자동으로 locked (true)으로 설정합니다.

이것은 다른 유닛들이 이 유닛을 밀어내는 것을 방지합니다 (예: 좁은 통로를 막을 때).

이 설정이 true일 때  SetTarget 이나  Move 를 호출하면, 원하는 속도가 0이 아니면  locked 필드가 true로, 0이면 false로 설정됩니다.

참조: 이 설정은  Pathfinding.RVO.RVOController.lockWhenNotMoving 멤버에 해당합니다.



Locked
잠긴 유닛은 움직일 수 없습니다.

다른 유닛들은 여전히 이 유닛을 피하지만, 회피 품질은 최적이 아닙니다.

참조: 이 설정은 Pathfinding.RVO.RVOController.locked 멤버에 해당합니다.



Debug
장면 뷰에서 디버그 정보를 그리도록 설정합니다.

참조: 이 설정은 Pathfinding.RVO.RVOController.debug 멤버에 해당합니다.




Integration with physics

종종 에이전트에 콜라이더를 추가하고 싶을 수 있습니다, 예를 들어 총알에 맞게 하기 위해서입니다. 하지만 단순히 에이전트에 콜라이더(와 리지드바디)를 추가하면, 혼잡한 시나리오에서 로컬 회피 품질이 크게 떨어질 수 있습니다. 이는 매우 혼잡할 때 에이전트들이 약간 겹칠 수 있기 때문인데, 물리 시스템은 이를 방지하지만 로컬 회피 시스템만큼 효율적으로 하지 못해 더 나쁜 움직임을 초래할 수 있습니다.

에이전트에 콜라이더가 있는 경우, 에이전트 간의 충돌을 비활성화하는 것을 권장합니다. 에이전트를 별도의 레이어에 배치하고, Unity 물리 설정에서 해당 레이어와의 충돌을 비활성화할 수 있습니다. Unity에는 2D와 3D 물리에 대한 별도의 설정이 있으므로 올바른 설정을 변경해야 합니다.

 

Keeping the agents on the navmesh

로컬 회피를 사용할 때 에이전트가 다른 에이전트를 그래프 밖으로 밀어내는 경우를 쉽게 발견할 수 있습니다. 이는 일반적으로 바람직하지 않지만, 다행히도 이를 완화할 방법이 있습니다.

RVOSimulator.useNavmeshAsObstacle  옵션을 활성화하여 로컬 회피 시스템이 네브메쉬를 장애물로 처리하도록 할 수 있습니다. 이는 성능에 영향을 미치지만, 에이전트가 그래프 밖으로 밀려날 가능성을 크게 줄입니다.

에이전트는 로컬 회피를 사용하지 않고도 그래프에 자신을 제한할 수 있습니다.  RichAIFollowerEntity 이동 스크립트를 사용하는 경우, 이동 처리 방식 때문에 자동으로 그래프에 제한됩니다.  AIPath 스크립트를 사용하는 경우  constrainInsideGraph 옵션을 사용하여 이를 활성화할 수 있습니다. 그러나 로컬 회피 에이전트가 네브메쉬를 인식하지 않는 경우( RVOSimulator.useNavmeshAsObstacle 옵션 사용), 그래프 밖으로 이동하려고 시도할 수 있으며, 이는 그래프 가장자리에서 불안정한 움직임을 초래할 수 있습니다. 특히 혼잡한 시나리오에서 그러합니다.

아래 이미지는 여러 에이전트가 로컬 회피를 사용하여 코너의 동일한 지점으로 이동하도록 명령받았을 때의 상황을 보여줍니다. 에이전트를 그래프에 제한하지 않으면 쉽게 장애물 안으로 밀려나는 것을 볼 수 있습니다.


Performance

로컬 회피의 성능은 매우 높습니다. 특히 로컬 회피 시뮬레이션이 매우 높은 FPS로 실행될 필요가 없다는 점을 고려하면 더욱 그렇습니다. 높은 FPS로 실행해도 품질이 크게 향상되지 않기 때문에 CPU 사이클의 낭비일 뿐입니다. 저의 경우, 상당히 강력한 컴퓨터(i9 프로세서, 16코어)에서 30,000개의 에이전트를 좋은 FPS로 시뮬레이션할 수 있었습니다. 로컬 회피 시뮬레이션은 고정된 60 FPS로 실행되었고, 게임은 몇 백 FPS로 실행되었으며, 최저 FPS는 약 60이었습니다. 이 시뮬레이션의 시각화는 각 에이전트에 대한 하나의 사각형을 포함한 메쉬를 생성함으로써 단순하게 처리되었습니다. 이렇게 많은 숫자의 게임 오브젝트를 생성하는 것은 매우 느리기 때문에, 각 에이전트에 대해 게임 오브젝트를 생성하는 것은 적합하지 않았습니다. 테스트했을 때 이렇게 많은 게임 오브젝트를 생성하는 데 약 10초가 소요되었습니다. 에이전트는 원형으로 배치되었으며, 각 에이전트는 원의 반대편 지점으로 이동하려고 시도했습니다. 따라서 가능한 한 혼잡한 상황이었습니다.

그러나 게임에서 이처럼 많은 수의 에이전트를 가질 수 있다고 기대하지 마세요. 위의 테스트는 매우 경량이었고, 실제 게임에서는 많은 다른 요소들로 인해 많은 오버헤드가 발생합니다.

위의 이미지는 앞서 언급한 스트레스 테스트를 보여줍니다.

참조
example_local_avoidance_lightweight

 

Local Avoidance Priorities

모든 에이전트가 동일하지만, 어떤 에이전트는 더 중요할 수 있습니다. 여기서 우선순위가 등장합니다. 에이전트가 더 높은 우선순위를 가지면, 다른 에이전트는 이를 더 피하려고 시도하고, 해당 에이전트는 다른 에이전트를 덜 피하려고 시도합니다.

일반적으로, 한 에이전트가 우선순위 A를 가지고 있고 다른 에이전트가 우선순위 B를 가지고 있다면, A는 충돌을 피하기 위한 책임의 이 비율을 차지합니다:

B / (A + B)

예를 들어, A가 우선순위 1을 가지고 있고 B가 우선순위 2를 가지고 있다면, A는 충돌 회피 책임의 2/3를 차지하고 B는 1/3를 차지하게 됩니다.

Local Avoidance Layers

에이전트 레이어와 회피할 레이어를 변경하여 일부 에이전트가 다른 캐릭터를 완전히 무시하도록 할 수 있습니다. 각 RVOController 에는 자신의 레이어와 다른 에이전트를 회피할 때 고려할 레이어를 지정하는 필드가 있습니다. 이 레이어는 Unity 레이어와는 다릅니다. 이들은 순수하게 로컬 회피에만 사용됩니다.

Layer

이 에이전트의 회피 레이어를 지정합니다.

Collides With

이 에이전트가 회피할 레이어 마스크를 지정합니다.

두 팀이 있는 게임을 만들 때 매우 유용합니다. 이런 경우에는 서로 다른 팀의 에이전트들이 서로를 회피하지 않도록 설정하고 싶을 수 있습니다. 팀 A의 모든 에이전트를 한 레이어에, 팀 B의 모든 에이전트를 다른 레이어에 배치한 다음, 에이전트가 자신의 팀 레이어에 있는 에이전트만 회피하도록 설정할 수 있습니다.

Player Characters

플레이어 캐릭터가 로컬 회피의 영향을 받지 않도록 설정하고 싶지만, 다른 에이전트가 플레이어를 회피하도록 하고 싶을 수 있습니다. 이는 RVOController 컴포넌트의 속도를 수동으로 설정하여 할 수 있습니다.

void Update () {
    var x = Input.GetAxis("Horizontal");
    var y = Input.GetAxis("Vertical");

    var v = new Vector3(x, 0, y) * speed;

    // RVOController의 속도를 재정의합니다. 이렇게 하면 한 시뮬레이션 단계 동안 로컬 회피 계산이 비활성화됩니다.
    rvo.velocity = v;
    transform.position += v * Time.deltaTime;
}

이렇게 하면 로컬 회피 시스템이 해당 에이전트에 대한 계산을 완전히 건너뛰고, 대신 설정한 속도로 이동한다고 가정합니다. 이는 장애물이 작아서 경로 탐색이 필요하지 않으며, 원형이어도 괜찮은 경우 이동 장애물에도 유용할 수 있습니다.

ManualRVOAgent.cs 라는 포함된 샘플 스크립트가 있으며, 이를 통해 기존의 캐릭터 이동 스크립트를 로컬 회피와 통합하는 방법을 보여줍니다.

Modifiers는 작은 스크립트로서 Seeker에 연결되어 경로가 호출자에게 반환되기 전이나 후에 경로를 전처리 및 후처리할 수 있습니다.

Modifiers는 Seeker 컴포넌트가 부착된 동일한 GameObject에 스크립트를 부착하기만 하면 사용 가능합니다. 코드를 수정하지 않아도 자동으로 작동합니다.

예를 들어, Raycast Modifier는 경로에서 불필요한 노드를 제거하여 더 짧지만 여전히 유효한 경로를 만들 수 있습니다. 또는 Smooth Modifier는 경로를 세분화하고 부드럽게 하여 더 미적으로 매력적으로 보이게 합니다.

아래는 그리드 그래프에서 가장 일반적인 Modifier 유형/Modifier 조합의 비교입니다.

 

SimpleSmoothModifier
RaycastModifier
FunnelModifier
RadiusModifier
StartEndModifier
AlternativePath
Modifiers
참조
참고
모든 이동 스크립트가 Modifiers를 사용하는 것은 아닙니다. AIPath와 AILerp 이동 스크립트는 모든 Modifiers를 지원하지만, RichAI 이동 스크립트는 대부분의 Modifiers를 무시합니다 (경로를 부드럽게 하거나 단순화하는 모든 Modifier를 무시합니다). 그리고  FollowerEntity 이동 스크립트는 Modifiers를 전혀 사용하지 않습니다. RichAI와 FollowerEntity 스크립트는 자체 경로 평활화 코드를 포함하고 있습니다. RichAI 이동 스크립트를 사용할 때도 다른 Modifiers를 부착할 수 있습니다.

 

Simple Smooth Modifier

simple smooth modifier 는 경로를 부드럽게 하기 위한 Modifier입니다. 이는 경로를 세분화하고 정점을 서로 가까이 이동시키거나 스플라인, 즉 베지어 곡선을 사용할 수 있습니다.
아래는 Smooth Modifier를 껐을 때와 켰을 때의 경로를 보여주는 두 개의 이미지입니다. 부드러워진 경로가 코너를 약간 자르는 것을 볼 수 있는데, 이는 Smooth Modifier가 경로를 부드럽게 할 때 월드 지형을 고려하지 않기 때문에 발생할 수 있는 문제입니다. 

변수 문서는 Simple Smooth Modifier 문서를 참조하세요.

Funnel Modifier

funnel modifier는 내브메쉬(navmeshes)나 그리드 그래프에서 경로를 간단하게 만드는 Modifier입니다. 이름에서 알 수 있듯이 이 Modifier는 경로에 깔때기 알고리즘을 적용합니다. 이는 경로 탐색기가 계산한 경로 통로 내부에서 가장 짧은 경로를 반환합니다.

"Modules"를 클릭하고 "Modifiers"로 이동하면 모든 Modifiers의 목록을 볼 수 있습니다.

Raycast Modifier

raycast modifier 는 일련의 직선 검사를 통해 경로를 간단하게 만듭니다. 이 Modifier는 주로 그리드 그래프에서 유용하지만, 게임에 따라 점 그래프를 사용할 때도 유용할 수 있습니다.

 

Custom modifiers

사용자 정의 경로 Modifier를 작성하는 것도 가능합니다.

Writing Modifiers 참조

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

Local Avoidance > Custom Local Avoidance Movement Script  (0) 2024.05.25
Local Avoidance  (0) 2024.05.25
Searching for paths  (0) 2024.05.24
Movement scripts > Writing a movement script  (0) 2024.05.24
Movement scripts  (0) 2024.05.23

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

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