'전체 글'에 해당되는 글 261건

  1. 2026.08.20 CommonLoadingScreen 분석 및 정리
728x90

• LoadingScreenWidget
• 타입: FSoftClassPath
• 설명: 로딩 스크린에 표시할 위젯 클래스(블루프린트 또는 C++ UserWidget)를 지정합니다.
이 위젯이 실제 로딩 화면에 표시됩니다.

• LoadingScreenZOrder
• 타입: int32
• 설명: 로딩 스크린 위젯이 뷰포트에 추가될 때의 Z-Order(레이어 우선순위)입니다.
값이 높을수록 다른 UI 위에 표시됩니다. 기본값은 10000입니다.

• HoldLoadingScreenAdditionalSecs
• 타입: float
• 설명: 로딩이 끝난 후에도 추가로 로딩 스크린을 유지할 시간(초)입니다.
텍스처 스트리밍 등으로 인한 블러 현상을 방지하기 위해 사용합니다.

• LoadingScreenHeartbeatHangDuration
• 타입: float
• 설명: 이 값(초)보다 더 오래 로딩 스크린이 유지되면 "로딩이 멈췄다"고 간주합니다.
0이면 비활성화됩니다.

• LogLoadingScreenHeartbeatInterval
• 타입: float
• 설명: 로딩 스크린이 유지되는 동안, 이 간격(초)마다 "무엇이 로딩 스크린을 붙잡고 있는지" 로그를 남깁니다.
0이면 비활성화됩니다.

• LogLoadingScreenReasonEveryFrame
• 타입: bool
• 설명: true로 설정하면, 매 프레임마다 로딩 스크린이 표시/숨겨지는 이유를 로그로 출력합니다.

• ForceLoadingScreenVisible
• 타입: bool
• 설명: true로 설정하면, 항상 로딩 스크린을 강제로 표시합니다.
디버깅 용도로 사용합니다.

• HoldLoadingScreenAdditionalSecsEvenInEditor
• 타입: bool
• 설명: true로 설정하면, 에디터 환경에서도 HoldLoadingScreenAdditionalSecs 지연을 적용합니다.
로딩 스크린을 반복적으로 테스트할 때 유용합니다.

• ForceTickLoadingScreenEvenInEditor
• 타입: bool
• 설명: true로 설정하면, 에디터 환경에서도 로딩 스크린의 틱(Tick)을 강제로 한번 호출하여 로딩 스크린이 즉시 표시 되도록 합니다. 기본값은 true입니다.

“지금 로딩이 필요한가?”를 판단하는 주체가 ILoadingProcessInterface(로딩 프로세서) 구현체
로딩 매니저가 매 프레임 이 구현체들을 체크해서 필요하면 지정된 로딩 위젯을 띄웁니다.
아래 코드 구문처럼 인터페이스를 상속받고 있는 리스트를 체크 해서 필요 여부 체크 GameState or Gamestate안에 구성하고있는 각 모듈의 컴포넌트(UGameStateComponent) or 외부에서 등록된 것들

// Ask the game state if it needs a loading screen	
if (ILoadingProcessInterface::ShouldShowLoadingScreen(GameState, /*out*/ DebugReasonForShowingOrHidingLoadingScreen))
{
	return true;
}

// Ask any game state components if they need a loading screen
for (UActorComponent* TestComponent : GameState->GetComponents())
{
	if (ILoadingProcessInterface::ShouldShowLoadingScreen(TestComponent, /*out*/ DebugReasonForShowingOrHidingLoadingScreen))
	{
		return true;
	}
}

// Ask any of the external loading processors that may have been registered.  These might be actors or components
// that were registered by game code to tell us to keep the loading screen up while perhaps something finishes
// streaming in.
for (const TWeakInterfacePtr<ILoadingProcessInterface>& Processor : ExternalLoadingProcessors)
{
	if (ILoadingProcessInterface::ShouldShowLoadingScreen(Processor.GetObject(), /*out*/ DebugReasonForShowingOrHidingLoadingScreen))
	{
		return true;
	}
}
  • ‘어떤 배경을 쓸지’ 결정하는 데이터 소스 준비
    • 라이라처럼 모드/맵 선택 데이터(Primary Asset)를 만들고, TSoftClassPtr<UUserWidget>로 로딩 배경 위젯을 지정.
  • 레벨 전환 트리거에서 서브시스템에 저장
    • 트래블 직전에 LoadingScreenSubsystem->SetLoadingWidgetClass(ChosenClass) 같은 식으로 “교체할 클래스” 전달.
  • Common Loading Screen 세팅에서 Host 위젯만 지정
    • Host는 항상 동일한 틀, “배경”은 Construct에서 교체되므로 Host만 플러그인 세팅에 등록.
  • Host 위젯 Construct에서 Subsystem 조회 → SetContent
    • 서브시스템에서 클래스를 꺼내 새 배경 위젯 생성 → 네임드 슬롯/패널에 SetContent.
  • 해제 조건 신호 정리
    • 라이라처럼 Experience 시스템을 쓰지 않는다면, 맵 로드 완료 + 게임플레이 준비 완료를 알리는 별도 이벤트/플래그를 만들어 ShouldShowLoadingScreen() 로직(혹은 등가의 판정)에서 참고하도록 해야 “무한 로딩”을 피합니다. 커뮤니티에서도 라이라 신호를 그대로 기대해 붙였다가 로딩이 안 사라지는 이슈가 보고됩니다.

