분수대 액터는 분수대 구조물의 외관과 충돌을 담당할 스태틱메시 컴포넌트와 물의 외관을 담당할 스태틱메시 컴포넌트로 구성됨.
이때 분수대 구조물 스태틱메시 컴포넌트를 루트 컴포넌트로 지정함.
C++에서 액터가 두 개의 스태틱메시 컴포넌트를 가지려면 분수대 액터의 멤버 변수로 두 개의 UStaticMeshComponent 클래스의 포인터를 선언해주어야 함.
참고) 필자는 분수대 C++ 클래스의 이름을 Fountain 으로 명명하였음.
🚨하단부의 내용이 이해가 안될 경우 Chapter 1. 개발 환경 설정 참조
UStaticMeshComponent 클래스는 포인터로 선언했으므로 구현부에서 메모리를 동적으로 할당해 대입할 것임.
이때 메모리 관리를 위해 매번 포인터를 직접 소멸시키는 것은 매우 성가신 일인데
언리얼 엔진은 언리얼 실행 환경을 통해 더 이상 사용되지 않는 객체에 할당된 메모리를 자동으로 소멸시키는 기능을 제공함.
이 기능을 사용하려면 멤버 변수 선언부 상단에 UPROPERTY 매크로를 작성해주면 됨.
매크로 선언을 누락할 경우 메모리 누수가 발생해 원인을 파악하기 힘든 에러가 발생하므로 항상 잊지 않고 넣어주도록 주의해야 함.
이때 UPROPERTY 매크로 안에 VisibleAnywhere 키워드를 추가하면 언리얼 에디터 내부의 디테일 창에서 컴포넌트의 속성 편집이 가능해짐.
최종적인 Fountain.h 헤더 파일의 코드는 아래와 같음.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "EngineMinimal.h"
#include "GameFramework/Actor.h"
#include "Fountain.generated.h"
UCLASS()
class ARENABATTLE_API AFountain : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AFountain();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* Body;
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* Water;
};
자동 생성된 Fountain.h 헤더 파일에서
#include "CoreMinimal.h"
를
#include "EngineMinimal.h"
로 변경해주고
UPROPERTY()
UStaticMeshComponent* Body;
UPROPERTY()
UStaticMeshComponent* Water;
위의 내용을 public 멤버 변수 선언부에 추가해주기만 하면 됨.
UPROPERTY() 매크로를 모든 객체에서 사용할 수 있는 것은 아님.
이 기능은 언리얼 오브젝트라고 하는 객체에서만 사용 가능함.
언리얼 오브젝트는 언리얼 실행 환경에 의해 관리되는 C++ 객체를 의미함.
콘텐츠를 구성하는 객체는 모두 언리얼 오브젝트임.
클래스 선언 매크로: 클래스가 언리얼 오브젝트임을 선언하기 위한 매크로.
클래스 선언부 윗줄에 UCLASS라는 매크로를 선언하고
클래스 내부에는 GENERATED_BODY 매크로를 선언함.
클래스 이름 접두사: 항상 규칙에 맞는 접두사가 붙어야 함.
액터 클래스에는 A 접두사, 액터가 아닌 클래스에는 U 접두사가 붙음.
분수대는 액터이므로 AFountain, 액터의 구성 요소인 스태틱메시 컴포넌트는 액터가 아니므로 UStaticMeshComponent라는 클래스 이름을 가져야 함.
generated.h 헤더 파일: 소스 코드를 컴파일하기 이전에 언리얼 엔진은 언리얼 헤더 툴(Unreal Header Tool)을 사용해 클래스 선언을 분석하고 언리얼 실행 환경에 필요한 부가 정보를 별도의 파일에 생성함.
이때 언리얼 헤더 툴에 의해 자동으로 생성되는 부가 파일이 generated.h 헤더 파일임.
외부 모듈에의 공개 여부: 윈도우의 DLL 시스템은 DLL 내 클래스 정보를 외부에 공개할지 결정하는 _declspec(dllexport) 키워드를 제공함.
언리얼 엔진에서 이 키워드를 사용하려면 모듈명_API 키워드를 클래스 선언 앞에 추가해야 함. 이 키워드가 없으면 다른 모듈에서 해당 객체에 접근 불가함.
앞에서 분수대 액터의 선언을 위해 Fountain.h 헤더 파일을 수정함.
Fountain.cpp 파일은 분수대 액터의 정의와 관련된 파일임.
이 파일에서는 스태틱메시 컴포넌트를 실제로 생성하는 로직을 구현함.
생성자 코드에서 컴포넌트를 생성하는 용도로 언리얼 엔진은 new가 아닌 CreateDefaultSubobject API 함수 를 제공함.
분수대 액터의 구축은 AFountain 클래스의 생성자 코드에서 CreateDefaultSubobject API 함수 를 사용하여 진행함.
또한 두 컴포넌트를 생성하기 때문에 그 중 액터를 대표할 루트 컴포넌트를 지정해야 함. 위에서 분수대 구조물을 루트 컴포넌트로 지정하기로 했기 때문에 Body 컴포넌트 루트 컴포넌트로 지정하는 코드도 작성해주어야 함.
최종적인 Fountain.cpp 파일의 코드는 아래와 같음.
// Fill out your copyright notice in the Description page of Project Settings.
#include "Fountain.h"
// Sets default values
AFountain::AFountain()
{
// 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;
Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BODY"));
Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WATER"));
}
// Called when the game starts or when spawned
void AFountain::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AFountain::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
자동 생성된 Fountain.cpp 파일에서
Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BODY"));
Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Water"));
RootComponent = Body;
Water->SetupAttachment(Body);
위의 내용을 AFountain 생성자 내부에 추가해주고 프로젝트 파일을 빌드해주면 됨.
CreateDefaultSubobject API에 사용하는 문자열 값은 액터에 속한 컴포넌트를 구별하기 위한 Hash 값 생성에 사용됨.
따라서 문자열 값은 다른 컴포넌트와 중복되지 않는 한 어떤 값을 넣어도 상관없음.
언리얼 엔진은 문자열을 생성할 때 모든 플랫폼에서 동일하게 2바이트 문자열 체계를 유지시키는 TEXT 매크로를 제공함.

