앞서 정의한 Cooldown을 만든 이유는 본 게임에 들어가기 전에 약간의 휴식시간이면서 다음 라운드를 준비하는 시간이라고 할 수 있다. 몇몇 FPS게임은 이러한 시간을, 상점시스템을 구현하여 해당 UI를 띄움으로서 플레이어에게 무기 구매를 가능케 하는 시간으로 활용한다.
이를 구현하기 위해 가장 먼저 떠오른 로직은 GameMode의 쿨다운때 해당 HUD를 캐스팅하여 Store화면을 띄우게 하는것이다.
void ACDGameMode::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (MatchState==MatchState::WaitingToStart)
{
Countdown=FMath::CeilToInt(WarmUpTime+LevelStartingTime-GetWorld()->GetTimeSeconds());
UE_LOG(LogTemp,Display,TEXT("%f"), Countdown);
if (Countdown==-1)
{
StartMatch();
}
}
else if (MatchState==MatchState::InProgress)
{
Countdown=WarmUpTime+MatchTime+LevelStartingTime-GetWorld()->GetTimeSeconds();
if (Countdown<=0.f)
{
SetMatchState(MatchState::Cooldown);
}
}
else if (MatchState==MatchState::Cooldown)
{
Countdown= CooldownTime + WarmUpTime + MatchTime - GetWorld()->GetTimeSeconds() + LevelStartingTime;
ATeamGameMode* TeamGameMode = Cast<ATeamGameMode>(this);
if (TeamGameMode)
{
CDHUD->AddStore(true);//store를 보이게 했다가
}
if (Countdown<=0.f)
{
if (TeamGameMode)
{
CDHUD->AddStore(false);//쿨다운 타임이 끝나면 스토어를 다시 끄게 만든다.
}
RestartGame();
}
}
}
이렇게 하면 된다고 생각할 수 있다. 과거의 내가 했던것 처럼.
주의해야 할 것은 GameMode에서 직접 HUD를 건들면 절대 안된다는 사실이다.
멀티플레이어 게임에서 HUD를 건들때 대부분 Controller를 경유해서 HUD를 띄우는 데는 이유가 있다. GameMode나 PlayerState, GameState같은 경우는 서버 전용 함수다. 즉 서버에서는 직접 클라이언트의 HUD를 조작하지 못한다. 만약 서버가 클라이언트의 UI를 조작하려고 하면 충돌이 일어나며, 만약 우회적인 루트로 가능케 한다고 하더라도, 이는 권장되지 않는 방식이다.
예를 들어 위의 코드처럼 CDHUD를 띄운다고 가정하자. 이 CDHUD는 오로지 하나의 객체만 생성되고 그 하나의 객체를 모든 플레이어가 접근하는 것이다. 배정된 마우스 컨트롤러도 하나고 그 객체에 전달될 웨폰도 하나일텐데 공통된 HUD를 만진다는건 어불성설이다.
때문에 존재하는게 Controller다. Controller는 클라이언트마다 하나씩 존재하며, Controller -> HUD의 접근 방식은 자기 소유의 화면만 건드리게 되는것이다.
간략하게 다시 Controller와 GameMode에 대한 역할을 살펴보자.
PlayerController 줄여서 PC의 역할은 플레이어의 개인 관리이다.
각 플레이어의 하나하나 입력, HUD 표시, 개인 세팅을 담당한다. GameMode가 지시하게 되면 플레이어 별로 구체적인 행동을 실행하게 만드는 역할이 바로 PC의 역할이다.
GameMode의 역할은 게임의 규칙과 흐름관리이다.
즉 게임 전체 진행을 관리하는 관리자이자, 라운드 시작, 쿨다운, 게임 끝과 같은 상태 전환을 결정한다. 또한 모든 플레이어를 대상으로 일괄적으로 명령하는 입장에 있다.
따라서 내가 택한 방법은 GameMode에서 Notify하여 Controller에게 전달해줌으로써 해당 UI를 보이게 하는 것이다. 컨트롤러 입장에서는 그저 GameMode에서 지금 쿨다운 시간이다라는 걸 공지받고, 그것이 끝나는 타이밍만 알면 된다.
void ACDGameMode::NotifyPlayersCooldown(bool IsActivate)
{
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
ACDPlayerController* CDController = Cast<ACDPlayerController>(*It);
if (CDController)
{
CDController->ShowStoreWidget(IsActivate);
}
}
}
따라서 해당 코드처럼, 게임에 접속한 플레이어들에게 지금 storewidget을 띄울 시간임을 알리는 함수를 만들고, PC에서 해당 정보를 전달받아 HUD를 띄우는 함수를 간접적으로 실행하기만 하면된다.
void ACDPlayerController::ShowStoreWidget(bool bShow)
{
CDHUD=CDHUD==nullptr?Cast<ACDHUD>(GetHUD()):CDHUD;
if (CDHUD)
{
CDHUD->AddStore(bShow);
}
}
최종적으로 Cooldowntime에 해당 시간을 Notify하는 함수를 bool타입으로 집어넣기만 하면된다.
else if (MatchState==MatchState::Cooldown)
{
Countdown= CooldownTime + WarmUpTime + MatchTime - GetWorld()->GetTimeSeconds() + LevelStartingTime;
if (!bNotifiedCooldown)
{
NotifyPlayersCooldown(true);
bNotifiedCooldown=true;
}
if (Countdown<=0.f)
{
NotifyPlayersCooldown(false);
RestartGame();
}
}
주의 해야 할것은 해당 모드를 결정하는 함수는 Tick에서 실행된다. 따라서 매 프레임마다 해당 함수를 호출할 가능성이 있기에 최초를 감지하는 플래그인 bNotifiedCooldown과 같은 변수를 집어넣어서 설계하는게 좋다. Round기반의 경우 WaitingtoStart에서 다시 초기화하여 사용한다면, 안정적인 함수를 만들어낼 수 있다.