언리얼 엔진 일지

[언리얼 엔진 5.6] 로비 상호작용 처리 흐름 및 클라이언트 UI 호출 구조 정리

shju9803 2025. 12. 5. 20:24

1. 로비 상호작용 공통: 상호작용 성공 시 로그 강화

변경 내용

AAO_LobbyInteractable::OnInteractionSuccess_BP_Implementation 에서 상호작용 흐름 전체에 로그를 촘촘하게 심었습니다.

  • 공통 진입 로그 추가
AO_LOG(LogJSH, Log,
    TEXT("[LobbyInteract] OnSuccess Enter | This=%s Type=%d Interactor=%s"),
    *GetName(),
    static_cast<int32>(InteractType),
    *GetNameSafe(Interactor));
  • Interactor 가 APawn 이 아닐 때, AAO_PlayerController_Lobby 를 못 찾을 때 각각 Warning 로그 추가
  • 각 InteractType 분기(Ready / StartGame / InviteFriends / Wardrobe / default)에 모두 로그 추가

의도

  • “로비 인터랙터를 누르면 어디까지 코드가 들어가는지” 를 한눈에 볼 수 있도록, 진입 ~ 분기 ~ RPC 호출까지 전 과정을 로깅
  • 특히 패키지 게스트 환경에서 Invite/ Wardrobe 상호작용이 어디까지 도달하는지 확인하기 위해

2. Ready 상호작용: 로컬 선반영 + OnRep 공개

변경 내용

2-1. Ready 토글: 클라이언트 로컬 선반영 추가

case EAO_LobbyInteractType::Ready:
{
    if (bIsHost)
    {
        AO_LOG(LogJSH, Log,
            TEXT("[LobbyInteract] ReadyToggle: Host tried to use Ready, ignored | PC=%s"),
            *GetNameSafe(PC));
        return;
    }

    if (AAO_PlayerState* PS = PC->GetPlayerState<AAO_PlayerState>())
    {
        const bool bNewReady = !PS->IsLobbyReady();

        // 1) 로컬 선반영
        if (PC->IsLocalController())
        {
            AO_LOG(LogJSH, Log,
                TEXT("[LobbyInteract] ReadyToggle: local predict SetLobbyReady(%d) | PC=%s"),
                static_cast<int32>(bNewReady),
                *GetNameSafe(PC));

            PS->SetLobbyReady(bNewReady);
            PS->OnRep_LobbyIsReady();
        }

        // 2) 실제 서버 요청
        AO_LOG(LogJSH, Log,
            TEXT("[LobbyInteract] ReadyToggle: call Server_SetReady(%d) | PC=%s"),
            static_cast<int32>(bNewReady),
            *GetNameSafe(PC));

        PC->Server_SetReady(bNewReady);
    }
    else
    {
        AO_LOG(LogJSH, Warning,
            TEXT("[LobbyInteract] ReadyToggle: PlayerState is null | PC=%s"),
            *GetNameSafe(PC));
    }
    break;
}
  • 기존에는 서버에서 bLobbyIsReady 를 바꾼 뒤, Replication 이 돌아오기를 기다려야 화면이 갱신됨
  • 이제는 로컬 컨트롤러일 때 먼저 SetLobbyReady + OnRep_LobbyIsReady() 를 호출해서 UI를 즉시 반영
  • 그 뒤에 서버에 Server_SetReady 로 “진짜 상태”를 요청

2-2. OnRep_LobbyIsReady 위치 변경

class AO_API AAO_PlayerState : public APlayerState
{
public:
    // 호스트 여부 (입장 순서 0번을 호스트로 간주)
    bool IsLobbyHost() const;

    // 레디 상태가 복제될 때 호출
    UFUNCTION()
    void OnRep_LobbyIsReady();

protected:
    UPROPERTY(ReplicatedUsing=OnRep_LobbyIsReady)
    bool bLobbyIsReady;
    ...
};
  • OnRep_LobbyIsReady() 선언을 protected → public 으로 끌어올려서,
    로컬 예측용으로 PS->OnRep_LobbyIsReady() 를 직접 호출 가능하게 변경

의도

  • Ready 버튼을 눌렀을 때 “눌렀는지 안 눌렀는지” 가 바로 보이도록, 클라이언트 화면에서 먼저 선반영
  • 서버와의 최소한의 desync 를 감수하는 대신, 로비 UX를 훨씬 부드럽게 만들기 위함
  • OnRep_ 을 public 으로 열어서 “UI 업데이트 로직”을 재사용 (복제/선반영 모두 같은 경로 타게)

3. Invite / Wardrobe 상호작용: 서버 경유 RPC 정리

3-1. CanInteraction 규칙 완화

case EAO_LobbyInteractType::InviteFriends:
    // 모두 초대 UI 허용
    return true;
  • 기존: return PC->IsLocalController();
  • 변경 후: 상호작용 가능 여부는 별도 로직에서 관리하고, 타입 자체는 “누구나 상호작용 가능”으로 단순화

3-2. AAO_PlayerController_Lobby RPC 재구성

서버 RPC 추가

UFUNCTION(Server, Reliable)
void Server_RequestInviteOverlay();

UFUNCTION(Server, Reliable)
void Server_RequestWardrobe();
void AAO_PlayerController_Lobby::Server_RequestInviteOverlay_Implementation()
{
    AO_LOG(LogJSH, Log,
        TEXT("Server_RequestInviteOverlay: Called on Server | PC=%s HasAuthority=%d IsLocal=%d"),
        *GetName(),
        HasAuthority() ? 1 : 0,
        IsLocalController() ? 1 : 0);

    Client_OpenInviteOverlay();
}

