(UE5) 13. 라인 트레이스로 총격전 실행

0. 소개

슈팅 FPS나 TPS 게임을 많이 해봤다면 두 가지 주요 슈팅 방법이 있다는 것을 알고 있을 것입니다.

그 중 두 가지는 다음과 같습니다.

  • 발사체 방법
  • 레이캐스트 방법

발사체 방법

발사체 방식을 사용하면 실제로 발사체(예: 총알)를 쏘기 때문입니다.

  • 발사체에 따른 중력 적용
  • 발사체 속도

다음과 같은 다른 글머리 기호 속성을 적용할 수 있습니다.

이 발사체 방식을 사용하는 게임의 대표적인 예는 싸움터”있습니다.

레이캐스트 방식

출시는 RayCast 프로세스를 기반으로 합니다.

즉각적인 피드백받을 수 있습니다.

따라서 발사체 방식과 달리 중력, 속도 등의 속성이 각 발사체를 구분하지 않고 동일하게 적용된다.

RayCast 방식을 사용하는 대표적인 게임은 “카운터 스트라이크”있습니다.

이 글에서는 두 RayCast 방식을 사용하여 총격을 구현합니다.

1. 레이캐스트란?

레이 캐스트 실제 구현 방법에 들어가면 매우 복잡해집니다.

기본 개념은 간단합니다.

아래 이미지를 보면 먼저 물체 A에서 전방으로 빔(빔)이 발사됩니다.

그러면 발사된 광선은 어딘가에 부딪쳤을 것입니다.

물체가 B라고 가정하면 그 지점에서 B는 광선이 부딪힌 결과이므로 타격 결과라고 합니다.


레이 캐스트의 간단한 예

위와 같이 특정 물체에서 광선을 방출하여 특정 물체를 찾는 것을 레이 캐스트라고 합니다.

이 방법은 객체를 찾는 것뿐만 아니라 광선 끝에 특정 메쉬를 그리는 등 여러 가지 방법으로 사용할 수 있습니다.

2. 시행

2-1 입력 설정

다음 기사를 읽기 전에 이전 기사를 읽으십시오.
(UE5) 10. 무기 액터 만들기
(UE5) 11. 스켈레탈 메시 소켓으로 무기 설치

저는 무기를 발사할 때 마우스 왼쪽 버튼을 눌렀다 떼기 위해 많은 제어 방법을 사용합니다.

따라서 UE에서 지원하는 액션 매핑을 사용합니다.

액션 맵은 “키를 눌렀다 떼는” 액션을 만드는 좋은 방법입니다.


예: 점프(스페이스바 누르기), 앉기(Ctrl 누르기)

아래와 같이 action mapping에 “Fire”를 추가하고 “Left Mouse Button” 버튼에 할당해 봅시다.


액션 매핑 설정

그런 다음 C++ 코드로 돌아가서 추가된 “발사” 작업을 바인딩합니다.

// APlayerCharacter.h 파일
protected:
	void StartFire() const;
	void StopFire() const;
// APlayerCharacter.cpp 파일
void APlayerCharacter::StartFire()
{

}

void APlayerCharacter::StopFire()
{

}

void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	// "Fire" Action Mappings에 대해 메서드 바인딩
	PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &APlayerCharacter::StartFire);
	PlayerInputComponent->BindAction("Fire", IE_Released, this, &APlayerCharacter::StopFire);
}

BindAction 메서드를 작업 맵에 대한 바인딩으로 사용하는 것을 볼 수 있습니다.

여기에서 IE_Pressed 및 IE_Released라는 매개변수를 볼 수 있으며 해당 값은 EInputEvent라는 열거형 값을 사용합니다.

EInputEvent는 열거형 클래스가 아닌 열거형입니다.


따라서 EInputEvent::IE_Pressed가 아닌 IE_Pressed로 액세스할 수 있습니다.

자세한 내용은 C++11 열거형 클래스를 찾습니다!
!

