Unreal - 플레이어 리스폰

2025. 5. 24. 19:22·Unreal5 프로젝트 다이어리

플레이어의 리스폰을 만들어보겠습니다

먼저 리스폰하는 함수가 존재하는 위치는 플레이어가 사용하고있는 플레이어컨트롤러입니다

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
'Unreal5 프로젝트 다이어리' 카테고리의 다른 글
  • Unreal - 새로하기, 이어하기 메인메뉴 만들기
  • Unreal - 던전 클리어 위젯 만들기
  • Unreal - 던전진행도 위젯 만들기 (스칼라 파라미터 조절하기)
  • Unreal - 자동 흡수되는 확률형 드롭 아이템 만들기
lucodev
lucodev
커피와 노트북 그리고 개발
  • lucodev
    루코 개발테이블
    lucodev
  • 전체
    오늘
    어제
    • 분류 전체보기 (125) N
      • Unreal5 프로젝트 다이어리 (73)
      • Unreal5 프로젝트 다이어리2 (5) N
      • Unreal 팁 (8)
      • Unreal 디버깅 (8)
      • C++ 프로그래머스 다이어리 (23) N
        • Stack (3)
        • Hash (4)
        • Heap (2)
        • Sort (3) N
      • 코드 개인보관함 (8) N
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 링크

  • 공지사항

  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 태그

    언리얼 시퀀스
    언리얼
    언리얼 behavior tree
    언리얼 컷씬
    unreal 시퀀스
    언리얼 비헤이비어트리
    언리얼 로딩창
    언리얼 모션매칭
    unreal sequence
    unreal 컷씬
    언리얼 motionmatching
    unreal 모션매칭
    언리얼 페이드 아웃
    언리얼 foot step
    언리얼 look at
    unreal loading
    언리얼 로딩
    언리얼 behaviortree
    unreal look at
    unreal 로딩
  • hELLO· Designed By정상우.v4.10.3
lucodev
Unreal - 플레이어 리스폰
상단으로

티스토리툴바