서론
이번에는 테스트중 발견한 버그의 수정과 새로운 몬스터추가, 캐릭터 사망시 부활 기능을 추가해 보려고 합니다.
몬스터의 사망 모션 캔슬 버그는 문제점을 찾기 위해서 비헤이비어 트리의 연결을 바꿔도 보고 서비스 노드를
수정해 보기도 하였지만 결국 원인은 타이머에 의한 예기치 않은 몽타주 정지 동작이 문제였습니다.
타이머 관리에 대한 필요성을 많이 느꼈습니다..
몬스터의 사망 모션 캔슬 버그
몬스터가 스턴 상태 이후 사망하면 사망 모션이 캔슬되는 모습입니다.
bool ARPGEnemy::ApplyStun()
{
if (AnimInstance->GetIsAttacking())
{
// 진행중인 몽타주 종료 후 스턴
AnimInstance->StopAllMontages(0.f);
AnimInstance->SetStunned(true);
AnimInstance->SetIsAttacking(false);
// 스턴 해제를 위한 타이머
const float StunTime = 5.0f;
GetWorldTimerManager().SetTimer(
StunTimerHandle,
[&]() {
AnimInstance->StopAllMontages(0.0f);
AnimInstance->SetStunned(false);
ARPGAIController* AIController = Cast<ARPGAIController>(Controller);
UBlackboardComponent* BlackboardComp = AIController->GetBlackboardComponent();
BlackboardComp->SetValueAsBool(FName(TEXT("Stunned")), false);
},
StunTime,
false);
...
위 코드는 몬스터의 스턴에 관련된 함수 입니다.몬스터가 스턴 상태에 걸리면 StunTime 시간 이후 타이머가 작동 되는데, 이 타이머가 작동 된 후 몬스터가 사망하면
설정한 시간 이후 StopAllMontage로 인해 몬스터의 사망 몽타주가 캔슬되는 것이 원인이었습니다.
// 스턴 관련
protected:
FTimerHandle StunTimerHandle;
타이머를 관리하기 위해 몬스터의 헤더파일에 다음과 같은 타이머 핸들러 변수를 추가하였습니다.
void ARPGEnemy::SetDead()
{
ARPGAIController* AIController = Cast<ARPGAIController>(Controller);
// AI 중단
if (AIController)
{
AIController->StopAI();
}
// 스턴 타이머가 걸려있다면 제거
if (StunTimerHandle.IsValid())
{
GetWorldTimerManager().ClearTimer(StunTimerHandle);
StunTimerHandle.Invalidate();
}
이후 SetDead 함수에서 스턴 타이머가 걸려있는지 체크 한 다음 타이머가 존재한다면 해당 타이머를 제거하는
과정을 추가하였습니다.
새로운 몬스터 추가
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat, Meta = (AllowPrivateAccess = "true"))
float MaxHp;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat, Meta = (AllowPrivateAccess = "true"))
float CurrentHp;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat, Meta = (AllowPrivateAccess = "true"))
float PatrolRadius;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat, Meta = (AllowPrivateAccess = "true"))
float DetectRadius;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat, Meta = (AllowPrivateAccess = "true"))
float AttackRange;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat, Meta = (AllowPrivateAccess = "true"))
float AttackDamage;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat, Meta = (AllowPrivateAccess = "true"))
float DefenseRange;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat, Meta = (AllowPrivateAccess = "true"))
float RespawnTime;
/* 몬스터의 스턴 지속시간 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat, Meta = (AllowPrivateAccess = "true"))
float StunTime;
/* 몬스터를 잡았을 때 지급할 스탯 포인트 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat, Meta = (AllowPrivateAccess = "true"))
int RewardStatPoint;
새로운 몬스터를 간편하게 추가하기 위해서 몬스터의 스탯에 관한 변수들을 블루프린트 내부에서 수정할 수 있게
코드를 수정하였습니다.
기존에 있던 Enemy보다 강력한 적인 Enemy_Strong을 블루프린트를 통해 생성하였습니다.
스탯 컴포넌트의 변수들을 블루프린트에서 수정할 수 있게 하여서 몬스터의 다양한 스탯들을
간편하게 수정할 수 있도록 하였습니다.
데미지 이외에도 몬스터마다 특색을 가질 수 있도록 공격 모션을 바꿀 수 있게
공격 몽타주에 대한 설정을 애님 인스턴스에서 몬스터 클래스로 이동하였습니다.
새롭게 추가된 공격 모션은 기존에 있던 공격 모션과는 패링 타이밍이 다르며 공격 속도 또한 빠르게 설정하였습니다.
성공적으로 패링하는 모습입니다.
이 몬스터의 경우 기존 몬스터보다 강력하지만 처치 시 지급되는 스탯 포인트를 5로 설정하여
처치 한다면 빠르게 강해질 수 있도록 하였습니다.
몬스터 처치 시 체력 회복
체력 회복은 스탯 포인트를 체력에 투자하여 회복 할 수 있도록 하였지만 다른 체력 회복 수단을
추가로 넣기로 하였습니다.
몬스터를 잡으면 체력을 일정 회복하도록 하여 강한 몬스터와 전투 후 체력이 부족해졌다면
약한 몬스터를 잡아 체력을 채워 다시 싸울 수 있는 상황을 의도하였습니다.
void ARPGEnemy::SetDead()
{
...
// 사망시 스탯 포인트 리워드 지급, 체력회복
AController* CharacterController = GetWorld()->GetFirstPlayerController();
IRPGBattleRewardInterface* BattleRewardInterface = Cast<IRPGBattleRewardInterface>(CharacterController->GetPawn());
BattleRewardInterface->StatPointReward(StatComp->GetRewardStatPoint());
BattleRewardInterface->HPRegen(StatComp->GetRewardHP());
...
캐릭터에게 스탯 포인트를 지급하기 위해서 만든 리워드 인터페이스에 HPRegen이라는 가상함수를 추가하였습니다.
이를 이용해 몬스터의 SetDead 함수에서 몬스터를 처치하면 캐릭터 내부의 HPRegen 함수를 호출하도록 하고
회복되는 HP는 몬스터의 스탯 컴포넌트의 RewardHP만큼 회복하도록 하였습니다.
이를 이용해 강한 적을 잡으면 더 많은 체력을 회복할 수 있도록 설정할 수 있을 것 입니다.
void ARPGCharacter::HPRegen(float RewardHP)
{
StatComp->SetHp(StatComp->GetCurrentHp() + RewardHP);
}
캐릭터의 HPRegen 함수입니다. 보상으로 받은 체력 회복량을 현재 체력에 더하여
스탯 컴포넌트의 SetHp 함수에 보내주어 체력을 변경시키고 델리게이트를 통하여 HpBar의 설정 또한
변경할 수 있을 것 입니다.
성공적으로 체력이 회복 되는 것을 확인할 수 있습니다!
캐릭터 사망 시 일정시간 이후 부활
캐릭터의 사망 시 게임이 끝나는 것이 아닌 다시 부활하도록 설정하였습니다.
사망시 모든 과정을 잃어버리는 로그라이크 방식이 아닌 스탯을 그대로 유지하도록
캐릭터 사망 후 타이머를 이용하여 일정 시간 이후에 캐릭터의 체력을 MaxHp 만큼 회복시키고
최초에 캐릭터가 생성된 위치를 기억하여 그 곳에 캐릭터를 이동시키도록 하였습니다.
void ARPGCharacter::SetDead()
{
// 이전 부활 타이머가 있다면 제거
if (GetWorldTimerManager().IsTimerActive(RespawnTimerHandle))
{
GetWorldTimerManager().ClearTimer(RespawnTimerHandle);
}
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
SetActorEnableCollision(false);
APlayerController* PlayerController = Cast<APlayerController>(GetController());
if (PlayerController)
{
DisableInput(PlayerController);
}
AnimInstance->PlayDeadMontage();
// 일정 시간 후에 부활하기 위한 타이머 설정
GetWorldTimerManager().SetTimer(RespawnTimerHandle, this, &ARPGCharacter::Respawn, StatComp->GetRespawnTime(), false);
}
void ARPGCharacter::Respawn()
{
// 부활 위치로 플레이어를 이동
SetActorLocation(InitialLocation);
// 충돌 및 이동모드 초기화
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
SetActorEnableCollision(true);
APlayerController* PlayerController = Cast<APlayerController>(GetController());
if (PlayerController)
{
EnableInput(PlayerController);
}
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), RespawnParticle, InitialLocation);
AnimInstance->StopAllMontages(0.f);
// 최대 체력만큼 회복
StatComp->SetHp(StatComp->GetMaxHp());
}
사망 시 MovementMode와 충돌모드 변경, 캐릭터 컨트롤러를 제거 하였기 때문에
모두 원래 상태로 돌려주는 과정을 넣었고, 부활 자리에 파티클을 생성해 부활 이펙트를 주었습니다.
성공적으로 부활 기능이 구현된 모습입니다.
다음 포스팅에서는 캐릭터의 체력과 퀘스트 진행상황을 나타내는 HUD를 추가하도록 하겠습니다.
감사합니다!
'[게임 개발] 개발 일지 > RPG' 카테고리의 다른 글
24. 개발 마무리 (0) | 2023.10.07 |
---|---|
23. HUD 위젯 추가 (1) | 2023.10.07 |
21. 퀘스트 시스템 - 카운팅과 보상 (0) | 2023.10.06 |
20. 퀘스트 시스템 - 퀘스트 표시와 수락 (0) | 2023.10.06 |
19. 퀘스트 시스템 - 데이터 테이블 구축 (0) | 2023.10.05 |