Alt+Tab 이후 마우스 캡처가 풀리는 PC 게임 문제: 커서 잠금 상태를 안정적으로 복원하는 패턴
PC 게임에서 Alt+Tab으로 포커스를 잃었다가 돌아올 때 마우스 커서 잠금과 입력 모드를 안정적으로 복원하는 방법을 OS, Unity, Unreal Engine 관점에서 정리합니다.

핵심 요약
Alt+Tab으로 포커스를 잃으면 게임이 사용하던 마우스 캡처가 해제되고 커서 클리핑이나 엔진의 입력 모드도 플랫폼에 따라 초기화되거나 무효화될 수 있습니다. 이때 엔진이 기억하는 “게임플레이 중에는 커서를 잠근다”는 의도와 실제 OS 상태가 어긋날 수 있습니다.
해결책은 포커스 복귀 이벤트에서 무조건 커서를 잠그는 것이 아닙니다. 게임이 관리하는 DesiredState를 기준으로 현재 UI 상태를 다시 확인하고 포커스 복귀 직후 한 프레임 정도 지난 뒤 커서 잠금과 입력 모드를 재적용합니다. 메뉴가 열려 있다면 커서를 잠그지 않아야 하며 게임플레이 상태라면 플랫폼별 입력 모드도 함께 복원해야 합니다.
Alt+Tab 시 마우스 캡처는 왜 풀리는가?
FPS, TPS, 3D 액션 게임은 보통 다음 두 동작을 조합합니다.
- 커서를 창이나 뷰포트 안에 가두는 클리핑(clipping)
- 특정 창으로 마우스 메시지를 보내는 캡처(capture)
Windows의 ClipCursor(&rect)는 커서의 화면상 이동 범위를 제한하고 SetCapture(hwnd)는 창이 마우스 입력을 캡처하도록 요청합니다. 두 API는 서로 다른 기능입니다. 특히 SetCapture는 포커스가 다른 창으로 이동하거나 사용자가 다른 창을 클릭하면 유지되지 않을 수 있습니다.
ClipCursor는 공유 리소스이므로 애플리케이션이 더 이상 제어권을 갖지 않을 때 ClipCursor(NULL)로 직접 해제하는 책임이 있습니다. 포커스 상실 시 클리핑이 해제되는 시점은 Windows, 창 모드, 엔진 구현에 따라 다를 수 있으므로 “OS가 항상 자동으로 해제한다”고 단정하면 안 됩니다.
| 목적 | Win32 예시 | Unity | Unreal Engine | 주의할 점 |
|---|---|---|---|---|
| 창 안에 가두기 | ClipCursor(&RECT) | CursorLockMode.Confined | EMouseLockMode::LockAlways 등 | 중앙 고정과는 다른 기능 |
| 뷰포트 중앙에 잠그기 | 엔진 또는 상대 입력 구현 | CursorLockMode.Locked | 입력 모드·뷰포트 설정 조합 | API 대응이 일대일은 아님 |
| 마우스 메시지 캡처 | SetCapture(HWND) | 엔진이 내부 처리 | EMouseCaptureMode | 포커스 전환 시 다시 설정될 수 있음 |
CursorLockMode.Confined는 창 안에 커서를 가두는 모드이고 Locked는 Game view 중앙에 커서를 잠그는 모드입니다. Unreal Engine의 EMouseLockMode와 EMouseCaptureMode도 각각 잠금 정책과 캡처 정책을 나타내므로 하나의 값으로 두 동작을 대체할 수 없습니다.

