컨트롤 추가
참고 항목
이 항목은 DirectX를 사용하여 간단한 UWP(유니버설 Windows 플랫폼) 게임 만들기 자습서 시리즈의 일부입니다. 해당 링크의 항목은 시리즈의 컨텍스트를 설정합니다.
[ Windows 10의 UWP 앱에 맞게 업데이트되었습니다. Windows 8.x 문서는 보관함을 참조하세요. ]
좋은 UWP(유니버설 Windows 플랫폼) 게임은 다양한 인터페이스를 지원합니다. 잠재적인 플레이어는 실제 버튼이 없는 태블릿, 게임 컨트롤러가 연결된 PC 또는 고성능 마우스와 게임 키보드가 있는 최신 데스크톱 게임 장비에서 Windows 10을 사용할 수 있습니다. 게임에서 컨트롤은 MoveLookController 클래스에 구현되어 있습니다. 이 클래스는 세 가지 유형의 입력(마우스 및 키보드, 터치, 게임 패드) 모두를 단일 컨트롤러에 집계합니다. 그 결과로 1인칭 슈팅 게임에서 여러 디바이스와 함께 작동하는 장르 표준 이동-보기 컨트롤러를 사용할 수 있게 되었습니다.
참고 항목
컨트롤에 대한 자세한 내용은 게임용 이동-보기 컨트롤 및 게임용 터치 컨트롤을 참조하세요.
목표
이제 게임이 렌더링되고 있지만, 플레이어를 이동시키거나 대상에게 슈팅할 수는 없습니다. UWP DirectX 게임에서 다음과 같은 유형의 입력에 대해 1인칭 슈팅 게임 이동-보기 컨트롤을 구현하는 방법에 대해 살펴보겠습니다.
- 마우스 및 키보드
- 터치
- Gamepad
참고 항목
이 샘플의 최신 게임 코드를 다운로드하지 않은 경우 Direct3D 샘플 게임으로 이동합니다. 이 샘플은 UWP 기능 샘플의 큰 컬렉션의 일부입니다. 샘플을 다운로드하는 방법에 대한 지침은 Windows 개발을 위한 샘플 애플리케이션을 참조하세요.
공용 컨트롤 동작
터치 컨트롤 및 마우스/키보드 컨트롤에는 매우 유사한 핵심 구현 사항이 있습니다. UWP 앱에서 포인터는 단순히 화면의 지점입니다. 마우스를 슬라이딩하거나 터치 스크린에서 손가락을 슬라이딩함으로써 이동할 수 있습니다. 따라서 단일 이벤트 집합에 등록할 수 있으며 플레이어가 마우스 또는 터치 스크린을 사용하여 포인터를 이동하거나 누를 수 있다는 걱정을 할 필요가 없습니다.
샘플 게임에서 MoveLookController 클래스를 초기화하면 4개의 포인터 특정 이벤트와 1개의 마우스 특정 이벤트를 등록합니다.
이벤트 | 설명 |
---|---|
CoreWindow::PointerPressed | 마우스 왼쪽 또는 오른쪽 버튼을 누르거나 터치 표면을 터치했습니다. |
CoreWindow::PointerMoved | 마우스가 이동되었거나 터치 표면에서 끌기 동작이 이루어졌습니다. |
CoreWindow::PointerReleased | 마우스 왼쪽 버튼이 해제되었거나 터치 표면에 닿는 개체가 해제되었습니다. |
CoreWindow::PointerExited | 포인터가 기본 창 외부로 이동했습니다. |
Windows::Devices::Input::MouseMoved | 마우스가 특정 거리를 이동했습니다. 현재 X-Y 위치가 아닌 마우스 이동 델타 값에만 관심이 있습니다. |
이러한 이벤트 처리기는 애플리케이션 창에서 MoveLookController가 초기화되는 즉시 사용자 입력을 수신하기 시작하도록 설정되어 있습니다.
void MoveLookController::InitWindow(_In_ CoreWindow const& window)
{
ResetState();
window.PointerPressed({ this, &MoveLookController::OnPointerPressed });
window.PointerMoved({ this, &MoveLookController::OnPointerMoved });
window.PointerReleased({ this, &MoveLookController::OnPointerReleased });
window.PointerExited({ this, &MoveLookController::OnPointerExited });
...
// There is a separate handler for mouse-only relative mouse movement events.
MouseDevice::GetForCurrentView().MouseMoved({ this, &MoveLookController::OnMouseMoved });
...
}
InitWindow에 대한 전체 코드는 GitHub에서 확인할 수 있습니다.
게임이 특정 입력을 반드시 수신해야 하는 경우를 판단할 수 있도록 MoveLookController 클래스는 컨트롤러 유형에 관계 없이 3개의 컨트롤러 특정 상태를 가집니다.
시스템 상태 | 설명 |
---|---|
없음 | 컨트롤러의 초기화된 상태입니다. 게임에서 어떤 컨트롤러 입력도 예상되지 않기 때문에 모든 입력이 무시됩니다. |
WaitForInput | 컨트롤러는 왼쪽 마우스 클릭, 터치 이벤트, 게임 패드의 메뉴 버튼을 사용하여 플레이어가 게임에서 메시지를 승인하는 동안 기다립니다. |
Active | 컨트롤러는 활성 게임 플레이 모드에 있습니다. |
WaitForInput 상태와 게임 일시 중지
일시 중지되면 게임은 WaitForInput 상태가 됩니다. 플레이어가 게임의 주 창 밖에서 포인터를 이동시키거나 일시 중지 버튼(P 키나 게임 패드의 시작 버튼)를 누를 때 이러한 일시 중지가 발생합니다. MoveLookController는 이러한 버튼 누름을 등록하고 IsPauseRequested 메서드가 호출될 때 게임 루프에 알립니다. 이때 IsPauseRequested에서 true를 반환하면 게임 루프에서 MoveLookController의 WaitForPress를 호출하여 컨트롤러를 WaitForInput 상태로 전환합니다.
WaitForInput 상태가 되면 활성 상태로 되돌아갈 때까지 게임에서 거의 모든 게임 플레이 입력 이벤트의 처리가 중지됩니다. 예외는 일시 중지 버튼인데, 이 버튼을 누르면 게임이 활성 상태로 되돌아갈 수 있습니다. 일시 중지 버튼 외에 게임을 활성 상태로 되돌리기 위해서는 플레이어가 메뉴 항목을 선택해야 합니다.
활성 상태
활성 상태 동안에는 MoveLookController 인스턴스가 활성화된 모든 입력 디바이스에서 이벤트를 처리하고 플레이어의 의도를 해석합니다. 그 결과로, 플레이어 보기에서 속도 및 보기 방향을 업데이트하고 게임 루프에서 업데이트가 호출된 후 업데이트된 데이터를 게임과 공유합니다.
모든 포인터 입력은 활성 상태에서 추적되며 포인터 작업에 따라 서로 다른 포인터 ID를 사용합니다. PointerPressed 이벤트가 수신되면, MoveLookController는 창에서 만든 포인터 ID 값을 확보합니다. 포인터 ID는 특정 유형의 입력을 나타냅니다. 예를 들어, 멀티 터치 디바이스에서는 여러 활성 입력이 동시에 있을 수 있습니다. ID는 플레이어가 사용 중인 입력을 추적하는 데 사용됩니다. 한 이벤트가 터치 스크린의 이동 사각형에 있으면 포인터 ID가 할당되어 이동 사각형의 모든 포인터 이벤트를 추적합니다. 화재 사각형의 다른 포인터 이벤트는 별도의 포인터 ID로 개별 추적됩니다.
참고 항목
마우스에서의 입력과 게임 패드의 오른쪽 섬스틱에서의 입력은 별도로 처리되는 ID를 갖습니다.
포인터 이벤트가 특정 게임 작업에 매핑된 후 MoveLookController 개체가 기본 게임 루프와 공유하는 데이터를 업데이트해야 합니다.
샘플 게임에서 Update 메서드를 호출하면 입력이 처리되고 속도 및 보기 방향 변수(m_velocity 및 m_lookdirection)가 업데이트됩니다. 그런 다음, 게임 루프가 퍼블릭 Velocity 및 LookDirection 메서드를 호출하여 검색을 합니다.
참고 항목
Update 메서드에 대한 자세한 내용은 이 페이지 뒷부분에서 확인할 수 있습니다.
게임 루프는 MoveLookController 인스턴스에서 IsFiring 메서드를 호출하여 플레이어가 발사되는지 여부를 테스트할 수 있습니다. MoveLookController는 플레이어가 세 가지 입력 유형 중 하나에서 실행 버튼을 눌렀는지 확인하기 위해 검사합니다.
bool MoveLookController::IsFiring()
{
if (m_state == MoveLookControllerState::Active)
{
if (m_autoFire)
{
return (m_fireInUse || (m_mouseInUse && m_mouseLeftInUse) || PollingFireInUse());
}
else
{
if (m_firePressed)
{
m_firePressed = false;
return true;
}
}
}
return false;
}
이제 세 가지 컨트롤 유형의 구현에 대해 좀더 자세히 살펴보겠습니다.
상대 마우스 컨트롤 추가
마우스 이동이 감지되면 해당 이동을 사용하여 카메라의 새 피치와 요를 결정할 수 있습니다. 이를 위해 동작의 절대 x-y 픽셀 좌표를 기록하는 대신 마우스가 이동한 상대 거리(이동 시작과 정지 사이의 델타)를 처리하는 상대 마우스 컨트롤을 구현합니다.
이렇게 하려면 MouseMoved 이벤트에서 반환된 Windows::Device::Input::MouseEventArgs::MouseDelta 인수 개체에서 MouseDelta::X 및 MouseDelta::Y 필드를 검사하여 X(가로 동작) 및 Y(세로 동작) 좌표의 변경 내용을 가져옵니다.
void MoveLookController::OnMouseMoved(
_In_ MouseDevice const& /* mouseDevice */,
_In_ MouseEventArgs const& args
)
{
// Handle Mouse Input via dedicated relative movement handler.
switch (m_state)
{
case MoveLookControllerState::Active:
XMFLOAT2 mouseDelta;
mouseDelta.x = static_cast<float>(args.MouseDelta().X);
mouseDelta.y = static_cast<float>(args.MouseDelta().Y);
XMFLOAT2 rotationDelta;
// Scale for control sensitivity.
rotationDelta.x = mouseDelta.x * MoveLookConstants::RotationGain;
rotationDelta.y = mouseDelta.y * MoveLookConstants::RotationGain;
// Update our orientation based on the command.
m_pitch -= rotationDelta.y;
m_yaw += rotationDelta.x;
// Limit pitch to straight up or straight down.
float limit = XM_PI / 2.0f - 0.01f;
m_pitch = __max(-limit, m_pitch);
m_pitch = __min(+limit, m_pitch);
// Keep longitude in sane range by wrapping.
if (m_yaw > XM_PI)
{
m_yaw -= XM_PI * 2.0f;
}
else if (m_yaw < -XM_PI)
{
m_yaw += XM_PI * 2.0f;
}
break;
}
}
터치 지원 추가
터치 컨트롤은 태블릿에서 사용자를 지원하기에 적합합니다. 이 게임은 특정한 게임 내 작업에 맞게 각각을 정렬하면서 화면의 특정 영역에 대한 영역 지정을 해제하여 터치 입력을 수집합니다. 이 게임의 터치 입력은 세 가지 영역을 사용합니다.
다음 명령에는 터치 컨트롤 동작이 요약되어 있습니다. 사용자 입력 | 작업 :------- | :-------- 사각형 이동 | 터치 입력은 가상 조이스틱으로 변환되는데, 세로 이동은 전방/후방 위치 동작으로, 가로 이동은 왼쪽/오른쪽 위치 동작으로 해석됩니다. 실행 사각형 | 구를 실행합니다. 이동 및 실행 사각형 외부에서 터치 | 카메라 보기의 회전(피치 및 요)을 변경합니다.
MoveLookController는 포인터 ID를 검사 이벤트가 발생한 위치를 확인하고 다음 작업 중 하나를 수행합니다.
- 이동 또는 화재 사각형에서 PointerMoved 이벤트가 발생한 경우, 컨트롤러의 포인터 위치를 업데이트합니다.
- PointerMoved 이벤트가 화면의 나머지 부분(보기 컨트롤로 정의됨)에서 발생한 경우, 보기 방향 벡터의 피치 및 요의 변경 사항을 계산합니다.
일단 터치 컨트롤이 구현되면 이전에 Direct2D를 사용해 그렸던 사각형이 이동, 실행, 모양 영역이 어디인지를 플레이어에게 나타냅니다.
이제 각 컨트롤을 구현하는 방법을 살펴보겠습니다.
이동 및 실행 컨트롤러
화면의 왼쪽 아래 사분면에 있는 이동 컨트롤러 사각형은 방향 패드로 사용됩니다. 이 공간의 왼쪽과 오른쪽으로 엄지 손가락을 밀면 플레이어가 왼쪽과 오른쪽으로 이동하고, 위쪽과 아래쪽으로 밀면 카메라가 앞뒤로 이동합니다. 이 설정 후, 화면의 오른쪽 아래 사분면에 있는 실행 컨트롤러를 탭하면 구가 실행됩니다.
SetMoveRect 및 SetFireRect 메서드는 2개의 2D 벡터를 가져와서 화면에서 각 사각형의 왼쪽 위와 오른쪽 아래 모서리 위치를 지정하여 입력 사각형을 만듭니다.
이제 m_fireUpperLeft 및 m_fireLowerRight에 매개 변수가 할당되는데, 이들은 사용자가 사각형 내부를 터치하고 있는지 판단하는 데 도움이 됩니다.
m_fireUpperLeft = upperLeft;
m_fireLowerRight = lowerRight;
화면 크기가 조정되면 이러한 사각형이 적절한 크기로 다시 그려집니다.
컨트롤에 대한 영역 지정이 해제되면 사용자는 실제로 이를 사용하는 시기를 결정합니다. 이를 위해 사용자가 포인터를 누르거나 이동하거나 해제하는 경우에 대해 MoveLookController::InitWindow 메서드에서 몇 가지 이벤트 처리기를 설정합니다.
window.PointerPressed({ this, &MoveLookController::OnPointerPressed });
window.PointerMoved({ this, &MoveLookController::OnPointerMoved });
window.PointerReleased({ this, &MoveLookController::OnPointerReleased });
OnPointerPressed 메서드를 사용하여 이동 또는 실행 사각형 안을 사용자가 처음으로 누르면 어떻게 되는지를 가장 먼저 확인합니다. 여기에서 컨트롤 터치 영역과 포인터가 해당 컨트롤러에 이미 있는지 여부를 확인합니다. 손가락으로 특정 컨트롤을 터치하는 것이 처음이라면 다음과 같이 합니다.
- m_moveFirstDown 또는 m_fireFirstDown에 터치한 위치를 2D 벡터로 저장합니다.
- m_movePointerID 또는 m_firePointerID에 포인터 ID를 할당합니다.
- 해당 컨트롤에 대해 포인터가 활성 상태가 되었기 때문에 해당되는 InUse 플래그(m_moveInUse 또는 m_fireInUse)를
true
로 설정합니다.
PointerPoint point = args.CurrentPoint();
uint32_t pointerID = point.PointerId();
Point pointerPosition = point.Position();
PointerPointProperties pointProperties = point.Properties();
auto pointerDevice = point.PointerDevice();
auto pointerDeviceType = pointerDevice.PointerDeviceType();
XMFLOAT2 position = XMFLOAT2(pointerPosition.X, pointerPosition.Y);
...
case MoveLookControllerState::Active:
switch (pointerDeviceType)
{
case winrt::Windows::Devices::Input::PointerDeviceType::Touch:
// Check to see if this pointer is in the move control.
if (position.x > m_moveUpperLeft.x &&
position.x < m_moveLowerRight.x &&
position.y > m_moveUpperLeft.y &&
position.y < m_moveLowerRight.y)
{
// If no pointer is in this control yet.
if (!m_moveInUse)
{
// Process a DPad touch down event.
// Save the location of the initial contact
m_moveFirstDown = position;
// Store the pointer using this control
m_movePointerID = pointerID;
// Set InUse flag to signal there is an active move pointer
m_moveInUse = true;
}
}
// Check to see if this pointer is in the fire control.
else if (position.x > m_fireUpperLeft.x &&
position.x < m_fireLowerRight.x &&
position.y > m_fireUpperLeft.y &&
position.y < m_fireLowerRight.y)
{
if (!m_fireInUse)
{
// Save the location of the initial contact
m_fireLastPoint = position;
// Store the pointer using this control
m_firePointerID = pointerID;
// Set InUse flag to signal there is an active fire pointer
m_fireInUse = true;
...
}
}
...
사용자가 이동 컨트롤을 터치하고 있는지 실행 컨트롤을 터치하고 있는지 판단했다면 이제는 플레이어가 손가락을 눌러 이동을 하고 있는지 확인합니다. MoveLookController::OnPointerMoved 메서드를 사용하여 어떤 포인터가 이동되었는지 확인하고 새 위치를 2D 벡터로 저장합니다.
PointerPoint point = args.CurrentPoint();
uint32_t pointerID = point.PointerId();
Point pointerPosition = point.Position();
PointerPointProperties pointProperties = point.Properties();
auto pointerDevice = point.PointerDevice();
// convert to allow math
XMFLOAT2 position = XMFLOAT2(pointerPosition.X, pointerPosition.Y);
switch (m_state)
{
case MoveLookControllerState::Active:
// Decide which control this pointer is operating.
// Move control
if (pointerID == m_movePointerID)
{
// Save the current position.
m_movePointerPosition = position;
}
// Look control
else if (pointerID == m_lookPointerID)
{
...
}
// Fire control
else if (pointerID == m_firePointerID)
{
m_fireLastPoint = position;
}
...
사용자가 컨트롤 내에서 제스처를 하면 포인터가 해제됩니다. MoveLookController::OnPointerReleased 메서드를 사용하여 어떤 포인터가 해제되었는지 확인하고 일련의 재설정을 수행합니다.
이동 컨트롤이 해제된 경우 다음을 수행합니다.
- 게임 시 이동되는 일이 없도록 모든 방향에서 플레이어의 속도를
0
로 설정합니다. - 사용자가 더 이상 이동 컨트롤러를 터치하지 않기 때문에 m_moveInUse를
false
로 전환합니다. - 이동 컨트롤러에 더 이상 포인터가 없기 때문에 이동 포인터 ID를
0
로 설정합니다.
if (pointerID == m_movePointerID)
{
// Stop on release.
m_velocity = XMFLOAT3(0, 0, 0);
m_moveInUse = false;
m_movePointerID = 0;
}
실행 컨트롤이 해제된 경우에는 실행 컨트롤에 더 이상 포인터가 없기 때문에 m_fireInUse 플래그를 false
로, 실행 포인터 ID를 0
로 전환합니다.
else if (pointerID == m_firePointerID)
{
m_fireInUse = false;
m_firePointerID = 0;
}
보기 컨트롤러
화면에서 사용되지 않은 영역에 대한 터치 디바이스 포인터 이벤트는 보기 컨트롤러로 처리됩니다. 이 영역 주위를 손가락을 밀어 플레이어 카메라의 피치와 요(회전)를 변경합니다.
MoveLookController::OnPointerPressed 이벤트가 이 영역의 터치 디바이스에서 발생하고 게임 상태가 활성으로 설정되어 있으면 포인터 ID가 할당됩니다.
// If no pointer is in this control yet.
if (!m_lookInUse)
{
// Save point for later move.
m_lookLastPoint = position;
// Store the pointer using this control.
m_lookPointerID = pointerID;
// These are for smoothing.
m_lookLastDelta.x = m_lookLastDelta.y = 0;
m_lookInUse = true;
}
여기에서 MoveLookController는 이벤트를 실행한 포인터의 포인터 ID를 보기 영역에 해당되는 특정 변수에 할당합니다. 보기 영역에서 터치가 발생하는 경우 m_lookPointerID 변수는 이벤트를 실행시킨 포인터 ID로 설정됩니다. 또한 부울 변수 m_lookInUse는 아직 컨트롤이 해제되지 않았음을 나타내도록 설정됩니다.
이제 샘플 게임에서 PointerMoved 터치 스크린 이벤트를 처리하는 방법을 살펴보겠습니다.
MoveLookController::OnPointerMoved 메서드 내에서 어떤 종류의 포인터 ID가 이벤트에 할당되었는지 확인합니다. m_lookPointerID의 경우 포인터의 위치에서 변경을 계산합니다. 그런 다음, 이 델타 값을 사용하여 회전을 어느 정도 변경해야 하는지를 계산합니다. 마지막으로, 게임에 사용할 m_pitch 및 m_yaw를 업데이트하여 플레이어 회전을 변경할 수 있습니다.
// This is the look pointer.
else if (pointerID == m_lookPointerID)
{
// Look control.
XMFLOAT2 pointerDelta;
// How far did the pointer move?
pointerDelta.x = position.x - m_lookLastPoint.x;
pointerDelta.y = position.y - m_lookLastPoint.y;
XMFLOAT2 rotationDelta;
// Scale for control sensitivity.
rotationDelta.x = pointerDelta.x * MoveLookConstants::RotationGain;
rotationDelta.y = pointerDelta.y * MoveLookConstants::RotationGain;
// Save for next time through.
m_lookLastPoint = position;
// Update our orientation based on the command.
m_pitch -= rotationDelta.y;
m_yaw += rotationDelta.x;
// Limit pitch to straight up or straight down.
float limit = XM_PI / 2.0f - 0.01f;
m_pitch = __max(-limit, m_pitch);
m_pitch = __min(+limit, m_pitch);
...
}
마지막으로, 샘플 게임이 PointerReleased 터치 스크린 이벤트를 어떻게 처리하는지 살펴보겠습니다.
사용자가 터치 제스처를 완료하고 화면에서 손을 떼면 MoveLookController::OnPointerReleased가 시작됩니다.
PointerReleased 이벤트를 실행한 포인터의 ID가 이전에 기록된 이동 포인터의 ID인 경우 플레이어가 보기 영역에 대한 터치를 중지한 것이기 때문에 MoveLookController에서 속도가 0
로 설정됩니다.
else if (pointerID == m_lookPointerID)
{
m_lookInUse = false;
m_lookPointerID = 0;
}
마우스 및 키보드 지원 추가
이 게임은 키보드 및 마우스에 대해 다음과 같은 컨트롤 레이아웃을 가지고 있습니다.
사용자 입력 | 작업 |
---|---|
수 | 플레이어를 앞으로 이동 |
A | 플레이어를 왼쪽으로 이동 |
S | 플레이어를 뒤로 이동 |
D | 플레이어를 오른쪽으로 이동 |
X | 보기를 위로 이동 |
스페이스바 | 보기를 아래로 이동 |
P | 게임을 일시 중지 |
마우스 이동 | 카메라 보기의 회전(피치 및 요) 변경 |
왼쪽 마우스 버튼 | 구 실행 |
키보드를 사용하기 위해 샘플 게임은 MoveLookController::InitWindow 메서드 내에 2개의 새로운 이벤트인 CoreWindow::KeyUp 및 CoreWindow::KeyDown을 등록합니다. 이러한 이벤트는 키 누름과 해제를 처리합니다.
window.KeyDown({ this, &MoveLookController::OnKeyDown });
window.KeyUp({ this, &MoveLookController::OnKeyUp });
마우스는 포인터를 사용하지만 터치 컨트롤과 약간 다르게 처리됩니다. 컨트롤 레이아웃에 맞춰지도록 MoveLookController는 마우스가 이동될 때마다 카메라를 회전시키고 왼쪽 마우스 버튼을 누를 때 이벤트를 실행합니다.
이는 MoveLookController의 OnPointerPressed 메서드에서 처리됩니다.
이 메서드에서 Windows::Devices::Input::PointerDeviceType
열거형 값에서 어떤 종류의 포인터 디바이스가 사용되고 있는지 확인합니다.
게임이 활성 상태이고 PointerDeviceType이 터치가 아니면 이를 마우스 입력으로 가정합니다.
case MoveLookControllerState::Active:
switch (pointerDeviceType)
{
case winrt::Windows::Devices::Input::PointerDeviceType::Touch:
// Behavior for touch controls
...
default:
// Behavior for mouse controls
bool rightButton = pointProperties.IsRightButtonPressed();
bool leftButton = pointProperties.IsLeftButtonPressed();
if (!m_autoFire && (!m_mouseLeftInUse && leftButton))
{
m_firePressed = true;
}
if (!m_mouseInUse)
{
m_mouseInUse = true;
m_mouseLastPoint = position;
m_mousePointerID = pointerID;
m_mouseLeftInUse = leftButton;
m_mouseRightInUse = rightButton;
// These are for smoothing.
m_lookLastDelta.x = m_lookLastDelta.y = 0;
}
break;
}
break;
플레이어가 마우스 버튼 중 하나를 누르는 것을 중지하면 CoreWindow::PointerReleased 마우스 이벤트가 발생하면서 MoveLookController::OnPointerReleased 메서드가 호출되고 입력이 완료됩니다. 이 때 왼쪽 마우스 버튼을 누르고 있다가 놓으면 구의 실행이 중지됩니다. 보기는 항상 사용되므로 게임에서 동일한 마우스 포인터를 계속 사용하여 진행 중인 보기 이벤트를 추적합니다.
case MoveLookControllerState::Active:
// Touch points
if (pointerID == m_movePointerID)
{
// Stop movement
...
}
else if (pointerID == m_lookPointerID)
{
// Stop look rotation
...
}
// Fire button has been released
else if (pointerID == m_firePointerID)
{
// Stop firing
...
}
// Mouse point
else if (pointerID == m_mousePointerID)
{
bool rightButton = pointProperties.IsRightButtonPressed();
bool leftButton = pointProperties.IsLeftButtonPressed();
// Mouse no longer in use so stop firing
m_mouseInUse = false;
// Don't clear the mouse pointer ID so that Move events still result in Look changes.
// m_mousePointerID = 0;
m_mouseLeftInUse = leftButton;
m_mouseRightInUse = rightButton;
}
break;
이제 마지막 지원 컨트롤 유형인 게임 패드에 대해 살펴보겠습니다. 게임 패드는 포인터 개체를 사용하지 않기 때문에 터치 및 마우스 컨트롤과 별개로 처리됩니다. 이 때문에 몇 가지 이벤트 처리기와 메서드를 새로 추가해야 합니다.
게임 패드 지원 추가
이 게임에서는 Windows.Gaming.Input API 호출을 통해 게임 패드 지원이 추가됩니다. 이 API 집합은 레이싱 휠, 플라이트 스틱 같은 게임 컨트롤러 입력에 액세스할 수 있도록 해줍니다.
게임 패드 컨트롤은 다음과 같습니다.
사용자 입력 | 작업 |
---|---|
왼쪽 아날로그 스틱 | 플레이어를 이동 |
오른쪽 아날로그 스틱 | 카메라 보기의 회전(피치 및 요) 변경 |
오른쪽 트리거 | 구 실행 |
시작/메뉴 버튼 | 게임 일시 중지 또는 다시 시작 |
InitWindow 메서드에서 게임 패드가 추가 상태인지 제거 상태인지 판단하기 위해 2개의 이벤트가 새로 추가됩니다. 이러한 이벤트는 m_gamepadsChanged 속성을 업데이트합니다. 이 속성은 UpdatePollingDevices 메서드에서 알려진 게임 패드의 목록이 변경되었는지 여부를 확인하는 데 사용됩니다.
// Detect gamepad connection and disconnection events.
Gamepad::GamepadAdded({ this, &MoveLookController::OnGamepadAdded });
Gamepad::GamepadRemoved({ this, &MoveLookController::OnGamepadRemoved });
참고 항목
UWP 앱은 포커스가 없는 동안 게임 컨트롤러에서 입력을 받을 수 없습니다.
UpdatePollingDevices 메서드
MoveLookController 인스턴스의 UpdatePollingDevices 메서드는 즉시 게임 패드의 연결 여부를 확인합니다. 연결되어 있으면 Gamepad.GetCurrentReading로 상태 판독을 시작합니다. 이렇게 하면 GamepadReading 구조체가 반환되기 때문에 어떤 버튼이 클릭되었는지 또는 어떤 섬스틱이 이동되었는지 확인할 수 있습니다.
게임의 상태가 WaitForInput이면 게임이 다시 시작될 수 있도록 컨트롤러의 시작/메뉴 버튼에 대해서만 수신 대기를 합니다.
상태가 활성이면 사용자 입력을 확인하고 어떤 게임 내 작업이 필요한지 판단합니다. 예를 들어, 사용자가 특정 방향으로 왼쪽 아날로그 스틱을 이동한다는 것은 스틱이 이동되는 방향으로 플레이어를 이동시켜야 한다는 것을 의미합니다. 스틱의 특정 방향으로의 이동은 데드존의 반경보다 더 크게 등록되어야 하며, 그러지 않으면 아무 동작도 수행되지 않습니다. 이 데드존 반경은 “드리프팅”을 막기 위해 반드시 필요합니다. 스틱 위에 놓여 있는 플레이어의 엄지 손가락의 작은 이동까지 컨트롤러가 잡아낼 때 이러한 드리프팅이 발생합니다. 데드존이 없으면 컨트롤이 사용자의 움직임에 너무 민감하게 이루어질 수 있습니다.
섬스틱 입력은 x축과 y축 모두에서 -1에서 1 사이의 값을 갖습니다. 다음 상수는 썸스틱 데드존의 반경을 지정합니다.
#define THUMBSTICK_DEADZONE 0.25f
이 변수를 사용하여 실행 가능한 섬스틱 입력을 처리하게 됩니다. 이동은 한쪽 축에서 [-1, -.26] 또는 [.26, 1]의 값으로 이루어집니다.
UpdatePollingDevices 메서드의 이 부분은 왼쪽 및 오른쪽 섬스틱을 처리합니다. 각 스틱의 X 값과 Y 값을 확인하여 데드존을 벗어났는지 여부를 알아봅니다. 하나 또는 둘 모두가 있는 경우, 해당되는 구성 요소를 업데이트합니다. 예를 들어 왼쪽 섬스틱이 X축을 따라 왼쪽으로 이동 중인 경우 m_moveCommand 벡터의 x 구성 요소에 -1을 추가합니다. 이 벡터는 모든 디바이스의 모든 이동을 집계하는 데 사용되고, 추후 플레이어가 어디로 이동해야 하는지 계산하는 데 사용됩니다.
// Use the left thumbstick to control the eye point position
// (position of the player).
// Check if left thumbstick is outside of dead zone on x axis
if (reading.LeftThumbstickX > THUMBSTICK_DEADZONE ||
reading.LeftThumbstickX < -THUMBSTICK_DEADZONE)
{
// Get value of left thumbstick's position on x axis
float x = static_cast<float>(reading.LeftThumbstickX);
// Set the x of the move vector to 1 if the stick is being moved right.
// Set to -1 if moved left.
m_moveCommand.x -= (x > 0) ? 1 : -1;
}
// Check if left thumbstick is outside of dead zone on y axis
if (reading.LeftThumbstickY > THUMBSTICK_DEADZONE ||
reading.LeftThumbstickY < -THUMBSTICK_DEADZONE)
{
// Get value of left thumbstick's position on y axis
float y = static_cast<float>(reading.LeftThumbstickY);
// Set the y of the move vector to 1 if the stick is being moved forward.
// Set to -1 if moved backwards.
m_moveCommand.y += (y > 0) ? 1 : -1;
}
왼쪽 스틱이 이동을 컨트롤하는 방법과 유사하게, 오른쪽 스틱도 카메라 회전을 컨트롤합니다.
오른쪽 섬스틱 동작은 마우스 및 키보드 컨트롤 설정에 있는 마우스 이동 동작과 일치합니다. 스틱이 데드존을 벗어나는 경우 현재 포인터 위치와 현재 사용자가 보려는 위치 간의 차이를 계산합니다. 포인터 위치(pointerDelta)의 이러한 변경은 추후 Update 메서드에 적용되도록 카메라 회전의 피치 및 요를 업데이트하는 데 사용됩니다. pointerDelta 벡터는 마우스 및 터치 입력에서 포인터 위치의 변경을 추적하기 위해 MoveLookController::OnPointerMoved 메서드에서도 사용되기 때문에 친숙해 보일 수 있습니다.
// Use the right thumbstick to control the look at position
XMFLOAT2 pointerDelta;
// Check if right thumbstick is outside of deadzone on x axis
if (reading.RightThumbstickX > THUMBSTICK_DEADZONE ||
reading.RightThumbstickX < -THUMBSTICK_DEADZONE)
{
float x = static_cast<float>(reading.RightThumbstickX);
// Register the change in the pointer along the x axis
pointerDelta.x = x * x * x;
}
// No actionable thumbstick movement. Register no change in pointer.
else
{
pointerDelta.x = 0.0f;
}
// Check if right thumbstick is outside of deadzone on y axis
if (reading.RightThumbstickY > THUMBSTICK_DEADZONE ||
reading.RightThumbstickY < -THUMBSTICK_DEADZONE)
{
float y = static_cast<float>(reading.RightThumbstickY);
// Register the change in the pointer along the y axis
pointerDelta.y = y * y * y;
}
else
{
pointerDelta.y = 0.0f;
}
XMFLOAT2 rotationDelta;
// Scale for control sensitivity.
rotationDelta.x = pointerDelta.x * 0.08f;
rotationDelta.y = pointerDelta.y * 0.08f;
// Update our orientation based on the command.
m_pitch += rotationDelta.y;
m_yaw += rotationDelta.x;
// Limit pitch to straight up or straight down.
m_pitch = __max(-XM_PI / 2.0f, m_pitch);
m_pitch = __min(+XM_PI / 2.0f, m_pitch);
게임의 컨트롤은 구 실행 기능이 없으면 완전할 수 없습니다!
또한 이 UpdatePollingDevices 메서드는 오른쪽 트리거의 누름 여부를 확인합니다. 누름인 경우, m_firePressed 속성이 true로 전환되면서 구 실행을 시작하도록 게임에 알립니다.
if (reading.RightTrigger > TRIGGER_DEADZONE)
{
if (!m_autoFire && !m_gamepadTriggerInUse)
{
m_firePressed = true;
}
m_gamepadTriggerInUse = true;
}
else
{
m_gamepadTriggerInUse = false;
}
Update 메서드
마무리를 위해 Update 메서드에 대해 자세히 알아보겠습니다. 이 메서드는 지원되는 모든 입력에서 플레이어가 수행한 모든 이동 또는 회전을 병합하여 속도 벡터를 생성하고 게임 루프가 액세스할 수 있도록 피치 및 요 값을 업데이트합니다.
Update 메서드는 UpdatePollingDevices를 호출하여 컨트롤러 상태를 업데이트함으로써 처리를 시작합니다. 또한 이 메서드는 게임 패드에서 모든 입력을 수집하고 m_moveCommand 벡터에 해당 이동을 추가합니다.
Update 메서드에서 다음과 같이 입력을 확인합니다.
- 플레이어가 이동 컨트롤러 사각형을 사용하는 경우에는 포인터 위치의 변경 여부를 판단하고, 사용자가 컨트롤러의 데드존 밖으로 포인터를 이동시킨 경우에는 이를 토대로 계산을 합니다. 데드존을 벗어난 경우 m_moveCommand 벡터 속성이 가상 조이스틱 값으로 업데이트됩니다.
- 이동 키보드 입력 중 하나를 누르면
1.0f
또는-1.0f
의 값이 m_moveCommand 벡터의 해당 구성 요소에 추가됩니다. 예를 들어 앞으로 이동은1.0f
이고 뒤로 이동은-1.0f
입니다.
모든 이동 입력을 고려했다면 이제 m_moveCommand 벡터를 실행하여 몇 가지 계산을 통해 게임 환경과 관련해 플레이어의 방향을 보여주는 새 벡터를 생성합니다.
환경과 관련된 이동을 계산하고 이를 해당 방향의 속도로 플레이어에게 적용합니다.
마지막으로 m_moveCommand 벡터를 (0.0f, 0.0f, 0.0f)
으로 재설정하여 다음 게임 프레임을 준비합니다.
void MoveLookController::Update()
{
// Get any gamepad input and update state
UpdatePollingDevices();
if (m_moveInUse)
{
// Move control.
XMFLOAT2 pointerDelta;
pointerDelta.x = m_movePointerPosition.x - m_moveFirstDown.x;
pointerDelta.y = m_movePointerPosition.y - m_moveFirstDown.y;
// Figure out the command from the virtual joystick.
XMFLOAT3 commandDirection = XMFLOAT3(0.0f, 0.0f, 0.0f);
// Leave 32 pixel-wide dead spot for being still.
if (fabsf(pointerDelta.x) > 16.0f)
m_moveCommand.x -= pointerDelta.x / fabsf(pointerDelta.x);
if (fabsf(pointerDelta.y) > 16.0f)
m_moveCommand.y -= pointerDelta.y / fabsf(pointerDelta.y);
}
// Poll our state bits set by the keyboard input events.
if (m_forward)
{
m_moveCommand.y += 1.0f;
}
if (m_back)
{
m_moveCommand.y -= 1.0f;
}
if (m_left)
{
m_moveCommand.x += 1.0f;
}
if (m_right)
{
m_moveCommand.x -= 1.0f;
}
if (m_up)
{
m_moveCommand.z += 1.0f;
}
if (m_down)
{
m_moveCommand.z -= 1.0f;
}
// Make sure that 45deg cases are not faster.
if (fabsf(m_moveCommand.x) > 0.1f ||
fabsf(m_moveCommand.y) > 0.1f ||
fabsf(m_moveCommand.z) > 0.1f)
{
XMStoreFloat3(&m_moveCommand, XMVector3Normalize(XMLoadFloat3(&m_moveCommand)));
}
// Rotate command to align with our direction (world coordinates).
XMFLOAT3 wCommand;
wCommand.x = m_moveCommand.x * cosf(m_yaw) - m_moveCommand.y * sinf(m_yaw);
wCommand.y = m_moveCommand.x * sinf(m_yaw) + m_moveCommand.y * cosf(m_yaw);
wCommand.z = m_moveCommand.z;
// Scale for sensitivity adjustment.
// Our velocity is based on the command. Y is up.
m_velocity.x = -wCommand.x * MoveLookConstants::MovementGain;
m_velocity.z = wCommand.y * MoveLookConstants::MovementGain;
m_velocity.y = wCommand.z * MoveLookConstants::MovementGain;
// Clear movement input accumulator for use during next frame.
m_moveCommand = XMFLOAT3(0.0f, 0.0f, 0.0f);
}
다음 단계
컨트롤이 추가되었다면 이제 몰입감 높은 게임을 위해 추가해야 할 또 다른 기능이 있습니다. 바로 사운드입니다! 음악과 사운드 효과는 모든 게임에 있어 중요합니다. 사운드 추가에 대해서는 다음에 살펴보겠습니다.