ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 블루아카이브 루트슈터 팬게임 프로젝트(13) - 체력, 잔탄 UI
    루트슈터 프로젝트 2024. 1. 11. 22:32

    안녕하세요. 이번 시간에는 체력바와 현재 장비한 총기의 잔탄수를 확인할 수 있는 UI를 만들어 보았습니다. 비교적 간단하다고 할 수 있지만 계속 삽질하느라 시간이 다 가버렸네요. 결론은 체력바와 같은 간단한 UI는 그냥 블루프린트에서 처리시키는게 더 편하다는 것입니다. 

     

     

    PlayerStatus.h

    UCLASS()
    class PROJECTBLUELANDS_API UPlayerStatus : public UUCustomUI
    {
    	GENERATED_BODY()
    
    public:
    	void Init() override;
    	class UPlayerHPBar* hpBar;
    	class UEXPBar* expBar;
    	class UMagazineState* magBar;
    
    };

     

    먼저 플레이어 상태 (체력바, 경험치바, 잔탄수 UI) 를 표시하는 UI들을 모두 묶을 UI 클래스를 하나 선언해줍니다. 이곳에는 보이듯이 체력바와 경험치, 잔탄수에 대한 UI들을 가지고 있습니다. 경험치바에 대한 설명은 아마 다음 포스팅에 할 것 같습니다. 

     

     

     

    UCLASS()
    class PROJECTBLUELANDS_API UPlayerHPBar : public UUCustomUI
    {
    	GENERATED_BODY()
    
    public:
    	UPROPERTY(BlueprintReadWrite, meta=(BindWidget))
    	TObjectPtr<class UProgressBar> PlayerHPBar;
    	UPROPERTY(BlueprintReadWrite, meta=(BindWidget))
    	TObjectPtr<class UTextBlock> NowHPText;
    };

     

    체력바 UI입니다. 체력바를 나타내는 progressbar와 체력의 정확한 수치 숫자를 나타내는 텍스트 블록을 하나 준비 해놓습니다. 

     

     

     

    체력바 UI의 블루프린트입니다. 구조는 단순하고 체력바의 퍼센트 프로그래스에 함수를 하나 바인딩 해줍니다. 

     

     

     

    최대  체력과 현재 체력을 받아서 나눠주면 퍼센티지가 나오므로 이 숫자를 퍼센트에 바인딩 해주면 됩니다. 정말 간단합니다

     

     

     

    UCLASS()
    class PROJECTBLUELANDS_API UMagazineState : public UUCustomUI
    {
    	GENERATED_BODY()
    
    public:
    	UPROPERTY(BlueprintReadWrite, meta=(BindWidget))
    	TObjectPtr<class UTextBlock> LeftBullets;
    	UPROPERTY(BlueprintReadWrite, meta=(BindWidget))
    	TObjectPtr<class UTextBlock> FullBullet;
    
    	void InitUI(int nowMag, int fullMag);
    	
    };

     

    다음은 잔탄수에 대한 UI입니다. 잔탄수에 대한 UI는 어떻게든 C++로 구현해보고 싶어서 비틀어본 결과 해괴한 결과가 나오긴 했는데 일단은 설명을 하겠습니다. 먼저 체력바와 마찬가지로 UI 클래스를 하나 생성해줍니다. 남은 탄약수와 한탄창에 있는 총알 수를 나타내는 텍스트 블록 2개를 생성해줍니다. InitUI는 총이 바뀌거나 탄창의 총알 개수가 바뀔 때 선언되는 새로고침 함수입니다. 

     

    void UMagazineState::InitUI(int nowMag, int fullMag)
    {
        LeftBullets->SetText(FText::FromString(FString::FromInt(nowMag)));
        FullBullet->SetText(FText::FromString(FString::FromInt(fullMag)));
    }

     

    cpp 코드는 다음과 같습니다.

     

    PlayerCharacter.cpp

    if(gameUIClass)
    	{
    		
    		APlayerController* con = Cast<APlayerController>(Controller);
    		if(!con) return;
    		gameUIWidget = CreateWidget<UUGameUI>(con, gameUIClass);
    		if (!gameUIWidget) return;
    		
    		gameUIWidget->player = this;
    		gameUIWidget->Init();
    		gameUIWidget->AddToViewport();
    		//gameUIWidget->SetVisibility(ESlateVisibility::Hidden);
    		if(Cast<UUInventory>(gameUIWidget->GetWidgetFromName(FName("Inventory"))))
    		{
    			inventoryUI = Cast<UUInventory>(gameUIWidget->GetWidgetFromName(FName("Inventory")));
    			inventoryUI->SetVisibility(ESlateVisibility::Hidden);
    			UE_LOG(LogTemp, Error, TEXT("found inventory")); 
    		}
    		if(Cast<UPlayerStatus>(gameUIWidget->GetWidgetFromName(FName("Status"))))
    		{
    			auto status = Cast<UPlayerStatus>(gameUIWidget->GetWidgetFromName(FName("Status")));
    			if(Cast<UMagazineState>(status->GetWidgetFromName(FName("Magazine")))) magazineUI = Cast<UMagazineState>(status->GetWidgetFromName(FName("Magazine"))); 
    		}
    		
    	}

     

    다음으로는 플레이어 클래스의 cpp 코드입니다. 여기서 게임 UI를 소환해서 뷰포트에 넣어줍니다. 보시면 GetWidgetFromName 함수를 이용해 UI들중 인벤토리 UI만 받아내서 인벤토리만 보이지 않게 변경해주는 내용을 보실 수 있습니다. 이처럼 UI클래스 안에 다른 UI들이 여러개 있다면 특정 UI만 찾아서 받아낼 수 있습니다. 여기서 Name의 기준은 블루프린트의 이름입니다. 여기서 제 탄창 UI(이름 Magazine)는 모든 UI 클래스 -> Status UI -> Magazine 에 있기 때문에 Status를 한번 불러준 다음에 탄창 UI를 불러올 수 있었습니다. 한번에 받아오는건 불가능 하더라고요.. 

     

    여기서 잔탄 UI가 변경되는 경우를 생각해보시면 

    1. 총알을 소비했을 때(발사했을때)

    2. 재장전을 했을 때

    2. 총을 바꿨을 때 (장비 교체)

    3. 아이템 옵션의 변동(아이템 교체로 인해 탄창크기 옵션 수치에 변동이 있을때)

     

    제가 지금 봤을때에는 이정도 있기 때문에 이 경우를 생각해서 InitUI 함수를 선언해주기로 했습니다. 

     

     

    void APlayerCharacter::Fire()
    {
    	if(gun && gun->nowMag !=0 ){
    		UAnimInstance* animIn = GetMesh()->GetAnimInstance();
    		if(animIn && FireMont){
    			animIn->Montage_Play(FireMont);
    		} else UE_LOG(LogTemp, Display, TEXT("Fire animation failed")); 
    
    		FVector startPoint = cameraComp->GetComponentLocation() + cameraComp->GetForwardVector() * 100;
    		FVector endPoint = startPoint + cameraComp->GetForwardVector() * 10000;
    		gun->Fire(startPoint, endPoint);
    		Recoil(0.3);
    		magazineUI->InitUI(gun->nowMag, gun->gunStat.maxMag * (1.0 + (playerStat[4] / 100.0)));
    	} else if(gun && gun->nowMag == 0) StartReload();
    }

     

    총알을 발사했을 때 

     

    void APlayerCharacter::FinishReload()
    {
    	gun->GunReloaded();
    	magazineUI->InitUI(gun->nowMag, gun->gunStat.maxMag * (1.0 + (playerStat[4] / 100.0)));
    	bNowReloading = false;
    }

     

    재장전이 끝났을 때 

     

    void APlayerCharacter::GunChangeTo(int slotNum)
    {
    	if(nowEquipGun == slotNum || gunSlot[slotNum].itemType == EItemType::EIT_DEFAULT) return;
    	ABluelandsGameModeBase* gameMode;
    	gameMode = Cast<ABluelandsGameModeBase>(UGameplayStatics::GetGameMode(this)); //슬롯의 총을 소환하기 위해 게임모드 불러오기 
    
    	if(gameMode)
    	{
    		TSubclassOf<AItemBase> temp = gameMode->GetGunBP(gunSlot[slotNum].weaponStat.gunBPNum); //바꾸자 하는 총의 BP 번호를 넣기
    		//TSubclassOf<AItemBase> temp = gameMode->GetGunBP(slotNum);
    		if(temp)
    		{
    			gun->Destroy();
    			gun = GetWorld()->SpawnActor<AGun>(temp);
    			gun->gunStat = gunSlot[slotNum].weaponStat;
    			gun->Tags.Add(TEXT("Equipped"));
    			gun->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, TEXT("WeaponSocket"));
    			gun->SetOwner(this);
    			gun->GunReloaded();
    			magazineUI->InitUI(gun->nowMag, gun->gunStat.maxMag * (1.0 + (playerStat[4] / 100.0))); // 총을 바꾸면 탄창 크기가 바꿔질 수 있으니 초기화 
    			nowEquipGun = slotNum;
    		}
    	}
    
    }

     

    장비한 총기를 교체했을 때 

     

     

    void APlayerCharacter::InitPlayerStat() //방어구로 얻는 플레이어 스탯 초기화
    {
    	for(int t = 0; t<7; t++) //플레이어 스탯 초기화
    	{
    		playerStat[t] = 0.0;
    	}
    
    	for(int i = 0; i<3; i++)
    	{
    		if(armorSlot[i].itemType == EItemType::EIT_DEFAULT) continue; //방어구 칸이 비어있는 경우 패스
    		switch (armorSlot[i].armorStat.armorType)
    		{
    		case EArmorType::EAT_HAT:
    			playerStat[1] += armorSlot[i].armorStat.mainStat;
    			break;
    		case EArmorType::EAT_WATCH:
    			playerStat[5] += armorSlot[i].armorStat.mainStat;
    			break;
    		case EArmorType::EAT_BACKPACK:
    			playerStat[0] += armorSlot[i].armorStat.mainStat;
    			break;
    		default:
    			break;
    		}
    		for(int t = 1; t<7; t++)
    		{
    			playerStat[t] += armorSlot[i].armorStat.option[t];
    		}
    	}
    	playerMaxHP = playerMaxHPByLevel + playerStat[0]; //플레이어의 체력이 변경될 수 있으니 초기화 
    	magazineUI->InitUI(gun->nowMag, gun->gunStat.maxMag * (1.0 + (playerStat[4] / 100.0))); // 탄창 크기가 변경될 수 있으니 초기화 
    }

     

    방어구 아이템에서 얻어지는 옵션에 변동이 있을 때 magazineUI를 한번씩 새로고침 해주면 됩니다. 

     

     

     

    그렇게 나온 결과물입니다. UI가 기본 디자인이라 퀄리티가 낮긴 하지만 잘 나오긴 합니다.. 다음 시간에는 경험치와 레벨 시스템에 대해 포스팅 하도록 하겠습니다. 

Designed by Tistory.