빌드가 완료됐다면 위에 첨부된 사진과 같이 언리얼 에디터에서 콘텐츠 브라우저>C++ 클래스>Arena Battle 폴더로 이동 후 Fountain 액터를 뷰포트로 드래그해 분수대 액터를 생성하면 됨.
분수대 액터가 정상적으로 생성된 후 뷰포트에 뭔가가 보이진 않지만 아웃라이너 창과 디테일 창을 확인해보면 아래의 사진처럼 Fountain1 액터, 그리고 Body와 Water 스태틱메시 컴포넌트가 생성되어 있는 것을 확인할 수 있음.

이후 분수대 액터의 루트 컴포넌트인 Body를 선택하고 아래 사진처럼 스태틱메시 섹션의 드롭다운 버튼을 눌러 SM_Plains_Castle_Fountain_01 애셋을 선택해주면 분수대가 뷰포트 창에서 보이게 됨.

동일한 방법으로 Water 컴포넌트의 스태틱메시를 선택 후 드롭다운에서 SM_Plains_Fountain_02 애셋을 선택한 뒤에 트랜스폼의 위치 Z값을 135로 변경하면 아래 사진처럼 물이 흐르는 분수대가 완성됨.

Z값을 변경하고 난 후 오른쪽에 생성되는 화살표를 누르면 Z값이 변경하기 이전의 값으로 되돌아감.
분수대를 바닥면에 붙이려면 뷰포트에서 분수대 액터를 선택 후 End 키를 누르면 됨. 이때 분수대가 바닥보다 낮은 위치에 있으면 아무런 변화가 없음.
액터의 생성자에서 Water 컴포넌트에 SetRelativeLocation을 사용하면 루트 컴포넌트인 Body에 대하여 Water 컴포넌트의 상대적 위치를 결정해줄 수 있음.
위치의 좌표값은 언리얼 엔진이 제공하는 구조체인 FVector를 사용하여 전달함.
Water->SetRelativeLocation(FVector(0.0f, 0.0f, 135.0f));
위의 코드를 생성자 내부에 추가해주면 됨.
언리얼 오브젝트의 생성자 코드에서 설정한 값은 언리얼 오브젝트의 기본값이 됨.
✅ 클래스 이름에 붙은 F 접두사는 언리얼 오브젝트와 관련없는 일반 C++ 클래스 혹은 구조체를 의미함.
조명과 이펙트는 각각 UPointLightComponent와 UParticleSystemComponent 클래스를 사용하면 됨.
Body와 Water를 선언할 때와 같이 해당 클래스 포인터로 선언하고
윗줄에 UPROPERTY(VisibleAnywhere) 키워드를 헤더 파일에 추가해주면 됨.
최종적인 Fountain.h 헤더 파일의 코드는 아래와 같음.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "EngineMinimal.h"
#include "GameFramework/Actor.h"
#include "Fountain.generated.h"
UCLASS()
class ARENABATTLE_API AFountain : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AFountain();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* Body;
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* Water;
UPROPERTY(VisibleAnywhere)
UPointLightComponent* Light;
UPROPERTY(VisibleAnywhere)
UParticleSystemComponent* Splash;
};
생성자 코드(Fountain.cpp 파일)에서도 동일하게
CreateDefaultSubobject 함수로 컴포넌트를 생성해주고
SetupAttachment 함수를 사용해 루트 컴포넌트의 자식이 되도록 계층 구조를 설정해준 후
조명과 이펙트의 위치를 조정해주면 됨.
최종적인 Fountain.cpp 파일의 코드는 아래와 같음.
// Fill out your copyright notice in the Description page of Project Settings.
#include "Fountain.h"
// Sets default values
AFountain::AFountain()
{
// 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;
Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BODY"));
Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WATER"));
Light = CreateDefaultSubobject<UPointLightComponent>(TEXT("LIGHT"));
Splash = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("SPLASH"));
RootComponent = Body;
Water->SetupAttachment(Body);
Light->SetupAttachment(Body);
Splash->SetupAttachment(Body);
Water->SetRelativeLocation(FVector(0.0f, 0.0f, 135.0f));
Light->SetRelativeLocation(FVector(0.0f, 0.0f, 195.0f));
Splash->SetRelativeLocation(FVector(0.0f, 0.0f, 195.0f));
}
// Called when the game starts or when spawned
void AFountain::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AFountain::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}

