UE C++ 회전·이동·타이머 발판과 랜덤 스포너
베이스 액터 하나를 bool 분기로 공유하기
발판 종류가 넷으로 늘면 클래스를 종류마다 파고 싶어진다. 베이스 액터를 bool 분기로 공유하는 구조를 먼저 잡고, FTimerHandle 주기 동작과 SpawnActor 랜덤 스폰, Tick 낙사 리스폰을 얹었다.
회전 발판과 움직이는 장애물을 C++로 만들었다. 발판 종류가 회전·이동·타이머·랜덤 스포너까지 늘어나는 과제라, 종류마다 클래스를 새로 파는 대신 베이스 액터 하나를 bool 분기로 공유하는 구조부터 잡고 시작했다. 이 글에서는 그 구현 네 가지를 순서대로 이야기하려 한다 — 베이스 발판의 회전·이동 분기, FTimerHandle로 주기 동작을 거는 타이머 발판, SpawnActor 랜덤 스포너, 그리고 Tick으로 낙사를 감지해 되돌리는 리스폰이다.
BasePlatformActor — bool로 회전/이동 분기
회전 발판과 이동 발판을 클래스마다 따로 구현하는 대신, 단일 베이스 클래스 ABasePlatformActor에 회전/이동 로직을 전부 넣고 bool 변수로 분기하게 잡았다. ARotatingPlatform, AMovingPlatform 자식 클래스는 생성자에서 기본값만 오버라이드하면 된다. 이렇게 하면 발판 종류가 늘어도 동작 로직은 한 곳에만 있다.
핵심은 매 프레임 도는 이 분기다. 이동은 왕복이라 누적 오프셋이 MaxRange를 넘는 순간 방향을 뒤집는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 회전
if (bShouldRotate)
AddActorLocalRotation(FRotator(0.f, RotationSpeed * DeltaTime, 0.f));
// 이동 (왕복)
if (bShouldMove)
{
CurrentOffset += MoveDir * MoveSpeed * DeltaTime;
if (FMath::Abs(CurrentOffset) >= MaxRange)
{
MoveDir *= -1.f;
CurrentOffset = FMath::Sign(CurrentOffset) * MaxRange;
}
// EMoveAxis(X/Y/Z)에 따라 위치 적용
SetActorLocation(NewLocation);
}
이동 축은 EMoveAxis 열거형으로 빼서, 에디터 Details 패널에서 드롭다운으로 고를 수 있게 했다.
1
2
3
4
5
6
7
UENUM(BlueprintType)
enum class EMoveAxis : uint8
{
X UMETA(DisplayName = "X (앞뒤)"),
Y UMETA(DisplayName = "Y (좌우)"),
Z UMETA(DisplayName = "Z (상하)")
};
자식 클래스가 하는 일은 정말 기본값 두 줄이 전부다.
1
2
3
4
5
6
7
// ARotatingPlatform 생성자
bShouldRotate = true;
bShouldMove = false;
// AMovingPlatform 생성자
bShouldRotate = false;
bShouldMove = true;
| 클래스 | 역할 | 기본 활성 |
|---|---|---|
| ABasePlatformActor | 회전 + 이동 로직 전체 보유 | 둘 다 false |
| ARotatingPlatform | 회전 전용 | bShouldRotate = true |
| AMovingPlatform | 이동 전용 | bShouldMove = true |
TimerPlatform — FTimerHandle 도전과제
도전과제는 발판이 일정 시간 후 사라지고, 또 일정 시간 후 복구되는 사이클을 반복하는 것. FTimerHandle(예약해 둔 타이머를 식별·해제할 때 쓰는 핸들)을 두 개 쓰고, 각 타이머를 일회성으로 걸어 서로를 예약하게 하면 사이클이 유지된다.
1
2
3
4
BeginPlay
└→ [ActiveDuration 후] DeactivatePlatform (숨김 + 콜리전 제거)
└→ [InactiveDuration 후] ActivatePlatform (복구)
└→ [ActiveDuration 후] DeactivatePlatform ... (반복)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void ATimerPlatform::DeactivatePlatform()
{
SetActorHiddenInGame(true);
SetActorEnableCollision(false);
// 일회성 타이머 → ActivatePlatform 호출
GetWorld()->GetTimerManager().SetTimer(
InactiveTimerHandle, this,
&ATimerPlatform::ActivatePlatform,
InactiveDuration, false
);
}
void ATimerPlatform::ActivatePlatform()
{
SetActorHiddenInGame(false);
SetActorEnableCollision(true);
// 다시 DeactivatePlatform 예약 (사이클 유지)
GetWorld()->GetTimerManager().SetTimer(
ActiveTimerHandle, this,
&ATimerPlatform::DeactivatePlatform,
ActiveDuration, false
);
}
SetTimer(..., false)일회성으로 서로 호출 → 정상 사이클 동작
RandomSpawner — SpawnActor + 랜덤 도전과제
스포너의 동작은 두 갈래다. BeginPlay에서는 플레이어 위치 기준으로 초기 발판 N개를 1초 간격 타이머로 순차 스폰하고, Tick에서는 플레이어가 마지막 발판 800cm 이내로 접근하면 추가 배치를 재시작한다.
스폰 위치는 X축 앞 방향으로만 나가게 했다. 앞으로 가는 거리는 항상 양수로 뽑고, 좌우와 높이는 범위 안에서 분산시킨다.
1
2
3
4
5
6
7
float ForwardDist = FMath::RandRange(MinSpawnDistance, MaxSpawnDistance);
float LateralDist = FMath::RandRange(-MaxSpawnDistance * 0.4f, MaxSpawnDistance * 0.4f);
float Height = FMath::RandRange(-HeightVariation, HeightVariation);
SpawnLocation.X += ForwardDist; // 항상 앞으로
SpawnLocation.Y += LateralDist; // 좌우 분산
SpawnLocation.Z += Height;
스폰된 발판에는 회전 속도·이동 속도·이동 범위·이동 축을 랜덤으로 부여한다.
1
2
3
4
NewPlatform->RotationSpeed = FMath::RandRange(30.f, 150.f);
NewPlatform->MoveSpeed = FMath::RandRange(100.f, 350.f);
NewPlatform->MaxRange = FMath::RandRange(150.f, 400.f);
NewPlatform->MoveAxis = static_cast<EMoveAxis>(FMath::RandRange(0, 1));
발판 사이 거리는 감으로 정하지 않고 캐릭터 스탯에서 역산했다. JumpZVelocity=700, MaxWalkSpeed=500 기준으로 최대 점프 수평 거리가 약 700cm라서, MaxSpawnDistance는 550cm, SpawnInterval은 2초로 잡았다.
낙사 리스폰 — Character Tick
낙사 처리는 단순하게 캐릭터 Tick에서 Z 좌표를 검사하고, 기준 이하로 떨어지면 시작 위치로 순간이동시킨다.
1
2
3
4
5
6
7
8
9
10
void ANBC_AssignmentCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (GetActorLocation().Z < FallDeathZ)
{
SetActorLocation(RespawnLocation);
GetCharacterMovement()->Velocity = FVector::ZeroVector;
}
}
기준 높이와 리스폰 위치는 UPROPERTY로 노출해서 에디터에서 바로 조정할 수 있게 했다.
1
2
3
4
5
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Respawn")
float FallDeathZ; // 기본값 -500.f
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Respawn")
FVector RespawnLocation; // 기본값 (0, 0, 90)
여기서 놓치기 쉬운 게 Velocity = FVector::ZeroVector다. 낙하 속도를 초기화하지 않으면 리스폰 직후 캐릭터가 튕긴다.
핵심 요약 — 발판 동작을 베이스 클래스 한 곳에 몰아넣고 자식은 bool 기본값만 바꾸게 하면, 발판 종류가 늘어도 로직 수정이 한 곳에서 끝난다. 그리고 일회성 타이머 두 개가 서로를 예약하게 하면 반복 타이머 없이도 사라짐/복구 사이클을 만들 수 있다.