TIL(Today I Learned)
6번 과제인 회전 발판과 움직이는 장애물 과제를 수행한 과정을 적어보겠다.
C++로 액터를 생성한 뒤 움직이고, 회전시키는 장애물로 레벨을 구성해보는 것이 필수 과제,
랜덤하게 스폰하는 액터를 만드는 것이 도전과제이다.
코드를 작성하기에 앞서 캐릭터를 리스폰 해주는 로직을 블루프린트로 만들었다
전에 리뷰한 Side Scroller에 있었던 리스폰 로직을 활용했다.
PlayerController 클래스에서 Possess 중인 액터가 Destroy되면, PlayerStart의 트랜스폼 위치에 캐릭터를 새로 Spawn하고, 해당 캐릭터를 다시 Possess한다.

Character 블루프린트에는 특정 높이가 되었을 때 Character를 Destroy 해주었다.
원래 떨어지면 Ragdoll 효과를 주고 싶었서 바닥에 벽을 설치하고 Physics Simulation을 켜주었으나 벽에 부딪히는것보다 밑으로 떨어지는게 자연스러워서 벽은 제거하였다.

특정 높이에서 캐릭터가 Destroy 되는 것은 월드 세팅의 킬 Z에서 쉽게 설정할 수 있다.


새 프로젝트를 C++로 생성을 해주고, 새로운 C++클래스를 액터로 생성한다.

처음 액터를 생성하면 생기는 .h 파일이다.
UCLASS()
이 클래스를 언리얼에서 인식하게 해줌 (에디터에서 보이게 함)
GENERATED_BODY()
엔진이 자동으로 내부 코드를 붙여줌 (반드시 있어야 함)
UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
GENERATED_BODY()
public:
// 생성자
AItem();
protected:
// 액터가 월드에 '배치된 직후' 한 번만 호출
virtual void BeginPlay() override;
// 매 프레임마다 자동으로 호출
virtual void Tick(float DeltaTime) override;
};
다음 .cpp 파일이다.
#include "Item.h" // 자기 자신과 짝이 되는 헤더를 가장 먼저 include해야 함.
// 생성자 구현부
AItem::AItem()
{
PrimaryActorTick.bCanEverTick = true; // (이후 강의에서 배우는 내용)
}
// BeginPlay() 구현부
void AItem::BeginPlay()
{
// 부모 클래스(AActor)의 BeginPlay()를 먼저 호출
Super::BeginPlay();
}
// Tick() 구현부
void AItem::Tick(float DeltaTime)
{
// 부모 클래스(AActor)의 Tick() 먼저 호출
Super::Tick(DeltaTime);
}
이제 이 클래스에 3D 메시를 할당해주기 위해서는 액터에 추가적인 컴포넌트 (Component)를 붙여줘야 한다.
컴포넌트는 언리얼 엔진에서 Actor가 어떤 역할을 하거나 특정 속성을 갖도록 만들어주는 부품 (파츠) 개념이다.
하나의 Actor가 여러 종류의 컴포넌트를 조합하여 다양한 기능을 구현할 수 있다.
Root Component
모든 Actor는 하나의 Root Component를 가져야 한다.
이 Root가 액터의 위치, 회전, 크기(트랜스폼)의 기준이다.
다른 모든 컴포넌트는 Root에 붙는 하위 컴포넌트다.
Scene Component
트랜스폼(위치/회전/크기) 기능만 있는 기본 컴포넌트
눈에 보이지 않음 (Mesh, Light 같은 시각적 요소는 아님)
보통 루트로 사용해서 아래에 다른 컴포넌트를 붙임
언리얼 에디터에서 툴 -> 새로운 C++클래스
C++ 액터 클래스를 생성한다.

