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);
	...
}

 

+ Recent posts