언리얼 엔진 5/개발 일지

[UE5] 블루아카이브 TPS게임 개발일지 (11) - 라인 트레이싱

ciel45 2023. 12. 19. 22:50

이제 사격 시 라인 트레이싱을 통해 명중 여부를 판별하는 코드를 작성할 것이다.

 

일반적인 생각으로는 탄환이 총구에서 나가는 것이 맞지만, 게임 로직의 면에서는 조금 다르다.

 

탄환은 카메라 시점을 기준으로 앞으로 쭉 뻗어나가야 한다.

 

TPS 게임이든, FPS 게임이든 총구의 방향은 정면이 아닌, 살짝 기울어진 경우가 대부분이다.

따라서 라인트레이싱을 총구의 방향 그대로 하면 화면 정중앙에 적을 맞추고 쏴도 맞지 않는 불합리한 경우가 생긴다.

 

따라서 라인트레이싱의 시작점을 카메라의 위치(TPS게임이므로 살짝 더 앞)으로,

종료점을 시작점 + 카메라의 정면 벡터 * 사거리로 할 예정이다.

 

이어서 코드를 살펴보도록 하자.

 

void AKannaCharacter::Fire() // 여기서는 상태 전환, 애니메이션만 재생
{
	if (ActionState != EActionState::EAS_Aiming) return; // 조준 중일 때만 사격 가능
		
	UAnimInstance * AnimInstance = GetMesh()->GetAnimInstance();

	if (AnimInstance && FireMontage)
	{
		AnimInstance->Montage_Play(FireMontage);
	}

	FVector StartPoint = ViewCamera->GetComponentLocation() + ViewCamera->GetForwardVector() * 100;
	FVector Direction = ViewCamera->GetForwardVector();

	if(CurrentWeapon)
		CurrentWeapon->Fire(StartPoint, Direction); // 총의 발사는 인터페이스에 delegate
}

캐릭터 .cpp의 Fire 함수이다.

 

Gun 클래스가 라인트레이싱의 시작점인 카메라에 대해 아는것, 즉 캐릭터의 컴포넌트에 의존성을 가지는 것은 디자인적으로 좋지 못하다. 따라서 이러한 것들은 캐릭터에서 계산하여 Gun 클래스에 인자로 넘겨주도록 하였다.

 

시작점에서 카메라 위치에  + ViewCamera->GetForwardVector() * 100를 해준 것은 TPS게임이므로 카메라와 캐릭터 사이의 거리가 살짝 있어 그것을 감안하여 앞쪽으로 옮겨준 것이다.

 

 

이제부터 사용할 주요 함수는 LineTraceSingleByChannel()이다.

해당 함수에 대한 자세한 설명은 여기를 참고하자. https://docs.unrealengine.com/5.0/en-US/API/Runtime/Engine/Engine/UWorld/LineTraceSingleByChannel/

 

UWorld::LineTraceSingleByChannel

Trace a ray against the world using a specific channel and return the first blocking hit

docs.unrealengine.com

 

 

보면, 함수의 포맷은 다음과 같다.

bool LineTraceSingleByChannel
(
    struct FHitResult & OutHit,
    const FVector & Start,
    const FVector & End,
    ECollisionChannel TraceChannel,
    const FCollisionQueryParams & Params,
    const FCollisionResponseParams & ResponseParam
) const

 

FHitResult를 레퍼런스로 넘겨주고 있다. 

FHitResult는 트레이스의 결과에 대한 여러가지 값들을 가지고 있다.

예를 들면 맞춘 액터/컴포넌트의 레퍼런스, 히트 포인트, 트레이스의 시작점과 끝점 등을 가지고 있다.

 

FHitResult를 레퍼런스로 넘겨주는 것은 결과값을 리턴받는 것과 비슷하다.

따라서 우리가 FHitResult 구조체를 만들어서 이 함수의 매개변수로 넣어주면, 적당한 값이 그 구조체에 작성되어 우리는 그 값을 사용할 수 있다.

 

2, 3번째 인자는 말그대로 시작점과 끝점이며, 

4번째 인자는 채널이다. 채널이란 히트를 판정할 수단을 의미한다. 여기서는 'Visibility'채널을 사용할 것이다. 눈에 보이는 것은 라인트레이싱으로 탐지하겠다는 의미이다.

 

5, 6번째 인자는 적절한 디폴트 매개변수가 들어가있으므로 생략한다.

아래는 Pistol.cpp에 구현된 라인트레이싱을 이용한 Fire 함수이다.

 

void APistol::Fire(FVector& StartPoint, FVector& Direction)
{
	UE_LOG(LogTemp, Warning, TEXT("PISTOL FIRE")); //로그에 출력

	FHitResult HitResult;
	FVector EndPoint = StartPoint + Direction * Range;

	if (GetWorld())
	{
		DrawDebugLine(GetWorld(), StartPoint, EndPoint, FColor::Red, true, -1.f, 0, 2.f);

		if (GetWorld()->LineTraceSingleByChannel(
			HitResult,
			StartPoint,
			EndPoint,
			ECollisionChannel::ECC_Visibility))
		{
			if (HitResult.GetActor())
			{
				GEngine->AddOnScreenDebugMessage(-1, 1.f, FColor::Red, FString::Printf(TEXT("%s"), *(HitResult.GetActor())->GetName()));
			}
		}
	}
}

LineTraceSingleByChannel은 UWorld의 멤버 함수이므로, GetWorld()를 사용한 것을 볼 수 있다.

 

또한 이 함수는 bool을 리턴하므로, 그 리턴 값을 이용하여 히트 확인 후, GetActor()를 통해 액터의 이름을 출력하는 것까지 확인할 수 있다.

 

그 위의 DrawDebugLine은 트레이싱을 시각화하기 위한 함수이다. 테스트 영상에서 보게 될것이다.

 

 

그리고 Enemy의 Collision을 조금 더 손봐주었다. Visibility 채널의 라인트레이싱에 막힐 수 있도록 Trace Responses 탭의 Visibility를 Block으로 설정한 것을 볼 수 있다.

 

 

아래는 지금까지의 작업의 테스트 영상이다. 로그 문구에 주목해보자.

 

 

사격 기능의 보완점들은 다음과 같다.

  • 조준점 표시
  • 적일 경우 히트 시 데미지 판정
  • 메쉬에 히트 시 파티클 이펙트
  • 피격 효과음