만들어둔 게임 인스턴스 클래스를 사용하려면 [세팅 - 프로젝트 세팅 - 맵&모드] 에서 게임 인스턴스 클래스를 바꿔주어야 한다.
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;
}
// 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 코드를 삭제한다면 소켓에 붙지 않고 맵 중앙에 칼이 스폰된 상태 그대로 있게된다.