udemy의 [Unreal Engine C++ The Ultimate Game Developer Course]

 

인프런 루키스님의 언리얼 엔진 4 입문 (C++ 기반) 하나만으로는 확실히 부족하다고 느껴서 병행해서 진행할 예정이다.

 

언리얼 온라인 러닝이나 공식 문서를 찾아가며 공부하는것도 확실하겠지만 현재 단계에서는 마냥 찾아가며 보는건 시간을 효율적으로 쓰지 못할 것 같다.

 

가격도 얼마 안해서 부담은 안되는데 자동번역으로 보는거라 조금 걱정이 되긴 한다.

구글이 있으니 어떻게든 되겠지.

'언리얼 엔진 > UE4 C++' 카테고리의 다른 글

[UE C++] The Pawn Class #1  (0) 2022.09.04
[UE C++] The Actor Class #2  (0) 2022.09.04
[UE C++] The Actor Class #1  (0) 2022.09.03
[UE C++] Intro to Unreal Engine C++  (0) 2022.09.03
[UE C++] Download UE4, and Intro to the Engine  (0) 2022.09.03

위젯 블루프린트를 생성해서 작업한다.

 

 

 

실습할 환경. 너비는 200, 하이트는 50으로 설정한다.

 

팔레트에서 Progress Bar를 가져와서 계층구조에 넣는데, 차후 코드에서 작성할 때 동일한 이름으로 맞춰서 작성하기 때문에 이름을 의미있게 지어주어야 좋다.

 

 

 

Progress Bar를 사용하고 속성도 저렇게 맞춰준다.

 

블루프린트 방식으로 관리해도 되지만 속도가 느린 경향이 있으므로 C++로 작업할 때는 위젯 하나당 C++ 파일을 만들어서 세트로 연동시키는 것이 더 좋다.

실습에서는 User Widget을 상속받은 C++클래스를 만들어 주도록 한다.

 

 

애니메이션 블루프린트를 관리할 때, MyAnimInstance에 온갖 필요한 정보를 넣어놓고 사용하고 C++ 코드에서는 애니메이션 블루프린트의 존재를 모르고 클래스를 통해 정보를 전달하는 느낌으로 만들어서 사용했었다.

그와 유사하게 중간에 하나의 클래스를 만들어서 데이터를 관리한다고 생각하면 된다.

 

 

C++ 클래스를 만들었다면 기본으로 설정되어있는 부모 클래스를 만들어둔 클래스로 변경한다.

 

// MyCharacter.h
UPROPERTY(VisibleAnywhere)
class UWidgetComponent* HpBar;

// MyCharacter.cpp
#include "Components/WidgetComponent.h"
#include "MyCharacterWidget.h"

AMyCharacter::AMyCharacter()
{
	// .. 생략
    
    HpBar = CreateDefaultSubobject<UWidgetComponent>(TEXT("HPBAR"));
    HpBar->SetupAttachment(GetMesh());

    // 월드공간 기준으로 배치될지(3D UI), 스크린 좌표 기준으로 배치될지(2D UI) 결정
    // Screen은 절대로 화면 밖으로 짤리지 않는다
    HpBar->SetWidgetSpace(EWidgetSpace::Screen);

	// ConstructorHelpers로 파일을 불러들여서 설정하는 익숙한 그것
    static ConstructorHelpers::FClassFinder<UUserWidget> UW(TEXT("WidgetBlueprint'/Game/UI/WBP_HPBar.WBP_HPBar_C'"));
    if (UW.Succeeded())
    {
        HpBar->SetWidgetClass(UW.Class);
        HpBar->SetDrawSize(FVector2D(200.f, 50.f));
    }
}

void AMyCharacter::PostInitializeComponents()
{
	// .. 생략
    HpBar->InitWidget();

    // todo : 체력이 변화 될 때의 델리게이트 바인딩을 해주면 됨
}

 

아직 MyCharacterWidget은 작성하지 않았다. 여기까지 작성하고 실행하면 아래와 같은 결과가 나온다.

 

 

HpBar->SetRelativeLocation(FVector(0.f, 0.f, 200.f));

 

MyCharacter의 생성자에서 해당 코드를 추가해주면 체력바가 캐릭터 머리 위로 위치하게 된다.

 

 

이제 실시간으로 체력이 변화하는것을 구현할 때이다.

 

// MyStatComponent.h
DECLARE_MULTICAST_DELEGATE(FOnHpChanged); // 델리게이트 추가

public:
	void SetHp(int32 NewHp); // 앞으로 체력 변동은 이 메소드가 맡게된다
    void GetMaxHp() { return MaxHp; }
    float GetHpRatio() { return Hp / static_cast<float>(MaxHp); }

