Part 3. 게임플레이 프레임워크: World · Level · GameMode · GameState · Controller · Pawn · Character

Semi·2026년 4월 16일

Unreal Engine 정리

목록 보기
3/3
post-thumbnail

공식 문서(Gameplay Framework, Game Mode and Game State, Characters in Unreal Engine, Pawn in Unreal Engine, UWorld API, ULevel API, UGameplayStatics API) 기반 검증 완료 버전


이 문서를 읽기 전에

Part 1에서 UObject 시스템(리플렉션·GC·CDO), Part 2에서 Actor 시스템(컴포넌트·라이프사이클·Transform)을 배웠다면, 이제 그 위에 올라서는 게임플레이 프레임워크를 볼 차례다.

게임플레이 프레임워크는 Actor들을 역할에 따라 분리해서 조직한 구조다. "누가 게임 규칙을 정하는가", "게임 전체 상태는 누가 갖고 있는가", "플레이어의 의지는 누가 표현하는가", "플레이어의 물리적 몸체는 무엇인가"를 각각 전담 클래스로 나눈다.

그 전에 먼저 게임이 돌아가는 공간 자체인 World와 Level의 구조를 이해해야 한다. GetWorld()가 무엇을 반환하는지, GameMode와 GameState가 어디에 속해있는지를 이해하는 데 기반이 되기 때문이다.


1. 언리얼 엔진 전체 클래스 계층

모든 것은 UObject에서 시작한다.

UObject  ← 리플렉션·GC·직렬화·CDO의 최상위 기반
    │
    ├─ UEngine                    ← 엔진 코어 (앱 전체 생명주기)
    │       └─ UGameEngine        ← 게임 전용
    │
    ├─ UGameInstance              ← 앱 생명주기와 동일 (레벨 전환 무관)
    │
    ├─ UWorld                     ← 게임이 돌아가는 런타임 컨테이너
    │
    ├─ ULevel                     ← 레벨 콘텐츠 단위 (.umap 하나)
    │
    ├─ UBlueprintFunctionLibrary  ← 정적 유틸리티 함수 모음
    │       └─ UGameplayStatics
    │
    └─ AActor                     ← 월드에 존재하는 모든 것
            ├─ AGameModeBase / AGameMode
            ├─ AGameStateBase / AGameState
            ├─ APlayerController
            ├─ APlayerState
            ├─ APawn
            │       └─ ACharacter
            └─ (레벨에 배치되는 일반 액터들)

2. UWorld와 ULevel — 게임이 돌아가는 공간

2-1. World와 Level의 차이

이 둘을 혼동하기 쉽다. 정확하게 구분하면:

공식 API 문서:

"The World is the top level object representing a map or a sandbox in which Actors and Components will exist and be rendered."

공식 ULevel 문서:

"The level object. Contains the level's actor list, BSP information, and brush list. Every Level has a World as its Outer."

Level = Map (완전히 같은 개념)
→ .umap 파일 하나에 해당하는 콘텐츠 단위
→ 에디터에서 배치한 Actor들, 라이팅, 지형 등을 담음
→ "맵을 열다" = "레벨을 열다" (혼용이 맞음)

World (월드)
→ 레벨을 실행하는 런타임 컨테이너
→ 게임 로직이 실제로 돌아가는 공간
→ GameMode, GameState, PlayerController가 여기에 소속됨
→ GetWorld()로 접근하는 대상이 바로 UWorld

관계:
.umap 파일 = Level = Map     (콘텐츠 단위)
World = 그 레벨을 실행하는 환경 (런타임 단위)

OpenLevel이 월드를 교체하는 이유:
레벨(콘텐츠)을 교체하려면
그 레벨을 담고 있는 월드(컨테이너)째로 교체해야 하기 때문
→ 함수명은 OpenLevel이지만 실제로는 World 전체 교체

2-2. UWorld와 ULevel의 소속 관계

UEngine
    ├─ UGameInstance*             ← 엔진 멤버 (월드와 형제 관계)
    └─ UWorld* CurrentWorld       ← 현재 활성 월드

UWorld
    ├─ AGameMode* AuthorityGameMode   ← 직접 멤버 포인터
    ├─ AGameState* GameState          ← 직접 멤버 포인터
    ├─ TArray<APlayerController*>     ← PlayerController 목록
    ├─ ULevel* PersistentLevel        ← 항상 로드된 메인 레벨
    └─ TArray<ULevelStreaming*>        ← 스트리밍 레벨들

ULevel
    └─ TArray<AActor*> Actors         ← 레벨에 배치된 모든 Actor

소속 관계 핵심:

GameMode, GameState   → UWorld의 멤버 → 월드 교체 시 함께 파괴
레벨에 배치된 Actor들 → ULevel::Actors → 레벨 파괴 시 함께 파괴
GameInstance          → UEngine의 멤버 → 월드 교체와 무관하게 유지

GameInstance와 UWorld는 형제 관계:
→ 둘 다 UEngine 아래에 있음
→ GameInstance가 World를 소유하는 게 아님
→ GameInstance가 레벨 전환에도 살아있는 이유:
  UEngine이 파괴될 때(앱 종료)까지 유지되기 때문

