시작하기 전에
이전 섹션인 아이템 및 데이터 관리에서 다음 목표를 완료했는지 확인합니다.
아이템 데이터 구조체,
UDataAsset클래스,DA_Pickup_001이라는 소모품 타입 데이터 에셋 인스턴스 및 데이터 테이블을 구성합니다.
새 픽업 클래스 생성하기
지금까지 아이템의 구조체와 데이터를 정의하고 저장하는 방법을 배웠습니다. 이 섹션에서는 이 데이터를 플레이어가 상호작용하고 효과를 얻을 수 있는 아이템이자, 테이블 데이터의 구체적인 표현인 게임 내 '픽업'으로 전환하는 방법을 알아봅니다. 픽업은 장착할 수 있는 도구나 재료를 주는 박스, 일시적인 부스트를 주는 파워업 등이 될 수 있습니다.
초기 선언으로 픽업 클래스 구성을 시작하려면 다음 단계를 따릅니다.
언리얼 에디터에서 툴(Tools) > 새 C++ 클래스(New C++ Class)로 이동합니다. 액터(Actor)를 부모 클래스로 선택하고 클래스 이름을
PickupBase로 지정합니다. 클래스 생성(Create Class)을 클릭합니다.Visual Studio에서
PickupBase.h를 열고 파일 상단에 다음 구문을 추가합니다.#include "Components/SphereComponent.h". 픽업에 스피어 컴포넌트를 추가하여 플레이어와 픽업 간의 콜리전을 탐지합니다.#include "AdventureCharacter.h". 픽업과 이 클래스의 캐릭터가 오버랩되는지 확인할 수 있도록 1인칭 캐릭터 클래스에 레퍼런스를 추가합니다. (이 튜토리얼에서는AdventureCharacter를 사용합니다)UItemDefinition에 대한 포워드 선언입니다. 각 픽업이 참조하는, 연결된 데이터 에셋 아이템입니다.
C++// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "Components/SphereComponent.h" #include "CoreMinimal.h" #include "AdventureCharacter.h" #include "GameFramework/Actor.h" #include "PickupBase.generated.h"APickupBase클래스 정의 위의UCLASS()매크로에서BlueprintType및Blueprintable지정자를 추가하여 블루프린트 생성용 베이스 클래스로 노출시킵니다.C++UCLASS(BlueprintType, Blueprintable) class ADVENTUREGAME_API APickupBase : public AActor {PickupBase.cpp에서ItemDefinition.h에 대한#include를 추가합니다.C++// Copyright Epic Games, Inc. All Rights Reserved. #include "PickupBase.h" #include "ItemDefinition.h"
테이블 데이터로 픽업 초기화하기
픽업은 기본 액터에 불과하므로 게임이 시작되면 제대로 작동하는 데 필요한 데이터를 제공해야 합니다. 픽업은 데이터 테이블에서 값 행을 가져와서 해당 값을 ItemDefinition 데이터 에셋('레퍼런스 아이템')에 저장해야 합니다.
데이터 테이블에서 데이터 가져오기
PickupBase.h의 public 섹션에서 새로운 void 함수 InitializePickup()을 선언합니다. 이 함수를 사용하여 데이터 테이블의 값으로 픽업을 초기화할 수 있습니다.
// Initializes this pickup with values from the data table.
void InitializePickup();테이블에서 데이터를 가져오려면 픽업 블루프린트에 데이터 테이블(Data Table) 에셋과 앞서 아이템 ID와 똑같이 구성한 행 이름(Row Name)이라는 두 가지 프로퍼티가 필요합니다.
protected 섹션에서 PickupItemID라는 이름의 FName 프로퍼티를 선언합니다. 그 프로퍼티에 EditInstanceOnly 및 Category = "Pickup | Item Table" 지정자를 지정합니다. 이 지정자는 연결된 데이터 테이블에서 이 픽업의 ID입니다.
// The ID of this pickup in the associated data table.
UPROPERTY(EditInstanceOnly, Category = "Pickup | Item Table")
FName PickupItemID;픽업에는 디폴트 아이템 ID가 없어야 하므로 EditInstanceOnly 지정자를 사용하면 월드의 픽업 인스턴스에서는 이 프로퍼티를 편집할 수 있지만, 아키타입(또는 클래스 디폴트)에서는 편집할 수 없습니다.
Category 텍스트에서 세로 막대(|)는 중첩된 서브섹션을 생성합니다. 따라서 이 예시에서 언리얼 엔진은 에셋의 디테일(Details) 패널에 아이템 테이블(Item Table)이라는 이름의 서브섹션이 있는 픽업(Pickup) 섹션을 생성합니다.
그런 다음, PickupDataTable이라는 이름으로 UDataTable에 TSoftObjectPtr을 선언합니다. PickupItemID와 동일한 지정자를 지정합니다. 이 지정자는 픽업이 데이터를 가져오는 데 사용하는 데이터 테이블입니다.
이 데이터 테이블은 런타임에 로드되지 않을 수 있으므로 비동기적으로 로드할 수 있도록 여기에서는 TSoftObjectPtr을 사용합니다.
헤더 파일을 저장하고 PickupBase.cpp로 전환하여 InitializePickup()을 구현합니다.
함수 내부에서 제공된 PickupDataTable이 유효한지, PickupItemID에 값이 있는지 if 구문에서 확인합니다.
/**
* Initializes the pickup with default values by retrieving them from the associated data table.
*/
void APickupBase::InitializePickup()
{
if (PickupDataTable && !PickupItemID.IsNone())
{
}
}if 구문에 데이터 테이블에서 값 행을 얻는 코드를 추가합니다. ItemDataRow라는 이름으로 const FItemData 포인터를 선언하고 PickupDataTable에서 FindRow()를 호출한 결과로 설정합니다. FItemData를 찾을 행 타입으로 지정합니다.
const FItemData* ItemDataRow = PickupDataTable->FindRow<FItemData>();FindRow()는 다음 두 개의 실행인자를 받습니다.
찾으려는
FName행 이름.PickupItemID를 행 이름으로 전달합니다.행을 찾을 수 없는 경우. 디버깅에 사용할 수 있는
FString타입 컨텍스트 스트링.Text("내 컨텍스트입니다.")를 사용하여 컨텍스트 스트링을 추가하거나,ToString()을 사용하여 아이템 ID를 컨텍스트 스트링으로 변환할 수 있습니다.
if (PickupDataTable && !PickupItemID.IsNone())
{
// Retrieve the item data associated with this pickup from the Data Table
const FItemData* ItemDataRow = PickupDataTable->FindRow<FItemData>(PickupItemID, PickupItemID.ToString());
}레퍼런스 아이템 생성하기
픽업의 행 데이터를 얻은 후에는 그 정보를 저장할 데이터 에셋 타입 ReferenceItem을 생성하고 초기화합니다.
이렇게 레퍼런스 아이템에 데이터를 저장하면 언리얼 엔진은 아이템에 대해 알아야 할 때 해당 데이터를 쉽게 참조할 수 있으며, 더 비효율적인 테이블 데이터 룩업을 덜 수행해도 됩니다.
PickupBase.h의 protected 섹션에서 ReferenceItem이라는 이름의 UItemDefinition에 대한 TObjectPtr을 선언합니다. 이는 픽업의 데이터를 저장하는 데이터 에셋입니다. 여기에 VisibleAnywhere 및 Category = "Pickup | Reference Item" 지정자를 지정합니다.
// Data asset associated with this item.
UPROPERTY(VisibleAnywhere, Category = "Pickup | Reference Item")
TObjectPtr<UItemDefinition> ReferenceItem;헤더 파일을 저장하고 다시 PickupBase.cpp로 전환합니다.
InitializePickup()에서 FindRow() 호출 후 UItemDefinition 타입의 NewObject에 ReferenceItem을 설정합니다.
언리얼 엔진에서 NewObject<T>()는 런타임에 UObject에서 파생된 인스턴스를 동적으로 생성하기 위한 템플릿화된 함수입니다. 이 함수는 새 오브젝트에 포인터를 반환합니다. 보통 다음과 같은 구문을 사용합니다.
T* Object = NewObject<T>(Outer, Class);
여기서 T는 생성하는 UObject의 타입, Outer는 이 오브젝트의 소유자, 그리고 Class는 생성하는 오브젝트의 클래스입니다. Class 실행인자는 종종 T::StaticClass()가 되는데, 이는 T의 클래스 타입을 나타내는 UClass 포인터를 제공합니다. 하지만, UE는 Outer가 현재 클래스라고 가정하고 T를 사용하여 UClass를 추론하기 때문에 보통은 두 실행인자를 모두 생략할 수 있습니다.
this를 외부 클래스로 전달하고 UItemDefinition::StaticClass()를 클래스 타입으로 전달하여 베이스 UItemDefinition을 생성합니다.
ReferenceItem = NewObject<UItemDefinition>(this, UItemDefinition::StaticClass());픽업의 정보를 ReferenceItem에 복사하려면 ReferenceItem의 각 필드를 ItemDataRow의 연결된 필드로 설정합니다. WorldMesh의 경우, ItemDataRow에서 참조되는 ItemBase에서 WorldMesh 프로퍼티를 가져옵니다.
ReferenceItem = NewObject<UItemDefinition>(this, UItemDefinition::StaticClass());
ReferenceItem->ID = ItemDataRow->ID;
ReferenceItem->ItemType = ItemDataRow->ItemType;
ReferenceItem->ItemText = ItemDataRow->ItemText;
ReferenceItem->WorldMesh = ItemDataRow->ItemBase->WorldMesh;InitializePickup() 호출
BeginPlay()에서 게임이 시작될 때 InitializePickup()을 호출하여 픽업을 초기화합니다.
// Called when the game starts or when spawned
void APickupBase::BeginPlay()
{
Super::BeginPlay();
// Initialize this pickup with default values
InitializePickup();
}파일을 저장합니다. 이제 PickupBase.cpp는 다음과 같은 모습이어야 합니다.
// Copyright Epic Games, Inc. All Rights Reserved.
#include "PickupBase.h"
// Sets default values
APickupBase::APickupBase()
{
// Set this actor to call Tick() every frame.
PrimaryActorTick.bCanEverTick = true;
}
게임 내 함수 기능 생성하기
픽업에 필요한 아이템 데이터는 갖췄지만, 아직 게임 월드에서 표시되고 작동하는 방식을 설정해야 합니다. 플레이어가 볼 수 있는 메시, 플레이어가 접촉할 때를 결정하는 콜리전 볼륨, 플레이어가 아이템을 집었을 때처럼 픽업이 사라졌다가 일정 시간이 지나면 리스폰되도록 하는 로직이 필요합니다.
메시 컴포넌트 추가하기
1인칭 카메라, 메시 및 애니메이션 추가에서 플레이어 캐릭터를 설정할 때와 마찬가지로 CreateDefaultSubobject 템플릿 함수를 사용하여 스태틱 메시 오브젝트를 픽업 클래스의 자손 컴포넌트로 생성한 다음 이 컴포넌트에 아이템의 메시를 적용합니다.
PickupBase.h의 protected 섹션에서 PickupMeshComponent라는 이름의 UStaticMeshComponent에 TObjectPtr을 선언합니다. 이는 월드에서 픽업을 표현할 메시입니다.
코드를 사용하여 데이터 에셋의 메시를 이 프로퍼티에 할당할 것이므로, 언리얼 에디터에서 보이기는 하지만 편집할 수 없도록 VisibleDefaultsOnly 및 Category = "Pickup | Mesh" 지정자를 지정합니다.
// The mesh component to represent this pickup in the world.
UPROPERTY(VisibleDefaultsOnly, Category = "Pickup | Mesh")
TObjectPtr<UStaticMeshComponent> PickupMeshComponent;헤더 파일을 저장하고 PickupBase.cpp로 전환합니다.
APickupBase 생성 함수에서 PickupMeshComponent 포인터를 UStaticMeshComponent 타입의 CreateDefaultSubobject()를 호출한 결과로 설정합니다. Text 실행인자에서 오브젝트의 이름을 "PickupMesh"로 지정합니다.
그런 다음, 메시가 제대로 인스턴스화되었는지 확인하기 위해 PickupMeshComponent가 null이 아닌지 확인합니다.
// Sets default values
APickupBase::APickupBase()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
// Create this pickup's mesh component
PickupMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("PickupMesh"));
check(PickupMeshComponent != nullptr);
}InitializePickup()의 구현으로 이동합니다.
픽업의 메시 컴포넌트에 WorldMesh를 적용하기 전에, 메시 컴포넌트를 TSoftObjectPtr로 정의했기 때문에 메시가 로드되었는지 확인해야 합니다.
먼저 TempItemDefinition이라는 이름으로 새 UItemDefinition 포인터를 선언하고 ItemDataRow->ItemBase.Get()을 호출한 결과로 설정합니다.
UItemDefinition* TempItemDefinition = ItemDataRow->ItemBase.Get();그런 다음, if 구문에서 WorldMesh.IsValid()를 호출하여 WorldMesh가 지금 로드되었는지 확인합니다.
// Check if the mesh is currently loaded by calling IsValid().
if (TempItemDefinition->WorldMesh.IsValid()) {
}로드되었다면 SetStaticMesh()를 호출하여 PickupMeshComponent를 설정하고, Get()으로 WorldMesh를 얻습니다.
// Check if the mesh is currently loaded by calling IsValid().
if (TempItemDefinition->WorldMesh.IsValid()) {
// Set the pickup's mesh to the associated item's mesh
PickupMeshComponent->SetStaticMesh(TempItemDefinition->WorldMesh.Get());
}메시가 로드되지 않은 경우, 메시에서 LoadSynchronous()를 호출하여 강제로 로드합니다. 이 함수는 해당 오브젝트에 대한 에셋 포인터를 로드하고 반환합니다.
if 구문 뒤에 있는 else 구문에서 WorldMesh라는 이름으로 새 UStaticMesh 포인터를 선언하고 WorldMesh.LoadSynchronous()를 호출하여 설정합니다.
그런 다음, SetStaticMesh()를 사용하여 PickupMeshComponent를 설정합니다.
else {
// If the mesh isn't loaded, load it by calling LoadSynchronous().
UStaticMesh* WorldMesh = TempItemDefinition->WorldMesh.LoadSynchronous();
PickupMeshComponent->SetStaticMesh(WorldMesh);
}else 구문 뒤에 SetVisiblity(true)를 사용하여 PickupMeshComponent를 보이게 합니다.
// Set the mesh to visible.
PickupMeshComponent->SetVisibility(true);콜리전 셰이프 추가하기
콜리전 볼륨 역할을 할 스피어 컴포넌트를 추가한 다음, 해당 컴포넌트에서 콜리전 쿼리를 활성화합니다.
PickupBase.h의 protected 섹션에서 SphereComponent라는 이름으로 USphereComponent에 TObjectPtr을 선언합니다. 이는 콜리전 탐지에 사용되는 스피어 컴포넌트입니다. 여기에 EditAnywhere, BlueprintReadOnly 및 Category = "Pickup | Components" 지정자를 지정합니다.
// Sphere Component that defines the collision radius of this pickup for interaction purposes.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Pickup | Components")
TObjectPtr<USphereComponent> SphereComponent;헤더 파일을 저장하고 PickupBase.cpp로 전환합니다.
APickupBase 생성 함수에서 PickupMeshComponent를 설정한 후, 타입은 USphereComponent를, 이름은 "SphereComponent"를 사용하여 CreateDefaultSubobject의 호출 결과로 SphereComponent를 설정합니다. 나중에 null check를 추가합니다.
// Create this pickup's sphere component
SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent"));
check(SphereComponent != nullptr);이제 두 컴포넌트를 모두 설정했으니 SetupAttachment()를 사용하여 PickupMeshComponent를 SphereComponent에 붙입니다.
// Attach the sphere component to the mesh component
SphereComponent->SetupAttachment(PickupMeshComponent);SphereComponent를 MeshComponent에 붙인 다음 SetSphereRadius()를 사용하여 스피어의 크기를 설정합니다. 이 값은 픽업 콜라이더가 충돌할 수 있을 만큼 커야 하지만, 캐릭터가 실수로 부딪힐 정도로 커서는 안 됩니다.
// Set the sphere's collision radius
SphereComponent->SetSphereRadius(32.f);InitializePickup()에서 SetVisibility(true) 줄 뒤에 SetCollisionEnabled()를 호출하여 SphereComponent가 충돌할 수 있게 만듭니다.
이 함수는 엔진에 어떤 타입의 콜리전을 사용할지 알려주는 열거형(ECollisionEnabled)을 받습니다. 캐릭터가 픽업과 충돌하고 콜리전 쿼리를 트리거할 수 있어야 하지만, 충돌 시 튕겨나가는 피직스가 픽업에 있으면 안 되므로 ECollisionEnabled::QueryOnly 옵션을 전달합니다.
// Set the mesh to visible and collidable.
PickupMeshComponent->SetVisibility(true);
SphereComponent->SetCollisionEnabled(ECollisionEnabled::QueryOnly); 이제 PickupBase.cpp가 다음과 같아야 합니다.
// Copyright Epic Games, Inc. All Rights Reserved.
#include "PickupBase.h"
#include "ItemDefinition.h"
// Sets default values
APickupBase::APickupBase()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
픽업 콜리전 시뮬레이션
이제 픽업에 콜리전 셰이프가 생겼으니, 픽업과 플레이어 간의 콜리전을 탐지하고 충돌 시 픽업이 사라지도록 하는 로직을 추가합니다.
콜리전 이벤트 구성하기
PickupBase.h의 protected 섹션에서 OnSphereBeginOverlap()이라는 이름의 void 함수를 선언합니다.
USphereComponent와 같이 UPrimitiveComponent에서 상속하는 모든 컴포넌트는 이 함수를 구현하여 컴포넌트가 다른 액터와 오버랩될 때 코드를 실행할 수 있습니다. 이 함수는 사용하지 않을 파라미터를 몇 개 받으므로, 다음 파라미터만 전달합니다.
UPrimitiveComponent* OverlappedComponent: 오버랩된 컴포넌트입니다.AActor* OtherActor: 해당 컴포넌트와 오버랩되는 액터입니다.UPrimitiveComponent* OtherComp: 오버랩된 액터의 컴포넌트입니다.int32 OtherBodyIndex: 오버랩된 컴포넌트의 인덱스입니다.bool bFromSweep, const FHitResult& SweepResult: 콜리전이 발생한 위치와 각도 같은 콜리전에 대한 정보입니다.
// Code for when something overlaps the SphereComponent.
UFUNCTION()
void OnSphereBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);헤더 파일을 저장하고 PickupBase.cpp로 전환합니다.
언리얼 엔진의 콜리전 이벤트는 다이내믹 멀티캐스트 델리게이트를 사용하여 구현됩니다. UE에서 이 델리게이트 시스템은 하나의 오브젝트가 여러 함수를 동시에 호출할 수 있게 합니다. 마치 구독자가 메일링 리스트(즉 다른 함수)에 메시지를 브로드캐스팅하는 것과 비슷합니다. 델리게이트에 함수를 바인딩하는 것은 메일링 리스트에 함수를 구독시키는 것과 비슷합니다. '델리게이트'는 이벤트인데, 이 경우에는 플레이어와 픽업 간의 콜리전입니다. 이벤트가 발생하면 언리얼 엔진은 해당 이벤트에 바인딩된 모든 함수를 호출합니다.
언리얼 엔진에는 몇 가지 다른 바인딩 함수가 있지만, 델리게이트인 OnComponentBeginOverlap은 다이내믹 델리게이트이므로 AddDynamic()을 사용하는 것이 좋습니다. 그리고 UObject 클래스에서 UFUNCTION을 바인딩하는 것이므로 리플렉션 지원을 위해 AddDynamic()이 필요합니다. 다이내믹 멀티캐스트 델리게이트에 대한 자세한 내용은 멀티캐스트 델리게이트를 참조하세요.
PickupBase.cpp의 InitializePickup()에서 AddDynamic 매크로를 사용하여 OnSphereBeginOverlap()을 스피어 컴포넌트의 OnComponentBeginOverlap 이벤트에 바인딩합니다.
// Register the Overlap Event
SphereComponent->OnComponentBeginOverlap.AddDynamic(this, &APickupBase::OnSphereBeginOverlap);작업을 저장합니다. 이제 캐릭터가 픽업의 스피어 컴포넌트와 충돌할 때 OnSphereBeginOverlap()이 실행됩니다.
콜리전 후 픽업 숨기기
PickupBase.cpp에서 OnSphereBeginOverlap()을 구현하여 플레이어가 집어 든 것처럼 보이도록 픽업을 사라지게 만듭니다.
먼저, 디버그 메시지를 추가하여 이 함수가 트리거될 시점을 알립니다.
void APickupBase::OnSphereBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, TEXT("Attempting a pickup collision"));
}이 함수는 다른 액터가 픽업과 충돌할 때마다 실행되므로, 충돌하는 것이 1인칭 캐릭터인지 확인해야 합니다.
Character라는 이름의 새 AAdventureCharacter 포인터를 선언하고 Character 클래스 이름에 OtherActor를 캐스팅하여 설정합니다(이 튜토리얼에서는 AAdventureCharacter를 사용합니다).
// Checking if it's an AdventureCharacter overlapping
AAdventureCharacter* Character = Cast<AAdventureCharacter>(OtherActor);if 구문에서 Character가 null이 아닌지 확인합니다. Null은 캐스트에 실패했으며 OtherActor가 AAdventureCharacter 타입이 아님을 나타냅니다.
if 구문 내에서 이 함수가 반복적으로 트리거되지 않도록 RemoveAll()을 호출하여 이 함수에서 OnComponentBeginOverlap의 등록을 해제합니다. 이렇게 하면 콜리전이 종료됩니다.
if (Character != nullptr)
{
// Unregister from the Overlap Event so it is no longer triggered
SphereComponent->OnComponentBeginOverlap.RemoveAll(this);
}그런 다음, SetVisibility(false)를 사용하여 PickupMeshComponent를 보이지 않게 설정하고 SetCollisionEnabled()를 사용하여 픽업 메시와 스피어 컴포넌트를 모두 충돌 불가능으로 설정한 다음, NoCollision 옵션을 전달합니다.
if (Character != nullptr)
{
// Unregister from the Overlap Event so it is no longer triggered
SphereComponent->OnComponentBeginOverlap.RemoveAll(this);
// Set this pickup to be invisible and disable collision
PickupMeshComponent->SetVisibility(false);
PickupMeshComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
SphereComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}PickupBase.cpp를 저장합니다.
픽업 리스폰 만들기
이제 캐릭터가 픽업과 상호작용할 수 없으니, 일정 시간이 지나면 리스폰되도록 설정합니다.
PickupBase.h의 protected 섹션에서 bShouldRespawn이라는 이름의 bool을 선언합니다. 이 bool을 사용하여 리스폰을 켜거나 끌 수 있습니다.
RespawnTime이라는 이름의 float를 선언하고 4.0f로 초기화합니다. 이 float는 픽업이 리스폰될 때까지 기다려야 하는 시간입니다.
두 프로퍼티 모두에 EditAnywhere, BlueprintReadOnly 및 Category = "Pickup | Respawn" 지정자를 지정합니다.
// Whether this pickup should respawn after being picked up.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Pickup | Respawn")
bool bShouldRespawn;
// The time in seconds to wait before respawning this pickup.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Pickup | Respawn")
float RespawnTime = 4.0f;RespawnTimerHandle이라는 이름의 FTimerHandle을 선언합니다.
// Timer handle to distinguish the respawn timer.
FTimerHandle RespawnTimerHandle;언리얼 엔진에서 게임플레이 타이머는 FTimerManager가 처리합니다. 이 클래스에는 설정된 딜레이 후에 함수나 델리게이트를 호출하는 SetTimer() 함수가 포함되어 있습니다. FTimerManager의 함수는 FTimerHandle을 사용하여 함수를 시작, 일시정지, 재개 또는 무한 루프합니다. RespawnTimerHandle을 사용하여 픽업을 리스폰할 시점을 알립니다. 타이머 매니저(Timer Manager) 사용에 관한 자세한 내용은 게임플레이 타이머를 참조하세요.
헤더 파일을 저장하고 PickupBase.cpp로 전환합니다.
픽업 리스폰을 구현하려면 타이머 관리자를 사용하여 잠시 기다린 후 InitializePickup()을 호출하는 타이머를 설정합니다.
리스폰이 활성화된 경우에만 픽업을 리스폰해야 하므로 OnSphereBeginOverlap의 하단에 bShouldRespawn이 true인지 확인하는 if 구문을 추가합니다.
if (bShouldRespawn)
{
}이 if 구문에서 GetWorldTimerManager()를 사용하여 타이머 매니저를 가져오고 타이머 매니저에서 SetTimer()를 호출합니다. 이 함수는 다음 구문을 사용합니다.
SetTimer(InOutHandle, Object, InFuncName, InRate, bLoop, InFirstDelay);
축약어 뜻:
InOutHandle은 타이머를 제어하는FTimerHandle입니다(RespawnTimerHandle).Object는 호출하는 함수를 소유한UObject입니다. 이를 사용합니다.InFuncName은 호출하려는 함수에 대한 포인터입니다(이 경우InitializePickup()).InRate는 함수를 호출하기 전에 대기할 시간(초)을 지정하는 플로트 값입니다.bLoop는 타이머를Time초마다 반복(true)하거나 한 번만 실행(false)하도록 설정합니다.InFirstDelay(선택 사항)는 루핑 타이머에서 첫 번째 함수를 호출하기 전의 초기 타임 딜레이입니다. 지정하지 않으면 UE는InRate를 딜레이로 사용합니다.
InitializePickup()을 한 번만 호출하여 픽업을 교체해야 하므로 bLoop를 false로 설정합니다.
원하는 리스폰 시간을 설정합니다. 이 튜토리얼에서는 초기 딜레이 없이 4초 후에 픽업이 리스폰됩니다.
// If the pickup should respawn, wait an fRespawnTime amount of seconds before calling InitializePickup() to respawn it
if (bShouldRespawn)
{
GetWorldTimerManager().SetTimer(RespawnTimerHandle, this, &APickupBase::InitializePickup, RespawnTime, false, 0);
}완성된 OnSphereBeginOverlap() 함수는 다음과 같은 모습이어야 합니다.
/**
* Broadcasts an event when a character overlaps this pickup's SphereComponent. Sets the pickup to invisible and uninteractable, and respawns it after a set time.
* @param OverlappedComponent - the component that was overlapped.
* @param OtherActor - the Actor overlapping this component.
* @param OtherComp - the Actor's component that overlapped this component.
* @param OtherBodyIndex - the index of the overlapped component.
* @param bFromSweep - whether the overlap was generated from a sweep.
* @param SweepResult - contains info about the overlap such as surface normals and faces.
*/
void APickupBase::OnSphereBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
코드를 저장하고 Visual Studio에서 컴파일합니다.
레벨에 픽업 구현하기
픽업을 구성하는 코드를 정의했으니 이제 게임에서 테스트할 차례입니다!
레벨에 픽업을 추가하려면 다음 단계를 따릅니다.
언리얼 에디터 콘텐츠 브라우저의 에셋 트리에서 Content > FirstPerson > Blueprints로 이동합니다.
Blueprints 폴더에 픽업 클래스를 저장할 Pickups라는 이름의 자손 폴더를 새로 생성합니다.
에셋 트리에서 C++ Classes 폴더로 이동합니다. PickupBase 클래스를 우클릭하여 해당 클래스에서 블루프린트를 생성합니다.
이름을
BP_PickupBase로 지정합니다.경로는 Content/FirstPerson/Blueprints/Pickups를 선택하고 픽업 베이스 클래스 생성(Create Pickup Base Class)을 클릭합니다.
Blueprints > Pickups 폴더로 돌아갑니다.
BP_PickupBase블루프린트를 레벨로 드래그합니다. PickupBase의 인스턴스가 레벨에 나타나고 아웃라이너(Outliner) 패널에서 자동으로 선택됩니다. 하지만 여기에는 아직 메시가 없습니다.BP_PickupBase액터를 선택한 상태로 디테일 패널에서 다음 프로퍼티를 설정합니다.픽업 아이템 ID(Pickup Item ID)를
pickup_001로 설정합니다.픽업 데이터 테이블(Pickup Data Table)을
DT_PickupData로 설정합니다.리스폰 여부(Should Respawn)를 true로 설정합니다.
플레이(Play)를 클릭하여 게임을 테스트할 때, 픽업은 픽업 아이템 ID(Pickup Item ID)를 사용하여 데이터 테이블을 쿼리하고 pickup_001과 관련된 데이터를 얻습니다. 픽업은 테이블 데이터와 DA_Pickup_001 데이터 에셋에 대한 레퍼런스를 사용하여 레퍼런스 아이템을 초기화하고 해당 스태틱 메시를 로드합니다.
픽업 위로 지나가면 픽업이 사라졌다가 4초 후에 다시 나타나는 것을 볼 수 있습니다.
다른 픽업 로드하기
픽업 아이템 ID를 다른 값으로 설정하면 픽업이 대신 테이블의 해당 행에서 데이터를 얻습니다.
픽업 아이템 ID 전환을 실험하려면 다음 단계를 따릅니다.
DA_Pickup_002라는 이름의 새 데이터 에셋을 생성합니다. 이 에셋에 다음 프로퍼티를 설정합니다.
ID: pickup_002
아이템 타입(Item Type): Consumable
이름(Name): Test Name 2
설명(Description): Test Description 2
월드 메시(World Mesh):
SM_ChamferCube
DT_PickupData테이블에 새 행을 추가하고 새 행의 필드에 데이터 에셋의 정보를 입력합니다.BP_PickupBase액터에서 픽업 아이템 ID를pickup_002로 변경합니다.
플레이를 클릭하여 게임을 테스트하면 DA_Pickup_002의 값으로 픽업이 대신 스폰되어야 합니다!
에디터에서 픽업 액터 업데이트하기
픽업이 게임 내에서 작동하고 있지만, 디폴트 메시가 없기 때문에 에디터에서 시각화하기 어려울 수도 있습니다.
이 문제를 해결하기 위해 PostEditChangeProperty() 함수를 사용합니다. 이 함수는 에디터가 프로퍼티를 변경할 때 언리얼 엔진이 호출하는 에디터 내 함수이므로, 프로퍼티가 변경될 때 이 함수를 사용하여 뷰포트에서 액터를 최신 상태로 유지할 수 있습니다. 예를 들어, 플레이어의 디폴트 체력이 변경되면서 UI 엘리먼트를 업데이트하거나, 스피어가 원점에 다가가거나 멀어지는 것에 따라 구체의 크기를 스케일 조절할 수 있습니다.
이 프로젝트에서는 픽업 아이템 ID가 변경될 때마다 픽업의 새 스태틱 메시를 적용하는 데 사용할 것입니다. 이렇게 하면 플레이를 클릭할 필요 없이 픽업 타입을 변경하고 뷰포트에 즉시 업데이트되는 것을 확인할 수 있습니다!
픽업 관련 변경사항이 에디터에 즉시 표시되도록 하려면 다음 단계를 따릅니다.
PickupBase.h의protected섹션에서#if WITH_EDITOR매크로를 선언합니다. 이 매크로는 언리얼 헤더 툴에 툴 안의 모든 것을 에디터 빌드용으로만 패키지로 만들고, 게임의 출시 버전용으로 컴파일하지 말라고 지시합니다. 이 매크로는#endif문으로 끝냅니다.C++#if WITH_EDITOR #endif매크로 안에
PostEditChangeProperty()에 대한 가상 void 함수 오버라이드를 선언합니다. 이 함수는 변경된 프로퍼티, 변경 타입 등에 대한 정보를 포함하는FPropertyChangedEvent에 대한 레퍼런스를 받습니다.헤더 파일을 저장합니다.C++#if WITH_EDITOR // Runs whenever a property on this object is changed in the editor virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; #endifPickupBase.cpp에서PostEditChangeProperty()함수를 구현합니다. 먼저Super::PostEditChangeProperty()함수를 호출하여 부모 클래스 프로퍼티 변경사항을 처리합니다.C++/** * Updates this pickup whenever a property is changed. * @param PropertyChangedEvent - contains info about the property that was changed. */ void APickupBase::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { // Handle parent class property changes Super::PostEditChangeProperty(PropertyChangedEvent); }변경된 프로퍼티의 이름을 저장하기 위해
ChangedProperty라는 이름으로 새const FName변수를 생성합니다.C++// Handle parent class property changes Super::PostEditChangeProperty(PropertyChangedEvent); const FName ChangedPropertyName;PropertyChangedEvent에Property가 포함되어 있는지 검증하고 해당 프로퍼티를 저장하려면PropertyChangedEvent.Property를 조건으로 하는 삼항 연산자를 사용합니다. 조건이 true이면ChangedPropertyName을PropertyChangedEvent.Property->GetFName()으로 설정하고, false이면NAME_None으로 설정합니다.C++// If a property was changed, get the name of the changed property. Otherwise use none. const FName ChangedPropertyName = PropertyChangedEvent.Property ? PropertyChangedEvent.Property->GetFName() : NAME_None;NAME_None은 '유효한 이름 없음' 또는 'null 이름'을 의미하는FName타입의 글로벌 언리얼 엔진 상수입니다.이제 프로퍼티의 이름을 알았으니, ID가 변경된 것을 탐지하면 언리얼 엔진이 메시를 업데이트하도록 할 수 있습니다.
if구문에서ChangePropertyName이APickupBase클래스와PickupItemID를 전달하여GET_MEMBER_NAME_CHECKED()를 호출한 결과와 같은지 확인합니다. 이 매크로는 컴파일 시간 검사를 통해 전달한 프로퍼티가 전달했던 클래스에 존재하는지 확인합니다.데이터 테이블에서도 값을 검색하게 되므로
if문을 입력하기 전에 테이블이 유효한지도 확인합니다.C++// Verify that the changed property exists in this class and that the PickupDataTable is valid. if (ChangedPropertyName == GET_MEMBER_NAME_CHECKED(APickupBase, PickupItemID) && PickupDataTable) { }InitializePickup()에서와 동일한 방식으로if문 내에서FindRow를 호출하여 이 픽업과 관련된 데이터 행을 얻습니다.이번에는 계속하기 전에
PickupItemID가 테이블에 있는지 확인하기 위해 중첩된if구문에FindRow줄을 넣습니다.C++// Verify that the changed property exists in this class and that the PickupDataTable is valid. if (ChangedPropertyName == GET_MEMBER_NAME_CHECKED(APickupBase, PickupItemID) && PickupDataTable) { // Retrieve the associated ItemData for this pickup. if (const FItemData* ItemDataRow = PickupDataTable->FindRow<FItemData>(PickupItemID, PickupItemID.ToString())) { } }UE가 행 데이터를 찾았다면,
ItemDataRow에서 참조되는 새 메시가 포함된 데이터 에셋을 저장하기 위한TempItemDefinition변수를 생성합니다.C++if (const FItemData* ItemDataRow = PickupDataTable->FindRow<FItemData>(PickupItemID, PickupItemID.ToString())) { UItemDefinition* TempItemDefinition = ItemDataRow->ItemBase;메시를 업데이트하려면
PickupMeshComponent에서SetStaticMesh를 사용하여 임시 데이터 에셋의WorldMesh를 전달합니다.C++// Set the pickup's mesh to the associated item's mesh PickupMeshComponent->SetStaticMesh(TempItemDefinition->WorldMesh.Get());SetSphereRadius(32.f)를 사용하여 스피어 컴포넌트의 콜리전 반경을 설정합니다.C++// Set the sphere's collision radius SphereComponent->SetSphereRadius(32.f);코드를 저장하고 Visual Studio에서 컴파일합니다.
완성된 PostEditChangeProperty() 함수는 다음과 같은 모습이어야 합니다.
/**
* Updates this pickup whenever a property is changed.
* @param PropertyChangedEvent - contains info about the property that was changed.
*/
void APickupBase::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
// Handle parent class property changes
Super::PostEditChangeProperty(PropertyChangedEvent);
// If a property was changed, get the name of the changed property. Otherwise use none.
에디터로 돌아와 아웃라이너에서 BP_PickupBase 액터가 선택되어 있는지 확인합니다. 픽업 아이템 ID를 pickup_001에서 pickup_002로 변경했다가 다시 원래대로 변경합니다. ID를 변경하면 뷰포트에서 픽업의 메시가 업데이트됩니다.
다른 메시를 실험하는 경우, 게임을 한 번 플레이하여 새 메시가 완전히 로드되도록 해야 새 메시가 에디터 뷰포트에 표시될 것입니다.
다음 순서
다음으로, 픽업 클래스를 더 확장하여 커스텀 도구를 만들어 캐릭터에 장착해 보겠습니다!
완성된 코드
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Components/SphereComponent.h"
#include "CoreMinimal.h"
#include "AdventureCharacter.h"
#include "GameFramework/Actor.h"
#include "PickupBase.generated.h"
// Copyright Epic Games, Inc. All Rights Reserved.
#include "PickupBase.h"
#include "ItemDefinition.h"
// Sets default values
APickupBase::APickupBase()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;