A* Pathfinding Project의 모든 그래프는 시스템에 추가 기능으로 작성됩니다. 이를 통해 자신만의 특수화된 그래프 유형을 추가하는 것이 (상대적으로) 쉽습니다.
이 튜토리얼에서는 시스템에서 사용할 수 있는 기본 그래프 생성기를 설정하는 방법을 보여 드리겠습니다. 전체 스크립트는 여기에서 확인할 수 있습니다: PolarGraphGenerator.cs
A Basic Graph
가장 간단한 그래프는 다음과 같이 생겼습니다:
using System.Collections.Generic;
using UnityEngine;
using Pathfinding;
using Pathfinding.Serialization;
using Pathfinding.Util;
using Unity.Jobs;
// 기본 그래프 유형에서 새로운 그래프를 상속받습니다
[JsonOptIn]
// 코드 스트리핑을 사용할 때 클래스가 제거되지 않도록 합니다 (https://docs.unity3d.com/Manual/ManagedCodeStripping.html 참조)
[Pathfinding.Util.Preserve]
public class SimpleGraph : NavGraph {
// 그래프가 스캔되었고 경로 찾기에 사용될 수 있으면 true를 반환해야 합니다
public override bool isScanned => true;
class SimpleGraphScanPromise : IGraphUpdatePromise {
public SimpleGraph graph;
// 이 메서드에서는 그래프를 업데이트하는 데 필요한 비동기 계산을 실행할 수 있습니다.
// 이 코루틴이 완료된 후 Apply 메서드가 호출됩니다.
// 이 메서드에서 생성된 모든 JobHandle은 다음 반복 및 Apply 메서드 호출 전에 대기합니다.
public IEnumerator<JobHandle> Prepare() => null;
public void Apply (IGraphUpdateContext ctx) {
// 여기서 그래프를 스캔하는 코드를 작성합니다.
// 이전 노드를 제거합니다(있는 경우).
graph.DestroyAllNodes();
}
}
protected override IGraphUpdatePromise ScanInternal () => new SimpleGraphScanPromise { graph = this };
public override void GetNodes (System.Action<GraphNode> action) {
// 이 메서드는 그래프의 모든 노드에 대해 대리자를 호출해야 합니다.
}
}
이 그래프는 노드를 생성하지 않지만, 새 그래프 추가 목록에서 회색으로 표시되어야 합니다. 이제 스캔 로직을 생성해 봅시다!
Scanning
먼저, 위의 코드를 포함하는 새 스크립트를 생성하고 이름을 PolarGraph로 변경하고 PolarGraph.cs로 저장합니다.
ScanInternal 메서드는 그래프를 스캔해야 할 때 호출됩니다. Scan 메서드는 여러 노드를 생성하고 노드 간의 연결을 생성해야 합니다. 우리는 극좌표 그래프를 생성할 것입니다. 즉, 행 대신 원이 있는 원형으로 배열된 그리드와 같은 것입니다. "circles"는 그래프에서 동심원의 수를 나타내고 "steps"는 각 원에서 노드의 수를 나타냅니다. 아래 이미지에서 노드 간의 연결을 볼 수 있습니다. 노드 자체는 해당 선분의 교차점에 배치됩니다.
첫 번째 단계는 노드를 저장할 임시 배열을 생성하고 그래프를 구성할 변수를 추가하는 것입니다. PointGraph에서 사용되는 PointNode 노드 유형을 사용할 것입니다. 이는 우리가 사용하려는 것과 본질적으로 동일한 내용을 포함하기 때문입니다.
public int circles = 10;
public int steps = 20;
public Vector3 center = Vector3.zero;
public float scale = 2;
// 여기서 그래프의 모든 노드를 저장할 것입니다.
PointNode[] nodes;
GraphTransform transform;
// 지정된 위치에 단일 노드를 생성합니다.
PointNode CreateNode (Vector3 position) {
var node = new PointNode(active);
// 노드 위치는 Int3로 저장됩니다. Vector3를 Int3로 변환할 수 있습니다.
node.position = (Int3)position;
return node;
}
public override IEnumerable<Progress> ScanInternal () {
// 모든 노드를 포함하는 2D 배열을 생성합니다.
// 이는 서로 다른 노드를 참조하기 쉽게 하기 위한 임시 배열입니다.
PointNode[][] circleNodes = new PointNode[circles][];
yield break;
}
Editor
그래프가 새 그래프 추가 목록에 아직 표시되지 않는 이유는 그래프 에디터가 필요하기 때문입니다. 이제 이를 생성하겠습니다. AstarPathfindingProject/Editor/GraphEditors/(또는 Js 지원을 활성화한 경우 AstarPathfindingEditor/Editor/GraphEditors/) 또는 다른 Editor 폴더에 새 스크립트를 만듭니다. 스크립트 이름을 PolarGeneratorEditor.cs로 지정합니다.
아래는 극좌표 그래프를 위한 매우 간단한 에디터입니다. 이 코드를 방금 만든 파일에 복사하세요. 이렇게 하면 그래프를 생성할 수 있어야 합니다.
using UnityEditor;
using Pathfinding;
[CustomGraphEditor(typeof(PolarGraph), "Polar Graph")]
public class PolarGeneratorEditor : GraphEditor {
// GUI는 여기에 작성합니다.
public override void OnInspectorGUI (NavGraph target) {
var graph = target as PolarGraph;
graph.circles = EditorGUILayout.IntField("Circles", graph.circles);
graph.steps = EditorGUILayout.IntField("Steps", graph.steps);
graph.scale = EditorGUILayout.FloatField("Scale", graph.scale);
graph.center = EditorGUILayout.Vector3Field("Center", graph.center);
}
}
CustomGraphInspector 속성은 이 클래스가 PolarGraph 유형의 그래프에 대한 사용자 정의 에디터임을 시스템에 알리며, 그래프 목록에 표시될 이름은 "Polar Graph"가 됩니다.
시도해 보세요! AstarPath 인스펙터의 Graphs 탭을 열고, Add Graph를 클릭하여 이제 목록에 있어야 하는 Polar Graph를 추가합니다.
Adding nodes
이제 실제로 그래프를 생성하는 작업을 시작해야 합니다. 이 지점 이후의 모든 코드 조각은 Scan 함수에 들어가야 합니다. 노드의 회전, 위치 지정 등을 가능하게 하기 위해 행렬을 사용할 것입니다. 배열의 첫 번째 노드는 Vector3.zero에 배치할 중앙 노드여야 합니다. 노드 위치는 Int3로 저장됩니다. 이는 Vector3와 유사하지만 부동 소수점 좌표 대신 정수 좌표를 사용합니다. Vector3와 Int3 간에는 명시적 캐스트가 가능합니다.
// 이전 노드 제거(있는 경우)
graph.DestroyAllNodes();
var circles = graph.circles;
var steps = graph.steps;
// 모든 노드를 포함하는 2D 배열 생성
// 이는 서로 다른 노드를 참조하기 쉽게 하기 위한 임시 배열입니다.
PointNode[][] circleNodes = new PointNode[circles][];
// 노드를 #center로 이동시키고
// 위치를 #scale로 스케일링하는 행렬 생성
// GraphTransform 클래스에는 이를 처리하기 위한 다양한 유틸리티 메서드가 있습니다.
graph.transform = new GraphTransform(Matrix4x4.TRS(graph.center, Quaternion.identity, Vector3.one * graph.scale));
// 중심에 중앙 노드 배치
circleNodes[0] = new PointNode[] {
graph.CreateNode(CalculateNodePosition(0, 0, graph.transform))
};
이제 주어진 각도와 원에서 노드의 위치를 계산해야 합니다.
static Vector3 CalculateNodePosition (int circle, float angle, GraphTransform transform) {
// 중심에서 노드 방향을 가져옵니다.
var pos = new Vector3(Mathf.Sin(angle), 0, Mathf.Cos(angle));
// 원 번호로 곱하여 그래프 공간에서 노드 위치를 가져옵니다.
pos *= circle;
// 행렬로 곱하여 월드 공간에서 노드 위치를 가져옵니다.
pos = transform.Transform(pos);
return pos;
}
이제 그래프의 나머지 부분을 설정합니다. 원별로, 노드별로 설정합니다.
// 각 스텝이 사용할 각도 크기(라디안)
float anglesPerStep = (2 * Mathf.PI) / steps;
for (int circle = 1; circle < circles; circle++) {
circleNodes[circle] = new PointNode[steps];
for (int step = 0; step < steps; step++) {
// 중심에 대한 노드의 각도를 가져옵니다.
float angle = step * anglesPerStep;
Vector3 pos = CalculateNodePosition(circle, angle, graph.transform);
circleNodes[circle][step] = graph.CreateNode(pos);
}
}
이제 모든 노드를 올바른 위치에 설정했지만, 다른 노드와의 연결이 없습니다. 이를 추가해 봅시다:
// 모든 원을 반복합니다.
// 원 0은 중앙 노드뿐이므로 지금은 건너뜁니다.
for (int circle = 1; circle < circles; circle++) {
for (int step = 0; step < steps; step++) {
// 현재 노드를 가져옵니다.
PointNode node = circleNodes[circle][step];
// 여기 있는 노드는 항상 정확히 네 개의 연결을 가집니다. 그리드와 유사하지만 극좌표입니다.
// 마지막 원에 있는 노드는 세 개의 연결만 가집니다.
int numConnections = circle < circles - 1 ? 4 : 3;
var connections = new Connection[numConnections];
// 현재 원에서 시계 방향으로 다음 노드를 가져옵니다.
// 각 원의 마지막 노드는 원의 첫 번째 노드에 연결되어야 하므로 모듈로 연산자를 사용합니다.
connections[0].node = circleNodes[circle][(step + 1) % steps];
// 반시계 방향 노드. 여기서는 언더플로우를 확인합니다.
connections[1].node = circleNodes[circle][(step - 1 + steps) % steps];
// 이전 원의 노드(중심 방향)
if (circle > 1) {
connections[2].node = circleNodes[circle - 1][step];
} else {
// 중앙 노드로 연결 생성, 특별한 경우
connections[2].node = circleNodes[circle - 1][0];
}
// 이 외부에 다른 원이 있는지 확인합니다.
if (numConnections == 4) {
// 다음 원의 노드(중심에서 밖으로)
connections[3].node = circleNodes[circle + 1][step];
}
for (int q = 0; q < connections.Length; q++) {
// Node.position은 Int3입니다. 여기서는 두 위치 사이를 이동하는 비용을 가져옵니다.
connections[q].cost = (uint)(node.position - connections[q].node.position).costMagnitude;
}
node.connections = connections;
}
}
첫 번째 노드(중앙)는 특별한 경우로, 첫 번째 원(또는 두 번째, 어떻게 보느냐에 따라 다름)의 모든 노드와 연결됩니다. 즉, 첫 번째 원의 노드는 steps개의 연결을 갖게 됩니다.
// 중앙 노드는 특별한 경우이므로 따로 처리해야 합니다.
PointNode centerNode = circleNodes[0][0];
centerNode.connections = new Connection[steps];
// 첫 번째 원의 모든 노드를 중앙 노드의 연결로 지정합니다.
for (int step = 0; step < steps; step++) {
centerNode.connections[step] = new Connection(
circleNodes[1][step],
// centerNode.position은 Int3입니다. 여기서는 두 위치 사이를 이동하는 비용을 가져옵니다.
(uint)(centerNode.position - circleNodes[1][step].position).costMagnitude,
isOutgoing: true,
isIncoming: true
);
}
이제 남은 것은 노드를 이동 가능하게 만드는 것입니다. 기본값은 이동 불가능입니다. 이 튜토리얼에서는 장애물 검사 등을 다루지 않지만, Unity의 Physics 클래스를 공부하면 이를 구현할 수 있습니다.
// 모든 노드를 nodes 배열에 저장합니다.
List<PointNode> allNodes = new List<PointNode>();
for (int i = 0; i < circleNodes.Length; i++) {
allNodes.AddRange(circleNodes[i]);
}
graph.nodes = allNodes.ToArray();
// 모든 노드를 이동 가능하게 설정합니다.
for (int i = 0; i < graph.nodes.Length; i++) {
graph.nodes[i].Walkable = true;
}
이전 코드 조각을 Scan 함수에 차례로 배치했다면, 이제 작동하는 스캔 함수를 가지게 됩니다.
마지막으로 그래프를 스캔할 수 있도록 GetNodes 메서드를 구현해야 합니다. 모든 노드가 배열에 저장되어 있으므로 매우 간단합니다.
public override void GetNodes (System.Action<GraphNode> action) {
if (nodes == null) return;
for (int i = 0; i < nodes.Length; i++) {
// 대리자 호출
action(nodes[i]);
}
}
이제 그래프를 생성하고, 매개변수를 편집하고, Scan을 클릭하면 그래프가 장면 뷰에 나타납니다.
멋지죠?
하지만 인스펙터를 선택 해제한 다음 다시 선택하면 설정이 저장되지 않았음을 알 수 있습니다. 이는 마지막으로 직렬화를 추가해야 하기 때문입니다. 모든 그래프 설정은 JSON으로 직렬화됩니다(http://www.json.org/ 참조). 필드를 직렬화하려면 각 필드에 속성을 추가해야 합니다. JsonMember 속성은 해당 필드를 직렬화하려고 한다는 것을 직렬화 도구에 알립니다.
전체 스크립트는 여기에서 확인할 수 있습니다: PolarGraphGenerator.cs
More Stuff
다른 기능을 사용자 정의하려면 더 많은 메서드를 재정의할 수 있습니다. 특히 GetNearest 및 GetNearestForce 메서드입니다. 이는 지점에서 가장 가까운 노드를 찾는 방법을 제어합니다. 기본 구현이 있지만, 그래프에 많은 노드가 포함된 경우 검색 속도가 느릴 수 있습니다. OnDrawGizmos 메서드를 재정의하여 그래프를 기본 구현과 다르게 그릴 수도 있습니다.
더 많은 정보를 직렬화해야 하는 경우, SerializeExtraInfo, DeserializeExtraInfo를 재정의하고, 노드를 로드한 후 설정해야 하는 경우 PostDeserialization 함수를 재정의할 수 있습니다.
모든 기능을 설명하는 것은 이 튜토리얼의 범위를 벗어나므로 다른 그래프 유형이 이를 구현한 방법을 확인할 수 있습니다.
The End
이제 이 그래프를 다른 그래프처럼 사용할 수 있어야 합니다! 이 튜토리얼이 그래프 생성기를 작성하는 데 도움이 되었기를 바랍니다.
'유니티 에셋 > A* Pathfinding project pro' 카테고리의 다른 글
Editing graph connections manually (0) | 2024.05.28 |
---|---|
Extending The System > Writing Modifiers (0) | 2024.05.28 |
Extending The System (0) | 2024.05.28 |
Creating graphs during runtime (0) | 2024.05.28 |
Off-mesh links (0) | 2024.05.28 |