[UE5] 캐릭터 피격과 사망 / 자성 이동 / 아이템 자동 습득 구현

연하·2024년 5월 21일
0

Trapper

목록 보기
5/32
post-thumbnail

원래는 프로젝트를 진행하면서 글을 작성하는데, 프로토타입 발표가 곧이라 급하게 작업하는 바람에 하루 늦게 작성하는 정리글. 기억나는대로 작업의 흐름을 기록할 예정이다 :)

모든 구현이 1차 프로토타입 검증용이므로, 많이 부족하고 급하게 짠 코드들이 많습니다. 추후 공부해나가며 개선할 예정입니다!

캐릭터 피격/사망/리스폰

float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;

TakeDamage 함수를 오버라이드 해주었다. 몬스터와 체력관련 부분을 공유하기 때문에 나중에 컴포넌트로 따로 빼주어야 할 것 같긴한데, 데미지를 입었을 때 피격 애니메이션을 재생하는 부분이 있어서 우선은 캐릭터에서 구현되도록 만들어 보았다. float CurrentHP, float MaxHP, bool IsDamaged, bool IsDead 변수도 선언해 주었다.

float ATrapperPlayer::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	float Damage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
	Damage = FMath::Min(CurrentHP, Damage);
	CurrentHP -= Damage;
	UE_LOG(LogTemp, Warning, TEXT("Damage : %f, HP : %f"), Damage, CurrentHP);

	if (CurrentHP <= 0)
	{
		Death();
	}
	else
	{
		Alive();
	}

	return Damage;
}

void ATrapperPlayer::Death()
{
	IsDead = true;
	GetCharacterMovement()->SetMovementMode(MOVE_None);
	GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);

	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	AnimInstance->Montage_Play(DeathAnimationMontage, 1.0);

	CastChecked<ATrapperPlayerController>(GetController())->PlayerDeath(this);
}

void ATrapperPlayer::Alive()
{
	IsDamaged = true;
	GetCharacterMovement()->SetMovementMode(MOVE_None);
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	AnimInstance->Montage_Play(DamagedAnimationMontage, 1.0);
}

데미지를 입으면 현재 체력에서 깎아주고, 체력이 0보다 낮을 경우와 클 경우로 나누어 처리를 해주었다.

Alive() 함수에서는 데미지를 입은동안 움직일 수 없게끔 처리를 해주고, 피격 애니메이션이 재생되도록 했다.

Death() 함수에서도 마찬가지로 캐릭터가 죽었을 때의 처리를 해주고, 컨트롤러에서 플레이어가 죽었을때의 처리(UI, 컨트롤러 Disabled)를 해주는 함수를 호출하도록 해주었다.

void ATrapperPlayerController::PlayerDeath(APawn* pawn)
{
	UE_LOG(LogTemp, Warning, TEXT("Player Death!"));
	pawn->DisableInput(this);

// 	UUserWidget* RespawnScreen = CreateWidget(this, RespawnScreenClass);
// 	if (RespawnScreen)
// 	{
// 		RespawnScreen->AddToViewport();
// 	}

// 	FTimerDelegate TimerDelegate;
// 	TimerDelegate.BindLambda([this, pawn]() { PlayerRespawn(pawn); });
// 	GetWorldTimerManager().SetTimer(RespawnTimer, TimerDelegate, RestartDelay, false);

	FTimerHandle RespawnTimer;
	GetWorldTimerManager().SetTimer(RespawnTimer, this, &APlayerController::RestartLevel, RestartDelay);
}

컨트롤러의 PlayerDeath(). 컨트롤러를 분리해주고, 5초뒤에 레벨을 다시 불러와준다. 주석처리한 부분은 리스폰 UI를 출력하는 코드와 나중에 레벨을 다시 불러오지 않고 플레이어만 따로 생존처리 하는식으로 바뀔 수도 있을 것 같아서 우선 간략하게 구현해 두었다.

피격 애니메이션과 사망 애니메이션은 애니메이션 몽타주로 구현해 주었다.

