Blog

ENGINEERING NOTE

[Unreal Engine] Gameplay Framework 1

출처 ㅣ Cedric Neukirchen - Multiplayer Network Compendium해당 페이지에서는 언리얼 엔진의 "Gameplay Framework"에서 가장 일반적으로 사용되는 클래스들에 대해 설명합니다. 또한 이러한 클래스들이 어떻게 사용되는지에

Unreal Engine

Gameplay Framework

해당 페이지에서는 언리얼 엔진의 "Gameplay Framework"에서 가장 일반적으로 사용되는 클래스들에 대해 설명합니다. 또한 이러한 클래스들이 어떻게 사용되는지에 대한 간단한 예제도 제공합니다.

이 목록은 프레임워크에 대한 소개를 목적으로 하며, 이후 다른 내용에서 이 클래스들을 언급할 수 있도록 하기 위함입니다.

예제들은 모두 복제(Replication)에 대한 지식을 필요로 합니다. 만약 예제를 이해하기 어렵다면, 복제(Replication) 관련 챕터를 먼저 읽은 후 다시 참고해도 괜찮습니다.

게임 장르에 따라 이 클래스들을 다르게 사용할 수도 있습니다. 여기서 제시하는 예제와 설명이 클래스를 활용하는 유일한 방법은 아닙니다.


AGameMode

⚠️ 주의
4.14 버전부터 AGameMode 클래스가 AGameModeBase와 AGameMode로 분리되었습니다. GameModeBase는 기능이 더 적으며, 일부 게임은 기존 AGameMode 클래스의 모든 기능이 필요하지 않을 수 있습니다.

AGameMode 클래스는 게임의 규칙을 정의하는 데 사용됩니다. 여기에는 APawn, APlayerController, APlayerState 등과 같은 다른 게임플레이 프레임워크 클래스를 스폰하는 것이 포함됩니다.

이 클래스는 서버에서만 사용할 수 있습니다. 클라이언트는 AGameMode 클래스의 인스턴스를 가지지 않으며, 이를 가져오려고 하면 nullptr만 반환됩니다.

Examples & Usage

GameMode의 사용 예시는 오래된 1인칭 슈팅 게임(예: Unreal Tournament)에서 볼 수 있습니다:

데스매치, 팀 데스매치, 깃발 뺏기 등

즉, GameMode는 다음과 같은 것들을 정의할 수 있습니다:

  • 팀이 있는지, 모두가 각자 경쟁하는지
  • 승리 조건

- 개인 또는 팀이 몇 번의 킬을 달성해야 하는지

  • 점수 획득 방식

- 상대를 처치하기 - 깃발을 훔치기

  • 사용할 캐릭터
  • 허용되는 무기

- 권총만? - 탄창 하나만?

멀티플레이어 환경에서는 GameMode가 플레이어 관리 및 경기 흐름을 제어하는 데 도움이 되는 다양한 기능을 제공합니다.

Blueprint Examples

Functions

첫 번째로 살펴볼 부분은 블루프린트 버전의 Override Function 섹션입니다:

이 함수들에 대한 로직을 구현하여 게임의 규칙에 맞게 동작을 변경할 수 있습니다. 예를 들어, GameMode가 DefaultPawn을 스폰하는 방식이나 게임이 시작할 준비가 되었는지 판단하는 방법을 변경할 수 있습니다.

예시로는 모든 플레이어가 서버에 접속하여 준비가 되었는지 확인하는 로직이 있습니다.

하지만 경기 중에 발생하는 특정 상황에 반응할 수 있는 이벤트들도 있습니다.

그 중 자주 사용하는 좋은 예시는 'Event OnPostLogin'입니다.

이 이벤트는 새로운 플레이어가 게임에 접속할 때마다 호출됩니다. 이후 연결 과정에 대해 더 배우게 되겠지만, 지금은 이대로 진행하겠습니다.

이 이벤트는 연결된 플레이어의 UConnection이 소유한 유효한 PlayerController를 전달합니다(이 부분도 나중에 더 설명합니다).

이를 통해 해당 플레이어와 상호작용할 수 있습니다. 예를 들어, 새로운 Pawn을 스폰하거나 PlayerController를 배열에 저장해두는 용도로 사용할 수 있습니다.

앞서 언급했듯이, GameMode를 사용하여 게임의 전체적인 경기 흐름을 관리할 수 있습니다. 이를 위해 'Ready To Start Match'와 같은 오버라이드 가능한 함수들이 있습니다.

