본문 바로가기

개발 공부 기록/UnrealEngine5

이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 7

개요

'이득우의 언리얼 C++ 게임 개발의 정석'의 9장 충돌 설정과 대미지 전달 내용을 바탕으로 하고 있습니다.

알라딘 책 구매 링크

 

이득우의 언리얼 C++ 게임 개발의 정석

언리얼 엔진 학습에 목말라하는 게임 개발자에게 단비 같은 언리얼 엔진 프로그래밍 책이다. 에픽게임즈 본사의 개발자 프로그램 언리얼 데브 그랜트를 수상한 저자의 책으로, 언리얼 엔진의

www.aladin.co.kr

 

본문

다양한 물리 효과를 구현하려면 콜리전이라 불리는 물리적 충돌 영역을 설정해야 한다.

언리얼 엔진에서 콜리전은 크게 세 가지 방법으로 제작할 수 있다.

  • 스태틱메시 애셋: 스태틱메시 애셋에 콜리전 영역을 심는 방법이다. 스태틱메시 애셋에 콜리전을 심으면 스태틱메시 컴포넌트에서 비주얼과 충돌이라는 두 가지 기능을 설정할 수 있어 관리가 편해진다.
  • 기본 도형 컴포넌트: 구체, 박스, 캡슐의 기본 도형을 사용해 충돌 영역을 지정하는 방법이다. 스태틱메시와 별도로 충돌 영역을 제작하는 데 사용한다. 스켈레탈 메시를 움직일 때 주로 사용한다.
  • 피직스 애셋: 일반적으로 캐릭터의 이동은 캡슐 컴포넌트를 사용해 처리한다. 하지만 특정 상황에서 캐릭터의 각 관절이 움직이는 RagDoll 효과를 구현할 때 이 피직스 애셋을 사용한다. 캐릭터의 각 부위에 기본 도형으로 충돌 영역을 설정하고 이를 연결해 캐릭터의 물리를 설정한다. 피직스 애셋은 스켈레탈 메시에만 사용할 수 있다.

콜리전이 심어진 충돌체에는 반드시 하나의 콜리전 채널을 설정해야 한다. 언리얼 엔진은 8개의 기본 콜리전 채널을 제공하고 있다.
각 채널의 용도는 다음과 같다.

  • WorldStatic: 움직이지 않는 정적인 배경 액터에 사용하는 콜리전 채널이다.
  • WorldDynamic: 움직이는 액터에 사용하는 콜리전 채널이다.
  • Pawn: 플레이어가 조종하는 물체에 주로 사용한다. 캐릭터의 충돌을 담당하는 캡슐 컴포넌트에 설정된다.
  • Visibility: 배경 물체가 시각적으로 보이는지 탐지하는 데 사용한다. 탐지에서 폰은 제외된다.
    마우스로 물체를 선택하는 피킹 기능을 구현할 때 사용한다.
  • Camera: 카메라 설정을 위해 카메라와 목표물 간에 장애물이 있는지 탐지하는 데 사용한다.
  • PhysicsBody: 물리 시뮬레이션으로 움직이는 컴포넌트에 설정한다.

만들어둔 캐릭터에 할당된 캡슐 컴포넌트를 클릭하고 콜리전→콜리전 프리셋을 보면 Pawn으로 설정되어 있다.

 

콜리전 활성화됨(Collision Enabled) 항목을 보면 세 가지 설정을 할 수 있다.

  • Query: 두 물체의 충돌 영역이 서로 겹치는지 테스트하는 설정이다. 충돌 영역의 겹침을 감지하는 것은 언리얼 엔진에서 오버랩이라 부르며, 충돌 영역이 겹치면 관련 컴포넌트에 BeginOverlap 이벤트가 발생한다. 지정한 영역에 물체가 충돌하는지 탐지하는 레이캐스트나 스윕 기능도 Query에 속한다.
  • Physics: 물리적인 시뮬레이션을 사용할 때 설정한다.
  • Query and Physics: 위의 두 기능을 모두 사용하는 설정이다.

