게임 루프의 의도
게임 시간 진행을 유저 입력, 프로세서 속도와 디커플링하기 위함이다.
게임 시간 진행을 유저의 입력, 프로세서 속도와 디커플링한다는 뜻은
게임의 시간 흐름을 실제 하드웨어나 입력 이벤트와 직접 묶어두지 않고 독립적으로 관리한다는 의미이다.
1. 유저 입력과의 디커플링
디커플링이 되지 않았을 때의 문제 :
만약 게임 로직이 "키 입력이 들어올 때만 시간이나 상태가 변한다"면,
입력이 없을 때 게임 시간이 멈춰버리거나 불균형해진다.
디커플링으로 해결하는 방법 :
게임 시간은 입력과 무관하게 계속 흘러가야 한다.
입력은 단순히 행동을 트리거 할 뿐, 시간 자체의 흐름을 조절하지 않는다.
2. 프로세서 속도와의 디커플링
디커플링이 되지 않았을 때의 문제 :
옛날 방식처럼 "프레임 1회 = 1 tick"으로 시간을 진행하면,
CPU 성능이 빠른 PC에서 게임 속도가 너무 빠르게, 느린 PC에서는 느리게 돌아간다.
디커플링으로 해결하는 방법 :
CPU가 초딩 몇 프레임을 뽑든 관계없이, "실제 경과 시간 (delta time)"을 기반으로 게임 시간을 진행시킨다.
ex) postion += velocity * deltaTime;
즉, 게임 시간 흐름을 하드웨어나 외부 요인에 종속되지 않게 독립적으로 관리한다는 뜻은
유저 입력과 같은 이벤트를 시간과 독립적으로 진행시킨다.
이를 통해 게임 시간은 계속 독립적으로 흐르게 하는 것이다.
첫 번째, CPU 혹은 FPS가 달라도 동일한 게임 시간이 흐르게 만드는 것이다.
결과적으로 공정하고 예측 가능한 게임 플레이가 가능해진다.
두 번째, 게임 시간은 유저의 입력이나 CPU 속도 같은 외부 요인에 직접 연결되지 않도록 한다.
독립적으로 관리하여, 어떤 환경에서도 동일한 게임 경험을 제공해야 한다.
이를 게임 루프 패턴의 의도라고 볼수있다.
즉, 게임 루프 패턴은 게임 시간 진행을 유저 입력, 프로세서 속도와 디커플링하는 것이 목적이며,
가능하게 하는 방법이다.
게임 루프의 핵심 1 : 반복과 이벤트
게임 루프 패턴은 거의 모든 게임에서 사용하며, 어느 것도 서로 같지 않고,
게임이 아닌 분야에서는 그다지 쓰이지는 않는다.
게임 루프는 코드를 처리하는 방법을 게임이 종료가 될 때 까지 반복한다.
끊임 없이 돌아가는 것이 게임 루프의 첫 번째 핵심이자.
루프에서 사용자 입력을 처리하지만 마냥 기다리고 있지 않고
또한 이러한 반복 속에서 이전 호출 이후 들어오노 유저 입력을 처리한다.
즉각 적으로 처리하지 않고 Update의 순서를 적용하는 것이다.
게임 루프의 핵심 2 : 반복과 게임 월드에서의 시간
루프가 입력을 기다리지 않는다면 루프가 도는 데 시간이 얼마나 걸릴까?
게임 루프가 돌 때마다 게임 상태가 조금씩 진행 되는 것은 당연하지만, 아리송 할 것이다.
이를 알 수 있는 것 즉, 실제 시간 동안 게임 루프가 얼마나 많이 돌았는지 측정하는 것이 FPS이다.
게임 루프가 빠르게 돌면 FPS가 올라가면서 부드럽고 빠른 화면을 볼 수 있다.
게임루프가 느리면 스톱모션 영화처럼 뚝뚝 끊겨져 보인다.
프레임 레이트
프레임 레이트는한 프레임에 얼마나 많은 작업을 하는가를 측정하는 것이다.
물리 계산이 복잡하고 게임 객체가 많으며 그래픽이 정교해 CPU, GPU가 계속 바쁘다면 한 프레임에 걸리는 시간이 늘어난다.
프레임 레이트가 늘어나는 또 다른 요인은 코드가 실행되는 플랫폼의 속도이다.
하드웨어가 속도가 빠르다면 같은 시간에 더 많은 코드를 실행할 것이다.
멀티코어, GPU, 전용 오디오 하드웨어, OS 스케줄러 등도 한 틱에 걸리는 시간에 영향을 준다.
따라서,
게임 루프는 어떤 하드웨어서라도 일정한 속도로 실행하는 것이 게임 루프의 핵심 업무이다.
언제 쓸 것인가?
게임 루프는 게임 하는 내내 실행되어
한 번 돌 때마다 멈춤 없이 유저의 입력을 처리한 뒤
게임 상태를 업데이트하고 게임 화면을 렌더한다.
시간 흐름에 따라 게임 플레이 속도를 조절한다.
부적합한 패턴은 안 쓰는 것만 못하기 때문에 언제 쓸 것인가는 신중하게 생각해야 한다.
게임 엔진을 쓰고 있다면 직접 게임 엔진을 만들 필요는 없다.
거의 모든 게임에 게임 루프가 들어가야 한다.
게임 루프 패턴 주의 사항
게임 루프는 전체 게임 코드 중에서도 가장 핵심에 해당한다.
10% 코드가 프로그램 실행 시간의 90%를 차지한다고들 한다.
게임 루프는 그 10%에 들어가기 때문에 최적화를 고려해 깐깐하게 만들어야 한다.
주의 사항 1 : 플랫폼의 이벤트 루프에 맞추어야 할 수 있다.
그래픽 UI와 이벤트 루프가 들어 있는 OS나 플랫폼에서 게임을 만들 경우,
애플리케이션 루프가 두 개 있는 셈이므로 서로를 잘 맞춰야 한다.
이 말이 단 두 문장으로는 이해되지 않을 것이다.
왜 플랫폼 이벤트 루프를 고려해야할까?
GUI/입력/윈도우 관리는 OS나 브라우저의 이벤트가 주도한다.
게임이 자체 루프를 돌리면 루프가 2개가 되기 때문에 서로 막지 않게 잘 맞춰야 한다.
브라우저 같이 내부 루프가 절대적인 곳은 플랫폼 루프를 타야만 한다.
두 가지 접근
1) 제어권을 가져와 “내 게임 루프”만 돌리기 (Win32 예)
핵심: 블로킹 호출 금지. GetMessage()는 입력이 올 때까지 멈추므로 X
대신 PeekMessage()로 메시지를 있으면 처리/없으면 계속 진행.
// C++ Win32 — non-blocking message pump + 고정 시간 스텝
MSG msg{};
bool running = true;
LARGE_INTEGER freq, prev; QueryPerformanceFrequency(&freq); QueryPerformanceCounter(&prev);
double acc = 0.0;
const double dt = 1.0 / 60.0; // 고정 스텝 60Hz
while (running) {
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) { running = false; break; }
TranslateMessage(&msg);
DispatchMessage(&msg);
}
LARGE_INTEGER now; QueryPerformanceCounter(&now);
acc += double(now.QuadPart - prev.QuadPart) / double(freq.QuadPart);
prev = now;
while (acc >= dt) {
Update(dt); // 물리/게임 로직: 고정 스텝
acc -= dt;
}
Render(acc / dt); // 보간 렌더링(옵션)
}
2) 플랫폼 루프에 “맞춰 타기” (브라우저/모바일 등)
브라우저는 requestAnimationFrame(rAF)이 디스플레이 리프레시와 이벤트 루프에 맞춰 콜백을 호출.
게임 틱 여기서 수행.
// 브라우저/TS — rAF + 고정 스텝
let last = performance.now();
let acc = 0;
const dt = 1 / 60;
function frame(t: number) {
const delta = (t - last) / 1000;
last = t;
acc += Math.min(delta, 0.25); // 스파이크 클램프
while (acc >= dt) {
update(dt);
acc -= dt;
}
render(acc / dt);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
Cocos Creator/TypeScript
- Cocos는 엔진이 메인 루프와 deltaTime을 제공합니다.
- update(dt: number)에서 로직만 수행하고, 블로킹 호출/바쁜 대기를 넣지 않는다.
- 물리는 고정 스텝, 렌더는 프레임 기반으로 분리하고 싶다면, accumulator 패턴을 스크립트에 구현
const FIXED_DT = 1/60;
let acc = 0;
export class PlayerCtrl extends Component {
update(dt: number) {
acc += Math.min(dt, 0.25);
while (acc >= FIXED_DT) {
this.fixedUpdate(FIXED_DT); // 입력 처리→상태 갱신→충돌/물리
acc -= FIXED_DT;
}
this.render(acc / FIXED_DT); // 보간 렌더
}
}
Unity
타이밍/루프 기본기
- Update vs FixedUpdate 분리
- Update: 입력 샘플링, 비물리 로직 (Time.deltaTime)
- FixedUpdate: 물리/힘 가하기 (Time.fixedDeltaTime)
- Rigidbody 이동은 FixedUpdate에서 AddForce/MovePosition + Rigidbody.interpolation = Interpolate로 보간.
- 타임스케일 제어
- 일시정지: Time.timeScale = 0 (물리/애니메이션도 멈춤). UI 타이머/쿨타임은 Time.unscaledDeltaTime로 별도 관리.
- 프레임 스파이크 흡수
- 누적기(Accumulator) 패턴으로 고정 스텝 유지, delta는 Mathf.Min(delta, 0.25f) 같은 클램프
float acc;
const float DT = 1f / 60f;
void Update() {
float dt = Mathf.Min(Time.deltaTime, 0.25f);
acc += dt;
while (acc >= DT) { StepFixed(DT); acc -= DT; }
Render(acc / DT); // 보간
}
이벤트 루프/플랫폼 적응
- VSync/타겟 프레임: 전력/발열 관리용
- PC: QualitySettings.vSyncCount 또는 Application.targetFrameRate = 60/120.
- 모바일: 디바이스별 지원 프레임 확인(90/120Hz) → 옵션 제공.
- 백그라운드/포커스
- OnApplicationPause/Focus로 일시정지/재개. rAF처럼 백그라운드 스로틀 고려.
- 새 Input System
- 입력은 이벤트 기반(Action) + Update에서 샘플링 혼합.
- 입력 자체가 타임링을 결정하지 않게 하고, 로직은 dt로 진행.
성능/스레딩
- 메인 스레드 블로킹 금지: 파일/네트워크는 async/await + UnityWebRequest, 무거운 연산은 Jobs + Burst로 이동.
- 결과만 메인 스레드에서 적용.
- GC/메모리: new 최소화, List.Clear() 재사용, foreach 박싱 주의, string 결합은 StringBuilder.
- Physics 설정: Fixed Timestep(Project Settings)과 Maximum Allowed Timestep으로 물리 안정성 확보.
네트워크/동기화
- 서버 틱(고정) 기준으로 로직 진행, 클라 렌더는 보간.
- NetworkTime(엔진/프레임워크별) 사용, Time.realtimeSinceStartup로 참조 시간 가져와 드리프트 모니터링.
Unreal Engine
타이밍/루프 기본기
- 엔진 루프를 탑승: UE는 자체 메인 루프가 있으니 직접 루프를 만들지 않습니다.
- Tick 설계
- AActor::Tick(float DeltaSeconds)에서 비물리 로직.
- 물리는 고정 스텝 성격 → Substepping 사용 권장(특히 빠른 물체/불안정 접촉).
- Project Settings > Physics > Framerate Dependent Physics Timestep 끄고
Use Substepping 켜기, Max Substep Delta Time / Max Substeps 조정.
- Project Settings > Physics > Framerate Dependent Physics Timestep 끄고
- 타이머/시간 소스
- 반복/지연은 FTimerManager 사용(월드 시간 기반, 타임스케일/일시정지 영향 옵션).
- 전역 델타는 UGameplayStatics::GetWorldDeltaSeconds 또는 GetWorld()->GetDeltaSeconds().
// Accumulator 패턴 (Actor)
float Acc = 0.f;
constexpr float Dt = 1.f/60.f;
void AMyActor::Tick(float DeltaSeconds) {
Super::Tick(DeltaSeconds);
Acc = FMath::Min(Acc + DeltaSeconds, 0.25f);
while (Acc >= Dt) { StepFixed(Dt); Acc -= Dt; }
Render(Acc / Dt); // 보간
}
이벤트 루프/플랫폼 적응
- VSync/프레임 레이트
- 콘솔/PC: r.VSync, t.MaxFPS, rhi.SyncInterval 조합.
- 프로젝트 세팅에서 플랫폼별 프로파일링(전력/발열 고려).
- 일시정지/포커스
- UGameplayStatics::SetGamePaused, FCoreDelegates::ApplicationHasReactivatedDelegate 등으로 포커스 이벤트 처리.
- UI(UMG) 타이머는 Real Time(언스케일드) 기반으로 별도 운용 가능.
- 입력(Enhanced Input)
- 액션/매핑 컨텍스트로 이벤트 기반 처리. 입력은 트리거일 뿐, 게임 시간은 DeltaSeconds로 독립.
스레딩/비동기
- 게임 스레드 블로킹 금지
- 파일/압축/경로탐색 등 무거운 연산은 Async(EAsyncExecution::ThreadPool, ...)로 오프로딩 → 결과는 AsyncTask(ENamedThreads::GameThread, ...)로 되돌려 UI/월드 변경.
- 타이머/딜레이
- FTimerHandle 기반 반복 작업으로 틱과 분리. Latent Action(블루프린트 Delay)는 게임 시간 의존 → 필요 시 RealTime 기반 커스텀 사용.
네트워크/동기화
- 서버 권위 + 고정 틱
- 서버에서 상태를 고정 스텝으로 갱신, 클라이언트는 보간/예측.
- NetUpdateFrequency, MinNetUpdateFrequency, bReplicateMovement 튜닝.
- 시간 동기: 서버 시간을 레퍼런스로 삼고 클라 드리프트 보정.
렌더/애니 파이프라인 정렬
- 카메라/본 보간은 TickGroup 활용:
- 물리(PrePhysics) → 로직(DuringPhysics/PostPhysics) → 카메라(Late) 순으로 의도한 그룹에 배치하여 프레임 지터 최소화.
- Substepping + Interpolation으로 초당 프레임 변동에도 움직임 일정성 유지.
공통 체크리스트 (Unity & UE)
- 시간 독립성: 입력/프레임과 분리, 모든 시스템은 dt 또는 고정 스텝 기준.
- 보간(Render Interpolation): 시뮬레이션은 고정, 렌더는 현재와 다음 상태 사이를 보간.
- 백그라운드 정책: 포커스 변경/탭 전환 시 일시정지 및 타이머(언스케일드) 분리.
- 스파이크 방어: delta 클램프, 누적기 상한, 물리 설정(서브스텝/최대 틱).
- 비동기화: IO/경로탐색/네트워크는 스레드/잡 시스템으로, 결과만 메인 스레드 반영.
- 프로파일링 우선: 프레임 타임(총/CPU/GPU)과 GC, 물리 스텝, 드로우콜, 밴치 대역폭 추적.
'컴퓨터 프로그래밍 공부 > 디자인 패턴' 카테고리의 다른 글
| 이중 패턴 (3) | 2025.08.10 |
|---|---|
| 프로토 타입 (9) | 2025.08.03 |
| 게임 프로그래밍 패턴 1장 정리 (3) | 2025.07.20 |