2-3. PersistentLevel과 StreamingLevel

PersistentLevel (영구 레벨):
→ 월드가 살아있는 동안 항상 로드 상태 유지
→ 레벨 스트리밍의 기준이 되는 메인 레벨
→ 월드 교체(OpenLevel) 시에는 함께 교체됨

StreamingLevel (스트리밍 레벨):
→ PersistentLevel 위에 필요에 따라 추가/제거하는 레벨들
→ 로드(메모리)와 가시성(화면 표시)을 별개로 제어 가능
→ 오픈월드처럼 넓은 맵을 구역 단위로 나눠 관리할 때 사용

비유:
PersistentLevel = 무대 바닥 (항상 있음)
StreamingLevels = 무대 위에 올려놓는 소품들 (추가/제거 가능)

2-4. 레벨 전환 방식 비교

[OpenLevel — 완전 교체]
UGameplayStatics::OpenLevel(GetWorld(), LevelName)
→ 현재 World 전체 언로드 → 새 World 생성
→ GameState, GameMode, 모든 Actor 파괴
→ 가장 단순, 싱글플레이에 적합

[LoadStreamingLevel — 유지하면서 추가]
→ 현재 World 유지하면서 추가 레벨 로드
→ World 파괴 없음
→ 오픈월드, 구역 단위 로드/언로드에 사용

[Seamless Travel — OpenLevel의 멀티 강화 옵션]
→ 월드 교체는 동일하지만 전환 과정이 비동기(Non-blocking)
→ 멀티플레이에서 플레이어 연결 끊김 방지 목적
→ PlayerController는 기본적으로 새로 생성되지만
  PlayerState의 데이터는 CopyProperties()를 통해 새 인스턴스에 복사됨
→ GameState는 기본적으로 파괴됨

2-5. 레벨 배치 Actor vs 시스템 객체

같은 AActor를 상속받지만 관리 주체가 다르다.

레벨 배치 Actor (SpawnVolume, BaseItem 등):
→ 에디터에서 직접 배치한 것들
→ ULevel::Actors 배열에 저장됨
→ GetAllActorsOfClass()로 검색

시스템 객체 (GameMode, GameState, PlayerController):
→ 레벨에 배치하는 게 아님
→ 엔진이 월드 생성 시 자동으로 만드는 것들
→ UWorld의 직접 멤버 포인터로 관리
→ GetGameMode(), GetGameState()로 바로 접근

GetAllActorsOfClass()를 직접 ULevel::Actors 접근 대신 쓰는 이유:

직접 ULevel->Actors에 접근하면:
→ PersistentLevel만 봐서 로드된 StreamingLevel의 Actor를 못 찾음
→ 삭제 예정(PendingKill) Actor도 배열에 포함되어 있음
→ nullptr 체크, 유효성 검사를 전부 직접 해야 함

GetAllActorsOfClass()가 내부적으로 처리하는 것:
→ PersistentLevel + 현재 로드된(loaded) StreamingLevel 통합 순회
→ 아직 로드되지 않은 StreamingLevel의 Actor는 반환 안 됨
→ 유효성 체크 (PendingKill 제외)
→ 클래스 타입 필터링
→ 파생 클래스(IsA 계열)도 포함해서 반환
→ 이 모든 것을 한 번에 안전하게 처리

3. 게임플레이 프레임워크 전체 구조

3-1. 전체 클래스 구성

공식 문서:

"A game is made up of a GameMode and GameState. Human players joining the game are associated with PlayerControllers. These PlayerControllers allow players to possess pawns in the game so they can have physical representations in the level."

[게임 규칙 · 진행]
AGameModeBase      ← 게임의 규칙과 흐름. 서버에만 존재.
AGameStateBase     ← 게임 전체 공개 상태. 서버 + 모든 클라이언트.

[플레이어 · AI 제어]
APlayerController  ← 플레이어의 의지(입력·카메라·UI). 서버 + 소유 클라이언트.
AAIController      ← AI의 판단. Pawn을 빙의해서 제어.

[물리적 몸체]
APawn              ← 컨트롤러가 조종할 수 있는 모든 엔티티의 기반.
ACharacter         ← APawn + 캡슐 충돌 + 스켈레탈 메시 + 이동 시스템.

[플레이어 데이터]
APlayerState       ← 플레이어 개인 데이터(점수·이름). 서버 + 모든 클라이언트.
AGameInstance      ← 레벨 전환에도 유지되는 전역 데이터. 앱 전체 생명주기.

3-2. 비유로 이해하기

공식 문서의 보드게임 비유:

GameMode    = 게임 마스터만 갖고 있는 룰북
             (게임 규칙 정의, 서버만 볼 수 있음)

GameState   = 모든 플레이어가 볼 수 있는 게임 보드
             (현재 점수, 남은 시간 등 공개 정보)

PlayerController = 플레이어의 두뇌
             (어떻게 움직일지 결정하는 의사결정 주체)