이 함수들과 이벤트들은 현재 MatchState를 제어하는 데 사용할 수 있습니다. 대부분은 'Ready To Start Match' 함수가 TRUE를 반환할 때 자동으로 호출되지만, 직접 수동으로 사용할 수도 있습니다.

'New State'는 단순한 'FName' 타입입니다. 이제 “왜 이 부분이 AGameState 클래스에서 처리되지 않는가?”라는 질문을 할 수 있습니다.

실제로 이러한 GameMode 함수들은 GameState와 긴밀하게 협력합니다.

이것은 클라이언트가 접근할 수 없는 MatchState를 관리할 수 있는 지점을 제공하기 위한 것입니다. GameMode는 서버에만 존재하기 때문입니다!

Variables

GameMode에서 사용할 수 있는 중요한 변수들도 있습니다.

다음은 이미 상속된 변수들의 목록입니다. 일부는 GameMode 블루프린트의 ClassDefaults에서 설정할 수 있습니다.

대부분은 'Default Player Name'처럼 쉽게 이해할 수 있습니다. 이 변수는 연결된 모든 플레이어에게 기본 PlayerName을 부여하며, 이는 APlayerState 클래스를 통해 접근할 수 있습니다.

또는 'bDelayedStart'는 'Ready To Start Match'의 기본 구현이 모든 조건을 충족하더라도 게임 시작을 지연시킵니다.

중요한 변수 중 하나는 'Options String'입니다. 이 옵션들은 '?'로 구분되며, 'OpenLevel' 함수나 콘솔 명령어로 'ServerTravel'을 호출할 때 전달할 수 있습니다.

'Parse Option'을 사용하여 'MaxNumPlayers'와 같은 전달된 옵션을 추출할 수 있습니다.

UE C++ Examples

모든 블루프린트 작업은 C++에서도 구현할 수 있습니다. 동일한 정보를 반복하지 않기 위해 이전 블루프린트 예제를 C++로 재현하는 코드 예제를 제공합니다.

'ReadyToStartMatch'는 'BlueprintNativeEvent'이므로 실제 C++ 구현 함수 이름은 'ReadyToStartMatch_Implementation'입니다. 이 함수를 오버라이드해야 합니다.

c++
// .h
// 이 매치에서 필요한/허용되는 최대 플레이어 수
int32 MaxNumPlayers;

// ReadyToStartMatch 오버라이드 구현
virtual bool ReadyToStartMatch_Implementation() override;
c++
// .cpp
bool ATestGameMode::ReadyToStartMatch_Implementation()
{
    Super::ReadyToStartMatch();

    return MaxNumPlayers == NumPlayers;
}

'OnPostLogin' 함수는 virtual이며 C++에서는 'PostLogin'이라고 부릅니다.

cpp
// PlayerController 목록
UPROPERTY()
TArray<APlayerController*> PlayerControllerList;

// PostLogin 함수 오버라이드
virtual void PostLogin(APlayerController* NewPlayer) override;
cpp
void ATestGameMode::PostLogin(APlayerController* NewPlayer)
{
    Super::PostLogin(NewPlayer);

    PlayerControllerList.Add(NewPlayer);
}

AGameState

⚠️주의
4.14 버전부터 GameState 클래스가 AGameStateBase와 AGameState로 분리되었습니다. GameStateBase는 기능이 더 적으며, 일부 게임은 기존 GameState 클래스의 모든 기능이 필요하지 않을 수 있습니다.

AGameState 클래스는 서버와 클라이언트 간에 정보를 공유하는 데 가장 중요한 클래스입니다.

GameState는 게임/매치의 현재 상태를 추적하는 데 사용됩니다. 멀티플레이어에서는 연결된 플레이어(APlayerState) 목록을 포함하는 것이 중요합니다.

또한, 모든 클라이언트에 복제되므로 누구나 접근할 수 있습니다. 이로 인해 GameState는 멀티플레이어 게임에서 정보 측면에서 가장 중심적인 클래스 중 하나입니다.

GameMode가 승리에 필요한 킬 수를 정의한다면, GameState는 각 플레이어 및/또는 팀의 현재 킬 수를 추적합니다!

여기에 어떤 정보를 저장할지는 전적으로 개발자에게 달려 있습니다. 점수 배열이나 그룹 및 길드 추적을 위한 커스텀 구조체 배열 등도 저장할 수 있습니다.

