게임 루프와 주요 클래스 구조
게임 루프 안에서 GameInstance·GameMode·Controller·Pawn·컴포넌트가 맡는 역할과 호출 흐름을 정리합니다.
C++로 Blueprint 클래스 확장에서 만든 액터는 BeginPlay에서 문구를 한 번 출력했고, Tick은 비워 두었습니다. 그렇다면 Tick은 정확히 언제, 얼마나 자주 불릴까요? 그리고 점수나 남은 시간처럼 여러 액터가 함께 쓰는 값은 어느 클래스에 두어야 할까요? 이 질문에 답하려면 엔진이 매 프레임 무슨 일을 하는지와, 게임 전체의 흐름을 나눠 맡는 주요 클래스가 무엇인지를 알아야 합니다.
이 글에서는 먼저 게임 루프(game loop)에서 Tick이 어떤 자리에 있는지와 시간에 따라 값을 바꾸는 올바른 방법을 살펴봅니다. 그다음 UGameInstance, AGameModeBase, AGameStateBase, APlayerController, APawn, 컴포넌트가 각각 무엇을 맡는지 정리하고, 데이터를 어느 클래스에 둘지 고르는 기준을 세웁니다.
게임 루프와 Tick
게임은 기본적으로 끝없이 반복되는 주기로 동작하는데, 이 반복을 게임 루프라고 부릅니다. 언리얼 엔진도 매 프레임(frame) 여러 작업을 하며 게임 세계를 갱신합니다. 이 루프 안에서 플레이어의 입력 처리, AI 갱신, 물리 시뮬레이션, 애니메이션, 렌더링 같은 게임 안의 변화가 이루어지며, 각 작업이 어떤 주기와 스레드에서 어떤 순서로 실행되는지는 시스템마다의 설정에 따라 달라집니다.
한 프레임의 처리에 참여하는 주요 작업은 다음과 같습니다. 이 목록은 엔진 전체의 고정된 실행 순서를 뜻하지는 않습니다.
- 입력 처리: 키보드, 마우스, 게임패드의 입력을 게임 동작으로 연결합니다.
- 게임 로직: 활성화된 액터의
Tick과 컴포넌트의TickComponent가 정해진 주기로 실행됩니다. - 물리와 AI: 충돌과 중력 같은 시뮬레이션, 그리고 환경 인식과 행동 선택을 갱신합니다.
- 렌더링과 사운드: 카메라, 메시, 텍스처, 조명으로 화면을 만들고 음악과 효과음을 처리합니다.
- 네트워크: 멀티플레이에서 필요한 상태를 정해진 복제 조건과 주기에 따라 주고받습니다.
이 작업들이 아주 짧은 시간 안에 되풀이되면서 부드러운 게임 화면이 만들어집니다. AActor::Tick(float DeltaTime)에 코드를 쓰면 그 코드는 게임 로직을 갱신하는 단계에서 매 프레임 실행될 수 있습니다. 여기서 DeltaTime은 이번 틱에 전달된 경과 시간(초)입니다.
다만 모든 액터에 Tick을 켜 두면 할 일이 없는 액터까지 매 프레임 호출되어 성능을 낭비합니다. 그래서 필요한 액터와 컴포넌트에만 Tick을 켜고, 매 프레임 처리가 정말 필요한지부터 따져 봅니다. Tick을 쓰더라도 실행 간격(tick interval), 틱 그룹(Tick Group), 먼저 실행되어야 할 다른 틱과의 의존성을 설정할 수 있습니다.
프레임 수가 아니라 경과 시간으로 계산하기
물리나 충돌, 감속 없이 한 축을 따라 초당 100cm로 움직이는 계산을 생각해 봅시다. 한 틱의 이동량은 100 × DeltaTime cm입니다. 1초 동안 틱이 30번 일어나 각각 약 1/30초가 전달되든, 60번 일어나 각각 약 1/60초가 전달되든, 전달된 경과 시간의 합이 1초라면 총 이동량은 둘 다 100 × 1 = 100cm입니다.
반대로 DeltaTime을 곱하지 않고 매 틱 100cm를 더하면, 30번의 틱에서는 3,000cm, 60번의 틱에서는 6,000cm가 되어 프레임 수에 따라 결과가 달라집니다. 빠른 컴퓨터에서 캐릭터가 더 빨리 움직이는 버그가 이렇게 생깁니다. 이 비교는 일정한 속도를 단순히 더해 가는 계산이므로, 실제 물리 이동이나 시간 배율, 일시 정지 같은 조건까지 해결해 주는 코드는 아닙니다.
언제 갱신할지 고르기
UNREAL C++ · CH2-4
언제 갱신해야 하는가
연속 계산은 Tick, 사건에 대한 반응은 이벤트, 일정 간격이나 지연 작업은 타이머를 먼저 검토합니다.
| 방식 | 알맞은 작업 | 주의할 경계 |
|---|---|---|
| Tick | 연속 이동·보간처럼 주기적으로 갱신할 계산 | 활성화·실행 간격·Tick Group·선행 의존성 확인 |
| 이벤트 | 입력·충돌·상태 변경에 대한 반응 | 사건이 발생하고 연결된 대상이 있을 때 호출 |
| 타이머 | 일정 간격의 갱신이나 지연 작업 | 프레임 처리의 영향을 받으므로 정확한 실시간 마감 보장 아님 |
- Tick
- 알맞은 작업: 연속 이동·보간처럼 주기적으로 갱신할 계산주의할 경계: 활성화·실행 간격·Tick Group·선행 의존성 확인
- 이벤트
- 알맞은 작업: 입력·충돌·상태 변경에 대한 반응주의할 경계: 사건이 발생하고 연결된 대상이 있을 때 호출
- 타이머
- 알맞은 작업: 일정 간격의 갱신이나 지연 작업주의할 경계: 프레임 처리의 영향을 받으므로 정확한 실시간 마감 보장 아님
컴포넌트는 로직을 담는 단위입니다. 필요한 컴포넌트 안에서도 Tick·이벤트·타이머를 선택합니다. 이 비교는 엔진 전체의 고정된 프레임 실행 순서가 아닙니다.
위 그림은 값을 갱신하는 시점에 따라 고를 수 있는 세 가지 방식을 비교합니다. Tick은 연속 이동이나 보간처럼 주기적으로 갱신해야 하는 계산에 알맞고, 쓸 때는 활성화 여부와 실행 간격, 틱 그룹, 선행 의존성을 확인합니다. 이벤트는 입력, 충돌, 상태 변경처럼 어떤 사건에 반응하는 일에 알맞으며, 그 사건이 일어나고 연결된 대상이 있을 때만 호출됩니다.
타이머는 일정한 간격으로 갱신하거나 일정 시간 뒤에 작업을 실행할 때 씁니다. 그림의 주의 사항처럼 타이머도 프레임 처리의 영향을 받으므로, 정확한 실시간 마감을 보장하지는 않습니다. 그림 아래의 설명대로 컴포넌트는 로직을 담는 단위일 뿐이고, 컴포넌트 안에서도 Tick과 이벤트, 타이머 가운데 알맞은 방식을 다시 고릅니다. 이 비교 역시 엔진 전체의 고정된 프레임 실행 순서를 나타내지는 않습니다.
게임의 흐름을 나눠 맡는 클래스
게임 루프와 함께 게임 전체의 흐름을 제어하는 핵심 클래스들이 있습니다. 이 클래스들은 각자 정해진 역할을 맡고, 서로 협력해 하나의 게임을 이룹니다.
UGameInstance
UGameInstance는 게임이 실행되는 동안 유지되는 상위 객체입니다. 일반적인 실행에서는 게임이 시작될 때 만들어져 게임이 끝날 때까지 남아 있습니다. 레벨이 바뀌어도 파괴되지 않으므로, 같은 게임 인스턴스 안에서 레벨 전환을 넘어 유지해야 할 진행 상황, 설정, 세션 정보 같은 데이터를 관리할 수 있습니다.
그래서 게임 전반의 데이터 관리, 세션 관리, 로딩 화면 전환, 설정을 저장하고 불러오는 일 등에 씁니다. 다만 에디터에서 플레이(PIE)할 때는 PIE 인스턴스마다 따로 존재하고, 네트워크 복제나 디스크 저장을 자동으로 해 주지는 않습니다.
AGameModeBase와 AGameMode
AGameModeBase와 그것을 상속해 매치 흐름 같은 기능을 더한 AGameMode는 게임의 규칙과 흐름을 정하는 클래스입니다. 어떤 캐릭터 클래스를 쓸지, 플레이어를 어디서 스폰할지, 점수를 어떻게 계산할지, 승리와 패배 조건이 무엇인지를 여기서 정합니다.
게임 모드는 서버에만 존재하고 클라이언트에는 복제되지 않습니다. 게임 규칙을 서버 한곳에서 관리해야 하기 때문입니다. 클라이언트는 다음에 볼 AGameStateBase를 통해 게임 상태를 전달받습니다. 게임 모드는 플레이어의 접속과 종료 처리, 게임 시작과 종료 로직, 기본 Pawn과 Controller, HUD 클래스 지정 같은 일에 쓰이며, 실제 UI 표시와 입력 처리는 보통 클라이언트의 PlayerController, HUD, 위젯 쪽에서 합니다.
AGameStateBase와 AGameState
AGameStateBase와 AGameState는 게임의 현재 상태를 모든 클라이언트와 공유하는 클래스입니다. 게임 모드의 규칙에 따라 바뀌는 점수, 남은 시간, 현재 라운드, 참여한 플레이어 목록 같은 정보를 담아 모든 플레이어에게 전달합니다. 그래서 점수판, 라운드 정보, 게임 진행 상태를 표시할 때 씁니다.
게임 스테이트는 서버에서 클라이언트로 상태를 전달하는 복제 경로를 제공하지만, 직접 추가한 변수가 저절로 전송되는 것은 아닙니다. 변수를 복제 대상으로 선언하고 등록하는 설정이 따로 필요하며, 이 방법은 복제(Replication) 시스템 이해에서 다룹니다.
APlayerController
APlayerController는 플레이어의 입력을 처리하고, 그 플레이어가 조종하는 APawn이나 ACharacter를 제어하는 클래스입니다. 키보드, 마우스, 게임패드의 입력 이벤트를 받아 게임 로직으로 바꾸고, 캐릭터를 움직이거나 상호작용하게 합니다. 캐릭터 이동, 카메라 제어, UI 상호작용, 인벤토리, 무기 발사처럼 플레이어와 관련된 거의 모든 입력과 상호작용 로직에 관여합니다.
PlayerController는 플레이어마다 하나씩 있으며, 서버와 그 플레이어의 클라이언트에 존재합니다. 다른 플레이어의 클라이언트에는 보통 그 PlayerController가 없습니다. AI가 조종하는 캐릭터는 AAIController가 맡습니다.
APawn과 ACharacter
APawn은 APlayerController나 AAIController가 빙의(Possess)해 조종할 수 있는 액터의 기본 클래스입니다. 즉 플레이어나 AI가 월드 안에서 조종하는 실제 몸체입니다. 폰은 월드에 배치되고 트랜스폼을 가지며, 플레이어 캐릭터, 적 AI, 차량, 비행기처럼 플레이어나 AI가 움직이는 대상을 만들 때 씁니다.
ACharacter는 APawn을 상속하며, 걷기, 뛰기, 점프, 웅크리기 같은 사람형 이동을 구현하는 데 특화되어 있습니다. UCharacterMovementComponent를 기본으로 가지고 있어 복잡한 이동을 쉽게 처리할 수 있습니다.
UActorComponent와 USceneComponent
UActorComponent는 액터가 소유하고 등록하는 기능 단위이고, USceneComponent는 UActorComponent를 상속해 3D 공간의 위치, 회전, 스케일과 부착(attachment) 계층을 갖는 컴포넌트입니다. 액터 하나에 여러 컴포넌트를 조립해 복잡한 기능을 만들 수 있는데, 3D 모델을 그리는 UStaticMeshComponent, 카메라 시점을 맡는 UCameraComponent, 나이아가라 이펙트를 재생하는 UNiagaraComponent, 사운드를 내는 UAudioComponent가 그 예입니다. 시각적 표현, 충돌 감지, 물리, 카메라, 사운드, 직접 만든 로직처럼 나눌 수 있는 기능을 컴포넌트로 구현합니다.
데이터를 어디에 둘까
클래스 이름을 외우기보다, 데이터가 어느 실행 환경에서 얼마나 오래 필요한지를 먼저 묻는 편이 위치를 정하기 쉽습니다.
UNREAL C++ · CH2-4
데이터의 수명과 공유 범위로 위치 결정
레벨 간 유지, 서버 규칙, 공유 상태, 로컬 입력, 월드 객체의 데이터는 필요한 수명과 실행 환경이 다릅니다.
| 데이터 예 | 둘 위치 | 수명·공유 범위 |
|---|---|---|
| 레벨 간 선택값 | GameInstance | 같은 게임 인스턴스에서 유지; 복제·디스크 저장은 별도 |
| 승리 규칙 | GameMode | 서버 권한에서 판단; 원격 클라이언트에 인스턴스 복제 안 함 |
| 현재 라운드 | GameState | 클라이언트에 공유할 상태; 사용자 정의 필드는 복제 설정 필요 |
| 플레이어별 점수 | PlayerState | 해당 플레이어의 공유할 상태; Pawn 재생성과 분리 가능 |
| 로컬 입력·UI | PlayerController | 로컬 플레이어의 처리와 서버 측 제어를 구분 |
| 몸체·개별 기능 | Pawn·컴포넌트 | 스폰·파괴되는 월드 대상의 수명에 맞춰 관리 |
- 레벨 간 선택값
- 둘 위치: GameInstance수명·공유 범위: 같은 게임 인스턴스에서 유지; 복제·디스크 저장은 별도
- 승리 규칙
- 둘 위치: GameMode수명·공유 범위: 서버 권한에서 판단; 원격 클라이언트에 인스턴스 복제 안 함
- 현재 라운드
- 둘 위치: GameState수명·공유 범위: 클라이언트에 공유할 상태; 사용자 정의 필드는 복제 설정 필요
- 플레이어별 점수
- 둘 위치: PlayerState수명·공유 범위: 해당 플레이어의 공유할 상태; Pawn 재생성과 분리 가능
- 로컬 입력·UI
- 둘 위치: PlayerController수명·공유 범위: 로컬 플레이어의 처리와 서버 측 제어를 구분
- 몸체·개별 기능
- 둘 위치: Pawn·컴포넌트수명·공유 범위: 스폰·파괴되는 월드 대상의 수명에 맞춰 관리
데이터 배치의 예시입니다. 복제 조건과 레벨 이동·재접속 시 보존 정책은 게임이 명시해야 합니다. 오래 사는 객체의 참조도 월드 액터의 생존을 보장하지 않습니다.
위 그림의 표는 데이터의 수명과 공유 범위에 따라 둘 위치를 정리합니다. 레벨 사이에 유지할 선택값은 같은 게임 인스턴스에서 유지되는 GameInstance에 두되, 복제와 디스크 저장은 따로 구성합니다. 승리 규칙은 서버 권한에서 판단하는 GameMode에 두며, 이 인스턴스는 원격 클라이언트에 복제되지 않습니다. 현재 라운드처럼 클라이언트와 공유할 상태는 GameState에 두고, 직접 만든 필드에는 복제 설정을 더합니다.
플레이어별 점수는 그 플레이어의 공유 상태를 담는 PlayerState에 두는데, PlayerState는 몸체인 Pawn과 분리되어 있어 Pawn이 다시 생성되어도 점수를 유지할 수 있습니다. 다만 재접속이나 레벨 이동 때 어떤 데이터를 보존할지, 직접 만든 필드를 복제할지는 따로 정해야 하는 정책입니다. 로컬 입력과 UI는 PlayerController에서 처리하되 로컬 플레이어의 처리와 서버 쪽 제어를 구분하고, 몸체와 개별 기능은 스폰되고 파괴되는 월드 대상의 수명에 맞춰 Pawn과 컴포넌트에서 관리합니다.
그림 아래의 주의처럼 오래 사는 객체가 월드 액터의 참조를 가지고 있어도 그 액터의 생존이 연장되지는 않습니다. 예를 들어 GameInstance가 레벨의 어떤 액터를 가리키고 있어도, 레벨이 바뀌면 그 액터는 사라집니다. 그래서 타이머와 델리게이트처럼 액터에 묶인 연결은 EndPlay 같은 알맞은 수명 시점에 해제하고, 나중에 쓸 참조는 유효한지 다시 확인합니다.
정리
- 게임 루프는 매 프레임 입력, 게임 로직, 물리와 AI, 렌더링, 네트워크 작업을 되풀이하며, 각 작업의 순서와 주기는 시스템 설정에 따라 다릅니다.
Tick은 게임 로직 단계에서 실행되며, 시간에 따른 변화는DeltaTime을 곱해 계산해야 프레임 수와 관계없이 같은 결과가 나옵니다.- 연속 계산은 Tick, 사건에 대한 반응은 이벤트, 일정 간격이나 지연 작업은 타이머를 먼저 검토하고, Tick은 필요한 곳에만 켭니다.
- GameInstance는 레벨을 넘어 유지되는 객체, GameMode는 서버의 규칙, GameState와 PlayerState는 공유할 상태, PlayerController는 입력과 조종, Pawn과 Character는 조종되는 몸체, 컴포넌트는 기능 단위입니다.
- 데이터는 필요한 수명과 공유 범위로 위치를 정하고, 오래 사는 객체의 참조가 월드 액터의 생존을 보장하지 않는다는 점을 기억합니다.
연습 문제
1. 세 가지 값의 위치와 갱신 방법
같은 게임 인스턴스에서 맵을 바꿔도 유지할 난이도 선택값, 서버가 정하고 여러 클라이언트가 읽을 현재 라운드, 소지품 수가 바뀔 때만 다시 그리는 로컬 UI가 있습니다. 앞의 두 값은 각각 어디에 둘까요? 마지막 UI의 갱신은 매 프레임 Tick과 상태 변경 이벤트 가운데 무엇부터 검토할까요?
해설 보기
난이도 선택값은 맵 전환을 넘어 유지되어야 하므로 GameInstance에 둡니다. 현재 라운드는 서버가 정하고 여러 클라이언트가 읽어야 하므로 GameState의 복제 경로에 두며, 직접 만든 라운드 필드에는 복제 선언과 등록 같은 설정도 해야 합니다.
소지품 UI는 수가 바뀔 때만 다시 그리면 되므로 상태 변경 이벤트부터 검토합니다. 값이 바뀌지 않는 프레임에도 매번 Tick으로 확인하는 것은 낭비이고, 나중에 숫자가 부드럽게 변하는 연출처럼 연속 보간이 필요해지면 그때 필요한 주기 계산을 따로 둡니다.
2. GameInstance에 넣으면 저장될까
난이도 선택값을 GameInstance에 넣었습니다. 이 값은 다른 클라이언트에게도 전달되고, 게임을 껐다 켜도 남아 있을까요?
해설 보기
둘 다 아닙니다. GameInstance는 그 실행 안에서 레벨 전환을 넘어 값을 유지할 뿐, 네트워크 복제나 디스크 저장을 자동으로 해 주지 않습니다. 다른 클라이언트와 공유하려면 GameState나 PlayerState 같은 복제 경로를 쓰고, 게임을 다시 켠 뒤에도 남기려면 7장에서 다룰 SaveGame 같은 저장 기능을 따로 구성해야 합니다.
3. 프레임에 따라 달라지는 이동
어떤 액터가 Tick에서 위치에 매번 5cm를 더합니다. 30fps 환경과 120fps 환경에서 2초 동안의 이동 거리는 각각 얼마이며, 어떻게 고쳐야 할까요?
해설 보기
2초 동안 30fps에서는 틱이 약 60번, 120fps에서는 약 240번 일어나므로, 이동 거리는 각각 약 5 × 60 = 300cm와 5 × 240 = 1,200cm가 되어 네 배 차이가 납니다.
원하는 속도가 초당 150cm라면 매 틱 150 × DeltaTime을 더하도록 고칩니다. 그러면 틱 수와 관계없이 전달된 경과 시간의 합이 2초일 때 두 환경 모두 150 × 2 = 300cm를 움직입니다.
틱의 실행 간격, 틱 그룹, 선행 의존성은 Epic의 Actor Ticking 문서에서, 게임플레이 클래스의 역할은 Gameplay Framework 문서에서 확인할 수 있습니다.
이것으로 2장을 마칩니다. 2장에서는 리플렉션 매크로와 지정자로 C++ 클래스를 엔진과 블루프린트에 공개하는 방법, UObject와 AActor의 차이와 생성, 수명 관리, C++ 클래스를 블루프린트로 확장하는 방법, 그리고 게임 루프와 주요 클래스가 맡는 일을 익혔습니다. 다음 장의 Pawn, Character, Controller 이해부터는 폰과 캐릭터, 컨트롤러를 직접 구현하고, 입력 처리와 컴포넌트, 애니메이션 블루프린트 연동으로 게임플레이를 만들어 갑니다.