Query and Physics 설정을 사용하면 모든 기능이 잘 동작하지만 물리 엔진이 수행할 계산량이 많아진다. 따라서 각 액터마다 필요한 기능이 무엇인지를 잘 파악하고 설계하여 Query나 Physics만 설정하는 것이 도움이 된다. 그리고 Query 기능을 사용하는 경우 관련 이벤트가 발생하도록 위의 오버랩 이벤트 생성(Generates Overlap Events) 옵션을 활성화 한다.

 

마지막으로 해당 컴포넌트에 설정된 콜리전 채널이 상대방 컴포넌트의 콜리전 채널과 어떻게 반응할지 지정하는 작업이 필요하다.
다른 콜리전 채널과의 반응에 무시, 겹침, 블록이라는 세 가지 값을 지정할 수 있다.

  • 무시Ignore: 콜리전이 있어도 아무 충돌이 발생하지 않는다.
  • 겹침Overlap: 무시와 동일하게 물체가 뚫고 지나갈 수 있지만 이벤트를 발생시킨다.
  • 블록Block: 물체가 뚫고 지나가지 못하도록 막는다.

기본으로 제공하는 것 외에도 개발을 하다보면 직접 커스텀한 콜리전 채널이 필요한 경우가 존재한다.
프로젝트 세팅 → 콜리전으로 가면 새로운 물리 설정을 할 수 있는 콜리전 설정 메뉴가 존재한다.

콜리전 채널은 오브젝트 채널과 트레이스 채널로 나뉜다.

  • 오브젝트 채널: 콜리전 영역에 지정하는 콜리전 채널
  • 트레이스 채널: 어떤 행동에 설정하는 콜리전 채널

오브젝트 채널에서 ‘새 오브젝트 채널’을 클릭해서 새롭게 오브젝트 채널을 추가할 수 있고 이 오브젝트 채널이 다른 채널과 문제가 없도록
직접 프리셋을 설정할 수 있다.

하단의 프리셋에서 ‘새 프로파일’을 누르면 아래와 같이 직접 새로운 프리셋을 만들 수 있는 창이 나온다.

 

기존에 만들어진 Trigger 프리셋이 새로 만든 ABCharacter 프리셋과 충돌이 일어났을 때 겹침의 효과를 내려면 Trigger 프리셋을 클릭하여 ABCharacter에 대한 콜리전 반응을 오버랩으로 수정하면 된다.

 

캐릭터에 대한 프리셋 채널을 생성했을 때 신경써야 할 프리셋들은 다음과 같다.

  • OverlapAll: 겹침으로 설정한다
  • OverlapAllDynamic: 겹침으로 설정한다.
  • IgnoreOnlyPawn: 폰만 충돌을 무시하도록 설정한다. 무시로 설정한다.
  • OverlapOnlyPawn: 폰에만 겹침 이벤트가 발생하도록 설정한다. 겹침으로 설정한다.
  • Spectator: 외부 관중과의 충돌을 설정한다. 무시로 설정한다.
  • CharacterMesh: 캐릭터 메시에 사용되는 물리 설정이다. 무시로 설정한다.
  • RagDoll: 스켈레탈 메시의 피직스 애셋 물리를 가동하기 위한 물리 설정이다. 무시로 설정한다.
  • Trigger: 지정한 영역에 물체가 들어오면 이벤트가 발동하는 용도로 설정한다. 겹침으로 설정한다.
  • UI: UI 요소에 사용하는 설정이다. 겹침으로 설정한다.

설정을 마쳤으면 캡슐 컴포넌트가 해당 프리셋을 사용하도록 코드로 기본값을 변경한다.

// ABCharacter.cpp

AABCharacter::AABCharacter()
{
	// ... 생성자 내부 생략 ..
	GetCapsuleComponent()->SetCollisionProfileName(TEXT("ABCharacter"));
}

 

트레이스 채널을 사용해 물리적 충돌 여부를 가리는 함수 중 하나로 SweepSingleByChannel이 있다. 물리는 월드의 기능이므로
GetWorld() 함수를 사용해 월드에게 명령을 내려야 한다.

