데이터 기반의 게임 시스템 구축
csv파일을 언리얼에서 Import할 수 있음.
동일한 이름의 속성을 가진 구조체가 필요.
GameData 폴더를 만들고 ABCharacterStat.h 이라는 헤더파일(구조체) 생성
엑셀 데이터의 임포트
DataAsset과 유사하게 FTableRowBase를 상속받은 구조체를 선언
엑셀의 Name 칼럼을 제외한 칼럼과 동일하게 UPROPERTY 속성을 선언
엑셀 데이터를 csv로 익스포트한 후 언리얼 엔진에 임포트
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "ABCharacterStat.generated.h"
USTRUCT(BlueprintType)
struct FABCharacterStat : public FTableRowBase
{
GENERATED_BODY()
public:
FABCharacterStat() : MaxHp(0.0f), Attack(0.0f), AttackRange(0.0f), AttackSpeed(0.0f), MovementSpeed(0.0f) {}
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
float MaxHp;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
float Attack;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
float AttackRange;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
float AttackSpeed;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
float MovementSpeed;
FABCharacterStat operator+(const FABCharacterStat& Other) const
{
const float* const ThisPtr = reinterpret_cast<const float* const>(this);
const float* const OtherPtr = reinterpret_cast<const float* const>(&Other);
FABCharacterStat Result;
float* ResultPtr = reinterpret_cast<float*>(&Result);
int32 StatNum = sizeof(FABCharacterStat) / sizeof(float);
for (int32 i = 0; i < StatNum; i++)
{
ResultPtr[i] = ThisPtr[i] + OtherPtr[i];
}
return Result;
}
};
ABCharacterStat.h
DataTable를 생성하고 엑셀 파일을 선택해 열면 csv파일의 내용이 등록됨
데이터를 관리할 싱글톤 클래스의 설정
언리얼 엔진에서 제공하는 싱글톤 클래스
- 게임 인스턴스
- 애셋 매니저
- 게임 플레이 관련 액터 (게임 모드, 게임 스테이트)
- 프로젝트에서 싱글톤으로 등록한 언리얼 오브젝트
언리얼 오브젝트 생성자에서 사용하지 않도록 주의
GameObject를 상속받은 ABGameSingleton 클래스 생성
설정에서 싱글톤 클래스를 등록해주고 에디터 재시작
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "ABCharacterStat.h"
#include "ABGameSingleton.generated.h"
DECLARE_LOG_CATEGORY_EXTERN(LogABGameSingleton, Error, All);
/**
*
*/
UCLASS()
class ARENABATTLE_API UABGameSingleton : public UObject
{
GENERATED_BODY()
public:
UABGameSingleton();
static UABGameSingleton& Get();
// Character Stat Data Section
public:
FORCEINLINE FABCharacterStat GetCharacterStat(int32 InLevel) const { return CharacterStatTable.IsValidIndex(InLevel - 1) ? CharacterStatTable[InLevel - 1] : FABCharacterStat(); }
UPROPERTY()
int32 CharacterMaxLevel;
private:
TArray<FABCharacterStat> CharacterStatTable;
};
ABGameSingleton.h
// Fill out your copyright notice in the Description page of Project Settings.
#include "GameData/ABGameSingleton.h"
DEFINE_LOG_CATEGORY(LogABGameSingleton);
UABGameSingleton::UABGameSingleton()
{
static ConstructorHelpers::FObjectFinder<UDataTable> DataTableRef(TEXT("/Script/Engine.DataTable'/Game/ArenaBattle/GameData/ABCharacterStatTable.ABCharacterStatTable'"));
if (nullptr != DataTableRef.Object)
{
const UDataTable* DataTable = DataTableRef.Object;
check(DataTable->GetRowMap().Num() > 0);
TArray<uint8*> ValueArray;
DataTable->GetRowMap().GenerateValueArray(ValueArray);
Algo::Transform(ValueArray, CharacterStatTable,
[](uint8* Value)
{
return *reinterpret_cast<FABCharacterStat*>(Value);
}
);
}
CharacterMaxLevel = CharacterStatTable.Num();
ensure(CharacterMaxLevel > 0);
}
UABGameSingleton& UABGameSingleton::Get()
{
UABGameSingleton* Singleton = CastChecked< UABGameSingleton>(GEngine->GameSingleton);
if (Singleton)
{
return *Singleton;
}
UE_LOG(LogABGameSingleton, Error, TEXT("Invalid Game Singleton"));
return *NewObject<UABGameSingleton>();
}
ABGameSingleton.cpp
프로젝트의 주요 레이어
캐릭터 스탯 시스템
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "GameData/ABCharacterStat.h"
#include "ABCharacterStatComponent.generated.h"
DECLARE_MULTICAST_DELEGATE(FOnHpZeroDelegate);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnHpChangedDelegate, float /*CurrentHp*/);
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ARENABATTLE_API UABCharacterStatComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UABCharacterStatComponent();
protected:
// Called when the game starts
virtual void BeginPlay() override;
public:
FOnHpZeroDelegate OnHpZero;
FOnHpChangedDelegate OnHpChanged;
void SetLevelStat(int32 InNewLevel);
FORCEINLINE float GetCurrentLevel() const { return CurrentLevel; }
FORCEINLINE void SetModifierStat(const FABCharacterStat& InModifierStat) { ModifierStat = InModifierStat; }
FORCEINLINE FABCharacterStat GetTotalStat() const { return BaseStat + ModifierStat; }
FORCEINLINE float GetCurrentHp() const { return CurrentHp; }
float ApplyDamage(float InDamage);
protected:
void SetHp(float NewHp);
UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat)
float CurrentHp;
UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat)
float CurrentLevel;
UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
FABCharacterStat BaseStat;
UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
FABCharacterStat ModifierStat;
};
ABCharacterStatComponent.h
// Fill out your copyright notice in the Description page of Project Settings.
#include "CharacterStat/ABCharacterStatComponent.h"
#include "GameData/ABGameSingleton.h"
// Sets default values for this component's properties
UABCharacterStatComponent::UABCharacterStatComponent()
{
CurrentLevel = 1;
}
// Called when the game starts
void UABCharacterStatComponent::BeginPlay()
{
Super::BeginPlay();
SetLevelStat(CurrentLevel);
SetHp(BaseStat.MaxHp);
}
void UABCharacterStatComponent::SetLevelStat(int32 InNewLevel)
{
CurrentLevel = FMath::Clamp(InNewLevel, 1, UABGameSingleton::Get().CharacterMaxLevel);
BaseStat = UABGameSingleton::Get().GetCharacterStat(CurrentLevel);
check(BaseStat.MaxHp > 0.0f);
}
float UABCharacterStatComponent::ApplyDamage(float InDamage)
{
const float PrevHp = CurrentHp;
const float ActualDamage = FMath::Clamp<float>(InDamage, 0, InDamage);
SetHp(PrevHp - ActualDamage);
if (CurrentHp <= KINDA_SMALL_NUMBER)
{
OnHpZero.Broadcast();
}
return ActualDamage;
}
void UABCharacterStatComponent::SetHp(float NewHp)
{
CurrentHp = FMath::Clamp<float>(NewHp, 0.0f, BaseStat.MaxHp);
OnHpChanged.Broadcast(CurrentHp);
}
ABCharacterStatComponent.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ABStageGimmick.generated.h"
DECLARE_DELEGATE(FOnStageChangedDelegate);
USTRUCT(BlueprintType)
struct FStageChangedDelegateWrapper
{
GENERATED_BODY()
FStageChangedDelegateWrapper() { }
FStageChangedDelegateWrapper(const FOnStageChangedDelegate& InDelegate) : StageDelegate(InDelegate) {}
FOnStageChangedDelegate StageDelegate;
};
UENUM(BlueprintType)
enum class EStageState : uint8
{
READY = 0,
FIGHT,
REWARD,
NEXT
};
UCLASS()
class ARENABATTLE_API AABStageGimmick : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AABStageGimmick();
public:
FORCEINLINE int32 GetStageNum() const { return CurrentStageNum; }
FORCEINLINE void SetStageNum(int32 NewStageNum) { CurrentStageNum = NewStageNum; }
protected:
virtual void OnConstruction(const FTransform& Transform) override;
// Stage Section
protected:
UPROPERTY(VisibleAnywhere, Category = Stage, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UStaticMeshComponent> Stage;
UPROPERTY(VisibleAnywhere, Category = Stage, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UBoxComponent> StageTrigger;
UFUNCTION()
void OnStageTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
// Gate Section
protected:
UPROPERTY(VisibleAnywhere, Category = Gate, Meta = (AllowPrivateAccess = "true"))
TMap<FName, TObjectPtr<class UStaticMeshComponent>> Gates;
UPROPERTY(VisibleAnywhere, Category = Gate, Meta = (AllowPrivateAccess = "true"))
TArray<TObjectPtr<class UBoxComponent>> GateTriggers;
UFUNCTION()
void OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
void OpenAllGates();
void CloseAllGates();
// State Section
protected:
UPROPERTY(EditAnywhere, Category = Stage, Meta = (AllowPrivateAccess = "true"))
EStageState CurrentState;
void SetState(EStageState InNewState);
UPROPERTY()
TMap<EStageState, FStageChangedDelegateWrapper> StateChangeActions;
void SetReady();
void SetFight();
void SetChooseReward();
void SetChooseNext();
// Fight Section
protected:
UPROPERTY(EditAnywhere, Category = Fight, Meta = (AllowPrivateAccess = "true"))
TSubclassOf<class AABCharacterNonPlayer> OpponentClass;
UPROPERTY(EditAnywhere, Category = Fight, Meta = (AllowPrivateAccess = "true"))
float OpponentSpawnTime;
UFUNCTION()
void OnOpponentDestroyed(AActor* DestroyedActor);
FTimerHandle OpponentTimerHandle;
void OnOpponentSpawn();
// Reward Section
protected:
UPROPERTY(VisibleAnywhere, Category = Reward, Meta = (AllowPrivateAccess = "true"))
TSubclassOf<class AABItemBox> RewardBoxClass;
UPROPERTY(VisibleAnywhere, Category = Reward, Meta = (AllowPrivateAccess = "true"))
TArray<TWeakObjectPtr<class AABItemBox>> RewardBoxes;
TMap<FName, FVector> RewardBoxLocations;
UFUNCTION()
void OnRewardTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
void SpawnRewardBoxes();
// Stage Stat
protected:
UPROPERTY(VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
int32 CurrentStageNum;
};
ABStageGimmick.h
// Fill out your copyright notice in the Description page of Project Settings.
#include "Gimmick/ABStageGimmick.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
#include "Physics/ABCollision.h"
#include "Character/ABCharacterNonPlayer.h"
#include "Item/ABItemBox.h"
#include "Engine/OverlapResult.h"
// Sets default values
AABStageGimmick::AABStageGimmick()
{
// Stage Section
Stage = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Stage"));
RootComponent = Stage;
static ConstructorHelpers::FObjectFinder<UStaticMesh> StageMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Stages/SM_SQUARE.SM_SQUARE'"));
if (StageMeshRef.Object)
{
Stage->SetStaticMesh(StageMeshRef.Object);
}
StageTrigger = CreateDefaultSubobject<UBoxComponent>(TEXT("StageTrigger"));
StageTrigger->SetBoxExtent(FVector(775.0, 775.0f, 300.0f));
StageTrigger->SetupAttachment(Stage);
StageTrigger->SetRelativeLocation(FVector(0.0f, 0.0f, 250.0f));
StageTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
StageTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABStageGimmick::OnStageTriggerBeginOverlap);
// Gate Section
static FName GateSockets[] = { TEXT("+XGate"), TEXT("-XGate"), TEXT("+YGate"), TEXT("-YGate") };
static ConstructorHelpers::FObjectFinder<UStaticMesh> GateMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_GATE.SM_GATE'"));
for (FName GateSocket : GateSockets)
{
UStaticMeshComponent* Gate = CreateDefaultSubobject<UStaticMeshComponent>(GateSocket);
Gate->SetStaticMesh(GateMeshRef.Object);
Gate->SetupAttachment(Stage, GateSocket);
Gate->SetRelativeLocation(FVector(0.0f, -80.5f, 0.0f));
Gate->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
Gates.Add(GateSocket, Gate);
FName TriggerName = *GateSocket.ToString().Append(TEXT("Trigger"));
UBoxComponent* GateTrigger = CreateDefaultSubobject<UBoxComponent>(TriggerName);
GateTrigger->SetBoxExtent(FVector(100.0f, 100.0f, 300.0f));
GateTrigger->SetupAttachment(Stage, GateSocket);
GateTrigger->SetRelativeLocation(FVector(70.0f, 0.0f, 250.0f));
GateTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
GateTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABStageGimmick::OnGateTriggerBeginOverlap);
GateTrigger->ComponentTags.Add(GateSocket);
GateTriggers.Add(GateTrigger);
}
// State Section
CurrentState = EStageState::READY;
StateChangeActions.Add(EStageState::READY, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetReady)));
StateChangeActions.Add(EStageState::FIGHT, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetFight)));
StateChangeActions.Add(EStageState::REWARD, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetChooseReward)));
StateChangeActions.Add(EStageState::NEXT, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetChooseNext)));
// Fight Section
OpponentSpawnTime = 2.0f;
OpponentClass = AABCharacterNonPlayer::StaticClass();
// Reward Section
RewardBoxClass = AABItemBox::StaticClass();
for (FName GateSocket : GateSockets)
{
FVector BoxLocation = Stage->GetSocketLocation(GateSocket) / 2;
RewardBoxLocations.Add(GateSocket, BoxLocation);
}
// Stage Stat
CurrentStageNum = 0;
}
void AABStageGimmick::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
SetState(CurrentState);
}
void AABStageGimmick::OnStageTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
SetState(EStageState::FIGHT);
}
void AABStageGimmick::OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
check(OverlappedComponent->ComponentTags.Num() == 1);
FName ComponentTag = OverlappedComponent->ComponentTags[0];
FName SocketName = FName(*ComponentTag.ToString().Left(2));
check(Stage->DoesSocketExist(SocketName));
FVector NewLocation = Stage->GetSocketLocation(SocketName);
TArray<FOverlapResult> OverlapResults;
FCollisionQueryParams CollisionQueryParam(SCENE_QUERY_STAT(GateTrigger), false, this);
bool bResult = GetWorld()->OverlapMultiByObjectType(
OverlapResults,
NewLocation,
FQuat::Identity,
FCollisionObjectQueryParams::InitType::AllObjects,
FCollisionShape::MakeSphere(775.0f),
CollisionQueryParam
);
if (!bResult)
{
FTransform NewTransform(NewLocation);
AABStageGimmick* NewGimmick = GetWorld()->SpawnActorDeferred<AABStageGimmick>(AABStageGimmick::StaticClass(), NewTransform);
if (NewGimmick)
{
NewGimmick->SetStageNum(CurrentStageNum + 1);
NewGimmick->FinishSpawning(NewTransform);
}
}
}
void AABStageGimmick::OpenAllGates()
{
for (auto Gate : Gates)
{
(Gate.Value)->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
}
}
void AABStageGimmick::CloseAllGates()
{
for (auto Gate : Gates)
{
(Gate.Value)->SetRelativeRotation(FRotator::ZeroRotator);
}
}
void AABStageGimmick::SetState(EStageState InNewState)
{
CurrentState = InNewState;
if (StateChangeActions.Contains(InNewState))
{
StateChangeActions[CurrentState].StageDelegate.ExecuteIfBound();
}
}
void AABStageGimmick::SetReady()
{
StageTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
for (auto GateTrigger : GateTriggers)
{
GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
}
OpenAllGates();
}
void AABStageGimmick::SetFight()
{
StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
for (auto GateTrigger : GateTriggers)
{
GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
}
CloseAllGates();
GetWorld()->GetTimerManager().SetTimer(OpponentTimerHandle, this, &AABStageGimmick::OnOpponentSpawn, OpponentSpawnTime, false);
}
void AABStageGimmick::SetChooseReward()
{
StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
for (auto GateTrigger : GateTriggers)
{
GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
}
CloseAllGates();
SpawnRewardBoxes();
}
void AABStageGimmick::SetChooseNext()
{
StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
for (auto GateTrigger : GateTriggers)
{
GateTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
}
OpenAllGates();
}
void AABStageGimmick::OnOpponentDestroyed(AActor* DestroyedActor)
{
SetState(EStageState::REWARD);
}
void AABStageGimmick::OnOpponentSpawn()
{
const FTransform SpawnTransform(GetActorLocation() + FVector::UpVector * 88.0f);
AABCharacterNonPlayer* ABOpponentCharacter = GetWorld()->SpawnActorDeferred<AABCharacterNonPlayer>(OpponentClass, SpawnTransform);
if (ABOpponentCharacter)
{
ABOpponentCharacter->OnDestroyed.AddDynamic(this, &AABStageGimmick::OnOpponentDestroyed);
ABOpponentCharacter->SetLevel(CurrentStageNum);
ABOpponentCharacter->FinishSpawning(SpawnTransform);
}
}
void AABStageGimmick::OnRewardTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
for (const auto& RewardBox : RewardBoxes)
{
if (RewardBox.IsValid())
{
AABItemBox* ValidItemBox = RewardBox.Get();
AActor* OverlappedBox = OverlappedComponent->GetOwner();
if (OverlappedBox != ValidItemBox)
{
ValidItemBox->Destroy();
}
}
}
SetState(EStageState::NEXT);
}
void AABStageGimmick::SpawnRewardBoxes()
{
for (const auto& RewardBoxLocation : RewardBoxLocations)
{
FTransform SpawnTransform(GetActorLocation() + RewardBoxLocation.Value + FVector(0.0f, 0.0f, 30.0f));
AABItemBox* RewardBoxActor = GetWorld()->SpawnActorDeferred<AABItemBox>(RewardBoxClass, SpawnTransform);
if (RewardBoxActor)
{
RewardBoxActor->Tags.Add(RewardBoxLocation.Key);
RewardBoxActor->GetTrigger()->OnComponentBeginOverlap.AddDynamic(this, &AABStageGimmick::OnRewardTriggerBeginOverlap);
RewardBoxes.Add(RewardBoxActor);
}
}
for (const auto& RewardBox : RewardBoxes)
{
if (RewardBox.IsValid())
{
RewardBox.Get()->FinishSpawning(RewardBox.Get()->GetActorTransform());
}
}
}
ABStageGimmick.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Interface/ABAnimationAttackInterface.h"
#include "Interface/ABCharacterWidgetInterface.h"
#include "Interface/ABCharacterItemInterface.h"
#include "ABCharacterBase.generated.h"
DECLARE_LOG_CATEGORY_EXTERN(LogABCharacter, Log, All);
UENUM()
enum class ECharacterControlType : uint8
{
Shoulder,
Quater
};
DECLARE_DELEGATE_OneParam(FOnTakeItemDelegate, class UABItemData* /*InItemData*/);
USTRUCT(BlueprintType)
struct FTakeItemDelegateWrapper
{
GENERATED_BODY()
FTakeItemDelegateWrapper() {}
FTakeItemDelegateWrapper(const FOnTakeItemDelegate& InItemDelegate) : ItemDelegate(InItemDelegate) {}
FOnTakeItemDelegate ItemDelegate;
};
UCLASS()
class ARENABATTLE_API AABCharacterBase : public ACharacter, public IABAnimationAttackInterface, public IABCharacterWidgetInterface, public IABCharacterItemInterface
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
AABCharacterBase();
...
// Stat Section
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UABCharacterStatComponent> Stat;
// UI Widget Section
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Widget, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UWidgetComponent> HpBar;
virtual void SetupCharacterWidget(class UABUserWidget* InUserWidget) override;
// Item Section
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Equipment, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class USkeletalMeshComponent> Weapon;
UPROPERTY()
TArray<FTakeItemDelegateWrapper> TakeItemActions;
virtual void TakeItem(class UABItemData* InItemData) override;
virtual void DrinkPotion(class UABItemData* InItemData);
virtual void EquipWeapon(class UABItemData* InItemData);
virtual void ReadScroll(class UABItemData* InItemData);
// Stat Section
public:
int32 GetLevel();
void SetLevel(int32 InNewLevel);
};
ABCharacterBase.h
// Fill out your copyright notice in the Description page of Project Settings.
#include "Character/ABCharacterBase.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "ABCharacterControlData.h"
#include "Animation/AnimMontage.h"
#include "ABComboActionData.h"
#include "Physics/ABCollision.h"
#include "Engine/DamageEvents.h"
#include "CharacterStat/ABCharacterStatComponent.h"
#include "UI/ABWidgetComponent.h"
#include "UI/ABHpBarWidget.h"
#include "Item/ABWeaponItemData.h"
DEFINE_LOG_CATEGORY(LogABCharacter);
// Sets default values
AABCharacterBase::AABCharacterBase()
{
// Pawn
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
// Capsule
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
GetCapsuleComponent()->SetCollisionProfileName(CPROFILE_ABCAPSULE);
// Movement
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->MaxWalkSpeed = 500.f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
// Mesh
GetMesh()->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -100.0f), FRotator(0.0f, -90.0f, 0.0f));
GetMesh()->SetAnimationMode(EAnimationMode::AnimationBlueprint);
GetMesh()->SetCollisionProfileName(TEXT("NoCollision"));
static ConstructorHelpers::FObjectFinder<USkeletalMesh> CharacterMeshRef(TEXT("/Script/Engine.SkeletalMesh'/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Cardboard.SK_CharM_Cardboard'"));
if (CharacterMeshRef.Object)
{
GetMesh()->SetSkeletalMesh(CharacterMeshRef.Object);
}
static ConstructorHelpers::FClassFinder<UAnimInstance> AnimInstanceClassRef(TEXT("/Game/ArenaBattle/Animation/ABP_ABCharacter.ABP_ABCharacter_C"));
if (AnimInstanceClassRef.Class)
{
GetMesh()->SetAnimInstanceClass(AnimInstanceClassRef.Class);
}
static ConstructorHelpers::FObjectFinder<UABCharacterControlData> ShoulderDataRef(TEXT("/Script/ArenaBattle.ABCharacterControlData'/Game/ArenaBattle/CharacterControl/ABC_Shoulder.ABC_Shoulder'"));
if (ShoulderDataRef.Object)
{
CharacterControlManager.Add(ECharacterControlType::Shoulder, ShoulderDataRef.Object);
}
static ConstructorHelpers::FObjectFinder<UABCharacterControlData> QuaterDataRef(TEXT("/Script/ArenaBattle.ABCharacterControlData'/Game/ArenaBattle/CharacterControl/ABC_Quater.ABC_Quater'"));
if (QuaterDataRef.Object)
{
CharacterControlManager.Add(ECharacterControlType::Quater, QuaterDataRef.Object);
}
static ConstructorHelpers::FObjectFinder<UAnimMontage> ComboActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattle/Animation/AM_ComboAttack.AM_ComboAttack'"));
if (ComboActionMontageRef.Object)
{
ComboActionMontage = ComboActionMontageRef.Object;
}
static ConstructorHelpers::FObjectFinder<UABComboActionData> ComboActionDataRef(TEXT("/Script/ArenaBattle.ABComboActionData'/Game/ArenaBattle/CharacterAction/ABA_ComboAttack.ABA_ComboAttack'"));
if (ComboActionDataRef.Object)
{
ComboActionData = ComboActionDataRef.Object;
}
static ConstructorHelpers::FObjectFinder<UAnimMontage> DeadMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattle/Animation/AM_Dead.AM_Dead'"));
if (DeadMontageRef.Object)
{
DeadMontage = DeadMontageRef.Object;
}
// Stat Component
Stat = CreateDefaultSubobject<UABCharacterStatComponent>(TEXT("Stat"));
// Widget Component
HpBar = CreateDefaultSubobject<UABWidgetComponent>(TEXT("Widget"));
HpBar->SetupAttachment(GetMesh());
HpBar->SetRelativeLocation(FVector(0.0f, 0.0f, 180.0f));
static ConstructorHelpers::FClassFinder<UUserWidget> HpBarWidgetRef(TEXT("/Game/ArenaBattle/UI/WBP_HpBar.WBP_HpBar_C"));
if (HpBarWidgetRef.Class)
{
HpBar->SetWidgetClass(HpBarWidgetRef.Class);
HpBar->SetWidgetSpace(EWidgetSpace::Screen);
HpBar->SetDrawSize(FVector2D(150.0f, 15.0f));
HpBar->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
// Item Actions
TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::EquipWeapon)));
TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::DrinkPotion)));
TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::ReadScroll)));
// Weapon Component
Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Weapon"));
Weapon->SetupAttachment(GetMesh(), TEXT("hand_rSocket"));
}
void AABCharacterBase::PostInitializeComponents()
{
Super::PostInitializeComponents();
Stat->OnHpZero.AddUObject(this, &AABCharacterBase::SetDead);
}
void AABCharacterBase::SetCharacterControlData(const UABCharacterControlData* CharacterControlData)
{
// Pawn
bUseControllerRotationYaw = CharacterControlData->bUseControllerRotationYaw;
// CharacterMovement
GetCharacterMovement()->bOrientRotationToMovement = CharacterControlData->bOrientRotationToMovement;
GetCharacterMovement()->bUseControllerDesiredRotation = CharacterControlData->bUseControllerDesiredRotation;
GetCharacterMovement()->RotationRate = CharacterControlData->RotationRate;
}
void AABCharacterBase::ProcessComboCommand()
{
if (CurrentCombo == 0)
{
ComboActionBegin();
return;
}
if (!ComboTimerHandle.IsValid())
{
HasNextComboCommand = false;
}
else
{
HasNextComboCommand = true;
}
}
void AABCharacterBase::ComboActionBegin()
{
// Combo Status
CurrentCombo = 1;
// Movement Setting
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
// Animation Setting
const float AttackSpeedRate = Stat->GetTotalStat().AttackSpeed;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->Montage_Play(ComboActionMontage, AttackSpeedRate);
FOnMontageEnded EndDelegate;
EndDelegate.BindUObject(this, &AABCharacterBase::ComboActionEnd);
AnimInstance->Montage_SetEndDelegate(EndDelegate, ComboActionMontage);
ComboTimerHandle.Invalidate();
SetComboCheckTimer();
}
void AABCharacterBase::ComboActionEnd(UAnimMontage* TargetMontage, bool IsProperlyEnded)
{
ensure(CurrentCombo != 0);
CurrentCombo = 0;
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
}
void AABCharacterBase::SetComboCheckTimer()
{
int32 ComboIndex = CurrentCombo - 1;
ensure(ComboActionData->EffectiveFrameCount.IsValidIndex(ComboIndex));
const float AttackSpeedRate = Stat->GetTotalStat().AttackSpeed;
float ComboEffectiveTime = (ComboActionData->EffectiveFrameCount[ComboIndex] / ComboActionData->FrameRate) / AttackSpeedRate;
if (ComboEffectiveTime > 0.0f)
{
GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle, this, &AABCharacterBase::ComboCheck, ComboEffectiveTime, false);
}
}
void AABCharacterBase::ComboCheck()
{
ComboTimerHandle.Invalidate();
if (HasNextComboCommand)
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
CurrentCombo = FMath::Clamp(CurrentCombo + 1, 1, ComboActionData->MaxComboCount);
FName NextSection = *FString::Printf(TEXT("%s%d"), *ComboActionData->MontageSectionNamePrefix, CurrentCombo);
AnimInstance->Montage_JumpToSection(NextSection, ComboActionMontage);
SetComboCheckTimer();
HasNextComboCommand = false;
}
}
void AABCharacterBase::AttackHitCheck()
{
FHitResult OutHitResult;
FCollisionQueryParams Params(SCENE_QUERY_STAT(Attack), false, this);
const float AttackRange = Stat->GetTotalStat().AttackRange;
const float AttackRadius = 50.0f;
const float AttackDamage = Stat->GetTotalStat().Attack;
const FVector Start = GetActorLocation() + GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius();
const FVector End = Start + GetActorForwardVector() * AttackRange;
bool HitDetected = GetWorld()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
if (HitDetected)
{
FDamageEvent DamageEvent;
OutHitResult.GetActor()->TakeDamage(AttackDamage, DamageEvent, GetController(), this);
}
#if ENABLE_DRAW_DEBUG
FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
float CapsuleHalfHeight = AttackRange * 0.5f;
FColor DrawColor = HitDetected ? FColor::Green : FColor::Red;
DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, AttackRadius, FRotationMatrix::MakeFromZ(GetActorForwardVector()).ToQuat(), DrawColor, false, 5.0f);
#endif
}
float AABCharacterBase::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
Stat->ApplyDamage(DamageAmount);
return DamageAmount;
}
void AABCharacterBase::SetDead()
{
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
PlayDeadAnimation();
SetActorEnableCollision(false);
HpBar->SetHiddenInGame(true);
}
void AABCharacterBase::PlayDeadAnimation()
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->StopAllMontages(0.0f);
AnimInstance->Montage_Play(DeadMontage, 1.0f);
}
void AABCharacterBase::SetupCharacterWidget(UABUserWidget* InUserWidget)
{
UABHpBarWidget* HpBarWidget = Cast<UABHpBarWidget>(InUserWidget);
if (HpBarWidget)
{
HpBarWidget->SetMaxHp(Stat->GetTotalStat().MaxHp);
HpBarWidget->UpdateHpBar(Stat->GetCurrentHp());
Stat->OnHpChanged.AddUObject(HpBarWidget, &UABHpBarWidget::UpdateHpBar);
}
}
void AABCharacterBase::TakeItem(UABItemData* InItemData)
{
if (InItemData)
{
TakeItemActions[(uint8)InItemData->Type].ItemDelegate.ExecuteIfBound(InItemData);
}
}
void AABCharacterBase::DrinkPotion(UABItemData* InItemData)
{
UE_LOG(LogABCharacter, Log, TEXT("Drink Potion"));
}
void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
UABWeaponItemData* WeaponItemData = Cast<UABWeaponItemData>(InItemData);
if (WeaponItemData)
{
if (WeaponItemData->WeaponMesh.IsPending())
{
WeaponItemData->WeaponMesh.LoadSynchronous();
}
Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh.Get());
Stat->SetModifierStat(WeaponItemData->ModifierStat);
}
}
void AABCharacterBase::ReadScroll(UABItemData* InItemData)
{
UE_LOG(LogABCharacter, Log, TEXT("Read Scroll"));
}
int32 AABCharacterBase::GetLevel()
{
return Stat->GetCurrentLevel();
}
void AABCharacterBase::SetLevel(int32 InNewLevel)
{
Stat->SetLevelStat(InNewLevel);
}
ABCharacterBase.cpp
베이스 스탯으로 지정됨
액터의 생성과 지연 생성의 프로세스
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Item/ABItemData.h"
#include "GameData/ABCharacterStat.h"
#include "ABWeaponItemData.generated.h"
/**
*
*/
UCLASS()
class ARENABATTLE_API UABWeaponItemData : public UABItemData
{
GENERATED_BODY()
public:
FPrimaryAssetId GetPrimaryAssetId() const override
{
return FPrimaryAssetId("ABItemData", GetFName());
}
public:
UPROPERTY(EditAnywhere, Category = Weapon)
TSoftObjectPtr<USkeletalMesh> WeaponMesh;
UPROPERTY(EditAnywhere, Category = Stat)
FABCharacterStat ModifierStat;
};
ABWeaponItemData.h
애셋의 경로가 NPCMeshes라는 변수에 선언되어 있음.
+는 배열을 의미
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Character/ABCharacterBase.h"
#include "Engine/StreamableManager.h"
#include "ABCharacterNonPlayer.generated.h"
/**
*
*/
UCLASS(config=ArenaBattle)
class ARENABATTLE_API AABCharacterNonPlayer : public AABCharacterBase
{
GENERATED_BODY()
public:
AABCharacterNonPlayer();
protected:
virtual void PostInitializeComponents() override;
protected:
void SetDead() override;
void NPCMeshLoadCompleted();
UPROPERTY(config)
TArray<FSoftObjectPath> NPCMeshes;
TSharedPtr<FStreamableHandle> NPCMeshHandle;
};
ABCharacterNonPlayer.h
// Fill out your copyright notice in the Description page of Project Settings.
#include "Character/ABCharacterNonPlayer.h"
#include "Engine/AssetManager.h"
AABCharacterNonPlayer::AABCharacterNonPlayer()
{
GetMesh()->SetHiddenInGame(true);
}
void AABCharacterNonPlayer::PostInitializeComponents()
{
Super::PostInitializeComponents();
ensure(NPCMeshes.Num() > 0);
int32 RandIndex = FMath::RandRange(0, NPCMeshes.Num() - 1);
NPCMeshHandle = UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(NPCMeshes[RandIndex], FStreamableDelegate::CreateUObject(this, &AABCharacterNonPlayer::NPCMeshLoadCompleted));
}
void AABCharacterNonPlayer::SetDead()
{
Super::SetDead();
FTimerHandle DeadTimerHandle;
GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle, FTimerDelegate::CreateLambda(
[&]()
{
Destroy();
}
), DeadEventDelayTime, false);
}
void AABCharacterNonPlayer::NPCMeshLoadCompleted()
{
if (NPCMeshHandle.IsValid())
{
USkeletalMesh* NPCMesh = Cast<USkeletalMesh>(NPCMeshHandle->GetLoadedAsset());
if (NPCMesh)
{
GetMesh()->SetSkeletalMesh(NPCMesh);
GetMesh()->SetHiddenInGame(false);
}
}
NPCMeshHandle->ReleaseHandle();
}
ABCharacterNonPlayer.cpp
랜덤하게 NPC 스폰
이득우의 언리얼 프로그래밍 Part2 - 언리얼 게임 프레임웍의 이해 강의 | 이득우 - 인프런
이득우 | 대기업 현업자들이 수강하는 언리얼 C++ 프로그래밍 전문 과정입니다. 언리얼 C++ 프로그래밍을 사용해 핵&슬래시 로그라이크 게임 예제를 처음부터 끝까지 체계적으로 제작하는 방법을
www.inflearn.com
'게임 개발 > 언리얼 5' 카테고리의 다른 글
언리얼 5 - 행동트리 모델의 구현 (0) | 2024.12.16 |
---|---|
언리얼5 - 행동트리 모델의 이해 (1) | 2024.12.15 |
언리얼 5 - 무한맵의 제작 (0) | 2024.12.11 |
언리얼5 - 아이템 시스템 (1) | 2024.12.10 |
언리얼5 - 캐릭터 스탯과 위젯 (2) | 2024.11.28 |