[CH3/06] 회전 발판과 움직이는 장애물 퍼즐 스테이지

김여울·2025년 7월 9일
0

내일배움캠프

목록 보기
40/111

회전 발판과 움직이는 장애물 퍼즐 스테이지

🧩 만들 것

  • 회전하는 발판
  • 슬라이딩 도어(위로)
  • 시간 지연 후 폭발하는 탱크(드럼통) + 파티

1️⃣ 회전 발판 (RotatingPlatform)

  • Tick()에서 AddActorLocalRotation(RotationSpeed * DeltaTime)으로 회전

  • FRotator 타입의 RotationSpeedUPROPERTY(EditAnywhere)로 지정해 블루프린트에서 설정 가능

  • 회전축 조정 시 피벗 위치가 중요하며, 회전이 꼭짓점 기준으로 되면 피벗을 수정해야 함

  • 피벗 수정은 StaticMeshAsset 에디터에서 "Enable Pivot Editing" 버튼으로 조정 가능
    📎 [Unreal UE5] 피벗 옮기기
    📎 언리얼 5 - 액터의 피벗 위치 변경하기 (How to Change Actor Pivots)

  • Yaw(Y축) 기준 회전이 어긋나면 Roll(X축)으로 변경 시 해결될 수 있음

    // 생성자
    PlatformMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("PlatformMesh"));
    RootComponent = PlatformMesh;
    RotationSpeed = FRotator(0.0f, 90.0f, 0.0f);  // Y축 기준 90도 회전

📄 RotatingPlatform.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "RotatingPlatform.generated.h"

UCLASS()
class MYPROJECT_API ARotatingPlatform : public AActor
{
	GENERATED_BODY()
	
public:	
	ARotatingPlatform();	// 생성자

	virtual void Tick(float DeltaTime) override;

protected:
	virtual void BeginPlay() override;

private:	
	// 피벗 보정
	UPROPERTY()
	USceneComponent* RootScene;

	UPROPERTY()
	UStaticMeshComponent* PlatformMesh;

public:
	// 회전 속도 (에디터에서 조절 가능)
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Rotation")
	FRotator RotationSpeed;
};

✨ RotatingPlatform.cpp

#include "RotatingPlatform.h"

ARotatingPlatform::ARotatingPlatform()
{
	PrimaryActorTick.bCanEverTick = true;

	// 중심 잡기 위한 Scene 루트 생성
	RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
	RootComponent = RootScene;

	// 메쉬 생성 및 루트에 붙이기
	PlatformMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("PlatformMesh"));
	PlatformMesh->SetupAttachment(RootScene);

	// ✔ 메쉬 피벗 보정 위치 설정 
	// 아래 값은 발판 높이의 절반만큼 내림
	// 예: 메쉬 높이가 100이라면 -50 하면 중심 맞음
	PlatformMesh->SetRelativeLocation(FVector(0.f, 0.f, 200.f)); // 값은 직접 조절해봐야 정확해

	// ✔ 회전 방향: Roll (Y축 기준 회전)
	RotationSpeed = FRotator(0.f, 0.f, 90.f);
}

void ARotatingPlatform::BeginPlay()
{
	Super::BeginPlay();
}

// Tick 함수에서 회전 처리
void ARotatingPlatform::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// 프레임 독립적 움직임 -> * DeltaTime
	AddActorLocalRotation(RotationSpeed * DeltaTime);

}

피벗 보정을 두 가지 방식으로 시도했지만 안된다...

2️⃣ 슬라이딩 도어 (MovingDoor)

  • Tick()에서 DeltaTime을 곱한 거리만큼 Z축 이동
    → 프레임 독립성을 보장

  • StartLocation, MaxRange, MoveSpeed, bMovingUp 사용

  • 일정 거리 도달 시 방향 반전

📄 MovingDoor.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MovingDoor.generated.h"

UCLASS()
class MYPROJECT_API AMovingDoor : public AActor
{
	GENERATED_BODY()
	
public:	
	AMovingDoor();
	virtual void Tick(float DeltaTime) override;

protected:
	virtual void BeginPlay() override;

private:	
	UPROPERTY()
	USceneComponent* RootScene;

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent* DoorMesh;

	// 시작 위치 저장
	FVector StartLocation;

	// 방향 전환 (왕복 이동 범위 계산 때 필요)
	// true -> 위로, false -> 아래로
	bool bMovingUp;

public:
	// 에디터에서 조절 가능하게
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door Settings")
	float MoveSpeed = 100.f;

	// 위로 올라갈 수 있는 최대 높이
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door Settings")
	float MaxRange = 200.f;
};

✨ MovingDoor.cpp

#include "MovingDoor.h"

AMovingDoor::AMovingDoor()
{
	PrimaryActorTick.bCanEverTick = true;

	RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
	RootComponent = RootScene;

	DoorMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("DoorMesh"));
	DoorMesh->SetupAttachment(RootScene);
}