private:
    UPROPERTY(EditAnywhere, Category = Stat, Meta = (AllowPrivateAccess = true))
    int32 MaxHp; // 최대체력 추가
    
public:
	FOnHpChanged OnHpChanged;


// MyStatComponent.cpp
void UMyStatComponent::SetLevel(int32 NewLevel)
{
	auto MyGameInstance = Cast<UMyGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
	if (MyGameInstance)
	{
		auto StatData = MyGameInstance->GetStatData(NewLevel);
		if (StatData)
		{
			Level = StatData->Level;
			SetHp(StatData->MaxHp);
			MaxHp = StatData->MaxHp;
			Attack = StatData->Attack;
		}
	}
}

void UMyStatComponent::SetHp(int32 NewHp)
{
	Hp = NewHp;
	if (Hp < 0)
		Hp = 0;

	// 옵저버 패턴 이용. 구독하는 클래스들에게 메시지를 날려줌
    // 싱글턴으로 접근해서 수정하는 다른방법도 있다. 다만 옵저버 패턴을 이용하면 결합도가 느슨해진다
	OnHpChanged.Broadcast();
}

void UMyStatComponent::OnAttacked(float DamageAmount)
{
	int32 NewHp = Hp - DamageAmount;
	SetHp(NewHp);

	UE_LOG(LogTemp, Warning, TEXT("OnAttacked %d"), Hp);
}

 

// MyCharacterWidget.h
UCLASS()
class TESTUNREALENGINE_API UMyCharacterWidget : public UUserWidget
{
	GENERATED_BODY()

public:
	void BindHp(class UMyStatComponent* StatComp);

	void UpdateHp();

private:
	TWeakObjectPtr<class UMyStatComponent> CurrentStatComp;

	// 블루프린트에서 만들었던 Progress Bar와 매핑 되게끔 유도함
	// 따로 초기화를 하지 않아도 이름을 동일하게 하면 블루프린트에서 만들어준 것을 알아서 찾아서 바인딩 함
	UPROPERTY(meta=(BindWidget))
	class UProgressBar* PB_HpBar;
};

// MyCharacterWidget.cpp
#include "MyStatComponent.h"
#include "Components/ProgressBar.h"

void UMyCharacterWidget::BindHp(UMyStatComponent* StatComp)
{
	// PB_HpBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PB_HpBar"));
	// 자동 바인딩을 하지 않을거면 이렇게 수동으로 해주면 됨
	CurrentStatComp = StatComp;
	StatComp->OnHpChanged.AddUObject(this, &UMyCharacterWidget::UpdateHp);
}

void UMyCharacterWidget::UpdateHp()
{
	if (CurrentStatComp.IsValid())
	{
		PB_HpBar->SetPercent(CurrentStatComp->GetHpRatio());
	}
}

 

// MyCharacter.cpp
void AMyCharacter::PostInitializeComponents()
{
	// .. 생략
	// todo
	auto HpWidget = Cast<UMyCharacterWidget>(HpBar->GetUserWidgetObject());
	if (HpWidget)
	{
		HpWidget->BindHp(Stat);
	}
}

 

전체적인 흐름은 꽤 복잡할 수 있다.

 

MyCharacter에서 HpBar에 대한 세팅과 BindHp로 바인딩

-> 피격시 AttackCheck에 의해 TakeDamage를 호출

-> TakeDamage에서 OnAttacked를 호출

-> OnAttacked에서 SetHp를 호출

-> SetHp에서 브로드캐스팅 호출

-> 맨 처음 BindHP에서 구독을 한 UpdateHp 호출

-> 최종적으로 UpdateHp에서 CurrentStatComp에서 체력 비율을 받아와서 Progress Bar에 적용하게 된다.

 

전반적인 흐름은 UI를 만들고 이름을 지어준 것을 이어 받아서 C++ 클래스에서 잘 바인딩 하여 연동시켜주면 된다.

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] Behavior Tree #1  (0) 2022.09.12
[UE4 입문] AI Controller  (0) 2022.09.11
[UE4 입문] 스탯 매니저  (0) 2022.09.02
[UE4 입문] 아이템 줍기  (0) 2022.09.02
[UE4 입문] 소켓 실습  (0) 2022.09.02

DX나 유니티의 경우 싱글톤 패턴이나 전역 클래스를 이용해서 어디서든 편하게 접근할 수 있는 데이터 매니저를 만들어서 관리하면 굉장히 편했으나, 언리얼의 경우 이미 구조가 어느정도 잡혀있기 때문에 데이터 매니저를 만든다고 하면 어디에 둘지 굉장히 고민이 된다.

