게임 개발 (언리얼 엔진)

UE4 생존게임 제작 - 7 (몬스터 공격 충돌 시스템 및 플레이어 상태 UI 제작)

언린이 2020. 11. 5. 16:38

1. 몬스터 공격 충돌 시스템 제작

 

UI를 제작하기 전에 플레이어의 피가 감소하는 모습을 보기 위해서 몬스터 공격 충돌 시스템을 먼저 제작하도록 하겠습니다. 몬스터 공격 충돌 시스템은 플레이어 충돌 시스템과 거의 유사합니다.

 

 

 

protected:
	UPROPERTY(Category = Collision, VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	UBoxComponent* AttackBox;

 

먼저 콜리전 세팅에서 AnimalAttack 채널과 프로파일을 만들었습니다. 그리고 나서 해당 프로파일을 적용할 박스 컴포넌트를 생성하였습니다.

 

 

	AttackBox->SetupAttachment(GetMesh(), TEXT("LION_-R-Hand_Socket"));
	AttackBox->SetBoxExtent(FVector(20.f, 20.f, 20.f));

 

그 다음 생성자에서 박스 컴포넌트를 소켓에 부착해주고 크기를 설정하였습니다. 박스의 크기는 나중에 블루프린트를 사용해 정밀하게 적용할 것입니다.

 

 

// 충돌을 빠져나갈 때, 호출될 델리게이트에 적용할 함수
void AAnimal::AttackEndOverlap(UPrimitiveComponent* OverlappedComponent,
	AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	// AttackEndOverlap이 계속 발동되어 설정해놓은 bool 변수
	if (bEndOverlapEnable)
	{
		// 이 변수를 false로 초기화해주고 Attack이 끝나면 노티파이를 이용해 다시 true로 바꾼다
		bEndOverlapEnable = false;

		FDamageEvent DamageEvent;

		// 여기서 OtherActor는 몬스터와 충돌한 객체, 즉 플레이어 객체이다
		float fDamage = OtherActor->TakeDamage(AnimalState.fAttack, DamageEvent, GetController(), this);
	}
}

 

충돌을 빠져나갈 때, 호출될 델리게이트에 적용할 함수입니다. 함수가 공격 한번에 한번씩만 불릴 수 있도록 bool 변수를 통해 막아주었습니다. 해당 bool 변수는 공격이 끝나면 다시 true로 바꿔줄 것입니다.

그리고 몬스터의 AttackBox는 플레이어의 박스 컴포넌트와만 충돌하도록 설정하였기 때문에 여기서 OtherActor는 플레이어입니다. 이 함수가 실행되면 플레이어의 TakeDamage 함수가 실행되어 플레이어에게 데미지를 줄 수 있습니다.

 

 

void ALion::BeginPlay()
{
	Super::BeginPlay();

	LionAnim = Cast<ULionAnim>(GetMesh()->GetAnimInstance());

	// 델리게이트에 생성한 함수 등록
	AttackBox->OnComponentBeginOverlap.AddDynamic(this, &AAnimal::AttackBeginOverlap);
	AttackBox->OnComponentEndOverlap.AddDynamic(this, &AAnimal::AttackEndOverlap);
}

 

델리게이트에 생성한 함수를 등록하였는데, 각각의 동물들마다 따로해주었습니다. 왜냐하면 몬스터들의 기본클래스에서 등록하면 아직 this가 생성되지 않았기 때문에 에러가 발생하기 때문입니다.

 

 

// 박스 컴포넌트의 충돌을 설정하는 함수
void AAnimal::EnableAttackCollision(bool bEnable)
{
	if (bEnable)
	{
		AttackBox->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
	}
	else
	{
		AttackBox->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	}
}

// bool 변수를 조작하는 함수
void AAnimal::SetEndOverlapEnable(bool bEndOverlap)
{
	bEndOverlapEnable = bEndOverlap;
}

 

EnableAttackCollision 함수를 이용해서 몬스터가 공격을할 때만 박스 컴포넌트가 충돌을 감지하도록 하였습니다. 왜냐하면 공격을 하지 않을때는 굳이 충돌을 감지하지 않아도 되기 때문입니다.

SetEndOverlapEnable 함수는 몬스터의 공격이 끝났을 때 호출될 노티파이에서 사용할 것인데, 공격을 한번씩만 적용시키기 위해 사용한 bool 변수를 true로 바꿔주기 위한 용도입니다.

 

 

// 공격이 시작할 때 호출되는 노티파이 함수
void UAnimalAnim::AnimNotify_AttackStart()
{
	AAnimal* pOwner = Cast<AAnimal>(TryGetPawnOwner());

	if (IsValid(pOwner))
		pOwner->EnableAttackCollision(true);
}

// 공격이 끝날 때 호출되는 노티파이 함수
void UAnimalAnim::AnimNotify_AttackEnd()
{
	AAnimal* pOwner = Cast<AAnimal>(TryGetPawnOwner());

	if (IsValid(pOwner))
	{
		pOwner->AttackEnd();
		pOwner->EnableAttackCollision(false);
		pOwner->SetEndOverlapEnable(true);
	}
}

 

노티파이 함수를 사용하여 공격이 시작할 때, 박스 컴포넌트를 충돌을 감지하는 상태로 바꿔주었습니다.

그리고 공격이 끝날 때는 충돌을 감지하지 않도록 바꿔주고 SetEndOverlapEnable 함수를 호출해서 bool 변수를 true로 바꿔주었습니다.

 

 

2. 플레이어 상태 UI 제작

 

 

UserWidget을 상속받는 C++ 프로젝트를 만들고, 해당 프로젝트를 상속받은 위젯 블루프린트를 생성하였습니다.

 

 

 

전체 화면의 한 부분에 UI를 띄울 것이기 때문에 캔버스 패널을 만들고 그 안에 두개의 프로그레스 바를 넣어주었습니다. 한 프로그레스 바는 체력 게이지를, 다른 하나는 수분 게이지를 표현하는 것입니다. 두 게이지 중 하나라도 0이 되면 플레이어는 죽습니다.

 

 

 

프로그레스 바를 캔버스 패널에 배치한 모습입니다.

각각의 프로그레스 바의 색은 체력은 빨간색으로 수분은 하늘색으로 설정하였습니다.

그리고 프로그레스 바 디테일에서 변수인지 부분을 off 해줘야 합니다. 그래야 C++ 프로젝트에서 해당 프로그레스 바들을 불러올 수 있습니다.

 

 

public:
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
	class UProgressBar* HPBar;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
	class UProgressBar* MoistureBar;

 

void UUISurvivorState::NativeConstruct()
{
	Super::NativeConstruct();

	HPBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("HPBar")));
	MoistureBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("MoistureBar")));
}

 