//
//	EInputEvent
//
UENUM( BlueprintType, meta=(ScriptName="InputEventType"))
enum EInputEvent
{
	IE_Pressed              =0,
	IE_Released             =1,
	IE_Repeat               =2,
	IE_DoubleClick          =3,
	IE_Axis                 =4,
	IE_MAX                  =5,
};

변수 이름으로

  • 눌렸다는 것은 눌렀을 때
  • 버튼을 놓으면 해제됨

당신은 그것을 볼 수 있습니다

따라서 바인딩 코드의 해석은 다음과 같습니다.

  • 마우스 왼쪽 버튼을 눌렀을 때 : StartFire 메소드 실행
  • 마우스 왼쪽 버튼을 놓을 때: StopFire 메소드가 실행됩니다.

// "Fire" Action Mappings에 대해 메서드 바인딩
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &APlayerCharacter::StartFire);
PlayerInputComponent->BindAction("Fire", IE_Released, this, &APlayerCharacter::StopFire);

2-2 StartFire 및 StopFire 메서드 구현

시작하기 전에 이전에 무기를 부착하기 위한 코드를 작성했습니다.

// APlayerCharacter.cpp 파일

void APlayerCharacter::AttachWeapon(TSubclassOf<class AWeapon> weapon)
{
	if (weapon)
	{
		_equipWeapon = GetWorld()->SpawnActor<AWeapon>(weapon);

		const USkeletalMeshSocket* weaponSocket = GetMesh()->GetSocketByName("WeaponSocket");

		if (_equipWeapon && weaponSocket)
		{
			weaponSocket->AttachActor(_equipWeapon, GetMesh());
		}
	}
}

멤버 변수 _equipWeapon이 여기에 있습니다.

현재 장비하고 있는 무기에 대한 정보가 있습니다.

그리고 무기마다 발사 기능이 다르기 때문에 Weapon Actor 내부에 정의하는 것이 맞을 것 같아서 _equipWeapon의 메소드를 호출하여 발사 기능을 구현하도록 하겠습니다.

이와 관련하여 StartFire() 메소드의 구현은 다음과 같다.

매개변수 값으로 그만큼추가 이유는 2-3의 “돌격소총 구현”에서 설명합니다.

void APlayerCharacter::StartFire()
{
	if (_equipWeapon)
	{
		_equipWeapon->StartFire(this);
	}
}

발사 기능이 무기 액추에이터 안에 있기 때문에 자연스럽게 사격을 멈출 수 있는 기능도 무기 액추에이터 안에 있으면 좋을 것 같습니다.

StopFire() 메서드는 다음과 같이 정의할 수도 있습니다.

void APlayerCharacter::StopFire()
{
	if (_equipWeapon)
	{
		_equipWeapon->StopFire();
	}
}

2-3 무기 작동기에서 StartFire 및 StopFire 구현

이 글은 당신이 아래 정보를 읽었다고 가정합니다.


클래스 구조는 다음 글에도 설명되어 있으니 아래 내용을 한 번 정독하신 후 읽어보시기 바랍니다.


(UE5) 10. 무기 액터 만들기

Weapon.h 파일 배포

무기 액터를 상속 구조로 만듭니다.


웨폰 액터 상속 구조

따라서 일반적으로 사용되는 무기 발사 시작 및 중지 방법은 무기 클래스에 있습니다.

순수하지 않은 가상 함수로 만들고 아이들이 덮어쓰도록 하겠습니다.

그리고 무기마다 발사 방법다를 수 있으므로 열거형 클래스로 FireType을 정의했습니다.

이러한 모든 요소를 ​​포함하는 Weapon.h의 전체 코드는 다음과 같습니다.

StartFire() 및 StopFire() 가상 기능그것이 무엇인지 조심하십시오!

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Weapon.generated.h"

