728x90
반응형
- 정리
- 데이터 기반의 게임 시스템 구축
- 외부 데이터 파일로부터 게임 데이터를 관리하는 다양한 방법의 학습
- 데이터를 관리하는 싱글톤 클래스 생성 방법의 이해
- 데이터 테이블 기반의 캐릭터 스텟 시스템 구축
- 지연 생성을 활용한 액터 초기화 방법의 이해
- 데이터 기반의 게임 시스템 구축
- 데이터 기반의 게임 시스템 구축
- 이때까지 사용한 데이터를 데이터 기반으로 변경하기 (csv)
- 엑셀 기반의 csv파일을 만들었다면, 언리얼 에디터 내에서 이것과 동일한 이름을 가진 속성의 구조체를 선언해줘야 한다.
- 엑셀 데이터의 임포트
- csv파일을 언리얼 엔진은 임포트를 할 수 있도록 기본적으로 기능을 제공하고 있다.
- DataAsset과 유사하게 FTableRowBase를 상속받은 구조체를 선언
- 엑셀 데이터 테이블의 컬럼에는 Name이라는 컬럼이 들어가야 한다. (키값이 됨)
- 엑셀의 Name 컬럼을 제외한 컬럼과 동일하게 UPROPERTY 속성을 선언
- 엑셀 데이터를 csv로 익스포트한 후 언리얼 엔진에 임포트
- Name 칼럼에는 고유한 식별자를 부여
- 나머지에는 데이터를 집어 넣고, 구조체에는 이 다섯 개의 속성만 선언해서 지정해주면 된다.
- 텍스트 파일로 헤더파일을 만들어주고 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) {}
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;
// 연산자 재정의
// 모든 멤버변수가 float이라고 가정이 되면, 구조체의 데이터는 float의 집합으로 구성됨
// 이 크기만큼 개수를 확인하고 더해주면 새로운 연산자를 추가해도 덧셈 연산을 고칠 필요가 없다.
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
- 똑같은 폴더명의 폴더를 만들고, Miscellaneous>Data Table을 생성해준다.
- 정상적으로 헤더파일이 컴파일 되었다면 방금 만든 ABCharacterStat라는 구조체를 선택할 수 있게 나온다.
- 생성한 Data Table에서 Reimport를 하여 반영할 csv파일을 넣어준다.
- 데이터를 관리할 싱글톤 클래스의 설정
- 언리얼 엔진에서 제공하는 싱글톤 클래스
- 게임 인스턴스
- 에셋 매니저
- 게임 플레이 관련 액터 (게임 모드, 게임 스테이트)
- 프로젝트에 싱글톤으로 등록한 언리얼 오브젝트
- 언리얼 오브젝트 생성자에서 사용하지 않도록 주의
- 언리얼 엔진에서 제공하는 싱글톤 클래스
#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:
// Get함수, 값이 유효하면 전자, 아니면 그냥 기본 생성자를 만들어서 넘겨주도록 선언
FORCEINLINE FABCharacterStat GetCharacterStat(int32 InLevel) const { return CharacterStatTable.IsValidIndex(InLevel) ? CharacterStatTable[InLevel - 1] : FABCharacterStat(); }
UPROPERTY()
// 총 몇개의 레벨?
int32 CharacterMaxLevel;
private:
TArray<FABCharacterStat> CharacterStatTable; // 캐릭터 스탯들에 대한 테이블들을 내부적으로 보관해서 필요한 게임 객체들에게 제공하기 위한 Talbe Array
};
- ABGameSingleton.h
- 해당 ABGameSingleton 클래스를 통해서 스탯들에 대한 테이블들을 필요한 게임 객체들에게 제공할수있도록 Table Array를 설정한다.
- Get함수를 통해 Table Array의 값이 유효하면 해당 값을, 아니면 기본 생성자를 만들어서 넘겨준다.
#include "GameData/ABGameSingleton.h"
DEFINE_LOG_CATEGORY(LogABGameSingleton);
// 엑셀 테이블에서 만든 로우 테이블 에셋을 접근해서 TArray로 변환, 저장시키기
UABGameSingleton::UABGameSingleton()
{
// 만든 DataTable 주소를 가져온다.
// DataTable은 맵 형태로 Key,Value 값으로 들어온다.
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);
// Key값은 순차적으로 오기 때문에 Key값은 필요없고, Value값만 Array에 저장
TArray<uint8*> ValueArray;
DataTable->GetRowMap().GenerateValueArray(ValueArray);
Algo::Transform(ValueArray, CharacterStatTable,
[](uint8* Value)
{
return *reinterpret_cast<FABCharacterStat*>(Value);
}
);
}
// 배열의 갯수가 0보다 큰지 확인
CharacterMaxLevel = CharacterStatTable.Num();
ensure(CharacterMaxLevel > 0);
}
// Singleton Get() 함수
UABGameSingleton& UABGameSingleton::Get()
{
// CastChecked로 강력하게 검사, GEngine에 있는 아까 적용한 GameSingleton을 가져오기
UABGameSingleton* Singleton = CastChecked<UABGameSingleton>(GEngine->GameSingleton);
if (Singleton)
{
return *Singleton;
}
// 혹시 몰라서 만약 Singleton이 null 값이라면 에러를 띄우도록 함
// 코드의 흐름을 위해서 return값의 인스턴스를 생성하고 리턴함
UE_LOG(LogABGameSingleton, Error, TEXT("Invalide Game Singleton"));
return *NewObject<UABGameSingleton>();
}
- ABGameSingleton.cpp
- 생성자에서는 DataTable을 초기화시켜주고, Key값은 순차적으로 정렬되어 있기 때문에 따로 필요 없이 Value값만 Array를 통해 저장시켜준다.
- Get()함수에서는 Project Settings에서 GameSingleton으로 설정한 싱글톤 클래스를 가지고 오고 리턴하도록 구현해준다.
- 프로젝트의 주요 레이어
- 게임 레이어 : 기믹과 NPC
- 미들웨어 레이어 : 캐릭터 스탯 컴포넌트, 아이템 박스
- 데이터 레이어 : 스탯 데이터, 데이터 관리를 위한 싱글톤 클래스
- 위에서 아래로만 참조할 수 있고, 아래에서 위로 참조하지 못하게 구조를 구성
- 캐릭터 스탯 시스템은 다음과 같이 구성
class ARENABATTLE_API UABCharacterStatComponent : public UActorComponent
{
// ...
// Stat Section
// 레벨을 설정해주는 함수
void SetLevelStat(int32 InNewLevel);
// 레벨 Getter 함수
FORCEINLINE float GetCurrentLevel() const { return CurrentLevel; }
// 무기 .. 등을 획득했을때 ModifierStat을 변경해줄 수 있는 Setter 함수
FORCEINLINE void SetModifierStat(const FABCharacterStat& InModifierStat) { ModifierStat = InModifierStat; }
// 캐릭터의 전체 스텟값을 받아올 수 있도록
FORCEINLINE FABCharacterStat GetTotalStat() const { return BaseStat + ModifierStat; }
// 해당 네 가지 값들은 캐릭터가 초기화될 때마다 언제든지 바뀔 수 있다. 그렇기 때문에 언리얼 엔진을 저장할 때는 해당 정보가 저장되지 않도록 Transient 키워드를 추가하고, 에디터에서는 읽기 전용으로 설정
// 디스크에 저장할 필요가 없는 휘발성 데이터의 경우 Transient 라는 키워드를 추가하여 디스크에 저장할때 불필요한 공간이 낭비되지 않도록 지정할 수 있음
UPROPERTY(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
- 스텟 정보의 경우 csv파일을 통한 게임 싱글톤으로부터 제공받고, BaseStat, ModifierStat를 통해 관리해준다.
- 전체 스텟값을 받아올 수 있는 GetTotalStat함수도 만들어 관리
#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)
{
// GameSingleton에서 데이터를 가져오기
CurrentLevel = FMath::Clamp(InNewLevel, 1, UABGameSingleton::Get().CharacterMaxLevel);
BaseStat = UABGameSingleton::Get().GetCharacterStat(CurrentLevel);
check(BaseStat.MaxHp > 0.0f);
}
- ABCharacterStatComponent.cpp
UCLASS()
class ARENABATTLE_API AABStageGimmick : public AActor
{
// ...
// CurrentStageNum 관련 Getter, Setter
public:
FORCEINLINE int32 GetStageNum() const { return CurrentStageNum; }
FORCEINLINE void SetStageNum(int32 NewStageNum) { CurrentStageNum = NewStageNum; }
// Stage Stat
protected:
UPROPERTY(VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
int32 CurrentStageNum;
}
- ABStageGimmick.h
- 스테이지 기믹에도 스텟을 활용하여 로직을 구성해준다.
- 현재 스테이지 레벨에 따라 NPC의 데이터를 업데이트해주기
// 게이트의 트리거가 발동된다면
void AABStageGimmick::OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
// ...
// 아무것도 없다고 판정되었을 때
if (!bResult)
{
AABStageGimmick* NewGimmick = GetWorld()->SpawnActor<AABStageGimmick>(NewLocation, FRotator::ZeroRotator);
if (NewGimmick)
{
NewGimmick->SetStageNum(CurrentStageNum + 1);
}
}
}
// NPC 소환
void AABStageGimmick::OnOpponentSpawn()
{
const FVector SpawnLocation = GetActorLocation() + FVector::UpVector * 88.0f;
// OpponentClass를 TSubclassOf로 한정시켰기 때문에 캐릭터에 있는 ABCharacterNonPlayer를 상속받은 캐릭터에 한정해서만 액터를 스폰시키게 된다.
AActor* OpponentActor = GetWorld()->SpawnActor(OpponentClass, &SpawnLocation, &FRotator::ZeroRotator);
AABCharacterNonPlayer* ABOpponentCharacter = Cast<AABCharacterNonPlayer>(OpponentActor);
// 캐스팅이 정상적으로 된다면, OnDestroyed 델리게이트에 함수를 바인드
if (ABOpponentCharacter)
{
ABOpponentCharacter->OnDestroyed.AddDynamic(this, &AABStageGimmick::OnOpponentDestroyed);
// NPC가 생성될 때 CurrentStageNum에 따라 Level을 설정할 수 있도록
ABOpponentCharacter->SetLevel(CurrentStageNum);
}
}
- ABStageGimmick.cpp
- 게이트의 트리거가 발동된다면 Stage의 CurrentStageNum을 증가 시켜준다.
- NPC가 생성되면 레벨을 CurrentStageNum을 활용하여 Level을 설정할 수 있도록 해준다.
- 하지만 위와 같은 코드를 작성하고 나면, 다음스테이지에 가게 될 경우 CurrentStageNum은 증가하면서 NPC의 MaxHp는 증가하지만 CurrentHp는 그대로인 모습을 확인 할 수 있다.
- 위와 같은 현상이 일어나는 이유는 다음과 같다. 액터의 생성을 SpawnActor로 사용하게 된다면 액터와 액터가 가지고 있는 모든 컴포넌트의 BeginPlay 함수가 바로 호출이 됨.
- 그렇기 때문에 SpawnActor로 NPC를 생성하자, ABCharacterStatComponent에서의 BeginPlay가 실행이 되고 그 안에 있는 SetLevelStat함수와 SetHp함수가 실행되게 된다.
// Called when the game starts
void UABCharacterStatComponent::BeginPlay()
{
Super::BeginPlay();
SetLevelStat(CurrentLevel);
SetHp(BaseStat.MaxHp);
}
- ABCharacterStatComponent.cpp
- CurrentLevel은 아직 이전 레벨이기 때문에 1레벨 데이터로 NPC가 생성될 것이고, 이후 ABStageGimmick에서는 기믹을 이미 SpawnActor를 호출해서 BeginPlay를 실행한 상태에서 SetLevel을 진행했기 때문에
// NPC 소환
void AABStageGimmick::OnOpponentSpawn()
{
const FVector SpawnLocation = GetActorLocation() + FVector::UpVector * 88.0f;
// OpponentClass를 TSubclassOf로 한정시켰기 때문에 캐릭터에 있는 ABCharacterNonPlayer를 상속받은 캐릭터에 한정해서만 액터를 스폰시키게 된다.
AActor* OpponentActor = GetWorld()->SpawnActor(OpponentClass, &SpawnLocation, &FRotator::ZeroRotator);
AABCharacterNonPlayer* ABOpponentCharacter = Cast<AABCharacterNonPlayer>(OpponentActor);
// 캐스팅이 정상적으로 된다면, OnDestroyed 델리게이트에 함수를 바인드
if (ABOpponentCharacter)
{
ABOpponentCharacter->OnDestroyed.AddDynamic(this, &AABStageGimmick::OnOpponentDestroyed);
// NPC가 생성될 때 CurrentStageNum에 따라 Level을 설정할 수 있도록
ABOpponentCharacter->SetLevel(CurrentStageNum);
}
}
- ABStageGimmick.cpp
- 언리얼에서는 액터를 지연 생성하는 SpawnActorDeferred라는 함수를 제공한다. 마지막에 FinishSpawning 함수를 호출해줘야지만 BeginPlay가 호출이 된다.
- FinishSpawning 함수를 호출하기 전에 초기화를 진행하면 BeginPlay로 설정되어 있는 SetHp함수를 올바로 실행할 수 있게 된다.
// NPC 소환
void AABStageGimmick::OnOpponentSpawn()
{
const FTransform SpawnTransform(GetActorLocation() + FVector::UpVector * 88.0f);
// OpponentClass를 TSubclassOf로 한정시켰기 때문에 캐릭터에 있는 ABCharacterNonPlayer를 상속받은 캐릭터에 한정해서만 액터를 스폰시키게 된다.
// SpawnActor -> SpawnActorDeferred 지연생성으로 변경
AABCharacterNonPlayer* ABOpponentCharacter = GetWorld()->SpawnActorDeferred<AABCharacterNonPlayer>(OpponentClass, SpawnTransform);
// 캐스팅이 정상적으로 된다면, OnDestroyed 델리게이트에 함수를 바인드
if (ABOpponentCharacter)
{
ABOpponentCharacter->OnDestroyed.AddDynamic(this, &AABStageGimmick::OnOpponentDestroyed);
// NPC가 생성될 때 CurrentStageNum에 따라 Level을 설정할 수 있도록
ABOpponentCharacter->SetLevel(CurrentStageNum);
// SpawnActorDeferred 지연 생성 완료, 이후 BeginPlay 함수 실행
ABOpponentCharacter->FinishSpawning(SpawnTransform);
}
}
- ABStageGimmick.cpp
- SpawnActor를 SpawnActorDeferred함수로 변경시켜주고, 인자로 FTransform을 받기 때문에 그에 맞게 수정해준다.
- 이후, SetLevel을 실행해준 뒤, FinishSpawning을 실행해서 지연 생성을 완료해주고 BeginPlay함수가 실행되도록해준다.
- 다음과 같이 스폰 코드가 초기값이 반영이 되어야 한다면 지연생성 코드로 바꿔주는 것이 좋다.
- void AABStageGimmick::OnOpponentSpawn()
- SpawnActor →SpawnActorDeferred로 변경
- void AABStageGimmick::SpawnRewardBoxes()
- SpawnActor → SpawnActorDeferred로 변경
- 생성자에 있던 Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABItemBox::OnOverlapBegin); 를 void AABItemBox::PostInitializeComponents() 에 위치하도록 변경하면서 지연생성 후 바인딩 되도록 변경
- void AABStageGimmick::OnOpponentSpawn()
- Weapon에 추가 스탯을 지정해주고, NPC의 Mesh를 랜덤하게 나올 수 있도록 설정하기
#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()
// ...
UPROPERTY(EditAnywhere, Category = Stat)
FABCharacterStat ModifierStat;
};
- UABWeaponItemData.h
- WeaponItemData에 ModifierStat 변수를 추가해서 획득하게 되면 Weapon의 스텟이 적용될 수 있도록 설정해준다.
- [Script/ArenaBattle.ABCharacterNonPlayer]
- C++ 객체이고, ArenaBattle 모듈의 ABCharcterNonPlayer라는 언리얼 오브젝트를 가리키고 있다.
- +NPCMeshes
- NPCMeshes라는 TArray변수가 ABCharcterNonPlayer에 선언이 되어 있다면 이것의 값을 지정하는 것
#pragma once
#include "CoreMinimal.h"
#include "Character/ABCharacterBase.h"
#include "Engine/StreamableManager.h" // 비동기 위함
#include "ABCharacterNonPlayer.generated.h"
/**
*
*/
// Config 폴더의 DefaultArenaBattle.ini를 사용해서 불러들이겠다는 의미
UCLASS(config=ArenaBattle)
class ARENABATTLE_API AABCharacterNonPlayer : public AABCharacterBase
{
GENERATED_BODY()
public:
AABCharacterNonPlayer();
protected:
virtual void PostInitializeComponents() override;
protected:
void SetDead() override;
// NPC Mesh 로딩이 완료되었을 때 신호를 받는 함수
void NPCMeshLoadCompleted();
// UPROPERTY에 config를 붙이게 되면 해당 config 파일로부터 데이터를 불러오겠다는 의미
// 경로가 있기 때문에 FSoftObjectPath
UPROPERTY(config)
TArray<FSoftObjectPath> NPCMeshes;
// 비동기 위한 FStreamableHandle
TSharedPtr<FStreamableHandle> NPCMeshHandle;
};
- AABCharacterNonPlayer.h
- PostInitializeComponents() 함수를 오버라이드 해주고, ini 파일에 나와 있는 것처럼 NPCMeshes라는 TArray 변수를 만들어 준다. 경로값이 들어가기 때문에 자료형은 FSoftObjectPath로
- 비동기 로딩을 위한 헤더 추가와 TSharedPtr<FStreamableHandle> NPCMeshHandle; 를 추가해준다.
#include "Character/ABCharacterNonPlayer.h"
#include "Engine/AssetManager.h" // 에셋 매니저, 비동기 로드 위한
AABCharacterNonPlayer::AABCharacterNonPlayer()
{
GetMesh()->SetHiddenInGame(true);
}
void AABCharacterNonPlayer::PostInitializeComponents()
{
Super::PostInitializeComponents();
// NPCMeshes가 제대로 들어왔는지 확인
ensure(NPCMeshes.Num() > 0);
int32 RandIndex = FMath::RandRange(0, NPCMeshes.Num() - 1);
// 에셋 매니저를 사용해서 비동기 방식으로 AsyncLoad로 진행, 비동기가 끝나면 NPCMeshLoadCompleted 함수를 실행할 수 있도록 바인드
NPCMeshHandle = UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(NPCMeshes[RandIndex], FStreamableDelegate::CreateUObject(this, &AABCharacterNonPlayer::NPCMeshLoadCompleted));
}
// ...
// 비동기가 끝나면 NPCMeshLoadCompleted 함수 실행
void AABCharacterNonPlayer::NPCMeshLoadCompleted()
{
// 로딩이 완료된 후 Handle이 유효한가?
if (NPCMeshHandle.IsValid())
{
//GetLoadedAsset를 사용해서 Mesh 가져오기
USkeletalMesh* NPCMesh = Cast<USkeletalMesh>(NPCMeshHandle->GetLoadedAsset());
if (NPCMesh)
{
GetMesh()->SetSkeletalMesh(NPCMesh);
GetMesh()->SetHiddenInGame(false);
}
}
// Handle은 사용 완료후 해지
NPCMeshHandle->ReleaseHandle();
}
- AABCharacterNonPlayer.cpp
- PostInitializeComponents()에서 Mesh가 정상적으로 들어왔는지 확인 후 랜덤으로 Mesh를 비동기 로딩한다.
- 비동기 로딩이 끝나면 발생하는 델리게이트에 AABCharacterNonPlayer::NPCMeshLoadCompleted()를 바인딩해주고, Mesh를 적용시켜준다.
- Mesh를 적용시켜주고 나면 NPCMeshHandle은 해지해준다.
해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part2 -언리얼 게임 프레임웍의 이해>
강의를 수강하고 정리한 내용입니다.
이득우의 언리얼 프로그래밍 Part2 - 언리얼 게임 프레임웍의 이해 | 이득우 - 인프런
청강문화산업대학교에서 언리얼 엔진, 게임 수학, UEFN 게임제작을 가르치고 있습니다. - 이득우의 언리얼 C++ 프로그래밍, 넥슨 코리아 공식 교육 교재 선정 2023 - 스마일게이트 언리얼 프로그래
www.inflearn.com
728x90
'공부 > 이득우의 언리얼 프로그래밍' 카테고리의 다른 글
[Study] Part 2 - 행동 트리 모델의 구현 (12/15) (0) | 2024.06.20 |
---|---|
[Study] Part 2 - 행동 트리 모델의 이해 (11/15) (0) | 2024.06.19 |
[Study] Part 2 - 무한맵의 제작 (9/15) (0) | 2024.06.17 |
[Study] Part 2 - 아이템 시스템 (8/15) (0) | 2024.06.13 |
[Study] Part 2 - 캐릭터 스탯과 위젯 (7/15) (0) | 2024.06.04 |