0. 개요
드디어..! 마참내..! 1주차 언리얼 강의를 다 들었다...!
이제 마음대로 코드를 치고 뜯고 맛볼 수 있게되었다..! ㅠㅠㅠㅠㅠㅠ 마참내..
그래서 우선 오늘 TIL은 강의 내용 정리 위주로 빠르게 쓱쓱 작성하고, 이후에는 과제를 하러 가야겠다. ㅎㅎ
음.. 과제에 대한 내용을 조금 적자면..
이전 처럼 뭔가.. 스테이지 진행하는 느낌의 구조로 작게 만들고
움직이는, 회전하는 함정 뿐 아니라 이전 플랫포머 게임에서 구현했었던 대부분의 함정을 만들어볼까 한다.
사실 아직 캐릭터는 C++로 만드는 강의를 듣지 못했기 때문에 캐릭터는 빠르게 블루프린트로 만들거나..
언리얼 3인칭 템플릿을 이용해 기본 캐릭터를 이용할것 같다.
아님 2주차 강의듣고 추가하지 뭐..
그리고.. 랜덤한 위치에 함정을 잘 생성하는 기능, 이전에 만들었던 레이저 퍼즐을 C++로 구현하기 정도일 것 같다.
시간이 많은 편은 아니니까 기능 구현을 쭉 하는 느낌으로만 만들까 싶다.
이전 플랫포머 게임처럼 만들기에는 시간이 너무 많이 소요될 것 같다.
1. 강의 정리
1. 로그
로그는 디버깅에 있어서 기본이다.
게임 개발 중 특정 함수가 제대로 호출되는지, 변수에 어떤 값이 들어있는지를 빠르게 확인해야 할 때 로그 메시지가 큰 도움이 된다.
언리얼 엔진에서는 UE_LOG() 매크로를 사용해 Output Log 창에 메시지를 남길 수 있다.
<로그창 띄우는 법>
로그창을 확인하기 위해서 "창" → "출력 로그"를 선택해 컨텐츠 드로어 옆에 고정되도록 새 창을 띄우거나 좌측 하단에서 필요할 때마다 출력 로그를 클릭해 확인할 수 있다.
<C++에서 로그 추가 방법>
void AItem::BeginPlay()
{
Super::BeginPlay();
// LogTemp = 로그 카테고리 / 어떤 카테고리에 속한 로그인지 구분짓는 것.
// Warning = 경고 메세지 (메세지 수준) / 노란 색상으로 출력된다. / 이외에 Display등이 있다.
// TEXT = 출력할 메세지
UE_LOG(LogTemp, Warning, TEXT("MyLog!"));
}
상세 레벨 | 콘솔에 출력됨 | 에디터 로그에 출력됨 | 텍스트 컬러 | 추가정보 |
Fatal | 예 | 해당 없음 | 해당 없음 | 세션이 크래시 됩니다. |
Error | 예 | 예 | 빨간색 | 해당 없음 |
Warning | 예 | 예 | 노란색 | 해당 없음 |
Display | 예 | 예 | 흰색 | 해당 없음 |
Log | 아니요 | 예 | 회색 | 해당 없음 |
Verbose | 아니요 | 아니요 | 해당 없음 | 해당 없음 |
VeryVerbose | 아니요 | 아니요 | 해당 없음 | 로그 마스크 및 특수 열거형 값을 사용하여 텍스트 컬러를 구성할 수 있습니다. |
https://dev.epicgames.com/documentation/ko-kr/unreal-engine/logging-in-unreal-engine
<자체 로그 카테고리 정의>
// .h
// 로그 카테고리를 선언하는 것이다 (커스텀 카테고리 생성?)
// Warning을 넣어주면, Warning 이상의 로그만 출력하겠다는 것
// All의 의미는 나중에는 모든 메세지 수준을 출력할 수 있도록 열어두겠다는 것이다.
DECLARE_LOG_CATEGORY_EXTERN(LogSparta, Warning, All);
// .cpp
DEFINE_LOG_CATEGORY(LogSparta);
팀 협업을 할때, 고유한 로그 카테고리를 만들어서 관리하면, 디버깅할때 어디에서 문제가 발생했는지 확인이 편하다.
이후 다른 클래스에서도 LogSparta 카테고리를 사용하고 싶다면 이 헤더 파일(Item.h)을 포함해야 한다.
보통은 이런 로그 카테고리를 여럿이 공유하기 때문에, 별도의 공용 헤더에 선언해 두는 경우가 많다.
<로그 확인하기>
- 로그 지우기
우선 확인이 편하게 로그를 지워줄 수 있다.
- 로그 필터링
카테고리를 지정한 이유는 로그 필터를 할 수 있기 때문이다.
주의할 점 로그 필터는 한번이라도 출력된 카테고리들만 뜨기 때문에 아직 LogTemp가 출력된적이 없다면, 필터 목록에 해당 카테고리가 존재하지 않는다.
=> 로그를 보는데 필터에 카테고리가 없다? 그러면 아직 출력이 된적이 없음을 의심해야한다.
- 로그 확인
<Tip>
개발 중에는 로그를 통해 디버깅하는 습관이 매우 중요합니다.
다만, 프로젝트 최적화 단계나 출시 단계가 되면 불필요한 로그는 제거하거나 로그 레벨을 낮춰야 합니다.
너무 많은 로그는 성능 저하를 일으키고, 민감한 정보가 노출될 위험도 있기 때문입니다.
2. 라이프 사이클
언리얼 엔진에서 Actor는 게임 도중에 언제든지 생성(Spawn)될 수 있고, 필요 없어지면 파괴(Destroy)될 수 있다. 이를 Actor 라이프 사이클이라 부르며, 이 과정을 이해하면 게임 로직을 보다 효율적이고 안정적으로 작성할 수 있다.
<액터 라이프 사이클을 알아야하는 이유>
액터에는 여러가지 콜백 함수(어떤 시점이 되면 자동으로 호출되는 함수들)들이 있는데, 생성되고 삭제되는 순간까지 이러한 콜백 함수들이 호출된다.
즉, 게임 로직을 구현할 때, 어떤 시점에서 로직을 짜야하는지를 생각하기 위해서 우선적으로 이런 라이프 사이클을 잘 이해해야 한다.
- 초기화 시점 결정
- 생성자 (Constructor), PostInitializeComponents, BeginPlay 등이 각각 언제 호출되는지 알아야 적절한 곳에 코드를 배치할 수 있다.
- 예) 컴포넌트 생성(CreateDefaultSubobject)은 생성자에서, 다른 액터 참조나 월드 접근은 BeginPlay에서 처리.
- 성능 관리
- 매 프레임마다 호출되는 Tick 함수는 비용이 클 수 있다.
- 따라서 필요한 액터만 Tick을 활성화하거나 이벤트 기반으로 전환해 최적화해야 한다.
- 리소스 정리
- 액터가 사라질 때 (EndPlay, Destroyed 등) 메모리를 해제하거나 특정 상태를 저장해야 할 수 있다.
- 적절한 시점에 필요한 정리 작업을 하지 않으면 메모리 누수나 예외 상황이 발생할 수 있다.
<주요 라이프 사이클 함수>
언리얼 엔진의 Actor는 생성 → 초기화 → 월드 배치 → Tick(실행) → 제거 순으로 동작하며, 이를 지원하기 위해 여러 함수가 자동 호출된다.
- 생성자 (Constructor)
- C++ 클래스 객체가 메모리에 생성될 때 단 한 번 호출된다.
- 아직 월드에 완전히 등록된 상태가 아니므로, 다른 액터나 월드 관련 기능은 안전하게 호출하기 어렵다.
- 보통 컴포넌트 생성(CreateDefaultSubobject) 및 기본 변수 초기화에 사용.
- PostInitializeComponents()
- 액터의 모든 컴포넌트가 생성·초기화를 마친 뒤 자동으로 호출함.
- 즉, 컴포넌트가 완성된 직후에 바로 호출되는 함수
- 컴포넌트들이 이미 준비된 상태이므로, 컴포넌트 간 상호작용 초기화 코드를 넣기 좋다.
- BeginPlay()
- 게임이 시작 (Play 모드)되거나, 런타임 중 액터가 새로 생성 (Spawn)되는 순간에 한 번 호출된다.
- 이 시점에서는 월드와 다른 액터들이 준비된 상태이므로, 자유롭게 상호작용 코드를 작성할 수 있다.
- AI, 게임 모드, 플레이어 컨트롤러 등 다른 시스템과의 연동도 보통 이 시점에 처리한다.
- Tick(float DeltaTime)
- 매 프레임마다 반복 호출되며, 실시간 업데이트가 필요한 로직 (캐릭터 이동, 물리 연산 등)을 넣어 준다.
- 불필요한 액터에는 Tick을 끄고, 이벤트 기반으로 전환하면 성능을 절약할 수 있습니다.
- 이 처럼 Tick 함수는 게임 성능과 매우 밀접한 관련이 있기 때문에 주의하면서 사용해야 한다.
- Destroyed()
- Destroy() 함수를 직접 호출해 액터를 제거할 때 직전에 호출된다.
(단, 게임 종료나 레벨 전환 시에는 호출되지 않을 수 있음) - 보통 EndPlay에서 주요 정리를 마치고, Destroyed()에서 메모리 해제나 사운드/파티클 정리 등 최종 작업을 수행한다.
- Destroyed가 불리면 마지막에 EndPlay도 같이 호출된다.
- Destroy() 함수를 직접 호출해 액터를 제거할 때 직전에 호출된다.
- EndPlay(const EEndPlayReason::Type EndPlayReason)
- 액터가 더 이상 월드에서 활동하지 않을 때 (파괴, 게임 종료, 레벨 전환 등) 호출된다.
- EEndPlayReason::Type은 언리얼 엔진에서 EndPlay 함수가 호출되는 이유를 나타내는 열거형(enum) 타입.
- 이 함수에서 자원 해제나 상태 저장을 처리한다
<로그로 시점 확인해보기>
▽코드
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"
DECLARE_LOG_CATEGORY_EXTERN(LogSparta, Warning, All);
UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
GENERATED_BODY()
protected:
USceneComponent* SceneRoot;
UStaticMeshComponent* StaticMeshComp;
public:
AItem();
protected:
virtual void BeginPlay() override;
virtual void PostInitializeComponents() override;
virtual void Destroyed() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
public:
virtual void Tick(float DeltaTime) override;
};
#include "Item.h"
DEFINE_LOG_CATEGORY(LogSparta);
AItem::AItem()
{
PrimaryActorTick.bCanEverTick = true;
// ..
..//
//*GetName()을 하면 현제 인스턴스의 이름을 가져오게 된다.
UE_LOG(LogSparta, Warning, TEXT("%s Constructor"), *GetName());
}
void AItem::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogSparta, Warning, TEXT("%s BeginPlay"), *GetName());
}
void AItem::PostInitializeComponents()
{
Super::PostInitializeComponents();
UE_LOG(LogSparta, Warning, TEXT("%s PostInitializeComponents"), *GetName());
}
void AItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AItem::Destroyed()
{
UE_LOG(LogSparta, Warning, TEXT("%s Destroyed"), *GetName());
Super::Destroyed();
}
void AItem::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
UE_LOG(LogSparta, Warning, TEXT("%s EndPlay"), *GetName());
Super::EndPlay(EndPlayReason);
}
코드에서 쓰인 함수
- GetName()
- 현재 액터의 이름을 문자열로 반환해 준다.
인스턴스마다 다른 이름이 자동으로 붙기 때문에 어떤 액터가 로그를 찍는지 식별하기 좋다.
- 현재 액터의 이름을 문자열로 반환해 준다.
- Tick 메시지 주의
- Tick 함수는 매 프레임 호출되므로, 로그를 찍으면 메시지가 빗발칠 수 있다.
- 디버깅이 필요한 순간에만 제한적으로 사용하거나, 조건문을 통해 특정 상황에서만 로그를 찍는 것이 좋다.
- EndPlay vs. Destroyed
- EndPlay는 액터가 월드에서 사라지는 모든 상황 (게임 종료, 레벨 전환, Destroy 호출 등)에 대해 호출된다.
- Destroyed는 보통 Destroy() 함수가 명시적으로 불렸을 때만 호출되며, 게임 종료나 레벨 언로드 시에는 호출되지 않을 수 있다.
▽테스트

