Unreal Actor Class(2)

정혜창·2025년 1월 21일
2

내일배움캠프

목록 보기
21/43
post-thumbnail

오늘은 어제 배운 강의를 바탕으로 움직이는 액터들을 만들어 보았다. 선형으로 이동하는 액터, Sin을 활용해서 자연스럽게 반복이동과 제자리 회전하는 액터, 게임 시작후 3초 뒤에 스폰되어 10초동안 중심점을 기준으로 회전이동한 뒤 사라지는 액터를 구현해보았다.

1. 선형으로 이동하는 액터

SceneRootComponent, StaticMeshComponent는 이전 시간과 동일하게 선언하고 적절하게 스태틱메시와, 머터리얼을 지정해주었다. 그리고 MaxDistance를 멤버변수로 선언해서 변위(CurrentOffset)이 MaxDistance보다 크거나 같으면 Direction에 -1을 곱해주어 반대방향으로 이동하게끔 설계하였다.

코드

#include "MoveActor.h"

// Sets default values
AMoveActor::AMoveActor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
	SetRootComponent(SceneRoot);

	StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
	StaticMeshComp->SetupAttachment(SceneRoot);

	static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/Floor_400x400.Floor_400x400"));
	if (MeshAsset.Succeeded())
	{
		StaticMeshComp->SetStaticMesh(MeshAsset.Object);
	}

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

	MaxDistance = 300.0f;
	MoveSpeed = 100.0f;
	CurrentOffset = 0.0f;
	Direction = 1;
}

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

	if (FMath::Abs(CurrentOffset) >= MaxDistance)
	{
		Direction *= -1;
	}

	float MoveStep = MoveSpeed * DeltaTime * Direction;
	CurrentOffset += MoveStep;

	AddActorLocalOffset(FVector(MoveStep, 0.0f, 0.0f), true);
}

코드해석

  • FMath::Abs(CurrentOffset) 은 변위의 절대값을 구하는 함수이다. 이값이 MaxDistance보다 크거나 같으면 방향이 바뀌게 해주었다.
  • MoveStep 에 속도 DeltaTime 방향을 계산한 값을 넣어서 DeltaTime 당 거리값을 계산한다. 그리고 CurrentOffset에 계속 업데이트 해주어서 누적이동값을 계산하는식으로 설계하였다.
  • 최종적으로 AddActorLocalOffset(FVctor(MoveStep, 0.0f, 0.0f), true 현재 위치로부터 이동할 상대적인 벡터를 인자로
    MoveStep을 넘겨줌으로써 매 프레임마다 이동해야 할 상대적인 거리를 추가해준다.

위의 그림처럼 잘 움직이는 것을 볼 수 있다. 그러나 MaxDistance가 되면 급격하게 방향전환을 하기 때문에 자연스러운 이동을 할 수 없었다. 따라서 자연스러운 이동을 하는 방법이 없을까 생각해보니 Sin삼각함수를 이용한 액터이동이 있었다.


2. Sin(삼각함수)을 이용한 액터이동

코드

#include "RotateCar.h"

// Sets default values
ARotateCar::ARotateCar()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	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_Star_C.M_Star_C"));
	if (MaterialAsset.Succeeded())
	{
		StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
	}
	Amplitude = 300.0f;
	Frequency = 1.0f;
	RotateSpeed = 180.0f;
}

// Called when the game starts or when spawned
void ARotateCar::BeginPlay()
{
	Super::BeginPlay();
	InitialPosition = GetActorLocation();
}

// Called every frame
void ARotateCar::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	float Time = GetWorld()->GetTimeSeconds();
	float Offset = Amplitude * FMath::Sin(Frequency * Time * 2.0f * PI);

	if (!FMath::IsNearlyZero(RotateSpeed))
	{
		AddActorLocalRotation(FRotator(0.0f, RotateSpeed * DeltaTime, 0.0f));
	}
	SetActorLocation(InitialPosition + FVector(Offset, 0.0f, 0.0f));
}

코드해석