물론, 언리얼은 이미 구비가 되어있다. C++ 클래스의 GameInstance를 상속받아 만들면 된다.

 

보통 기획 직군이 밸런스를 담당해서 붙여줄텐데 데이터(수치)들을 하드코딩해서 관리하면 편하게 작업할 수 없고 수정사항이 발생할 때마다 프로그래머가 고쳐주어야 하는 불편함이 있다.

뿐만 아니라 하드코딩을 해버리면 실행파일 자체에 묶여서 들어가기때문에 수치만 조정하고 싶어도 다시 빌드해서 배포하는 수밖에 없다.

그러므로 데이터와 코드는 분리하는게 여러모로 편하다

 

 

// MyGameInstance.h

#include "Engine/DataTable.h"

USTRUCT()
struct  FMyCharacterData : public FTableRowBase
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 Level;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 Attack;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 MaxHp;
};

 

 

행 구조 선택에서 C++ 코드로 작성해둔 구조체를 선택해주면 된다.

 

 

 

이제 이 데이터 테이블은 바이너리에 묶여서 나가는 방식이 아닌 외부 파일로 빠지게 된다.

행을 추가해서 각각의 데이터를 정해놓을 수 있다. 물론 이 방법뿐만 json이나 xml등을 파싱해서 사용하는것도 하나의 방법이다.

 

 

// MyGameInstance.h
class TESTUNREALENGINE_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()

public:
	UMyGameInstance();

	virtual void Init() override;

	FMyCharacterData* GetStatData(int32 Level);

private:
	UPROPERTY()
	class UDataTable* MyStats;
	
};

// MyGameInstance.cpp
UMyGameInstance::UMyGameInstance()
{
	static ConstructorHelpers::FObjectFinder<UDataTable> DATA(TEXT("DataTable'/Game/Data/StatTable.StatTable'"));
	
	MyStats = DATA.Object;
}

void UMyGameInstance::Init()
{
	Super::Init();

	UE_LOG(LogTemp, Warning, TEXT("MyGameInstance %d"), GetStatData(1)->Attack);
}

FMyCharacterData* UMyGameInstance::GetStatData(int32 Level)
{
	return MyStats->FindRow<FMyCharacterData>(*FString::FromInt(Level), TEXT(""));
}

 

 

만들어둔 게임 인스턴스 클래스를 사용하려면 [세팅 - 프로젝트 세팅 - 맵&모드] 에서 게임 인스턴스 클래스를 바꿔주어야 한다.

 

 

A캐릭터와 B캐릭터간의 전투 시스템이 구현되고 A가 B를 공격했을 때, A클래스에서 B클래스에 접근해서 체력을 깎아야 하는지, 혹은 중간의 매니저쪽에서 처리해야 하는지가 고민이 된다.

대부분의 경우에서는 데미지를 받는 쪽에서 피격 함수를 만들어서 처리하는게 여러 상황들이 발생했을 때를 고려하면 좀 더 깔끔하다.

 

데이터 테이블로 데이터를 외부로 빼서 관리한다고 해도 캐릭터의 현재 체력같은 것들은 결국 클래스에서 관리를 해야하는 부분이다. 그렇다고 해당 데이터들을 계속 추가하는 것은 많아지면 관리하기 어려우므로 스탯 컴포넌트를 따로 만들어서 스탯끼리 관리하고 캐릭터에 붙여놓는게 조금 더 편리한 방법일 수 있다.

 

// MyStatComponent.h
class TESTUNREALENGINE_API UMyStatComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	UMyStatComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;
	virtual void InitializeComponent() override;

public:
	void SetLevel(int32 Level);
	void OnAttacked(float DamageAmount);

	int32 GetLevel() { return Level; }
	int32 GetHp() { return Hp; }
	int32 GetAttack() { return Attack; }

private:
	UPROPERTY(EditAnywhere, Category=Stat, Meta=(AllowPrivateAccess=true))
	int32 Level;

	UPROPERTY(EditAnywhere, Category = Stat, Meta = (AllowPrivateAccess = true))
	int32 Hp;
	
	UPROPERTY(EditAnywhere, Category = Stat, Meta = (AllowPrivateAccess = true))
	int32 Attack;
};

// MyStatComponent.cpp
#include "MygameInstance.h"
#include "Kismet/GameplayStatics.h"

