이 항목에서는 UWP(유니버설 Windows 플랫폼) 게임에서 입력 디바이스를 효과적으로 사용하기 위한 패턴과 기술에 대해 설명합니다.
이 항목을 읽으면 다음에 대해 알아봅니다.
- 플레이어를 추적하는 방법 및 현재 사용 중인 입력 및 탐색 디바이스
- 버튼 전환을 감지하는 방법(누름-놓기, 놓기-누름)
- 단일 테스트를 사용하여 복잡한 단추 정렬을 검색하는 방법
입력 디바이스 클래스 선택
아케이드스틱, 플라이트스틱, 게임 패드 등 다양한 유형의 입력 API를 사용할 수 있습니다. 게임에 사용할 API를 어떻게 결정합니까?
게임에 가장 적합한 입력을 제공하는 API를 선택해야 합니다. 예를 들어 2D 플랫폼 게임을 만드는 경우 게임 패드 클래스만 사용하고 다른 클래스를 통해 사용할 수 있는 추가 기능을 신경 쓰지 않을 수 있습니다. 이렇게 하면 게임이 게임 패드만 지원하도록 제한되고 추가 코드 없이도 다양한 게임 패드에서 작동하는 일관된 인터페이스를 제공합니다.
반면, 복잡한 비행 및 레이싱 시뮬레이션의 경우 모든 RawGameController 개체를 기준선으로 열거하여 단일 플레이어가 여전히 사용하는 별도의 페달 또는 스로틀과 같은 디바이스를 포함하여 매니아 플레이어가 가질 수 있는 모든 틈새 장치를 지원하도록 할 수 있습니다.
여기에서 입력 클래스의 FromGameController 메서드(예: Gamepad.FromGameController)를 사용하여 각 디바이스에 더 큐레이팅된 보기가 있는지 확인할 수 있습니다. 예를 들어, 디바이스가 게임패드인 경우에도 이에 맞게 버튼 매핑 인터페이스를 조정하고 선택할 수 있도록 몇 가지 합리적인 기본 버튼 매핑을 제공할 수 있습니다. ( RawGameController만 사용하는 경우 플레이어가 게임 패드 입력을 수동으로 구성하도록 요구하는 것과는 대조적입니다.)
또는 RawGameController(각각 HardwareVendorId 및 HardwareProductId사용)의 VID(공급업체 ID) 및 PID(제품 ID)를 확인하고, 플레이어가 수동 매핑을 통해 나중에 나오는 알 수 없는 디바이스와 호환되는 상태를 유지하면서 인기 있는 디바이스에 대해 제안된 단추 매핑을 제공할 수 있습니다.
연결된 컨트롤러 추적
각 컨트롤러 유형에는 연결된 컨트롤러 목록(예: Gamepad.Gamepad)이 포함되어 있지만, 고유한 컨트롤러 목록을 유지하는 것이 좋습니다. 자세한 내용은 게임 패드 목록을 참조하세요(각 컨트롤러 유형에는 자체 토픽에 비슷한 이름의 섹션이 있습니다).
그러나 플레이어가 컨트롤러를 분리하거나 새 컨트롤러를 연결하면 어떻게 되나요? 이러한 이벤트를 처리하고 그에 따라 목록을 업데이트해야 합니다. 자세한 내용은 게임 패드 추가 및 제거를 참조하세요(다시 말하지만 각 컨트롤러 유형에는 자체 토픽에 비슷한 이름의 섹션이 있습니다).
추가 및 제거된 이벤트는 비동기적으로 발생하므로 컨트롤러 목록을 처리할 때 잘못된 결과를 얻을 수 있습니다. 따라서 컨트롤러 목록에 액세스할 때마다 한 번에 하나의 스레드만 액세스할 수 있도록 잠금을 설정해야 합니다. 이 작업은 동시성 런타임, 특히 critical_section 클래스, <ppl.h>을 사용하여 수행할 수 있습니다.
고려해야 할 또 다른 점은 연결된 컨트롤러 목록이 처음에는 비어 있고 채우는 데 1~2초가 걸린다는 것입니다. 따라서 시작 메서드에서 현재 게임 패드만 할당하면 null!
이를 해결하려면 주 게임 패드를 "새로 고치는" 방법이 있어야 합니다(단일 플레이어 게임에서 멀티 플레이어 게임에는 보다 정교한 솔루션이 필요함). 그런 다음, 컨트롤러 추가 및 제거 이벤트 처리기에서 또는 업데이트 메서드에서 이 메서드를 호출해야 합니다.
다음 메서드는 목록의 첫 번째 게임 패드를 반환합니다(또는 목록이 비어 있는 경우 nullptr ). 그런 다음 컨트롤러로 작업할 때는 언제나 nullptr을(를) 확인해야 하는 것을 잊지 마세요. 연결된 컨트롤러가 없을 때(예: 게임을 일시 중지하여) 게임 플레이를 차단할지, 아니면 입력을 무시하면서 게임 플레이를 계속할 것인지는 사용자에게 달려 있습니다.
#include <ppl.h>
using namespace Platform::Collections;
using namespace Windows::Gaming::Input;
using namespace concurrency;
Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();
Gamepad^ GetFirstGamepad()
{
Gamepad^ gamepad = nullptr;
critical_section::scoped_lock{ m_lock };
if (m_myGamepads->Size > 0)
{
gamepad = m_myGamepads->GetAt(0);
}
return gamepad;
}
게임 패드에서 입력을 처리하는 방법의 예는 다음과 같습니다.
#include <algorithm>
#include <ppl.h>
using namespace Platform::Collections;
using namespace Windows::Foundation;
using namespace Windows::Gaming::Input;
using namespace concurrency;
static Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();
static Gamepad^ m_gamepad = nullptr;
static critical_section m_lock{};
void Start()
{
// Register for gamepad added and removed events.
Gamepad::GamepadAdded += ref new EventHandler<Gamepad^>(&OnGamepadAdded);
Gamepad::GamepadRemoved += ref new EventHandler<Gamepad^>(&OnGamepadRemoved);
// Add connected gamepads to m_myGamepads.
for (auto gamepad : Gamepad::Gamepads)
{
OnGamepadAdded(nullptr, gamepad);
}
}
void Update()
{
// Update the current gamepad if necessary.
if (m_gamepad == nullptr)
{
auto gamepad = GetFirstGamepad();
if (m_gamepad != gamepad)
{
m_gamepad = gamepad;
}
}
if (m_gamepad != nullptr)
{
// Gather gamepad reading.
}
}
// Get the first gamepad in the list.
Gamepad^ GetFirstGamepad()
{
Gamepad^ gamepad = nullptr;
critical_section::scoped_lock{ m_lock };
if (m_myGamepads->Size > 0)
{
gamepad = m_myGamepads->GetAt(0);
}
return gamepad;
}
void OnGamepadAdded(Platform::Object^ sender, Gamepad^ args)
{
// Check if the just-added gamepad is already in m_myGamepads; if it isn't,
// add it.
critical_section::scoped_lock lock{ m_lock };
auto it = std::find(begin(m_myGamepads), end(m_myGamepads), args);
if (it == end(m_myGamepads))
{
m_myGamepads->Append(args);
}
}
void OnGamepadRemoved(Platform::Object^ sender, Gamepad^ args)
{
// Remove the gamepad that was just disconnected from m_myGamepads.
unsigned int indexRemoved;
critical_section::scoped_lock lock{ m_lock };
if (m_myGamepads->IndexOf(args, &indexRemoved))
{
if (m_gamepad == m_myGamepads->GetAt(indexRemoved))
{
m_gamepad = nullptr;
}
m_myGamepads->RemoveAt(indexRemoved);
}
}
사용자 및 해당 디바이스 추적
모든 입력 디바이스는 사용자와 연관되어 있어, 사용자의 신원을 게임플레이, 도전 과제, 설정 변경 및 기타 활동에 연결할 수 있습니다. 사용자는 언제든지 로그인하거나 로그아웃할 수 있으며, 이전 사용자가 로그아웃한 후에도 시스템에 연결된 상태로 유지되는 입력 디바이스에서 다른 사용자가 로그인하는 것이 일반적입니다. 사용자가 로그인하거나 로그아웃하면 IGameController.UserChanged 이벤트가 발생합니다. 이 이벤트에 대한 이벤트 처리기를 등록하여 사용 중인 플레이어와 디바이스를 추적할 수 있습니다.
사용자 정체성은 입력 디바이스가 해당 UI 탐색 컨트롤러에 연결된 방식이기도 합니다.
이러한 이유로 플레이어 입력은 디바이스 클래스의 User 속성( IGameController 인터페이스에서 상속됨)과 추적 및 상관 관계를 지정해야 합니다.
GitHub의 UserGamepadPairingUWP 샘플 앱은 사용 중인 사용자 및 디바이스를 추적하는 방법을 보여 줍니다.
버튼 전환 감지
단추를 처음 누르거나 놓을 때를 알고 싶을 때도 있습니다. 예를 들어, 단추 상태가 해제에서 눌림으로 또는 눌림에서 해제로 전환되는 정확한 순간입니다. 이를 확인하려면 이전 디바이스 읽기를 기억하고 현재 판독값과 비교하여 변경된 내용을 확인해야 합니다.
다음 예제에서는 이전 읽기를 기억하기 위한 기본적인 방법을 보여 줍니다. 게임 패드는 여기에 표시되지만 아케이드 스틱, 레이싱 휠 및 기타 입력 장치 유형에 대한 원칙은 동일합니다.
Gamepad gamepad;
GamepadReading newReading();
GamepadReading oldReading();
// Called at the start of the game.
void Game::Start()
{
gamepad = Gamepad::Gamepads[0];
}
// Game::Loop represents one iteration of a typical game loop
void Game::Loop()
{
// move previous newReading into oldReading before getting next newReading
oldReading = newReading, newReading = gamepad.GetCurrentReading();
// process device readings using buttonJustPressed/buttonJustReleased (see below)
}
다른 작업을 수행하기 전에, Game::Loop은 이전 루프 반복에서 읽은 게임 패드 값인 newReading의 기존 값을 oldReading에 이동시키고, 그런 다음 현재 반복을 위해 newReading에 새로운 게임 패드 읽기를 채웁니다. 이렇게 하면 단추 전환을 검색하는 데 필요한 정보를 얻을 수 있습니다.
다음 예제에서는 단추 전환을 검색하기 위한 기본 방법을 보여 줍니다.
bool ButtonJustPressed(const GamepadButtons selection)
{
bool newSelectionPressed = (selection == (newReading.Buttons & selection));
bool oldSelectionPressed = (selection == (oldReading.Buttons & selection));
return newSelectionPressed && !oldSelectionPressed;
}
bool ButtonJustReleased(GamepadButtons selection)
{
bool newSelectionReleased =
(GamepadButtons.None == (newReading.Buttons & selection));
bool oldSelectionReleased =
(GamepadButtons.None == (oldReading.Buttons & selection));
return newSelectionReleased && !oldSelectionReleased;
}
이 두 함수는 먼저 단추 선택 newReading와 oldReading에서 부울 상태를 파생한 후, 부울 논리를 수행하여 목표 전환이 발생했는지 확인합니다. 이러한 함수는 새 판독값에 대상 상태(각각 누름 또는 놓기)
복잡한 단추 배치 탐지
입력 디바이스의 각 버튼은 눌림(아래) 또는 놓임(위) 여부를 나타내는 디지털 신호를 제공합니다. 효율성을 위해 단추 판독값은 개별 부울 값으로 표시되지 않습니다. 대신, 모두 GamepadButtons와 같은 디바이스별 열거형으로 표현되는 비트 필드로 압축됩니다. 특정 단추를 읽기 위해 비트 마스킹은 관심 있는 값을 격리하는 데 사용됩니다. 해당 비트가 설정되면 버튼이 눌려집니다(아래쪽). 그렇지 않으면 해제됩니다(위쪽).
단일 버튼이 눌리거나 놓이는 방식을 기억하십시오. 게임 패드는 여기에 표시되지만, 아케이드 스틱, 레이싱 휠 및 기타 입력 장치 유형에도 적용되는 원칙은 동일합니다.
GamepadReading reading = gamepad.GetCurrentReading();
// Determines whether gamepad button A is pressed.
if (GamepadButtons::A == (reading.Buttons & GamepadButtons::A))
{
// The A button is pressed.
}
// Determines whether gamepad button A is released.
if (GamepadButtons::None == (reading.Buttons & GamepadButtons::A))
{
// The A button is released (not pressed).
}
볼 수 있듯이 단일 버튼의 상태를 확인하는 것은 쉽습니다. 하지만 때로는 여러 버튼이 동시에 눌렸는지 놓였는지, 아니면 몇몇 버튼은 눌려 있고 몇몇 버튼은 아닌 상태로 배열되어 있는지를 알고 싶을 때가 있습니다. 여러 단추를 테스트하는 것은 특히 혼합 단추 상태의 가능성이 있는 단일 단추를 테스트하는 것보다 더 복잡하지만 단일 및 다중 단추 테스트에 모두 적용되는 이러한 테스트에 대한 간단한 수식이 있습니다.
다음 예제에서는 게임 패드 단추 A와 B를 둘 다 누를지 여부를 결정합니다.
if ((GamepadButtons::A | GamepadButtons::B) == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A and B buttons are both pressed.
}
다음 예제에서는 게임 패드 단추 A와 B가 모두 릴리스되었는지 여부를 결정합니다.
if ((GamepadButtons::None == (reading.Buttons & GamepadButtons::A | GamepadButtons::B))
{
// The A and B buttons are both released (not pressed).
}
다음 예제에서는 단추 B가 해제되는 동안 게임 패드 단추 A를 누를지 여부를 결정합니다.
if (GamepadButtons::A == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A button is pressed and the B button is released (B is not pressed).
}
5가지 예제 모두가 공통으로 갖고 있는 공식은, 테스트할 버튼의 배치가 동등 연산자의 왼쪽에 있는 식에 의해 지정되며, 고려할 버튼은 오른쪽에 있는 마스크 표현식에 의해 선택된다는 것입니다.
다음 예제에서는 이전 예제를 다시 작성하여 이 수식을 보다 명확하게 보여 줍니다.
auto buttonArrangement = GamepadButtons::A;
auto buttonSelection = (reading.Buttons & (GamepadButtons::A | GamepadButtons::B));
if (buttonArrangement == buttonSelection)
{
// The A button is pressed and the B button is released (B is not pressed).
}
이 수식은 해당 상태의 정렬에서 임의의 수의 단추를 테스트하기 위해 적용할 수 있습니다.
배터리 상태 가져오기
IGameControllerBatteryInfo 인터페이스를 구현하는 모든 게임 컨트롤러의 경우 컨트롤러 인스턴스에서 TryGetBatteryReport를 호출하여 컨트롤러의 배터리에 대한 정보를 제공하는 BatteryReport 개체를 가져올 수 있습니다. 배터리가 충전하는 속도(ChargeRateInMilliwatts), 새 배터리의 예상 에너지 용량(DesignCapacityInMilliwattHours), 현재 배터리의 완전 충전 에너지 용량(FullChargeCapacityInMilliwattHours)과 같은 속성을 얻을 수 있습니다.
배터리 상태를 상세히 보고할 수 있는 게임 컨트롤러의 경우, 배터리 및 기타 정보에 대해 에서 설명한 바와 같이 상세한 정보를에서 확인할 수 있습니다. 그러나 대부분의 게임 컨트롤러는 해당 수준의 배터리 보고를 지원하지 않으며 대신 저렴한 하드웨어를 사용합니다. 이러한 컨트롤러의 경우 다음 고려 사항을 염두에 두어야 합니다.
ChargeRateInMilliwatts 및 DesignCapacityInMilliwattHours은(는) 항상 NULL입니다.
RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours를 계산하여 배터리 비율을 얻을 수 있습니다. 이러한 속성의 값을 무시하고 계산된 백분율만 처리해야 합니다.
이전 글머리 기호의 백분율은 항상 다음 중 하나로 됩니다.
- 100% (전체)
- 70% (중간)
- 40%(낮음)
- 10%(위험)
코드가 남은 배터리 수명 비율에 따라 일부 작업(예: 그리기 UI)을 수행하는 경우 위의 값을 준수하는지 확인합니다. 예를 들어 컨트롤러의 배터리가 부족할 때 플레이어에게 경고하려면 10%도달하면 경고합니다.
참고하십시오
- Windows.System.User 클래스
- Windows.Gaming.Input.IGameController 인터페이스
- Windows.Gaming.Input.GamepadButtons 열거형
- UserGamepadPairingUWP 샘플 앱