Unreal Engine
Unreal Engine - Actor의 생성과 응용
gbleem
2025. 1. 21. 17:56
1. Actor 클래스 생성 및 삭제
1-1. Actor와 Object 비교
- UObject
- 언리얼 엔진에서 모든 클래스의 최상위 부모 클래스
- 월드에 배치할 수 없고, 데이터나 로직만 담당할 수 있다.
- ex) 플레이어의 능력치, AI정보, 게임 설정 값 등
- Actor
- UObject를 상속한 클래스로 월드에 spawn 할 수 있다.
- 공간 정보(위치, 회전, 크기)를 가지고 있고, 여러 컴포넌트를 추가로 붙일 수 있다.
- 실제 게임에서 볼 수 있고, 상호작용할 수 있는 캐릭터, 몬스터, 아이템, 파티클 효과 등을 AActor를 기반으로 제작한다.
1-2. C++로 Actor 클래스 만들기
- 클래스를 만들 때 클래스 타입을 설정(public& private)
- 이때 public으로 생성한다면,
- 헤더 파일은 public폴더에 .cpp파일은 private 폴더에 위치하게 된다.
- 이 방식의 장점은 프로젝트의 다른 모듈에서도 쉽게 #include 할 수 있다는 점이다.
- private로 생성한다면,
- 해당 모듈에서만 접근 가능하게 할 수 있어서, 캡슐화 시킬 수 있다.
- 이때 public으로 생성한다면,
- cf) 모듈이 무엇인가?
- 아래 사진의 네모를 친 부분을 하나의 모듈이라고 한다. (source 안에 존재하는 것)
- 나중에 프로젝트가 커지면, 다양한 모듈을 한 프로젝트안에서 만들 수 있다.
- 이때, public과 private를 잘 구분해서 만드는 것이 중요하다.
1-3. C++ 클래스 삭제하기
- vs에서 remove를 통해 삭제하기
- 실제 파일이 존재하는 폴더에서 삭제하기
- vs를 다시 빌드
- 에디터 재실행
2. 컴포넌트 추가
2-1. 기본적인 C++ 클래스 구조 이해
#include
- #pragma once
- 컴파일 시 단 한번만 처리하도록 해주는 지시어
- CoreMinimal.h
- 언리얼 엔진에서 사용하는 기본 타입(FString, TArray)과 매크로(UE_LOG), 유틸함수 포함
- GameFramework
- AActor를 상속받기 위해 쓰는 헤더 파일
- .generated.h
- 언리얼 엔진의 리플렉션 시스템에서 필요한 코드를 생성하는 역할
선언부
- UCLASS
- 언리얼 엔진의 리플렉션 시스템에서 인식하도록 해주는 매크로
- BP로도 확장 가능하게하며, 에디터에서 여러 기능과도 연동하게 한다.
- AItem
- 클래스 이름에 접두사를 붙이기
- A : Actor 계열
- U : Object 계열
- F : 일반 구조체
- T : 템플릿
- E : 열거형
- 클래스 이름에 접두사를 붙이기
- ..._API
- 해당 클래스를 모듈 외부로 export하기 위한 매크로
- GENERATED_BODY()
- UCLASS() 매크로와 짝을 이루고, 언리얼 리플렉션에 필요한 코드를 생성해주는 매크로
2-2. C++로 컴포넌트 추가 및 루트 컴포넌트 설정
- 전체 코드의 흐름
- UScene 컴포넌트를 Root 컴포넌트로 설정
- Static Mesh 컴포넌트를 Root의 하위 컴포넌트로 설정
- Static Mesh에 실제 Mesh Asset과 Material을 지정
- 관련 함수 정리
- CreateDefaultSubobject<T>(TEXT(" "))
- 컴포넌트를 생성하고 초기화할 때 사용하는 함수
- T는 컴포넌트의 유형
- TEXT(" ") 안에 넣을 내용은 컴포넌트의 식별 이름 (에디터에 뜨는 이름)
- ConstructorHelpers::FObjectFinder<T>
- 특정 리소스를 경로 기반으로 로드하는 클래스
- 뒤에 변수명과 경로를 붙여주면 된다.
- 해당 경로는 content 폴더에서 에셋을 우클릭 후 레퍼런스 복사를 하여 가져오면 된다.
- CreateDefaultSubobject<T>(TEXT(" "))
- cf)
- 우리가 아래의 코드를 보면, SceneRoot와 StaticMeshComponent를 UPROPERTY() 과 같은 매크로를 쓰지 않았기 때문에, 에디터에서 해당 이름이 뜨는 것을 볼 수 없다. (아래 사진 참고 - static mesh관련된 내용을 디테일 창에서 볼 수 없다)
- 단, root component의 경우는 자동으로 리플렉션 시스템에 등록되기 때문에 아래 사진에서 처럼 볼 수 있다.
실제 구현 코드 모습
- .h 파일
protected:
USceneComponent* SceneRoot;
UStaticMeshComponent* StaticMeshComponent;
- .cpp의 생성자
AItem::AItem()
{
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMeshComponent->SetupAttachment(SceneRoot);
static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/SM_Chair.SM_Chair"));
if (MeshAsset.Succeeded())
{
StaticMeshComponent->SetStaticMesh(MeshAsset.Object);
}
static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Materials/M_Power_Sphere.M_Power_Sphere"));
if (MaterialAsset.Succeeded())
{
StaticMeshComponent->SetMaterial(0, MaterialAsset.Object);
}
}
3. Actor의 사이클
3-1. Log 추가하기
- 로그 관련 정리글(커스텀 카테고리 만들기)
- UE_LOG(LogTemp, Warning, TEXT(""))
- 로그 카테고리
- 로그 수준
- Display : 흰색
- Warning : 노란색
- Error : 빨간색
- 출력 메시지
3-2. Actor의 라이프 사이클
- 생성자
- 객체가 메모리에 생성될 때 단 한 번 호출
- 컴포넌트 생성(CreateDefaultSubobject 함수 사용) 및 기본 변수 초기화에 사용
- PostInitializeComponents()
- 액터의 모든 컴포넌트가 생성, 초기화를 마친 뒤 호출
- 주로 컴포넌트 간 상호작용 초기화 코드를 위해 사용
- BeginPlay()
- 게임이 시작 되거나, 액터가 새로 생성(스폰)되는 순간 한 번 호출
- AI, 게임모드, 플레이어 컨트롤러 등 다른 시스템과의 연동 처리
- Tick(float DeltaTime)
- 매 프레임 마다 반복 호출, 실시간 업데이트
- Destroyed()
- 직접 호출하여, 액터 제거 직전에 사용
- 게임 종료나 레벨 전환 시에는 호출되지 않을 수 있다.
- 메모리 해제나, 사운드/파티클 정리 등의 작업
- Destroy 호출되면, Endplay도 자동으로 호출
- EndPlay(const EEndPlayReason::Type EndPlayReason)
- 액터가 더 이상 월드에서 활동하지 않을 때 호출
- EEndPlayReason::Type 은 함수가 호출되는 이유를 나타내는 타입
- 자원 해제나 상태 저장을 처리
3-3. 사용 예제
- 사용 함수 정리
- GetName() : 현재 액터의 이름을 문자열로 반환
실제 구현 코드
- .h 파일
virtual void PostInitializeComponents() override;
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
virtual void Destroyed() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
- .cpp 파일
AItem::AItem()
{
UE_LOG(LogSparta, Warning, TEXT(" Constructor %s"), *GetName());
}
void AItem::PostInitializeComponents()
{
Super::PostInitializeComponents();
UE_LOG(LogSparta, Warning, TEXT(" PostInitializeComponents %s"), *GetName());
}
void AItem::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogSparta, Warning, TEXT(" BeginPlay %s"), *GetName());
}
void AItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AItem::Destroyed()
{
UE_LOG(LogSparta, Warning, TEXT(" Destroyed %s"), *GetName());
Super::Destroyed();
}
void AItem::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
UE_LOG(LogSparta, Warning, TEXT(" EndPlay %s"), *GetName());
Super::EndPlay(EndPlayReason);
}
실행 모습
- 액터 클래스를 월드에서 삭제 후 다시 배치했을 때의 로그
- 게임 플레이를 누른 후 종료를 했을 때의 로그
- 게임 플레이 도중 강제로 월드에서 액터를 제거했을 때의 로그
4. Actor 응용 (transform)
4-1. 월드 좌표 & 로컬 좌표
- 월드 좌표
- 게임 월드를 기준으로 한 좌표계
- SetActorLocation(), SetActorRotation() 등의 함수로 월드 좌표계 상에서 actor의 위치와 회전을 바꿀 수 있다.
- 로컬 좌표
- 액터 자신이나 부모 액터를 기준으로 한 좌표계
- 움직이고자 하는 액터와 해당 액터의 자식들은 다 같이 회전한다.
- SetRelativeRotation(), SetRelativeLocation() - 상대적인!
4-2. 사용 예제
이동, 회전, scale 변경 예제
- 아래와 같은 코드를 통해서 월드 기준 좌표계를 이동시켜줄 수 있다.
- Location과 Scale은 FVector 타입
- Rotation은 Ratator 타입
- 주의할 점은 Rotator가 각각 pitch(y축), yaw(z축), roll(x축) 순서로 이루어져 있다는 점이다.
- cf) Rotator의 짐벌락을 막기 위해서 FQuat을 쓰기도 한다.
SetActorLocation(FVector(300.f, 200.f, 100.f));
SetActorRotation(FRotator(0.f, 90.f, 0.f)); //pitch yaw roll
SetActorScale3D(FVector(2.f));
- 한번에 Transform을 통해서 이동시켜 줄 수도 있다.
FVector NewLocation(300.f, 200.f, 100.f);
FRotator NewRotation(0.f, 90.f, 0.f);
FVector NewScale(2.f);
FTransform NewTransform(NewRotation, NewLocation, NewScale);
SetActorTransform(NewTransform);
Tick을 이용한 회전 구현 예제
- Tick 함수를 활성화
- 생성자에서 PrimaryActorTick.bCanEverTick = true; 해주기
- 만약 Tick 함수를 쓰지 않을 때 false 해주면, 성능 낭비를 막을 수 있다.
- Delta Time
- Delta Time = 1초 / 프레임
- 우리가 구현한 것이 모든 컴퓨터에서 같은 동작을 해야하기 때문에 꼭 Tick에서의 동작은 Deltatime을 곱해서 구현하기
- IsNearly~ 함수
- 부동 소수점 문제를 해결하기 위해서 특정 숫자와 가까운 값인지 아닌지를 체크해주는 함수
- 아래 코드의 IsNearlyZero는 0이 아니지만, 매우 작아서 0과 가까운 수라면 true를 반환해주는 함수이다.
void AItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
//부동 소수점을 방지하는 코드
if (!FMath::IsNearlyZero(RotationSpeed))
{
AddActorLocalRotation(FRotator(0.f, RotationSpeed * DeltaTime, 0.f));
}
}