Post

2026-04-22 TIL (42일차)

2026-04-22 TIL (42일차)

Unreal 게임 플로우 설계와 데이터 관리 아키텍처

게임 플로우(Game Flow)란 플레이어가 게임을 시작하고, 진행하며, 클리어(또는 게임 오버)하기까지의 모든 규칙과 흐름을 의미합니다. 언리얼 엔진에서는 이 흐름을 어디에 구현하느냐가 프로젝트의 안정성과 직결됩니다.


1. 게임 흐름을 어디에 구현할까? (아키텍처 설계)

작성하신 대로 GameModeBaseGameStateBase와, GameModeGameState와 짝을 맞추어 사용하는 것이 기본 원칙입니다. 게임 흐름은 프로젝트 규모와 멀티플레이 여부에 따라 구현 위치가 달라집니다.

🔹 1) GameMode에 구현하는 경우 (서버 / 규칙 관리자)

  • 역할: 게임의 ‘심판’ 역할입니다. 승리 조건, 스폰 규칙, 데미지 공식 등을 처리합니다.
  • 멀티플레이 특징: 멀티플레이 시 GameMode는 오직 ‘서버(호스트)’에만 존재합니다. 클라이언트(접속자)의 컴퓨터에는 아예 존재하지 않습니다. 따라서 클라이언트가 해킹으로 규칙을 조작하는 것을 원천 차단할 수 있습니다.

🔹 2) GameState에 구현하는 경우 (전역 상황판)

  • 역할: 심판(GameMode)이 내린 결정의 ‘결과물’을 들고 있는 전광판입니다. 현재 스코어, 남은 시간, 퀘스트 진행도 등을 가집니다.
  • 멀티플레이 특징: 서버에 있는 GameState가 모든 클라이언트에게 복제(Replication)되어 뿌려집니다. 즉, 클라이언트가 “우리 팀 지금 몇 점이지?”를 알고 싶다면 GameState를 열어봐야 합니다.

🔹 3) 기타 구현 위치 (프로그래머 스타일에 따라 다름)

  • PlayerController: 플레이어 개인의 흐름(UI 열기, 개인 미션 진행, 인벤토리)을 관리할 때 사용합니다.
  • LevelScriptActor (레벨 블루프린트): 특정 맵(레벨)에서만 발생하는 고유한 기믹이나 시네마틱 연출 흐름을 짤 때 사용합니다.

멀티플레이 정석 흐름: 클라이언트가 몬스터를 때림 -> 서버의 GameMode가 죽었다고 판정하고 점수 10점 올림 -> 점수 결과를 GameState에 기록 -> GameState가 모든 클라이언트의 화면(UI)에 10점 올랐다고 동기화해 줌.


2. 스폰(Spawn) 함수가 AActor*를 반환해야 하는 이유

기존에 단순히 화면에 아이템을 띄우기만 할 때는 void 반환형으로 충분했습니다. 하지만 GameState에서 게임 흐름을 제어하려면 AActor* (액터 포인터)를 반환받아야 합니다.

  • void 반환의 한계 (Fire and Forget): 엔진에게 “아이템 하나 생성해!”라고 명령만 하고 끝입니다. 방금 태어난 아이템이 메모리 어디에 있는지 알 길이 없습니다.
  • AActor* 반환의 장점 (추적과 관리): 아이템을 생성함과 동시에 그 아이템의 ‘메모리 주소’를 돌려받습니다.
    • 왜 필요할까?: GameState가 “맵에 코인이 10개 스폰됐고, 다 먹으면 게임 클리어”라는 규칙을 관리한다고 칩시다. GameState는 방금 태어난 코인들의 주소(AActor*)를 배열(TArray)에 담아두고 추적해야 합니다. 그래야 코인이 파괴될 때 남은 개수를 카운팅하고, 0개가 되면 다음 스테이지로 넘기는 흐름을 만들 수 있습니다.


3. 언리얼 IsA 함수의 정확한 기능

IsA 함수는 객체가 특정 클래스인지, 혹은 그 클래스를 상속받은 자식 클래스인지 안전하게 확인할 때 사용하는 매우 유용한 함수입니다. (메모하신 ‘하위 클래스까지 인지해준다’는 말이 정확합니다. = 다형성)

  • 동작 원리: if (MyItem->IsA(ACoinItem::StaticClass()))
    • 만약 MyItemACoinItem 이라면? 👉 True
    • 만약 MyItemACoinItem을 상속받아 만든 ABigCoinItem이나 ASmallCoinItem이라면? 👉 부모가 코인이므로 True
    • 만약 MyItem이 힐링 포션이라면? 👉 False
  • Cast와의 차이: Cast<ACoinItem>(MyItem)은 진짜로 그 타입으로 변환해서 기능을 쓰고자 할 때(무거움) 쓰고, IsA는 단순히 “너 코인 계열 맞지?”라고 족보(타입)만 검사할 때(가벼움) 사용합니다.


