2 minute read

작업 내용

유닛 이동을 위해 NavMesh 복구

주말 동안 유닛 이동을 완벽하게 구현하기 위해 다양한 방법으로 Grid 상에서 이동하는 방식을 조절해봤다. 가장 큰 문제는 Grid 값을 true/false로 바꿀 때, 유닛이 한 번 이동 명령이 내려지면 끝까지 가면 좋겠지만, 중간에 사용자가 다른 명령을 내리거나 적 유닛, 아군 유닛을 만나 변화 사항이 생길 때 이 값을 수정하는 것이 제대로 작동하지 않는 예외 상황들이었다.

지금까지는 RTS 게임을 하면서 본 모습을 기억하며 로직을 구현하려 했지만, 커버하지 못하는 예외 상황들이 많이 발생했다. 그래서 스타크래프트의 이동 로직을 정리해보며 작동 방식을 세세히 살펴봤다.

스타크래프트 이동 로직

  1. 목표 설정: 유저가 유닛을 선택하고 이동 명령을 내리면 해당 유닛은 목표 지점을 설정한다.
  2. 경로 찾기: 유닛은 A* 알고리즘을 사용해 현재 위치에서 목표 지점까지의 최적 경로를 찾는다.
  3. 경로 따라가기: 유닛은 설정된 경로를 따라 이동하며, 이동 도중 장애물이나 다른 유닛을 만나면 경로를 재계산하여 우회한다.
  4. 충돌 처리: 이동 중 다른 유닛과 충돌하는 경우 서로의 경로를 조정한다.
  5. 동적 경로 수정: 이동 도중 새로운 명령이 내려지거나 환경이 변화하면 경로를 실시간으로 수정한다.
  6. 목표 도착 및 행동 수행: 유닛이 목표 지점에 도착하면 해당 위치에서의 명령을 수행한다.

내 구현 방식에 있어서 충돌 감지를 해결하려고 Grid에 각 유닛의 이동 좌표를 true/false로 조절했지만, 생각지 못한 에러들이 발생했다. 그래서 충돌 감지는 실제 충돌로 감지하는 것이 좋다고 판단했고, 현재와 같이 transform을 변형해서 이동하는 방식은 적합하지 않았다.

결국, NavMesh를 복귀시키기로 했다. 대신 Grid는 버리지 않고 유닛의 이동 시 위치를 Grid 기준으로 선정하고, 다중 유닛 제어 시 대형을 맞출 때 Grid의 가까운 지점을 찾아 대형을 맞추어 주는 데 활용했다.

MoveState의 변화

public override void Enter()
{
    userUnit.Action.IsAlert = false;
    userUnit.Action.IsOnTheMove = true;
    userUnit.Action.IsTracking = false;
    userUnit.Action.IsHold = false;

    // 목적지까지 경로 계산
    targetPos = userUnit.Action.TargetPosition;
    agent.SetDestination(GridGenerator.Instance.Grid.World2WorldGrid(targetPos));
}

public override void Exit() {}

public override void Update()
{
    if (!agent.pathPending)
    {
        if (agent.remainingDistance <= agent.stoppingDistance)
        {
            if (!agent.hasPath || agent.velocity.sqrMagnitude == 0f)
            {
                userUnit.StateMachine.ChangeState(userUnit.IdleState);
            }
        }
    }
}

이전보다 코드가 훨씬 단순하고 명확해졌다.

UserUnitController

public void OnMouseRightClick(InputAction.CallbackContext context)
{
    if (selectedUnits.Count > 0)
    {
        MouseStartPos = mouseDrag.ReadValue<Vector2>();
        Vector2 worldMousePos = mainCamera.ScreenToWorldPoint(MouseStartPos);
        targetPositionMarker.gameObject.SetActive(true);
        targetPositionMarker.SetPosition(GridGenerator.Instance.Grid.World2WorldGrid(worldMousePos));

        LinkedList<Vector2> destinationList = new LinkedList<Vector2>();
        int count = selectedUnits.Count;

        // 배치 위치 찾기
        for (int i = 0; i < count; i++)
        {
            destinationList.AddLast(FindDestination(worldMousePos));
        }
        LinkedListNode<Vector2> node = destinationList.First;

        // 유닛별로 위치 할당
        foreach (var unit in selectedUnits)
        {
            ((IMovable)unit)?.Move(node.Value);
            node = node.Next;
        }

        foreach (Vector2 n in destinationList)
        {
            GridGenerator.Instance.Grid.SetGridValue(n, true);
        }
    }
    SetReadyToAttack(false);
}

다중으로 유닛 선택 시 대형을 갖출 수 있도록 주변 위치를 찾아 할당한다. 결과적으로 NavMesh는 충돌 감지와 이동을, Grid는 유닛들의 배치 위치를 조정하는 역할을 하게 되었다.

마무리

이번 작업을 하면서 A* 알고리즘을 만들어보긴 했지만, 실제로 적용하려면 부가적으로 많은 작업이 필요함을 배웠다. 덕분에 NavMesh에 있던 기능들이 왜 있었고, 어떻게 잘 활용하면 될지를 배웠다. Grid도 유닛들의 배치를 구현하기 좋았고, 나중에 크기가 다른 유닛이나 건물을 배치할 때 그리드를 활용해 배치하기 쉬울 것이다.