이뿐만이 아니라 조건 체크 종류에는 맵 로딩중인지, 월드가 시작이 아직 안됬는지. 로컬플레이어의 플레이어 컨트롤러에 해당 인터페이스가 있다면 로컬플레이어마다 체크를 한다던지 체크를 하는 조건이 다양하게 설정되어있다.

만약 멀티 게임이라면 지금 드는 생각은  AGameState가 모든 PlayerState가 Ready인지 집계하고, ILoadingProcessInterface 를 구현하여 “모두 Ready 될 때까지 true(계속 보여 줘)”를 반환하게 하면, 로딩 화면이 자연스럽게 전원 준비 완료까지 유지가 될거 같다.
RPC를 이용해서 각 클라에서 Ready상태값을 서버에 요청해서 갱신해주고 서버에서 모든 플레이어들의 Ready상태값을 확인한다 식의?...

아래 코드 느낌처럼.. 이거 재대로 돌아가는 코드 아니다.. 그냥 생각나는대로 정리만 해본것

//PlayerState
.h
UCLASS()
class AMyPlayerState : public APlayerState
{
    GENERATED_BODY()
public:
    AMyPlayerState();

    UPROPERTY(ReplicatedUsing=OnRep_IsReady, BlueprintReadOnly, Category="Ready")
    bool bIsReady = false;
}

.cpp
AMyPlayerState::AMyPlayerState()
{
    bReplicates = true;
}

void AMyPlayerState::SetReady(bool bInReady)
{
    if (!HasAuthority())
    {
        return; // 서버에서만 상태 확정
    }

    const bool bChanged = (bIsReady != bInReady);
    bIsReady = bInReady;
    if (bChanged)
    {
        OnRep_IsReady();

        // 서버에서 집계 갱신
        if (AGameStateBase* GS = GetWorld()->GetGameState())
        {
            if (AMyGameState* MGS = Cast<AMyGameState>(GS))
            {
                MGS->RecalcAllPlayersReady();
            }
        }
    }
}

void AMyPlayerState::OnRep_IsReady()
{
    // 클라에서 UI 갱신이 필요하면 여기서 브로드캐스트 등
}

void AMyPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const
{
    Super::GetLifetimeReplicatedProps(Out);
    DOREPLIFETIME(AMyPlayerState, bIsReady);
}

//PlayerController
.h
UCLASS()
class AMyPlayerController : public APlayerController
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable, Category="Ready")
    void MarkClientReady(bool bReady = true);

protected:
    UFUNCTION(Server, Reliable)
    void ServerSetClientReady(bool bReady);
    void ServerSetClientReady_Implementation(bool bReady);
};

.cpp
void AMyPlayerController::MarkClientReady(bool bReady)
{
    if (IsLocalController())
    {
        ServerSetClientReady(bReady);
    }
}

void AMyPlayerController::ServerSetClientReady_Implementation(bool bReady)
{
    if (AMyPlayerState* PS = GetPlayerState<AMyPlayerState>())
    {
        PS->SetReady(bReady); // 서버에서 복제 + 집계 트리거
    }
}

//GameState
.h
UCLASS()
class AMyGameState : public AGameState, public ILoadingProcessInterface
{
    GENERATED_BODY()
public:
    AMyGameState();

    // 집계 결과(복제): 클라에서도 UI가 참고할 수 있게끔
    UPROPERTY(ReplicatedUsing=OnRep_AllPlayersReady, BlueprintReadOnly, Category="Ready")
    bool bAllPlayersReady = false;