생성할 때, 임시 객체가 만들어졌다가 삭제되는것 같다..
심지어 생성자도 2번 호출된다..?

삭제시 Destroyed가 호출된다.

실행하면, 생성자와 ComponentInit이 호출되고 BeginPlay가 호출된다.

실행 상태에서 삭제시 Destroyed와 endgame이 모두 호출된다.

반면 실행 상태에서 그냥 정지 버튼을 눌러 종료할 경우 EndGame만 호출된다.
<뷰포트 실행중 마우스 탈출>
뷰포트로 실행중일 때 Shift+F1을 누르면 마우스를 뺴낼 수 있다.
3. Transform 속성 이해하기
<Transform의 3가지 속성>
- 위치 (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) 형태로 표현된다.
🔎각 각 에디터에서 Viewport를 클릭하고, W,E,R키로 기즈모 모드를 변경할 수 있다.
<FTransform 자료형>
언리얼 엔진에서는 위치, 회전, 스케일을 하나로 묶어 효율적으로 관리하기 위한 구조체인 FTransform이 있다.
FTransform의 내부 3요소
- Translation : 위치를 표현하는 FVector
- Rotation : 회전을 표현하는 FRotator
- Scale3D : 스케일을 표현하는 FVector
FTransform을 활용하면, 위치·회전·스케일을 한번에 다룰 수 있어, Transform 관련 연산을 보다 편리하게 처리할 수 있다.
<좌표계의 개념>
- 월드 좌표계 (World Space)
- 게임 전체 세계를 기준으로 한 절대적인 좌표계
- SetActorLocation(), GetActorLocation() 처럼 액터 자체를 이동·회전·스케일할 때 대부분 월드 좌표계를 기준으로 한다.
- 로컬 좌표계 (Local Space)
- 액터 자신이나 부모 액터 (또는 부모 컴포넌트)의 Transform을 기준으로 한 상대적인 좌표계이다.
- 계층 구조 (부모-자식 관계)가 있는 경우, 자식은 부모의 Transform에 종속되어 움직인다.
추가적으로 편하게
에디터의 Viewport에서 볼때는 월드 좌표계 기준 블루프린트 에디터의 Viewport에서 볼때는 로컬 좌표계 기준 으로 이해는 조금 더 편하다.
<부모-자식 컴포넌트 관계>
- 액터에는 여러 컴포넌트가 붙을 수 있으며, 최상위에 있는 **루트 컴포넌트 (Root Component)**를 기준으로 다른 컴포넌트들이 Attach (부착) 관계를 맺을 수 있다.
- 부모 액터 (또는 부모 컴포넌트)가 이동·회전·스케일되면 자식들은 상대 좌표값에 따라 함께 이동한다.
- 부모-자식 관계가 맺어져 있다면,
- GetRelativeTransform(): 부모 기준의 상대 위치·회전·스케일을 가져옴
- SetRelativeLocation(), SetRelativeRotation(): 부모 기준으로 자식의 위치·회전을 조정
- 이처럼 로컬 좌표계를 적절히 활용하면, 여러 컴포넌트를 한꺼번에 움직이거나, 특정 컴포넌트만 부모 기준으로 움직이게 만들 수 있다.
< BeginPlay()를 이용해 초기 Transform 세팅해주기 >
BeginPlay()는 액터가 월드에 배치된 후 초기화가 끝나면 자동으로 호출된다.
때문에 액터의 초기 위치 설정은 BeginPlay()에서 해주는게 적당하다.
void AItem::BeginPlay()
{
Super::BeginPlay();
// 초반 위치및 회전을 잡아준다.
SetActorLocation(FVector(300.0f, 200.0f, 100.0f));
// FRotator는 피치 요 롤 값 순서이다.
SetActorRotation(FRotator(0.0f, 90.0f, 0.0f));
// FVector(2.f)으로 전달하면 전체가 다 2배로 커진다.
SetActorScale3D(FVector(2.f, 1.f, 1.f));
}
SetActorLocation(FVector NewLocation): 액터 위치 이동
SetActorRotation(FRotator NewRotation): 액터 회전
SetActorScale3D(FVector NewScale): 액터 스케일 변경
GetActorLocation(), GetActorRotation(), GetActorScale3D(): 현재 Transform 정보 가져오기
void AItem::BeginPlay()
{
Super::BeginPlay();
// Transform을 이용해 함수 하나를 호출해 처리하기
FVector NewLocation(300.0f, 200.0f, 100.0f);
FRotator NewRotation(0.0f, 90.0f, 0.0f);
FVector NewScale(2.f);
FTransform NewTransform(NewRotation, NewLocation, NewScale);
SetActorTransform(NewTransform);
}
SetActorTransform(FTransform NewTransform): 위치·회전·스케일을 한 번에 설정
단, Transform은 Rotation이 Location보다 앞에서 받는다.
<결과 확인>
다만 회전의 경우 Yaw인자 전달은 분명 FRotator의 2번째 였는데, Yaw가 Z축기준 회전이라 에디터에서는 3번째에 표시된 것을 확인할 수 있다..
회전 부분이 지금 햇갈리게 나와있긴 한데, 각각 어떤 축을 기준으로 회전하게 하는지만 알면 그렇게 이상한 것도 아니다.
< 게임 프레임 업데이트와 Tick >
- 언리얼 엔진은 게임 실행 중 매 프레임마다 여러 작업을 수행한다.
- 렌더링(Rendering): 화면 그리기 (일반적으로 1초에 60프레임, 120프레임 등)
- 물리 연산: 충돌·중력·마찰 등 물리 엔진 처리
- 오브젝트 업데이트: 게임 내 액터들의 상태 갱신
- 특정 액터가 “매 프레임마다” 수행할 로직이 있다면, 언리얼 엔진은 그 액터의 Tick(float DeltaTime) 함수를 매 프레임 호출해 준다.
< Tick 함수 활성화 하기 >
- 액터를 생성할 때 기본적으로 Tick() 함수를 사용하려면, 아래와 같이 생성자에서 설정해야 힌다.
AItem::AItem()
{
// 이 값이 true로 설정되어야 tick 함수를 사용할 수 있다.
// tick을 사용하지 않는다면 false를 설정해두는게 맞다.
// 사용을 안한다 하더라도 부모쪽 tick이 돌아가는 등의 상황이 있을 수 있어서
// 사용하지 않는다면 꺼두는것이 좋다.
PrimaryActorTick.bCanEverTick = true;
}
- bCanEverTick가 false면 엔진은 성능 최적화를 위해 해당 액터의 Tick을 호출하지 않는다.
- 불필요한 Tick 호출은 성능에 부담을 줄 수 있으므로, 사용하지 않는다면 false를 명시해주어 최적화 효과와 코드 명확성을 높이는 것이 좋다.
< DeltaTime이란? >
- Tick(float DeltaTime) 함수에서 DeltaTime은 “직전 프레임부터 현재 프레임까지 걸린 시간(초)”이다.
- 60 FPS 환경: DeltaTime ≈ 1/60초 ≈ 0.0167초
- 120 FPS 환경: DeltaTime ≈ 1/120초 ≈ 0.0083초
- 프레임 레이트가 높을수록 DeltaTime이 작아지고, 낮을수록 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을 매 프레임마다 더해줌
<구현>
//...
class SPARTAPROJECT_API AItem : public AActor
{
GENERATED_BODY()
protected:
USceneComponent* SceneRoot;
UStaticMeshComponent* StaticMeshComp;
// 회전 속도 변수를 추가해 주었다.
float RotationSpeed;
//... 헤더 중략
// 생성자에서 변수 초기화
AItem::AItem()
: RotationSpeed(90.f)
{
// ...
// ...
}
void AItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 틱당 90도 회전
// 문제는 매 프레임마다 호출하는데, 문제는 컴퓨터 마다 프레임이 다르다...
// 때문에 어떤 컴퓨터에서는 빠르게 회전하고 안좋은 컴퓨터에선 느리게 회전할 것이다.
// DeltaTime을 곱하면 어떤 컴퓨터든 동일한 움직임을 가질 수 있게 된다.
// 프레임 독립적인 계산이라고 한다.
// 틱에서 하는 일은 최대한 줄여주어야 하기 때문에
// 방어 코드로 0.f가 아닐 때만 동작하도록 추가해주었다.
// FMath::IsNearlyZero() 0에 근사하면 true를 반환해준다.
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을 곱해주면 초당 이동 거리를 일정하게 만들 수 있다.
<회전 관련 주의>
FRotator는 짐벌락 현상이 발생할 수 있기 때문에 쿼터니언을 사용한다. 수업중에는 다루지 않을 것인데, 짐벌락 현상이 발생하면 쿼터니언을 사용해야 한다.
이전에 쿼터니언에 대해 살짝 공부했었는데, 다시 한번 더 공부해 봐야겠다.
< BluePrint 이해 >
Blueprint는 언리얼 엔진에서 제공하는 시각적 스크립팅 도구로, 노드 (블록)를 연결하여 게임 로직을 작성한다.
- 장점
- Blueprint 그래프를 수정 후, 에디터에서 Play 버튼을 누르면 곧바로 결과를 확인할 수 있다.
아이디어를 빠르게 검증하고 반복할 때 큰 장점이 된다. - “레고 블록”을 쌓듯이 노드를 연결해 로직을 작성하므로, 프로그래밍 언어에 익숙하지 않은 초급자도 쉽게 접근할 수 있다.
- Blueprint 그래프를 수정 후, 에디터에서 Play 버튼을 누르면 곧바로 결과를 확인할 수 있다.
- 한계점
- 노드 수가 많아질수록 그래프가 복잡해져, 가독성과 유지보수가 어려워질 수 있다.
- C++과 비교했을 때, Blueprint는 내부적으로 추가 해석 과정을 거칩니다.따라서 물리 연산이나 AI 같은 높은 성능이 필요한 시스템에서는 병목이 될 수 있다.
⇒ 이미 구현 되어있는 함수들을 다시 시각적인 노드로 나타내기 위해서 한번 더 감싸고 있는 구조라서 병목현상이 더 심하다. 즉, 속도가 느릴 수 밖에 없다..
< C++ (네이티브 코드 프로그래밍) 이해 >
- 장점
- 엔진 코어까지 직접 수정 가능하며, 복잡하고 성능이 중요한 게임 로직을 빠르고 최적화된 방식으로 구현할 수 있다.
- 표준 라이브러리와 외부 라이브러리를 자유롭게 사용할 수 있어, 대규모 프로젝트에 적합하다.
- 포인터, 템플릿 같은 C++ 언어적 기능을 통해 메모리와 로직을 정교하게 다룰 수 있다.
- 한계점
- C++ 코드를 수정하면 에디터를 재시작하거나 Live Coding을 다시 컴파일해야 하므로, 반복 작업 시 조금 번거로울 수 있다.
< Blueprint와 C++의 상호보완적 관계 >
- 실무에서는 Blueprint와 C++를 함께 사용하는 경우가 많다고 하신다.
하나만 사용하는 것보다 각각의 장점을 취하는 하이브리드 워크플로우가 일반적이다. - Blueprint 활용: UI 제작, 간단한 이벤트 처리, 시각적 연출 등 빠른 프로토타이핑과 직관적인 로직 작성에 사용한다.
- C++ 활용: 높은 성능이 필요한 게임플레이 로직이나 엔진 레벨의 확장, 복잡한 수학 연산 등에 사용한다.
- 이렇게 분업하면 개발 속도와 퍼포먼스를 모두 확보하기가 수월해진다.
<리플렉션 (Reflection)이란?>
- 언리얼 엔진의 리플렉션 시스템은 C++ 클래스의 변수 및 함수 정보를 엔진 내부의 메타데이터 형태로 저장하고, 이를 에디터나 블루프린트에서 활용할 수 있게 만들어주는 기술이다.
- C++ 클래스에 있는 여러 멤버(변수, 함수 등)를 “반사”해, 에디터와 블루프린트에서 직접 설정, 호출이 가능하도록 합니다.
- 이 덕분에 프로그래머가 만든 C++ 로직의 뼈대를 디자이너나 다른 팀원들이 에디터에서 직관적으로 조정할 수 있다.
- 매개변수를 코드에서만 변경하는 것이 아니라, 에디터에서 바로 조정(슬라이더나 숫자 입력)하여 반복 테스트를 빠르게 진행할 수 있다.
- 리플렉션 시스템을 제대로 이해하고 활용하면, 큰 프로젝트에서도 개발 효율과 협업 효과를 극대화할 수 있다.
<C++ 클래스 리플렉션에 등록하기>
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
// 리플렉션 시스템과 관련된 모든 것이 담겨있다.
// 주의할 점은 헤더 참조 순서중 가장~~~ 마지막에 배치되어야 한다.
#include "Item.generated.h"
// 인자를 비웠을 때는 UCLASS(Blueprintable, BlueprintType)와 동일하다.
// Blueprintable : 블루프린트에서 상속이 가능하다.
// NotBlueprintable : 블루프린트에서 상속을 막아준다.
// BlueprintType : 블루프린트에서 이 클래스를 변수로 선언하거나 참조할 수 있게 된다.
UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
// 리플렉션 시스템에 등록되기 위한 데이터를 자동으로 만들어주는 언리얼측 메크로
// UCLASS() 메크로와 항상 같이 페어로 있어야 하는 메크로이다.
GENERATED_BODY()
- #include "Item.generated.h"
- 언리얼 엔진이 자동 생성하는 헤더 파일로, 클래스의 리플렉션 및 엔진 통합에 필요한 코드가 들어 있다.
- 🌟반드시 헤더 파일의 가장 마지막 #include 구문 아래에 위치해야 한다. 🌟
(다른 #include보다 아래에 오지 않으면 빌드 에러 발생 위험이 있다.)
- UCLASS()
- 해당 클래스를 언리얼 엔진의 리플렉션 시스템에 등록한다는 의미다.
- 이 매크로가 있어야만 블루프린트 등 에디터 차원에서 이 클래스를 인식하고 사용할 수 있다.
- GENERATED_BODY()
- 언리얼의 코드 생성 도구가 사용하는 코드를 삽입하는 역할을 한다.
- 클래스 내부에 필요한 리플렉션 정보를 자동으로 생성해 줍니다.
< UCLASS() 매크로의 주요 지정자 >
- UCLASS() 매크로는 클래스를 리플렉션 시스템에 등록하면서, 추가적으로 몇 가지 옵션 (지정자)을 설정할 수 있다.
- 기본 동작
- 만약 UCLASS()에 옵션을 주지 않으면, 블루프린트에서 상속이 가능하고 변수로 참조가 가능한 형태로 등록됩니다. 즉, 내부적으로 Blueprintable, BlueprintType과 동일한 효과를 가지게 된다.
- UCLASS(Blueprintable, BlueprintType) 와 동일하다는 의미.
- 주요 옵션
- Blueprintable
- 블루프린트에서 상속 가능한 클래스로 만듭니다.
⇒ C++ 클래스를 기반으로 블루프린트 클래스 생성 가능.
- 블루프린트에서 상속 가능한 클래스로 만듭니다.
- NotBlueprintable
- 블루프린트에서 이 클래스를 상속할 수 없도록 한다.
- BlueprintType
- 블루프린트에서 변수나 참조로 사용할 수 있게 한다.
- 이 옵션만 있으면, 상속은 허용되지 않고 참조만 가능하다.
- 필요에 따라 이 지정자들을 조합해 클래스가 어떻게 블루프린트와 상호작용해야 할지 명시할 수 있습니다.
- Blueprintable
< C++ 클래스 상속 받은 Blueprint 클래스 생성 >
이제 BP_Item 블루프린트가 생성되었고, 기본 부모 클래스는 Item이 됩니다. 이 블루프린트 안에서 C++로 작성된 속성이나 함수를 시각적으로 다룰 수 있다.
<🌟UPROPERTY() 매크로의 주요 지정자🌟>
UPROPERTY()에는 여러 지정자를 작성해, 에디터에서의 표시 여부나 Blueprint 접근성, 읽기/쓰기 권한 등을 자세하게 설정할 수 있다.
- 편집 가능 범위 지정자
- VisibleAnywhere: 읽기 전용으로 표시되며, 수정은 불가능
- EditAnywhere: 클래스 기본값, 인스턴스 모두에서 수정 가능
- EditDefaultsOnly: 클래스 기본값에서만 수정 가능
- EditInstanceOnly: 인스턴스에서만 수정 가능
- Blueprint 접근성 지정자
- BlueprintReadWrite: Blueprint 그래프에서 Getter/Setter로 값을 읽거나 쓸 수 있습니다.
- BlueprintReadOnly: Blueprint 그래프에서 Getter 핀만 노출되어, 읽기만 가능합니다.
- Category 지정자
- Details 패널에서 이 변수는 “Rotation” 범주(폴더) 아래에 표시됩니다.
- 여러 변수를 비슷한 카테고리에 묶으면, 세부 정보 패널에서 깔끔하게 정리되어 보입니다.
- 메타 옵션 지정자
- meta=(ClampMin="0.0"): 에디터에서 변수 입력 시 최소값을 제한할 수 있습니다.
- meta=(AllowPrivateAccess="true"): 해당 멤버가 private로 선언되어 있어도, 에디터나 Blueprint에서 접근할 수 있도록 허용합니다.
- 만약 UPROPERTY()만 있고, 추가 지정자를 하나도 주지 않는다면?
- 엔진 리플렉션 시스템에는 등록되지만, 에디터나 Blueprint에 노출되지는 않습니다.
- “엔진이 변수의 존재는 알고 있지만, 외부에서는 보이지 않게 숨겨둔 상태”라고 볼 수 있습니다.
- 리플렉션에 등록만 되어 있어도 가비지 컬렉션(메모리 관리)과 직렬화(세이브/로드) 같은 엔진 내부 기능이 작동할 수 있습니다.
<Item 클래스 변수 리플렉션 등록>
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"
UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
GENERATED_BODY()
protected:
UPROPERTY(VisibleAnywhere,BlueprintReadOnly, Category="Item|Component")
USceneComponent* SceneRoot;
UPROPERTY(EditAnywhere,BlueprintReadWrite, Category="Item|Component")
UStaticMeshComponent* StaticMeshComp;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, category="Item|Properties")
float RotationSpeed;
public:
AItem();
}
위와 같이 UPROPERTY() 지정자를 설정해두면, Blueprint 클래스를 통해 해당 변수들을 에디터 내에서 쉽게 조정할 수 있게 된다.
이렇게 코드 수정 없이 에디터에서 다양한 속성을 즉시 변경하고 테스트할 수 있다는 점이 리플렉션의 큰 장점이다.
< 함수 리플렉션이란? >
- 함수 또한 블루프린트에서 직접 호출할 수 있도록 등록할 수 있다. 이렇게 하면, 복잡한 C++ 로직을 Blueprint에서 간단한 노드로 불러와 제어할 수 있으므로 작업 효율을 높일 수 있다.
- UFUNCTION()은 멤버 함수를 등록한다.
- C++에서 만든 함수를 Blueprint 노드로 노출하고 싶을 때, UFUNCTION() 매크로를 사용한다.
< UFUNCTION() 매크로의 주요 지정자 >
- Blueprint 관련 지정자
- BlueprintCallable
- Blueprint 이벤트 그래프(노드)에서 호출(Execute) 가능한 함수로 만든다.
- BlueprintPure
- Getter 역할만 수행합니다. (Exec 핀 없이 Return Value만 노출) 블루프린트에서의 퓨어함수 만들기 기능!
- BlueprintImplementableEvent
- 함수의 선언만 C++에 있고, 구현은 블루프린트에서 하도록 한다. C++ 코드에서는 함수 이름만 정의하고, 실제 동작은 Blueprint Event Graph 안에서 이벤트 노드처럼 구현된다.
- BlueprintCallable
- 만약 UFUNCTION()에 지정자를 하나도 쓰지 않았다면?
- UPROPERTY()와 마찬가지로, 함수가 언리얼 리플렉션에 등록되긴 하지만, 특별히 Blueprint에 노출되지는 않는다.
- “엔진이 함수의 존재는 파악하되, Blueprint에서 직접 호출할 수 없게 숨겨둔 상태”
<Item 클래스 함수 리플렉션 등록>
// ...
public:
virtual void Tick(float DeltaTime) override;
// 함수를 리플렉션 시스템에 등록
// UPROPERTY와 마찬가지로 인자를 전달하지 않으면, 단순히 리플렉션 시스템이 해당 함수를 알고있기만 한 상태이다.
UFUNCTION(BlueprintCallable, category="Item|Action")
void ResetActorPosition();
// 실행핀이 없는 퓨어함수로 만들어준다.
UFUNCTION(BlueprintPure, category="Item|Properties")
float GetRotationSpeed() const;
// 구현은 블루프린트에서 했는데, 호출은 C++에서 가능하게도 만들어주는 것이다.
UFUNCTION(BlueprintImplementableEvent, category="Item|Properties")
void OnItemPickup();
// ...
UFUNCTION을 사용하면, 블루프린트 (BP_Item)의 Event Graph 창에서 우클릭한 뒤, 함수 이름을 검색하면 아래와 같이 노드가 노출된다.
블루프린트에서 이벤트처럼 구현한 OnItemPickedUp은 C++에선 함수 이름만 존재하고 실제 코드는 없다.
대신, Blueprint에서 이벤트 그래프를 통해 시각적 로직으로 구현해두면, C++에서 OnItemPickedUp()를 부르는 순간 그 이벤트가 실행된다.
2. 마무리
월요일부터 오늘까지는 강의만 듣고 정리만 했어서 사실 TIL에 과제 진행사항이나 이런것은 적지 못했다.. ㅠ
그래도 이제 내일부터는 과제와 과제하면서 막혔던 부분들을 정리해서 올릴 것이다. ㅎㅎ
오늘은 생각보다 정리가 빨리 끝난 관계로 빨리 과제하러 가야겠다!
- 20:56
아! 오늘 과제 진행한거는 내일 TIL에 작성하려 한다. ㅎㅎ
내일도 화이팅!!
'프로그래밍 > Unreal 부트캠프' 카테고리의 다른 글
TIL 2025.01.24 기록 (0) | 2025.01.24 |
---|---|
TIL 2025.01.23 기록 (0) | 2025.01.23 |
TIL 2025.01.21 기록 (0) | 2025.01.21 |
WIL 2025.01.20 기록 (0) | 2025.01.20 |
TIL 2025.01.20 기록 (0) | 2025.01.20 |