4. 레벨 전환 초기화와 GameInstance (싱글톤)

GameState는 치명적인 약점이 하나 있습니다. 바로 현재 레벨(World)에 종속된 액터라는 점입니다.

  • 초기화 문제: 레벨 1에서 레벨 2로 넘어갈 때(Level Transition), 이전 레벨에 있던 모든 액터(GameState 포함)는 메모리에서 파괴되고 새로운 레벨의 GameState가 BeginPlay를 호출하며 0부터 다시 태어납니다. 누적된 스코어가 다 날아가 버립니다.
  • 해결책: GameInstance 사용
    • GameInstance는 레벨이 아니라 ‘게임 프로그램(애플리케이션)’ 자체에 종속된 객체입니다. 게임이 켜질 때 단 1개만 생성되고, 게임을 완전히 종료할 때까지 절대 파괴되지 않습니다. (디자인 패턴의 싱글톤 객체와 유사합니다.)
    • 따라서 여러 레벨에 걸쳐서 유지해야 하는 정보(총 누적 점수, 플레이어의 인벤토리, 환경 설정값 등)는 반드시 GameInstance에 저장해 두고 필요할 때마다 꺼내 써야 합니다.




UI 시스템 및 데이터 연동

1. HUD와 UMG의 이해

  • 과거의 HUD (Canvas 기반): 언리얼 엔진 초기 시절, C++ 코드로 화면의 좌표를 계산해 글자나 이미지를 직접 ‘그리는(Draw)’ 방식이었습니다. 직관적이지 않고 작업이 매우 불편했습니다.
  • 현대의 UMG (Unreal Motion Graphics): 현재 언리얼에서 UI를 만드는 표준 방식입니다. 눈에 보이는 시각적 에디터(위젯 블루프린트)를 제공하며, 내부적으로는 Slate라는 언리얼 기본 UI 프레임워크를 기반으로 돌아갑니다.

🔹 위젯 블루프린트 기본 팁

  • Canvas Panel: 도화지 역할을 합니다. UI 요소들을 자유롭게 배치하기 위해 가장 먼저 깔아줍니다.
  • 해상도 기준: 보통 1920x1080 (FHD) 해상도를 기준으로 작업하며, 모니터 비율이 달라져도 UI가 망가지지 않도록 앵커(Anchor) 설정과 화면 비율 테스트를 수시로 해주는 것이 좋습니다.
  • Justification: 텍스트나 위젯의 정렬(좌, 우, 중앙)을 의미합니다.


2. Build.cs에 UMG를 추가하는 이유?

언리얼 엔진의 C++ 코드를 작성하다 보면 Build.cs 파일에 모듈을 추가해야 할 때가 있습니다.

1
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "UMG" });
  • 이유: 언리얼 엔진은 거대한 기능들을 ‘모듈(Module)’ 단위로 쪼개어 관리합니다. 내 C++ 코드에서 UUserWidget 같은 UI 관련 기능을 사용하려면, 엔진에게 “나 UMG 모듈 좀 가져다 쓸게!” 라고 허락을 맡고 링크해 줘야 합니다. 이 설정을 안 하면 컴파일 에러가 발생합니다.

🔹 Public vs Private 의 차이:

  • PublicDependency: 내 게임 모듈뿐만 아니라, 내 모듈을 참조하는 ‘다른 모듈’들에게도 이 종속성을 노출할 때 씁니다. (일반적인 단일 게임 프로젝트에서는 여기다 다 넣어도 무방합니다.)
  • PrivateDependency: 오직 내 모듈 내부(.cpp 파일)에서만 몰래 사용할 때 씁니다. 초대형 프로젝트에서 컴파일 속도를 최적화하기 위해 엄격히 구분하지만, 소규모/개인 프로젝트에서는 크게 체감되지 않으므로 보통 Public에 다 넣습니다.


3. HUDWidgetClass vs HUDWidgetInstance 의 차이

