그냥 게임개발자

[UE5] 6장 - 아이템 줍기, 스탯 매니저(C++) 본문

UE5_Tutorial1

[UE5] 6장 - 아이템 줍기, 스탯 매니저(C++)

sudoju 2023. 10. 1. 11:17

본 강의는 인프런 루키스 강의를 보고 작성한 글인점을 알려드립니다.

 

1. 아이템 줍기

2. 스탯 매니저

 

MyWeapon이라는 클래스를 만들어보자.

 

일단 MyWeapon에 필요한 함수는 PostInitializeComponents(), OnCharacterOverlap()

필요한 변수는 UStaticMeshComponent(Weapon), UBoxComponent(Trigger)이렇게 4가지다.

 

그럼 코드를 짜보자.

protected:
    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;

 

 

PostInitializeComponents()

언리얼 엔진도 유니티와 마찬가지로 실행되는 함수의 순서가 따로 있다.

일단 클래스를 생성하면 BeginPlay가 있는데 이것이 제일 먼저 실행될 것 같지만,

이와 같이 BeginPlay가 실행되기 전에 PostInitializeComponents가 먼저 실행이된다.

PostInitializeComponents는 모든 컴포넌트 구성요소가 초기화가 다 된 후에 실행되는 함수이다.

그렇기에 제일 먼저 실행된다고 볼 수 있다.

유니티에서는 Awake()문이라고 생각하면 편하다.

 

OnCharacterOverlap()

우리가 지금 이 클래스의 물건이 어떤 액터와 닿았는지 체크하기 위해 만든 커스텀 함수

 

UBoxComponent

단순 충돌에 사용되는 상자형태의 컴포넌트이다.

에디터에서 선으로 표시가 된다.

이제 우리는 Cpp파일에서 정의한 함수를 짜고 변수들을 이용할 것이다.

그전에 우리는 이 클래스에는 당장의 TickComponent가 필요가 없기 때문에 다 지워줄 것이며, bCanEverTick도 false로 바꾸어 줄 것이다.

 

또한 우리는 아이템을 주우면 어디에 장착할지 설정해주어야 하기 때문에, 캐릭터의 스켈레톤으로 가서 우리가 장착하려는 부분의 소켓을 생성해주자.

 

이 스켈레톤 메쉬에서 hand를 검색해보면

이렇게 왼손을 찾을 수 있게 된다.

만약 캐릭터 오른손을 찾고싶으면 hand_r을 찾으면 된다(파라곤 캐릭터 기준)

이 곳에서 소켓을 생성해주자.

이렇게 생성이 되는데 잘 적용이 되는지 아무 메쉬나 추가해보자.

이런 식으로 이상한 곳에 놓아질 텐데 잘 설정해줘서 빈손에 놓아주자.

 

그럼 저장한번 하고 이제 충돌체를 설정해줘야 한다.

ProjectSettings에 가서

Channel과 Trace를 설정해주자.

저번글에서는 Trace를 설정 안해주어서 이번에 하는 것을 보여주려고 한다.

MyCharacter충돌체, MyCollectible충돌체

MyCharacter와 MyCollectible은 서로 닿아서 이벤트가 생겨야 하기 때문에 Overlap으로 해놓는다.

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;

	// 필요한 컴포넌트를 생성화 해서 변수에 담는다.
	Weapon = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WEAPON"));
	Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TRIGGER"));


	// 필요한 메쉬를 불러와 Weapon에 적용시킨다.
	static ConstructorHelpers::FObjectFinder<UStaticMesh> SW(TEXT("/Script/Engine.StaticMesh'/Engine/EditorResources/FieldNodes/_Resources/SM_FieldArrow.SM_FieldArrow'"));
	if (SW.Succeeded())
	{
		Weapon->SetStaticMesh(SW.Object);
	}

	// Weapon은 이 클래스의 RootComponent의 자식으로
	// Trigger는 Weapon의 자식으로 설정해준다.
	Weapon->SetupAttachment(RootComponent);
	Trigger->SetupAttachment(Weapon);

	// Weapon과 Trigger의 충돌체 이름을 지어준 후
	// Trigger의 박스크기를 정해준다.
	Weapon->SetCollisionProfileName(TEXT("MyCollectible"));
	Trigger->SetCollisionProfileName(TEXT("MyCollectible"));
	Trigger->SetBoxExtent(FVector(30.f, 30.f, 30.f));
}

// Called when the game starts or when spawned
void AMyWeapon::BeginPlay()
{
	Super::BeginPlay();
	
}

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"));

	// OtherActor의 AMyCharater가 있으면 캐릭터가 주운것으로 판단
	AMyCharacter* MyCharacter = Cast<AMyCharacter>(OtherActor);
	if (MyCharacter)
	{
		FName WeaponSocket(TEXT("hand_l_socket"));

		AttachToComponent(MyCharacter->GetMesh(),
			FAttachmentTransformRules::SnapToTargetNotIncludingScale,
			WeaponSocket);
	}
}

 

이제 우리는 이 캐릭터의 장착을 하는 것 까지 완벽해졌다.

 

이제 이 액터를 뷰포트에 아무데나 배치해놓고 장착해보자.

잘 됐다.

이제 우리는 공격을 했을 때 데미지를 주는 코드를 짜볼 것이다.

이런 식으로 말이다.

우리가 저번에 공격은 만들었지만 데미지를 주는 코드는 만들지 못했다.

이제 만들 것이다.

 

일단 스탯을 만들어보자.

