웹 빌드에서 조작이 망가진다는 제보가 여러 갈래로 들어왔습니다. "화면이 갑자기 뒤집히면서 컨트롤이 잘 안 된다", "마우스가 자꾸 화면 밖으로 나가서 컨트롤 미스가 남", 그리고 저 혼자 한쪽으로 걸어가는 증상.
원인은 각각 달랐는데, 뒤에 깔린 실수는 같았습니다. 브라우저에서 넘어온 값을 신뢰 경계 밖에서 온 입력으로 다루지 않았습니다.
605도짜리 마우스 이동
브라우저의 movementX/Y는 포인터 잠금이 걸리는 순간 "예전 커서 자리에서 잠금 지점까지"를 한 번에 실어 보내기도 하고, 그냥 수천 픽셀짜리 스파이크를 뱉기도 합니다. Godot 웹은 그 값을 그대로 넘겨주는데, 시점 갱신 코드에 상한이 하나도 없었습니다.
기본 감도가 0.0022 rad/px입니다. 세로 4800px짜리 값 하나면 605도입니다. 시점 상한인 88도까지 튀어서 하늘을 보고 굳고, 하늘에서는 조준이 안 되니 "컨트롤이 안 된다"가 됩니다.
두 겹으로 막았습니다.
하나. 이벤트 하나가 350px을 넘으면 버립니다. 기본 감도에서 44도라 사람이 낼 수 있는 값이 아닙니다. 빠른 플릭도 여러 이벤트로 쪼개져 들어오기 때문입니다. 잘라서 쓰지 않고 버리는 이유는, 잘라도 44도가 튀는 건 마찬가지라서입니다.
둘. 잠금이 걸린 뒤 첫 이동 이벤트를 버립니다. 브라우저가 커서 이동분을 한꺼번에 실어 보내는 자리가 정확히 거기입니다. 화면을 클릭해 다시 잠글 때 시야가 홱 도는 것도 같은 원인이었습니다.
검사는 "스파이크를 버렸는가"만 보면 안 됩니다. 상한을 아주 낮게 잡아 정상 조작까지 전부 버려도 그 검사는 통과합니다. 그래서 평범한 이동이 통과하는지와, 한 번 버린 뒤에도 마우스가 계속 먹는지를 같이 봅니다.
[smoke] 시야 스파이크: 평범한 이동 통과 true (5.04°) · 스파이크 버림 true (버린 수 1)
[smoke] 스파이크 뒤 시야 변화 0.0000° · 그 뒤 정상 조작 5.04° · 상한 88°
엔진은 "잠갔다"고 답하는데 안 잠겨 있었습니다
Input.mouse_mode는 요청한 상태를 돌려줍니다. 실제 상태가 아닙니다.
웹에서 포인터 잠금은 브라우저가 거절할 수 있습니다. Esc로 푼 직후에는 잠깐 재요청이 막히고, 사용자 제스처 없이 요청하면 무시됩니다. 그런데도 엔진은 여전히 "잠금 상태"라고 답합니다.
그러면 게임은 조작을 받고 있다고 믿고, 커서는 페이지 위를 돌아다닙니다. 더 나쁜 건 다시 잠글 기회가 영영 안 온다는 것입니다. 재요청 조건이 "잠금 상태가 아니면" 하나였는데, 엔진이 계속 잠금 상태라고 말하니 그 조건은 영원히 거짓입니다.
이제 브라우저에 직접 물어봅니다(document.pointerLockElement). 0.15초에 한 번이라 비싸지 않습니다.
한 가지는 일부러 느슨하게 뒀습니다. 브라우저에 묻는 데 실패하면 "잠겨 있다"로 봅니다. 평소라면 모를 때는 막는 쪽을 고르는데, 여기서는 반대가 맞습니다. 잘못 막으면 웹에서 조작이 통째로 안 되고, 잘못 통과시키면 고치기 전 상태로 돌아갈 뿐입니다. 치르는 대가의 크기가 다릅니다.
키가 눌린 채 굳었습니다
노트북 키보드에서 나던 증상입니다. 처음엔 터치 조이스틱 문제로 단정하고 그쪽만 고쳤는데, 터치 UI가 꺼져 있으면 터치 입력은 0에서 움직이지 않습니다. 키보드 쪽이 굳는 것이었습니다.
앞선 수정들은 전부 "창이 뒤로 넘어간다는 알림이 온다"는 가정에 기대고 있었습니다. 웹에서는 그 알림이 안 오는 경우가 있습니다. 가정이 필요 없는 방법을 하나 더 뒀습니다. 매 프레임 조종 중인지 확인하고, 아니면 그 틱의 입력을 안 받고 액션을 계속 놓습니다. 알림이 오든 안 오든, 돌아온 순간에는 깨끗합니다.
저 혼자 오른쪽으로 걸어갔습니다
단서는 "반대 방향으로는 못 움직인다"였습니다. 입력이 무시되는 게 아니라 더해지고 있다는 뜻입니다. 이동값이 키보드와 터치의 합이라, 터치 쪽이 (1,0)에 굳으면 왼쪽 키는 상쇄만 하고 떼는 순간 다시 오른쪽으로 갑니다.
조이스틱은 손가락 자리가 비어 있을 때만 잡힙니다. 손가락이 낀 채로 남으면 새 터치가 영영 되찾지 못합니다. 해제 이벤트는 캔버스 밖에서 손을 떼거나, 터치가 취소되거나, 탭이 넘어가면 안 옵니다. 키보드도 같습니다. 누른 채 알트탭하면 떼는 이벤트가 안 옵니다.
빠져나올 길을 세 군데 뒀습니다. 같은 번호로 새 터치가 오면 낀 것으로 보고 정리하고, 창이 뒤로 넘어가면 전부 놓고, 일시정지를 여닫으면 초기화합니다. 한 군데만 두면 그게 안 불릴 때 탈출구가 없습니다.
남는 것
넷 다 "브라우저가 알려 준 값"이거나 "브라우저가 알려 줄 거라 믿은 알림"이었습니다. 포인터 잠금이 뱉는 이동량은 외부 입력인데, 검사 없이 각도에 곱하고 있었습니다.
그리고 이 경로는 웹에서만 돌아서 헤드리스로는 검증이 안 됩니다. 데스크톱에서는 항상 통과하는 분기라, 스모크로는 "동작이 그대로인 것"까지만 확인했습니다. 실제 확인은 브라우저에서 해야 합니다.