Dev10 적 체력바/체력 감소, 적 죽음, 골렘/고블린 추가, 플레이어의 죽음/부활
적 체력바와 체력 감소
먼저 체력바 위젯을 만들어준다.
기본색은 반투명한 검정색이고 빨간색으로 채우도록 한다.
이건 퍼센트에 바인딩한 함수. 적 체력바의 경우 타겟 화살표와 거의 똑같다.
(체력바 영상으로 타겟 화살표를 만든 것이기 때문)
플레이어(메인)의 전투타겟의 현재 체력을 최대 체력으로 나눈 값을 반환하여 퍼센트에
반영한다.
퍼센트의 범위는 0~1이다. 1이면 최대값이기 때문에 체력바가 빨간색으로 꽉 채워진다.
현재 몬스터의 체력 기본값은 100, 최대 체력 기본값도 100이기 때문에 100/100 = 1
따라서 공격하지 않은 처음 상태에서는 모두 체력바가 꽉 차있다.
이제 스크립트 부분. 타겟 화살표와 방법이 똑같다.
MainPlayerController.h
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Widgets")
TSubclassOf<UUserWidget> WEnemyHPBar;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Widgets")
UUserWidget* EnemyHPBar;
bool bEnemyHPBarVisible;
void DisplayEnemyHPBar();
void RemoveEnemyHPBar();
MainPlayerController.cpp
in BeginPlay()
if (WEnemyHPBar)
{
EnemyHPBar = CreateWidget<UUserWidget>(this, WEnemyHPBar);
if (EnemyHPBar)
{
EnemyHPBar->AddToViewport();
EnemyHPBar->SetVisibility(ESlateVisibility::Hidden);
}
FVector2D Alignment(0.f, 0.f);
EnemyHPBar->SetAlignmentInViewport(Alignment);
}
틱 함수는 조금 달라졌다.
in Tick()
void AMainPlayerController::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (TargetArrow)
{
FVector2D PositionInViewport;
ProjectWorldLocationToScreen(EnemyLocation, PositionInViewport);
PositionInViewport.Y -= 130.f;
PositionInViewport.X -= 100.f;
EnemyHPBar->SetPositionInViewport(PositionInViewport);
PositionInViewport.Y -= 120.f;
PositionInViewport.X += 50.f;
TargetArrow->SetPositionInViewport(PositionInViewport);
FVector2D SizeInViewport = FVector2D(200.f, 20.f);
EnemyHPBar->SetDesiredSizeInViewport(SizeInViewport);
}
}
PositionInViewport의 X와 Y 값을 조절해서 체력바의 위치부터 잡고,
X와 Y 값을 또 조절하여 타겟 화살표 위치를 잡는다.
타겟화살표가 체력바보다 더 위에 있어야하기 때문에 둘을 같은 위치에 둘 수 없다.
체력바를 보이게 하거나 숨기는 함수.
void AMainPlayerController::DisplayEnemyHPBar()
{
if (EnemyHPBar)
{
bEnemyHPBarVisible = true;
EnemyHPBar->SetVisibility(ESlateVisibility::Visible);
}
}
void AMainPlayerController::RemoveEnemyHPBar()
{
if (EnemyHPBar)
{
bEnemyHPBarVisible = false;
EnemyHPBar->SetVisibility(ESlateVisibility::Hidden);
}
}
그리고 타겟팅할 때 타겟 화살표와 같이 보이게 하거나 안 보이게 한다.
in Main.cpp
void AMain::Targeting() //Targeting using Tap key
{
if (bOverlappingCombatSphere) //There is a enemy in combatsphere
{
if (targetIndex >= Targets.Num()) //타겟인덱스가 총 타겟 가능 몹 수 이상이면 다시 0으로 초기화
{
targetIndex = 0;
}
//There is already exist targeted enemy, then targetArrow remove
if (MainPlayerController->bTargetArrowVisible)
{
MainPlayerController->RemoveTargetArrow();
MainPlayerController->RemoveEnemyHPBar();
}
bHasCombatTarget = true;
CombatTarget = Targets[targetIndex];
targetIndex++;
MainPlayerController->DisplayTargetArrow();
MainPlayerController->DisplayEnemyHPBar();
}
}
적 체력 감소(포스팅 쓰다가 고쳤는데 성공함)
우리는 플레이어의 무기가 아닌 마법 공격에 적 콜리전이 오버랩되었을 경우에 적에게 데미지를 주고 싶으므로 MagicSkill 스크립트를 이용한다.
MagicSkill.h
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Comabat")
float Damage;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
TSubclassOf<UDamageType> DamageTypeClass;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Combat")
AController* MagicInstigator;
FORCEINLINE void SetInstigator(AController* Inst) { MagicInstigator = Inst; }
먼저 마법 공격마다 각각 가지고 있는 데미지를 선언한다.
그 뒤에 DamageTypeClass와 MagicInstigator는 cpp파일에서 설명하겠다.
MagicSkill.cpp
오버랩 시작 함수이다.
void AMagicSkill::OnComponentBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor)
{
AEnemy* Enemy = Cast<AEnemy>(OtherActor);
if (Enemy)
{
UGameplayStatics::PlaySound2D(this, ExplosionSound);
if (this->GetName().Contains("Wind"))
{
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ParticleFX, GetActorLocation());
Destroy(true);
}
if (DamageTypeClass)
{
UGameplayStatics::ApplyDamage(Enemy, Damage, MagicInstigator, this, DamageTypeClass);
}
}
}
}
데미지를 주기 위해서 UGameplayStatics의 ApplyDamage함수를 이용한다.
매개변수는 차례대로 데미지를 받을 상대, 데미지값, 컨트롤러, 데미지 원인 액터(마법),
데미지 타입 클래스 이다.
이 함수를 사용하면 첫번째 매개변수의 TakeDamage()가 자동으로 호출되는 것 같다.
마지막 매개변수인 데미지타입 클래스 때문에 헤더에서 DamageTypeClass를 선언해줬다.
문서 설명으로는 발생한 손상을 설명하는 클래스라고 한다. 에디터에서는 특정 형태의 피해를 정의하고 설명하며 다양한 소스로부터 피해에 대한 대응을 사용자가 정의할 수 있도록 해준다고 한다.
이 데미지 타입 클래스는 마법 스킬 블루프린트에서 직접 설정해준다.
아무튼 근데 문제는 컨트롤러였다.
매개변수로 넣어줄 컨트롤러 때문에 헤더 파일에서 AController* MagicInstigator를 선언했고, 컨트롤러를 할당해줄 메서드 SetInstigator()도 정의했다.
플레이어의 컨트롤러를 받아와야 하는데 MagicSkill 스크립트 안에서는 그 어디에서도 Main에 접근하지 않는다…
그래서 이런저런 여러 방법들을 시도하다가 포기하고 ApplyDamage함수를 안 쓰고
직접 Enemy 스크립트에서 TakeDam()을 만들어서 호출하도록 했는데,
포스팅 쓰면서 컨트롤러 받아오기에 성공했다..!!
받아온 방법은 이렇다.
in 생성자
AController* controller = Cast<AController>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
SetInstigator(controller);
GetPlayerController()라는 너무 좋은 함수를 이용해서 플레이어의 컨트롤러를 가져와 컨트롤러 변수에 넣고, 헤더에서 정의한 Set함수로 MagicInstigator에 컨트롤러를 할당해준다. 이 방법은 npc의 MoveToTarget함수 그리고 블루프린트(의 액션)에서 힌트를 얻었다.
아무튼 이렇게 해서 데미지를 입히면 Enemy의 TakeDamage함수가 실행된다.
적의 체력 감소와 죽음을 함께 설명하겠다.
Enemy.h
FTimerHandle DeathTimer;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
float DeathDelay;
bool bHasValidTarget;
virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;
void Die();
UFUNCTION(BlueprintCallable)
void DeathEnd();
bool Alive();
void Disappear();
죽은 뒤 실행될 타이머와 타이머 시간을 선언해준다.
그 밑의 불 변수는 유효한 (전투)타겟이 있는지 판별해주는 건데, 조금 이따가 설명하겠다.
TakeDamage() 를 오버라이딩한다.
그리고 체력이 0이하일 때 호출될 Die(),
죽는 애니메이션이 끝나면 호출될 DeathEnd(),
얘가 죽었는지 살았는지를 불로 반환해주는 Alive(),
그리고 죽음 타이머가 다 돌면 호출되는 Disappear()
Enemy.cpp
in 생성자
타이머 시간 설정해주고 불 변수 기본값 false로 둔다.
DeathDelay = 3.f;
bHasValidTarget = false;
오버라이딩한 TakeDamage함수이다.
float AEnemy::TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser)
{
if (Health - DamageAmount <= 0.f)
{
Health = 0.f;
Die();
}
else
{
Health -= DamageAmount;
}
return DamageAmount;
}
데미지가 현재 체력보다 크거나 같으면 체력을 0으로 설정한 뒤 Die()를 호출한다.
데미지가 현재 체력보다 낮으면 체력에서 데미지를 뺀 값으로 체력을 초기화한다.
굳이 반환하지 않아도 되는데 반환형이 있는 이유는 오버라이딩 때문이다.
부모의 함수와 반환형이 반드시 같아야 하기 때문.
위에서 호출된 Die메서드이다.
void AEnemy::Die()
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance)
{
AnimInstance->Montage_Play(CombatMontage);
AnimInstance->Montage_JumpToSection(FName("Death"), CombatMontage);
}
SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Dead);
}
전투 몽타주를 재생시키고 몽타주 섹션 Death로 점프한다.
그 다음, 상태를 EMS_Dead로 설정한다.
Death애니메이션을 실행하다가 DeathEnd라는 노티파이를 만나면
DeathEnd()를 호출한다.
void AEnemy::DeathEnd()
{
GetMesh()->bPauseAnims = true;
GetMesh()->bNoSkeletonUpdate = true;
GetWorldTimerManager().SetTimer(DeathTimer, this, &AEnemy::Disappear, DeathDelay);
}
애니메이션을 일시중지시키고, 스켈레톤 업데이트를 못하게 한다.
DeathDelay에 3을 넣어줬으므로 3초 뒤 Disappear()가 호출된다.
void AEnemy::Disappear()
{
CombatCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision);
CombatCollision2->SetCollisionEnabled(ECollisionEnabled::NoCollision);
AgroSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
CombatSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Destroy();
}
콜리전을 모두 없앤 뒤 월드에서 없어지도록 파괴한다.
반환형이 bool인 Alive(), 죽지 않은 상태면 true를 반환한다.
여러 메서드에 조건으로 껴넣었다.
bool AEnemy::Alive()
{
return GetEnemyMovementStatus() != EEnemyMovementStatus::EMS_Dead;
}
월드(= 레벨, 맵)에 적을 추가로 더 배치했다!
골렘 한 마리, 고블리 3마리를 배치했다.
고블린 같은 경우는 무기가 스태틱 메시로 따로 되어있길래 내가 직접 블루프린트에서 배치해준 뒤, 콜리전 안에 넣었다.
그리고 고블린과 골렘의 경우, 스킬 3개를 랜덤하게 사용하도록 했다.
(Grux는 스킬이 하나다. 뭔가 더 있는 것 같은데 쓰기가 힘들어보여서 하나만 씀.)
in Enemy.cpp
void AEnemy::Attack()
{
if (Alive() && bHasValidTarget)
{
if (AIController)
{
AIController->StopMovement();
SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Attacking);
}
if (!bAttacking)
{
bAttacking = true;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance)
{
AnimInstance->Montage_Play(CombatMontage);
if (this->GetName().Contains("Grux"))
{
AnimInstance->Montage_JumpToSection(FName("Attack"), CombatMontage);
}
else
{
int num = FMath::RandRange(1, 3);
switch (num)
{
case 1:
AnimInstance->Montage_JumpToSection(FName("Attack1"), CombatMontage);
break;
case 2:
AnimInstance->Montage_JumpToSection(FName("Attack2"), CombatMontage);
break;
case 3:
AnimInstance->Montage_JumpToSection(FName("Attack3"), CombatMontage);
break;
}
}
}
}
}
}
골렘의 3번 스킬의 경우 땅을 치는 건데, 이거는 콜리전에 오버랩되었을 때 데미지를 입는 게 아니라 골렘의 전투 범위 안에만 있으면 무조건 데미지를 입게 하고 싶어서 일부러 콜리전을 키는 노티파이도 추가하지 않았다.
대신 HitGround라는 노티파이를 추가하여 HitGround()를 호출하게 했다.
void AEnemy::HitGround()
{
if (CombatTarget)
{
UGameplayStatics::ApplyDamage(CombatTarget, Damage, AIController, this, DamageTypeClass);
}
}
여기서 바로 전투타겟에게 데미지를 준다.
플레이어의 죽음, 부활
죽는 거는 Enemy와 거의 흡사하다.
Main.h
UENUM(BlueprintType)
enum class EMovementStatus :uint8
{
EMS_Normal UMETA(DeplayName = "Normal"),
EMS_Dead UMETA(DeplayName = "Dead"),
EMS_MAX UMETA(DeplayName = "DefaultMAX")
};
FTimerHandle DeathTimer;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
float DeathDelay;
virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;
void Die();
UFUNCTION(BlueprintCallable)
void DeathEnd();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Movement")
EMovementStatus MovementStatus;
FORCEINLINE void SetMovementStatus(EMovementStatus Status) { MovementStatus = Status; }
virtual void Jump() override;
void Revive();
UFUNCTION(BlueprintCallable)
void RevivalEnd();
기존에 없었던 MovementStatus을 두어 상태를 알 수 있도록 했고, Set함수도 정의했다.
점프 함수를 오버라이딩 선언했고 부활에 관한 메서드 2개(Revive(), ReviveEnd())를 선언했다.
Main.cpp
float AMain::TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser)
{
if (HP - DamageAmount <= 0.f)
{
HP = 0.f;
Die();
if (DamageCauser)
{
AEnemy* Enemy = Cast<AEnemy>(DamageCauser);
if (Enemy)
{
Enemy->bHasValidTarget = false;
Enemy->SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Idle);
}
}
}
else
{
HP -= DamageAmount;
}
return DamageAmount;
}
기본 작동은 Enemy와 같다. 플레이어가 죽으면 플레이어를 공격한 몬스터는 유효한 타겟이 없는 것이며, 유휴상태로 돌아간다.
Enemy의 bHasValidTarget은 플레이어가 몬스터의 전투범위 안에 들어왔을 때 true가 되고 몬스터는 이것이 true여야 공격을 할 수 있다.
위에서 호출된 Die메서드이다.
void AMain::Die()
{
if (MovementStatus == EMovementStatus::EMS_Dead) return;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance && CombatMontage)
{
AnimInstance->Montage_Play(CombatMontage);
AnimInstance->Montage_JumpToSection(FName("Death"));
}
SetMovementStatus(EMovementStatus::EMS_Dead);
}
전투 몽타주에서 Death 애니메이션 부분을 재생하고 플레이어의 상태를 EMS_Dead로 바꾼다.
이미 상태가 EMS_Dead라면 함수를 탈출한다. (=이 함수가 여러번 실행되지 않게 한다.)
Enemy와 마찬가지로 전투 몽타주에서 DeathEnd라는 노티파이를 만나면 DeathEnd()를 호출한다.
void AMain::DeathEnd()
{
GetMesh()->bPauseAnims = true;
GetMesh()->bNoSkeletonUpdate = true;
GetWorldTimerManager().SetTimer(DeathTimer, this, &AMain::Revive, DeathDelay);
}
애니메이션과 스켈레톤 업데이트를 중지한다.
bNoSkeletonUpdate을 true시키면 본을 리프레시하지 않고 틱도 돌지 않게 된다고 한다.
(출처는 에디터)
Enemy는 3초 뒤면 사라지지만 플레이어는 부활할 것이므로 Revive메서드를 호출하게 한다.
플레이어를 부활시키는 메서드이다.
void AMain::Revive() // if player is dead, spawn player at the initial location
{
this->SetActorLocation(FVector(-192.f, 5257.f, 3350.f));
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance && CombatMontage)
{
GetMesh()->bPauseAnims = false;
AnimInstance->Montage_Play(CombatMontage);
AnimInstance->Montage_JumpToSection(FName("Revival"));
GetMesh()->bNoSkeletonUpdate = false;
HP += 50.f;
}
}
먼저 플레이어의 위치를 지정한 곳(초기 위치)으로 이동시킨다.
그 다음, 애니메이션 중지를 해제하고 전투 몽타주를 재생하고 Revival섹션으로 점프한다.
스켈레톤 업데이트 중지도 해제하고 체력을 반으로 채워준다.
Mixamo에서 일어서는 애니메이션을 다운받아 사용했다. (지팡이를 잡게끔 고치고, 다리 부분도 좀 수정했다.)
RevivalEnd라는 노티파이를 만나면 RevivalEnd()를 호출한다.
void AMain::RevivalEnd()
{
SetMovementStatus(EMovementStatus::EMS_Normal);
}
여기서 플레이어의 상태를 다시 Normal로 바꾼다.
그리고 점프 오버라이딩은 왜 했냐면 기존에는 점프할 때 Main의 부모 클래스인
ACharacter의 Jump함수를 호출했는데
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMain::Jump);//&ACharacter::Jump
PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
여기에는 별다른 조건이 없기 때문에 우리가 죽은 상태에서 점프를 하는 것이 가능하다.
void ACharacter::Jump()
{
bPressedJump = true;
JumpKeyHoldTime = 0.0f;
}
따라서 오버라이딩해서 조건을 넣고 조건에 맞을 때만 ACharacter::Jump를 호출하도록 한다.
void AMain::Jump()
{
if (MovementStatus != EMovementStatus::EMS_Dead)
{
Super::Jump();
}
}
이렇게 하면 죽었을 때는 점프를 못 한다.
+MovementStatus != EMovementStatus::EMS_Dead 조건을 MoveForward(), MoveRight()에도 추가해줘서 죽은 상태면 이동이 불가하다.
예전부터 거슬렸던 문제도 이번에 고쳤다!
적의 전투범위에 들어가서 적이 공격을 시작한 뒤(공격 애니메이션 재생) 적의 전투범위를 벗어나면 공격 애니메이션인 상태로 플레이어를 쫓아오는 문제를 해결했다. (공격 애니메이션이 끝나야 달리는 애니메이션이 됨. 하지만 공격 애니메이션이 끝나지 않더라도 바로 달리는 애니메이션이었으면 좋겠음.)
Enemy.cpp
전투범위를 벗어났을 때 실행되는 메서드이다.
void AEnemy::CombatSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
if (OtherActor && Alive())
{
AMain* Main = Cast<AMain>(OtherActor);
if (Main)
{
bOverlappingCombatSphere = false;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->Montage_Stop(0.1f, CombatMontage);
AttackEnd();
if (EnemyMovementStatus == EEnemyMovementStatus::EMS_Attacking)
{
MoveToTarget(Main);
CombatTarget = nullptr;
}
}
}
}
애님인스턴스를 불러와서 몽타주를 중지시키고 AttackEnd()를 호출하게 했다.
Montage_Stop함수의 첫번째 매개변수에는 무조건 0보다 큰 값을 줘야한다고 한다.
NavMesh도 조금 수정했다.
경사각을 좀 더 커버하고 싶었는데 월드아웃라이너의 RecastNavMesh-Default에서 셀 높이와 에이전트 최대 스텝 높이를 높여주니 해결되었다.
한동안 남캐로만 테스트하다가 이번에 여캐도 설정할 거 다 설정하고 테스트했다!
다음 할 것 : 적 보간(공격 시 플레이어 보게 하기), npc의 공격