Transform 속성 이해하기
Actor와 Transform
- Actor (액터)는 언리얼 엔진에서 게임 세계(월드)에 존재하는 모든 오브젝트를 의미합니다. 건물, 캐릭터, 아이템 등 눈에 보이는 모든 것들이 액터로 표현됩니다.
- 액터에는 세 가지 중요한 속성이 있습니다.
- 위치(Location)
- 회전(Rotation)
- 크기(Scale)
- 이 세 가지 속성을 Transform이라고 부르며, 뷰포트 상에서 보통 x축 (빨간색), y축 (초록색), z축 (파란색)으로 표시됩니다.

- 위치 (Location)
- 액터가 월드에서 어느 지점에 있는지를 나타냅니다.
- 예) FVector(100.0f, 200.0f, 300.0f)라면 월드의 x축으로 100, y축으로 200, z축으로 300만큼 떨어진 위치를 의미합니다.
- 회전 (Rotation)
- 액터가 어느 방향을 바라보는지, 어떤 각도로 기울어져 있는지를 나타냅니다.
- 언리얼 C++에서는 주로 FRotator(Pitch, Yaw, Roll) 형태로 표현합니다.
- Roll: 좌우로 기울어지는 회전 (x축을 축으로 하는 회전)
- Pitch: 앞뒤 방향의 기울어짐 (y축을 축으로 하는 회전)
- Yaw: 좌우 방향 회전 (z축을 축으로 하는 회전)
- 스케일 (Scale)
- 액터의 크기 비율입니다. FVector(X, Y, Z) 형태로 표현되며, 예) FVector(2.0f, 2.0f, 2.0f)는 기본 크기의 2배를 의미합니다.
FTransform 자료형
- 언리얼 엔진에는 위치, 회전, 스케일을 하나로 묶어 효율적으로 관리하기 위한 구조체인 FTransform이 있습니다.
- FTransform은 내부적으로 다음 세 요소를 보관합니다.
- Translation: 위치를 표현하는 FVector
- Rotation: 회전을 표현하는 FRotator
- Scale3D: 스케일을 표현하는 FVector
- FTransform을 활용하면 위치·회전·스케일을 한 번에 다룰 수 있어, Transform 관련 연산을 보다 편리하게 처리할 수 있습니다.
좌표계의 개념
- 월드 좌표계 (World Space)
- 게임 전체 세계를 기준으로 한 절대적인 좌표계입니다.
- SetActorLocation(), GetActorLocation()처럼 액터 자체를 이동·회전·스케일할 때 대부분 월드 좌표계를 기준으로 합니다.
- 로컬 좌표계 (Local Space)
- 액터 자신이나 부모 액터 (또는 부모 컴포넌트)의 Transform을 기준으로 한 상대적인 좌표계입니다.
- 계층 구조 (부모-자식 관계)가 있는 경우, 자식은 부모의 Transform에 종속되어 움직입니다.
부모-자식 컴포넌트 관계
- 액터에는 여러 컴포넌트가 붙을 수 있으며, 최상위에 있는 루트 컴포넌트 (Root Component)를 기준으로 다른 컴포넌트들이 Attach (부착) 관계를 맺을 수 있습니다.
- 부모 액터 (또는 부모 컴포넌트)가 이동·회전·스케일되면 자식들은 상대 좌표값에 따라 함께 이동합니다.
- 부모-자식 관계가 맺어져 있다면,
- GetRelativeTransform(): 부모 기준의 상대 위치·회전·스케일을 가져옴
- SetRelativeLocation(), SetRelativeRotation(): 부모 기준으로 자식의 위치·회전을 조정
- 이처럼 로컬 좌표계를 적절히 활용하면, 여러 컴포넌트를 한꺼번에 움직이거나, 특정 컴포넌트만 부모 기준으로 움직이게 만들 수 있습니다.
C++ 코드로 Transform 다루기
Transform 조정 함수
언리얼 에디터에서 액터를 선택하면 3D Gizmo(화살표, 회전 핸들, 스케일 핸들)을 마우스로 직접 움직여서 위치·회전·스케일을 조정할 수 있습니다.
- 단축키 W: 이동(Translate)
- 단축키 E: 회전(Rotate)
- 단축키 R: 스케일(Scale)
- 게임 로직에서 액터의 Transform을 변경하고 싶다면, C++ 또는 블루프린트 함수를 사용합니다. 대표적인 함수들은 아래와 같습니다.
- SetActorLocation(FVector NewLocation): 액터 위치 이동
- SetActorRotation(FRotator NewRotation): 액터 회전
- SetActorScale3D(FVector NewScale): 액터 스케일 변경
- GetActorLocation(), GetActorRotation(), GetActorScale3D(): 현재 Transform 정보 가져오기
- SetActorTransform(FTransform NewTransform): 위치·회전·스케일을 한 번에 설정
BeginPlay() 함수에서 Transform 변경
Actor의 Transform 값들을 옮길 때, 이때 life cycle의 begin play함수에 구현하는 게 좋음.
왜냐면 begin play는 world에 스폰 된 직후에 딱 한 번 호출되는 함수이기 때문.
<Item.h>
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"
UCLASS()
class TEST01_API AItem : public AActor
{
GENERATED_BODY()
public:
AItem();
protected:
USceneComponent* SceneRoot;
UStaticMeshComponent* StaticMeshComp;
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
};
<Item.cpp>
#include "Item.h"
AItem::AItem()
{
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMeshComp->SetupAttachment(SceneRoot);
static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/SM_Chair.SM_Chair"));
if (MeshAsset.Succeeded())
{
StaticMeshComp->SetStaticMesh(MeshAsset.Object);
}
static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Materials/M_Metal_Burnished_Steel.M_Metal_Burnished_Steel"));
if (MaterialAsset.Succeeded())
{
StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
}
}
void AItem::BeginPlay()
{
Super::BeginPlay();
SetActorLocation(FVector(300.0f, 200.0f, 100.0f));
SetActorRotation(FRotator(0.0f, 90.0f, 0.0f));
//- pitch, yall, roll
//- z축으로 90도 회전하겠다는 말임
// 추가적으로 공부해보면 좋을 것 : 짐벌락 문제, Quaternion (언리얼에서는 FQuat이라는 형태로 나타냄)
SetActorScale3D(FVector(2.0f, 1.0f, 1.0f));
// x축으로 2배만큼 커지게 됨
// 전체를 2배로 하고 싶다면 SetActorScale3D(FVector(2.0f)); 라고 하면 됨
}
void AItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}