프로그레스 바를 선언하고, NativeConstruct 함수에서 GetWidgetFromName 함수를 사용하여 프로그레스 바의 이름을 통해 불러올 수 있습니다. UserWidget 클래스는 생성자가 없고 NativeConstruct 함수가 그 역할을 대신합니다.

 

 

// HP 게이지를 세팅하는 함수
void UUISurvivorState::SetHP(float fPercent)
{
	if (IsValid(HPBar))
		HPBar->SetPercent(fPercent);
}

// 수분 게이지를 세팅하는 함수
void UUISurvivorState::SetMoisture(float fPercent)
{
	if (IsValid(MoistureBar))
		MoistureBar->SetPercent(fPercent);
}

 

이 두 함수는 프로그레스 바를 업데이트 해주는 함수입니다. SetPercent 함수는 언리얼에서 제공해주는 함수입니다.

 

 

3. Main Game Mode에 플레이어 상태 위젯 추가

 

protected:
	TSubclassOf<UUserWidget> SurvivorStateWidgetClass;
	UUISurvivorState* SurvivorStateWidget;

 

	// 블루프린트 클래스를 받아온다
	static ConstructorHelpers::FClassFinder<UUISurvivorState> SurvivorStateWidgetAsset(TEXT("WidgetBlueprint'/Game/UI/UI_SurvivorState.UI_SurvivorState_C'"));

	if (SurvivorStateWidgetAsset.Succeeded())
		SurvivorStateWidgetClass = SurvivorStateWidgetAsset.Class;

 

void AMainGameMode::BeginPlay()
{
	Super::BeginPlay();

	if (IsValid(SurvivorStateWidgetClass))
	{
		SurvivorStateWidget = Cast<UUISurvivorState>(CreateWidget(GetWorld(), SurvivorStateWidgetClass));

		if (IsValid(SurvivorStateWidget))
		{
			// 위젯을 뷰포트에 띄우는 함수
			SurvivorStateWidget->AddToViewport();
		}
	}
}

 