void AMovingDoor::BeginPlay()
{
	Super::BeginPlay();

	StartLocation = GetActorLocation(); // 처음 위치 저장
	bMovingUp = true; // true -> 처음엔 위로 올라감
}

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

	// 현재 문 위치 받아옴
	FVector CurrentLocation = GetActorLocation();
	float MoveDelta = MoveSpeed * DeltaTime;

	// 위로 이동 중
	if (bMovingUp)
	{
		CurrentLocation.Z += MoveDelta;	// Z위치 값 더하기 -> 위로 움직임
		if (CurrentLocation.Z >= StartLocation.Z + MaxRange)	// 최대 높이 넘거나 도달하면
		{
			CurrentLocation.Z = StartLocation.Z + MaxRange;	// 그 위치에 멈추고
			bMovingUp = false;	// 다음 프레임부터는 내려감
		}
	}
	else
	{
		CurrentLocation.Z -= MoveDelta;	// Z위치 값 줄이기 -> 아래로 움직임
		if (CurrentLocation.Z <= StartLocation.Z)	// 시작 위치보다 낮아지면
		{
			CurrentLocation.Z = StartLocation.Z;	// 원래 위치로 고정
			bMovingUp = true;	// 다음 프레임부터는 위로 올라감
		}
	}

	SetActorLocation(CurrentLocation);
}

3️⃣ 폭발 탱크 (ExplodingTank)

  • 일정 시간 뒤 Explode() 함수 호출 → 메시 숨기고 충돌 제거

  • GEngine->AddOnScreenDebugMessage() 로 디버그 메시지 띄움

    • UE_LOG()는 콘솔에서만 보임!
    • 화면에 출력하려면 GEngine 사용
  • 디버그 메시지가 안 보이면 Play In Editor (PIE) 모드나 Output Log 설정 확인

    GEngine->AddOnScreenDebugMessage(
       -1,             // 키값: -1이면 새로운 메시지로 계속 추가
       2.0f,           // 2초간 표시
       FColor::Green,
       TEXT("BOOM! ExplodingTank exploded!")
    );
  • 파티클 시스템(P_Explosion) 사용

    UPROPERTY(EditAnywhere)
    UParticleSystem* ExplosionEffect;
    UGameplayStatics::SpawnEmitterAtLocation(
       GetWorld(),
       ExplosionEffect,
       GetActorLocation(),
       FRotator::ZeroRotator,
       FVector(3.0f)  // 불이 너무 작아서 크기 3배 확대
    );

📄 ExplodingTank.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ExplodingTank.generated.h"

DECLARE_LOG_CATEGORY_EXTERN(LogExplodingTank, Log, All);

UCLASS()
class MYPROJECT_API AExplodingTank : public AActor
{
	GENERATED_BODY()
	
public:	
	AExplodingTank();

protected:
	virtual void BeginPlay() override;

private:
	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent* TankMesh;

	// 폭발 타이머 핸들
	FTimerHandle ExplosionTimer;

	UPROPERTY(EditAnywhere, Category = "Effect")
	UParticleSystem* ExplosionEffect;

	// 폭발 처리 함수
	UFUNCTION()
	void Explode();

public:
	// 대기 시간 (에디터에서 조절 가능)
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Explosion Settings")
	float ExplosionDelay = 3.0f;
};

✨ ExplodingTank.cpp

#include "ExplodingTank.h"
#include "TimerManager.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"

DEFINE_LOG_CATEGORY(LogExplodingTank);

AExplodingTank::AExplodingTank()
{
	// 메시 설정, 루트 컴포넌트 설정
	TankMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("TankMesh"));
	RootComponent = TankMesh;
}


void AExplodingTank::BeginPlay()
{
	Super::BeginPlay();

	// 일정 시간 뒤에  Explode 실행
	GetWorld()->GetTimerManager().SetTimer(
		ExplosionTimer,
		this,
		&AExplodingTank::Explode,
		3.0f,
		false	// 반복 안 함
	);
}

void AExplodingTank::Explode()
{
	// 폭발하고 메시 숨기기, 충돌 끄기
	TankMesh->SetVisibility(false);
	TankMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);

	// 폭발 파티클 재생
	if (ExplosionEffect)
	{
		UGameplayStatics::SpawnEmitterAtLocation(
			GetWorld(),
			ExplosionEffect,
			GetActorLocation()
		);
	}

	// 폭발 로그 출력
	UE_LOG(LogTemp, Warning, TEXT("BOOM! ExplodingTank exploded!"))

	// 월드에 출력
	if (GEngine)
	{
		GEngine->AddOnScreenDebugMessage(
			-1,
			5.0f,
			FColor::Red,
			TEXT("BOOM! ExplodingTank exploded!"));
	}

	// 폭발 이펙트 생성
	UGameplayStatics::SpawnEmitterAtLocation(
		GetWorld(),
		ExplosionEffect,
		GetActorLocation(),
		FRotator::ZeroRotator,
		FVector(3.0f) // 크기 확대
	);
}

💥 저지른 실수

  • UPROPERTY()는 클래스 스코프 안에 있어야 함
    함수 밖, 클래스 블록 안에 선언해야 빌드 오류 없음

  • 피벗 편집 기능은 일부 StaticMesh에는 비활성화되어 있을 수 있음
    계속 안 고쳐져서 축이 이상하다...

  • 디버그 메시지가 화면에 안 보이면 GEngine->AddOnScreenDebugMessage() 사용

  • Yaw 값으로 회전축이 원하는 대로 안 될 경우, Roll 또는 Pitch로 바꿔 시도

💡 기억하자

  • SceneComponent는 위치와 회전(트랜스폼)의 기준이 된다 그래서 Root Component로 사용한다

  • 모든 DeltaTime 기반 로직은 프레임 독립성을 보장해준다

  • 디버그용 메시지는 GEngine으로 시각화, 로그용은 UE_LOG

0개의 댓글