- BeginPlay()는 액터가 월드에 배치된 후 초기화가 끝나면 자동으로 호출됩니다. 여기서 SetActorLocation 등으로 Transform을 바꾸면 게임 시작과 동시에 적용됩니다.
- SetActorScale3D(FVector(2.0f))와 같이 스케일 값을 통일하면, 모든 축 (x, y, z)이 같은 비율로 확대·축소됩니다.
Actor 위치를 옮기고, 그걸 한 번에 관리하는 코드 작성하는 법
(Actor를 구현해놓은 함수에 추가)
FVector NewLocation(100.0f, 200.0f, 100.0f);
FRotator NewRotation(0.0f, 80.0f, 0.0f);
FVector NewScale(2.0f);
FTransform NewTransform(NewRotation, NewLocation, NewScale);
SetActorTransform(NewTransform);
FTransform이라는 구조체 형태로 위 세가지를 한 번에 관리할 수 있음!
Tick 함수와 프레임 독립적인 로직 이해하기
게임 프레임 업데이트와 Tick
- 언리얼 엔진은 게임 실행 중 매 프레임마다 여러 작업을 수행합니다.
- 렌더링(Rendering): 화면 그리기 (일반적으로 1초에 60프레임, 120프레임 등)
- 물리 연산: 충돌·중력·마찰 등 물리 엔진 처리
- 오브젝트 업데이트: 게임 내 액터들의 상태 갱신
- 특정 액터가 “매 프레임마다” 수행할 로직이 있다면, 언리얼 엔진은 그 액터의 Tick(float DeltaTime) 함수를 매 프레임 호출해줍니다.
Tick 함수 활성화해서 회전 구현하기
Tick은 Tick 함수를 켜야만 작동됨.
- 액터를 생성할 때 기본적으로 Tick() 함수를 사용하려면, 아래와 같이 생성자에서 설정해야 합니다.
PrimaryActorTick.bCanEverTick = true;
- bCanEverTick가 false면 엔진은 성능 최적화를 위해 해당 액터의 Tick을 호출하지 않습니다.
- 불필요한 Tick 호출은 성능에 부담을 줄 수 있으므로, 사용하지 않는다면 false를 명시해주어 최적화 효과와 코드 명확성을 높이세요.
아이템을 회전시키는 거기 때문에 local로 하는 게 나음.
생각해보면 어차피 의자가 스스로 도는데 world rotation 하는 건 좀 이상함.
world rotation 같은 건 태양계 같은 거 구현할 때 쓰는 거임!
void AItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
AddActorLocalRotation(FRotator(0.0f, RotationSpeed, 0.0f));
}
이렇게 짠다면 90도씩 매 프레임마다 더해주면서 회전이 되긴 하겠지만, 문제는 얘는 매 프레임마다 호출이 되는 거임.
그니까 120프레임이면 1초에 120번 호출이 된다는거임. 그니까 컴퓨터 좋은 사람은 아주 팽팽 도는거임!
이건 잘못 구현한 것. 어떤 상황에서도 동일하게 구현되어야 함. 그래서 Delta time (프레임당 시간) 이 필요함.
DeltaTime이란?
- Tick(float DeltaTime) 함수에서 DeltaTime은 “직전 프레임부터 현재 프레임까지 걸린 시간(초)”입니다.
- 60 FPS 경: DeltaTime ≈ 1/60초 ≈ 0.0167초
- 120 FPS 환경: DeltaTime ≈ 1/120초 ≈ 0.0083초
- 프레임 레이트가 높을수록 DeltaTime이 작아지고, 낮을수록 DeltaTime이 커집니다.
1초에 90도씩 회전을 하고 싶다? -> 프레임 수만큼 나눠주면 됨.
120프레임이라면 90도 / 120 (1초에 120씩 호출되니까 90도를 120으로 나눠서 1초가 되면 120번 불려서 총 90도 회전하도록)
AddActorLocalRotation(FRotator(0.0f, RotationSpeed * DeltaTime, 0.0f));
한 프레임당 델타 타임만큼만 돌아가도록 코드 설정!
프레임이 좋을수록 RotationSpeed * DeltaTime 값이 작을거임.
DeltaTime을 활용한 프레임 독립적인 움직임
- 단순히 “매 프레임마다 X 좌표를 1씩 증가”시키면, FPS가 높을수록 더 빨리 움직여 게임 체감 속도가 달라집니다.
- 이를 방지하려면, DeltaTime을 곱해서 초 단위 기준으로 이동·회전을 계산해야 합니다.
- 예) 초당 100만큼 이동하고 싶다면: 100 * DeltaTime을 매 프레임마다 더해줌
- 60 FPS → 한 프레임당 1.67씩 이동, 60 프레임 곱하면 100
- 120 FPS → 한 프레임당 0.83씩 이동, 120 프레임 곱하면 100
- 이처럼 어느 FPS 환경에서도 동일한 실제 속도를 유지할 수 있습니다.
- 예) 초당 100만큼 이동하고 싶다면: 100 * DeltaTime을 매 프레임마다 더해줌
그치만 Tick 함수는 항상 계산을 최소화 해야 함.
매번 모든 게 불린다는 건 좋지 않음!
그래서 방어 코드가 필요함.
if (RotationSpeed != 0.0f)
{
AddActorLocalRotation(FRotator(0.0f, RotationSpeed * DeltaTime, 0.0f));
}
여기서 또 주의할 점!
부동 소수점은 약간의 작은 오차가 발생할 가능성이 큼 (ex. 0.1 + 0.1 = 0.199999 로 나온다던지...)
그래서 위와 같이 코드를 작성하면 0.0000000000001 이정도 나와도 회전을 하게 됨
이걸 방지하기 위해서 아래와 같은 코드를 짤 수 있음!
if (!FMath::IsNearlyZero(RotationSpeed))
{
AddActorLocalRotation(FRotator(0.0f, RotationSpeed * DeltaTime, 0.0f));
}
-> 근접하게 0이 되면 0으로 쳐주겠다는 의미의 코드를 포함시킴!
즉, 위 코드는 0이 아닌 상황(0과 근접한 것도 아님)에서 회전을 실행하겠다는 로직이 됨.
<Item.h>
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"
UCLASS()
class TEST01_API AItem : public AActor
{
GENERATED_BODY()
public:
AItem();
protected:
USceneComponent* SceneRoot;
UStaticMeshComponent* StaticMeshComp;
float RotationSpeed;
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
};
<Item.cpp>
#include "Item.h"
AItem::AItem()
{
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMeshComp->SetupAttachment(SceneRoot);
static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/SM_Chair.SM_Chair"));
if (MeshAsset.Succeeded())
{
StaticMeshComp->SetStaticMesh(MeshAsset.Object);
}
static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Materials/M_Metal_Burnished_Steel.M_Metal_Burnished_Steel"));
if (MaterialAsset.Succeeded())
{
StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
}
PrimaryActorTick.bCanEverTick = true;
RotationSpeed = 90.0f;
}
void AItem::BeginPlay()
{
Super::BeginPlay();
SetActorLocation(FVector(300.0f, 200.0f, 100.0f));
SetActorRotation(FRotator(0.0f, 90.0f, 0.0f));
SetActorScale3D(FVector(2.0f, 1.0f, 1.0f));
}
void AItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (!FMath::IsNearlyZero(RotationSpeed))
{
AddActorLocalRotation(FRotator(0.0f, RotationSpeed * DeltaTime, 0.0f));
}
}
- FMath::IsNearlyZero는 부동소수점 비교에서 안전하게 0에 가까운지 확인해주는 함수입니다.
- RotationSpeed = 360.0f면 1초에 한 바퀴(360도) 회전, 180.0f면 2초에 한 바퀴 회전하게 됩니다.
- AddActorLocalRotation()은 액터의 로컬 기준으로 회전을 추가해주는 함수입니다.
- 만약 월드 좌표 기준으로 회전하고 싶다면 AddActorWorldRotation()을 사용할 수도 있습니다.
- 만약 Pitch나 Roll 방향 회전을 주고 싶다면, FRotator(Pitch, Yaw, Roll) 부분을 적절히 수정하면 됩니다.
- 이 원리를 이동 (Translate)에도 동일하게 적용할 수 있습니다. 예를 들어, AddActorWorldOffset()에 DeltaTime을 곱해주면 초당 이동 거리를 일정하게 만들 수 있습니다.
- RocationSpeed 값에 따라 회전 속도가 달라지는 것도 확인해볼 수 있음.
'학습 > Unreal' 카테고리의 다른 글
| [실습] Actor 회전과 이동 기능 구현 (0) | 2026.04.14 |
|---|---|
| C++ 클래스와 리플렉션 시스템 활용하기 (0) | 2026.04.10 |
| Actor의 라이프 사이클, 로그 카테고리 정의 및 추가 (0) | 2026.04.09 |
| Actor 클래스에 컴포넌트 추가하기 (0) | 2026.04.08 |
| C++로 Actor 클래스 생성 및 삭제하기 (0) | 2026.04.08 |
