본문 바로가기

강동새싹언리얼수업/언리얼

231123 Widget in Actor C++

플레이어에 Widget을 달아보자.

사실 플레이어는 Widget에서 GetFirstCharacter()를 이용해서 HP변수에 접근해서 HPBar 업데이트가 가능하다.

그런데 적은 레벨에 여러개가 있으니 접근이 어렵다. 따라서 적Character에 HPBar를 붙여주고 직접 관리해줘야 한다. 

블루프린트에서 하는 법은 위에 블로그 해놓왔고 여기서는 C++에서 한다.이거 완전 하드 코딩이다. 왜냐하면 이렇게 않하면 만드는 적마다 Widget 컴포넌트를 붙여주고 설정해야 하기 때문이다. 하지만 FULL C++로 만드는건 어렵다 ㅎㅎ

기존의 UserWidget에서 Pawn에 접근할수 있는 GetOwner 함수가 없다 따라서 GetOwner를 가져오려면 서로 WidgetComponent의 GetOwner 함수를 사용하여 가져와야 한다. WidgetComponent는 Pawn에 붙어있으니 가능하다

그러기 위해선 WidgetComponent와 UserWidget을 둘 다 상속받아 새로운 클래스를 만들어 주어야 한다.

왜냐면 WidgetComponent는 UserWidget에게 자신의 Owner 정보를 넘겨야 하고

UserWidget은 넘어온 Owner 정보를 저장하고 있어야 하니까..

 

이를 처리하고자, 기존의 UWidgetComponent가 UUserWidget을 만들 때, 해당 UUserWidget에게 Owner 정보를 넘기도록 한다.

좀 다르지만 캐릭터에 위젯 컴포넌트를 +로 수동으로 붙이면 인스턴스 생성이 생략되고 이걸 끌어다 놓고 Cast해서 widget에 접근해서 설정하는 법도 있다. 이걸 C++로 구현한다.

C++클래스를 만든자 UserWidget을 상속받고 이름은 HPBar로 한다.

widget표시에 필요한 변수들을 선언해준다 .widget designer에서 써야 하므로 BlueprintReadWrite를 선언해준다.

HPBar.h

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "HPBar.generated.h"

/**
 *
 */
UCLASS()
class TPSPROJECT_API UHPBar : public UUserWidget
{
	GENERATED_BODY()
public:
	UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "HPBar")
	FText ctextId;
	UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "HPBar")
	FText cInfo;
	UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "HPBar")
	int cHP;
	UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "HPBar")
	int cMaxHP;
	UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "HPBar")
	int cMP;
	UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "HPBar")
	int cMaxMP;
};

HPBar.cpp는 편집하지 않는다.

Widget Blueprint를 하나 만드는데 부모를 방금만든 HPBar로 한다.

열어보면 HPBar가 상속되어 있음을 확인할 수 있다.

다음과 같이 디자인 해준다.

ProgressBar 2개, Text 2개는 변수화 해준다.

4개다 Create Binding해준다.

MyBlueprint 톱니바퀴를 눌러 Show Inherited Variables를 체크해준다

그럼 상속된 HPBar의 변수들이 보인다 이 변수들을 끌어다

다음 4개의 Binding함수를 만든다.

MP Progressbar

HP Progressbar

HP Text

MP Text

Enemy.h에 위젯을 연결할 변수를 선언해준다.

	UPROPERTY(EditAnywhere)
	TSubclassOf<class UHPBar> widgetRef;
	UPROPERTY()
	class UHPBar* hpBar;
	UPROPERTY()
	class UWidgetComponent* widgetComp;

Enemy.cpp 생성자끝부분에 widgetComp를 초기화 해준다.

LoadClass에서 UUserWidget으로 로딩안하면 잘 안된다. UHPBar로 로딩해서  UUserWidget 으로 Casting하면 된지도 모르겠다 BeginPlay()에서 hpBar를 초기화 해준다.

여기서 CreateDefaultSuboject '컴포넌트'를 만들어주는 함수이다. 이때 인자로 넘어가는 FName 액터에 속한 컴포넌트들을 식별하는데 사용된다. 다른 컴포넌트들과 이름이 중복되어선 안된다.

그리고 기능이 아닌, Transform을 가지고 있는 컴포넌트이기때문에 SetupAttachment Transform을 설정해주어야 한다.

#include <Components/WidgetComponent.h>
#include "Components/widget.h"
#include "HPBar.h"

widgetComp = CreateDefaultSubobject<UWidgetComponent>(TEXT("HPWidget"));
widgetComp->SetupAttachment(GetMesh());
widgetComp->SetRelativeLocation(FVector(0, 0, 200.0f));

이제 우리가 만든 Widget Blueprint Reference를 가져와, WidgetComponent에게 넘겨주면 된다.