Pawn        = 게임 보드 위의 말(token)
             (실제 월드에서 보이는 물리적 표현)

3-3. 싱글플레이어에서의 서버-클라이언트 구조

언리얼은 싱글플레이어 게임에서도 내부적으로 서버-클라이언트 구조를 유지한다. 다만 서버와 클라이언트가 같은 프로세스 안에 있을 뿐이다.

싱글플레이어:
[로컬 서버 역할 + 로컬 클라이언트 역할] = 하나의 프로세스

멀티플레이어:
[서버 프로세스] ←네트워크→ [클라이언트 프로세스 A]
               ←네트워크→ [클라이언트 프로세스 B]

"GameMode는 서버에만 존재"의 의미는 멀티플레이어 환경에서 클라이언트 프로세스에는 GameMode 인스턴스가 없다는 뜻이다. 싱글플레이어에서는 항상 유효한 포인터를 반환한다.

복제(Replication) 개념 — 지금은 개념만:

UPROPERTY(Replicated) 변수:
서버에서 변경 → 자동으로 모든 클라이언트에 동기화 (서버 → 클라이언트)
클라이언트에서 서버로는 기본적으로 안 됨

스테이지 타이머:
서버에서 RemainingTime 감소
→ Replicated 변수이므로 자동으로 클라이언트에 복제
→ 클라이언트 화면에도 같은 시간 표시

코인 획득 점수:
클라이언트가 "점수 추가 함수 실행해줘"라고 서버에 RPC 요청
→ 서버가 실제 계산
→ 결과가 Replicated 변수로 클라이언트에 복제
→ 서버가 권위자(Authority): 치팅 방지의 기본 원리

싱글플레이어에서는 서버 = 클라이언트이므로
이런 고민 없이 GameState에 바로 접근해서 값을 변경하면 됨
Replication 심화는 멀티플레이어 파트에서 별도 다룸

4. 게임 객체 생성 순서와 BeginPlay 호출 순서

4-1. 게임 실행 시 객체 생성 순서

공식 문서:

"The general order of events is to initialize the engine, create and initialize a GameInstance, then load a level, and finally start playing."

1. GameInstance 생성
   (UEngine에 등록. 레벨 로드 전에 완료.)

2. 레벨 파일(.umap) 로드 → 새 UWorld 생성
   (UWorld 안에 PersistentLevel 구성)

3. GameMode 생성
   (WorldSettings에 지정된 클래스로, UWorld의 멤버로 생성)

4. GameState 생성
   (GameMode의 PreInitializeComponents 내부에서 생성.
    GameMode::GameStateClass 기준. UWorld의 멤버로 생성.)

5. PlayerController 생성
   (GameMode::PlayerControllerClass 기준)

6. Pawn/Character 생성 및 PlayerController에 빙의

7. HUD 생성

8. ULevel::Actors에 배치된 Actor들 생성
   (레벨에 직접 배치한 SpawnVolume, 아이템 등)

9. 각 Actor들의 BeginPlay() 호출

4-2. BeginPlay 호출 순서

공식 커뮤니티 위키:

"Order of called actors is unknown, we can't never rely on Actor X calling BeginPlay before Actor Y."

Actor들 간의 BeginPlay 호출 순서는 보장되지 않는다. 엔진이 특정 Actor보다 다른 Actor의 BeginPlay를 먼저 호출해줄 것이라는 가정에 코드를 짜면 안 된다.

보장되는 것:
→ BeginPlay는 모든 Actor의 생성과 초기화가 끝난 뒤 호출됨
→ 즉, BeginPlay 시점에는 다른 시스템 객체들이 이미 존재함
   (GameMode, GameState, PlayerController 모두 준비됨)

보장되지 않는 것:
→ GameMode::BeginPlay()가 Character::BeginPlay()보다
   반드시 먼저 호출된다는 순서 보장 없음
→ Actor A의 BeginPlay()가 Actor B의 BeginPlay()보다
   먼저 호출된다는 보장 없음

그러면 의존 관계가 있는 초기화는 어떻게 하는가?

잘못된 패턴:
void AMyCharacter::BeginPlay()
{
    Super::BeginPlay();
    // GameState의 BeginPlay가 먼저 실행됐을 거라고 가정하고 접근
    // → 순서 보장이 없으므로 위험
    GetGameState<ASpartaGameState>()->StartLevel();
}

올바른 패턴:
void AMyCharacter::BeginPlay()
{
    Super::BeginPlay();
    // BeginPlay 시점에 GameState 객체 자체는 이미 존재함
    // → 객체 접근은 안전, 다른 Actor의 BeginPlay 실행 여부에 의존하지 않음
    if (ASpartaGameState* GS = GetGameState<ASpartaGameState>())
    {
        GS->GetScore(); // GameState 객체의 값 읽기 → 안전
    }
}

BeginPlay에서 다른 Actor의 특정 함수가 "이미 실행됐을 것"이라는 가정을 없애고, 필요한 데이터는 직접 읽어오는 방식으로 설계해야 한다.

4-3. PostLogin — 플레이어 입장 처리