// Sets default values for this component's properties
UMyStatComponent::UMyStatComponent()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = false; // 틱이 따로 필요없음

	bWantsInitializeComponent = true; // InitializeComponent 함수 호출을 위해 true
	Level = 1;

	// ...
}


// Called when the game starts
void UMyStatComponent::BeginPlay()
{
	Super::BeginPlay();

	// ...
	
}

void UMyStatComponent::InitializeComponent()
{
	Super::InitializeComponent();

	SetLevel(Level);
}

void UMyStatComponent::SetLevel(int32 NewLevel)
{
	// GameInstance를 받아와서 안의 값을 가져옴
	auto MyGameInstance = Cast<UMyGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
	if (MyGameInstance)
	{
		auto StatData = MyGameInstance->GetStatData(NewLevel);
		if (StatData)
		{
			Level = StatData->Level;
			Hp = StatData->MaxHp;
			Attack = StatData->Attack;
		}
	}

}

void UMyStatComponent::OnAttacked(float DamageAmount)
{
	Hp -= DamageAmount;
	if (Hp < 0)
		Hp = 0;

	UE_LOG(LogTemp, Warning, TEXT("OnAttacked %d"), Hp);
}

// MyCharacter.h
	virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
	
    UPROPERTY(VisibleAnywhere)
    class UMyStatComponent* Stat;
    
// MyCharacter.cpp
#include "MyStatComponent.h"

AMyCharacter::AMyCharacter()
{
	// .. 생략
	Stat = CreateDefaultSubobject<UMyStatComponent>(TEXT("STAT"));
}

void AMyCharacter::AttackCheck()
{
	// .. 생략
    if (bResult && HitResult.Actor.IsValid())
    {
        FDamageEvent DamageEvent;
        UE_LOG(LogTemp, Log, TEXT("Hit Actor : %s"), *HitResult.Actor->GetName());
        // 데미지를 받는쪽의 TakeDamage를 호출한다
        HitResult.Actor->TakeDamage(Stat->GetAttack(), DamageEvent, GetController(), this);
    }
}

float AMyCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	Stat->OnAttacked(DamageAmount);

	return DamageAmount;
}

 

 

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] AI Controller  (0) 2022.09.11
[UE4 입문] UI 실습  (0) 2022.09.03
[UE4 입문] 아이템 줍기  (0) 2022.09.02
[UE4 입문] 소켓 실습  (0) 2022.09.02
[UE4 입문] 충돌 기초  (0) 2022.09.02

 

 

콜리전 채널과 프리셋 세팅을 우선 해준다.

 

// MyWeapon.h
protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	virtual void PostInitializeComponents() override;

private:
	UFUNCTION()
	void OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

public:
	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent* Weapon;

	UPROPERTY(VisibleAnywhere)
	class UBoxComponent* Trigger;
};

// MyWeapon.cpp
AMyWeapon::AMyWeapon()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = false; // 무기는 Tick이 필요없다

	Weapon = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WEAPON"));
	Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TRIGGER"));
	
	static ConstructorHelpers::FObjectFinder<UStaticMesh> SW(TEXT("StaticMesh'/Game/ParagonGreystone/FX/Meshes/Heroes/Greystone/SM_Greystone_Blade_01.SM_Greystone_Blade_01'"));
	if (SW.Succeeded())
	{
		Weapon->SetStaticMesh(SW.Object);
	}
	
	Weapon->SetupAttachment(RootComponent);
	Trigger->SetupAttachment(Weapon); // 안하면 트리거가 따라가지 않을 수 있음

	// 만들어둔 콜리전 프리셋 설정
	Weapon->SetCollisionProfileName(TEXT("MyCollectible"));
	Trigger->SetCollisionProfileName(TEXT("MyCollectible"));
	Trigger->SetBoxExtent(FVector(30.f, 30.f, 30.f)); // 충돌 박스 생성
}

void AMyWeapon::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	// 트리거 작동(충돌체크 발생)시 실행할 메소드를 콜백으로 넘겨줌
	Trigger->OnComponentBeginOverlap.AddDynamic(this, &AMyWeapon::OnCharacterOverlap);
}

void AMyWeapon::OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	UE_LOG(LogTemp, Log, TEXT("Overlapped"));

	AMyCharacter* MyCharacter = Cast<AMyCharacter>(OtherActor);

	if (MyCharacter)
	{
		FName WeaponSocket(TEXT("hand_l_socket"));

		// 충돌된 캐릭터(MyCharacter)의 WeaponSocket에 현재 컴포넌트를 붙여준다(MyWeapon)
		AttachToComponent(MyCharacter->GetMesh(),
			FAttachmentTransformRules::SnapToTargetNotIncludingScale,
			WeaponSocket);
	}
}

 

