
Hover = 1.0 → 1.1 : 마우스 대면 확대
Unhover = 1.1 → 1.0 : 마우스 빼면 원래 크기
Press = 1.0 → 0.9 : 클릭하면 줄어들기
Nickname_Committed 1.1 → 1.0 : 타자 칠 때마다 크기 변경

사운드는 Play Sound 2D로, 애니메이션은 Play Animation으로 연결하기
닉네임 적으려고 타자칠 때 사운드 2개 랜덤 재생하고 싶음
.wav 로는 안되고 사운드큐로 변경해서 설정해야 한다
.wav 우클릭 후 Create Cue
사운드 2개 Random 재생하기
Play Sound 2D에 사운드큐 넣기
버튼 사운드 추가했는데 레벨 이동이 너무 빨라서 소리가 묻힌다...
그래서 레벨 이동을 지연시켜서 소리 나오게 수정했다!
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "TitlePlayerController.generated.h"
/**
* TitleLevel의 PlayerController
* 김여울
*/
UCLASS()
class DC_API ATitlePlayerController : public APlayerController
{
GENERATED_BODY()
public:
ATitlePlayerController();
protected:
virtual void BeginPlay() override;
public:
// 레벨 지연 이동 - 버튼 소리가 안 들림
UFUNCTION(BlueprintCallable)
void RequestEnterWaitingLevelDeferred(const FString& InNickname, float DelaySeconds);
protected:
// 서버에서 맵 이동 수행(전용 서버/리스닝 서버 대응)
UFUNCTION(Server, Reliable)
void ServerGoToWaitingLevel(const FString& InNickname);
void ServerGoToWaitingLevel_Implementation(const FString& InNickname);
// 테스트/싱글 실행 대비: 클라에서 로컬로 열어줌
void ClientOpenWaitingLevelLocal();
// UI 스폰
void ShowTitleWidget();
private:
// 실제 이동 로직 (즉시 실행용 내부 헬퍼)
void DoEnterWaitingLevel(const FString& InNickname);
// 타이머 만료시 실제 이동 실행
UFUNCTION()
void OnTravelTimerExpired();
private:
// 위젯 블루프린트 할당용
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<class UUserWidget> TitleWidgetClass;
// 생성된 위젯 참조
UPROPERTY()
class UTitleLevelWidget* TitleWidgetRef;
// 지연 이동용
FTimerHandle TravelTimerHandle;
FString PendingNickname;
};
#include "../Title/TitlePlayerController.h"
#include "Blueprint/UserWidget.h"
#include "UI/TitleLevelWidget.h"
#include "Kismet/GameplayStatics.h"
#include "CineCameraActor.h"
#include "Game/DCGameInstance.h"
ATitlePlayerController::ATitlePlayerController()
{
static ConstructorHelpers::FClassFinder<UUserWidget> WBP(TEXT("/Game/DC/UI/OutGame/WBP_TitleLevel.WBP_TitleLevel_C"));
if (WBP.Succeeded())
{
TitleWidgetClass = WBP.Class;
}
bShowMouseCursor = true;
}
void ATitlePlayerController::BeginPlay()
{
Super::BeginPlay();
// 타이틀 UI 표시
ShowTitleWidget();
// 시네 카메라로 뷰 전환 (레벨에 1개 배치)
TArray<AActor*> Cameras;
UGameplayStatics::GetAllActorsOfClass(this, ACineCameraActor::StaticClass(), Cameras);
if (Cameras.Num() > 0)
{
SetViewTargetWithBlend(Cameras[0], 0.f);
}
// 입력모드 : Game & UI
FInputModeGameAndUI InputMode;
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
SetInputMode(InputMode);
}
// 3초 지연 호출
void ATitlePlayerController::RequestEnterWaitingLevelDeferred(const FString& InNickname, float DelaySeconds)
{
PendingNickname = InNickname;
// 전용 서버 환경에서는 클라이언트에서 사운드 재생되도록 최소 0.25초 보장
if (GetNetMode() == NM_Client && DelaySeconds < 0.25f)
{
DelaySeconds = 0.25f;
}
// 타이머 시작
GetWorldTimerManager().ClearTimer(TravelTimerHandle);
GetWorldTimerManager().SetTimer(
TravelTimerHandle,
this,
&ATitlePlayerController::OnTravelTimerExpired,
DelaySeconds,
false
);
}
void ATitlePlayerController::ShowTitleWidget()
{
if (!TitleWidgetClass)
{
UE_LOG(LogTemp, Warning, TEXT("TitleWidgetClass 미지정: 에디터에서 ATitlePlayerController의 TitleWidgetClass 설정"));
return;
}
if (!TitleWidgetRef)
{
UUserWidget* Created = CreateWidget<UUserWidget>(this, TitleWidgetClass);
TitleWidgetRef = Cast<UTitleLevelWidget>(Created);
if (TitleWidgetRef)
{
TitleWidgetRef->AddToViewport();
UE_LOG(LogTemp, Log, TEXT("Title Widget Added To Viewport"));
}
}
}
void ATitlePlayerController::OnTravelTimerExpired()
{
GetWorldTimerManager().ClearTimer(TravelTimerHandle); // 타이머 정리
DoEnterWaitingLevel(PendingNickname); // 레벨 이동
}
void ATitlePlayerController::DoEnterWaitingLevel(const FString& InNickname)
{
UE_LOG(LogTemp, Log, TEXT("▶ DoEnterWaitingLevel: %s"), *InNickname);
// 닉네임 저장
if (UDCGameInstance* GI = GetGameInstance<UDCGameInstance>())
{
GI->SetNickname(InNickname);
}
// 서버 권한이 있으면 서버가 직접 맵 이동
if (HasAuthority())
{
ServerGoToWaitingLevel(InNickname);
return; // 여기서 끝냄
}
// 클라이언트면 서버에 요청
ServerGoToWaitingLevel(InNickname);
// 로컬 테스트(에디터 단독 실행)일 경우 대비
if (!IsNetMode(NM_DedicatedServer))
{
ClientOpenWaitingLevelLocal();
}
}
void ATitlePlayerController::ServerGoToWaitingLevel_Implementation(const FString& InNickname)
{
UE_LOG(LogTemp, Warning, TEXT("▶ ServerGoToWaitingLevel_Implementation: %s"), *InNickname);
// 서버에서만 실행되도록 보장
if (!HasAuthority())
{
return; // 클라에서는 실행 안 함
}
UWorld* World = GetWorld();
if (!World)
{
return;
}
// WaitingLevel로 맵 이동
// Dedicated Server일 경우: 클라이언트는 이미 IP:Port로 접속해 있으므로
// ServerTravel만 호출하면 접속 클라들이 자동으로 맵 이동
World->ServerTravel(TEXT("/Game/DC/Maps/LV_Waiting.LV_Waiting"), true);
}
void ATitlePlayerController::ClientOpenWaitingLevelLocal()
{
// 싱글/PIE 단독 실행시 편의용
UGameplayStatics::OpenLevel(this, FName("LV_Waiting"));
}
#include "UI/TitleLevelWidget.h"
#include "Server/Title/TitlePlayerController.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Kismet/GameplayStatics.h"
#include "InputCoreTypes.h" // EKeys
void UTitleLevelWidget::NativeConstruct()
{
Super::NativeConstruct();
// 버튼 클릭 바인딩
if (StartButton)
{
StartButton->OnClicked.AddDynamic(this, &UTitleLevelWidget::OnStartClicked);
}
if (QuitButton)
{
QuitButton->OnClicked.AddDynamic(this, &UTitleLevelWidget::OnQuitClicked);
}
// Enter Key (텍스트 커밋) 바인딩
if (EditableTextBox_Nickname)
{
// 입력 전에는 비워두고
EditableTextBox_Nickname->SetText(FText::GetEmpty());
EditableTextBox_Nickname->OnTextCommitted.AddDynamic(this, &UTitleLevelWidget::OnNameCommitted);
EditableTextBox_Nickname->SetKeyboardFocus();
}
}
FReply UTitleLevelWidget::NativeOnPreviewKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
{
const FKey Key = InKeyEvent.GetKey();
// 텍스트박스에 포커스가 있고, 엔터/확인 키면 → 닉네임만 캐시하고 전파 차단
if (EditableTextBox_Nickname && EditableTextBox_Nickname->HasKeyboardFocus())
{
if (Key == EKeys::Enter || Key == EKeys::Virtual_Accept || Key == EKeys::Gamepad_FaceButton_Bottom)
{
CachedNickname = EditableTextBox_Nickname->GetText().ToString().TrimStartAndEnd();
return FReply::Handled(); // 여기서 끝: 버튼으로 안 넘어감
}
}
return Super::NativeOnPreviewKeyDown(InGeometry, InKeyEvent);
}
void UTitleLevelWidget::OnStartClicked()
{
// 최신 닉네임 사용
FString Nickname = EditableTextBox_Nickname
? EditableTextBox_Nickname->GetText().ToString()
: CachedNickname;
Nickname = Nickname.TrimStartAndEnd();
if (Nickname.IsEmpty())
{
Nickname = TEXT("Player");
}
if (ATitlePlayerController* PC = GetTitlePC())
{
// 버튼 소리 나오게 레벨 지연 이동 (버튼 사운드 재생)
PC->RequestEnterWaitingLevelDeferred(Nickname, 1.f);
}
}
void UTitleLevelWidget::OnQuitClicked()
{
// 1초 후 게임 종료 (사운드 재생 시간 확보용)
FTimerHandle QuitTimerHandle;
GetWorld()->GetTimerManager().SetTimer(
QuitTimerHandle,
this,
&UTitleLevelWidget::QuitGameWithDelay,
1.0f,
false
);
}
void UTitleLevelWidget::QuitGameWithDelay()
{
// 게임 종료 처리
if (APlayerController* PC = GetOwningPlayer())
{
UKismetSystemLibrary::QuitGame(this, PC, EQuitPreference::Quit, false);
}
}
void UTitleLevelWidget::OnNameCommitted(const FText& Text, ETextCommit::Type CommitType)
{
if (CommitType == ETextCommit::OnEnter)
{
// 닉네임만 저장, 이동 없음
CachedNickname = Text.ToString().TrimStartAndEnd();
}
}
class ATitlePlayerController* UTitleLevelWidget::GetTitlePC() const
{
return GetOwningPlayer<ATitlePlayerController>();
}
여울님의 글은 항상 부지런함이 넘쳐 고스란히 저에게도 전달된 그 땀을 느낄 수가 있어요.
상세한 설명은 물론이거니와 문제점을 찾고 해결하는데 있어 탁월한 해결을 보여주셨네요.