ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 블루아카이브 루트슈터 팬게임 프로젝트(10) - 데미지 표시 & 코드 정비
    루트슈터 프로젝트 2024. 1. 7. 23:20

    보더랜드3

     

    안녕하세요. 어제는 이사를 앞두고 엄청 바쁜 하루였기 때문에 포스팅을 하지 못했습니다. 사실 아직 이사가 끝나지 않았지만 개발은 멈추지 않습니다. 이번에는 간단하게 데미지 표시 UI 기능을 구현해보았습니다. 한발당 데미지를 표시해주는 기능은 플레이어의 성장 정도를 한눈에 알 수 있게 해주는 매우 중요한 척도이기 때문에 필수로 있어야 하는 기능입니다. 

     

     

    DamageIndicator.h

    UCLASS()
    class PROJECTBLUELANDS_API UDamageIndicator : public UUserWidget
    {
        GENERATED_BODY()
       
    public:
        UPROPERTY(BlueprintReadWrite, meta=(BindWidget))
        TObjectPtr<class UTextBlock> damageText;

        UPROPERTY(BlueprintReadWrite, meta=(BindWidgetAnim), Transient)
        class UWidgetAnimation* damageAnimation;

        void Init(float damage, bool isCritical);
    };

     

    데미지 표시를 해주는 UI 클래스를 하나 생성해줍니다. 데미지 숫자만 표시해주면 되기 때문에 매우 단순합니다... 만 UWidgetAnimation이라는 변수에 주목해 주시기 바랍니다. 언리얼에서는 UI에도 애니메이션을 부여해줄 수 있고 이것을 Widget Animation이라고 부릅니다. BindWidgetAnim을 선언해주시고, Transient를 붙여주셔야 컴파일시 오류가 뜨지 않습니다. 

     

    void UDamageIndicator::Init(float damage, bool isCritical)
    {
        if(isCritical)
        {
            damageText->SetColorAndOpacity(FSlateColor::FSlateColor(FColor::Yellow));
        }
        FString temp = FString::FromInt(int(damage));
        damageText->SetText(FText::FromString(temp));
        PlayAnimation(damageAnimation);
    }

     

    이 UI를 초기화 해주는 함수입니다. 데미지를 정수부분만 잘라서 텍스트로 옮겨주고 Set, 그리고 만약 이 데미지가 치명타 데미지라면 숫자 색깔을 노란색으로 바꿔줍니다. 일반 데미지는 하얀색, 치명타는 노란색으로 보여줌으로써 플레이어가 치명타를 가했는지 아닌지 식별할 수 있습니다. 

     

     

     

    이 UI의 블루프린터입니다. 사격을 할 때 편하게 데미지를 확인할 수 있도록 살짝 오른쪽 위에 데미지가 뜨도록 했습니다.

     

     

    그리고 여기서 중요한 위젯의 애니메이션입니다. 저는 UI 애니메이션을 데미지 텍스트가 처음에 한번 생성되고나서 위로 점점 올라가면서 사라지도록 하고싶었습니다. 그래서 Y 위치와 외형의 컬러 및 오파시티를 키 프레임 설정으로 조정하여 위로 올라 간 후 0.5초후에는 완전히 투명해지도록 설정하였습니다. 

     

     

     

    Projectile.h

    public:
        bool bIsCritical;

    그리고 투사체에 이 총알이 치명타인지 아닌지 변수 하나를 추가해줍니다. 

     

     

    Gun.h

    void AGun::SpawnBullet(FVector direction)
    {

        AProjectile* proj = (GetWorld()->SpawnActor<AProjectile>(
            projectileClass ,
            projectileSpawnPoint->GetComponentLocation(),
            projectileSpawnPoint->GetComponentRotation()
        ));
        if(proj)
        {
            if(FMath::RandRange(0.0, 100.0) < this->gunStat.critChance) //크리티컬 성공시
            {
                proj->SetDamage(gunStat.weaponDamage * (this->gunStat.critDamage/100.0)); //투사체 스폰 및 데미지 설정
                proj->bIsCritical = true;
            }
            else{
                proj->SetDamage(gunStat.weaponDamage); //투사체 스폰 및 데미지 설정
                proj->bIsCritical = false;
            }
            proj->SetOwner(this); //이 총알의 주인 = 총
            proj->SetVelocity(direction);
        }
    }

     

    제 게임에서는 총이 총알을 발사할때 데미지 계산을 전부 끝냅니다. 그래서 총의 총알 스폰 함수에서 모든 데미지 계산을 수행합니다. 총알을 스폰한 후 주사위를 굴려서 크리티컬을 성공하면 데미지 가산을 한 후 투사체의 isCritical을 true로 설정해줍니다. 

     

     

    Projectile.cpp

    void AProjectile::OnHit(UPrimitiveComponent* hitComp, AActor* otherActor, UPrimitiveComponent* otherComp, FVector normalImpulse, const FHitResult& hitResult)
    {
        auto myOwner = GetOwner();
        if(!myOwner)
        {
            Destroy();
            return;
        }
       
        AController* myOwnerInstigator = myOwner->GetInstigatorController();
        UClass* damageT = UDamageType::StaticClass();

        if(otherActor && otherActor != this && otherActor != myOwner)
        {
            UGameplayStatics::ApplyDamage(otherActor, damage, myOwnerInstigator, this, damageT);
        }
        Destroy();
    }

     

    그리고 Projectile의 cpp 파일입니다..만 여기서 변경한 점은 ApplyDamage에서 DamageCauser를 총이 아닌 총알로 바꾸어 줬습니다. 데미지 계산은 이미 총에서 총알을 발사할때 미리 끝내뒀기 때문에 더이상 총에 대한 정보가 필요 없을 것으로 판단했기 때문이고, 또한 투사체 정보를 받아와서 치명타가 적용된 총알인지 아닌지를 받아오고 싶었기 때문입니다. 이렇게 하는것이 과연 옳은것인지는 모르겠습니다만.. 아니면 DamageType 클래스를 이용해 보는것도 다른 선택지지 않을까 싶습니다. 

     

     

    HealthComponent

     

    void UHealthComponent::DamageTaken(AActor* damagedActor, float Damage, const UDamageType* damageType, AController* instigator, AActor* damageCauser)
    {
        if (Damage <= 0.0f) return;
        nowHealth -= Damage;

        if(ownerType == EObjectType::EO_ENEMY && enemy) //주인이 적일 경우
        {
            enemy->ShowHPBar(nowHealth / maxHealth);

            if(Cast<AProjectile>(damageCauser))
            {
                enemy->ShowDamageIndicator(Damage, Cast<AProjectile>(damageCauser)->bIsCritical);
            }
        }

        if(nowHealth <= 0.0f && bluelandsGameMode) bluelandsGameMode->ActorDied(damagedActor);
    }

     

    그리고 체력을 담당하는 컴포넌트에서 DamageTaken 함수에서 damageCauser를 캐스팅하여 적에게서 데미지 UI를 보여줄 수 있도록 함수를 실행시켜주었습니다. 

     

     

     

    EnemyCharacter.h

    public:
        UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
        TSubclassOf<class UDamageIndicator> damageIndicatorClass;

        class UDamageIndicator* damageIndicatorWidget[20];
        int indicatorIndex = 0;

        void ShowHPBar(float nowHPPercent);

        void ShowDamageIndicator(float damage, bool isCritical);

     

     

    방금 체력 컴포넌트에서 호출한 함수는 여기에서 선언해줍니다. 체력바와 데미지UI는 모든 적들이 가져야 하는 함수이기 때문에 적 캐릭터 클래스에 선언하여 적들이 모두 상속받을 수 있게 해줍니다. 여기서 주목할 포인트는 

        class UDamageIndicator* damageIndicatorWidget[20];
        int indicatorIndex = 0;

     

    이부분입니다. 

     

    EnemyCharacter.cpp

    void AEnemyCharacter::ShowDamageIndicator(float damage, bool isCritical)
    {
        if(indicatorIndex >= 20) indicatorIndex = 0;

        damageIndicatorWidget[indicatorIndex] =
                  Cast<UDamageIndicator>(CreateWidget(GetWorld(), damageIndicatorClass));
     
        if(damageIndicatorWidget[indicatorIndex])
        {
            damageIndicatorWidget[indicatorIndex]->AddToViewport();
            damageIndicatorWidget[indicatorIndex]->Init(damage, isCritical);
        }
        indicatorIndex++;
        if(indicatorIndex == 20) indicatorIndex = 0;
    }

     

    이 함수를 보시면 알겠지만, 데미지표시 UI를 20개의 인덱스의 배열을 만들어서 돌아가면서 쓰고 있습니다.

    이렇게 하는 이유는 UI를 하나만 계속 사용하게 되면 총을 연사했을 때 데미지 UI의 애니메이션이 계속 초기화되면서 처음에 가한 데미지 숫자가 안보이기 때문입니다. 

     

    https://youtu.be/6-OQiKK6W1I?si=S_KAk4ve8rhdP_bn

     

    맨처음에는 이분의 동영상을 참고하여 제작하였지만 이 방법대로 제작한다면 데미지를 연속으로 가했을때 처음에 가한 데미지 UI의 애니메이션이 초기화되어 앞에 가한 데미지 숫자가 안보이는 현상이 생깁니다. 이를 보완하기 위해 저는 20개의 UI 배열을 선언하여 0부터 19번까지 계속 돌아가면서 사용하였습니다. 

     

     

     

    적 캐릭터 블루프린트에 UI 블루프린트를 등록하고 플레이를 해본 결과입니다. UI 애니메이션도 정상 작동하고 숫자도 치명타인지 아닌지에 따라 색깔도 변해서 나옵니다. 

     

     

     

    마지막으로 8번 포스팅에서 체력 컴포넌트에 대해 이런 코드가 있었습니다. 

     

    void UHealthComponent::DamageTaken(AActor* damagedActor, float Damage, const UDamageType* damageType, AController* instigator, AActor* damageCauser)
    {
        if (Damage <= 0.0f) return;
        nowHealth -= Damage;
    
        if(AEnemyCharacter* enemy = Cast<AEnemyCharacter>(damagedActor))
        {
            UEnemyHPBarWidget* temp = Cast<UEnemyHPBarWidget>(enemy->hpBarComponent->GetWidget());
            temp->RefreshHP(nowHealth / maxHealth);
        }
    
        if(nowHealth <= 0.0f && bluelandsGameMode) bluelandsGameMode->ActorDied(damagedActor);
    }

     

    이것을 자세히 보시면.. 체력을 담당할 뿐인 컴포넌트가 위젯까지 알아야 한다는게 지금 보니까 정말 이상하더군요.

    이게 최선인가 생각해보니, 그냥 적 캐릭터 클래스에서 이 위젯의 처리를 해주는 함수를 하나 만들어주면 되는거였습니다. 

     

    EnemyCharacter.cpp

    void AEnemyCharacter::ShowHPBar(float nowHPPercent)
    {
        hpBarWidget->RefreshHP(nowHPPercent);
    }

     

    그냥 적 캐릭터 클래스에서 함수 한줄 추가하면 끝나는 것이었는데 괜히 저기에서 위젯을 다루겠다고 체력 컴포넌트에 위젯 헤더파일을 전부 선언하고 난리를 쳤었네요...

     

     

    나 뭐하는 놈이지..

     

     

    아무튼 지금이라도 알아서 고쳤으니 다행이라고 여기고, 이만 줄이겠습니다. 빨리 이사가 전부 마무리 되서 개발에 집중할 수 있으면 좋겠네요. 

     

Designed by Tistory.