플랫폼은 기본적으로 Scene컴포넌트와 Static Mesh를 갖고 헤더파일에 아래 코드를 추가해준다.
UPROPERTY()는 언리얼 엔진의 리플렉션 시스템이다.
리플렉션 시스템이란
언리얼 엔진이 C++ 코드에 정의된 클래스, 변수, 함수 등의 정보를 런타임 및 에디터에서 동적으로 탐색하고 사용하는 기능이다.
주요 키워드
EditAnywhere 에디터에서 수정 가능
VisibleAnywhere 에디터에서 보기만 가능
BlueprintReadWrite 블루프린트에서 읽기/쓰기 가능
BlueprintReadOnly 블루프린트에서 읽기만 가능
Category 에디터의 디테일 패널에서 그룹화
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "MovingPlatform|Components")
USceneComponent* SceneRoot;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "MovingPlatform|Components")
UStaticMeshComponent* StaticMeshComp;
소스파일에서,
플랫폼 액터는 SceneRoot를 루트 컴포넌트로 사용하고, 그 위에 StaticMeshComp를 부착한다.
SceneRoot는 액터의 기준 위치를 잡아주는 역할을 하며, StaticMeshComp는 실제 시각적으로 보이는 플랫폼 메시를 담당한다.
StaticMeshComp는 SceneRoot에 SetupAttachment로 연결되며, 이후 메시 설정과 충돌 설정은 StaticMeshComp를 통해 제어한다.
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject <UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMeshComp->SetupAttachment(SceneRoot);
계속해서, 움직이는 플랫폼을 구현해준다.
방향에 따라 X 좌표를 증가하고 감소시켜주는 것으로 이동을 구현했고,
최대거리를 벗어나지 않도록 Clamp하고 목표 거리에 도달하면 방향을 반대로하였다.
최대한 간단한 구현을 위해 한쪽 방향으로만 이동시켰다.
void AMovingPlatform::Move(float DeltaTime)
{
FVector CurrentLocation = GetActorLocation();
float Target = (Direction == 1) ? EndLocation.X : StartLocation.X;
CurrentLocation.X += MovingSpeed * DeltaTime * Direction;
float Min = FMath::Min(StartLocation.X, EndLocation.X);
float Max = FMath::Max(StartLocation.X, EndLocation.X);
CurrentLocation.X = FMath::Clamp(CurrentLocation.X, Min, Max);
SetActorLocation(CurrentLocation);
if (FMath::IsNearlyZero(CurrentLocation.X - Target))
{
Direction *= -1;
}
}
Move 함수는 Tick()함수에서 호출해준다.
Tick() 함수는 Unreal Engine에서 매 프레임 자동으로 호출되어 액터의 지속적인
동작을 처리한다.
이를 사용하려면 생성자에서 PrimaryActorTick.bCanEverTick = true;로 활성화해야 하며,
Tick(float DeltaTime) 함수를 오버라이드하여 그 위에 원하는 동작을 구현한다.
게임이 시작된 후부터 종료될 때까지 매 프레임마다 Tick()이 호출되어 실시간 처리를 수행한다.
2초 간격으로 움직이는 플랫폼이다.

다음으로 회전하는 플랫폼이다.
간단하게 액터의 Rotation을 설정해주면 끝이다. 마찬가지로 Tick()에서 호출한다.
void ARotatingPlatform::Rotate(float DeltaTime)
{
if (!FMath::IsNearlyZero(RotationSpeed))
{
AddActorLocalRotation(FRotator(0.0f, RotationSpeed * DeltaTime, 0.0f));
}
}
오브젝트의 머티리얼이 단색이라서 회전이 잘 보이지 않는다.
벽을 설치하여 회전 반대방향으로 이동하도록 유도하였다.

그리고, 회전 플랫폼 위에서는 회전 방향으로 카메라가 계속 회전하기 때문에
캐릭터 컴포넌트에서 베이스 회전 무시를 켜주어야 한다.

