포인터 잠금 및 1인칭 슈팅 게임 컨트롤

John McCutchan
John McCutchan

소개

Pointer Lock API는 브라우저 게임에서 FPS 컨트롤을 올바르게 구현하는 데 도움이 됩니다. 상대 마우스 움직임이 없으면 플레이어의 커서가 화면의 오른쪽 가장자리에 닿을 수 있고, 그 이후의 오른쪽 움직임은 무시됩니다. 즉, 뷰가 계속 오른쪽으로 화면 이동하지 않고 플레이어가 악당을 추적하여 기관총으로 공격할 수 없습니다. 플레이어가 죽고 짜증이 날 것입니다. 포인터 잠금을 사용하면 이러한 최적화되지 않은 동작이 발생하지 않습니다.

Pointer Lock API를 사용하면 애플리케이션에서 다음 작업을 할 수 있습니다.

  • 상대 마우스 움직임을 포함한 원시 마우스 데이터에 액세스
  • 모든 마우스 이벤트를 특정 요소로 라우팅

포인터 잠금을 사용 설정하면 마우스 커서가 숨겨지므로 원하는 경우 애플리케이션별 포인터를 그리거나 사용자가 마우스로 프레임을 이동할 수 있도록 마우스 포인터를 숨겨 둘 수 있습니다. 상대 마우스 움직임은 절대 위치와 관계없이 이전 프레임에서 마우스 포인터 위치의 델타입니다. 예를 들어 마우스 포인터가 (640, 480)에서 (520, 490)으로 이동한 경우 상대 이동은 (-120, 10)입니다. 원시 마우스 위치 변화를 보여주는 대화형 예는 아래를 참고하세요.

이 튜토리얼에서는 포인터 잠금 이벤트를 활성화하고 처리하는 방법과 1인칭 슈팅 게임 컨트롤 스킴을 구현하는 방법이라는 두 가지 주제를 다룹니다. 그렇습니다. 이 도움말을 읽고 나면 포인터 잠금을 사용하고 나만의 브라우저 게임에 Quake 스타일 컨트롤을 구현하는 방법을 알게 됩니다.

브라우저 호환성

브라우저 지원

  • Chrome: 37.
  • Edge: 13.
  • Firefox: 50
  • Safari: 10.1

소스

포인터 잠금 메커니즘

기능 감지

사용자의 브라우저가 포인터 잠금을 지원하는지 확인하려면 문서 객체에서 pointerLockElement 또는 공급업체 접두사가 추가된 버전을 확인해야 합니다. 코드:

var havePointerLock = 'pointerLockElement' in document ||
    'mozPointerLockElement' in document ||
    'webkitPointerLockElement' in document;

현재 포인터 잠금은 Firefox와 Chrome에서만 사용할 수 있습니다. Opera와 IE는 아직 지원하지 않습니다.

활성화 중

포인터 잠금을 활성화하는 방법은 두 단계로 이루어집니다. 먼저 애플리케이션이 특정 요소에 포인터 잠금을 사용 설정하도록 요청하고, 사용자가 권한을 부여한 직후에 pointerlockchange 이벤트가 발생합니다. 사용자는 언제든지 Esc 키를 눌러 포인터 잠금을 취소할 수 있습니다. 애플리케이션은 프로그래매틱 방식으로 포인터 잠금을 종료할 수도 있습니다. 포인터 잠금이 취소되면 pointerlockchange 이벤트가 실행됩니다.

element.requestPointerLock = element.requestPointerLock ||
                 element.mozRequestPointerLock ||
                 element.webkitRequestPointerLock;
// Ask the browser to lock the pointer
element.requestPointerLock();

// Ask the browser to release the pointer
document.exitPointerLock = document.exitPointerLock ||
               document.mozExitPointerLock ||
               document.webkitExitPointerLock;
document.exitPointerLock();