피격 몽타주와 사망 몽타주가 다른 점은 피격 몽타주엔 DamagedEnd라는 Notify가 들어간다는 것.

void ATrapperPlayer::DamagedAnimEnd()
{
	IsDamaged = false;
	GetCharacterMovement()->SetMovementMode(MOVE_Walking);
}

Blueprint Callable로 함수를 구현해주고(데미지 받은 상태 해제, 무브먼트 상태 되돌리기) 블루프린트에서 호출해주도록 했다. C++ 안에서 동작하도록 바꿔줄 예정이다.

애니메이션 블루프린트에서는 애니메이션 최종 출력 전에 두개의 몽타주를 거치도록(?) 설정해주었다.

그럼 이렇게 피격과 사망, 리스폰까지 잘 되는 것을 볼 수 있다.

애니메이션 몽타주 만들면서 이것저것 헤메인게 많았다. 겹치는 슬롯을 사용했을 때 뜨는 오류도 있었고, 원래는 피격과 사망 애니메이션을 하나의 몽타주 안에 넣어서 섹션을 나누어 단일로 재생하려고 했는데, 아무리 섹션을 이동시켜도 피격 애니메이션만 계속 나오는 문제가 있었다. 그래서 두개로 일단 나눠둔 것.. 아직 몽타주를 언제 어떻게 써야하는건지 모르겠기도 하고, 델리게이트 관련된 부분도 잘 모르겠어서.. 구현하면서 공부를 제대로 해야할 것 같다.

자성 이동

void ATrapperPlayer::FindMagneticPillar()
{
	FVector Start = GetActorLocation();
	FVector End = Start + GetControlRotation().Vector() * MagneticDistance;
	DrawDebugLine(GetWorld(), Start, End, FColor::Green, false, -1);

	FHitResult Result;
	bool HasHit = GetWorld()->LineTraceSingleByChannel(
		Result,
		Start,
		End,
		ECC_GameTraceChannel1
	);

	if (HasHit)
	{
		AActor* Target = Result.GetActor();
		TargetPosition = Target->GetActorLocation();
		FVector BoxSize = Target->GetComponentsBoundingBox().GetSize();
		TargetPosition.Z = BoxSize.Z + (GetCapsuleComponent()->GetScaledCapsuleHalfHeight() * 2);
		bHasTarget = true;
		DrawDebugSphere(GetWorld(), Result.ImpactPoint, 10, 10, FColor::Red, false, -1);

		// TODO : 외곽선 테두리 그리기처럼 포커싱 효과 주기 -> 기획에서 어떤 효과인지 알려주면 진행
	}
	else
	{
		bHasTarget = false;
	}
}

기존의 FindMagneticPillar() 함수는 타겟의 위치에 박스 콜라이더의 바운딩박스 사이즈를 가져와 Z축을 더해주도록 바꿔주었다.

void ATrapperPlayer::MagneticMove()
{
	if (!bIsMagneticMoving) return;

	ElapsedTime += GetWorld()->GetDeltaSeconds();

	float Alpha = FMath::Clamp(ElapsedTime / TotalMoveTime, 0.0f, 1.0f);

	// 포물선 높이 계산
	float Height = FMath::Sin(FMath::DegreesToRadians(Alpha)) * 100.0f;

	// 목표 위치와 현재 위치 사이를 보간
	FVector CurrentPosition = FMath::Lerp(StartPosition, TargetPosition, Alpha);

	// 보간된 위치의 Z축에 포물선 높이 추가
	CurrentPosition.Z += Height;

	// 현재 속도를 증가시키며 보간된 위치로 이동
	float CurrentSpeed = MagneticMoveSpeed + (Acceleration * ElapsedTime);
	FVector NewLocation = FMath::VInterpTo(GetActorLocation(), CurrentPosition, GetWorld()->GetDeltaSeconds(), CurrentSpeed);

	SetActorLocation(NewLocation);

	// 목표 지점에 도달했는지 확인
	if (Alpha >= 1.0f)
	{
		bIsMagneticMoving = false;
		ElapsedTime = 0.0f; // 초기화
	}
}