void AAO_PlayerController_Lobby::Server_RequestWardrobe_Implementation()
{
    AO_LOG(LogJSH, Log,
        TEXT("Server_RequestWardrobe: Called on Server | PC=%s HasAuthority=%d IsLocal=%d"),
        *GetName(),
        HasAuthority() ? 1 : 0,
        IsLocalController() ? 1 : 0);

    Client_OpenWardrobe();
}
  • 상호작용은 항상 서버에서 처리하도록, 인터랙터 → 서버(PC) → 클라이언트 구조로 통일
  • Wardrobe 도 패턴 동일하게 맞춰둠 (현재는 TODO 로그만 출력, 나중에 실제 UI 연결 예정)

클라이언트 RPC + 로컬 함수 분리

UFUNCTION(Client, Reliable)
void Client_OpenInviteOverlay();

UFUNCTION(Client, Reliable)
void Client_OpenWardrobe();

UFUNCTION()
void OpenInviteOverlay();

UFUNCTION()
void OpenWardrobe();

구현:

void AAO_PlayerController_Lobby::Client_OpenInviteOverlay_Implementation()
{
    AO_LOG(LogJSH, Log,
        TEXT("Client_OpenInviteOverlay: Called on Client | PC=%s HasAuthority=%d IsLocal=%d"),
        *GetName(),
        HasAuthority() ? 1 : 0,
        IsLocalController() ? 1 : 0);

    OpenInviteOverlay();
}

void AAO_PlayerController_Lobby::Client_OpenWardrobe_Implementation()
{
    AO_LOG(LogJSH, Log,
        TEXT("Client_OpenWardrobe: Called on Client | PC=%s HasAuthority=%d IsLocal=%d"),
        *GetName(),
        HasAuthority() ? 1 : 0,
        IsLocalController() ? 1 : 0);

    OpenWardrobe();
}

로컬 함수:

void AAO_PlayerController_Lobby::OpenInviteOverlay()
{
    if( !IsLocalController() )
    {
        AO_LOG(LogJSH, Warning,
            TEXT("OpenInviteOverlay: Not LocalController | PC=%s"),
            *GetName());
        return;
    }

    if( const UGameInstance* GI = GetGameInstance() )
    {
        if( UAO_OnlineSessionSubsystem* Sub = GI->GetSubsystem<UAO_OnlineSessionSubsystem>() )
        {
            AO_LOG(LogJSH, Log,
                TEXT("OpenInviteOverlay: ShowInviteUI | PC=%s"),
                *GetName());

            Sub->ShowInviteUI();
            return;
        }
    }

    AO_LOG(LogJSH, Warning,
        TEXT("OpenInviteOverlay: OnlineSessionSubsystem not found | PC=%s"),
        *GetName());
}

void AAO_PlayerController_Lobby::OpenWardrobe()
{
    if( !IsLocalController() )
    {
        AO_LOG(LogJSH, Warning,
            TEXT("OpenWardrobe: Not LocalController | PC=%s"),
            *GetName());
        return;
    }

    // TODO: 실제 Wardrobe UI 열기
    AO_LOG(LogJSH, Log,
        TEXT("OpenWardrobe: Open wardrobe UI (TODO) | PC=%s"),
        *GetName());
}

의도

  • 인터랙션 → 서버 → 해당 클라 라는 기본 패턴을 Invite/Wardrobe에도 맞춰서, 나중에 권한 제어나 로직을 바꾸기 쉽게 만듦
  • OpenInviteOverlay()/OpenWardrobe() 는 항상 로컬 클라에서만 UI를 열도록 IsLocalController() 체크 추가
  • 게스트 패키지 환경에서 “서버까지 호출은 되는데, 어디에서 끊기는지” 를 로그로 추적 가능

4. ReadyBoard 구조체 기본값 초기화 (로그 경고 해결)

변경 내용

USTRUCT(BlueprintType)
struct FAOLobbyReadyBoardEntry
{
    GENERATED_BODY()

public:
    UPROPERTY(BlueprintReadWrite)
    FString PlayerName = TEXT("");

    UPROPERTY(BlueprintReadWrite)
    bool bIsHost = false;

    UPROPERTY(BlueprintReadWrite)
    FString StatusLabel = TEXT("");

    UPROPERTY(BlueprintReadWrite)
    bool bStatusActive = false;

    int32 JoinOrder = -1;
};
  • BluePrintReadWrite 로 노출된 FString / bool 멤버에 직접 기본값을 부여
  • 이전에 떴던 로그:
    • BoolProperty FAOLobbyReadyBoardEntry::bStatusActive is not initialized properly.
      를 해결하기 위한 변경

의도

  • USTRUCT 멤버를 Blueprint에서 생성/사용할 때, C++ 쪽 기본값이 명확하지 않으면 에디터에서 경고를 띄움
  • 모든 필드에 명시적으로 초기값을 주어,
    “로비에 아무도 없는 상태” 에서도 안전하게 기본 상태가 보장되도록 정리

5. 정리: 이번 변경으로 얻은 것

  • 로비 인터랙션 흐름 가시성 강화
    → 어떤 상호작용이 서버/클라 어디까지 도달하는지 로그로 확인 가능
  • Ready 토글 UX 개선
    → 버튼 누른 즉시 레디 아이콘/상태가 바뀌고, 서버와 동기화는 백그라운드에서 처리
  • Invite / Wardrobe RPC 패턴 통일
    → 상호작용은 항상 서버를 거치고, 실제 UI 열기는 “해당 로컬 클라이언트”만 수행
  • ReadyBoard 구조체 초기화 경고 제거
    → 에디터/로그에 의미 없는 경고가 사라지고, 구조체 기본 상태가 명확해짐