PostLogin은 새 플레이어가 게임에 참가했을 때 GameMode에서 호출된다. 호출 시점에는 이미 PlayerController와 PlayerState가 생성 완료된 상태다.

공식 문서 (Persistent Data Compendium — Epic 공식 채널):

"PlayerState: Created by GameMode inside AController::InitPlayerState(), called from AGameModeBase::PostLogin()."

즉, PlayerState는 PostLogin 내부에서 생성된다. PostLogin을 오버라이드해서 Super::PostLogin(NewPlayer)를 먼저 호출하면, 그 시점에 PlayerState가 이미 생성 완료된 상태로 접근할 수 있다.

플레이어 입장 시 내부 흐름:
1. PlayerController 생성 (AGameModeBase::Login → SpawnPlayerController)
2. Super::PostLogin 내부:
   → AController::InitPlayerState() 호출
   → PlayerState 생성 → PlayerController에 연결
   → GameState.PlayerArray에 PlayerState 추가
3. PostLogin 오버라이드 본문 실행  ← Super::PostLogin() 이후이므로 PC, PS 모두 유효
4. GameMode.RestartPlayer() → Pawn 스폰 → Possess
void ASpartaGameMode::PostLogin(APlayerController* NewPlayer)
{
    Super::PostLogin(NewPlayer);

    // 이 시점에 PlayerController, PlayerState 모두 유효
    UE_LOG(LogTemp, Warning, TEXT("플레이어 입장: %s"),
        *NewPlayer->GetPlayerState<APlayerState>()->GetPlayerName());
}

5. GameMode / GameModeBase — 서버 전용 규칙 엔진

5-1. GameModeBase란 무엇인가

공식 문서:

"The GameMode sets the rules for the game."

AGameModeBase는 게임의 규칙과 흐름을 정의하는 클래스다. 서버에만 존재하며 클라이언트에서 GetAuthGameMode()를 호출하면 nullptr을 반환한다.

GameMode가 담당하는 것:
→ 어떤 클래스들을 사용할지 지정 (DefaultPawnClass, PlayerControllerClass 등)
→ 플레이어 스폰 (PostLogin에서 처리)
→ 게임 규칙 (승리 조건, 점수 계산, 라운드 진행 등)
→ 레벨 전환 타이밍

GameMode에 두면 안 되는 것:
→ 모든 클라이언트가 알아야 하는 게임 상태 → GameState
→ 개별 플레이어 데이터 → PlayerState

5-2. GameModeBase vs GameMode

AGameModeBaseAGameMode
특징기본 기능만 포함MatchState 시스템 포함
MatchState✅ WaitingToStart → InProgress → WaitingPostMatch
권장 대상대부분의 게임언리얼 토너먼트 스타일 매치

대부분의 게임에서는 AGameModeBase로 충분하다.

5-3. 사용 클래스 지정과 레벨별 설정

// GameMode 생성자에서 사용 클래스 지정
// → StaticClass()로 UClass*를 TSubclassOf 멤버 변수에 등록
ASpartaGameMode::ASpartaGameMode()
{
    DefaultPawnClass      = ASpartaCharacter::StaticClass();
    PlayerControllerClass = ASpartaPlayerController::StaticClass();
    GameStateClass        = ASpartaGameState::StaticClass();
    PlayerStateClass      = ASpartaPlayerState::StaticClass();
}
C++ 설정 = 기본값 (BP 없어도 돌아가는 안전망)
BP 설정  = 실제 운용 값 (BP CDO가 C++ CDO 복사 + 설정값 덮어씀)
→ 최종적으로 BP에서 설정한 값이 적용됨

레벨별 GameMode 설정:

방법 1: 프로젝트 기본값
Edit → Project Settings → Maps & Modes → Default GameMode

방법 2: 레벨별 설정 (우선순위 높음)
레벨 에디터 → Window → World Settings → GameMode Override

6. GameState / GameStateBase — 공개 게임 상태

6-1. GameStateBase란 무엇인가

AGameStateBase는 게임 전체에서 모든 플레이어가 알아야 하는 공개 정보를 담는 클래스다. GameMode와 달리 서버와 모든 클라이언트에 존재한다.

GameState가 담당하는 것:
→ 게임 타이머 (남은 시간, 경과 시간)
→ 팀 점수, 글로벌 점수
→ 접속 중인 플레이어 목록 (PlayerArray)
→ 현재 매치 상태

흐름:
GameMode (서버 전용)
    → 규칙에 따라 GameState 값을 변경
    → Replicated 변수 → 모든 클라이언트에 자동 동기화
    → 클라이언트에서 GameState 접근 가능

6-2. GameMode vs GameState — 무엇을 어디에 두는가

상황GameModeGameState
승리 조건 판정 로직
남은 시간 (모두에게 표시)
플레이어 스폰 로직
팀 점수 (HUD에 표시)
현재 라운드 번호

6-3. 커스텀 GameState 예시