해당 함수는 기본 도형을 인자로 받은 후 시작 지점에서 끝 지점까지 휩쓸면서 해당 영역 내에 물리 판정이 일어났는지를 조사한다.
이에 필요한 파라미터와 그 의미는 다음과 같다.

  • HitResult: 물리적 충돌이 탐지된 경우 관련된 정보를 담을 구조체
  • Start: 탐색을 시작할 위치
  • End: 탐색을 끝낼 위치
  • Rot: 탐색에 사용할 도형의 회전
  • TraceChannel: 물리 충돌 감지에 사용할 트레이스 채널 정보
  • CollisionShape: 탐색에 사용할 기본 도형 정보. 구체, 캡슐, 박스를 사용한다.
  • Params: 탐색 방법에 대한 설정 값을 모아둔 구조체
  • ResponseParams: 탐색 반응을 설정하기 위한 구조체
void AABCharacter::PostInitializeComponents()
{
	// ...
	// 애니메이션 인스턴스에 추가한 OnAttackHitCheck 델리게이트에 AttackCheck 함수를 추가함
	ABAnim->OnAttackHitCheck.AddUObject(this, &AABCharacter::AttackCheck);
}

void AABCharacter::AttackCheck()
{
	FHitResult HitResult;
	FCollisionQueryParams Params(NAME_None, false, this);
	bool bResult = GetWorld()->SweepSingleByChannel(
		HitResult,
		GetActorLocation(),
		GetActorLocation() + GetActorForwardVector() * 200.0f,
		FQuat::Identity,
		ECollisionChannel::ECC_GameTraceChannel2,
		FCollisionShape::MakeSphere(50.0f),
		Params
	);

	if (bResult)
	{
		// UE4에서는 HitResult.Actor.IsValid()로 되어있다.
		if (IsValid(HitResult.GetActor()))
		{
			ABLOG(Warning, TEXT("Hit Actor Name : %s"), *HitResult.GetActor()->GetName());
		}
	}
}

 

콜리전에 대한 자세한 내용은 아래 링크를 참고하면 도움이 될 것이다.

언리얼 엔진의 콜리전 개요 공식 문서

 

공격을 할 때마다 로그를 통해 확인 하는 것은 직관적이지 못하므로 시각적으로 확인할 수 있도록 언리얼 엔진에서는
디버그 드로잉 기능을 제공한다.

디버그 드로잉 기능을 사용하려면 ‘DrawDebugHelpers.h’ 헤더를 추가하여 사용할 수 있다.

헤더 파일에 AttackRange와 AttackRadius 값을 변수로 선언해주고 난 다음 구현을 적용하면 공격 범위에 해당하는 만큼 공격 판정이
발생하면 녹색으로, 맞지 않으면 빨간색 캡슐로 표현된다.

// AttackRange = 200.0f, AttackRadius = 5.0f

void AABCharacter::AttackCheck()
{
	FHitResult HitResult;
	FCollisionQueryParams Params(NAME_None, false, this);
	bool bResult = GetWorld()->SweepSingleByChannel(
		HitResult,
		GetActorLocation(),
		GetActorLocation() + GetActorForwardVector() * AttackRange,
		FQuat::Identity,
		ECollisionChannel::ECC_GameTraceChannel2,
		FCollisionShape::MakeSphere(AttackRadius),
		Params
	);

#if ENABLE_DRAW_DEBUG

	FVector TraceVec = GetActorForwardVector() * AttackRange;
	FVector Center = GetActorLocation() + TraceVec * 0.5f;
	float HalfHeight = AttackRange * 0.5f + AttackRadius;
	FQuat CapsuleRot = FRotationMatrix::MakeFromZ(TraceVec).ToQuat();
	FColor DrawColor = bResult ? FColor::Green : FColor::Red;
	float DebugLifeTime = 5.0f;

	DrawDebugCapsule(GetWorld(), Center, HalfHeight, AttackRadius, CapsuleRot, DrawColor, false, DebugLifeTime);

#endif

	if (bResult)
	{
		if (IsValid(HitResult.GetActor()))
		{
			ABLOG(Warning, TEXT("Hit Actor Name : %s"), *HitResult.GetActor()->GetName());
		}
	}
}

공격 판정이 적중하면 녹색 캡슐, 아니라면 빨간색 캡슐이 발생한다.

 

공격 판정에 이어서 공격 대미지 처리를 할 때 언리얼 엔진에서 제공하는 대미지 프레임워크를 사용하면 간편하게 처리할 수 있다.
액터 클래스 AActor의 TakeDamage 함수를 사용하면 되고 각 4가지 매개변수 인자가 존재한다.

  • DamageAmount: 전달할 대미지의 세기
  • DamageEvent: 대미지 종류
  • EventInstigator: 공격 명령을 내린 가해자
  • DamageCauser: 대미지 전달을 위해 사용한 도구

