내일배움캠프 63일차 7/15 TIL + NavMesh 복구
작업 내용
유닛 이동을 위해 NavMesh 복구
주말 동안 유닛 이동을 완벽하게 구현하기 위해 다양한 방법으로 Grid 상에서 이동하는 방식을 조절해봤다. 가장 큰 문제는 Grid 값을 true/false
로 바꿀 때, 유닛이 한 번 이동 명령이 내려지면 끝까지 가면 좋겠지만, 중간에 사용자가 다른 명령을 내리거나 적 유닛, 아군 유닛을 만나 변화 사항이 생길 때 이 값을 수정하는 것이 제대로 작동하지 않는 예외 상황들이었다.
지금까지는 RTS 게임을 하면서 본 모습을 기억하며 로직을 구현하려 했지만, 커버하지 못하는 예외 상황들이 많이 발생했다. 그래서 스타크래프트의 이동 로직을 정리해보며 작동 방식을 세세히 살펴봤다.
스타크래프트 이동 로직
- 목표 설정: 유저가 유닛을 선택하고 이동 명령을 내리면 해당 유닛은 목표 지점을 설정한다.
- 경로 찾기: 유닛은 A* 알고리즘을 사용해 현재 위치에서 목표 지점까지의 최적 경로를 찾는다.
- 경로 따라가기: 유닛은 설정된 경로를 따라 이동하며, 이동 도중 장애물이나 다른 유닛을 만나면 경로를 재계산하여 우회한다.
- 충돌 처리: 이동 중 다른 유닛과 충돌하는 경우 서로의 경로를 조정한다.
- 동적 경로 수정: 이동 도중 새로운 명령이 내려지거나 환경이 변화하면 경로를 실시간으로 수정한다.
- 목표 도착 및 행동 수행: 유닛이 목표 지점에 도착하면 해당 위치에서의 명령을 수행한다.
내 구현 방식에 있어서 충돌 감지를 해결하려고 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도 유닛들의 배치를 구현하기 좋았고, 나중에 크기가 다른 유닛이나 건물을 배치할 때 그리드를 활용해 배치하기 쉬울 것이다.