UserWidget으로 C++ 부모 클래스 생성
// FDCapturePopupWidget.h
UCLASS()
class FUNNYORDIE_API UFDCapturePopupWidget : public UUserWidget
{
GENERATED_BODY()
protected:
UPROPERTY(meta = (BindWidget))
class UButton* Out;
UPROPERTY(meta = (BindWidget))
class UButton* Spare;
virtual void NativeConstruct() override;
UFUNCTION()
void OnClickedOut();
UFUNCTION()
void OnClickedSpare();
};
// FDCapturePopupWidget.cpp
void UFDCapturePopupWidget::NativeConstruct()
{
Super::NativeConstruct();
if (Out) Out->OnClicked.AddDynamic(this, &UFDCapturePopupWidget::OnClickedOut);
if (Spare) Spare->OnClicked.AddDynamic(this, &UFDCapturePopupWidget::OnClickedSpare);
}
void UFDCapturePopupWidget::OnClickedOut()
{
if (AFDPlayerController* PC = Cast<AFDPlayerController>(GetOwningPlayer()))
{
PC->Server_RequestOut();
PC->SetInputMode(FInputModeGameOnly());
PC->SetShowMouseCursor(false);
}
RemoveFromParent();
}
meta = (BindWidget)을 썼기 때문에 WBP에서 C++ 변수 이름과 같은 이름의 위젯을 만들었음
에디터 작업
WBP (위 클래스를 부모클래스로 둠) 만들기

디자인은 UI 담당 팀원이 더 예쁘게 꾸밀 예정
PlayerController에서 위젯 띄우기
void AFDPlayerController::Client_ShowCapturePopup_Implementation()
{
if (!CaptureWidgetClass)
{
UE_LOG(LogTemp, Warning, TEXT("[플레이어 컨트롤러] CaptureWidgetClass가 할당되지 않음"));
return;
}
// 이미 떠있는 팝업이 있으면 중복 생성 방지
if (CaptureWidgetInstance && CaptureWidgetInstance->IsInViewport())
{
return;
}
CaptureWidgetInstance = CreateWidget<UFDCapturePopupWidget>(this, CaptureWidgetClass);
if (!CaptureWidgetInstance) return;
CaptureWidgetInstance->AddToViewport();
SetShowMouseCursor(true);
FInputModeUIOnly InputMode;
InputMode.SetWidgetToFocus(CaptureWidgetInstance->TakeWidget());
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
SetInputMode(InputMode);
UE_LOG(LogTemp, Log, TEXT("[플레이어 컨트롤러] 포획 팝업 UI 활성화"));
}
- SetShowMouseCursor(true): 커서 자체를 화면에 보이게 함
- FInputModeUIOnly: 입력 UI 전담
- SetWidgetToFocus(...TakeWidget()): 여러 위젯 중 어떤 위젯이 입력의 주인공인지 지정
- SetLockMouseToViewportBehavior(DoNotLock): 마우스가 뷰포트 안에 갇히지 않게 함 (버튼 클릭하려면 자유롭게 움직여야 하니까)
PlayerController 블루프린트로 가서 설정

알게된 것
왜 SetShowMouseCursor()를 쓰고 SetLockMouseToViewportBehavior가 필요한가
- bShowMouseCursor = true처럼 변수 직접 대입도 가능은 하지만 Setter 함수(SetShowMouseCursor)가 있다는 건 그 안에 부수 로직(커서 렌더링 갱신 등)이 있을 수 있다는 뜻이라 Setter를 쓰는 게 안전하다
- SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock)을 명시하지 않으면 환경에 따라 마우스가 뷰포트 안에 갇혀 팝업 버튼을 못 누르는 경우가 생길 수 있다
트러블슈팅
포획 판정 크래시 디버깅
포획 팝업에서 시간 초과(3초 대기)로 자동 아웃 처리가 될 때마다 에디터에서 크래시가 남
Unhandled Exception: EXCEPTION_ACCESS_VIOLATION reading address 0x0000000000000018
UnrealEditor_FunnyorDie!AFDTaggerCharacter::OnCaptureTimerExpired()
찾아보니 주소가 0x18처럼 작은 값으로 찍히는 건 전형적으로 이미 nullptr이 된 포인터를 갖고 뭔가를 하려 할 때 나는 패턴이라고 함
중단점 찍어가며 찾아보니
void AFDTaggerCharacter::OnCaptureTimerExpired()
{
if (!CapturedHider) return;
Internal_ResolveCaptureLocally(true); // ← 이 함수 끝에서 CapturedHider = nullptr
UE_LOG(LogTemp, Log, TEXT("... %s"), *CapturedHider->GetName()); // ← nullptr 접근
}
이게 문제였음
포획 판정을 마무리하는 공통 헬퍼 Internal_ResolveCaptureLocally는 마지막에 CapturedHider = nullptr로 상태를 초기화하는데 함수를 부르는 걸로 쓰다보니 깜빡하고 그 다음에도 CaptureHider를 써버림
-> 로그를 뺐더니 해결됨
크래시는 아니지만 잠재적 버그 발견
분업을 하다보니 봐주기 타이머를 걸고 지우는 코드가 서로 다른 변수를 쓰고 있었음
void AFDTaggerCharacter::OnSpareExpired(ACharacter* TargetHider)
{
...
GetWorldTimerManager().ClearTimer(SpareExpireTimerHandle); // 이미 만들어놓은 멤버 변수
...
}
void AFDTaggerCharacter::RequestSpare()
{
...
FTimerHandle SpareTimerHandle; // 지역 변수로 새로 만듦
GetWorldTimerManager().SetTimer(SpareTimerHandle, [this, SparedHider]()
{
OnSpareExpired(SparedHider);
}, 10.f, false);
...
}
수정
void AFDTaggerCharacter::RequestSpare()
{
...
GetWorldTimerManager().SetTimer(
SpareExpireTimerHandle, // 멤버 변수로 통일
[this, SparedHider]() { OnSpareExpired(SparedHider); },
10.f, false);
...
}
'프로젝트 > Funny Or Die (멀티플레이 술래잡기 게임)' 카테고리의 다른 글
| 에셋 (0) | 2026.07.07 |
|---|---|
| EndMatch 구현 (게임 한 바퀴 완주) (0) | 2026.07.03 |
| 본게임 포획 판정 구현 (0) | 2026.07.02 |
| 정찰 단계(Scouting) 구현하기 (0) | 2026.07.01 |
| Seamless Travel 이후 캐릭터 입력이 먹지 않던 문제 (0) | 2026.07.01 |