위의 코드만 있으면 됩니다. 브라우저가 포인터를 잠그면 사용자에게 애플리케이션에서 포인터를 잠갔음을 알리고 'Esc' 키를 눌러 취소할 수 있다고 안내하는 도움말이 팝업으로 표시됩니다.

Chrome의 포인터 잠금 정보 표시줄
Chrome의 포인터 잠금 정보 표시줄

이벤트 처리

애플리케이션에서 리스너를 추가해야 하는 이벤트는 두 가지가 있습니다. 첫 번째는 포인터 잠금 상태가 변경될 때마다 실행되는 pointerlockchange입니다. 두 번째는 마우스가 움직일 때마다 실행되는 mousemove입니다.

// Hook pointer lock state change events
document.addEventListener('pointerlockchange', changeCallback, false);
document.addEventListener('mozpointerlockchange', changeCallback, false);
document.addEventListener('webkitpointerlockchange', changeCallback, false);

// Hook mouse move events
document.addEventListener("mousemove", this.moveCallback, false);

pointerlockchange 콜백 내에서 포인터가 잠겼는지 또는 잠금 해제되었는지 확인해야 합니다. 포인터 잠금이 사용 설정되었는지 확인하는 것은 간단합니다. document.pointerLockElement가 포인터 잠금이 요청된 요소와 같은지 확인합니다. true이면 애플리케이션에서 포인터를 성공적으로 잠근 것이고 false이면 사용자 또는 자체 코드에서 포인터를 잠금 해제한 것입니다.

if (document.pointerLockElement === requestedElement ||
  document.mozPointerLockElement === requestedElement ||
  document.webkitPointerLockElement === requestedElement) {
  // Pointer was just locked
  // Enable the mousemove listener
  document.addEventListener("mousemove", this.moveCallback, false);
} else {
  // Pointer was just unlocked
  // Disable the mousemove listener
  document.removeEventListener("mousemove", this.moveCallback, false);
  this.unlockHook(this.element);
}

포인터 잠금이 사용 설정되면 clientX, clientY, screenX, screenY가 일정하게 유지됩니다. movementXmovementY는 마지막 이벤트가 전송된 이후 포인터가 이동한 픽셀 수로 업데이트됩니다. 의사 코드는 다음과 같습니다.

event.movementX = currentCursorPositionX - previousCursorPositionX;
event.movementY = currentCursorPositionY - previousCursorPositionY;

mousemove 콜백 내에서 상대 마우스 모션 데이터는 이벤트의 movementXmovementY 필드에서 추출할 수 있습니다.

function moveCallback(e) {
  var movementX = e.movementX ||
      e.mozMovementX          ||
      e.webkitMovementX       ||
      0,
  movementY = e.movementY ||
      e.mozMovementY      ||
      e.webkitMovementY   ||
      0;
}

오류 포착

포인터 잠금을 시작하거나 종료할 때 오류가 발생하면 pointerlockerror 이벤트가 실행됩니다. 이 이벤트에 연결된 데이터가 없습니다.

document.addEventListener('pointerlockerror', errorCallback, false);
document.addEventListener('mozpointerlockerror', errorCallback, false);
document.addEventListener('webkitpointerlockerror', errorCallback, false);

전체 화면이 필요한가요?

원래 포인터 잠금은 FullScreen API에 연결되었습니다. 즉, 포인터를 요소에 고정하려면 요소가 전체 화면 모드여야 합니다. 더 이상 그렇지 않으며 포인터 잠금은 애플리케이션의 모든 요소에 전체 화면이든 아니든 관계없이 사용할 수 있습니다.

FPS 컨트롤 예

이제 포인터 잠금을 사용 설정하고 이벤트를 수신했으므로 실제 예시를 살펴보겠습니다. Quake의 컨트롤이 어떻게 작동하는지 궁금하신 적이 있나요? 이제 코드로 설명해 드리겠습니다.