Examples and Usage

멀티플레이어 환경에서 AGameState 클래스는 게임의 현재 상태를 추적하는 데 사용되며, 여기에는 플레이어와 각 플레이어의 PlayerState도 포함됩니다.

GameMode는 GameState의 MatchState 관련 함수들이 호출되도록 관리하며, GameState는 클라이언트에서도 이러한 함수들을 사용할 수 있도록 해줍니다.

GameMode에 비해 GameState가 제공하는 기능은 많지 않지만, 클라이언트에 정보를 전달하는 로직을 구현하는 데 충분히 활용할 수 있습니다.

Blueprint Examples

Variables

기본 AGameState 클래스에서 몇 가지 변수를 사용할 수 있습니다. PlayerArray, MatchState, ElapsedTime은 모두 복제(Replicated)되어 클라이언트에서도 접근할 수 있습니다.

AuthorityGameMode는 해당되지 않습니다. GameMode는 서버에만 존재하므로 서버만 접근할 수 있습니다.

PlayerArray 자체는 직접적으로 복제되지 않지만, 각 PlayerState가 복제되며 생성 시 PlayerArray에 자신을 추가합니다. 또한 GameState가 이들을 수집하여 레이스 컨디션(race-condition) 문제를 방지합니다.

아래는 C++에서 PlayerState들이 PlayerArray에 수집되는 방법을 간단히 보여주는 예시입니다.

text
void APlayerState::PostInitializeComponents()
{
    // […]

    UWorld* World = GetWorld();
    // 이 PlayerState를 게임의 ReplicationInfo에 등록합니다.
    if (World->GameState != NULL)
    {
        World->GameState->AddPlayerState(this);
    }

    // […]
}
text
void AGameState::PostInitializeComponents()
{
    // […]

    for (TActorIterator<APlayerState> It(World); It; ++It)
    {
        AddPlayerState(*It);
    }
}

void AGameState::AddPlayerState(APlayerState* PlayerState)
{
    if (!PlayerState->bIsInactive)
    {
        PlayerArray.AddUnique(PlayerState);
    }
}

이 모든 과정은 서버와 클라이언트의 PlayerState 및 GameState 인스턴스에서 발생합니다!

Functions

두 팀 'A'와 'B'의 점수를 추적하는 간단한 함수 예시를 들 수 있습니다. 예를 들어, 한 팀이 점수를 획득할 때 호출되는 CustomEvent가 있다고 가정해봅시다.

이 이벤트는 어떤 팀이 점수를 얻었는지 알 수 있도록 boolean 값을 전달합니다. PlayerState, Team 등 식별에 사용하는 값을 전달할 수도 있습니다.

나중에 “Replication” 챕터에서 오직 서버만 변수를 복제할 수 있다는 규칙을 배우게 되므로, 반드시 서버만 이 이벤트를 호출해야 합니다.

이 이벤트는 다른 클래스(예: 누군가를 처치한 무기)에서 호출되며, 항상 서버에서 발생해야 하므로 별도의 RPC가 필요하지 않습니다.

이러한 변수들과 GameState가 복제되기 때문에, 필요한 다른 클래스에서 이 두 변수를 사용하고 가져올 수 있습니다. 예를 들어, 스코어보드 위젯에 표시하는 경우입니다.

UE C++ Examples

이 작은 예제를 재현하려면 좀 더 많은 코드가 필요하지만, 함수 자체에도 불구하고 복제를 설정하는 데 필요한 코드는 클래스당 한 번만 필요합니다.

text
// 복제를 위해 반드시 포함해야 합니다.
#include “UnrealNetwork.h”

// Replicated 지정자를 사용하여 변수를 복제하도록 표시
UPROPERTY(Replicated)
int32 TeamAScore;

UPROPERTY(Replicated)
int32 TeamBScore;

// 팀의 점수를 증가시키는 함수
void AddScore(bool bTeamAScored);

이 함수에 대해서는 복제 파트에서 더 자세히 다룹니다!

text
// 이 함수는 UPROPERTY 매크로의 Replicated 지정자를 통해 필요하며, 해당 매크로에 의해 선언됩니다.
void ATestGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(ATestGameState, TeamAScore);
    DOREPLIFETIME(ATestGameState, TeamBScore);
}
text
void ATestGameState::AddScore(bool bTeamAScored)
{
    if (bTeamAScored)
    {
        TeamAScore++;
    }
    else
    {
        TeamBScore++;
    }
}