MyWeapon C++ 클래스를 맵에 몇개 배치해두고 위로 지나다니면 충돌 처리시 소켓에 붙게 된다.

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] UI 실습  (0) 2022.09.03
[UE4 입문] 스탯 매니저  (0) 2022.09.02
[UE4 입문] 소켓 실습  (0) 2022.09.02
[UE4 입문] 충돌 기초  (0) 2022.09.02
[UE4 입문] 블렌드 스페이스  (0) 2022.09.02

게임에 따라 맨손으로 시작했다가 무기를 교체하며 들고 있을 수 있다.

그것을 구현하는 실습이다.

 

우선은 만들어둔 소켓을 C++ 클래스에서 직접 메시를 붙여주는것부터 시작하자.

 

 

스켈레톤 트리에서 왼손에 소켓을 추가해준다.

해당 소켓에서 우클릭 시, 프리뷰 애셋이란것을 추가해 줄 수 있는데 실제로 적용되는것은 아니고 말 그대로 위치를 잡아주기 위해 프리뷰 애셋을 하나 가져와서 작업하기 편하게 하는 것이다. 소켓의 위치를 조정해주면 된다.

 

// MyCharacter.h
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* Weapon;

// MyCharacter.cpp
AMyCharacter::AMyCharacter()
{
	// .. 생략
    FName WeaponSocket(TEXT("hand_l_socket"));
    if (GetMesh()->DoesSocketExist(WeaponSocket)) // hand_l_socket이 있는가?
    {
        // 있으면 메시 컴포넌트 생성
        Weapon = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WEAPON"));
        // 메시 파일까지 불러들여서
        static ConstructorHelpers::FObjectFinder<UStaticMesh> SW(TEXT("StaticMesh'/Game/ParagonGreystone/FX/Meshes/Heroes/Greystone/SM_Greystone_Blade_01.SM_Greystone_Blade_01'"));
        // 로드에 성공하면
        if (SW.Succeeded())
        {
            // 스태틱 메시를 설정해주고
            Weapon->SetStaticMesh(SW.Object);
        }
        // 해당 메시를 소켓에 붙여준다
        Weapon->SetupAttachment(GetMesh(), WeaponSocket);
    }
}

 

 

무기도 하나의 액터로 만들어서 소켓에 붙이는 방식을 사용할 수 있다.

 

 

// MyWeapon.h
class TESTUNREALENGINE_API AMyWeapon : public AActor
{
public:
    UPROPERTY(VisibleAnywhere)
    UStaticMeshComponent* Weapon;
};

// MyWeapon.cpp
AMyWeapon::AMyWeapon()
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = false; // 무기는 Tick이 필요없다

    Weapon = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WEAPON"));

    static ConstructorHelpers::FObjectFinder<UStaticMesh> SW(TEXT("StaticMesh'/Game/ParagonGreystone/FX/Meshes/Heroes/Greystone/SM_Greystone_Blade_01.SM_Greystone_Blade_01'"));
    if (SW.Succeeded())
    {

        Weapon->SetStaticMesh(SW.Object);
    }

    // 충돌까지도 설정하고 싶다면 콜리전 프리셋 설정. 현재는 NoCollision으로 설정
    Weapon->SetCollisionProfileName(TEXT("NoCollision"));
}

// MyCharacter.cpp
void AMyCharacter::BeginPlay()
{
    Super::BeginPlay();

    FName WeaponSocket(TEXT("hand_l_socket"));

	// 유니티의 Instantiate와 비슷한 기능
    auto CurrentWeapon = GetWorld()->SpawnActor<AMyWeapon>(FVector::ZeroVector, FRotator::ZeroRotator);

    if (CurrentWeapon)
    {
        // 위의 WeaponSocket에다가 SpawnActor로 만든 CurrentWeapon을 붙여준다
        CurrentWeapon->AttachToComponent(GetMesh(),
            FAttachmentTransformRules::SnapToTargetNotIncludingScale,
            WeaponSocket);
    }
}

 

MyCharacter의 생성자에서 소켓에 붙여주던 코드는 삭제하고 MyWeapon의 생성자와 MyCharacer의 Begin으로 코드를 분산시켰다.

 

AttachToComponent 코드를 삭제한다면 소켓에 붙지 않고 맵 중앙에 칼이 스폰된 상태 그대로 있게된다.

 

GetWorld 메소드는 전역으로 굉장히 많은 기능들을 들고있다.

소켓은 언리얼과 유니티 모두에 존재하는 개념이다.

+ Recent posts