플레이어의 리스폰을 만들어보겠습니다
먼저 리스폰하는 함수가 존재하는 위치는 플레이어가 사용하고있는 플레이어컨트롤러입니다
Revive함수와 스폰시킬 캐릭터참조할 TSubClassOf를 선언
UFUNCTION()
void RevivePlayer(ASwordCharacter* dyingCharacter);
UPROPERTY(EditAnywhere, Category = "Respawn")
TSubclassOf<ASwordCharacter> respawnCharacterClass;
죽을 캐릭터를 매개변수로 받아 위치를 구하고
빙의해제 (Unpossess) -> Destroy -> 새로운캐릭터 Spawn -> Possess 순서로 진행합니다
플레이어가 죽은 위치에서 스폰하게되면 콜리전에 겹치게되면 스폰하지않을경우가 있는데
SpawnCollisionHandleingMethod 값을 AdjustIfPossibleButAlwaysSpawn으로 설정할시 해결할수있습니다
void ASwordPlayController::RevivePlayer(ASwordCharacter* dyingCharacter)
{
FVector playerSpawnLoc = dyingCharacter->GetActorLocation();
FRotator playerSpawnRot = dyingCharacter->GetActorRotation();
UnPossess();
dyingCharacter->Destroy();
FActorSpawnParameters spawnParams;
spawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
ASwordCharacter* spawnCharacter = GetWorld()->SpawnActor<ASwordCharacter>(respawnCharacterClass, playerSpawnLoc, playerSpawnRot, spawnParams);
Possess(spawnCharacter);
swordCharacter = spawnCharacter;
}
이제 리스폰을 호출하는 함수의 타이밍은 플레이어가 데미지를 받았는데 hp가 <= 0 인시점
0보다 작아지는 시점에서 플레이어컨트롤러의 RevivePlayer에 플레이어(this)를 매개변수로 넘겨줍니다
void ASwordCharacter::GetDamage(float damage)
{
playerCurrentHp -= damage;
playerCurrentHp = FMath::Clamp(playerCurrentHp, 0.0f, playerMaxHp);
if (playerCurrentHp <= 0)
{
if (ASwordPlayController* playerCon = Cast<ASwordPlayController>(GetController()))
{
playerCon->RevivePlayer(this);
}
}
}
해당과정까지 완료되었다면 Player의 currentHp < = 0 인 조건이 충족되면 리스폰하게됩니다
플레이어가 죽으면 죽는 애니메이션이 나오고 그 다음 Destroy -> UI가 뜨고 UI에서 부활하시겠습니까? 버튼을 누르면
부활하는 애니메이션이 나오면서 리스폰하는 형태로 바꿔보도록 하겠습니다
캐릭터의 Respawn과 Die때 애니메이션을 나타내는 함수를 캐릭터한테 만들어줍니다
해당함수가 true면 살아나는 애니메이션을 false면 죽는 애니메이션을 재생합니다
void ASwordCharacter::TriggerDeathAndRevive(bool bIsDie)
{
if (bIsDie)
{
PlayAnimMontage(dieMontage);
}
else
{
PlayAnimMontage(respawnMontage);
}
}
해당 함수가 호출이되면
UI를 숨기고 죽는 애니메이션을 재생합니다
인풋을 막습니다
플레이어 캡슐 오버랩 이벤트를 막습니다
죽었을때 시점을 유지시키기위한 카메라홀드 액터를 만들어서
캐릭터의 PlayerCameraManager으로 카메라의 위치와 방향을 구해서
홀드액터에 먼저 시점을 변환후
죽는 애니메이션의 길이를 구한다음
그 애니메이션 길이가 끝나면 캐릭터를 Destroy한뒤 카메라를 홀드 카메라의 위치로 고정시킵니다
만약 홀드 카메라의 존재가 없으면 카메라는 월드상에 붕 떠버리는 현상이 일어납니다
void ASwordPlayController::RevivePlayer(ASwordCharacter* dyingCharacter)
{
dyingCharacter->characterWidget->SetVisibility(ESlateVisibility::Hidden);
FVector playerSpawnLoc = dyingCharacter->GetActorLocation();
FRotator playerSpawnRot = dyingCharacter->GetActorRotation();
//true -> die animation | false -> revive animation
dyingCharacter->TriggerDeathAndRevive(true);
DisableInput(this);
//no overlapevent
if (swordCharacter)
{
UCapsuleComponent* capsuleComp = swordCharacter->FindComponentByClass<UCapsuleComponent>();
if (capsuleComp)
{
capsuleComp->SetGenerateOverlapEvents(false);
}
}
//temporary hold camera actor
FVector cameraLoc = PlayerCameraManager->GetCameraLocation();
FRotator cameraRot = PlayerCameraManager->GetCameraRotation();
FActorSpawnParameters spawnTempCameraParams;
ACameraActor* cameraHolder = GetWorld()->SpawnActor<ACameraActor>(cameraLoc, cameraRot, spawnTempCameraParams);
if (cameraHolder && cameraHolder->GetCameraComponent())
{
cameraHolder->GetCameraComponent()->SetActive(true);
}
SetViewTargetWithBlend(cameraHolder, 0.1f);
//die animation Length calculate
float dieAnimLength = 0.f;
dieAnimLength = dyingCharacter->dieMontage->GetPlayLength() - 1.0f;
FTimerHandle th_destroyHandle;
GetWorld()->GetTimerManager().SetTimer(th_destroyHandle, [this, dyingCharacter, cameraHolder]()
{
dyingCharacter->Destroy();
SetViewTarget(cameraHolder);
}, dieAnimLength, false);
}
리스폰을 하는 트리거인 UI를 만들어보겠습니다
You Die 애니메이션을 만들어주고 리스폰버튼을 만들어줍니다
뒤에는 불투명한 블러를 깔아주었습니다
N초뒤 부활가능 text를 만듭니다
해당텍스트또한 바인드해줍니다
위젯을 만들어줍니다
UCLASS()
class BLASTERDREAM_API URespawnWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
UPROPERTY(meta = (BindWidget))
UButton* Button_Respawn;
UPROPERTY(meta = (BindWidget))
UTextBlock* TextBlock_TimerText;
UPROPERTY(meta = (BindWidgetAnim), Transient)
UWidgetAnimation* DieLogoAnim;
UPROPERTY(meta = (BindWidgetAnim), Transient)
UWidgetAnimation* RespawnHoverAnim;
UPROPERTY(meta = (BindWidgetAnim), Transient)
UWidgetAnimation* RespawnUnHoverAnim;
UFUNCTION()
void ShowButton();
UFUNCTION()
void ClickRespawnButton();
UFUNCTION()
void HoverRespawnButton();
UFUNCTION()
void UnhoverRespawnButton();
UFUNCTION()
void UpdateTimerText();
UFUNCTION()
void CountdownText();
UPROPERTY(EditAnywhere, Category="MySettings")
int32 countValue = 5;
FTimerHandle th_showButton;
};
NativeConstruct함수에서 타이머를 호출
CountdownText를 1초간격으로 호출
해당 함수에서 int32형 countValue를 감소시키며
0초가 되면 버튼을 나타냅니다
아닐시 UpdateTimerText함수로 N 초뒤 부활합니다 를 보여지게합니다
void URespawnWidget::NativeConstruct()
{
Super::NativeConstruct();
PlayAnimation(DieLogoAnim);
Button_Respawn->SetVisibility(ESlateVisibility::Collapsed);
if (Button_Respawn)
{
Button_Respawn->OnClicked.AddDynamic(this, &URespawnWidget::ClickRespawnButton);
Button_Respawn->OnHovered.AddDynamic(this, &URespawnWidget::HoverRespawnButton);
Button_Respawn->OnUnhovered.AddDynamic(this, &URespawnWidget::UnhoverRespawnButton);
}
UpdateTimerText();
GetWorld()->GetTimerManager().SetTimer(th_showButton, this, &URespawnWidget::CountdownText, 1.0f, true);
}
void URespawnWidget::ShowButton()
{
Button_Respawn->SetVisibility(ESlateVisibility::Visible);
}
void URespawnWidget::ClickRespawnButton()
{
APlayerController* pc = UGameplayStatics::GetPlayerController(this, 0);
if (pc)
{
ASwordPlayController* swordController = Cast<ASwordPlayController>(pc);
swordController->SpawnCharacter();
this->RemoveFromParent();
}
}
void URespawnWidget::HoverRespawnButton()
{
PlayAnimation(RespawnHoverAnim);
}
void URespawnWidget::UnhoverRespawnButton()
{
PlayAnimation(RespawnUnHoverAnim);
}
void URespawnWidget::UpdateTimerText()
{
if (TextBlock_TimerText)
{
FString customStr = FString::FromInt(countValue) + TEXT("초 뒤 부활합니다");
TextBlock_TimerText->SetText(FText::FromString(customStr));
}
}
void URespawnWidget::CountdownText()
{
countValue--;
if (countValue == 0)
{
TextBlock_TimerText->SetVisibility(ESlateVisibility::Collapsed);
ShowButton();
GetWorld()->GetTimerManager().ClearTimer(th_showButton);
}
else
{
UpdateTimerText();
}
}
다시 컨트롤러로 돌아가서 위젯을 참조
UPROPERTY(EditAnywhere, Category = "Respawn")
TSubclassOf<URespawnWidget> respawnWidget;
RevivePlayer함수 끝에 위젯을 보여주는코드를 추가합니다
if (respawnWidget)
{
URespawnWidget* respawnWidgetInstance = CreateWidget<URespawnWidget>(this, respawnWidget);
if (respawnWidgetInstance)
{
respawnWidgetInstance->AddToViewport();
}
}
또한 위젯에서 버튼을 누르면 호출할
플레이어가 스폰할 함수를 만들어줍니다
void ASwordPlayController::SpawnCharacter()
{
FActorSpawnParameters spawnParams;
spawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
ASwordCharacter* spawnCharacter = GetWorld()->SpawnActor<ASwordCharacter>(respawnCharacterClass, playerSpawnLoc, playerSpawnRot, spawnParams);
swordCharacter = spawnCharacter;
FTimerHandle th_reviveBefore;
GetWorld()->GetTimerManager().SetTimer(th_reviveBefore, [this, spawnCharacter]()
{
if (swordCharacter)
{
UCapsuleComponent* capsuleComp = swordCharacter->FindComponentByClass<UCapsuleComponent>();
if (capsuleComp)
{
capsuleComp->SetGenerateOverlapEvents(false);
}
}
//EnableInput(this);
Possess(spawnCharacter);
DisableInput(this);
InputTab();
spawnCharacter->TriggerDeathAndRevive(false);
}, 0.1, false);
float spawnAnimLength = 0.f;
spawnAnimLength = spawnCharacter->respawnMontage->GetPlayLength();
FTimerHandle th_reviveAfter;
GetWorld()->GetTimerManager().SetTimer(th_reviveAfter, [this, spawnCharacter]()
{
if (swordCharacter)
{
UCapsuleComponent* capsuleComp = swordCharacter->FindComponentByClass<UCapsuleComponent>();
if (capsuleComp)
{
capsuleComp->SetGenerateOverlapEvents(true);
}
}
EnableInput(this);
}, spawnAnimLength, false);
}
리스폰뒤에 s스킬의 프로그래스바의 타이머도 Clear해줘서 처음부터 게이지가 차게 만들어주고
리스폰후에도 위젯이 다시 나오도록 해줍니다
++ 캐릭터에서 플레이어컨트롤을 캐스팅하는방식을 이렇게 변경해주도록합니다
APlayerController* playerController = UGameplayStatics::GetPlayerController(this, 0);
죽을때의 함수입니다
void ASwordPlayController::RevivePlayer(ASwordCharacter* dyingCharacter)
{
if (dyingCharacter && dyingCharacter->characterWidget)
{
dyingCharacter->characterWidget->GetWorld()->GetTimerManager().ClearTimer(dyingCharacter->characterWidget->th_InnerProgressTimer);
dyingCharacter->characterWidget->RemoveFromParent();
dyingCharacter->characterWidget = nullptr;
}
playerSpawnLoc = dyingCharacter->GetActorLocation();
playerSpawnRot = dyingCharacter->GetActorRotation();
//true -> die animation | false -> revive animation
dyingCharacter->TriggerDeathAndRevive(true);
DisableInput(this);
//no overlapevent
if (swordCharacter)
{
UCapsuleComponent* capsuleComp = swordCharacter->FindComponentByClass<UCapsuleComponent>();
if (capsuleComp)
{
capsuleComp->SetGenerateOverlapEvents(false);
}
}
//temporary hold camera actor
FVector cameraLoc = PlayerCameraManager->GetCameraLocation();
FRotator cameraRot = PlayerCameraManager->GetCameraRotation();
FActorSpawnParameters spawnTempCameraParams;
ACameraActor* cameraHolder = GetWorld()->SpawnActor<ACameraActor>(cameraLoc, cameraRot, spawnTempCameraParams);
if (cameraHolder && cameraHolder->GetCameraComponent())
{
cameraHolder->GetCameraComponent()->SetActive(true);
}
SetViewTargetWithBlend(cameraHolder, 0.1f);
//die animation Length calculate
float dieAnimLength = 0.f;
dieAnimLength = dyingCharacter->dieMontage->GetPlayLength() - 1.0f;
FTimerHandle th_destroyHandle;
GetWorld()->GetTimerManager().SetTimer(th_destroyHandle, [this, dyingCharacter, cameraHolder]()
{
if (dyingCharacter)
{
dyingCharacter->Destroy();
}
swordCharacter = nullptr;
SetViewTarget(cameraHolder);
UnPossess();
}, dieAnimLength, false);
if (respawnWidget)
{
URespawnWidget* respawnWidgetInstance = CreateWidget<URespawnWidget>(this, respawnWidget);
if (respawnWidgetInstance)
{
respawnWidgetInstance->AddToViewport();
}
}
}
리스폰할떄의 함수입니다
void ASwordPlayController::SpawnCharacter()
{
FActorSpawnParameters spawnParams;
spawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
ASwordCharacter* spawnCharacter = GetWorld()->SpawnActor<ASwordCharacter>(respawnCharacterClass, playerSpawnLoc, playerSpawnRot, spawnParams);
swordCharacter = spawnCharacter;
//SetCharacterMainWidget();
FTimerHandle th_reviveBefore;
GetWorld()->GetTimerManager().SetTimer(th_reviveBefore, [this, spawnCharacter]()
{
if (swordCharacter)
{
UCapsuleComponent* capsuleComp = swordCharacter->FindComponentByClass<UCapsuleComponent>();
if (capsuleComp)
{
capsuleComp->SetGenerateOverlapEvents(false);
}
}
//spawnCharacter->characterWidget->SetVisibility(ESlateVisibility::Visible);
//EnableInput(this);
Possess(spawnCharacter);
spawnCharacter->swordPlayerController = this;
DisableInput(this);
InputTab();
spawnCharacter->TriggerDeathAndRevive(false);
}, 0.1, false);
float spawnAnimLength = 0.f;
spawnAnimLength = spawnCharacter->respawnMontage->GetPlayLength();
FTimerHandle th_reviveAfter;
GetWorld()->GetTimerManager().SetTimer(th_reviveAfter, [this, spawnCharacter]()
{
if (swordCharacter)
{
UCapsuleComponent* capsuleComp = swordCharacter->FindComponentByClass<UCapsuleComponent>();
if (capsuleComp)
{
capsuleComp->SetGenerateOverlapEvents(true);
}
}
EnableInput(this);
}, spawnAnimLength, false);
}
이제 스킬의 타이머가 리스폰한 시점으로 다시 처음부터 돌면서 리스폰하면 위젯이 정상적으로 다시 스폰되어 보입니다
리스폰했을때 hp를 full상태로 돌려놓겠습니다
플레이어의 hp는 게임인스턴스상의 savefile상에서 관리를 하니 인스턴스에서 값을 초기화시켜주도록 하겠습니다
인스턴스에서 플레이어의 hp를 full로 만들어주는함수를 만듭니다
void UStatGameInstance::ResetHpMp()
{
playerSaveHp = playerFullHp;
playerSaveMp = playerFullMp;
}
리스폰하는 함수에 해당 함수를 호출시킵니다
UStatGameInstance* statGameInstance = Cast<UStatGameInstance>(GetWorld()->GetGameInstance());
statGameInstance->ResetHpMp();
또한 배경을 회색으로 변경해보겠습니다
배경을 회색으로 변경하는법은 포스트프로세스의 ColorGrading값을 회색으로 변경하면됩니다
포스트프로세스를 tactoriterator로 맵상에서 찾은뒤 해당값을 변경하는 함수를 만듭니다
회색은 0.3, 0.3, 0.3 기본은 1.0, 1.0, 1.0 입니다
void ASwordPlayController::grayColorGradingSet()
{
for (TActorIterator<APostProcessVolume> it(GetWorld()); it; ++it)
{
APostProcessVolume* pv = *it;
if (pv)
{
pv->bUnbound = true;
pv->Settings.bOverride_ColorSaturation = true;
pv->Settings.ColorSaturation = FVector(0.3f, 0.3f, 0.3f);
}
}
}
void ASwordPlayController::grayColorGradingUnSet()
{
for (TActorIterator<APostProcessVolume> it(GetWorld()); it; ++it)
{
APostProcessVolume* pv = *it;
if (pv)
{
pv->bUnbound = true;
pv->Settings.bOverride_ColorSaturation = true;
pv->Settings.ColorSaturation = FVector(1.f, 1.f, 1.f);
}
}
}
리스폰할때 Niagara도 추가시켜주겠습니다
UPROPERTY(EditAnywhere, Category = "Respawn")
UNiagaraSystem* respawnNa;
if (respawnNa)
{
FVector niagarSpawnLoc = FVector(spawnCharacter->GetActorLocation());
niagarSpawnLoc.Z -= 90.f;
UNiagaraFunctionLibrary::SpawnSystemAtLocation(GetWorld(), respawnNa, niagarSpawnLoc);
}
최종결과물
'Unreal5 프로젝트 다이어리' 카테고리의 다른 글
Unreal - 새로하기, 이어하기 메인메뉴 만들기 (0) | 2025.05.29 |
---|---|
Unreal - 던전 클리어 위젯 만들기 (0) | 2025.05.25 |
Unreal - 던전진행도 위젯 만들기 (스칼라 파라미터 조절하기) (0) | 2025.05.23 |
Unreal - 자동 흡수되는 확률형 드롭 아이템 만들기 (0) | 2025.05.22 |
Unrela - 시퀀스 만들기 (AI편) (0) | 2025.05.22 |