[78. Enemy Health Bar]
전투 중 적의 체력을 표시해주는 체력바 같은 것은 매우 중요한 정보중 하나이다.
하지만 거리나 전투 유무와 관계없이 체력바가 매번 표시되는것은 너무 불필요한 정보들이다.
그래서 전투 중에만 체력바를 표시되게 만들면 좋을 것이다.
전투중(정확히는 나에게 적대중인 몬스터가 있는것)인 경우를 어떻게 판단하면 될까?
몬스터의 전투 범위 내에 들어갔을 때, 몬스터가 캐릭터의 SetCombatTarget에 접근해서 자기 자신으로 설정해주던 부분이 있었다. 그 부분을 활용하면 될 것이다.
대신 nullptr 여부를 따지는게 아니라 bool체크를 따로 할 것이다.
// Main.h
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Combat")
bool bHasCombatTarget;
// Main.cpp
AMain::AMain()
{
bHasCombatTarget = false;
}
캐릭터의 HealthBar를 만들어준것과 같이 EnemyHealthBar를 만들고 Percent에 바인딩 시켜준다.
// MainPlayerController.h
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Widgets")
TSubclassOf<UUserWidget> WEnemyHealthBar;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Widgets")
UUserWidget* EnemyHealthBar;
bool bEnemyHealthBarVisible;
void DisplayEnemyHealthBar();
void RemoveEnemyHealthBar();
}
// MainPlayerController.cpp
void AMainPlayerController::BeginPlay()
{
if (WEnemyHealthBar)
{
EnemyHealthBar = CreateWidget<UUserWidget>(this, WEnemyHealthBar);
if (EnemyHealthBar)
{
EnemyHealthBar->AddToViewport();
EnemyHealthBar->SetVisibility(ESlateVisibility::Hidden);
}
}
}
void AMainPlayerController::DisplayEnemyHealthBar()
{
if (EnemyHealthBar)
{
bEnemyHealthBarVisible = true;
EnemyHealthBar->SetVisibility(ESlateVisibility::Visible);
}
}
void AMainPlayerController::RemoveEnemyHealthBar()
{
if (EnemyHealthBar)
{
bEnemyHealthBarVisible = false;
EnemyHealthBar->SetVisibility(ESlateVisibility::Hidden);
}
}
// Main.h
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Controller")
class AMainPlayerController* MainPlayerController;
// Main.cpp
#include "MainPlayerController."
void AMain::BeginPlay()
{
Super::BeginPlay();
MainPlayerController = Cast<AMainPlayerController>(GetController());
}
// Enemy.cpp
void AEnemy::AgroSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
if (OtherActor)
{
AMain* Main = Cast<AMain>(OtherActor);
if (Main)
{
if (Main->MainPlayerController)
{
Main->MainPlayerController->RemoveEnemyHealthBar();
}
// .. 생략
}
void AEnemy::CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor && Alive())
{
AMain* Main = Cast<AMain>(OtherActor);
if (Main)
{
Main->SetCombatTarget(this);
if (Main->MainPlayerController)
{
Main->MainPlayerController->DisplayEnemyHealthBar();
}
// .. 생략
}
코드가 너무 길고 스파게티 코드가 되는듯한 느낌을 지울수가 없다.
아무리 실습이라고는 하지만 조심해야 할 코딩 스타일인것 같다.
아무튼 MainPlayerController에서 HUD를 그려줬던 것처럼 적의 체력바를 보여줄 것이다.
다만 On/Off가 되어야 하기 때문에 Main에서 PlayerController의 정보를 가지고 Enemy에서 접근할 것이다.
공격 범위 안에 들어오면 체력바가 켜지고 탐지 범위 밖으로 나가면 체력바가 꺼질 것이다.
다만 현재상태로 실행하면 체력바가 화면을 다 채워버릴 것이다.
그래서 적당한 크기로 조정하고 몬스터의 머리 위쪽으로 체력바를 표시하게 해 줄 것이다.
// Main.h
public:
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Combat")
FVector CombatTargetLocation;
FORCEINLINE void SetHasCombatTarget(bool HasTarget) { bHasCombatTarget = HasTarget; }
// Main.cpp
void AMain::Tick(float DeltaTime)
{
if (CombatTarget)
{
CombatTargetLocation = CombatTarget->GetActorLocation();
if (MainPlayerController)
{
MainPlayerController->EnemyLocation = CombatTargetLocation;
}
}
}
// MainPlayerController.h
public:
FVector EnemyLocation;
protected:
virtual void Tick(float DeltaTime) override;
// MainPlayerController.cpp
void AMainPlayerController::BeginPlay()
{
if (WEnemyHealthBar)
{
EnemyHealthBar = CreateWidget<UUserWidget>(this, WEnemyHealthBar);
if (EnemyHealthBar)
{
EnemyHealthBar->AddToViewport();
EnemyHealthBar->SetVisibility(ESlateVisibility::Hidden);
}
FVector2D Vec(0.f, 0.f);
EnemyHealthBar->SetAlignmentInViewport(Vec); // 초기 위치 정렬
}
}
void AMainPlayerController::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (EnemyHealthBar)
{
FVector2D PositionInViewport;
// 월드 좌표를 스크린 좌표로 변환
ProjectWorldLocationToScreen(EnemyLocation, OUT PositionInViewport);
FVector2D SizeInViewport(200.f, 25.f);
EnemyHealthBar->SetPositionInViewport(PositionInViewport);
EnemyHealthBar->SetDesiredSizeInViewport(SizeInViewport);
}
}
... 이제 코드는 이만 넣고 작업이나 하도록 하자.
이쯤 되니 코드가 너무 복잡하고 클래스 의존도가 커져서 기록할 의미가 크게 없는것 같다.
Main 클래스가 거의 슈퍼클래스급이 되었다.
적의 공격 범위나 감지 범위 여부에 따라 Main의 SetHasCombatTarget을 true/false로 잘 설정해야 한다.
적의 공격범위 안에 들어가면 체력이 잘 나오는 반면 공격범위에서 벗어나면 CombatTarget이 nullptr이 되버리기 때문에 체력바는 존재하지만 값을 가져오지 못하는 문제가 생긴다.
전투 영역을 벗어날 때 nullptr로 만드는 부분을 감지 범위를 벗어날 때로 옮겨주면 된다.
[79. Fine Tuning Character Death #1]
적의 죽음처리는 괜찮게 했지만 아직 캐릭터의 죽음 처리는 하지 않았다. 애니메이션이 한번 재생되고 말 뿐이다.
적의 죽음처리와 비슷하게 해주면 된다. 몽타주에 노티파이를 추가해주고 해당 노티파이가 호출되면 애니메이션 재생을 중지한다.
죽은 상태에서 공격이나 움직이는것은 부자연스러우므로 죽음 상태인지 체크하는 조건이 추가로 들어가야 할 것이다.
이동과 공격에 대한 부분은 죽음 여부로 감싸서 처리가 가능한데 점프는 상위 클래스의 기능을 바인딩 시켜서 사용한 것이라서 방법이 조금 다르다.
방법은 의외로 간단하다. 오버라이딩 해서 Super를 호출하는 부분을 감싸주면 된다.
또한 Tick 함수에서 스태미너를 체크하며 Sprint, Normal 상태로 변환하는 부분이 있는데 최상단에서 죽었는지 여부를 확인하고 return 시켜주면 간단히 해결된다.
마지막으로, 죽는 애니메이션이 재생되는 도중에 한번 더 공격을 받는다면 EndDeath가 호출되지 않아서 다시 처음부터 재생이 되는 문제가 있다.
이것 역시 죽음을 처리하는 Die 함수에서 상태를 확인해서 곧바로 return을 시켜주면 된다.
[80. Fine Tuning Character Death #2]
직전 챕터에서 마지막에 죽는 애니메이션이 연속으로 재생되는 경우를 방지했지만 애초에 죽은 상태의 플레이어를 더이상 공격하지 않게 막으면 된다.
몬스터가 유효한 대상을 가지고 있는지를 확인하는 불리언 값을 하나 추가해서 공격 범위 안에 들어오면 true, 감지 범위 밖으로 나가면 false로 설정하고 추가로 플레이어가 죽을 때 불리언값을 false로 만들어주면 될 것이다.