코드를 보면 위젯을 선언할 때 두 가지 변수를 만듭니다. 이 둘의 차이를 아는 것은 객체 지향 프로그래밍의 핵심입니다.

1
2
3
4
5
UPROPERTY(EditAnywhere)
TSubclassOf<UUserWidget> HUDWidgetClass; // 1번

UPROPERTY(VisibleAnywhere)
UUserWidget* HUDWidgetInstance; // 2번
  • 1. HUDWidgetClass (붕어빵 틀 / 설계도): * “어떤 종류의 UI를 띄울 것인가?”를 정해두는 변수입니다.
    • 에디터 디테일 패널에서 기획자가 직접 WBP_MyHUD 같은 블루프린트를 골라넣을 수 있도록 열어둔 껍데기(타입)입니다. 메모리에는 아직 UI가 존재하지 않습니다.
  • 2. HUDWidgetInstance (구워진 붕어빵 / 실제 객체): * 게임이 시작되고 CreateWidget 함수를 통해 설계도(Class)를 바탕으로 실제로 게임 메모리에 생성된 살아있는 UI 객체입니다.
    • 이 변수를 따로 저장해두는 이유: 생성된 UI의 점수를 바꾸거나, 화면에서 지우고 싶을 때 “그 UI”를 찾아가서 명령을 내려야 하므로, 생성된 객체의 메모리 주소(포인터)를 저장해두는 것입니다.


4. UI 데이터 연동 (Data Binding)의 2가지 방식

UI에 점수나 체력을 표시할 때 2가지 접근법이 있습니다.

  • 프로퍼티 바인딩 (Tick 기반 - 초보자용):
    • 블루프린트 에디터에서 함수를 묶어두는 방식입니다.
    • 단점: 데이터가 변하든 안 변하든 매 프레임(Tick)마다 계속 값을 읽어오기 때문에 UI가 많아지면 성능 저하(부하)의 주범이 됩니다. 실무에서는 지양합니다.
  • 이벤트 기반 갱신 (SetText - 실무용):
    • 평소에는 UI가 아무 일도 안 하고 가만히 있습니다.
    • 플레이어가 코인을 먹어서 점수가 변하는 ‘그 순간(이벤트)’에만 C++이나 블루프린트에서 UI의 SetText 함수를 한 번 호출하여 글자를 바꿔줍니다. 최적화에 가장 좋습니다.


5. RemoveFromParent()

이전에 UI를 화면에 띄울 때 AddToViewport()를 사용했던 것 기억하시나요? RemoveFromParent()는 그와 정확히 반대되는 기능을 수행합니다.

  • 정의: 위젯을 현재 속해있는 부모(Parent)로부터 분리하여 화면에서 지워버리는 함수입니다.
  • 언제 쓸까?: 열려있는 인벤토리를 닫을 때, 일시정지 메뉴에서 ‘X’ 버튼을 눌렀을 때, 혹은 화면에 떠 있던 데미지 숫자가 시간이 지나 사라져야 할 때 사용합니다.
  • 특징 (메모리 관리): 이 함수를 호출한다고 해서 UI 객체가 메모리에서 즉시 파괴되는 것은 아닙니다. 만약 HUDWidgetInstance처럼 변수에 포인터로 잘 저장해 두었다면 화면에서 안 보일 뿐 객체는 살아있습니다. 나중에 다시 AddToViewport()를 부르면 언제든 다시 나타납니다. (매번 새로 CreateWidget을 하는 것보다 훨씬 성능에 좋습니다!)
1
2
3
4
5
// UI 닫기 예시
if (HUDWidgetInstance)
{
    HUDWidgetInstance->RemoveFromParent();
}


6. 마우스 커서 및 입력 모드 제어 코드 분석

게임을 하다가 ‘I’ 키를 눌러 인벤토리를 열었는데, 마우스 커서가 없어서 버튼을 누를 수 없거나 마우스를 움직일 때마다 게임 속 카메라(시선)가 돌아가면 안되기 때문에 이를 방지하기 위한 필수 코드입니다.

1
2
bShowMouseCursor = true;
SetInputMode(FInputModeUIOnly());

위 코드는 주로 플레이어 컨트롤러(PlayerController) 내부에서 UI를 화면에 띄운 직후에 호출합니다.

🔹 1단계: bShowMouseCursor = true;

  • 의미: 화면에 윈도우 마우스 커서(화살표)를 보이게 만듭니다.
  • 역할: 플레이어가 마우스를 움직여 UI 버튼 위에 올리거나(Hover) 클릭할 수 있도록 시각적인 포인터를 제공합니다.