// SpartaGameState.h
UCLASS()
class ASpartaGameState : public AGameState
{
    GENERATED_BODY()
public:
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Score")
    int32 Score;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Coin")
    int32 SpawnedCoinCount;    // 스폰된 코인 수

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Coin")
    int32 CollectedCoinCount;  // 수집한 코인 수

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")
    float LevelDuration;       // 레벨 제한 시간 (초)

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")
    int32 CurrentLevelIndex;   // 현재 레벨 인덱스

    FTimerHandle LevelTimerHandle;

    void StartLevel();
    void OnLevelTimeUp();
    void OnCoinCollected();
    void EndLevel();
};

7. PlayerController — 플레이어의 의지

7-1. PlayerController란 무엇인가

공식 문서:

"PlayerControllers allow players to possess pawns in the game so they can have physical representations in the level."

APlayerController는 플레이어와 게임 사이의 인터페이스다.

PlayerController가 담당하는 것:
→ 플레이어 입력 처리 (Enhanced Input System)
→ Pawn 빙의(Possess) · 해제(UnPossess)
→ 카메라 관리 (PlayerCameraManager)
→ HUD · UI 관리
→ 플레이어 식별 (게임 내내 유지)

7-2. PlayerController의 생존 범위

PlayerController와 Pawn의 생존 기간이 다르다는 것이 핵심이다.

[데스매치 예시]

PlayerControllerA 생성 (게임 내내 유지)
    ↓
Pawn_1 스폰 → PlayerControllerA가 Pawn_1 빙의
    ↓
Pawn_1 사망 → 파괴
PlayerControllerA는 살아있음 (빙의 해제 상태)
    ↓
Pawn_2 스폰 → PlayerControllerA가 Pawn_2 빙의

PlayerController: 동일 인스턴스 유지
Pawn:             죽을 때마다 파괴, 리스폰 시 새로 생성

이것이 PlayerController에 "플레이어 개인 정보(점수 등)"를 두면 안 되는 이유다. Pawn이 바뀌어도 유지해야 하는 데이터는 PlayerState에 저장한다.

7-3. GetController() vs GetWorld()->GetFirstPlayerController()

두 함수는 목적이 다르다.

// [GetController()] — Pawn의 멤버 함수
// 반환 타입: AController* (PlayerController일 수도, AIController일 수도 있음)
// 목적: "나를 지금 조종하는 컨트롤러가 누구인가?"
AController* C = GetController();

// PlayerController인지 AIController인지 구분할 때:
APlayerController* PC = Cast<APlayerController>(GetController());
if (PC)
{
    // 플레이어가 조종 중
}
// PC가 nullptr이면 AI가 조종 중
// [GetWorld()->GetFirstPlayerController()] — 월드에서 접근
// 반환 타입: APlayerController*
// 목적: "이 월드에 존재하는 (첫 번째) PlayerController를 가져와"
// 싱글플레이어에서 또는 PlayerController에 직접 접근 불가한 상황에서 사용
APlayerController* PC = GetWorld()->GetFirstPlayerController();

핵심 차이:

GetController():
→ Pawn이 현재 빙의된 컨트롤러
→ 플레이어일 수도, AI일 수도 있음
→ Pawn 안에서 "내 컨트롤러가 누구인지" 확인할 때

GetWorld()->GetFirstPlayerController():
→ 월드에 존재하는 PlayerController (항상 플레이어 전용)
→ Pawn 밖에서 플레이어 컨트롤러에 접근할 때
→ 싱글플레이어 기준으로 인덱스 0 = 로컬 플레이어

7-4. 서버/클라이언트에서의 존재

서버:    접속한 모든 플레이어의 PlayerController 존재
클라이언트 A: 자신(플레이어 A)의 PlayerController만 존재
          다른 플레이어들의 PlayerController는 nullptr

→ "서버 + 소유 클라이언트에만 존재"
→ 모든 플레이어가 공유해야 하는 데이터는
  PlayerState 또는 GameState에 두어야 함

8. Pawn — 컨트롤러가 조종하는 물리적 몸체

8-1. Pawn이란 무엇인가

공식 API 문서:

"Pawn is the base class of all actors that can be possessed by players or AI. They are the physical representations of players and creatures in a level."

APawn이 AActor에 추가하는 것:
→ Controller에 의한 빙의(Possess) / 해제(UnPossess) 능력
→ 입력 수신 능력 (SetupPlayerInputComponent)
→ 이동 입력 누적 (AddMovementInput)

APawn이 제공하지 않는 것:
→ 구체적인 이동 물리 (걷기·점프·수영) → ACharacter가 추가
→ 뼈대 애니메이션 → ACharacter가 추가
→ 기본 충돌 형태 → ACharacter가 추가

8-2. APawn을 직접 사용하는 경우

APawn 직접 상속이 적합한 경우:
→ 탈것 (자동차, 비행기, 우주선)
→ RTS 유닛 (클릭 이동 방식)
→ 이동 방식이 직립 보행과 완전히 다른 경우

ACharacter 상속이 적합한 경우:
→ 직립 보행하는 모든 캐릭터
→ 걷기·뛰기·점프·웅크리기가 필요한 경우
→ 대부분의 플레이어 캐릭터

