시작하기 전에
C++ 기반 언리얼 엔진 프로젝트가 있는지 확인합니다.
이 페이지는 1인칭 카메라, 메시 및 애니메이션 추가에서 이어지는 내용입니다. 하지만, 데이터 기반 게임플레이에 대해서만 배우고 싶다면 1인칭 어드벤처 게임 코딩하기 튜토리얼 시리즈의 나머지 부분과 별개로 이 페이지만 완료할 수 있습니다.
게임 내 데이터 정리
게임 디자인에서 데이터를 정리하고 표현하는 방식은 매우 중요합니다. 상호작용 가능한 아이템의 퀄리티는 매우 다양하며, 게임 내에 존재할 수도 있고 데이터의 일부로만 존재할 수도 있습니다. 각 아이템 타입에 대해 별도의 클래스와 블루프린트를 생성하여 여러 액터에 데이터를 배포하고 통신하는 방식으로 이 데이터를 표현할 수 있습니다. 하지만, 게임이 커지고 저장 데이터가 늘어날수록 이 방법은 비효율적이 됩니다.
더 나은 접근 방식은 데이터 기반 게임플레이입니다. 값을 하드코딩하는 대신, 게임 내 시스템에서 관리하는 중앙화된 위치에 데이터를 정리할 수 있습니다. 데이터 기반 게임플레이를 사용하면 필요할 때 필요한 요소만 로드할 수 있습니다. 예를 들어, 여러 게임에서 스프레드시트 문서를 사용하여 대사를 정리하는데, 그 이유는 각 캐릭터에 수백 줄에 달하는 대사를 저장하는 대신 시스템에서 특정 대사 한 줄을 가져오는 것이 더 쉽기 때문입니다.
이 섹션에서는 이 방법론을 사용하여 커스텀 아이템을 생성하는 방법에 대해 알아봅니다.
데이터 기반 게임플레이 엘리먼트
기본 아이템 제작을 시작하기 전에 '아이템'을 정의하는 것이 무엇인지 생각해봐야 합니다. 아이템은 플레이어가 상호작용하는 모든 것이 될 수 있으므로, 모든 타입의 아이템에 유효한 최소한의 프로퍼티 세트가 있어야 합니다. 아이템 데이터 구조체(Item Data Struct)에서 이 최소한의 프로퍼티 세트를 설정합니다. 또한 이 아이템 데이터를 정리하고 표시할 수 있는 중앙화된 공간이 있어야 합니다. 이를 위해 데이터 테이블(Data Table) 에셋을 사용합니다.
아이템 데이터 구조체가 아이템이 지닌 데이터 타입을 정의하는 템플릿 역할을 한다면, 데이터 테이블과 데이터 에셋(Data Assets)이 그 구조체를 기반으로 실제 데이터 항목을 저장합니다.
아래 다이어그램을 보면 이 튜토리얼 섹션에서 생성할, 네 가지 데이터 기반 게임플레이 엘리먼트를 확인할 수 있습니다. 네 가지 엘리먼트를 모두 설정한 다음에는 빌드한 내용을 요약하는 데 도움이 되도록 이 다이어그램을 한 번 더 자세히 살펴보겠습니다.
먼저 아이템 데이터를 정의하는 파일 두 개를 생성합니다.
ItemData.h: 아이템 데이터 구조체(FItemData) 선언을 위한 컨테이너입니다.ItemDefinition.h: 언리얼 에디터에서 아이템 데이터를 사용할 수 있도록UDataAsset에서 상속하는 클래스입니다.
아이템 데이터 구조체는 UObject에서 상속되지 않으며 레벨에서 인스턴스화될 수 없으므로, 에디터에서 사용하고 참조할 데이터 에셋 클래스도 필요합니다.
그런 다음, 언리얼 에디터에서 ItemDefinition에 따라 데이터 테이블과 데이터 에셋 인스턴스를 생성합니다.
아이템 데이터 정의하기
아이템 데이터 구조체는 데이터 테이블의 각 아이템에 있어야 하는 데이터 또는 프로퍼티를 정의하며, 테이블의 열처럼 작동합니다.
아이템 데이터 구조체에는 다음과 같은 프로퍼티가 있습니다.
ID: 아이템의 고유한 이름으로, 나중에 테이블 행을 참조할 때 유용합니다.
아이템 타입(Item Type): 이 아이템의 타입으로, 이 경우에는 툴(Tool) 및 소모품(Consumable) 타입을 정의합니다.
아이템 텍스트(Item Text): 이름과 설명 등 아이템에 대한 텍스트 데이터입니다.
아이템 베이스(Item Base): 이 아이템과 연결된 아이템 정의(ItemDefinition) 데이터 에셋입니다.
자체적인 테이블 필드(열)를 만들려면 데이터 테이블 필드는 UPROPERTY()와 호환되는 어떤 타입이든 될 수 있다는 점에 유의하세요.
구조체의 헤더 파일 컨테이너 생성하기
아이템 데이터 구조체 정의를 저장할 새 폴더와 새 헤더(.h) 파일을 구성합니다.
ItemDefinition.h 내에 FItemData 구조체를 생성할 수 있지만, 구조체를 별도의 파일에 넣으면 데이터 엘리먼트를 정리하는 데 도움이 되고 재사용할 수 있습니다.
헤더 파일을 아이템 데이터 구조체의 컨테이너로 구성하려면 다음 단계를 따릅니다.
언리얼 에디터에서 툴(Tools) > 새 C++ 클래스(New C++ Class)로 이동합니다.
부모 클래스 선택(Choose Parent Class) 창에서 없음(None)을 부모 클래스로 선택하고 다음(Next)을 클릭합니다.
경로 옆의 폴더 아이콘을 클릭합니다.
Source/[프로젝트명]폴더에 이 클래스를 저장할Data라는 새 폴더를 생성합니다.그 클래스의 이름을
ItemData로 지정한 다음, 클래스 생성(Create Class)을 클릭합니다.언리얼 엔진에서 새 클래스 파일이 자동으로 열리지 않는다면, Visual Studio에서 프로젝트와
ItemData.h를 엽니다.ItemData.cpp의 모든 텍스트를 삭제한 다음, 파일을 저장하고 닫습니다. 이 파일은 사용하지 않을 것입니다.ItemData.h에서#include "CoreMinimal.h"줄 아래의 모든 내용을 삭제합니다.CoreMinimal.h헤더에는FName,FString같은 기본 타입과 데이터를 정의하는 데 필요한 기타 타입이 포함되어 있습니다.ItemData.h의 상단에#pragma once및 다음 include 구문을 추가합니다.#include "Engine/DataTable.h":FTableRowBase에서 구조체를 상속받을 때 필요합니다.#include "ItemData.generated.h": 언리얼 헤더 툴에 필요합니다. 코드가 제대로 컴파일되려면 이 include 구문이 마지막에 와야 합니다.
C++#pragma once #include "CoreMinimal.h" #include "Engine/DataTable.h" #include "ItemData.generated.h"UItemDefinition이라는 클래스에 대한 포워드 선언을 추가합니다. 이는 에디터에서 작업할 수 있는 데이터 에셋이 됩니다.C++class UItemDefinition;
아이템 프로퍼티 정의하기
아이템 타입과 텍스트 데이터에 대한 기존 변수 타입이 없으므로 이를 정의해야 합니다.
아이템 타입(Item Type) 열거형을 정의하려면 다음 단계를 따릅니다.
가능한 모든 아이템 타입을 나열하는 새로운
열거형 클래스(enum class)를 생성합니다. 이 튜토리얼에서는 툴 및 소모품 타입 아이템을 생성합니다.열거형 클래스의 이름을
EItemType으로 명명하고 타입을uint8로 지정합니다. 그 위에UENUM()매크로를 추가하여 언리얼 헤더 툴에 이 열거형을 선언합니다.C++// Defines the type of the item. UENUM() enum class EItemType : uint8 { };이 열거형에 다음 커스텀 값 두 개를 추가합니다.
DisplayName = "Tool"을 사용하여UMETA()매크로를 추가하는ToolDisplayName = "Consumable"을 사용하여UMETA()매크로를 추가하는Consumable
C++// Defines the type of the item. UENUM() enum class EItemType : uint8 { Tool UMETA(DisplayName = "Tool"), Consumable UMETA(DisplayName = "Consumable") };
이러한 아이템 타입은 언리얼 엔진에 내장된 것이 아닌 커스텀이므로, 기초 학습을 마친 뒤에는 원하는 타입을 만들 수 있습니다! QuestItem 타입이든 Currency 타입이든, 아니면 Disguise 타입이든, 원하는 타입이라면 무엇이든지 가능합니다.
아이템 텍스트 구조체를 정의하려면 다음 단계를 따릅니다.
EItemType다음에USTRUCT()매크로를 사용하여FItemText라는 이름으로 새 구조체를 생성합니다. 이 구조체에는 아이템에 대한 텍스트 데이터가 저장됩니다.C++// Contains textual data about the item. USTRUCT() struct FItemText { };FItemText안에GENERATED_BODY()매크로를 추가합니다.그런 다음,
Name및Description이라는 이름으로 두 개의FText프로퍼티를 추가하여 이 아이템의 이름과 설명을 저장합니다. 두 프로퍼티에 각각UPROPERTY()매크로를 추가하고EditAnywhere를 실행인자로 지정합니다.C++// Contains textual data about the item. USTRUCT() struct FItemText { GENERATED_BODY() // The text name of the item. UPROPERTY(EditAnywhere) FText Name;
아이템 데이터 구조체 생성하기
필수 구성 요소를 추가했으니 이제 아이템의 프로퍼티가 포함된 아이템 데이터 구조체를 생성합니다. 이러한 프로퍼티는 데이터 테이블의 필드가 됩니다.
FTableRowBase에서 상속되는 FItemData라는 이름의 구조체를 정의합니다. 어디서나 볼 수 있도록 public 지정자를 추가하고 언리얼 헤더 툴을 위해 GENERATED_BODY()를 추가합니다.
// Defines a basic item that can be used in a data table.
USTRUCT()
struct FItemData : public FTableRowBase
{
GENERATED_BODY()
};FTableRowBase는 언리얼 엔진에서 제공하는 베이스 구조체로, 데이터 테이블 에셋에서 커스텀 USTRUCT를 사용할 수 있게 합니다. 언리얼은 이 구조체를 사용하여 행 구조체를 시리얼라이즈하는 방법을 파악하며, CSV/JSON 파일에서의 데이터 임포트 및 익스포트를 지원하고, 테이블에서 데이터를 가져올 때 타입 안전성을 보장합니다.
FItemData 구조체에 다음 선언을 추가합니다.
ID라는 이름의FName. 데이터 테이블의 각 행에는 연결된FName이 참조용으로 필요합니다.ItemType이라는 이름의EItemType 열거형. 이는 앞서 선언한 아이템 타입의 열거형입니다.ItemText라는 이름의FItemText구조체. 이는 앞서 선언한 텍스트 데이터의 구조체입니다.
각 선언에 EditAnywhere 및 Category = "Item Data" 실행인자를 사용하여 UPROPERTY() 매크로를 추가합니다.
// The ID name of this item for referencing in a table row.
UPROPERTY(EditAnywhere, Category = "Item Data")
FName ID;
// The type of the item.
UPROPERTY(EditAnywhere, Category = "Item Data")
EItemType ItemType;
// Text struct including the item name and description.
UPROPERTY(EditAnywhere, Category = "Item Data")
ItemBase라는 이름의 UItemDefinition에 TObjectPtr을 하나 더 선언합니다. 여기에 구조체 내의 다른 프로퍼티와 동일하게 UPROPERTY 매크로를 지정합니다.
// The Data Asset item definition associated with this item.
UPROPERTY(EditAnywhere, Category = "Item Data")
TObjectPtr<UItemDefinition> ItemBase;TObjectPtr은 언리얼 엔진의 스마트 포인터 타입으로, UObject 파생 타입을 더 안전하게 참조할 수단입니다. 이는 에디터를 인지하며, 원시 UObject 포인터의 대체제로 가비지 컬렉션으로부터 안전합니다. TObjectPtr은 하드 레퍼런스이므로 런타임에 오브젝트를 계속 로드된 상태로 유지합니다.
다음 단계에서는 ItemDefinition이라는 이름으로 UDataAsset 클래스를 생성합니다. 데이터 테이블에서 ItemBase 필드를 사용하여 ItemDefinition Data Asset 인스턴스를 참조합니다.
이제 FItemData 구조체는 다음과 같은 모습이어야 합니다.
// Defines a basic item that can be used in a data table.
USTRUCT()
struct FItemData : public FTableRowBase
{
GENERATED_BODY()
// The ID name of this item for referencing in a table row.
UPROPERTY(EditAnywhere, Category = "Item Data")
FName ID;
코드를 저장합니다.
데이터 에셋 아이템 정의 빌드하기
지금까지 아이템 데이터, 즉 데이터 테이블에 표시되는 데이터 타입을 정의했습니다. 다음으로, ItemData.h에서 포워드 선언을 한 UItemDefinition 클래스를 구현합니다.
이 클래스는 UDataAsset에서 상속되므로 UObject입니다. 즉, 코드를 거치지 않고 에디터에서 바로 이 클래스의 인스턴스를 생성하고 사용할 수 있습니다. 데이터 테이블을 UItemDefinition 클래스의 인스턴스로 채울 것입니다.
ItemDefinition 데이터 에셋(DataAsset) 클래스(ItemDefinition.h)를 생성하려면 다음 단계를 따릅니다.
언리얼 에디터에서 툴(Tools) > 새 C++ 클래스(New C++ Class)로 이동합니다.
부모 클래스 선택(Choose Parent Class) 창에서 모든 클래스(All Classes)를 클릭합니다.
DataAsset을 검색하여 부모 클래스로 선택하고 다음(Next)을 클릭합니다.
ItemData.h에서 한 포워드 선언과 똑같이 클래스 이름을ItemDefinition으로 지정한 다음, 클래스 생성(Create Class)을 클릭합니다.
VS에서 자동으로 .h 파일과 .cpp 파일을 열 것입니다. 열리지 않으면 VS를 새로고침하고 파일을 수동으로 엽니다. 이제 .h 파일에서만 작업할 테니 원한다면 .cpp 파일을 닫아도 됩니다.
ItemDefinition.h에서 ItemData.h에 대한 include 구문을 추가합니다. 해당 파일에서 선언한 ItemType 및 ItemText 프로퍼티를 다시 사용해야 하기 때문입니다.
#include "CoreMinimal.h"
#include "Data/ItemData.h"
#include "ItemDefinition.generated.h"ItemDefinition.h에서 클래스 정의 위의 UCLASS() 매크로에 BlueprintType 및 Blueprintable 지정자를 추가하여 블루프린트 생성을 위한 베이스 클래스로 노출합니다.
// Defines a basic item with a static mesh that can be built from the editor.
UCLASS(BlueprintType, Blueprintable)
class FIRSTPERSON_API UItemDefinition : public UDataAsset
{
GENERATED_BODY()
public:
// Default constructor for the class.
UItemDefinition();
};public 섹션에서 ItemData.h의 FName ID, EItemType ItemType 및 FItemText ItemText 선언을 복사합니다.
아이템 정의는 FItemData 구조체와 동일한 데이터를 가져오므로 아이템에 대한 정보를 얻고자 할 때 원본 테이블을 참조할 필요가 없습니다.
public:
// The ID name of this item for referencing in a table row.
UPROPERTY(EditAnywhere, Category = "Item Data")
FName ID;
// The type of this item.
UPROPERTY(EditAnywhere, Category = "Item Data")
EItemType ItemType;
ItemText 다음에 WorldMesh라는 이름으로 UStaticMesh 타입의 TSoftObjectPtr을 선언합니다. 이 스태틱 메시를 사용하여 이 아이템을 월드에 표시할 것입니다.
TSoftObjectPtr은 특수한 타입의 위크 포인터로, 필요할 때만 로드되는 에셋 경로의 스트링 표현 역할을 합니다. 일반적으로 에셋을 참조하는 UObject 포인터 프로퍼티를 선언할 때, 그 프로퍼티가 포함된 오브젝트가 로드되면 해당 에셋이 로드됩니다. 이렇게 하면 게임이 시작될 때 모든 에셋이 로드되어 엄청난 오버헤드가 발생하고 속도가 크게 감소할 수 있습니다. TSoftObjectPtr은 필요할 때만 로드해야 하는 메시와 같은 대형 에셋에 유용합니다. 자세한 내용은 비동기 에셋 로딩을 참조하세요.
이 프로퍼티에 동일한 UPROPERTY(EditAnywhere, Category = "Item Data") 매크로를 추가합니다.
// Text struct including the item name and description.
UPROPERTY(EditAnywhere, Category = "Item Data")
FItemText ItemText;
// The Static Mesh used to display this item in the world.
UPROPERTY(EditAnywhere, Category = "Item Data")
TSoftObjectPtr<UStaticMesh> WorldMesh;
데이터 테이블 행은 CSV 행과 유사합니다. 즉, 텍스트 데이터만 저장하고 전체 에셋은 저장하지 않습니다. 데이터 관리를 최적화하려면 데이터 에셋에 있는 아이템의 메시, 머티리얼, 애니메이션과 같은 정보를 번들로 만드는 것이 좋습니다. 데이터 에셋은 한 가지 특정 아이템에 대한 모든 데이터가 있는 중앙 위치이기 때문입니다. 따라서 아이템의 스태틱 메시 프로퍼티는 FItemData 구조체가 아닌 UItemDefinition에 있습니다.
이제 완성된 UItemDefinition 클래스는 다음과 같은 모습이어야 합니다.
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Data/ItemData.h"
#include "ItemDefinition.generated.h"
/**
* Defines a basic item with a static mesh that can be built from the editor.
코드를 저장하고 Visual Studio에서 컴파일합니다.
데이터 에셋 인스턴스 생성하기
아이템 데이터(ItemData.h의 FItemData 구조체)와 아이템 정의(UItemDefinition 클래스)를 모두 정의했으므로, 이제 아이템 인스턴스와 데이터 테이블을 빌드하는 데 필요한 모든 조각이 모였습니다.
먼저 투사체 픽업 아이템에 대한 데이터 에셋을 생성한 다음, 데이터 테이블을 생성하고 데이터 에셋의 정보로 채웁니다.
ItemDefinition 클래스에서 데이터 에셋 항목을 생성하려면 다음 단계를 따릅니다.
콘텐츠 브라우저에서 Content > FirstPerson 폴더로 이동하여 추가(Add)를 클릭하거나 에셋 뷰(Asset View)에서 빈 영역을 우클릭하고 새 폴더(New Folder)를 선택한 다음 이름을
Data로 지정합니다.이 폴더에 게임의 데이터 에셋이 저장되고 정리됩니다.
Data폴더에서 추가를 클릭하거나 에셋 뷰에서 빈 영역을 우클릭하고 기타(Miscellaneous) > 데이터 에셋(Data Asset)을 선택합니다.원형 차트 아이콘이 있는 데이터 에셋 옵션을 선택했는지 확인합니다.
데이터 에셋 인스턴스의 클래스 선택(Pick Class For Data Asset Instance) 창에서 아이템 정의(Item Definition)(앞서 정의한 C++ 클래스)를 선택한 다음, 선택(Select)을 클릭합니다. 새 데이터 에셋을
DA_Pickup_001로 명명합니다.DA_Pickup_001을 더블클릭하여 엽니다. 해당 항목의 디테일(Details) 패널에서ItemDefinition.h에 정의한 모든 프로퍼티를 확인할 수 있습니다.ID로
pickup_001을 입력합니다.아이템 타입을 소모품(Consumable)으로 설정합니다.
아이템 텍스트를 열고 이름과 설명을 입력합니다.
월드 메시(World Mesh)를
SM_FoamBullet으로 설정합니다.창 왼쪽 상단 모서리에 있는 저장을 클릭하여 데이터 에셋을 저장합니다.
데이터 테이블 정의하기
이제 데이터 테이블을 채울 데이터 에셋이 하나 이상 준비되었으므로 테이블을 생성할 수 있습니다!
데이터 테이블의 각 행은 ItemData 구조체의 채워진 인스턴스 하나입니다.
데이터 테이블을 생성하려면 다음 단계를 따릅니다.
콘텐츠 브라우저의
Data폴더에서 빈 영역을 우클릭하고 기타(Miscellaneous) > 데이터 테이블(Data Table)을 선택합니다.행 구조체 선택(Pick Row Structure) 창에서 ItemData(
ItemData.h에서 정의한FItemData구조체)를 선택한 다음, 확인(OK)을 클릭합니다.새 테이블의 이름을
DT_PickupData로 지정한 다음 더블클릭하여 엽니다.
처음에는 데이터 테이블이 비어 있습니다. 하지만, FItemData에 정의했던 프로퍼티들이 테이블 상단에 제목으로 표시될 것이고, 행 이름(Row Name)이라는 추가 열도 보일 것입니다.
픽업 아이템의 데이터 에셋을 테이블에 행으로 추가하려면 다음 단계를 따릅니다.
추가(Add)를 클릭하여 테이블에 새 행을 추가합니다. 데이터 테이블 에디터는 데이터 테이블(Data Table) 탭의 왼쪽 상단 패널에 행 항목을 나열합니다.
새 행(NewRow)이라는 행 이름을 더블클릭하고pickup_001(데이터 에셋의 ID)로 변경합니다.행 이름으로
FName을 사용할 수 있지만, 코드에서 행을 쉽게 참조할 수 있도록 행 이름을 데이터 에셋의 ID와 똑같이 설정합니다.행 에디터(Row Editor) 패널에서
DA_Pickup_001데이터 에셋에서 설정한 것과 같은 값을 ID, 아이템 타입 및 아이템 텍스트 필드에 입력합니다.아이템 베이스(Item Base)를
DA_Pickup_001데이터 에셋으로 설정합니다.데이터 테이블을 저장합니다.
다 됐습니다! 이 단계에서 생성한 데이터 기반 게임플레이 엘리먼트의 다이어그램을 다시 살펴보고 모든 엘리먼트가 어떻게 연결되어 있는지 확인하세요.
FItemData 구조체에서 열을 가져오는 데이터 테이블을 생성했습니다. 생성한 소모품 타입 ItemDefinition 데이터 에셋 인스턴스의 데이터가 포함된 행으로 테이블을 채우고 ItemBase 포인터를 사용하여 데이터 에셋 자체를 참조했습니다. 마지막으로, 데이터 에셋 인스턴스는 앞서 생성한 부모 UItemDefinition 데이터 에셋 클래스에서 프로퍼티를 가져왔습니다.
다음 순서
다음 섹션에서는 이 아이템 정의를 확장하여 커스텀 픽업 클래스를 생성하고 레벨에서 인스턴스화하는 방법을 알아보겠습니다.
완성된 코드
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "ItemData.generated.h"
class UItemDefinition;
/**
* Defines the type of the item.
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Data/ItemData.h"
#include "ItemDefinition.generated.h"
/**
* Defines a basic item with a static mesh that can be built from the editor.