Unreal 개발 본 캠프 36일차

HappyCircle·2026년 1월 19일

Unreal 개발

목록 보기
53/163

📘 TIL – 회전 발판 & 이동 장애물 구현 (PatrolObstacle)

📌 과제 정보

  • 과제 번호: 6번
  • 주제: 회전 발판과 움직이는 장애물
  • 핵심 키워드: Tick 기반 이동, DeltaTime 프레임 독립, Overlap 반응, SpawnBounds

🎯 오늘의 목표

필수 요구사항

  • Tick 기반 이동 / 회전 구현
  • DeltaTime을 사용한 프레임 독립 처리
  • 플레이어와 충돌 시 밀려나는 반응
  • SpawnVolume 경계 밖으로 나가지 않도록 Clamp / 반사 로직 적용

🧱 BaseObstacle – Settings 기반 Move / Rotate 공통화

설계 의도

  • 이동 / 회전 로직을 BaseObstacle에 공통화
  • 장애물은 “설정값(Settings)”만 바꿔서 동작하도록 설계
  • Tick에서는 Move / Rotate만 호출

BaseObstacle.cpp (핵심)

#include "BaseObstacle.h"
#include "Components/StaticMeshComponent.h"

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

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

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

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

	MoveSettings.StartLocation = GetActorLocation();
	RotationSettings.StartRotation = GetActorRotation();
	RotationSettings.AccumulatedRotation = 0.f;
}

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

	Move(DeltaTime);
	Rotate(DeltaTime);
}

이동 로직 (Move)

void ABaseObstacle::Move(float DeltaTime)
{
	if (MoveSettings.MoveSpeed <= 0.f || MoveSettings.MaxRange <= 0.f) return;

	const FVector Cur = GetActorLocation();
	const float Dist = FVector::Dist(Cur, MoveSettings.StartLocation);

	if (Dist >= MoveSettings.MaxRange)
	{
		MoveSettings.MoveSign *= -1.f;
	}

	const FVector Dir = MoveSettings.MoveAxis.GetSafeNormal() * MoveSettings.MoveSign;
	AddActorWorldOffset(Dir * MoveSettings.MoveSpeed * DeltaTime);
}

회전 로직 (Rotate)

void ABaseObstacle::Rotate(float DeltaTime)
{
	if (!RotationSettings.bEnableRotation) return;
	if (RotationSettings.RotationSpeedPerSecond.IsNearlyZero()) return;

	AddActorLocalRotation(RotationSettings.RotationSpeedPerSecond * DeltaTime);
}

🚧 PatrolObstacle – 이동 + 회전 + 플레이어 밀어내기

핵심 아이디어
엔진 이벤트(델리게이트) → Handle 함수

실제 반응 로직 → OnObstacleBeginOverlap

인터페이스 규약을 유지한 설계

👉 “엔진 의존 코드”와 “게임 규칙 코드”를 분리

PatrolObstacle.h (핵심)

UCLASS()
class ROTATINGANDMOVING_API APatrolObstacle : public ABaseObstacle
{
	GENERATED_BODY()

public:
	APatrolObstacle();

protected:
	virtual void BeginPlay() override;
	virtual void Tick(float DeltaTime) override;

	virtual void OnObstacleBeginOverlap(AActor* OtherActor) override;
	virtual void OnObstacleEndOverlap(AActor* OtherActor) override;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Collision|Push")
	float PushStrength = 900.f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Collision|Push")
	float PushUpStrength = 120.f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Collision|Push")
	bool bPushUseObstacleToPlayerDir = true;

private:
	UFUNCTION()
	void HandleBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
		UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
		bool bFromSweep, const FHitResult& SweepResult);

	UFUNCTION()
	void HandleEndOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
		UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
};

PatrolObstacle.cpp – Overlap & 밀어내기 반응

#include "PatrolObstacle.h"
#include "GameFramework/Character.h"

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

	if (StaticMeshComp)
	{
		StaticMeshComp->SetGenerateOverlapEvents(true);
		StaticMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
		StaticMeshComp->SetCollisionResponseToAllChannels(ECR_Overlap);
	}
}

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

	if (StaticMeshComp)
	{
		StaticMeshComp->OnComponentBeginOverlap.AddDynamic(this, &APatrolObstacle::HandleBeginOverlap);
		StaticMeshComp->OnComponentEndOverlap.AddDynamic(this, &APatrolObstacle::HandleEndOverlap);
	}
}

플레이어 밀어내기 로직

void APatrolObstacle::OnObstacleBeginOverlap(AActor* OtherActor)
{
	ACharacter* Character = Cast<ACharacter>(OtherActor);
	if (!Character) return;

	FVector PushDir;

	if (bPushUseObstacleToPlayerDir)
	{
		PushDir = Character->GetActorLocation() - GetActorLocation();
		PushDir.Z = 0.f;
		PushDir = PushDir.GetSafeNormal();
	}
	else
	{
		PushDir = (MoveSettings.MoveAxis.GetSafeNormal() * MoveSettings.MoveSign);
		PushDir.Z = 0.f;
		PushDir = PushDir.GetSafeNormal();
	}

	const FVector LaunchVel =
		PushDir * PushStrength + FVector(0.f, 0.f, PushUpStrength);

	Character->LaunchCharacter(LaunchVel, true, true);
}

📦 SpawnBounds + ClampToBounds (경계 이탈 방지)

설계 포인트
SpawnVolume이 Bounds를 관리

Obstacle은 “경계 안에서만 움직인다”는 책임만 가짐

Clamp 발생 시 이동 방향 반사

BaseObstacle – Bounds 설정

void ABaseObstacle::SetSpawnBounds(const FBox& InBounds)
{
	SpawnBounds = InBounds;
	bUseSpawnBounds = true;
}

ClampToBounds 구현

bool ABaseObstacle::ClampToBounds()
{
	if (!bUseSpawnBounds) return false;

	FVector Loc = GetActorLocation();
	const FVector Min = SpawnBounds.Min;
	const FVector Max = SpawnBounds.Max;

	FVector Clamped;
	Clamped.X = FMath::Clamp(Loc.X, Min.X, Max.X);
	Clamped.Y = FMath::Clamp(Loc.Y, Min.Y, Max.Y);
	Clamped.Z = FMath::Clamp(Loc.Z, Min.Z, Max.Z);

	const bool bOut = !Loc.Equals(Clamped, 0.1f);
	if (bOut)
	{
		SetActorLocation(Clamped);
	}

	return bOut;
}

🔄 구조 요약 흐름

SpawnVolume
 └─ Bounds 생성
     └─ PatrolObstacle에 SetSpawnBounds 전달
         ├─ Tick
         │   ├─ Move (DeltaTime)
         │   ├─ Rotate (DeltaTime)
         │   └─ ClampToBounds → 반사
         └─ Overlap
             └─ OnObstacleBeginOverlap → 플레이어 밀어내기

🧠 Day 2 회고

Tick 기반 Move / Rotate 구조는 과제 요구사항을 충족하면서도 직관적

DeltaTime을 모든 이동/회전에 적용해 프레임 독립 확보

Overlap 이벤트를
엔진 델리게이트 → 인터페이스 규약 함수로 위임하니
설계가 깔끔하게 유지됨

SpawnBounds는 스포너가 관리하고
장애물은 “경계 안에서 행동”만 책임지는 구조가 가장 이상적

👉 다음 확장 포인트

Push 쿨타임 추가

Obstacle 타입별 반응 차별화

AI Pawn과의 공용 인터페이스화

profile
개발합시다!

0개의 댓글