커서 잠금 복원이 실패하는 주요 원인
1. 포커스 이벤트와 뷰포트 준비 시점의 차이
WM_SETFOCUS나 WM_ACTIVATEAPP, Unity의 OnApplicationFocus(true)는 포커스가 돌아왔다는 사실을 알려주는 이벤트이지 모든 렌더링·입력 하위 시스템이 같은 순간에 준비되었다는 보장은 아닙니다. 콜백 안에서 잠금 요청을 즉시 실행하면 엔진이나 플랫폼 플러그인의 후속 처리에 의해 덮어써질 수 있습니다. 그래서 보통 다음 프레임으로 작업을 미루고 필요하면 실제 빌드에서 한 프레임과 두 프레임 중 어느 쪽이 안정적인지 검증합니다.
2. UI 상태와 게임플레이 상태의 불일치
일시정지 메뉴나 인벤토리를 열어 커서를 보여줘야 하는데 포커스 복귀 코드가 무조건 Locked를 적용하면 UI를 조작할 수 없습니다. 반대로 게임플레이 상태인데 UI가 남긴 None 상태가 다시 적용되면 커서가 보이거나 뷰포트 밖으로 이동할 수 있습니다. 따라서 포커스 이벤트와 UI 이벤트가 각각 커서 API를 직접 호출하지 말고 하나의 상태 관리자를 통해 의도된 상태를 갱신해야 합니다.
3. 멀티 모니터와 상대 입력의 혼동
멀티 모니터 환경에서 커서가 다른 모니터로 이동하는 문제는 클리핑이 풀렸거나 창 경계 계산이 잘못된 경우가 많습니다. 반면 빠른 카메라 회전은 화면 좌표가 아니라 마우스 이동 델타를 처리하는 문제입니다. Raw Input(WM_INPUT)은 원시 델타를 받을 수 있게 해주지만 그것만으로 화면 커서를 잠그거나 포커스를 복원해 주지는 않습니다. 상대 입력을 사용할 때도 포커스·커서 잠금·캡처 정책을 별도로 관리해야 합니다.
커서 상태 머신과 포커스 라이프사이클
포커스 이벤트는 “현재 포커스를 얻었는가”를 나타내고 커서 상태는 “포커스를 얻었을 때 무엇을 해야 하는가”를 나타냅니다. 두 정보를 분리하면 포커스 복귀 시 메뉴가 열린 상태를 덮어쓰는 실수를 줄일 수 있습니다.
flowchart TD
A[게임플레이: 잠금 상태] -->|Alt+Tab| B[포커스 상실]
B --> C[엔진/플랫폼이 캡처·클리핑 해제]
C --> D[DesiredState 보존]
D -->|포커스 복귀| E[포커스 획득]
E --> F{UI가 열려 있는가?}
F -->|예| G[커서 표시·잠금 해제·UI 입력]
F -->|아니오| H[다음 프레임에 입력 모드 재적용]
H --> I[커서 잠금·숨김·캡처 정책 복원]
I --> A
안정적으로 복원하는 3단계 패턴
Step 1: 의도된 커서 상태를 한 곳에서 관리하기
Cursor.lockState나 Unreal Engine의 입력 모드 함수를 코드 곳곳에서 직접 호출하지 말고 CursorManager 같은 단일 진입점을 둡니다. 이 관리자는 현재 컨텍스트(게임플레이 또는 메뉴 UI), 포커스 여부, 포커스 복귀 후 적용할 DesiredState를 구분해 보관해야 합니다.
커서의 실제 상태를 매 프레임 억지로 덮어쓰기보다 상태가 바뀌거나 포커스가 복귀할 때 필요한 플랫폼 API를 다시 적용하는 편이 안전합니다.
Step 2: Unity에서 포커스 복귀를 한 프레임 미루기
Unity의 OnApplicationFocus(true)는 포커스 복귀를 알리는 지점입니다. 아래 예제는 포커스를 잃은 동안에는 상태를 바꾸지 않고 복귀 후 yield return null로 다음 프레임까지 기다린 다음 현재 컨텍스트를 다시 평가합니다. 프로젝트나 특정 창 모드에서 한 프레임으로 부족하다면 두 번째 프레임 대기를 추가하되 문서와 코드의 대기 프레임 수를 일치시키고 실제 빌드에서 검증해야 합니다.
using System.Collections;
using UnityEngine;
public sealed class CursorLockController : MonoBehaviour
{
public enum CursorContext { Gameplay, MenuUI }
private CursorContext _context = CursorContext.Gameplay;
private Coroutine _restoreCoroutine;
private bool _hasFocus;
private void OnEnable() => _hasFocus = Application.isFocused;
public void SetContext(CursorContext context)
{
_context = context;
if (_hasFocus) ApplyCursorState();
}
private void OnApplicationFocus(bool hasFocus)
{
_hasFocus = hasFocus;
if (!hasFocus)
{
if (_restoreCoroutine != null) StopCoroutine(_restoreCoroutine);
_restoreCoroutine = null;
return;
}
if (_restoreCoroutine != null) StopCoroutine(_restoreCoroutine);
_restoreCoroutine = StartCoroutine(RestoreAfterFocus());
}
private IEnumerator RestoreAfterFocus()
{
// 포커스 복귀와 뷰포트 입력 상태가 반영된 다음 적용한다.
yield return null;
if (_hasFocus) ApplyCursorState();
_restoreCoroutine = null;
}
private void ApplyCursorState()
{
switch (_context)
{
case CursorContext.Gameplay:
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
break;
case CursorContext.MenuUI:
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
break;
}
}
}
Unity 문서도 커서 상태는 운영체제나 Unity에 의해 변경될 수 있으므로 포커스 복귀나 UI 상태 변경 시 다시 확인하라고 안내합니다. Locked는 커서를 숨기는 동작이 포함되지만 Cursor.visible을 함께 설정하면 의도가 코드에 명확하게 드러납니다.
Step 3: Unreal Engine에서 입력 모드와 뷰포트 정책을 함께 재적용하기
Unreal Engine에서는 APlayerController의 입력 모드와 UGameViewportClient의 마우스 잠금·캡처 정책을 일관되게 유지해야 합니다. 아래 코드는 개념을 보여주는 예제이며 OnWindowFocusChanged, bIsMenuOpen, RestoreCursorLockState는 프로젝트에서 연결해야 하는 사용자 정의 코드입니다.
#include "Engine/GameViewportClient.h"
#include "GameFramework/PlayerController.h"
void AMyPlayerController::OnWindowFocusChanged(bool bIsFocused)
{
if (!bIsFocused || !GetWorld()) return;
// Slate와 뷰포트의 포커스 처리가 반영된 다음 틱에 재적용한다.
GetWorld()->GetTimerManager().SetTimerForNextTick(
this, &AMyPlayerController::RestoreCursorLockState);
}
void AMyPlayerController::RestoreCursorLockState()
{
if (!GetWorld()) return;
if (bIsMenuOpen)
{
FInputModeGameAndUI InputMode;
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
InputMode.SetHideCursorDuringCapture(false);
SetInputMode(InputMode);
bShowMouseCursor = true;
return;
}
FInputModeGameOnly InputMode;
InputMode.SetConsumeCaptureMouseDown(true);
SetInputMode(InputMode);
bShowMouseCursor = false;
if (UGameViewportClient* ViewportClient = GetWorld()->GetGameViewport())
{
ViewportClient->SetMouseLockMode(EMouseLockMode::LockAlways);
ViewportClient->SetMouseCaptureMode(
EMouseCaptureMode::CapturePermanently_IncludingInitialMouseDown);
}
}
SetMouseLockMode는 뷰포트가 클릭되었을 때의 잠금 정책을 설정하는 함수입니다. 따라서 이 설정만으로 모든 환경에서 즉시 커서가 중앙에 잠긴다고 가정하지 말고 실제 프로젝트의 입력 모드·창 모드·첫 클릭 정책과 함께 테스트해야 합니다. 메뉴에서 게임플레이로 전환하는 순간에도 같은 복원 함수를 호출하는 것이 좋습니다.
자주 묻는 질문
Q1. 포커스가 돌아오자마자 커서를 잠그면 왜 간헐적으로 무시되나요?
포커스 이벤트는 복귀 사실을 알릴 뿐, 엔진의 뷰포트와 플랫폼 입력 상태가 최종적으로 적용된 시점을 보장하지 않습니다. 같은 프레임의 후속 처리에서 입력 모드가 바뀌거나 잠금 요청이 덮어써질 수 있으므로 다음 프레임에 현재 상태를 다시 평가해 적용하는 방법이 유효합니다. 다만 모든 환경에서 동일한 지연이 필요한 것은 아니므로 대상 플랫폼과 창 모드별로 확인해야 합니다.
Q2. 듀얼 모니터에서 커서가 옆 모니터로 탈출합니다.
먼저 포커스가 있는 동안 실제 클리핑 또는 엔진의 Locked 상태가 유지되는지 확인해야 합니다. 카메라 회전 입력이 필요한 게임이라면 Raw Input 같은 상대 입력을 별도로 검토할 수 있지만 상대 입력은 커서 위치 제한의 대체물이 아닙니다. 포커스 복귀 시 클리핑·캡처·입력 모드를 각각 복원하고 창 좌표와 DPI 스케일링을 포함한 경계 계산을 검증하세요.
Q3. Borderless Windowed와 Exclusive Fullscreen에서 복원 방식이 다른가요?
다를 수 있습니다. Borderless Windowed는 데스크톱 창으로 동작하므로 포커스, 캡처, 클리핑과 창 크기·DPI 변화를 중심으로 확인합니다. Exclusive Fullscreen은 그래픽 API와 드라이버, 엔진 버전에 따라 디바이스나 스왑체인 재생성 같은 추가 처리가 발생할 수 있습니다. 따라서 “Exclusive Fullscreen이면 항상 디바이스 리셋이 발생한다”거나 “Borderless는 포커스 이벤트만 처리하면 된다”고 일반화하지 말고 엔진의 디바이스 로스트·리소스 복원 경로까지 함께 테스트해야 합니다.
프로덕션 적용 체크리스트
- 포커스 상실·복원과 UI 전환에서 커서 API를 직접 호출하지 않고
CursorManager를 경유하는가? - 포커스 복귀 시 현재 UI 상태를 다시 확인한 뒤 커서 잠금과 입력 모드를 적용하는가?
- 지연 프레임 수가 문서와 코드에서 일치하며 실제 빌드의 창 모드별로 검증되었는가?
- 클리핑, 중앙 잠금, 마우스 캡처, 상대 입력을 서로 다른 책임으로 구분했는가?
- 듀얼 모니터·고DPI·Borderless·Exclusive Fullscreen에서 Alt+Tab 복귀를 테스트했는가?



