[Unreal] 카드게임 - 게임 시작 선택 구현

개요

하스스톤의 경우 매칭이 잡히면 멀리건(첫 손패)를 선택하고 시작한다.

이때 손패를 바꾼경우 상대 플레이어의 멀리건이 끝날때까지 기다려야하고 선택을 하지 않았더라도 제한 시간이 지나면 자동으로 손패가 확정되고 게임이 시작된다.

선택 후 상대방을 기다림, 선택 안할 시 시간제한 후 자동 선텍

Class Diagram

현재 게임은 GameSetPhase 단계에서 자신의 Relic과 TacticalSkill을 선택하고 이후 멀리건을 선택하고 다음 Phase로 넘어간다.

간단하게 GameFlow를 보면

GameSetPhase 시작 -> SelectRelicAndTaticalSkill UI 띄움 -> 선택 완료 후 상대방 기다림 or 시간제한 후 자동 선택 -> 멀리건 UI -> 선택 완료 후 상대방 기다림 or 시간제한 후 자동 선택 -> TurnStart Phase 시작

이때 Widget은 Client에서만 존재하기 때문에 언리얼 Server 구조에 맞춰서 개발해야 한다.

Widget과 관련된 내용은 PlayerController, 전체 Phase 흐름은 GameState에서 관리하도록 했다.

두 플레이어의 응답 기다리기

플레이어가 응답을 끝냈더라도 상대방이 아직 선택을 하지 않았다면 기다려야한다.

이를 순서대로 보면 

  • 선택 관련 Widget을 클라이언트별로 생성
  • 선택완료를 누르거나 시간제한이 끝나면
    • Widget 닫기
    • 기다리는 Widget 생성
  • 모두 선택했다면 기다리는 Widget을 닫고 다음 Step으로

GameSetPhase

GameSetPhase가 시작하면 SelectRelicAndTaticalSkill() 을 호출하여 전술 스킬과 성유물을 선택할 수 있게 한다.

이때 Phase 관련함수는 서버에서 실행되고 각 PlayerController의 Client RPC를 호출하여 생성할 WidgetClass와 제한 시간을 파라미터로 넘겨준다. 

void UWildGameSetPhase::Server_NextStep_Implementation()
{
   GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, "GameSet NextStep");
   switch (CurrentStep)
   {
   case 0:
      SelectRelicAndTacticalSkill();
      break;
   case 1:
      Mulligan();
      break;
   default:
      Server_EndPhase();
   }
   CurrentStep++;
}

void UWildGameSetPhase::SelectRelicAndTacticalSkill()
{
   AGameStateBase* GameState = UGameplayStatics::GetGameState(this);
   if (!GameState) return;

   for (APlayerState* PlayerState : GameState->PlayerArray)
   {
      APlayerController* PlayerController = PlayerState->GetPlayerController();
      check(PlayerController);
      AWildTestPhasePlayerController* WildPlayerController = Cast<AWildTestPhasePlayerController>(PlayerController);
      check(WildPlayerController);
      WildPlayerController->Client_AddHUDAndSetTimer(SelectRelicAndTaticalSkillWidgetClass, TimeLimit);
   }
}

PlayerController

각 Client에서 WidgetClass로 UserWidget을 생성하고 Timer로 제한 시간이 끝날때 FinishSelect를 호출해 자동으로 선택이 완료되도록 한다.

FinshSelect는 Client에서 실행되는 함수와 Server에서 실행되는 함수로 나뉘어져있는데

  • Client - Client에 존재하는 선택Widget과 WaitWidget을 지우고 Timer를 Clear
  • Server - GameState에 이 Player는 선택을 완료했다고 전달