우리는 스탯관리를 GameInstance라는 클래스를 생성해서 관리할 것이다.

MyCharacter클래스에 스탯을 관리해도 되지만 나중에 게임이 방대하게 커지면 데이터를 관리하기가 힘들 것이다.

물론 JSON, XML 파일로도 관리할 수 있지만 일단 이번에는 언리얼 엔진에서 제공하는 데이터 테이블을 사용할 것이고 GameInstance라는 것은 유니티로 치자면 Manager와 같다고 보면된다.

 

그럼 우리는 GameInstance.h파일에 코드를 짜보자.

 

MyGameInstance.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "Engine/DataTable.h"
#include "MyGameInstance.generated.h"

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

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 Level;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 Attack;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 MaxHp;
};

/**
 * 
 */
UCLASS()
class FIRSTPROJECT_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:
	UMyGameInstance();

	virtual void Init() override;

	FMyCharacterData* GetStatData(int32 Level);

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

이런식으로 스탯을 짜보는 거다.

되게 간단하다.

이제 빌드하고 데이터 테이블을 만들어보자.

이런식으로 우리가 위에서 쓴 MyCharacterData Struct가 있다.

이를 선택해서 만들자.

대충 이렇게 데이터를 2개 행 정도 넣어놓고 저장한 다음 MyGameInstance.cpp 파일에서 불러오자.

 

MyGameInstacne.cpp

UMyGameInstance::UMyGameInstance()
{
	// 데이터 테이블 불러오기
	static ConstructorHelpers::FObjectFinder<UDataTable>DATA(TEXT("/Script/Engine.DataTable'/Game/Data/StatTable.StatTable'"));
	if (DATA.Succeeded())
	{
		// 데이터 테이블 적용
		MyStats = DATA.Object;
	}
}

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

	// 제대로 불러와지는지 확인
	UE_LOG(LogTemp, Warning, TEXT("MyGameInstance %d"), GetStatData(1)->Attack);
}

FMyCharacterData* UMyGameInstance::GetStatData(int32 Level)
{
	// FindRow를 통해 Level에 맞는 데이터를 불러옴
	return MyStats->FindRow<FMyCharacterData>(*FString::FromInt(Level), TEXT(""));
}

자 이제 빌드해서 실행해보자.

로그가 잘찍힌다.

 

이제 우리가 이 데이터를 사용하기 위해 ActorComponent클래스를 만들어서 사용해보자.

 

 

MyStatComoponent.h

class FIRSTPROJECT_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 NewLevel);
	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 "FirstProject/Actor/MyStatComponent.h"
#include "FirstProject/Instacne/MyGameInstance.h"
#include "Kismet/GameplayStatics.h"			// 싱글톤을 사용 할 수 있는 헤더


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;

	// ...

	// InitializeComponent를 실행하려면 아래를 true로 바꿔줘야 한다.
	bWantsInitializeComponent = true;

	Level = 1;
}



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

	// ...
	
}

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

	// 레벨을 설정해준다.
	SetLevel(Level);
}

void UMyStatComponent::SetLevel(int32 NewLevel)
{
	// GetGameInstance 싱글톤처럼 불러올 수 있는 함수
	// MyGameInstance를 캐스팅해온다.
	auto MyGameInstance = Cast<UMyGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
	if (MyGameInstance)
	{
		// 레벨에 맞는 스탯 데이터를 가져온다.
		auto StatData = MyGameInstance->GetStatData(Level);
		if (StatData)
		{
			// 스탯 데이터가 있다면 정해준다.
			// 이제 이 스탯으로 플레이어가 사용할 것.
			Level = StatData->Level;
			Hp = StatData->MaxHp;
			Attack = StatData->Attack;
		}
	}
}

void UMyStatComponent::OnAttacked(float DamageAmount)
{
	// 데미지가 들어오는데로 Hp를 깎아준다.
	Hp -= DamageAmount;
	if (Hp < 0)
		Hp = 0;

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

 

이제 이 데이터를 통해 MyCharacter에 적용하자.

 

MyCharacter.h

public:

	virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;

public:

	UPROPERTY(VisibleAnywhere)
	class UMyStatComponent* Stat;

TakeDamage라는 함수는 이미 언리얼 엔진에서 만들어져 있는 함수라서 우리가 커스텀 하기 위해 ovrride 함수로 사용할 것이다.

또한 Stat을 가져온다.

 

MyCharacter.cpp

AMyCharacter::AMyCharacter()
{
	// 스탯의 인스턴스를 가져와 넣어준다.
	Stat = CreateDefaultSubobject<UMyStatComponent>(TEXT("STAT"));
}

void AMyCharacter::AttackCheck()
{
	.
    .
    .
    
    
	if (bResult && HitResult.GetActor())
	{
		UE_LOG(LogTemp, Log, TEXT("Hit Actor : %s"), *HitResult.GetActor()->GetName());

		// 데미지 넣어주는 부분
        // 이 때 데미지는 Stat->GetAttack()을 가져온다.
		FDamageEvent DamageEvent;
		HitResult.GetActor()->TakeDamage(Stat->GetAttack(), DamageEvent, GetController(), this);
	}
}

// 데미지 주는 함수
float AMyCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	// 스탯의 OnAttacked 함수를 실행시킨다 
    // 이 때 데미지는 아까 받아온 Stat의 GetAttack() 함수에서 가져온다.
	Stat->OnAttacked(DamageAmount);

	return DamageAmount;
}

이제 공격해서 사용해보자.