터널 시스템 블루프린트에서 C++로 전환하기

문제:

블루프린트로 만든 BP_TunnelTrigger를 C++로 다시 작성해야 함

  • 블루프린트는 Git에서 diff 보기 어려움
  • C++은 컴파일 시점에 에러 잡힘
  • 노드 그래프보다 텍스트가 협업에 유리

기존 코드 확인

 

작업 전에 차량 폰에 이미 작성된 게 있는지 확인

DECLARE_MULTICAST_DELEGATE_OneParam(FOnTunnelStateChanged, bool);

FOnTunnelStateChanged OnTunnelToggleDelegate;

UFUNCTION(BlueprintCallable, Category="Tunnel")
void SetInTunnel(bool bNewInTunnel);

 

팀원이 델리게이트랑 SetInTunnel 함수 다 만들어놨음

새로 만들 필요 없이 그대로 호출만 하면 됨

 

중복 코드 만들기 전에 기존 코드부터 확인하자


델리게이트 패턴

 

처음에 헷갈렸던 부분 정리

DECLARE_MULTICAST_DELEGATE_OneParam(FOnTunnelStateChanged, bool);

 

터널 상태 변화 알림 채널 생성

FOnTunnelStateChanged OnTunnelToggleDelegate;

 

차량 폰이 채널 소유

OnTunnelToggleDelegate.Broadcast(bNewInTunnel);

 

알림 발송, 구독한 모든 컴포넌트에 동시 전달

다른 컴포넌트는 BeginPlay에서 AddUObject로 구독만 하면 됨

차량 폰이 어떤 컴포넌트가 구독하는지 알 필요 없음


BP_TunnelTrigger 삭제 사고

처음에 한 실수

1. Content Browser에서 BP_TunnelTrigger 우클릭
2. "참조가 있다" 경고 무시
3. Force Delete
4. 맵 저장

 

결과

CreateExport: Failed to load Outer for resource 'TunnelBox'
While trying to load package /Game/Maps/MainMap,
a dependent package /Game/Tunnel/BP_TunnelTrigger was not available.

 

맵 파일에는 BP 인스턴스 참조가 남아있는데 BP 파일이 사라진 상태

더 큰 문제

아키텍처 담당 팀원이 작업 중이던 터널 관련 파일도 영향받음

Git으로 복구

 

올바른 삭제 순서

1. 맵에서 BP_TunnelTrigger 인스턴스 모두 삭제
2. 맵 저장 (Ctrl+S)
3. BP 파일 삭제 (일반 Delete)

 

참조 정리 먼저, 파일 삭제는 그 다음

Force Delete는 마지막 수단


ATunnelTrigger C++ 클래스

 

TunnelTrigger.h

#pragma once

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

class UBoxComponent;

UCLASS()
class TEAM24UNREAL_API ATunnelTrigger : public AActor
{
    GENERATED_BODY()

public:
    ATunnelTrigger();

protected:
    virtual void BeginPlay() override;

    virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
    virtual void NotifyActorEndOverlap(AActor* OtherActor) override;

private:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Tunnel",
        meta = (AllowPrivateAccess = "true"))
    TObjectPtr<UBoxComponent> TunnelBox;
};

 

TunnelTrigger.cpp

ATunnelTrigger::ATunnelTrigger()
{
    PrimaryActorTick.bCanEverTick = false;

    TunnelBox = CreateDefaultSubobject<UBoxComponent>(TEXT("TunnelBox"));
    RootComponent = TunnelBox;

    TunnelBox->SetBoxExtent(FVector(500.f, 300.f, 200.f));
    TunnelBox->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
    TunnelBox->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
    TunnelBox->SetGenerateOverlapEvents(true);
}

void ATunnelTrigger::NotifyActorBeginOverlap(AActor* OtherActor)
{
    Super::NotifyActorBeginOverlap(OtherActor);

    if (ATeam24VehiclePawn* Pawn = Cast<ATeam24VehiclePawn>(OtherActor))
    {
        Pawn->SetInTunnel(true);
    }
}

void ATunnelTrigger::NotifyActorEndOverlap(AActor* OtherActor)
{
    Super::NotifyActorEndOverlap(OtherActor);

    if (ATeam24VehiclePawn* Pawn = Cast<ATeam24VehiclePawn>(OtherActor))
    {
        Pawn->SetInTunnel(false);
    }
}

 

핵심

  • CreateDefaultSubobject로 BoxComponent 생성
  • OverlapAllDynamic 프리셋 + SetGenerateOverlapEvents(true)
  • NotifyActorBeginOverlap/EndOverlap은 Actor가 제공하는 가상 함수
  • 블루프린트의 OnActorBeginOverlap 이벤트와 같은 역할