이후 언리얼 에디터에서 Splash를 선택한 후
파티클>템플릿에서 P_Water_Fountain_Splash_Base_01 애셋을 지정해주면 물이 찰랑이는 효과가 부여됨.
애셋 지정을 언리얼 에디터가 아닌 C++ 코드에서 하면 애셋이 자동으로 로딩되도록 할 수 있음.
언리얼 에디터에서 콘텐츠 브라우저>InfinityBladeGrassLands 폴더를 선택한 후
검색어로 Fountain을 입력해주면 아래 사진과 같아짐.

이제 분수대에 사용한 스태틱메시 애셋을 우클릭해 StaticMesh.h 열기 메뉴를 눌러줌.

C++ 코드에서 애셋을 불러들이려면 애셋의 키 값을 파악하고 애셋 관리 시스템에 키 값을 입력해 애셋의 포인터를 가져와야 함.
언리얼 엔진은 애셋의 키 값을 경로 값으로 사용하며, 애셋 위에 마우스를 올려 확인 가능함.
경로 값을 이용하려면 애셋을 우클릭한 후 레퍼런스 복사 메뉴를 선택하면 됨.
혹은 애셋을 선택한 후 Ctrl + C 단축키를 눌러도 무방함.
이후 복사한 레퍼런스를 이용해 아래의 코드를 AFountain 생성자 코드 내에 작성해주면 됨.
ConstructorHelpers::FObjectFinder<UStaticMesh>
SM_BODY(TEXT("/Script/Engine.StaticMesh'/Game/InfinityBladeGrassLands/Environments/Plains/Env_Plains_Ruins/StaticMesh/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01'"));
위의 코드에서 ConstructorHelpers::FObjectFinder는 ConstructorHelpers 클래스의 FObjectFinder를 사용해 변수를 선언한다는 의미임. 이 변수에 경로 값을 전달하면 됨.
또한 이 변수는 스태틱메시 애셋의 포인터이므로 이를 스태틱메시 컴포넌트의 SetStaticMesh 함수에 전달해주면 기능이 완성됨.
마지막으로 애셋의 경로 정보는 게임 실행 중에 변경되지 않으므로 static으로 선언해 여러 번 초기화되는 것을 막는 것이 바람직함.
이 모든 사항을 반영한 최종적인 Fountain.cpp 파일 코드는 아래와 같음.
// Fill out your copyright notice in the Description page of Project Settings.
#include "Fountain.h"
// Sets default values
AFountain::AFountain()
{
// 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;
Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BODY"));
Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WATER"));
Light = CreateDefaultSubobject<UPointLightComponent>(TEXT("LIGHT"));
Splash = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("SPLASH"));
RootComponent = Body;
Water->SetupAttachment(Body);
Light->SetupAttachment(Body);
Splash->SetupAttachment(Body);
Water->SetRelativeLocation(FVector(0.0f, 0.0f, 135.0f));
Light->SetRelativeLocation(FVector(0.0f, 0.0f, 195.0f));
Splash->SetRelativeLocation(FVector(0.0f, 0.0f, 195.0f));
static ConstructorHelpers::FObjectFinder<UStaticMesh>
SM_BODY(TEXT("/Script/Engine.StaticMesh'/Game/InfinityBladeGrassLands/Environments/Plains/Env_Plains_Ruins/StaticMesh/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01'"));
if (SM_BODY.Succeeded())
{
Body->SetStaticMesh(SM_BODY.Object);
}
static ConstructorHelpers::FObjectFinder<UStaticMesh>
SM_WATER(TEXT("/Script/Engine.StaticMesh'/Game/InfinityBladeGrassLands/Effects/FX_Meshes/Env/SM_Plains_Fountain_02.SM_Plains_Fountain_02'"));
if (SM_WATER.Succeeded())
{
Water->SetStaticMesh(SM_WATER.Object);
}
static ConstructorHelpers::FObjectFinder<UParticleSystem>
PS_SPLASH(TEXT("/Script/Engine.ParticleSystem'/Game/InfinityBladeGrassLands/Effects/FX_Ambient/Water/P_Water_Fountain_Splash_Base_01.P_Water_Fountain_Splash_Base_01'"));
if (PS_SPLASH.Succeeded())
{
Splash->SetTemplate(PS_SPLASH.Object);
}
}
// Called when the game starts or when spawned
void AFountain::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AFountain::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
언리얼 오브젝트의 속성 값은 객체를 관리하는 객체 유형과 값을 관리하는 값 유형으로 나뉨.
바이트: uint8정수: int32실수: float문자열: FString, FName구조체: FVector, FRotator, FTransform값 유형으로 클래스 멤버 변수를 선언하고 UPROPERTY 매크로를 설정해주면 매크로 선언과 동시에 미리 예약된 기본값이 지정됨.
예를 들어 정수 유형의 멤버 변수에 UPROPERTY 매크로 선언 시 초기 값으로 0이 할당됨.
값 유형과 객체 유형 모두 VisibleAnywhere 키워드를 사용하면 해당 속성의 데이터를 변경할 수 없음.
그러나 객체 유형의 경우, 객체에 속한 속성들은 에디터에서 편집할 수 있음.
만약 언리얼 에디터에서 객체의 속성 데이터를 변경하고 싶다면 UPROPERTY(VisibleAnywhere) 대신 UPROPERTY(EditAnywhere)의 형태로 EditAnywhere 키워드를 사용하면 됨.
본 게시글은 '이득우의 언리얼 C++ 게임 개발의 정석' 교재를 참고하여 정리하였음을 알립니다.
본 작성자는 저작권법을 준수하기 위해 최선을 다할 것입니다.
적법하지 않은 내용이 포함되어 있을 경우 작성자에게 알려주시면 시정하겠습니다.
향후 별도의 안내 없이 게시글 내용 일부 수정 혹은 비공개 조치될 수 있음에 양해의 말씀 드립니다.