티스토리 뷰

Programming/Algorithm

Game Expert | BlockMovement

글그리 2020. 8. 19. 14:23

타일 위에서 자연스럽게 움직이는 Movement를 구현해보도록 하자. 엔진에서 제공하는 NavigationMesh를 사용해서 만들 수도 있지만 더 세밀한 개성을 주기 위해서 먼저 간단한 움직임을 직접 구현해본다. 타일 이동이기 때문에 상하좌우 4방향으로 움직일 수 있고, 한 칸씩 이동한다. 이 떄 입력이 연속적으로 발생해도 모두 기억하고 순차적으로 이동할 수 있도록한다.

4방향으로 받는 입력에 대해 순차적으로 자연스럽게 이동한다.

 

BlockMovement를 바로 구현하기 위해 기본적인 준비물이 몇가지 필요한데, 캐릭터 Mesh와 속도에따라 애니메이션을 재생하는 Blueprint, 그리고 기본적인 맵을 구성한다.

필요에 따라 모습은 달라질 수 있다.

 

1. Queue

먼저 Controller에서 입력을 받으면 MovementComponent가 입력을 수신하고 Queue에 저장한다. 매 프레임 Queue를 검사하고 첫번째 방향으로 이동한다. 그런데 이렇게하니까 몇가지 문제가 발생했다. 원하는 지점에 도착한지 검사하기 위해서 일정 범위 안에 들어오면 도착했다고 판단하도록 했는데, 도착하자마자 Queue에서 입력을 제거해버리니까 경계선에서 캐릭터가 급정거해버린다. 또 여기에서 다음 방향으로 이동하도록 지시하면 오차가 누적되어서 결국 목적지에 도달하지 못하는 상황도 발생한다.

일정 범위안에 들어오면 멈추고 다음 방향으로 이동하는데, 범위 때문에 오차가 생기고 누적된다.

원하는 타일에 도착했는지 체크하는 방법
Actor의 Location은 FVector구조체를 쓰는데 이 구조체는 3개의 float으로 이루어져있다. 따라서 if (Location == Destination)과 같은 코드는 적절하지 못하다. 그래서 두 vector 사이의 거리가 어느정도 가까워지면 도착했다고 판단하도록했다.

 

2. 감속, 방향

먼저 캐릭터가 급정거하는것을 막기위해서 서서히 감속해야한다. 목적지로부터 일정거리 이상 좁혀지면 더이상 가속하지 않고 속도를 줄여서 해당 위치에 최대한 멈출 수 있도록한다.

언제 멈춰야할지 계산할 수 있을까?

등가속도 공식을 사용하면 현재속도에 대한 제동거리를 계산할 수 있고, 이를 실시간으로 계산해서 언제 멈춰야할지 알고, 서서히 멈추도록 구현한다.

등가속도 공식으로 제동거리를 계산할 수 있다. 

float BrakingTime = CurrentSpeed / Acceleration;
float BrakingDistance = BrakingTime * CurrentSpeed;

if ((Destination - CurrentLocation).Distance < BrakingDistance)
{
	// Brake
}

감속구간에 진입하면 서서히 멈추거나 다음 방향으로 이동한다.

그리고 이동방향을 실시간으로 계산하여 오차가 누적되지않도록 만든다. 이제 어느정도 괜찮아보이지만 남은 문제점이 있다. 이동중에 방향을 전환하면 바라보는 방향이 급격하게 바뀌어서 직선이동을 하게된다. 원하는 것은 자연스러운 이동이니까 이 부분도 개선해보자.

오차가 누적되지는 않지만 직선으로 이동하기 때문에 자연스럽지 않다.

 

3. 선회

자연스러운 방향전환을 위해서 가고자하는 방향과 실제 캐릭터의 방향을 분리시킨다. 입력받은 방향으로 이동하지만 캐릭터가 서서히 해당 방향으로 회전하면서 이동하기 때문에 이동경로가 곡선으로 자연스럽게 만들어진다.

실제 이동방향(하늘색)이 가고자하는 방향(초록색)으로 서서히 회전하면서 자연스러운 이동경로를 만들어낸다.

하지만 캐릭터가 회전하는 방향을 상황에 맞게 계산해주지않으면, 왼쪽을 향하기 위해서 시계방향으로 270도 회전하는 경우가 생길 수 있다. 

이렇게 이상한 회전이 나올 수 있다.

이러한 상황을 방지하기 위해서 내적,외적을 사용하여 회전할 방향을 계산해보자.

XY평면을 이동하기 때문에 Z값만 사용해서 어떤 방향으로 회전할지 계산할 수 있다.

float CrossZ = CurrentDirection.X * Direction.Y - CurrentDirection.Y * Direction.X;
float RotateVelocity = RotateSpeed * CrossZ;

Rotation.Yaw += RotateVelocity * DeltaTime;

하지만 외적으로 모든 문제가 해결되지는 않는데, 그 이유는 두 벡터가 평행할 때 외적의 결과가 0이된다는 성질 때문에 CrossZ값이 정면으로 바라볼 때와 뒤쪽을 바라볼 때 모두 0이된다. 이건 내적을 계산해서 구분할 수 있다.

내적으로 두 벡터의 사이각을 구할 수 있다.

내적으로 두 벡터의 사이각을 구할 수 있는데, 방향은 모두 단위벡터를 사용하기 때문에 벡터의크기는 무시할 수 있다.

내적 결과가 -0.9보다 작다면 일단 시계방향으로 먼저 회전한다.

float Product = FVector::DotProduct(CurrentDirection, Direction);
if (Product < -0.9f)
{
	RotateVelocity = RotateSpeed;
}
else
{
	float CrossZ = CurrentDirection.X * Direction.Y - CurrentDirection.Y * Direction.X;
    RotateVelocity = RotateSpeed * CrossZ;
}

Rotation.Yaw += RotateVelocity * DeltaTime;

 

결과물

 

 

자연스럽게 움직이는 모습을 볼 수 있다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함