종종 세계의 한 지점에 가장 가까운 노드가 무엇인지 알아내고 싶을 때가 있습니다. 기본 이동 스크립트를 사용하는 경우 노드와 전혀 상관없지만, 더 깊이 들어가면 매우 유용해집니다.
// 이 게임 오브젝트의 위치에 가장 가까운 노드를 찾습니다.
GraphNode node = AstarPath.active.GetNearest(transform.position).node;
if (node.Walkable) {
// 와우, 노드가 이동 가능하네요. 여기다 타워를 놓을 수 있습니다.
}
특정 노드만 가져와야 하는 경우도 있습니다. 예를 들어 "이 위치에서 가장 가까운 *이동 가능한* 노드는 무엇인가?"와 같은 경우입니다. 이는 NNConstraint 클래스를 사용하여 해결할 수 있습니다. 이 경우 Default NNConstraint를 사용할 수 있습니다(기본이라고 불리는 이유는 다른 것이 지정되지 않은 경우 경로 호출에 사용되기 때문입니다).
var constraint = NNConstraint.None;
// 검색을 이동 가능한 노드로만 제한합니다.
constraint.constrainWalkability = true;
constraint.walkable = true;
// 검색을 태그 3 또는 태그 5가 있는 노드로만 제한합니다.
// 'tags' 필드는 비트마스크입니다.
constraint.constrainTags = true;
constraint.tags = (1 << 3) | (1 << 5);
var info = AstarPath.active.GetNearest(transform.position, constraint);
var node = info.node;
var closestPoint = info.position;
이러한 제약 조건의 검색 범위는 무한하지 않지만 상당히 큽니다. 최대 거리는 A* Inspector -> Settings -> Max Nearest Node Distance 또는 AstarPath.maxNearestNodeDistance 에서 설정할 수 있습니다. 가장 가까운 노드가 'Max Nearest Node Distance'보다 멀리 있으면 `GetNearest` 메서드는 null 노드를 반환합니다.
위에서 `GetNearest` 호출로부터 노드를 가져오기 위해 `node` 필드에 접근해야 한다는 점을 눈치챘을 것입니다. `GetNearest` 메서드는 NNInfo 구조체를 반환하며, 이 구조체에는 두 개의 필드가 있습니다. 첫 번째는 node 필드로, 가장 적합한 노드를 포함하고 있습니다(노드를 찾을 수 없는 경우 null이 됩니다). 두 번째는 position 필드로, 쿼리 지점에서 해당 노드의 가장 가까운 점을 포함하고 있습니다. 그리드 그래프에서는 노드를 사각형으로 간주하므로 `position` 필드는 쿼리 지점에서 해당 사각형 내의 가장 가까운 점을 포함하게 됩니다. 내브메시 기반 그래프(RecastGraph 및 NavMeshGraph)에서는 `position` 필드가 가장 가까운 삼각형에서 가장 가까운 점을 포함하게 됩니다. 포인트 노드는 표면이 없으므로 `position`은 노드의 위치로 설정됩니다.
var info = AstarPath.active.GetNearest(transform.position);
var node = info.node;
var closestPoint = info.position;
Node connections
각 노드는 연결된 다른 노드를 저장합니다. 이것이 표현되는 방식은 그래프에 따라 다릅니다. 예를 들어, 그리드 그래프는 그리드 구조를 활용하여 인접한 그리드 노드들에 대한 모든 연결을 단 하나의 바이트로 저장할 수 있습니다! 이는 예를 들어 다른 노드들에 대한 참조 배열을 사용하는 것보다 최소 80배 더 메모리 효율적입니다.
노드의 모든 연결에 접근하려면 GetConnection 메서드를 사용할 수 있습니다. 이 메서드는 각 연결된 노드에 대해 호출되는 대리자를 매개변수로 받으며, 기본 표현을 추상화합니다.
GraphNode node = ...;
// 연결된 모든 노드에 선을 그리세요.
node.GetConnections(otherNode => {
Debug.DrawLine((Vector3)node.position, (Vector3)otherNode.position);
});
// 또는 결합하여 얻을 수 있습니다(더 빠름).
Int3 v0, v1, v2;
node.GetVertices(out v0, out v1, out v2);
Reachability
종종 캐릭터가 특정 노드에 도달할 수 있는지 여부를 판단하는 것이 유용합니다. 경로가 계산될 때 기본적으로 도달할 수 있는 대상에 가장 가까운 노드로 이동하려고 한다는 점을 유념하세요(앞서 논의한 AstarPath.maxNearestNodeDistance 까지의 최대 거리). 따라서 종종 이에 대해 걱정할 필요가 없습니다.
경로 찾기 시스템은 그래프의 연 connected components 를 계산하여 어떤 노드에서 다른 노드로 도달할 수 있는지 미리 계산합니다. 이는 장면 뷰에서 다양한 색상으로 표시됩니다(그래프 색상 옵션이 'Areas'로 설정된 경우). 각 노드의 Area 필드는 해당 연결된 구성 요소의 인덱스로 설정됩니다. 두 노드가 동일한 영역을 가지고 있다면, 그 사이에 유효한 경로가 있음을 의미합니다. 이를 PathUtilities.IsPathPossible 메서드를 사용하여 확인할 수도 있습니다.
var node1 = AstarPath.active.GetNearest(somePoint1);
var node2 = AstarPath.active.GetNearest(somePoint2);
if (PathUtilities.IsPathPossible(node1, node2)) {
// 노드들 사이에 유효한 경로가 있습니다.
}
이 영역 또는 연결된 구성 요소를 계산하는 과정을 문서와 코드의 여러 부분에서는 'flood filling'이라고 합니다.
또한 IsPathPossible 메서드에 태그 마스크나 노드 목록을 제공할 수도 있습니다.
때로는 스캐닝 프로세스를 어떤 방식으로든 수정하고 싶을 때가 있습니다. 예를 들어, 게임에 자체적인 노드 이동 가능 규칙이 있고 이를 그리드 그래프와 동기화하고 싶을 수 있습니다. 그래프가 스캔된 후 모든 노드를 반복하며 원하는 수정을 적용할 수도 있지만, 이는 복잡성을 추가하고 그래프 업데이트와 잘 맞지 않습니다.
그리드 그래프 규칙으로 로직을 작성하면 그래프 업데이트 및 ProceduralGraphMover 와 같은 스크립트와 원활하게 작동합니다. 또한 유니티 작업 시스템과 Burst 컴파일러를 사용하여 코드를 크게 가속화할 수 있습니다. 또한 그리드에서 어떤 연결이 유효하고 어떤 연결이 유효하지 않은지에 대한 필터를 쉽게 만들 수 있는 몇 가지 도우미 메서드도 포함되어 있습니다.
How a rule works conceptually(규칙이 개념적으로 작동하는 방식)
규칙은 개념적으로 다음과 같이 작동합니다:
규칙은 자체 등록을 하고 스캐닝 프로세스에 훅을 겁니다.
그래프가 스캔되거나 업데이트될 때, 해당 훅을 호출합니다.
훅 내부에서 규칙은 그래프 데이터를 원하는 대로 수정할 수 있습니다.
규칙은 스캐닝 프로세스의 다른 지점에 훅을 걸도록 등록할 수 있습니다. 이러한 지점을 패스라고 합니다. 대부분의 규칙은 단일 패스만 사용하지만, 원한다면 여러 개를 등록할 수도 있습니다.
이 단계는 매우 초기 단계로, 이 시점에서는 대부분의 데이터가 유효하지 않습니다. 이 패스를 사용하면 노드 위치를 수정하고 충돌 테스트 코드에서 이를 반영할 수 있습니다.
BeforeConnections
연결이 계산되기 이전에 실행됩니다.
이 시점에서는 높이 테스트와 충돌 테스트가 완료된 상태입니다(둘 다 활성화된 경우). 이는 가장 일반적으로 사용되는 패스입니다. 이 지점에서 이동 가능성을 수정하면 연결과 침식이 올바르게 계산됩니다.
AfterConnections
연결이 계산된 후에 실행됩니다.
연결을 직접 수정하려면 이 패스에서 수행해야 합니다.
참고 침식이 사용되는 경우 이 패스는 두 번 실행됩니다. 한 번은 침식 이전, 다른 한 번은 침식 이후 연결이 다시 계산될 때 실행됩니다.
AfterErosion
침식이 계산된 후, 연결이 다시 계산되기 이전에 실행됩니다.
침식이 사용되지 않는 경우 이 패스는 실행되지 않습니다.
PostProcess
모든 작업이 끝난 후에 실행됩니다.
이 패스는 모든 작업이 완료된 후에 실행됩니다. 이 패스에서 이동 가능성을 수정해서는 안 됩니다. 이 경우 노드 연결이 최신 상태가 아니게 됩니다.
AfterApplied
그래프 업데이트가 그래프에 적용된 후에 실행됩니다.
이 패스는 메인 스레드 패스로만 추가할 수 있습니다.
경고 이 시점에서는 컨텍스트의 네이티브 데이터가 유효하지 않습니다. 모든 데이터가 삭제된 상태입니다. 이 패스에서는 데이터를 수정할 수 없습니다.
이 설정은 Pathfinding.Graphs.Grid.Rules.GridGraphRule.Pass 멤버에 해당합니다.
규칙은 그래프를 스캔할 때와 그래프가 업데이트될 때 모두 호출됩니다. 그러나 그래프가 업데이트될 때는 그리드의 더 작은 부분 사각형에 대해서만 데이터를 제공합니다. 따라서 그리드 그래프 규칙은 항상 전체 그래프에 사용된다고 가정해서는 안 되며, 이를 염두에 두어야 합니다( Data layout 참조).
규칙은 프로젝트의 어느 곳에나 배치할 수 있습니다. 동적으로 찾아지고 그리드 그래프 인스펙터에서 'Add Rule' 버튼을 사용하여 추가할 수 있습니다. 또한 인스펙터에서 설정을 변경할 수 있도록 사용자 지정 편집기 GUI를 규칙에 추가할 수도 있습니다.
Unity Job System or Main Thread
규칙을 실행하는 방법에는 두 가지 옵션이 있습니다:
메인 스레드에서 실행합니다. 이 방법은 더 쉽고 보일러플레이트 코드가 적으며, 규칙에서 스레드에 안전하지 않은 데이터에 접근할 수 있습니다.
유니티 작업 시스템을 사용하여 실행합니다. 이 방법은 추가적인 보일러플레이트 코드가 있지만, 작업이 다른 그리드 그래프 스캔 코드와 병렬로 실행될 수 있으며 Burst 컴파일러를 사용할 수 있습니다. 이를 통해 매우 빠른 작업을 작성할 수 있습니다.
처음 작업을 작성할 때는 메인 스레드에서 실행되는 작업을 작성하는 것을 권장하며, 성능이 필요할 경우 작업 시스템으로 전환할 수 있습니다.
다음 섹션에서는 몇 가지 규칙 예제를 보여드립니다.
Writing a simple example rule
노이즈 함수를 기반으로 노드의 이동 가능성을 설정하는 간단한 규칙을 작성해보겠습니다. 결과는 다음과 같을 것입니다:
이를 수행하기 위해 모든 노드를 반복하면서 이동 가능성을 설정해야 합니다.
이를 수행하기 위해 모든 노드를 반복하면서 이동 가능성을 설정해야 합니다.
using UnityEngine;
using Pathfinding;
using Pathfinding.Graphs.Grid.Rules;
// Preserve 속성으로 표시하여 바이트코드 스트리핑이 사용될 때 이 클래스가 제거되지 않도록 합니다. 자세한 내용은 https://docs.unity3d.com/Manual/IL2CPP-BytecodeStripping.html 참조
[Pathfinding.Util.Preserve]
public class RuleExampleNodes : GridGraphRule {
public float perlinNoiseScale = 10.0f;
public float perlinNoiseThreshold = 0.4f;
public override void Register (GridGraphRules rules) {
// Register 메서드는 규칙이 처음 사용될 때 한 번 호출되고,
// 인스펙터에서 규칙 설정이 변경될 때 다시 호출됩니다.
// 필요한 경우 이 부분에서 사전 계산을 수행합니다.
// 그리드 그래프의 계산 코드에 훅을 겁니다
rules.AddMainThreadPass(Pass.BeforeConnections, context => {
// 이 콜백은 그래프를 스캔할 때와 그래프 업데이트 중에 호출됩니다.
// 여기서 원하는 대로 그래프 데이터를 수정할 수 있습니다.
// context.data 객체는 모든 노드 데이터를 NativeArrays로 포함하고 있습니다.
// 모든 데이터가 모든 패스에서 유효한 것은 아니며, 특정 시간에 계산되지 않았을 수 있습니다.
// GridGraphScanData 객체에 대한 문서에서 이에 대한 자세한 정보를 찾을 수 있습니다.
// 필요한 데이터 배열을 가져옵니다
var nodeWalkable = context.data.nodes.walkable;
var nodePositions = context.data.nodes.positions;
// 모든 노드를 반복하면서 일부 페를린 노이즈에 따라 이동 가능하거나 불가능하도록 표시합니다
for (int i = 0; i < nodePositions.Length; i++) {
var position = nodePositions[i];
nodeWalkable[i] &= Mathf.PerlinNoise(position.x / perlinNoiseScale, position.z / perlinNoiseScale) > perlinNoiseThreshold;
}
});
}
}
확인이 필요하다면 규칙이 실행될 때 콜백에서 그래프에 대한 많은 데이터를 포함하는 context 객체를 받게 됩니다. 사용할 수 있는 데이터에 대한 자세한 내용은 GridGraphRules.Context 클래스를 참조하십시오. 특히, 이 클래스의 data 필드는 GridGraphScanData 클래스의 인스턴스입니다.
이 코드는 프로젝트의 어느 곳에나 배치할 수 있으며, GridGraph 인스펙터에서 "Add Rule" 버튼을 클릭하면 표시됩니다. 모든 규칙에는 설정을 표시할 수 있는 대응되는 에디터 스크립트가 필요합니다. 이 스크립트는 Editor folder 에 배치해야 합니다.
using Pathfinding.Graphs.Grid.Rules;
using UnityEditor;
using UnityEngine;
namespace Pathfinding {
[CustomGridGraphRuleEditor(typeof(RuleExampleNodes), "Simple Example Rule")]
public class RuleExampleNodesEditor : IGridGraphRuleEditor {
public void OnInspectorGUI (GridGraph graph, GridGraphRule rule) {
var target = rule as RuleExampleNodes;
target.perlinNoiseScale = EditorGUILayout.FloatField("Noise Scale", target.perlinNoiseScale);
target.perlinNoiseThreshold = EditorGUILayout.FloatField("Noise Threshold", target.perlinNoiseThreshold);
}
public void OnSceneGUI (GridGraph graph, GridGraphRule rule) { }
}
}
이제 규칙이 작동할 것입니다! 아래에서 이 간단한 규칙이 어떻게 작동하는지 보여주는 비디오를 볼 수 있습니다.
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using Unity.Mathematics;
using Pathfinding;
using Pathfinding.Jobs;
using Pathfinding.Graphs.Grid;
using Pathfinding.Graphs.Grid.Rules;
[Pathfinding.Util.Preserve]
public class RuleExampleNodesBurst : GridGraphRule {
public float perlinNoiseScale = 10.0f;
public float perlinNoiseThreshold = 0.4f;
public override void Register (GridGraphRules rules) {
// 여기서 규칙을 메인 스레드에서 실행하는 대신 작업 시스템을 사용합니다.
// 이 경우 콜백 내부에서는 작업을 예약하기만 해야 합니다.
// 콜백에서 데이터를 직접 접근하는 것은 안전하지 않습니다.
rules.AddJobSystemPass(Pass.BeforeConnections, context => {
// 이 콜백은 그래프를 스캔할 때와 그래프 업데이트 중에 호출됩니다.
// 우리가 해야 할 일은 유니티 작업 시스템을 사용하여 작업을 예약하는 것뿐입니다.
// 종속성을 직접 처리할 필요는 없습니다.
// 적절한 [ReadOnly] 또는 [WriteOnly] 태그를 사용하는지 확인하십시오.
// 자세한 내용은 https://docs.unity3d.com/Manual/JobSystem.html 를 참조하십시오.
// context.data 객체는 모든 노드 데이터를 NativeArrays로 포함하고 있습니다.
new JobExample {
bounds = context.data.nodes.bounds,
nodeWalkable = context.data.nodes.walkable,
nodePositions = context.data.nodes.positions,
nodeNormals = context.data.nodes.normals,
perlinNoiseScale = perlinNoiseScale,
perlinNoiseThreshold = perlinNoiseThreshold,
}.Schedule(context.tracker);
});
}
[BurstCompile]
struct JobExample : IJob, GridIterationUtilities.INodeModifier {
public IntBounds bounds;
public float perlinNoiseScale;
public float perlinNoiseThreshold;
public NativeArray<bool> nodeWalkable;
[ReadOnly]
public NativeArray<Vector3> nodePositions;
[ReadOnly]
public NativeArray<float4> nodeNormals;
public void Execute () {
// 계산 중인 모든 노드를 효율적으로 반복하는 데 사용됩니다.
// ForEachNode 함수는 nodeNormals 배열을 사용하여 노드가 존재하는지 확인합니다.
// 노드가 존재하지 않는 경우 노말은 (0,0,0)입니다 (중요: 계층 그리드 그래프).
GridIterationUtilities.ForEachNode(bounds.size, nodeNormals, ref this);
}
public void ModifyNode (int dataIndex, int dataX, int dataLayer, int dataZ) {
var position = nodePositions[dataIndex];
nodeWalkable[dataIndex] &= Mathf.PerlinNoise(position.x / perlinNoiseScale, position.z / perlinNoiseScale) > perlinNoiseThreshold;
}
}
}
주의사항 일반적으로 유니티 작업 시스템을 사용할 때는 작업의 종속성을 수동으로 지정해야 합니다. 이는 번거롭고 오류가 발생하기 쉽기 때문에, 작업 구조체의 필드에 사용한 [ReadOnly] 및 [WriteOnly] 속성을 읽고 종속성을 자동으로 계산해주는 도우미 스크립트를 사용합니다. 자세한 내용은 JobDependencyTracker 클래스를 참조하십시오.
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using Unity.Mathematics;
using Pathfinding;
using Pathfinding.Jobs;
using Pathfinding.Graphs.Grid.Rules;
using Pathfinding.Graphs.Grid;
[Pathfinding.Util.Preserve]
public class RuleExampleConnection : GridGraphRule {
public override void Register (GridGraphRules rules) {
rules.AddJobSystemPass(Pass.AfterConnections, context => {
new JobExample {
bounds = context.data.nodes.bounds,
nodeConnections = context.data.nodes.connections,
nodeWalkable = context.data.nodes.walkable,
nodePositions = context.data.nodes.positions,
layeredDataLayout = context.data.nodes.layeredDataLayout,
}.Schedule(context.tracker);
});
}
[BurstCompile]
struct JobExample : IJob, GridIterationUtilities.IConnectionFilter {
public IntBounds bounds;
public NativeArray<ulong> nodeConnections;
[WriteOnly]
public NativeArray<bool> nodeWalkable;
[ReadOnly]
public NativeArray<Vector3> nodePositions;
public bool layeredDataLayout;
public void Execute () {
GridIterationUtilities.FilterNodeConnections(bounds, nodeConnections, layeredDataLayout, ref this);
}
public bool IsValidConnection (int dataIndex, int dataX, int dataLayer, int dataZ, int direction, int neighbourDataIndex) {
// 현재 노드와 인접 노드의 위치를 가져옵니다.
var position = nodePositions[dataIndex];
var neighbourPosition = nodePositions[neighbourDataIndex];
// 인접 노드가 이 노드와 Y 좌표가 1미터 이하로 다를 경우에만 연결을 허용합니다.
// 이는 GridGraph 설정의 "Max Climb"와 유사합니다.
return math.abs(position.y - neighbourPosition.y) < 1.0f;
}
}
}
노드가 메모리에 어떻게 배치되는지에 대한 자세한 내용은 Data layout 을 참조하십시오.
IsValidConnection 메서드는 업데이트되는 모든 연결에 대해 호출됩니다. 이 메서드는 스캐닝 프로세스에서 해당 시점에 유효한 연결에 대해서만 호출됩니다. 이는 이전에 비활성화된 연결을 다시 활성화할 수 없다는 것을 의미합니다.
Data layout
그리드 그래프가 스캔되거나 업데이트될 때, 모든 임시 데이터는 NativeArrays에 저장됩니다. 그리드 그래프의 작은 부분만 업데이트되는 경우, 이러한 임시 배열은 전체 그래프보다 작으므로 일반 노드와 동일한 인덱스로 접근할 수 없습니다.
참고 코드에서는 이 그래프 업데이트/스캔에 사용된 NativeArrays에서 x 및 z 좌표를 나타내기 위해 dataX와 dataZ라는 명명 규칙을 사용합니다.
dataX 와 dataZ 는 실제 그래프의 x 및 z 좌표와 일치하지 않을 수 있습니다. 이는 그래프 업데이트가 일반적으로 그래프의 작은 부분에만 영향을 미치기 때문입니다. 다음과 같이 이를 그리드의 x 및 z 좌표로 변환할 수 있습니다:
때때로 런타임 중에 그래프를 업데이트해야 할 필요가 있을 수 있습니다. 예를 들어, 플레이어가 새로운 건물을 건설했거나 문이 열렸을 때가 그렇습니다. 그래프는 완전히 다시 계산하거나 부분적으로 업데이트할 수 있습니다. 전체 재계산은 전체 맵을 변경한 경우에 좋지만, 그래프의 일부분만 변경해야 할 경우에는 작은 업데이트가 훨씬 빠르게 수행될 수 있습니다.
그래프를 업데이트하는 방법은 많고, 이를 원하는 다양한 시나리오가 있습니다. 여기 가장 일반적인 경우를 요약한 표가 있습니다. 게임마다 매우 다르므로, 이 목록에 없는 해결책이 여러분의 게임에 적합할 수도 있다는 점을 염두에 두세요.
초기 생성 시 사용된 설정과 동일한 설정으로 그래프를 재계산하고 싶을 수 있습니다. 그러나 그래프의 작은 영역만 업데이트한 경우, 전체 그래프를 재계산하는 것은 비효율적일 수 있습니다 ( AstarPath.Scan 사용). 예를 들어, 타워 디펜스 게임에서 플레이어가 새로운 타워를 배치한 경우입니다.
기존 그래프의 일부 설정을 변경하고 싶을 수 있습니다. 예를 들어, 일부 노드의 태그나 패널티를 변경하고 싶을 수 있습니다.
그래프의 작은 부분을 초기 생성 시와 동일한 방식으로 재계산하는 것은 NavMeshGraph를 제외한 모든 그래프에서 가능합니다. NavMeshGraph는 보통 완전히 재계산하는 것이 더 의미가 있습니다. 스크립트와 GraphUpdateScene 컴포넌트를 사용하여 "updatePhysics" 필드를 true로 설정하면 이 작업을 수행할 수 있습니다. 이 이름이 가장 설명적이지는 않아서 죄송합니다.
그리드 그래프는 예상대로 작동합니다. 경계를 지정하기만 하면 모든 작업을 자동으로 수행합니다. 그러나 침식 등 요소를 제대로 반영하기 위해 지정한 영역보다 약간 더 큰 영역을 재계산할 수 있습니다.
리캐스트 그래프는 한 번에 하나의 타일만 재계산할 수 있습니다. 따라서 업데이트 요청이 있을 때 경계에 닿는 모든 타일이 완전히 재계산됩니다. 따라서 재계산 시간이 너무 길어지는 것을 피하기 위해 타일 크기를 작게 사용하는 것이 좋습니다. 그러나 너무 작게 설정하면 본질적으로 그리드 그래프가 됩니다. 멀티스레딩을 사용하는 경우 타일 재계산의 상당 부분이 별도의 스레드로 오프로드되어 FPS에 미치는 영향을 줄일 수 있습니다.
포인트 그래프는 경계를 통과하는 모든 연결을 재계산합니다. 그러나 새로 추가된 노드를 GameObject로 인식하지는 않습니다. 이를 위해서는 AstarPath.Scan 을 사용해야 합니다.
기존 그래프의 속성을 변경하려면 여러 가지를 변경할 수 있습니다.
노드의 태그를 변경할 수 있습니다. 이를 통해 일부 유닛은 특정 지역을 통과할 수 있게 하고, 다른 유닛은 통과하지 못하게 할 수 있습니다. 태그에 대한 자세한 내용은 여기에서 확인할 수 있습니다: Working with tags.
노드의 패널티를 변경할 수 있습니다. 이를 통해 일부 노드를 다른 노드보다 더 어렵거나 느리게 통과하도록 하여, 에이전트가 특정 경로를 더 선호하게 만들 수 있습니다. 하지만 몇 가지 제한 사항이 있습니다. 사용되는 알고리즘이 음수 패널티를 처리할 수 없으므로 음수 패널티를 지정할 수 없습니다(처리할 수 있다고 해도 시스템은 훨씬 느려질 것입니다). 일반적인 트릭은 매우 큰 초기 패널티를 설정한 후(그래프 설정에서 가능) 그 높은 값에서 패널티를 줄이는 것입니다. 그러나 이렇게 하면 더 많은 노드를 검색해야 하므로 경로 탐색이 전반적으로 느려집니다. 필요한 패널티 값은 상당히 높습니다. 실제 "패널티 단위"는 없지만, 1000의 패널티는 대략 한 월드 유닛의 이동에 해당합니다.
노드의 통행 가능성도 직접 수정할 수 있습니다. 따라서 특정 경계 내의 모든 노드를 모두 통행 가능하게 하거나 모두 통행 불가능하게 만들 수 있습니다.
GraphUpdateScene component를 사용하는 방법에 대한 정보는 해당 클래스의 문서를 참조하십시오. GraphUpdateScene 컴포넌트 설정은 스크립트를 사용하여 그래프를 업데이트할 때 사용하는 GraphUpdateObject와 거의 1:1로 매핑됩니다. 따라서 스크립트만 사용할 예정이라도 해당 페이지를 읽는 것을 권장합니다.
Recalculating the whole graph
모든 그래프 또는 일부 그래프를 완전히 재계산하려면 다음과 같이 합니다:
// 모든 그래프를 재계산합니다.
AstarPath.active.Scan();
// 첫 번째 그리드 그래프만 재계산합니다.
var graphToScan = AstarPath.active.data.gridGraph;
AstarPath.active.Scan(graphToScan);
// 첫 번째 및 세 번째 그래프만 재계산합니다.
var graphsToScan = new [] { AstarPath.active.data.graphs[0], AstarPath.active.data.graphs[2] };
AstarPath.active.Scan(graphsToScan);
그래프를 비동기적으로 재계산할 수도 있습니다(프로 버전에서만 가능). 이는 좋은 프레임 레이트를 보장하지는 않지만, 최소한 로딩 화면을 표시할 수 있습니다.
// 예를 들어, 부착된 콜라이더의 경계 상자를 사용합니다.
Bounds bounds = GetComponent<Collider>().bounds;
AstarPath.active.UpdateGraphs(bounds);
// Pathfinding사용; //스크립트 상단에 추가
//예를 들어, 부착된 콜라이더의 경계 상자를 사용합니다.
Bounds bounds = GetComponent<Collider>().bounds;
var guo = new GraphUpdateObject(bounds);
// 일부 설정을 지정합니다.
guo.updatePhysics = true;
AstarPath.active.UpdateGraphs(guo);
이 메서드는 작업을 큐에 넣어 다음 경로 계산 전에 수행되도록 합니다. 바로 수행할 경우 경로 탐색과 충돌할 수 있으며, 특히 멀티스레딩이 활성화된 경우 다양한 오류를 초래할 수 있기 때문입니다. 따라서 그래프의 업데이트가 즉시 반영되지 않을 수 있지만, 다음 경로 탐색 계산이 시작되기 전에 항상 업데이트됩니다(거의 항상, AstarPath.limitGraphUpdates를 참조).
리캐스트 그래프는 navmesh cutting 을 사용하여 의사 업데이트(pseudo-update)할 수도 있습니다. 네비메쉬 컷팅은 네비메쉬에 장애물을 위한 구멍을 뚫을 수 있지만, 더 많은 네비메쉬 표면을 추가할 수는 없습니다. Pathfinding.NavmeshCut 을 참조하십시오. 이는 전체 리캐스트 타일을 재계산하는 것보다 훨씬 빠르지만, 제한적입니다.
GraphUpdateScene 컴포넌트를 사용하는 것이 Unity 에디터에서 알려진 그래프를 작업할 때 가장 쉽습니다. 예를 들어, 특정 영역의 태그를 코드 없이 쉽게 변경할 수 있습니다. 그러나 런타임 중에 동적으로 그래프를 업데이트하는 경우 코드로 그래프를 업데이트하는 것이 더 쉽습니다. 예를 들어, 타워 디펜스 게임에서 새로 배치된 건물을 반영하기 위해 그래프를 업데이트하는 경우가 그렇습니다.
Using Scripting
그래프를 업데이트하려면 GraphUpdateObject를 생성하고 원하는 매개변수를 설정한 다음, AstarPath.UpdateGraphs 메서드를 호출하여 업데이트를 큐에 넣습니다.
// using Pathfinding; //스크립트 상단에 추가
// 예를 들어, 부착된 콜라이더의 경계 상자를 사용합니다.
Bounds bounds = GetComponent<Collider>().bounds;
var guo = new GraphUpdateObject(bounds);
// S일부 설정을 지정합니다.
guo.updatePhysics = true;
AstarPath.active.UpdateGraphs(guo);
bounds 변수는 UnityEngine.Bounds 객체를 참조합니다. 이는 그래프를 업데이트할 축 정렬 상자를 정의합니다. 종종 새로 생성된 객체 주변의 그래프를 업데이트하고 싶어합니다. 예제에서는 부착된 콜라이더에서 경계 상자를 가져옵니다.
그러나 이 객체가 그래프에 의해 인식될 수 있는지 확인해야 합니다. 그리드 그래프의 경우, 객체의 레이어가 충돌 테스트 마스크 또는 높이 테스트 마스크에 포함되어 있는지 확인해야 합니다.
그리드 그래프를 사용할 때 노드의 모든 매개변수 또는 포인트 그래프를 사용할 때 연결을 다시 계산할 필요가 없는 경우, 불필요한 계산을 피하기 위해 updatePhysics( GridGraph specific details 참조)를 false로 설정할 수 있습니다.
일부 경우에는 GraphUpdateObject를 사용하여 그래프를 업데이트하는 것이 불편할 수 있습니다. 이러한 경우 그래프 데이터를 직접 업데이트할 수 있습니다. 하지만 주의가 필요합니다. GraphUpdateObject를 사용할 때 시스템이 많은 작업을 자동으로 처리해줍니다.
참조: 그래프 데이터 접근: 그래프 데이터에 접근하는 방법에 대한 정보는 Accessing graph data 를 참조하십시오. 노드 속성과 Pathfinding.GraphNode: 노드가 가지는 속성에 대한 정보는 Node properties and Pathfinding.GraphNode 를 참조하십시오. 그리드 그래프 규칙: 커스텀 그리드 그래프 규칙을 만드는 방법에 대한 정보는 Grid Graph Rules 를 참조하십시오. 그리드 그래프를 사용하는 경우, 커스텀 그리드 그래프 규칙을 작성하는 것이 종종 더 안정적입니다.
다음은 그리드 그래프의 모든 노드를 변경하고, 페를린 노이즈를 사용하여 노드가 통행 가능해야 하는지 여부를 결정하는 예제입니다:
AstarPath.active.AddWorkItem(new AstarWorkItem(ctx => {
var gg = AstarPath.active.data.gridGraph;
for (int z = 0; z < gg.depth; z++) {
for (int x = 0; x < gg.width; x++) {
var node = gg.GetNode(x, z);
// 이 예제는 페를린 노이즈를 사용하여 지도를 생성합니다.
node.Walkable = Mathf.PerlinNoise(x * 0.087f, z * 0.087f) > 0.4f;
}
}
// 모든 그리드 연결을 다시 계산합니다
// 일부 노드의 통행 가능성을 업데이트했기 때문에 이 작업이 필요합니다.
gg.RecalculateAllConnections();
// 하나 또는 몇 개의 노드만 업데이트하는 경우 성능을 위해
// gg.CalculateConnectionsForCellAndNeighbours를 해당 노드에만 사용하고 싶을 수 있습니다.
}));
위의 코드는 다음과 같은 그래프를 생성합니다:
그래프 데이터는 안전할 때만 수정해야 합니다. 경로 탐색이 언제든 실행될 수 있으므로 먼저 경로 탐색 스레드를 일시 중지한 후 데이터를 업데이트해야 합니다. 이를 가장 쉽게 수행하는 방법은 AstarPath.AddWorkItem 을 사용하는 것입니다.
AstarPath.active.AddWorkItem(new AstarWorkItem(() => {
// 여기서 그래프를 안전하게 업데이트하십시오
var node = AstarPath.active.GetNearest(transform.position).node;
node.Walkable = false;
}));
AstarPath.active.AddWorkItem(() => {
// 여기서 그래프를 안전하게 업데이트하십시오
var node = AstarPath.active.GetNearest(transform.position).node;
node.position = (Int3)transform.position;
});
버전 4.2 이전에는 이 데이터를 최신 상태로 유지하기 위해 QueueFloodFill과 같은 메서드를 수동으로 호출해야 했습니다. 그러나 4.2 이상에서는 그럴 필요가 없습니다. 시스템이 더 성능 좋은 방식으로 자동으로 처리합니다.
AstarPath.active.AddWorkItem(new AstarWorkItem((IWorkItemContext ctx) => {
ctx.EnsureValidFloodFill();
// 위 호출은 이 메서드가 그래프에 대한 최신 정보를 가지고 있음을 보장합니다.
if (PathUtilities.IsPathPossible(someNode, someOtherNode)) {
// 작업을 수행합니다
}
}));
AddWorkItem 메서드는 더 고급 방식으로도 사용할 수 있습니다. 예를 들어, 필요한 경우 계산을 여러 프레임에 걸쳐 분산시킬 수 있습니다:
AstarPath.active.AddWorkItem(new AstarWorkItem(() => {
// 첫 번째 메서드 호출 바로 전에 한 번 호출됩니다.
},
force => {
// 완료될 때까지 매 프레임마다 호출됩니다.
// 작업 항목이 완료되었음을 신호하려면 true를 반환하십시오.
// "force" 매개변수가 true인 경우,
// 작업 항목이 즉시 완료되어야 함을 의미합니다.
// 이 경우 이 메서드는 완료될 때까지 블록하고 true를 반환해야 합니다.
return true;
}));
일부 항목, 예를 들어 통행 가능성과 위치는 즉시 보이는 변경 사항입니다. 그러나 태그와 패널티는 그래프에 직접 표시되
지 않습니다. 그러나 태그와 패널티를 시각화할 수 있는 다른 보기 모드를 활성화할 수 있습니다.
보기 모드는 A* Inspector -> Settings -> Debug -> Graph Coloring에서 편집할 수 있습니다. 이를 "Tags"로 설정하면 모든 태그가 그래프에서 다른 색상으로 표시됩니다.
패널티를 표시하도록 설정할 수도 있습니다. 기본적으로 패널티가 가장 높은 노드는 순수한 빨간색으로 표시되고, 패널티가 가장 낮은 노드는 녹색으로 표시되도록 자동으로 조정됩니다.
Technical Details
GraphUpdateObject를 사용하여 그래프를 업데이트할 때, 모든 그래프가 반복되고 업데이트할 수 있는 그래프(모든 내장 그래프)가 ScheduleGraphUpdates 함수를 호출합니다.
각 노드는 그래프가 GraphUpdateObject.Apply 를 호출하여 업데이트됩니다. Apply 함수는 패널티, 통행 가능성 또는 지정된 다른 매개변수를 변경합니다. 그래프는 일반적으로 그래프를 업데이트하는 사용자 지정 로직도 포함하고 있으며, 그리드 그래프(GridGraph)가 그 예입니다 ( GridGraph specific details 참조).
이를 사용하기 위해 모든 함수 호출을 이해할 필요는 없으며, 이는 주로 소스 코드를 다루고자 하는 사람들을 위한 것입니다.
GridGraph specific details
그리드 그래프를 업데이트할 때 updatePhysics 변수는 매우 중요합니다. 이 값이 true로 설정되면 영향을 받는 모든 노드의 높이가 다시 계산되고 여전히 통행 가능한지 확인됩니다. 그리드 그래프를 업데이트할 때 이 값을 true로 설정하는 것이 일반적입니다.
그리드 그래프를 업데이트할 때, GraphUpdateObject의 Apply 함수(통행 가능성, 태그 및 패널티 변경)가 경계 내의 각 노드에 대해 호출됩니다. updatePhysics 변수를 확인하고, 값이 true이면(기본값) 지정된 충돌 테스트 설정의 직경에 따라 영역이 확장되고, 해당 영역 내의 모든 노드가 충돌을 확인합니다. 그러나 값이 false이면 해당 영역(확장되지 않음) 내의 노드에 대해서만 Apply 함수가 호출되고 그 외의 작업은 수행되지 않습니다.
Navmesh based graphs
네비메쉬 기반 그래프(NavMeshGraph 및 RecastGraph)는 기존 노드에서 패널티, 통행 가능성 등을 업데이트하는 것만 지원하며, 리캐스트 그래프의 경우 전체 타일을 완전히 재계산할 수 있습니다. GraphUpdateObjects를 사용하여 새 노드를 생성할 수는 없습니다(전체 타일을 재계산하는 경우 제외). GraphUpdateObject는 GUO의 경계와 교차하거나 포함된 모든 노드/삼각형에 영향을 미칩니다. 리캐스트 그래프의 경우navmesh cutting 을 사용하여 그래프를 빠르게 업데이트할 수도 있습니다.
PointGraphs
포인트 그래프는 경계 내의 모든 노드에서 Apply를 호출합니다. GraphUpdateObject.updatePhysics가 true로 설정된 경우(기본값 true), 경계 객체를 통과하는 모든 연결도 다시 계산됩니다.
참고 포인트 그래프 업데이트는 A* Pathfinding Project의 프로 버전에서만 가능합니다.
A* 프로 기능 이 기능은 A* Pathfinding Project 프로 버전에서만 제공되는 기능입니다. 이 함수/클래스/변수는 A* Pathfinding Project의 무료 버전에서는 존재하지 않거나 기능이 제한될 수 있습니다. 프로 버전은여기에서 구매할 수 있습니다.
클래스는 GraphUpdateObject를 상속하여 일부 기능을 재정의할 수 있습니다. 다음은 노드를 일부 오프셋으로 이동하면서 기본 기능을 유지하는 GraphUpdateObject의 예입니다.
using UnityEngine;
using Pathfinding;
public class MyGUO : GraphUpdateObject {
public Vector3 offset = Vector3.up;
public override void Apply (GraphNode node) {
// Keep the base functionality
base.Apply(node);
// 노드의 위치는 Int3이므로 offset을 캐스팅해야 합니다.
node.position += (Int3)offset;
}
}
그런 다음 해당 GUO를 다음과 같이 사용할 수 있습니다:
public void Start () {
MyGUO guo = new MyGUO();
guo.offset = Vector3.up*2;
guo.bounds = new Bounds(Vector3.zero, Vector3.one*10);
AstarPath.active.UpdateGraphs(guo);
}
Check for blocking placements
타워 디펜스 게임에서 일반적으로 플레이어가 배치한 타워가 스폰 지점과 목표 지점 사이의 경로를 차단하지 않도록 해야 합니다. 이것은 어려워 보일 수 있지만, 이를 위한 API가 있습니다.
예를 들어, 타워 디펜스 게임에서 플레이어가 타워를 배치할 때, 이를 인스턴스화한 후 UpdateGraphsNoBlock 메서드를 호출하여 새로 배치된 타워가 경로를 차단하는지 확인할 수 있습니다. 만약 경로를 차단한다면 즉시 타워를 제거하고 플레이어에게 선택한 위치가 유효하지 않음을 알립니다. UpdateGraphsNoBlock 메서드에 노드 목록을 전달할 수 있으므로, 예를 들어 시작 지점에서 목표 지점까지의 경로가 차단되지 않는지 뿐만 아니라 모든 유닛이 여전히 목표 지점에 도달할 수 있는지도 확인할 수 있습니다(적들이 주변을 돌아다닐 때 타워를 배치할 수 있는 경우).
var guo = new GraphUpdateObject(tower.GetComponent<Collider>().bounds);
var spawnPointNode = AstarPath.active.GetNearest(spawnPoint.position).node;
var goalNode = AstarPath.active.GetNearest(goalPoint.position).node;
if (GraphUpdateUtilities.UpdateGraphsNoBlock(guo, spawnPointNode, goalNode, false)) {
// 유효한 타워 위치
// 메서드 호출의 마지막 매개변수(alwaysRevert)가 false이기 때문에
// 그래프가 이제 업데이트되었고 게임이 계속 진행될 수 있습니다.
} else {
// 유효하지 않은 타워 위치입니다. 이것은 스폰 지점과 목표 지점 사이의 경로를 차단합니다.
// 그래프에 대한 영향이 원래대로 복원되었습니다.
Destroy(tower);
}
이 페이지는 예를 들어 에디터 스크립트에서 플레이 모드 외부에서 경로 탐색을 작동시키는 방법을 설명합니다.
에디터 모드에서의 경로 탐색, 즉 Unity 에디터에서 게임이 전혀 실행되지 않을 때의 경로 탐색은 플레이 모드와 동일한 방식으로 작동합니다. 주요 차이점은 경로 요청이 동기적이라는 점입니다. 즉, 즉시 계산됩니다. 일반적으로 경로 요청은 비동기적으로 수행되며 계산하는 데 여러 프레임이 걸릴 수 있습니다.
이를 작동시키려면 먼저 경로 탐색 시스템을 초기화해야 합니다. 에디터 모드에서는 그래프가 역직렬화되지 않았을 수 있으며, 그래프가 스캔되지 않았을 수 있기 때문입니다. AstarPath.FindAstarPath 메서드는 AstarPath.active 속성이 설정되었는지, 모든 그래프가 역직렬화되었는지 확인합니다(내부적으로 바이트 배열로 저장됩니다).
게임에서 플레이어 캐릭터를 키보드 또는 게임 패드를 사용하여 제어하거나, 포인트 앤 클릭 이동을 사용하여 제어하고자 할 수 있습니다. 이 튜토리얼에서는 사용할 수 있는 몇 가지 다른 설정을 설명합니다.
Case studies
Keyboard movement with pathfinding
키보드를 사용하여 캐릭터를 이동시키는 것은 게임에서 캐릭터를 제어하는 일반적인 방법입니다. 이는 액션 게임, 플랫폼 게임 및 플레이어가 캐릭터의 움직임을 직접 제어하는 다른 게임에서 자주 사용됩니다. 일반적으로 WASD 키 또는 화살표 키를 사용하여 캐릭터를 이동시킵니다.
이를 달성하기 위해 내장된 이동 스크립트 중 하나를 사용할 수 있으며, 현재 키보드 입력에 의해 나타나는 방향으로부터 몇 미터 떨어진 지점에 에이전트의 목적지를 지속적으로 설정합니다.
using UnityEngine;
using Pathfinding;
public class PlayerMovement : MonoBehaviour {
public float lookaheadDistance = 1f;
IAstarAI ai;
void OnEnable () {
ai = GetComponent<IAstarAI>();
}
void Update () {
// 현재 키보드 입력을 확인합니다.
var dx = Input.GetAxis("Horizontal");
var dz = Input.GetAxis("Vertical");
// 첫 번째 활성화된 카메라를 가져옵니다.
var cam = Camera.allCameras[0];
// 카메라를 기준으로 한 이동 방향을 계산합니다.
var forward = cam.transform.forward;
forward.y = 0;
forward.Normalize();
var right = cam.transform.right;
right.y = 0;
right.Normalize();
// AI의 목적지를 캐릭터 앞의 몇 미터 지점으로 설정합니다.
// 원하는 방향으로 이동합니다.
ai.destination = transform.position + (dx * right + dz * forward) * lookaheadDistance;
}
}
동영상에서는 현재 경로가 주황색으로 표시됩니다. 장애물에 부딪히려고 할 때에도 에이전트가 자동으로 이를 피하는 방법을 확인할 수 있습니다. 이것은 이러한 매우 짧은 경로에서도 경로 탐색을 사용하는 중요한 장점입니다.
"slowdown time" 또는 "slowdown distance" (이동 스크립트에 따라 다름)를 설정할 때 주의하십시오. 이 방법을 사용할 때, 설정이 너무 낮으면 목적지가 항상 에이전트와 매우 가까워서 에이전트가 매우 느리게 이동할 수 있습니다.
참고 이 방법은 매 프레임마다 경로를 자동으로 수정하여 매우 반응성이 뛰어난 FollowerEntity 이동 스크립트와 함께 사용할 때 가장 효과적입니다.
게임에 다른 NPC가 있는 경우, NPC가 플레이어 캐릭터를 피하게 하기 위해 로컬 회피를 사용할 수 있습니다. 일반적으로 플레이어가 NPC를 피하게 하고 싶지는 않을 것입니다.
특히 플레이어 캐릭터에서는 우선순위를 최대값으로 설정하고, "Collides With" 마스크를 "Nothing"으로 설정해야 합니다. 이렇게 하면 플레이어는 다른 모든 에이전트를 무시하지만, 다른 에이전트는 여전히 플레이어를 피하고, 다른 에이전트보다 플레이어를 피하는 것을 우선시하게 됩니다.
Keyboard movement without pathfinding
내장된 이동 스크립트를 사용하지 않고 플레이어 캐릭터에 대한 자체 이동 코드를 사용하는 것도 가능합니다.
이 경우 게임의 다른 에이전트가 플레이어 캐릭터를 피하도록 로컬 회피를 통합하고 싶을 수도 있습니다.
이를 위해 플레이어 캐릭터에 RVOController 컴포넌트를 추가할 수 있습니다. 그런 다음 이동 코드에서 RVOController's velocity 를 캐릭터의 원하는 속도로 수동으로 설정할 수 있습니다. 속도를 수동으로 설정하면 로컬 회피 시스템이 이 에이전트를 수동으로 제어되는 것으로 표시하고, 다른 에이전트는 이를 처리해야 합니다.
참고 매 프레임마다 속도를 설정해야 하며, 그렇지 않으면 로컬 회피 시스템이 에이전트가 더 이상 수동으로 제어되지 않는다고 생각할 것입니다.
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;
}
에이전트를 네비메쉬에 고정하고 싶다면, NavmeshClamp 컴포넌트를 사용할 수 있습니다.
Point and click movement
포인트 앤 클릭 이동은 게임에서 캐릭터를 이동시키는 일반적인 방법입니다. 전략 게임, RPG, 그리고 플레이어가 캐릭터의 움직임을 직접 제어하지 않는 다른 게임에서 자주 사용됩니다.
이것은 A* Pathfinding Project의 내장 이동 스크립트를 사용하는 데 완벽한 사용 사례이며, 구현하기 매우 간단합니다.
플레이어를 특정 지점으로 이동시키고 싶을 때마다 이동 스크립트의 destination 속성을 해당 지점으로 설정할 수 있습니다.
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;
}
}
}
참조 TargetMover 컴포넌트는 예제 장면에서 이와 같은 이동을 제공하기 위해 사용됩니다.
목표 지점으로 이동하는 것만으로는 충분하지 않을 때가 있습니다. 에이전트가 오프셋을 두고 목표를 따라가거나, 주어진 목표를 중심으로 원을 그리며 이동하거나, 단순히 한 지점으로 이동하는 것이 아닌 다른 행동을 할 필요가 있을 수 있습니다. 이는 매 프레임마다(또는 필요한 만큼 자주) 이동 스크립트의 destination 필드를 업데이트함으로써 달성할 수 있습니다.
예를 들어, 에이전트를 목표 주변에서 원을 그리며 이동시키려면, 에이전트가 원의 경계를 따라 움직일 수 있도록 목적지를 에이전트 바로 앞의 지점으로 설정해야 합니다.
이를 계산하기 위해 목표에서 에이전트로 향하는 벡터를 가져와서 정규화합니다. 이를 normal이라고 합니다. 그런 다음, 이 normal을 90도 회전시킵니다. 이를 tangent라고 합니다. 그러면 에이전트의 목적지는 다음과 같이 설정됩니다.
ai.destination = target.position + normal * r + tangent * k
목적지를 에이전트로부터 약간 떨어진 곳에 배치하고 싶습니다. 그렇지 않으면 에이전트가 목적지에 거의 도달했다고 생각하여 속도가 느려질 수 있습니다.
이 계산을 매 프레임마다 수행하고 목적지를 에이전트에 할당하면, 에이전트는 위의 비디오처럼 목표 주변을 원을 그리며 이동할 것입니다.
코드로는 다음과 같이 보일 것입니다:
public class MoveInCircle : VersionedMonoBehaviour {
public Transform target;
public float radius = 5;
public float offset = 2;
IAstarAI ai;
void OnEnable () {
ai = GetComponent<IAstarAI>();
}
void Update () {
var normal = (ai.position - target.position).normalized;
var tangent = Vector3.Cross(normal, target.up);
ai.destination = target.position + normal * radius + tangent * offset;
}
public override void DrawGizmos () {
if (target) Draw.Circle(target.position, target.up, radius, Color.white);
}
}
같은 종류의 설정을 사용하여 에이전트가 오프셋을 두고 목표를 따라가게 만들 수 있습니다: 매 프레임마다 에이전트의 목적지를 목표의 위치에 오프셋을 더한 위치로 업데이트하십시오.
참고 이 솔루션은 FollowerEntity 이동 스크립트와 함께 사용할 때 가장 부드럽게 작동합니다. FollowerEntity는 목적지를 설정할 때마다 경로를 수정하여 항상 최신 상태로 유지합니다. 다른 이동 스크립트에서도 작동하지만, 더 부드럽게 만들기 위해 경로 재계산 간의 시간을 줄여야 할 수도 있습니다.