다음은 일정시간마다 사라지는 것을 반복하는 플랫폼이다.
TogglePlatform함수에서 IsVisible 값을 매 초마다 반대 값으로 변경한 후
SetActorHiddenInGame(액터 감추기)과 SetActorEnableCollision(액터 충돌) 을 서로 반대의 bool값으로 설정한 뒤 이를 반복한다.
void AOnOffPlatfrom::TogglePlatform()
{
IsVisible = !IsVisible;
SetActorHiddenInGame(!IsVisible);
SetActorEnableCollision(IsVisible);
}
위 함수는 BeginPlay에서 사용한다.
SetTimer() 함수는 일정 시간 간격으로 특정 함수를 반복 실행하도록 예약하는 기능으로
TogglePlatform() 함수를 2초마다 반복해서 자동 호출하가 위해 타이머를 호출한다.
true는 반복 호출을 의미하고,
Timer는 이 타이머를 관리하기 위한 핸들로 헤더파일에 FTimerHandle Timer를 선언하였다.
void AOnOffPlatfrom::BeginPlay()
{
Super::BeginPlay();
GetWorld()->GetTimerManager().SetTimer(
Timer,
this,
&AOnOffPlatfrom::TogglePlatform,
2.0f,
true
);
}
OnOff 플랫폼

이제 랜덤하게 생성되는 플랫폼에 추가 효과를 주기 위해,
플랫폼을 밟으면 사라지게 만들고 그 플랫폼을 랜덤한 위치에 생성하도록 만들어보겠다.
먼저 헤더파일에 콜리전 박스를 생성한다.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "VanishPlatform|Components")
class UBoxComponent* CollisionBox;
이어서 소스파일로 가서 오버랩 이벤트를 구현한다.
플레이어가 CollisionBox에 오버랩되면 OnOverlapBegin()에서 일정 시간 후 Vanish() 함수를 호출하는 타이머를 설정한다.
Vanish()에서는 플랫폼을 보이지 않게 하고 충돌도 끈 뒤, 다시 일정 시간이 지나면
ReSpawn() 함수가 호출되도록 다시 타이머를 설정한다.
ReSpawn()에서는 플랫폼을 다시 보이게 하고 충돌도 복원한다.
void AVanishPlatform::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
if (OtherActor && OtherActor != this)
{
GetWorldTimerManager().SetTimer(Timer, this, &AVanishPlatform::Vanish, Delay, false);
}
}
void AVanishPlatform::ReSpawn()
{
SetActorHiddenInGame(false);
SetActorEnableCollision(true);
}
void AVanishPlatform::Vanish()
{
SetActorHiddenInGame(true);
SetActorEnableCollision(false);
GetWorldTimerManager().SetTimer(Timer, this, &AVanishPlatform::ReSpawn, Delay, false);
}

마지막으로 위 플랫폼을 랜덤 스폰시키는 액터를 만든다.
최대 시도 횟수를 설정해가며, 랜덤스폰 액터의 위치로부터 주변 범위에 무작위 위치를 생성한다.
새 위치가 중복된 위치가 아니면 플랫폼을 생성하고, 성공하면 생성한 플랫폼 목록에 추가하며 생성 개수를 증가시킨다.
void ARandomPlatformSpawner::SpawnPlatforms()
{
if (!PlatformClass)
{
UE_LOG(LogTemp, Error, TEXT("PlatformClass"));
return;
}
UWorld* World = GetWorld();
if (!World)
{
UE_LOG(LogTemp, Error, TEXT("World"));
return;
}
int32 Spawned = 0;
int32 MaxAttempts = SpawnCount * 10;
int32 Attempts = 0;
while (Spawned < SpawnCount && Attempts < MaxAttempts)
{
Attempts++;
FVector RandomLocation = GetActorLocation();
RandomLocation.X += FMath::FRandRange(-SpawnAreaExtent.X, SpawnAreaExtent.X);
RandomLocation.Y += FMath::FRandRange(-SpawnAreaExtent.Y, SpawnAreaExtent.Y);
RandomLocation.Z = GetActorLocation().Z;
if (IsLocationValid(RandomLocation))
{
FActorSpawnParameters Params;
Params.Owner = this;
AVanishPlatform* NewPlatform = World->SpawnActor<AVanishPlatform>(PlatformClass, RandomLocation, FRotator::ZeroRotator, Params);
if (NewPlatform)
{
SpawnedPlatforms.Add(NewPlatform);
Spawned++;
}
}
}
}