/** 발사 타입 */
UENUM(BlueprintType)
enum class EFireType : uint8
{
	EF_LineTrace	UMETA(DisplayName = "Line Trace"),
	EF_Projectile	UMETA(DisplayName = "Projectile"),
};

UCLASS()
class TPS_PROTOTYPE_API AWeapon : public AActor
{
	GENERATED_BODY()
	

public:	
	// Sets default values for this actor's properties
	AWeapon();
	inline int GetAmmoMaxCount() { return _ammoMaxCount; }
	inline float GetReloadingDelayTime() { return _reloadingDelayTime; }
protected:
	/** 액터의 스켈레톤 매시*/
	UPROPERTY(EditAnywhere, meta = (AllowPrivateAccess = "true"))
	class USkeletalMeshComponent* SkeletalMeshComponent;

	/** 탄약 최대 개수 */
	UPROPERTY(EditAnywhere,Category ="Weapon Properties", meta = (AllowPrivateAccess = "true"))
	int _ammoMaxCount = 30;

	/** 현재 소지한 탄약의 개수*/
	UPROPERTY(EditAnywhere, Category = "Weapon Properties", meta = (AllowPrivateAccess = "true"))
	int _ammoRemainCount;

	/** 재장전까지 걸리는 시간 */
	UPROPERTY(EditAnywhere, Category = "Weapon Properties", meta = (AllowPrivateAccess = "true"))
	float _reloadingDelayTime = 3.f;

	/** 발사 간의 간격 */
	UPROPERTY(EditAnywhere, Category = "Weapon Properties", meta = (AllowPrivateAccess = "true"))
	float _fireInterval = 0.1f;

	/** Line Trace의 Ray 길이 */
	UPROPERTY(EditAnywhere, Category = "Weapon Properties", meta = (AllowPrivateAccess = "true"))
	float _traceDistance = 1000.f;

	/** 발사 타입 */
	UPROPERTY(EditAnywhere, Category = "Weapon Properties", meta = (AllowPrivateAccess = "true"))
	EFireType _fireType = EFireType::EF_LineTrace;

	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	/** 발사를 시작하는 메서드 */
	virtual void StartFire(const class ACharacter* owner);

	/** 발사를 멈추는 메서드*/
	virtual void StopFire();

	/** 재장전 메서드*/
	virtual void Reloading();
	// Called every frame
	virtual void Tick(float DeltaTime) override;
};

2-4 AssaultRifle::StartFire 메소드 구현

먼저 AssaultRifle::StartFire 메서드를 구현합니다.

가장 먼저 기억해야 할 것은 사용자가 발사 버튼(마우스 왼쪽 버튼)을 누르고 있으면 발사 사이에 무기가 발사되어야 한다는 것입니다.

Unity에서는 코루틴을 사용하여 구현될 것이라고 생각하여 UE에서 동일한 기능을 수행할 수 있는 방법을 찾았습니다.

GetWorldTimerManager().SetTimer

나는 방법을 찾았고 그것으로 그것을 구현할 것입니다

아래의 공식 문서에 잘 설명되어 있습니다.


공식 문서를 읽고 아래 코드를 보면 이해하기 쉽습니다.

게임플레이 타이머

설정된 간격으로 작업을 수행하는 타이머 구조입니다.

docs.unrealengine.com

UCLASS()
class TPS_PROTOTYPE_API AAssaultRifle : public AWeapon
{
	GENERATED_BODY()
private:
	/** 발사 기능 타이머용 변수 
    	* 이 변수에 적용된 타이머에 대한 정보를 가지고 있다고 보면 된다.

*/ FTimerHandle FireTimerHandle; }
void AAssaultRifle::StartFire(const ACharacter* owner)
{
	if (owner)
	{
		switch (_fireType)
		{
			case EFireType::EF_LineTrace:
                        GetWorldTimerManager().SetTimer(
                                FireTimerHandle, // 해당 타이머를 관리하는 변수
                                (owner, this)() { FireWithLineTrace(owner); },  // 호출할 메서드
                                _fireInterval,// 호출 간 간격
                                true); // 루프 여부
			break;
			case EFireType::EF_Projectile:
			break;
		}
	}
}