8-3. UPawnMovementComponent — APawn용 이동 컴포넌트

공식 API 문서:

"PawnMovementComponent can be used to update movement for an associated Pawn. It also provides ways to accumulate and read directional input in a generic way."

이동 컴포넌트 계층 구조:

UActorComponent
    └── UMovementComponent
            └── UPawnMovementComponent       ← APawn 전용 이동 기반
                    ├── UCharacterMovementComponent  (ACharacter 전용)
                    ├── UFloatingPawnMovement        (중력 없는 자유 이동)
                    └── (직접 만든 커스텀 이동 컴포넌트)

APawn을 직접 상속받아 탈것 등을 만들 때 이동 방식이 필요하면 UPawnMovementComponent를 상속받아 구현한다.

// APawn 직접 상속 시 커스텀 이동 컴포넌트 패턴
UCLASS()
class UVehicleMovementComp : public UPawnMovementComponent
{
    GENERATED_BODY()
public:
    virtual void TickComponent(float DeltaTime, ELevelTick TickType,
        FActorComponentTickFunction* ThisTickFunction) override
    {
        // ConsumeInputVector()로 입력 누적값 읽기
        // MoveUpdatedComponent()로 실제 이동 처리
    }
};

9. ACharacter — 이동과 애니메이션이 내장된 Pawn

9-1. ACharacter란 무엇인가

공식 문서:

"With the addition of a CharacterMovementComponent, a CapsuleComponent, and a SkeletalMeshComponent, the Pawn class is extended into the highly-featured Character class."

추가된 것클래스역할
이동 시스템UCharacterMovementComponent걷기·뛰기·점프·수영·비행 물리 처리
충돌 캡슐UCapsuleComponent이동 충돌 계산의 기준 도형. RootComponent.
스켈레탈 메시USkeletalMeshComponent뼈대 기반 애니메이션 메시

9-2. CharacterMovementComponent

공식 문서:

"The CharacterMovementComponent allows avatars not using rigid body physics to move by walking, running, jumping, flying, falling, and swimming. It is specific to Characters, and cannot be implemented by any other class."

// CharacterMovementComponent 주요 설정값 — 생성자에서 설정
GetCharacterMovement()->MaxWalkSpeed = 600.f;
GetCharacterMovement()->MaxAcceleration = 2048.f;
GetCharacterMovement()->JumpZVelocity = 600.f;
GetCharacterMovement()->GravityScale = 1.0f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->BrakingDecelerationWalking = 2048.f;

// 이동 모드 전환
GetCharacterMovement()->SetMovementMode(MOVE_Flying);
GetCharacterMovement()->SetMovementMode(MOVE_Walking);

9-3. ACharacter 기본 구조

// SpartaCharacter.cpp
ASpartaCharacter::ASpartaCharacter()
{
    // ACharacter가 기본 제공 (직접 생성 불필요):
    // → CapsuleComponent (RootComponent)
    // → SkeletalMeshComponent
    // → CharacterMovementComponent

    // SpringArm — CapsuleComponent(RootComponent)에 부착
    SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
    SpringArmComp->SetupAttachment(RootComponent);
    SpringArmComp->TargetArmLength = 300.f;
    SpringArmComp->bUsePawnControlRotation = true;

    // Camera — SpringArm의 끝점(SocketName)에 부착
    // USpringArmComponent::SocketName
    // 공식 소스 주석: "The name of the socket at the end of the spring arm"
    // 스프링 암의 끝점(end)을 가리키는 static const FName 상수
    // 원점이 아닌 끝점에 붙여야 스프링 암의 길이 변화(벽 충돌로 인한 수축 등)가 카메라에 정확히 반영됨
    CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
    CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
    CameraComp->bUsePawnControlRotation = false;

    GetCharacterMovement()->MaxWalkSpeed = 600.f;
    GetCharacterMovement()->JumpZVelocity = 600.f;
}

9-4. 점프 처리

ACharacter에 점프 기능이 내장되어 있다.

// Enhanced Input에서 점프 바인딩
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started,
    this, &ACharacter::Jump);         // ACharacter 내장 함수
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed,
    this, &ACharacter::StopJumping);  // ACharacter 내장 함수

10. PlayerState — 플레이어 개인 데이터

APlayerState는 개별 플레이어의 데이터를 저장하는 클래스다. 서버와 모든 클라이언트에 존재해서 복제된다.

PlayerState에 두는 것:
→ 플레이어 이름, 개인 점수, 킬/데스 수, 팀 정보
→ 다른 플레이어들도 알아야 하는 개인 정보

PlayerController vs PlayerState:
PlayerController → 서버 + 소유 클라이언트만 존재, 입력·제어
PlayerState      → 서버 + 모든 클라이언트에 복제, 데이터 저장
// PlayerState 접근 방법
APlayerState* PS = GetPlayerState();                    // Pawn에서
APlayerState* PS = GetPlayerState<APlayerState>();      // PlayerController에서

// GameState에서 모든 플레이어 목록
for (APlayerState* PS : GetWorld()->GetGameState()->PlayerArray)
{
    UE_LOG(LogTemp, Warning, TEXT("플레이어: %s"), *PS->GetPlayerName());
}

