스크립팅하는애님 2024. 5. 23. 22:33

턴 기반 이동 및 커스텀 이동 규칙을 보여주는 예제 장면.

 

Overview

이 예제 장면은 육각형 그리드에서 턴 기반 이동을 보여줍니다. 표준 이동 스크립트를 사용하지 않고, 대신 모든 캐릭터의 이동을 하나의 스크립트에서 처리합니다.

참고
턴 기반 이동은 물론 포함된 이동 스크립트로도 수행할 수 있습니다.
이 장면은 간단한 퍼즐 게임을 보여줍니다. 오렌지 콘을 클릭하면 이동 범위가 표시되고, 별도의 타일을 클릭하여 이동할 수 있습니다. 오렌지 콘을 보라색 육각형으로 이동시키면 초록색 "문" 육각형 세트를 토글할 수 있는 옵션이 주어집니다.

퍼즐의 목표는 하나의 오렌지 콘을 화면 오른쪽의 빨간 영역에 있는 보라색 육각형으로 이동시키는 것입니다.
 

 

Graph setup

 

이 장면에서는 육각형 그리드 그래프가 사용됩니다. GridGraph를 생성하고 Shape 드롭다운을 육각형으로 변경하여 구성합니다.

육각형 그래프의 경우, 크기를 조정할 때 사용할 수 있는 두 가지 측정 방식이 있습니다. 이 장면에서는 육각형의 폭(마주보는 면 사이의 거리)을 1 월드 단위로 설정하지만, 지름(마주보는 꼭짓점 사이의 거리)을 사용할 수도 있습니다.

참고
2D 게임을 Tilemap 컴포넌트를 사용하여 빌드하는 경우, 그래프를 타일맵에 직접 정렬할 수 있습니다. 자세한 내용은 Pathfinding on tilemaps 를 참조하세요.

육각형 그래프가 찌그러진 사각형처럼 보일 수 있습니다. 이는 육각형 그래프가 내부적으로는 찌그러지고 다른 방식으로 연결된 그리드 그래프이기 때문입니다. 일부 게임에서는 레벨 외부에 불필요한 노드가 생길 수 있지만, 성능 면에서는 일반적으로 문제가 되지 않습니다.

씬의 모든 육각형 메쉬를 그래프와 정렬하기 위해 SnapToNode라는 스크립트를 사용합니다. 이 스크립트는 편집 모드에서도 실행되며, 변형이 이동될 때마다 가장 가까운 노드에 변형을 맞춥니다.

void Update () {
    if (transform.hasChanged && AstarPath.active != null) {
        var node = AstarPath.active.GetNearest(transform.position, NNConstraint.None).node;
        if (node != null) {
            transform.position = (Vector3)node.position;
            transform.hasChanged = false;
        }
    }
}

이렇게 하면 씬 뷰에서 프리팹을 쉽게 이동할 수 있으며, 자동으로 가장 가까운 육각형 노드에 맞춰집니다.

AstarPath.GetNearest(Vector3,NNConstraint)  참조

 

Purple triggers

씬의 보라색 육각형은 유니티의 내장 함수인 OnTriggerEnter를 사용하여 트리거됩니다. 이는 HexagonTrigger 클래스에서 처리됩니다. 이 메서드에서 스크립트는 객체가 에이전트인지, 그렇다면 유닛이 이 노드를 목표 노드로 설정했는지 또는 단순히 통과 중인지를 확인합니다. 확인이 완료되면 애니메이션이 재생됩니다.

void OnTriggerEnter (Collider coll) {
    var unit = coll.GetComponentInParent<TurnBasedAI>();
    var node = AstarPath.active.GetNearest(transform.position).node;

    // 에이전트인지, 그리고 에이전트가 이 노드를 향하고 있는지 확인합니다.
    if (unit != null && unit.targetNode == node) {
        visible = true;
        anim.CrossFade("show", 0.1f);
    }
}

애니메이션은 버튼을 나타내거나, 마지막에는 승리 메시지를 보여줍니다. 나타난 버튼들은 초록색 육각형의 통과 가능 여부를 토글할 수 있습니다. 버튼을 클릭하면 관련된 초록색 육각형에서  TurnBasedDoor.Toggle 을 호출합니다. 그러면 애니메이션이 재생되고, SingleNodeBlocker 컴포넌트를 사용하여 초록색 육각형 아래의 노드를 차단하거나 차단 해제합니다.

참고
Utilities for turn-based games 에 관한 자세한 정보는 SingleNodeBlocker 컴포넌트의 작동 방식을 확인하세요.

 

Agent movement