대미지를 가한 가해자는 폰이 아니라 폰에게 명령을 내린 플레이어 컨트롤러이므로 EventInstigator에는 폰이 아닌 컨트롤러의 정보를 전달해야 한다.

// ABCharacter.cpp

#include "Engine/DamageEvent.h"

void AABCharacter::AttackCheck()
{
	FHitResult HitResult;
	FCollisionQueryParams Params(NAME_None, false, this);
	bool bResult = GetWorld()->SweepSingleByChannel(
		HitResult,
		GetActorLocation(),
		GetActorLocation() + GetActorForwardVector() * AttackRange,
		FQuat::Identity,
		ECollisionChannel::ECC_GameTraceChannel2,
		FCollisionShape::MakeSphere(AttackRadius),
		Params
	);

#if ENABLE_DRAW_DEBUG

	FVector TraceVec = GetActorForwardVector() * AttackRange;
	FVector Center = GetActorLocation() + TraceVec * 0.5f;
	float HalfHeight = AttackRange * 0.5f + AttackRadius;
	FQuat CapsuleRot = FRotationMatrix::MakeFromZ(TraceVec).ToQuat();
	FColor DrawColor = bResult ? FColor::Green : FColor::Red;
	float DebugLifeTime = 5.0f;

	DrawDebugCapsule(GetWorld(), Center, HalfHeight, AttackRadius, CapsuleRot, DrawColor, false, DebugLifeTime);

#endif

	if (bResult)
	{
		if (IsValid(HitResult.GetActor()))
		{
			ABLOG(Warning, TEXT("Hit Actor Name : %s"), *HitResult.GetActor()->GetName());
			
			// Engine/DamageEvent.h 포함필수
			FDamageEvent DamageEvent;
			HitResult.GetActor()->TakeDamage(50.0f, DamageEvent, GetController(), this);
		}
	}
}

 

액터에게 대미지를 전달한 다음에는 실제로 대미지를 처리하는 코드를 추가로 구현한다.

로그로 찍어서 확인하기 위해 기존의 TakeDamage 함수를 오버라이드 하여 로그를 찍어본다.

// ABCharacter.h

virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;
// ABCharacter.cpp

float AABCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	float FinalDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
	ABLOG(Warning, TEXT("Actor : %s took Damage : %f"), *GetName(), FinalDamage);
	return FinalDamage;
}

 

만약 액터에 대미지를 받게 하기 싫다면 액터 속성에 있는 대미지 받기 가능 여부를 체크 해제 하도록 한다.

 

죽는 애니메이션 처리를 위해 애님인스턴스 클래스에 IsDead라는 변수를 설정추가하고 애니메이션 블루프린트를 수정한다.

// ABAnimInstance.h

public:
	void SetDeadAnim() { IsDead = true;  }
	
private:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Pawn, meta = (AllowPrivateAccess = true))
bool IsDead;
// ABAnimInstance.cpp

UABAnimInstance::UABAnimInstance()
{
	// ... 
	IsDead = fasle;
}

 

이제 IsDead 변수가 true가 되면 캐릭터는 죽는 애니메이션 동작을 재생한다.

캐릭터가 죽은 이후에 충돌 이벤트를 실행하지 않으려면 SetActorEnableCollision 함수를 사용하여 액터의 충돌 설정을 끄도록 한다.

아래 코드는 대미지를 0이상을 받으면 즉시 캐릭터를 죽게 만들고 충돌 이벤트를 꺼주는 코드이다.

// ABCharacter.cpp

float AABCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	float FinalDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
	ABLOG(Warning, TEXT("Actor : %s took Damage : %f"), *GetName(), FinalDamage);

	if (FinalDamage > 0.0f)
	{
		ABAnim->SetDeadAnim();
		SetActorEnableCollision(false);
	}

	return FinalDamage;
}

캐릭터가 대미지를 받으면 죽는 애니메이션과 함께 녹색 캡슐이 발생했고, 그 후로는 충돌 이벤트가 발생하지 않아 빨간색 캡슐이 나온다.