🔹 2단계: SetInputMode(FInputModeUIOnly());

  • 의미: 언리얼 엔진의 ‘키보드/마우스 입력 처리 방식(Input Mode)’‘UI 전용(UI Only)’으로 변경합니다.
  • 역할 (매우 중요): 이 코드가 실행되면 플레이어가 WASD를 누르거나 마우스를 클릭해도 캐릭터가 움직이거나 총을 쏘지 않습니다. 모든 입력 신호가 오직 UI 위젯으로만 전달되도록 게임 로직으로 가는 입력을 차단하는 역할을 합니다.

다시 게임으로 돌아가려면?

UI(예: 일시정지 메뉴)를 RemoveFromParent()로 닫았다면, 플레이어가 다시 캐릭터를 조종할 수 있도록 설정을 원래대로 되돌려 놓아야 합니다.

1
2
3
4
5
// 1. 마우스 커서를 다시 숨깁니다.
bShowMouseCursor = false;

// 2. 입력 모드를 다시 '게임 전용'으로 되돌려줍니다.
SetInputMode(FInputModeGameOnly());


7. 위젯 컴포넌트란?

  • 정의: 2D 위젯(User Widget)을 3D 액터의 컴포넌트로 부착하여, 월드 상의 특정 위치에 UI를 표시할 수 있게 해주는 기능입니다.
  • 주요 용도: 캐릭터 머리 위의 체력바(HP Bar), 닉네임, 월드 내 상호작용 프롬프트(예: “E 키를 눌러 열기”), 게임 내 컴퓨터 모니터 화면 등.


8. Space 설정: 스크린(Screen) vs 월드(World)

위젯 컴포넌트의 Space 옵션은 UI가 렌더링되는 방식과 위치 계산 방식을 결정합니다.

🔹 스크린 모드 (Screen Space)

  • 작동 방식: 위젯이 3D 공간에 배치된 것처럼 보이지만, 실제로는 플레이어의 2D 화면(Viewport) 최상단에 그려집니다.
  • 특징:
    • 고정된 가독성: 카메라와의 거리에 상관없이 UI 크기가 일정하게 유지되도록 설정할 수 있습니다.
    • 항상 위에 표시: 3D 월드의 벽이나 장애물에 가려지지 않고 항상 플레이어 눈에 보입니다.
    • 최적화: 2D 레이어에 직접 그리기 때문에 월드 모드보다 렌더링 비용이 저렴한 경우가 많습니다.
  • 추천 사용처: MOBA 게임의 플레이어 체력바, 이름표 등 정보 전달이 최우선인 UI.

🔹 월드 모드 (World Space)

  • 작동 방식: 위젯이 실제 3D 월드 안의 하나의 판(Quad)처럼 존재합니다.
  • 특징:
    • 원근감 적용: 카메라와 멀어지면 UI가 작아지고, 가까워지면 커집니다.
    • 폐색(Occlusion) 발생: 벽 뒤로 가면 UI도 같이 가려집니다.
    • 라이팅 영향: 월드의 빛과 그림자 영향을 받을 수 있어 게임 환경에 녹아든 느낌(몰입감)을 줍니다.
  • 추천 사용처: 게임 내 키오스크 화면, 문 옆의 도어락 패드, 몰입형 서바이벌 게임의 인벤토리 등.

🔹 요약

구분스크린(Screen)월드(World)
렌더링 위치2D 뷰포트 (항상 위)3D 월드 좌표 (물체 뒤에 가려짐)
크기 변화거리와 무관하게 일정하게 가능거리에 따라 원근감 적용
핵심 가치정보 전달의 명확성공간적 몰입감
대표 예시롤(LoL) 캐릭터 체력바데드 스페이스의 홀로그램 UI


9. WBP 핵심 프로퍼티 (주요 설정)

  • Widget Class: 이 컴포넌트가 실제로 화면에 띄울 위젯 블루프린트(WBP)를 지정합니다.
  • Draw Size: 위젯이 그려질 가상의 해상도 크기입니다. (예: 500x100)
  • Pivot: UI의 중심점을 설정합니다. (머리 위 체력바의 경우 하단 중앙인 [0.5, 1.0]이 유리합니다.)
  • Is Two Sided: 월드 모드에서 뒷면에서도 UI가 보이게 할지 결정합니다.
This post is licensed under CC BY 4.0 by the author.