1인칭 슈팅 게임 컨트롤은 다음 네 가지 핵심 메커니즘을 중심으로 구축됩니다.

  • 현재 조명 벡터를 따라 앞뒤로 이동
  • 현재 화면이 좌우로 이동하는 벡터를 따라 좌우로 이동
  • 뷰 요 (좌우) 회전
  • 뷰 피치 회전 (위아래)

이 제어 스키마를 구현하는 게임에는 카메라 위치, 카메라 보기 벡터, 상수 위 벡터라는 세 가지 데이터만 있으면 됩니다. 위 벡터는 항상 (0, 1, 0)입니다. 위의 네 가지 메커니즘은 모두 카메라 위치와 카메라 보기 벡터를 서로 다른 방식으로 조작합니다.

움직임

먼저 움직임이 있습니다. 아래 데모에서는 이동이 표준 W, A, S, D 키에 매핑됩니다. W 및 S 키를 사용해 카메라를 앞뒤로 이동합니다. A 및 D 키는 카메라를 왼쪽과 오른쪽으로 이동합니다. 카메라를 앞뒤로 이동하는 방법은 간단합니다.

// Forward direction
var forwardDirection = vec3.create(cameraLookVector);
// Speed
var forwardSpeed = dt * cameraSpeed;
// Forward or backward depending on keys held
var forwardScale = 0.0;
forwardScale += keyState.W ? 1.0 : 0.0;
forwardScale -= keyState.S ? 1.0 : 0.0;
// Scale movement
vec3.scale(forwardDirection, forwardScale * forwardSpeed);
// Add scaled movement to camera position
vec3.add(cameraPosition, forwardDirection);

좌우로 이동하려면 이동 방향이 필요합니다. 횡 이동 방향은 교차곱을 사용하여 계산할 수 있습니다.

// Strafe direction
var strafeDirection = vec3.create();
vec3.cross(cameraLookVector, cameraUpVector, strafeDirection);

좌우 이동 방향을 알면 좌우 이동을 구현하는 것은 앞뒤로 이동하는 것과 같습니다.

다음은 뷰를 회전하는 것입니다.

카메라 뷰의 요 또는 가로 회전은 상수 위 벡터를 중심으로 회전하는 것뿐입니다. 다음은 임의의 축을 중심으로 카메라 보기 벡터를 회전하는 일반적인 코드입니다. axis을 중심으로 deltaAngle라디안의 회전을 나타내는 쿼터니언을 구성한 다음 쿼터니언을 사용하여 카메라 보기 벡터를 회전합니다.

// Extract camera look vector
var frontDirection = vec3.create();
vec3.subtract(this.lookAtPoint, this.eyePoint, frontDirection);
vec3.normalize(frontDirection);
var q = quat4.create();
// Construct quaternion
quat4.fromAngleAxis(deltaAngle, axis, q);
// Rotate camera look vector
quat4.multiplyVec3(q, frontDirection);
// Update camera look vector
this.lookAtPoint = vec3.create(this.eyePoint);
vec3.add(this.lookAtPoint, frontDirection);

추천

피치 또는 카메라 보기의 수직 회전을 구현하는 방법은 비슷하지만 위 벡터를 중심으로 회전하는 대신 좌우 이동 벡터를 중심으로 회전을 적용합니다. 첫 번째 단계는 화면 이동 벡터를 계산한 다음 이 축을 중심으로 카메라 보기 벡터를 회전하는 것입니다.

요약

Pointer Lock API를 사용하면 마우스 커서를 제어할 수 있습니다. 웹 게임을 만드는 경우 플레이어가 흥분해서 마우스를 창 밖으로 옮겼는데 게임에서 마우스 업데이트를 받지 못해 죽지 않게 되면 좋아할 것입니다. 사용법은 간단합니다.

  • 포인터 잠금 상태를 추적하는 pointerlockchange 이벤트 리스너 추가
  • 특정 요소의 포인터 잠금 요청
  • 업데이트를 수신하도록 mousemove 이벤트 리스너 추가

외부 데모

참조