음, 타이머에 의해 주어진 주기에서 람다와 함께 (owner, this)() { FireWithLineTrace(owner); }.

람다에 입력되는 파라미터 값은 다음과 같습니다.


&&상기하다

지금 FireWithLineTrace(owner) 메서드에서 Ray가 어떻게 사용되는지 보자!
!

2-5 라인 트레이스

Launch 기능은 RayCast 방식을 사용하여 구현되므로 UE는 RayCast를 지원합니다.

LineTraceSingleByChannel사용할 것이다

LineTrace를 보면 싱글과 멀티가 있습니다.

이 둘의 차이점은 적중 결과가 단수인지 복수인지 여부입니다.

// Single은 OutHit이 하나이다.

bool UWorld::LineTraceSingleByChannel(struct FHitResult& OutHit,const FVector& Start,const FVector& End,ECollisionChannel TraceChannel,const FCollisionQueryParams& Params /* = FCollisionQueryParams::DefaultQueryParam */, const FCollisionResponseParams& ResponseParam /* = FCollisionResponseParams::DefaultResponseParam */) const { return FPhysicsInterface::RaycastSingle(this, OutHit, Start, End, TraceChannel, Params, ResponseParam, FCollisionObjectQueryParams::DefaultObjectQueryParam); } // Multi는 OutHits가 배열이다.

bool UWorld::LineTraceMultiByChannel(TArray<struct FHitResult>& OutHits,const FVector& Start,const FVector& End,ECollisionChannel TraceChannel,const FCollisionQueryParams& Params /* = FCollisionQueryParams::DefaultQueryParam */, const FCollisionResponseParams& ResponseParam /* = FCollisionResponseParams::DefaultResponseParam */) const { return FPhysicsInterface::RaycastMulti(this, OutHits, Start, End, TraceChannel, Params, ResponseParam, FCollisionObjectQueryParams::DefaultObjectQueryParam); }

UWorld::LineTraceSingleByChannel

특정 채널을 사용하여 세계에 대한 빔을 추적하고 첫 번째 차단 히트를 반환합니다.

docs.unrealengine.com

빔이 발사되는 시작점

LineTrace에는 빔이 발사되는 시작점의 정의가 필요합니다.

무기의 길이와 총구의 위치에 따라 시작점이 달라지므로 스켈레탈 메시의 소켓 위에 시작점을 두고 현재 구현된 돌격소총 스켈레탈 메시에 FireSocket을 생성합니다.


웨폰 액터 스켈레톤 메시의 FireSocket 위치

스켈레톤 메쉬의 GetSocketLocation 메서드를 통해 소켓의 위치를 ​​시작 벡터에 할당합니다.

const FVector start = _skeletalMeshComponent->GetSocketLocation("FireSocket");

빔이 발사되는 끝점

이제 광선의 끝점을 만들어야 하지만 광선의 끝점은

((사용자가 현재 조준하고 있는 방향 벡터 * 빔의 총 길이) + 시작 벡터의 위치).

광선의 길이만큼 방향 벡터를 늘리고 시작 벡터를 사용하여 평행 이동

사용자가 조준하는 방향 벡터는 사용자 컨트롤러 회전 값의 벡터때문에 아래와 같이 구할 수 있습니다.

FRtoator::Vector() 공식 문서그것을 보면 다음과 같이 설명된다.


“회전을 해당 방향을 가리키는 단위 벡터로 변환”

const FVector end = (ownerController->GetControlRotation().Vector() * _traceDistance) + start;

FCollisionQueryParams

Ray는 도중에 현재 개체와 충돌 반응을 나타내지 않아야 하므로

FCollisionQueryParams::AddIgnoredActor()를 통해 자신을 무시하도록 지정합니다.