플레이어 상태 위젯에 대한 정보를 Main Game Mode에서 들고있을 수 있도록 변수를 선언하였습니다.

그리고 나서 생성자에서 블루프린트 클래스를 받아온 뒤, BeginPlay에서 받아온 위젯을 뷰포트에 띄워주었습니다.

 

 

public:
	UUISurvivorState* GetSurvivorStateWidget() const
	{
		return SurvivorStateWidget;
	}

 

플레이어 클래스에서 체력 게이지와 수분 게이지를 설정하려면 위젯에 접근할 수 있도록 해줘야합니다.

그래서 Main Game Mode에 위젯의 포인터를 반환하는 함수를 만들어주었습니다.

 

 

	class UUISurvivorState* SurvivorStateWidget;

 

	// 현재 월드의 게임 모드를 받아온다
	AMainGameMode* GameMode = Cast<AMainGameMode>(GetWorld()->GetAuthGameMode());

	// 게임 모드에서 위젯을 받아온다
	SurvivorStateWidget = GameMode->GetSurvivorStateWidget();

	if (IsValid(SurvivorStateWidget))
	{
		// 체력 게이지와 수분 게이지를 설정한다
		SurvivorStateWidget->SetHP(SurvivorState.iHP / (float)SurvivorState.iHPMax);
		SurvivorStateWidget->SetMoisture(SurvivorState.iMoisture / (float)SurvivorState.iMoistureMax);
	}

 

플레이어 클래스에서 위젯의 정보를 들고있을 수 있도록 변수를 선언하였습니다.

그리고 나서 BeginPlay 함수에서 현재 월드에 설정된 게임 모드를 받아온 후, 위에서 만들어 둔 GetSurvivorStateWidget 함수를 사용하여 위젯을 받아왔습니다.

타당성 검사를 시행한 후, 체력 게이지와 수분 게이지를 설정하였습니다.

 

 

 

이제 뷰포트에 체력 게이지와 수분 게이지가 설정된 위젯의 모습을 확인할 수 있습니다.

 

 

void APlayerSurvivor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	MoistureDuration += DeltaTime;

	// 시간이 지남에 따라 수분 게이지를 낮춰준다
	if (MoistureDuration >= 10.f)
	{
		MoistureDuration = 0;

		SurvivorState.iMoisture -= 10;
		SurvivorStateWidget->SetMoisture(SurvivorState.iMoisture / (float)SurvivorState.iMoistureMax);
	}
}

 

float APlayerSurvivor::TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent,
	class AController* EventInstigator, AActor* DamageCauser)
{
	float fDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	// 현재 체력에서 fDamage의 크기만큼 감소시킨다
	SurvivorState.iHP -= (int32)fDamage;

	// 만약 HP가 0보다 작아진다면 Death 모션 적용
	if (SurvivorState.iHP <= 0)
	{
		SurvivorState.iHP = 0;

		GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);

		if (IsValid(SurvivorAnim))
			SurvivorAnim->SetAnimType(ESurvivorAnim::Death);

		ASurvivorController* pSurvivorController = GetController<ASurvivorController>();

		if (IsValid(pSurvivorController))
		{
			// 플레이어 사망시 게임 종료
			UKismetSystemLibrary::QuitGame(GetWorld(), pSurvivorController, EQuitPreference::Quit, true);
		}
	}

	// 플레이어 HP 게이지 세팅
	if (IsValid(SurvivorStateWidget))
		SurvivorStateWidget->SetHP(SurvivorState.iHP / (float)SurvivorState.iHPMax);

	return fDamage;
}

 

매초마다 실행되는 Tick 함수에서 시간이 지남에 따라 수분 게이지를 감소시켰고, 플레이어의 TakeDamage 함수에서 플레이어가 데미지를 받을 때마다 체력 게이지를 감소시켰습니다.

그리고 체력이 0보다 작아진다면 게임을 종료시켰습니다.

 

 

 

체력 게이지와 수분 게이지가 감소하는 모습을 확인할 수 있습니다.

 

 

다음에 해야할 일

 

시작 화면 UI 제작