오늘은 언리얼 엔진 C++의 핵심인 액터(Actor)에 대해 깊이 파고들었다. 단순히 C++ 클래스를 생성하는 것에서 그치지 않고, 눈에 보이도록 컴포넌트(Component)를 추가하고, 액터의 생성부터 소멸까지의 과정인 라이프사이클(Lifecycle)을 이해했다. 마지막으로 Tick 함수와 델타 타임(Delta Time)을 활용해 액터를 부드럽게 회전시키는 방법까지 실습하며 액터의 전반적인 흐름을 익혔다. 🤩
언리얼 엔진에서 월드(레벨)에 배치할 수 있는 모든 오브젝트를 "액터(Actor)"라고 한다. 캐릭터, 아이템, 건축물 모두 액터다. 하지만 C++로 액터 클래스를 처음 생성하고 월드에 배치하면 투명하고, 위치도 월드 원점(0,0,0)에 고정되어 제대로 제어할 수 없다.
이유는 액터가 '껍데기'에 불과하기 때문이다. 👽 실제 기능과 형태는 "컴포넌트(Component)"라는 부품을 조립해서 완성된다.
이 부품들을 C++ 코드로 직접 조립하는 과정을 진행했다.
액터는 사람처럼 태어나고, 살아가고, 죽는 생명 주기(Lifecycle)를 가진다. 각 단계마다 특정 함수들이 자동으로 호출된다.
생성자 (Constructor): 메모리에 생성될 때 단 한 번 호출. 컴포넌트를 생성하고 부착하는 일을 한다.BeginPlay(): 액터가 월드에 완전히 배치되고 게임이 시작될 때 호출. 다른 액터와 상호작용하는 로직을 넣기 좋다.Tick(): 게임이 실행되는 동안 매 프레임마다 호출된다. 캐릭터 이동, 회전처럼 지속적인 업데이트가 필요할 때 사용한다. (성능 저하의 주범이 될 수 있어 조심해야 한다! ⚠️)EndPlay(): 액터가 월드에서 사라지는 모든 경우(파괴, 레벨 변경 등)에 호출. 마무리 작업을 하기에 적합하다.이 순서를 UE_LOG로 직접 찍어보니 개념이 확실하게 잡혔다.
Tick 함수는 컴퓨터 성능에 따라 1초에 호출되는 횟수(FPS)가 다르다. 좋은 컴퓨터는 1초에 120번, 안 좋은 컴퓨터는 30번 호출될 수 있다. 만약 Tick 함수에서 매번 1도씩 회전시키면, 누구는 1초에 120도를, 누구는 30도를 돌게 된다.
델타 타임(DeltaTime)은 바로 이전 프레임부터 현재 프레임까지 걸린 '시간'이다. 이 값을 곱해주면 프레임이 아니라 '시간'에 기반하여 계산되므로, 모든 컴퓨터에서 동일한 속도로 움직이게 된다. 움직임을 구현할 때 델타 타임은 필수다!
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"
// 헤더 파일에서는 using namespace std; 사용을 지양하는 것이 좋다고 배웠다.
UCLASS()
class SPARTAUNREAL_API AItem : public AActor
{
GENERATED_BODY()
public:
AItem();
protected:
// Actor Lifecycle 함수 2. 게임 시작 시 호출됨.
virtual void BeginPlay() override;
public:
// Actor Lifecycle 함수 3. 매 프레임 호출됨.
virtual void Tick(float DeltaTime) override;
// 컴포넌트를 가리킬 포인터 변수.
// UPROPERTY 매크로는 언리얼 리플렉션 시스템이 이 변수를 인식하게 함.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
USceneComponent* SceneRoot;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
UStaticMeshComponent* Mesh;
private:
// Tick에서 사용할 회전 속도 변수
UPROPERTY(EditAnywhere, Category="Movement")
FRotator RotationSpeed;
};
#include "Item.h"
#include "Components/StaticMeshComponent.h"
using namespace std; // cpp 파일에서는 편의상 사용
// Actor Lifecycle 함수 1. 액터가 메모리에 생성될 때 가장 먼저 호출됨.
AItem::AItem()
{
// Tick 함수를 매 프레임 호출하도록 설정. 성능을 위해 필요없으면 꺼야 함.
PrimaryActorTick.bCanEverTick = true;
// 1. SceneComponent를 생성해서 루트 컴포넌트로 지정. (좌표계를 갖게 됨)
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
// 2. StaticMeshComponent를 생성하고 루트 컴포넌트에 자식으로 붙임.
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
Mesh->SetupAttachment(GetRootComponent());
// 3. C++ 코드로 에셋(3D 모델)을 찾아와서 메시에 할당.
// ConstructorHelpers는 생성자에서만 사용 가능!
static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/ArtResource/Props/SM_Sword.SM_Sword"));
if (MeshAsset.Succeeded())
{
Mesh->SetStaticMesh(MeshAsset.Object);
}
// 4. Tick에서 사용할 회전 속도 값 초기화 (1초에 Y축으로 90도 회전)
RotationSpeed = FRotator(0.f, 90.f, 0.f);
}
void AItem::BeginPlay()
{
Super::BeginPlay();
}
void AItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 5. 매 프레임마다 액터를 회전시킨다.
// RotationSpeed가 0에 가까운 값이 아닐 때만 회전시켜서 불필요한 연산을 줄임 (최적화)
if (!RotationSpeed.IsNearlyZero())
{
// DeltaTime을 곱해줘야 어떤 컴퓨터(프레임)에서도 동일한 속도로 회전한다. 이게 핵심!
AddActorLocalRotation(RotationSpeed * DeltaTime);
}
}
...dll 파일에 접근할 수 없다는 에러가 발생했다. 빌드할 땐 반드시 에디터를 종료하는 습관을 들여야 한다.DeltaTime 곱하는 걸 잊었다. 내 컴퓨터에서는 적당히 도는 것 같았는데, 프레임이 다른 환경에서는 엄청나게 빨리 돌아서 깜짝 놀랐다. 움직임에는 DeltaTime이 필수라는 걸 뼈저리게 느꼈다.Remove 했다. 하지만 실제 파일은 폴더에 그대로 남아있어서 계속 빌드 에러가 발생했다. 솔루션에서 제거 -> 실제 폴더에서 파일 삭제 -> .uproject 우클릭 -> Generate... 순서를 꼭 지켜야 한다.| 개념 | 설명 | 비고 |
|---|---|---|
| 액터 (Actor) | 언리얼 월드에 배치할 수 있는 모든 객체의 기본 단위. | 캐릭터, 아이템, 이펙트 등 눈에 보이는 대부분의 것. |
| 컴포넌트 (Component) | 액터에 부착하여 특정 기능(외형, 소리, 충돌 등)을 부여하는 부품. | 레고 블록처럼 조립해서 액터를 완성한다. |
| 루트 컴포넌트 (Root Comp) | 액터의 기준점이자 트랜스폼(위치/회전/크기) 정보를 담당하는 핵심 컴포넌트. | SetRootComponent() 함수로 지정해야 한다. |
| 라이프사이클 (Lifecycle) | 액터가 생성되고 소멸되기까지의 전 과정. 각 단계별로 특정 함수가 호출된다. | 생성자, BeginPlay, Tick, EndPlay 등. |
| Tick 함수 | 매 프레임마다 호출되는 함수로, 지속적인 업데이트가 필요할 때 사용한다. | 성능에 영향을 많이 주므로 꼭 필요할 때만 bCanEverTick = true로 설정. |
| 델타 타임 (Delta Time) | 한 프레임이 실행되는 데 걸린 시간. 프레임에 독립적인 로직을 만들 때 필수. | Tick 함수에서 움직임을 구현할 땐 반드시 곱해줘야 한다. |