이 예제 장면의 이동은 다른 예제들과 약간 다릅니다. 내장 이동 스크립트를 사용하지 않고, 대신 모든 이동을 처리하는 커스텀 스크립트 TurnBasedManager 를 사용합니다. 턴 기반 게임에 반드시 필요한 것은 아니지만, 이동의 대체 접근 방식을 보여줍니다.

  • 이 매니저는 몇 가지 작업을 수행합니다:
    마우스 클릭을 감지하여 클릭된 것에 따라 에이전트를 선택하거나 이동시킵니다.
  • 에이전트가 선택되면 한 턴 내에 이동할 수 있는 모든 노드를 표시합니다.
  • 에이전트가 이동할 때, 경로의 부드러운 버전을 따라 에이전트의 위치를 애니메이션으로 이동시킵니다.
  • 에이전트가 이동할 때마다 SingleNodeBlocker를 트리거하여 다른 에이전트가 동일한 육각형으로 이동할 수 없도록 합니다.

Movement animation

에이전트를 이동시키기 위해  TurnBasedManager.MoveAlongPath 메서드를 사용합니다. 이 메서드는  catmull-rom spline 을 사용하여 경로의 점들 사이를 부드럽게 보간합니다.

static IEnumerator MoveAlongPath (TurnBasedAI unit, ABPath path, float speed) {
    if (path.error || path.vectorPath.Count == 0)
        throw new System.ArgumentException("Cannot follow an empty path");

    // 매우 간단한 이동으로, 카멜 롬 스플라인(Catmull-Rom Spline)을 사용하여 보간합니다.
    float distanceAlongSegment = 0;
    for (int i = 0; i < path.vectorPath.Count - 1; i++) {
        var p0 = path.vectorPath[Mathf.Max(i-1, 0)];
        // 현재 구간의 시작점
        var p1 = path.vectorPath[i];
        // 현재 구간의 끝점
        var p2 = path.vectorPath[i+1];
        var p3 = path.vectorPath[Mathf.Min(i+2, path.vectorPath.Count-1)];

        // 스플라인의 길이를 근사합니다.
        var segmentLength = Vector3.Distance(p1, p2);

        // 각 프레임마다 에이전트를 앞으로 이동시키고, 구간의 끝에 도달할 때까지 반복합니다.
        while (distanceAlongSegment < segmentLength) {
            // 카멜 롬 스플라인을 사용하여 경로를 부드럽게 만듭니다. 자세한 내용은 [카멜 롬 스플라인](https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull%E2%80%93Rom_spline)을 참조하세요.
            var interpolatedPoint = AstarSplines.CatmullRom(p0, p1, p2, p3, distanceAlongSegment / segmentLength);
            unit.transform.position = interpolatedPoint;
            yield return null;
            distanceAlongSegment += Time.deltaTime * speed;
        }

        distanceAlongSegment -= segmentLength;
    }

    // 에이전트를 경로의 최종 지점으로 이동시킵니다.
    unit.transform.position = path.vectorPath[path.vectorPath.Count - 1];
}

 

Movement range

이동 범위를 표시하기 위해 ConstantPath 가 사용됩니다. 이는 주어진 최대 경로 비용까지 도달할 수 있는 모든 노드를 출력하는 특수 경로 유형입니다. 따라서 에이전트가 한 턴 내에 도달할 수 있는 모든 노드를 생성하는 데 완벽합니다.

참고
ConstantPath  유형에 대한 자세한 내용은 Path Types 예제 장면을 참조하세요.

이는 TurnBasedManager.GeneratePossibleMoves 메서드에서 처리됩니다. 이 메서드는 간단히 하기 위해 동기적으로 ConstantPath 를 계산하고, 경로의 모든 노드를 반복하여 각 노드의 위치에 프리팹을 인스턴스화합니다.

void GeneratePossibleMoves (TurnBasedAI unit) {
    var path = ConstantPath.Construct(unit.transform.position, unit.movementPoints * 1000 + 1);

    path.traversalProvider = unit.traversalProvider;

    // 경로 계산을 예약합니다.
    AstarPath.StartPath(path);

    // 경로 요청을 즉시 완료하도록 강제합니다
	// 이는 그래프가 충분히 작아서 지연을 일으키지 않을 것이라고 가정합니다.
    path.BlockUntilCalculated();

    foreach (var node in path.allNodes) {
        if (node != path.startNode) {
            // 도달할 수 있는 노드를 나타내는 새로운 노드 프리팹을 생성합니다
		// 참고: 실제 게임에서 이 기능을 사용할 경우, 
    	// 항상 새로운 GameObject를 인스턴스화하는 것을 피하기 위해 오브젝트 풀링을 사용하는 것이 좋습니다.
            var go = GameObject.Instantiate(nodePrefab, (Vector3)node.position, Quaternion.identity) as GameObject;
            possibleMoves.Add(go);

            go.GetComponent<Astar3DButton>().node = node;
        }
    }
}