11. GameInstance — 레벨을 넘어 유지되는 전역 데이터

UGameInstance는 게임 실행 내내 단 하나만 존재하며 레벨 전환에도 파괴되지 않는다.

GameInstance가 살아있는 이유:
UEngine의 멤버 포인터로 관리되기 때문
→ 엔진이 종료될 때(앱 종료)까지 유지
→ OpenLevel로 UWorld가 교체되어도 UEngine은 유지 → GameInstance도 유지

GameInstance에 두는 것:
→ 레벨 전환 후에도 유지해야 하는 데이터
→ 누적 점수, 현재 레벨 인덱스, 게임 설정

GameInstance에 두지 않는 것:
→ 멀티플레이어 복제가 필요한 데이터 (복제 미지원)
→ 현재 레벨 상태 데이터 → GameState
GameState vs GameInstance 비교:
생존 범위:  현재 레벨 내    vs  앱 전체 생명주기
레벨 전환:  파괴 후 재생성  vs  유지
용도:       레벨 내 공유 상태  vs  레벨 간 유지 데이터
예시:       SpawnedCoinCount  vs  TotalScore, CurrentLevelIndex

12. UGameplayStatics — 정적 유틸리티 함수 모음

공식 API 문서:

"Static class with useful gameplay utility functions that can be called from both Blueprint and C++"

UGameplayStatics:
→ UBlueprintFunctionLibrary를 상속받은 정적 유틸리티 클래스
→ 모든 함수가 static → 인스턴스 없이 클래스 이름으로 직접 호출
→ C++과 블루프린트 양쪽에서 모두 사용 가능
→ #include "Kismet/GameplayStatics.h" 필요

WorldContextObject를 첫 번째 인자로 받는 이유:

일반 멤버 함수:
→ 객체가 있으니 this가 자동으로 "나 자신"을 가리킴
→ GetWorld()로 자기 월드를 찾을 수 있음

UGameplayStatics::OpenLevel(GetWorld(), ...)
→ static 함수 = 특정 객체에 속하지 않음
→ "나 자신"이 없으니 GetWorld()를 자동으로 호출할 수 없음
→ 그래서 "어느 월드에서 실행할지"를 명시적으로 넘겨야 함
→ this를 넘길 수도 있음:
  UObject 계열이면 내부에서 GetWorld()를 호출해 월드를 찾아주기 때문
→ 인자 이름이 WorldContextObject인 이유:
  "월드를 찾기 위한 맥락(Context)이 되는 객체"

주요 함수들:

#include "Kismet/GameplayStatics.h"

// PlayerController 가져오기 (0 = 첫 번째 플레이어)
APlayerController* PC = UGameplayStatics::GetPlayerController(this, 0);

// 레벨 전환
UGameplayStatics::OpenLevel(this, FName("MainMenu"));

// 특정 클래스의 모든 Actor 찾기
TArray<AActor*> FoundActors;
UGameplayStatics::GetAllActorsOfClass(
    this, ASpawnVolume::StaticClass(), FoundActors);

// 데미지 적용
UGameplayStatics::ApplyDamage(
    Target, 10.f, Instigator, DamageCauser, UDamageType::StaticClass());

// 사운드 재생
UGameplayStatics::PlaySoundAtLocation(this, SoundAsset, Location);

// GameInstance 가져오기
UGameInstance* GI = UGameplayStatics::GetGameInstance(this);

13. 클래스 간 상호 접근 — 어디서 무엇을 가져오는가

// ──────────────────────────────────
// Actor / Pawn / Character 에서
// ──────────────────────────────────

GetWorld()                              // UWorld*
GetWorld()->GetAuthGameMode()           // AGameModeBase* (서버 전용)
GetWorld()->GetGameState()              // AGameStateBase*
GetWorld()->GetGameState<ASpartaGameState>() // 타입 지정 버전
GetWorld()->GetFirstPlayerController() // APlayerController* (싱글 기준)
GetGameInstance()                       // UGameInstance*

// ──────────────────────────────────
// Pawn / Character 에서 추가
// ──────────────────────────────────

GetController()                         // AController* (플레이어/AI 구분 필요)
Cast<APlayerController>(GetController()) // APlayerController*로 캐스팅
GetPlayerState()                        // APlayerState*
GetPlayerState<ASpartaPlayerState>()    // 타입 지정 버전

// ──────────────────────────────────
// PlayerController 에서
// ──────────────────────────────────

GetPawn()                               // APawn*
Cast<ASpartaCharacter>(GetPawn())       // 타입 지정 버전
GetPlayerState<APlayerState>()          // APlayerState*

14. 전체 흐름 — 게임 시작부터 플레이까지

[엔진 시작]
UGameInstance 생성 → Init() 호출 (앱 종료까지 유지)

[레벨 파일(.umap) 로드 → 새 UWorld 생성]
AGameMode 생성 (UWorld 멤버)
→ AGameState 생성 (GameMode 초기화 중 생성, UWorld 멤버)

