터널 시스템 블루프린트에서 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 명확, 컴파일 시점 에러 검출
'프로젝트 > 자율주행 위험구간 분석 시뮬레이터' 카테고리의 다른 글
| [트러블슈팅] 날씨 별 속도가 동일, 마찰력 줄어들면 코너에서 추락 (0) | 2026.05.18 |
|---|---|
| 날씨 시스템 구현하기 (0) | 2026.05.17 |
| [트러블슈팅] 차량을 도로 끝에서 멈추게 하기 (0) | 2026.05.11 |
| [트러블슈팅] 커브길에서 차량이 도로 밖으로 튕겨나가는 문제 (0) | 2026.05.11 |
| 파라미터 (0) | 2026.05.08 |
