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로 생성한다면, 
      • 해당 모듈에서만 접근 가능하게 할 수 있어서, 캡슐화 시킬 수 있다.

클래스 생성 화면

  • cf) 모듈이 무엇인가?
    • 아래 사진의 네모를 친 부분을 하나의 모듈이라고 한다. (source 안에 존재하는 것)
    • 나중에 프로젝트가 커지면, 다양한 모듈을 한 프로젝트안에서 만들 수 있다.
    • 이때, public과 private를 잘 구분해서 만드는 것이 중요하다.

1-3. C++ 클래스 삭제하기

  1. vs에서 remove를 통해 삭제하기
  2. 실제 파일이 존재하는 폴더에서 삭제하기
  3. vs를 다시 빌드
  4. 에디터 재실행

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 폴더에서 에셋을 우클릭 후 레퍼런스 복사를 하여 가져오면 된다.
  • 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 추가하기

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));
	}
}