FCollisionQueryParams의 공식 문서

FCollisionQueryParams collisionParams;
collisionParams.AddIgnoredActor(this);

DrawDebugLine

사실 Ray가 정상적으로 실행되고 있는지 확인하기 위해 DebugLine을 그려보면 좋을 것 같습니다.

위에서 얻은 시작 벡터와 종료 벡터를 통해 디버깅 라인을 그립니다.

DrawDebugLine(GetWorld(), start, end, FColor::Red, false, 1.0f);

UWorld::LineTraceSingleByChannel 메서드 구현

이제 UWorld::LineTraceSingleByChannel 메서드를 호출하여 실제로 빔을 발사합니다.

UWorld에 따라 GetWorld()를 통해 LineTraceSingleByChannel 메서드를 호출해 보겠습니다.

ECCollisionChannel은 보이는 모든 것이 적중되어야 하기 때문에 ECC_Visibility로 설정됩니다.

그 이유는
Ray가 게임에 보이는 특정 물체를 건드리면 총알이 물체에 부딪힌 흔적이 나타나기 때문입니다.

const UWorld* currentWorld = GetWorld();
if (currentWorld)
{
    DrawDebugLine(currentWorld, start, end, FColor::Red, false, 1.0f);

	// 명중!
if (currentWorld->LineTraceSingleByChannel( hitResult, start, end, ECC_Visibility, collisionParams)) { } }

적중 시 디버그 처리

LineTrace가 적중(히트!
)되면 제대로 적중되었는지 확인하기 위해 적중 액터의 이름을 에디터 디스플레이에 표시합니다.

if (hitResult.GetActor())
{
    auto* hitActor = hitResult.GetActor();
    GEngine->AddOnScreenDebugMessage(-1, 1.f, FColor::Red, FString::Printf(TEXT("Hit Actor Name: %s"), *hitActor->GetName()));
}

이것은 FireWithLineTrace 메서드에 필요한 모든 코드를 구현했습니다.

void AAssaultRifle::FireWithLineTrace(const ACharacter* owner)
{
	if (_ammoRemainCount <= 0)
	{
		StopFire();
	}

	if (owner)
	{
		const AController* ownerController = owner->GetController();

		if (ownerController)
		{
			const FVector start = SkeletalMeshComponent->GetSocketLocation("FireSocket");
			const FVector end = (ownerController->GetControlRotation().Vector() * _traceDistance) + start;
			FHitResult hitResult;
			FCollisionQueryParams collisionParams;
			collisionParams.AddIgnoredActor(this);

			DrawDebugLine(GetWorld(), start, end, FColor::Red, false, 1.0f);
			if (GetWorld()->LineTraceSingleByChannel(
				hitResult,
				start,
				end,
				ECC_Visibility,
				collisionParams))
			{
				if (hitResult.GetActor())
				{
					auto* hitActor = hitResult.GetActor();
					GEngine->AddOnScreenDebugMessage(-1, 1.f, FColor::Red,FString::Printf(TEXT("Hit Actor Name: %s"),*hitActor->GetName()));
				}
			}
		}
	}
}

2-6 AssaultRifle::StopFire 메소드 구현

StopFire는 매우 간단합니다.

AssaultRifle::StartFire 메서드에서 재생 중인 타이머를 지우기만 하면 됩니다.

void AAssaultRifle::StopFire()
{
	// ClearTimer를 통해 FireTimerHandle에서 재생되고 있는 타이머를 종료한다.

GetWorldTimerManager().ClearTimer(FireTimerHandle); }

3. 결과

DebugDrawLine을 통해 광선이 나가는 것을 볼 수 있으며 적중 액터의 이름도 디스플레이에 잘 각인되는 것을 볼 수 있습니다.

발사 버튼을 누르고 있으면 폭발적으로 발사되는 것을 볼 수 있고 발사 버튼에서 손을 떼면 발사가 중지되는 것을 볼 수 있습니다.