    // (선택) 기대 인원. 매치메이킹/세션 정보에서 세팅하면
    // "예상 인원 다 찰 때까지"도 함께 기다릴 수 있음.
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Ready")
    int32 ExpectedNumPlayers = 0; // 0이면 무시

    // 서버에서 호출: PlayerState 추가/변경/제거 시 집계
    void RecalcAllPlayersReady();

    // AGameState overrides
    virtual void AddPlayerState(APlayerState* PlayerState) override;
    virtual void RemovePlayerState(APlayerState* PlayerState) override;

    // ILoadingProcessInterface
    virtual bool ShouldShowLoadingScreen(FString& OutReason) const override;

protected:
    UFUNCTION()
    void OnRep_AllPlayersReady();

    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const override;
};

.cpp
AMyGameState::AMyGameState()
{
    bReplicates = true;
}

void AMyGameState::AddPlayerState(APlayerState* PlayerState)
{
    Super::AddPlayerState(PlayerState);
    if (HasAuthority())
    {
        RecalcAllPlayersReady();
    }
}

void AMyGameState::RemovePlayerState(APlayerState* PlayerState)
{
    Super::RemovePlayerState(PlayerState);
    if (HasAuthority())
    {
        RecalcAllPlayersReady();
    }
}

void AMyGameState::RecalcAllPlayersReady()
{
    if (!HasAuthority())
    {
        return;
    }

    const int32 NumPlayers = PlayerArray.Num();

    // 1) 인원수 조건: 기대 인원이 있으면 그 수만큼 모일 때까지 대기
    if (ExpectedNumPlayers > 0 && NumPlayers < ExpectedNumPlayers)
    {
        if (bAllPlayersReady != false)
        {
            bAllPlayersReady = false;
            OnRep_AllPlayersReady();
        }
        return;
    }

    // 2) 각 PlayerState의 Ready 플래그 확인
    bool bEveryoneReady = (NumPlayers > 0); // 0명이면 false로(원하면 정책 조정)
    for (APlayerState* PS : PlayerArray)
    {
        const AMyPlayerState* MyPS = Cast<AMyPlayerState>(PS);
        if (!MyPS || !MyPS->bIsReady)
        {
            bEveryoneReady = false;
            break;
        }
    }

    if (bAllPlayersReady != bEveryoneReady)
    {
        bAllPlayersReady = bEveryoneReady;
        OnRep_AllPlayersReady();
    }
}

bool AMyGameState::ShouldShowLoadingScreen(FString& OutReason) const
{
    // 로딩 화면 매니저가 매 프레임 호출
    if (!bAllPlayersReady)
    {
        OutReason = TEXT("Waiting for all players to be ready");
        return true; // 아직 붙잡아 둔다
    }
    return false; // 모두 준비됨 → 해제
}

void AMyGameState::OnRep_AllPlayersReady()
{
    // 클라 UI에 브로드캐스트하거나 로그 남기기 등
}

void AMyGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const
{
    Super::GetLifetimeReplicatedProps(Out);
    DOREPLIFETIME(AMyGameState, bAllPlayersReady);
    DOREPLIFETIME(AMyGameState, ExpectedNumPlayers);
}

 

만약 로딩화면이 떠야할 시점 전에 외부에서 로딩화면의 조건을 설정해야할 경우가 있다면 ULoadingProcessTask::CreateLoadingScreenProcessTask을 사용해서 외부 task를 등록하면 된다.
CreateLoadingScreenProcessTask()로 만드는 객체(ULoadingProcessTask)가 이미 ILoadingProcessInterface를 구현하고 있어서, 함수를 호출하는 쪽 클래스는 인터페이스를 구현할 필요가 없다. 태스크를 만들어 두고, 일이 끝났을 때 Unregister()만 호출하면 된다.
인자로 UObject* 타입의 WorldContextObject자리에 this가 들어가고 있는건데 당연하게도 UWorld(월드) 객체를 얻으려 하는거기때문에 대부분은 this면 될것이다. 

ULoadingProcessTask* LoadTask =
    ULoadingProcessTask::CreateLoadingScreenProcessTask(this, TEXT("Travel to Arena"));

// 2) 실제 작업 시작 (예: 맵 전환)
UGameplayStatics::OpenLevel(this, FName(TEXT("L_Arena")));

// 3) 작업이 정말 끝났을 때 Unregister
if (LoadTask)
{
    LoadTask->Unregister();   // <- 해제 신호 (이 호출 전까지는 로딩 화면 유지)
}


728x90
Posted by 정망스
,


맨 위로
홈으로 ▲위로 ▼아래로 ♥댓글쓰기 새로고침