엔진으로 개발을 하다보면 가장 중요한 것이 그 엔진의 라이플사이클이다.

객체의 초기화나 오브젝트 할당, 파괴 등의 중요한 역할을 하기 때문이다.

그래서 오늘은 언리얼 엔진의 라이플 사이클에 대해 먼저 알아보았다.

그리고 관련 함수들을 사용하여 C++로 액터를 동적으로 이동, 회전, 크기변화까지 시켜볼 생각이다.

 

언리얼엔진의 라이플사이클

생성자

  • 액터가 생성되기 전에 호출됨.
  • 객체 메모리에 생성될 때 1번 호출됨(주로 초기화 작업을 활용)
  • 객체에 컴포넌트를 추가할 때? 사용하는 것 같음

 

PostInitializeComponents()

  • 컴포넌트가 완성된 직후 호출
  • 월드에 객체가 등장하기 직전에 컴포넌트끼리 상호작용할 때 활용

 

BeginPlay()

  • 월드에 스폰된 직후에 호출됨
  • 다른 액터들도 이미 준비되어있어서 자유롭게 상호작용이 가능

 

Tick(float DeltaTime)

  • 매 프레임마다 호출됨(무거운 로직을 담으면 성능이 떨어질 수도 있기 때문에 조심해서 사용)
  • 캐릭터 이동, 물리 계산 같은 부분들에 활용

 

Destroyed()

  • 삭제되기 직전에 호출됨
  • 보통 리소스 정리같은 부분을 담당

 

Endplay(const EEndPlayReason::Type EndPlayReason )

  • 게임 종료의 상황에서 호출
  • 이 함수는 Enum 타입으로, 어떤 이유로 EndPlay 함수가 호출되었는지 구분할 수 있다고 한다!

 

한줄요약(결론)

생성자() -> PostInitializeComponents() -> BeginPlay() -> (TIck()) -> Destory() -> EndPlay() 순으로 호출된다.

 

+Desrtory와 EndPlay()의 차이

(언리얼 초심자는 이 두 함수를 굉장히 혼란스러워한다고 하길래 나만의 이해팁을 공유하고자 한다)

 

내가 이해하기로는 Destroy()는 크롬창의 각 탭에 달려있는 x의 느낌으로 이해했다.

 

근데 이제 EndPlay()는 크롬창의 전체닫기 같은 느낌으로 이해했다.

 

대강 요런 느낌??

 

그리고 일단 각 함수들이 언제 호출되는지 좀 명확하게 보고싶어서 언리얼 엔진의 로그시스템을 활용해주었다.

먼저 플레이 버튼을 누른 직후이다.

이렇게 생성자(Constructor) -> PostInitializeComponents -> BeginPlay 순으로 로그가 찍히는 것을 볼 수 있다.

 

 

플레이를 종료했을 때

플레이 종료를 하면 EndPlay가 호출되는 것을 볼 수 있다.

 

액터를 삭제했을 때

플레이 도중에 액터를 삭제해보았다. 그러니까 바로 Destory 함수가 호출이 된 것을 볼 수 있었다!

 

이제 이 액터를 C++ 코드로 Transform을 건드려볼려고 한다.

 

C++로 액체 Transform 동적으로 변화하게하기

플레이를 눌렀을 때, 위로 이동하면서 회전도 하고 크기도 계속 변화하는 별을 만드려고 했다.

그래서 일단은 위치지정을 해줬고, 그 다음에 이동값과 회전값 등을 더하는 형식으로 진행했다.

크기 변화는 초기크기와 최대크기를 지정해줬고, 특정 시간에 맞춰서 크기 변화를 계속 시켜줬다.

 

사용한 코드

#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_Star_C.SM_Star_C"));
	if (MeshAsset.Succeeded())
	{
		StaticMeshComp->SetStaticMesh(MeshAsset.Object);
	}

	static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Materials/M_Power_Sphere_Inst.M_Power_Sphere_Inst"));
	if (MaterialAsset.Succeeded())
	{
		StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
	}

	PrimaryActorTick.bCanEverTick = true;

	RotationSpeed = 90.0f;
	SpeedZ = 200.0f;
	TimeAccumulator = 0.0f;
	ChangeScaleTime = 2.0f;
	MaxScale = 5.0f;

}

void AItem::BeginPlay()
{
	Super::BeginPlay();
	//SetActorLocation(FVector(300.0f, 200.0f, 100.0f));
	//SetActorRotation(FRotator(0.0f, 90.0f, 0.0f));  // FRoatator의 형태 - pitch(y축), yaw(z축), roll(x축) 순
	//SetActorScale3D(FVector(2.0f));

	FVector NewLocation(300.0f, 200.0f, 100.0f);
	FRotator NewRotation(0.0f, 90.0f, 0.0f);
	FVector NewScale(2.0f);

	InitialScale = NewScale;

	FTransform NewTransform(NewRotation, NewLocation, NewScale);

	SetActorTransform(NewTransform);

	

}

void AItem::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// 부동소수점은 오차가 굉장히 크기 때문에 불필요한 0.00000001에서도 회전을 한다.
	// 그래서 언리얼에서 제공하는 FMath의 0에 근접한지 계산하는 함수를 사용해준다.
	if (!FMath::IsNearlyZero(RotationSpeed))
	{
		AddActorLocalRotation(FRotator(0.0f, RotationSpeed * DeltaTime, 0.0f));
	}
	
    AddActorLocalOffset(FVector(0.0f, 0.0f, SpeedZ * DeltaTime));
	TimeAccumulator += DeltaTime;

	if (TimeAccumulator > ChangeScaleTime)
		TimeAccumulator -= ChangeScaleTime;

	float ScaleFactor = InitialScale.X + MaxScale * FMath::Sin((2 * PI / ChangeScaleTime) * TimeAccumulator);

	SetActorScale3D(FVector(ScaleFactor, ScaleFactor, ScaleFactor));
		
}

언리얼에서 제공해주는 유용한 함수가 많아 신기했다. 특히 FMath 클래스의 Sin 함수에서 되게 간편하게 구현할 수 있다는 느낌을 받았다.

 

결과

어디까지 올라가니..?

아주 그냥 잘 날아다니고, 제일 왕 큰 별이 되어라 내 스타야...

잘 보이진 않지만 자세히보면 별이 회전하는 부분도 보인다..! ㅎㅎ

 

마지막 말

아직 언리얼 문법에 익숙치 않아 스케일 변화할 때는 gpt의 도움을 살짝 받았다..

메서드 이름에 집착하지말고 전체적인 개발 흐름에 대한 느낌을 익혀야할 것 같다...

천천히 조금씩 꾸준히하면 무엇이든 성장할 것이다!!!! 파이팅~~!!!!