[플레이어 입장 (PostLogin)]
APlayerController 생성 → APlayerState 생성 (PlayerController에 연결)
GameState.PlayerArray에 PlayerState 추가
PostLogin 호출 (이미 PC, PS 준비 완료 상태)

[Pawn 스폰]
GameMode.SpawnDefaultPawnAtTransform()
    → DefaultPawnClass로 Pawn 스폰
    → PlayerController.Possess(Pawn) 호출
    → PossessedBy(PlayerController) 호출

[게임 플레이]
PlayerController ← 플레이어 입력 수신
    → Pawn으로 이동 입력 전달
    → CharacterMovementComponent가 물리 처리
GameMode ← 게임 규칙 처리 (점수, 승패 판정)
    → GameState 값 변경 → 클라이언트에 복제

[Pawn 사망 → 리스폰]
GameMode → Pawn.Destroy()
PlayerController 유지 (빙의 해제)
PlayerState 유지 (점수 등 보존)
GameMode → 새 Pawn 스폰 → Possess

[레벨 전환 (OpenLevel)]
현재 World 교체 → GameMode, GameState, PlayerController, 레벨 Actor들 파괴
GameInstance 유지 → 다음 레벨에서 TotalScore, CurrentLevelIndex 사용 가능

15. 실무 판단 기준 — 무엇을 어디에 두는가

질문정답
"서버만 알아야 하는 게임 규칙"GameMode
"모든 플레이어가 볼 수 있는 게임 상태"GameState
"플레이어 입력·카메라·UI 관리"PlayerController
"다른 플레이어도 볼 수 있는 내 데이터"PlayerState
"내 캐릭터의 이동·애니메이션"Pawn / Character
"레벨 전환 후에도 유지해야 하는 데이터"GameInstance
"AI의 판단과 행동 결정"AIController
"월드 전체 검색, 레벨 전환, 사운드 재생 등 전역 유틸"UGameplayStatics
잘못된 패턴 → 올바른 패턴

PlayerController에 점수 저장
    → PlayerState에 저장

클라이언트에서 GetAuthGameMode() 사용
    → GetGameState()로 필요한 정보를 GameState에서 가져옴

GameMode에 플레이어 HUD 로직 작성
    → PlayerController 또는 HUD 클래스에 작성

Pawn에 레벨 간 유지 데이터 저장
    → GameInstance에 저장

핵심 요약

  • UWorld는 게임이 돌아가는 런타임 컨테이너다. Level(=Map)은 콘텐츠 단위이고, World는 그것을 실행하는 환경이다. OpenLevel은 이름과 달리 World 전체를 교체한다.

  • UEngine → UGameInstance / UWorld → ULevel → AActor 순서로 소속된다. GameInstance는 UEngine 멤버라 레벨 전환과 무관하게 유지된다. GameMode, GameState는 UWorld 멤버라 레벨 전환 시 파괴된다.

  • 게임 객체 생성 순서는 의존 관계 때문에 존재한다. GameInstance 생성 → 레벨(.umap) 로드 및 UWorld 생성 → GameMode → GameState → PlayerController → Pawn → 레벨 배치 Actor 순으로 생성된다. 단, Actor들 간의 BeginPlay 호출 순서는 보장되지 않으므로 특정 Actor의 BeginPlay가 다른 것보다 먼저 실행됐을 것이라는 가정에 의존하면 안 된다.

  • GetAllActorsOfClass()는 PersistentLevel과 모든 StreamingLevel을 통합 순회하고 유효성 체크까지 처리한다. ULevel::Actors에 직접 접근하는 것보다 안전하다.

  • GameMode는 서버 전용이다. 클라이언트에서 접근하면 nullptr. 대부분 AGameModeBase로 충분하다.

  • GameState는 서버 + 모든 클라이언트에 존재한다. GameMode가 변경하면 자동으로 클라이언트에 복제된다. 클라이언트 → 서버 방향은 기본적으로 되지 않는다.

  • PlayerController는 플레이어의 의지를 표현한다. Pawn이 죽어도 PlayerController는 유지된다. Pawn 내부에서 GetController()로 접근하고, 외부에서는 GetWorld()->GetFirstPlayerController()로 접근한다.

  • ACharacter는 APawn에 CapsuleComponent, SkeletalMeshComponent, CharacterMovementComponent를 추가한 클래스다. CharacterMovementComponent는 ACharacter 전용이며 APawn에 직접 붙일 수 없다.

  • UGameplayStatics는 static 유틸리티 함수 모음이다. 객체 없이 호출하므로 "어느 월드에서 실행할지" WorldContextObject를 명시적으로 넘겨야 한다. 이는 C++ 멤버 함수의 숨겨진 this와는 다른 개념이다.

profile
기반이 되는 강의를 진행하고, Ai와 다시 한 번 탐구하고 정리하여 포스팅합니다. 목적 자체가 저의 학습을 위함이기 때문에, 시리즈 관련 포스팅은 규칙적으로 나오지 않을 수 있습니다. 양해 부탁드립니다. (_ _)

0개의 댓글