void AWildTestPhasePlayerController::Client_AddHUDAndSetTimer_Implementation(TSubclassOf<UWildUserWidget> WidgetClass, float Time)
{
   if (!WidgetClass) return;

   //AddHUD
   UWildUserWidget* Widget = CreateWidget<UWildUserWidget>(this, WidgetClass);
   if (Widget)
   {
      Widget->AddToViewport();
      //TODO : Controller 모드를 처리하는 함수 필요
      bShowMouseCursor = true;
      SetInputMode(FInputModeUIOnly());
   }
   FTimerDelegate TimerDel;
   TimerDel.BindUFunction(this, FName("FinishSelect"), Widget);
   //SetTimer
   GetWorldTimerManager().SetTimer(
      SelectTimer,
      TimerDel,
      Time,
      false
   );

}

void AWildTestPhasePlayerController::Server_FinishSelect_Implementation()
{
   if (!HasAuthority()) return;

   AGameStateBase* GameState = UGameplayStatics::GetGameState(this);
   check(GameState);
   AWildTestPhaseGameState* WildGameState = Cast<AWildTestPhaseGameState>(GameState);
   check(WildGameState);

   //TODO : WildGameState의 하나의 함수로 만들 필요
   WildGameState->IsPlayerSelect.Add(true);
   WildGameState->PostSelect();

}

void AWildTestPhasePlayerController::FinishSelect(UWildUserWidget* OldWidget)
{
   if (IsValid(OldWidget))
   {
      OldWidget->RemoveFromParent();
   }

   //Clear Timer
   GetWorldTimerManager().ClearTimer(SelectTimer);

   //Add WaitWidget to Viewport
   WaitWidget = CreateWidget<UWildUserWidget>(this, WaitWidgetClass);
   if (WaitWidget)
   {
      WaitWidget->AddToViewport();
      //TODO : Controller 모드를 처리하는 함수 필요
      bShowMouseCursor = true;
      SetInputMode(FInputModeUIOnly());
   }
   Server_FinishSelect();
}

void AWildTestPhasePlayerController::Client_RemoveWaitWidget_Implementation()
{
   WILD_CHECK(IsValid(WaitWidget));
   WaitWidget->RemoveFromParent();
}

SelectRelicAndTacticalSkill Widget

간단하게 UI식별을 위한 Text, 남은 시간을 보여주는 Text, 선택을 끝내는 Button만 구성하였다.

PlayerController에 접근하여 남은 시간을 얻고, 버튼 클릭시에는 FinishSelect를 호출하도록 했다.

GameState

GameState에서는 두 플레이어가 모두 선택을 완료했는지 확인하고 선택을 했다면 Phase의 다음 단계로 넘어가도록 해야한다.

IsPlayerSelect 배열에 Player가 응답하면 원소를 추가하고 추가할때 원소가 2개라면 다음 스텝으로 넘어가도록 했다.

PlayerController에서는 선택완료시(FinishSelect를 호출할때) IsPlayerSelect에 Add하고  PostSelect를 호출해야한다.

void AWildTestPhaseGameState::PostSelect()
{
   if (IsPlayerSelect.Num() >= 2)
   {
      CurrentPhase->Server_NextStep();
      IsPlayerSelect.Empty();
      for (APlayerState* PlayerState : PlayerArray) {
         AWildTestPhasePlayerController* WildPlayerController = Cast<AWildTestPhasePlayerController>(PlayerState->GetPlayerController());
         //WILD_CHECK(!WildPlayerController);
         WildPlayerController->Client_RemoveWaitWidget();
      }
   }
}

선택을 완료했을 때 기다리는 상태의 플레이어의 WaitWidget을 제거해야하기 때문에 PlayerController들의 Client_RemoveWaitWidget을 호출하도록 한다.

결과

처음은 EndSelect를 둘 다 눌렀을 때 GameSetPhase의 다음 Step인 Mulligan으로 넘어가는 것을 볼 수 있고

두 번째는 한명이 EndSelect를 누르고 한 명이 시간을 초과했을 때 다음 Step으로 넘어가는 것을 볼 수 있다.