PIE 시작 시 차량이 이미 박스 안에 있는 경우

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

    TArray<AActor*> OverlappingActors;
    GetOverlappingActors(OverlappingActors, ATeam24VehiclePawn::StaticClass());

    for (AActor* Actor : OverlappingActors)
    {
        if (ATeam24VehiclePawn* Pawn = Cast<ATeam24VehiclePawn>(Actor))
        {
            Pawn->SetInTunnel(true);
        }
    }
}

 

PIE 시작 직후엔 Overlap 이벤트가 늦게 발생할 수 있음

BeginPlay에서 GetOverlappingActors로 직접 체크


SplineFollowerComponent에 델리게이트 구독

헤더에 추가

UFUNCTION()
void OnTunnelToggled(bool bInTunnel);

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Autopilot|Tunnel",
    meta=(AllowPrivateAccess="true"))
float TunnelSpeedScale = 0.7f;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Autopilot|Tunnel",
    meta=(AllowPrivateAccess="true"))
float TunnelLookAheadScale = 0.6f;

float BaselineMaxSpeed = 0.f;
float BaselineLookAheadBase = 0.f;
bool  bBaselineCached = false;

 

BeginPlay에 구독 추가

OwnerPawn->OnTunnelToggleDelegate.AddUObject(
    this, &USplineFollowerComponent::OnTunnelToggled);

 

차량 폰이 Broadcast 호출하면 본인 OnTunnelToggled 자동 호출


OnTunnelToggled 구현

void USplineFollowerComponent::OnTunnelToggled(bool bInTunnel)
{
    if (!bBaselineCached)
    {
        BaselineMaxSpeed = MaxSpeed;
        BaselineLookAheadBase = LookAheadBase;
        bBaselineCached = true;
    }

    if (bInTunnel)
    {
        MaxSpeed = BaselineMaxSpeed * TunnelSpeedScale;
        LookAheadBase = BaselineLookAheadBase * TunnelLookAheadScale;

        SmoothedTargetSpeed = FMath::Min(SmoothedTargetSpeed, MaxSpeed);
    }
    else
    {
        MaxSpeed = BaselineMaxSpeed;
        LookAheadBase = BaselineLookAheadBase;
    }
}

베이스라인 패턴이 왜 필요한가

 

처음엔 단순하게 짤까 생각

if (bInTunnel)
    MaxSpeed = MaxSpeed * 0.7f;
else
    MaxSpeed = MaxSpeed / 0.7f;

 

이러면 부동소수점 오차가 누적됨

터널 여러 번 들락날락하면 값이 점점 부정확해짐

베이스라인 방식

처음 호출 시 BaselineMaxSpeed에 원래 값 저장
그 후로는 항상 BaselineMaxSpeed * TunnelSpeedScale로 계산

 

몇 번을 들락날락해도 값이 정확함

원본을 저장하고 곱하기만 하자

비율로 곱하고 나누는 방식보다 안전


SmoothedTargetSpeed 클램프

SmoothedTargetSpeed = FMath::Min(SmoothedTargetSpeed, MaxSpeed);

 

이 한 줄이 중요

MaxSpeed만 줄이면 즉시 감속 효과가 없음

자율주행 코드가 SmoothedTargetSpeed를 천천히 보간하기 때문

터널 진입 시 즉각적인 감속

현재 속도가 새 MaxSpeed보다 크면 즉시 클램프


전체 흐름

1. 차량이 터널 박스 진입
2. ATunnelTrigger::NotifyActorBeginOverlap 호출
3. Cast → Pawn->SetInTunnel(true)
4. OnTunnelToggleDelegate.Broadcast(true)
5. 구독한 SplineFollowerComponent::OnTunnelToggled(true) 자동 호출
6. MaxSpeed 70%로 감소, LookAheadBase 60%로 감소
7. SmoothedTargetSpeed 즉시 클램프
8. 다음 Tick부터 천천히 운전

 

이탈도 같은 경로로 false 전달, 원래 값 복원

 


알게된 것

 

기존 코드 먼저 확인할 것

새로 만들기 전에 팀원이 이미 만든 게 있나 봐야 함

SetInTunnel 함수, 델리게이트 → 이미 있었음

 

파일 삭제 순서가 중요

1. 인스턴스 먼저 정리
2. 그 다음 파일 삭제

 

Force Delete는 마지막 수단

Git이 있어서 다행

변경사항 Discard로 복구 가능

 

협업에서 버전 관리는 필수

델리게이트는 결합도를 낮춤

차량 폰이 어떤 컴포넌트가 구독하는지 알 필요 없음

새 컴포넌트 추가해도 차량 폰 코드 안 건드림

베이스라인 패턴은 누적 오차를 막음

원본 저장 → 곱하기만

 

비율로 곱하고 나누는 것보다 안전

블루프린트와 C++은 같은 동작 가능

근데 협업에서는 C++이 유리

Git diff 명확, 컴파일 시점 에러 검출

+ Recent posts