마찬가지로 컴포넌트와, 스태틱매시, 머터리얼을 구현하는 방식을 비슷하게 했다. 다만 이번엔 멤버 변수를 진폭=거리(Amplitude), 주파수=속도(Frequency)로 주었다. 우선 이렇게 멤버변수를 둔 것에 대해 이해하려면 삼각함수를 조금이나마 이해해야한다.

기본적으로 sin함수는 -1 ~ 1 사이의 값을 가진다. 주기는 반복되는 간격을 의미하므로 기본적으로 2파이를 주기로 반복된다. 여기서 Sin함수에 어떤 값을 곱해주느냐에 따라서 진폭(Amplitude)이 달라진다. 그리고 어떤 주기를 가지느냐에 따라 2파이 동안 2바퀴를 돌수도 있고 혹은 0.5바퀴를 돌 수도 있다. 주파수는 1초동안 진동 횟수를 나타낸다. 따라서 주파수에 시간을 곱하면(f * t) 특정 시간 동안 얼마나 많은 주기를 지났는지 알 수 있다. 여기에 2파이를 곱해서 각도로 변환한 값을 구하면 해당 Sin값을 알아낼 수 있다.

따라서 주파수가 커질수록 이동하는 속도가 빨라지고 진폭이 커질수록 이동거리가 길어진다. 이것을 토대로 다시 코드를 보면

    float Time = GetWorld()->GetTimeSeconds();
	float Offset = Amplitude * FMath::Sin(Frequency * Time * 2.0f * PI);

Time 은 GetWorld()->GetTimeSeconds(); 을 통해 게임을 시작하고 Seconds 값을 Time에 준다. 그리고 위의 식을 그대로 넣어서 FMath::Sin(Frequency * Time * 2.0f * PI) 을 계산하고 Amplitude를 곱해주면 Offset은 시간에 따라 -Amplitude ~ Amplitude 값을 가지게 된다.
이값을 통해 SetActorLocation(InitialPosition + FVector(Offset, 0.0f, 0.0f)); 을 함수를 통해 최종 위치를 구할 수 있게된다.


액터자체 회전도 추가하기 위해 AddActorLocalRotation(FRotator(0.0f, RotateSpeed * DeltaTime, 0.0f)); 을 추가해주었다.

이전과 달리 최대 거리에서 속도가 느려지고 중간에서 최대속도를 내는 것을 볼 수 있다. 훨씬 자연스러운 움직임이여서 만족스러웠다.


3. 스폰 및 삭제 되면서 반경을 회전하는 액터

코드

#include "SpawnActor.h"

// Sets default values
ASpawnActor::ASpawnActor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

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

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

	Radius = 300.0f;
	AngularSpeed = PI;
	CurrentAngle = 0.0f;
}

// Called when the game starts or when spawned
void ASpawnActor::BeginPlay()
{
	Super::BeginPlay();
	FTimerHandle TimerHandle;

	GetWorld()->GetTimerManager().SetTimer(
		TimerHandle,
		this,
		&ASpawnActor::StartRotation,
		3.0f,
		false
	);
}

void ASpawnActor::StartRotation()
{
	CenterPosition = GetActorLocation();
	SetLifeSpan(10.0f);
}

void ASpawnActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	
	if (GetLifeSpan() <= 0.0f)
	{
		return;
	}

	CurrentAngle += AngularSpeed * DeltaTime;

	float X = CenterPosition.X + Radius * FMath::Cos(CurrentAngle);
	float Y = CenterPosition.Y + Radius * FMath::Sin(CurrentAngle);
	float Z = CenterPosition.Z;

	SetActorLocation(FVector(X, Y, Z));
}

게임에서는 특정 작업을 지연시켜 실행하거나, 일정 시간 간격으로 반복 실행해야 하는 경우가 자주 발생한다. Unreal Engine의 타이머 시스템은 이러한 작업을 간단하고 정확하게 처리할 수 있도록 설계되어있다.

SetTimer의 역할

  • 타이머를 등록하고 시간추적, 함수호출 예약한다.

TimerHandle(타이머핸들)

  • 타이머를 식별하고, 제어하거나 추적하기 위해 사용된다. 이 핸들을 사용하여 타이머를 취소할 수도 있고, 활성 상태인지 확인할 수 있다.