자성 이동 함수는 포물선으로 이동하도록 구현했다. 경과 시간과 원하는 총 이동 시간을 나눠서 알파값을 구해주고, 이 값을 사용해 포물선의 높이를 계산해준다. 뒤에 곱해주는 값은 포물선의 최대 높이를 결정해줌! 그 다음 타겟 위치와 현재 위치를 알파값을 사용해 보간해주고 보간된 위치에 포물선의 높이를 더해준다. 현재 속도는 기본 이동 속도에 가속도와 경과시간을 곱해서 더해주어 점점 빨라지는 속도를 구현해 주었다. 그리고 액터의 위치를 업데이트 해준 뒤, 만약 Alpha값이 1이 된다면 목표 위치에 도달했다는 의미이므로, 자성 이동 상태를 false로 바꿔주었다.

void ATrapperPlayer::Jump(const FInputActionValue& Value)
{
	if (bIsMagneticMoving)
	{
		bIsMagneticMoving = false;
		ElapsedTime = 0.0f;
	}

	// Super
	bPressedJump = true;
	JumpKeyHoldTime = 0.0f;
}

그리고 중간에 점프키를 누르면 점프하면서 자성이동을 멈춰야하는 부분이 있는데, 점프 자체를 언리얼 내장 함수를 쓰고 있어서 우선은 오버라이딩 해주고 자성이동 중에 점프키가 눌리면 자성이동을 멈추도록 해주었다. 근데 아직 점프하는 부분은 구현을 못했다.. 점프 함수를 좀 더 봐야 구현할 수 있을 것 같음..!!

지면에 서있을 때, 점프중일 때, 낙하할 때 모두 실행 가능하도록 애니메이션 상태머신을 설정해 주었다.

아이템 자동 습득

void ATrapperPlayer::FindMagneticItem()
{
	FVector PlayerLocation = GetActorLocation();
	TArray<FOverlapResult> OverlapResults;
	FCollisionShape Sphere = FCollisionShape::MakeSphere(MagneticPullRange);

	bool bHasOverlap = GetWorld()->OverlapMultiByChannel(
		OverlapResults,
		PlayerLocation,
		FQuat::Identity,
		ECC_GameTraceChannel3,
		Sphere
	);

	if (bHasOverlap)
	{
		for (auto& Result : OverlapResults)
		{
			AActor* OverlappedActor = Result.GetActor();
			UE_LOG(LogTemp, Warning, TEXT("Target Pos : %s"), *OverlappedActor->GetName());

			if (OverlappedActor)
			{
				MagneticPull(OverlappedActor);
			}
		}
	}
}

FindMagneticPillar 함수와 유사하다. 대신 OverlapMultiByChannel 함수를 사용해, 반경 내 해당되는 모든 아이템을 탐색해 TArray로 넣어준다. 액터가 존재할 경우, MagneticPull 을 실행해준다.

void ATrapperPlayer::MagneticPull(AActor* Item)
{
	if (!Item) return;

	FVector PlayerLocation = GetActorLocation() + FVector(0.f, 0.f, 20.f);
	FVector ItemLocation = Item->GetActorLocation();
	FVector NewPosition = FMath::VInterpTo(ItemLocation, PlayerLocation, GetWorld()->GetDeltaSeconds(), 5.f);
	
	const float Tolerance = 10.f;
	if (ItemLocation.Equals(PlayerLocation, Tolerance))
	{
		Item->Destroy();
	}

	Item->SetActorLocation(NewPosition);
}

플레이어의 좌표(아이템의 최종 목적지)와 아이템의 위치를 보간해 위치를 설정해주고, 플레이어의 위치와 가까워지면 아이템을 파괴한다. 추후엔 인벤토리로 들어가도록 구현할 예정이다.

아직 부족한 점은 많이 보이지만, 프로젝트 진행해가면서 하나씩 개선하면 되겠지!!! 파이팅!!

0개의 댓글