여기서 FObjectFinder가 아닌, FClassFinder임을 주의하자

이는 위에서 AnimInstance때와 똑같은데, 우리가 어떠한 Blueprint를 만들고 해당 Parent Class를 우리가 만든 Class로 지정해주었었다. 그런 에셋들에 대해서는 해당과 같이 가져온다고 생각하면 쉽다. 에디터를 열 때 (게임을 시작할때) 생성되어 있기 때문이다.

UClass* WidgetCompClass = LoadClass<UUserWidget>(NULL, TEXT("/Game/UI/WBP_HPEnemy.WBP_HPEnemy_C"));
if (WidgetCompClass) {
    widgetComp->SetWidgetClass(WidgetCompClass);
    widgetComp->SetWidgetSpace(EWidgetSpace::Screen);
    widgetComp->SetDrawSize(FVector2D(230, 75));
    widgetComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}

void AEnemy::BeginPlay()
{
	Super::BeginPlay();
	widgetComp->InitWidget();
	hpBar = Cast<UHPBar>(widgetComp->GetUserWidgetObject());

 

hpBar->cHP 변수에 접근해 상태를 업데이트 할 수 있다.

void UEnemyFSM::OnDamageProcess()
{
	// 체력감소
	hp--;
	// 만약 체력이 남아있다면
	if (hp > 0 )
	{
		// 상태를 피격으로 전환
		mState = EEnemyState::Damage;
		me->hpBar->cHP = hp;

 

다른 방법으로는 

커플링을 방지하기 위해

체력이 업데이트 될때

체력이 0이 되어서 사망했을때

에 대한 Delegate를 만들고, 캐릭터의 체력이 업데이트 될 때 Delegate에 등록되어있는 모든 객체들에게 알리도록 처리하였다.

 

이 사진에서 주의할 점은 UserWidget이 GetOwner() 라는 함수를 가지고 있다는 점인데,

이 함수는 현재 언리얼에서 제공하지 않고 있다.

 

우리의 Widget이 플레이어의 정보를 알려면 2가지 방법이 있다.

 

1. UUserWidget과 UWidgetComponent를 상속받은 새로운 클래스에서 Actor를 저장해놓고, 가져다 쓰기

2. 엔진을 커스타미징하여 UWidgetComponent에서 받아온 GetOwner를 UUserWidget에서 저장하기

 

필자는 2번 방식으로 사용했다.

필자가 직접 엔진 커스터마이징을 하여 제공하고 있는 것이고, 해당 내용은 아래 문서를 참고해달라

 

https://openmynotepad.tistory.com/120

 

 

CharacterWidget CharacterBase가 아닌, IABCharacterWidgetInterface를 통해 처리하게 하여 의존성을 없앴다.

 

우리의 캐릭터가 데미지를 입으면, OnHPChanged Delegate를 호출한다.

이 Delegate에 위의 UpdateHPBar가 Bind 되어 있는것이 보인다.

 

 

마지막으로 알아야 할 것은, 언제 Widget이 만들어 지는것인가에 대한 것이다.

이는, InitWidget를 보면 알 수 있다.

 

 

우리가 위에서, UserWidget의 Class를 가져와 WidgetComponent에 붙여준 것을 기억할 것이다.

그 붙여준 Class를 InitWidget때 생성하게된다.

 WidgetComponent는 빈 껍데기일 뿐이고, UserWidget을 담는 그릇정도로 생각하면 된다.

 

이로써 알 수 있는것은

 

1. Actor가 BeginPlay가 될 때, 모든 Actor들에 대해서 BeginPlay를 호출한다.

2. 이 BeginPlay가 호출 될 때, InitWidget이 호출되는데 이 때 Widget이 생성된다.

 

결론은

Actor-BeginPlay

WidgetComponent-InitWidget

UserWidget-NativeConstruct

순으로 호출된다.

이를 기억해두자

 

https://openmynotepad.tistory.com/119

 

[Unreal Engine 5] Widget ( UI, 체력바 )

우리가 어떠한 3D UI 요소를 만든다면, 그것을 게임 세계에 표현할 수 있다. 우리는 'Widget Blueprint Editor' 에서 다양한 Widget를 사용해 UI를 꾸밀 수 있고 함수 기능을 추가할 수 있다. 이번에는 이 Widg

openmynotepad.tistory.com

 

'강동새싹언리얼수업 > 언리얼' 카테고리의 다른 글

231127 DrawDebugSphere() and HitResult  (0) 2023.11.28
231127 라인트레이스  (0) 2023.11.27
231125 205 특강  (2) 2023.11.25
211124 LineTrace C++  (1) 2023.11.24
231123 LineTrace  (0) 2023.11.23