따라서 호출할 함수가 속한 객체를 저장하고 원하는 시간을 입력하면 SetTimer를 통해 타이머가 만료되었을 때, 지정된 함수를 호출 할 수 있다. 타이머가 만료되었을 때 실행할 함수를 미리 알고 있어야 하므로 함수 포인터를 넘겨준다. 따라서 이 부분의 코드를 다시 살펴보면

	GetWorld()->GetTimerManager().SetTimer(
		TimerHandle,
		this,
		&ASpawnActor::StartRotation,
		3.0f,
		false
	);
  1. SetTimer 호출 시, Unreal Engine은 타이머를 등록하고, FTimerHandle 과 함께 관련 데이터를 저장한다.

  2. Unreal Engine은 매 프레임마다 등록된 타이머의 남은 시간을 갱신한다(확인한다) 남은시간이 0에 도달하면 타이머가 만료되었다고 판단한다.

  3. 타이머가 만료되면 this(여기서는 ASpawnActor) 객체에서 StartRotation 함수가 호출된다.



이제 3초뒤에 원하는 객체에서 해당 함수를 실행할 것을 작성하였으므로 이제 기점을 중심으로 회전하는 것만 구현하면 된다. 우선 CenterPosition(중심점)을 배치되어있는 액터의 위치로 저장하고, 10초뒤에 소멸하기 위해 SetLifeSpan(10.0f)로 설정해주었다. 이후 본격적으로 구현한 Tick 라이프사이클 함수로 가보면 GetLifeSpan() <= 0.0f의 조건을 통해 액터의 수명이 없거나 이미 끝났다면, 함수의 나머지 코드를 실행하지 않고 return(종료)하도록 설정하였다.

만약 LifeSpan(수명)이 남아있다면 아래 코드를 실행하게 되는데 우선 초당 PI(180도) 만큼 도는 AngularSpeed 변수를 지정하고 시간에 따라 업데이트되는 각 CurrentAngle을 AngularSpeed * DeltaTime 값을 통해 업데이트 되도록 해주었다. 이후 액터의 위치를 구하기위해 X,Y 좌표를 구하는것을 알려면 sin, cos함수를 알아야하는데

이그림을 통해 반지름이 1일때 X,Y 좌표는 (rcos(θ), rsin(θ)) 인 것을 알 수 있다. 따라서 월드기준에서의 X,Y 좌표는

float X = CenterPosition.X + Radius * FMath::Cos(CurrentAngle);
float Y = CenterPosition.Y + Radius * FMath::Sin(CurrentAngle);
float Z = CenterPosition.Z;

이라는 것을 이해할 수 있다. Z축으로는 움직이지 않기 때문에 그대로 CenterPosition.Z로 두고 이제 필요한 X,Y,Z 좌표를 모두 구했으므로 매 틱마다 갱신되는 액터의 백터를 구할 수 있다.

SetActorLocation(FVector(X,Y,Z));

결과를 보면업로드중..

잘 작동하는 것을 볼 수 있다.


회고

하지만 완벽하게 없는상태에서 스폰되는게 아닌 액터가 배치되어 있는 상태에서 다른 곳에서 스폰되어 회전을 하는 형식이다. 액터를 따로 배치하지 않고 게임 시작후 원하는 시간 이후에 스폰되는 방식을 알아보아야겠다.

오늘 액터이동을 여러가지 구현하면서 게임수학이 너무나 중요하다는 것을 알게되었다. 좀 더 깊게 알수록 풍부하게 구현할 수 있겠다는 생각이 든다. 아직 SetTimer 부분의 개념이 매끄럽지 못하다. 타이머가 지나고 원하는 함수를 구현하기 위해서는 반환값과 매개변수가 없는 함수를 포인터로 예약해야하는데 매개변수가 있는 함수를 실행하기 위해서는 다른 방법이 필요하다. 이부분도 조금 더 알아보아야겠다.

profile
Unreal 1기

0개의 댓글

관련 채용 정보