Práticas de entrada para jogos
Este tópico descreve padrões e técnicas para usar efetivamente dispositivos de entrada em jogos UWP (Plataforma Universal do Windows).
Ao ler este tópico, você aprenderá:
- Como rastrear jogadores e quais dispositivos de entrada e navegação eles estão usando no momento
- Como detectar transições de botão (pressionado para liberado, liberado para pressionado)
- Como detectar arranjos complexos de botões com um único teste
Escolhendo uma classe de dispositivo de entrada
Existem muitos tipos diferentes de APIs de entrada disponíveis para você, como ArcadeStick, FlightStick e Gamepad. Como você decide qual API usar para o seu jogo?
Você deve escolher qualquer API que forneça a entrada mais apropriada para o seu jogo. Por exemplo, se você estiver fazendo um jogo de plataforma 2D, provavelmente poderá usar a classe Gamepad e não se preocupar com a funcionalidade extra disponível em outras classes. Isso restringiria o jogo ao suporte apenas a gamepads e forneceria uma interface consistente que funcionará em muitos gamepads diferentes sem a necessidade de código adicional.
Por outro lado, para simulações complexas de voo e corrida, talvez você queira enumerar todos os objetos RawGameController como uma linha de base para garantir que eles ofereçam suporte a qualquer dispositivo de nicho que os jogadores entusiastas possam ter, incluindo dispositivos como pedais separados ou acelerador que ainda são usados por um único jogador.
A partir daí, você pode usar o método FromGameController de uma classe de entrada, como Gamepad.FromGameController, para ver se cada dispositivo tem uma exibição mais organizada. Por exemplo, se o dispositivo também for um Gamepad, talvez você queira ajustar a interface do usuário de mapeamento de botões para refletir isso e fornecer alguns mapeamentos de botão padrão sensatos para escolher. (Isso contrasta com exigir que o jogador configure manualmente as entradas do gamepad se você estiver usando apenasRawGameController.)
Como alternativa, você pode examinar a ID do fornecedor (VID) e a ID do produto (PID) de um RawGameController (usando HardwareVendorId e HardwareProductId, respectivamente) e fornecer mapeamentos de botão sugeridos para dispositivos populares, mantendo-se compatível com dispositivos desconhecidos que serão lançados no futuro por meio de mapeamentos manuais pelo jogador.
Mantendo o controle de controladores conectados
Embora cada tipo de controlador inclua uma lista de controladores conectados (como Gamepad.Gamepads), é uma boa ideia manter sua própria lista de controladores. Consulte a lista de gamepads para obter mais informações (cada tipo de controlador tem uma seção com nome semelhante em seu próprio tópico).
No entanto, o que acontece quando o jogador desconecta seu controle ou conecta um novo? Você precisa lidar com esses eventos e atualizar sua lista de acordo. Consulte Adicionando e removendo gamepads para obter mais informações (novamente, cada tipo de controlador tem uma seção com nome semelhante em seu próprio tópico).
Como os eventos adicionados e removidos são gerados de forma assíncrona, você pode obter resultados incorretos ao lidar com sua lista de controladores. Portanto, sempre que você acessar sua lista de controladores, você deve colocar um bloqueio em torno dela para que apenas um thread possa acessá-la por vez. Isso pode ser feito com o Runtime de simultaneidade, especificamente a classe critical_section, em <ppl.h>.
Outra coisa a se pensar é que a lista de controladores conectados estará inicialmente vazia e levará um ou dois segundos para ser preenchida. Portanto, se você atribuir apenas o gamepad atual no método start, ele será nulo!
Para corrigir isso, você deve ter um método que "atualize" o gamepad principal (em um jogo para um jogador; jogos multijogador exigirão soluções mais sofisticadas). Em seguida, você deve chamar esse método nos manipuladores de eventos do controlador adicionado e removido do controlador ou no método de atualização.
O método a seguir simplesmente retorna o primeiro gamepad da lista (ou nullptr se a lista estiver vazia). Então você só precisa se lembrar de verificar se há nullptr sempre que fizer qualquer coisa com o controlador. Cabe a você decidir se deseja bloquear o jogo quando não houver um controle conectado (por exemplo, pausando o jogo) ou simplesmente fazer com que o jogo continue, ignorando a entrada.
#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;
}
Juntando tudo, aqui está um exemplo de como lidar com a entrada de um 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);
}
}
Rastreando usuários e seus dispositivos
Todos os dispositivos de entrada são associados a um usuário para que sua identidade possa ser vinculada à jogabilidade, conquistas, alterações de configurações e outras atividades. Os usuários podem entrar ou sair à vontade, e é comum que um usuário diferente entre em um dispositivo de entrada que permanece conectado ao sistema depois que o usuário anterior saiu. Quando um usuário entra ou sai, o evento IGameController.UserChanged é gerado. Você pode registrar um manipulador de eventos para esse evento para acompanhar os jogadores e os dispositivos que eles estão usando.
A identidade do usuário também é a maneira como um dispositivo de entrada é associado ao seu controlador de navegação de interface do usuário correspondente.
Por esses motivos, a entrada do jogador deve ser rastreada e correlacionada com a propriedade User da classe de dispositivo (herdada da interface IGameController ).
O aplicativo de exemplo UserGamepadPairingUWP no GitHub demonstra como você pode acompanhar os usuários e os dispositivos que eles estão usando.
Detectando transições de botão
Às vezes, você quer saber quando um botão é pressionado ou liberado pela primeira vez; ou seja, precisamente quando o estado do botão faz a transição de liberado para pressionado ou de pressionado para liberado. Para determinar isso, você precisa se lembrar da leitura anterior do dispositivo e comparar a leitura atual com ela para ver o que mudou.
O exemplo a seguir demonstra uma abordagem básica para lembrar a leitura anterior; Os gamepads são mostrados aqui, mas os princípios são os mesmos para Arcade Stick, Racing Wheel e outros tipos de dispositivos de entrada.
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)
}
Antes de fazer qualquer outra coisa, Game::Loop
move o valor existente de (a leitura do gamepad da iteração do loop anterior) para oldReading
, então preenche newReading
com uma nova leitura do newReading
gamepad para a iteração atual. Isso fornece as informações necessárias para detectar transições de botão.
O exemplo a seguir demonstra uma abordagem básica para detectar transições de botão:
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;
}
Essas duas funções primeiro derivam o estado booleano da seleção de botão de newReading
e oldReading
, em seguida, executam a lógica booleana para determinar se a transição de destino ocorreu. Essas funções retornam true somente se a nova leitura contiver o estado de destino (pressionado ou liberado, respectivamente) e a leitura antiga também não contiver o estado de destino; caso contrário, elas retornarão false.
Detecção de arranjos complexos de botões
Cada botão de um dispositivo de entrada fornece uma leitura digital que indica se ele está pressionado (para baixo) ou liberado (para cima). Para eficiência, as leituras de botão não são representadas como valores booleanos individuais; em vez disso, eles são todos empacotados em campos de bits representados por enumerações específicas do dispositivo, como GamepadButtons. Para ler botões específicos, o mascaramento bit a bit é usado para isolar os valores nos quais você está interessado. Um botão é pressionado (para baixo) quando seu bit correspondente é definido; caso contrário, ele será liberado (para cima).
Lembre-se de como os botões individuais são determinados a serem pressionados ou liberados; Os gamepads são mostrados aqui, mas os princípios são os mesmos para Arcade Stick, Racing Wheel e outros tipos de dispositivos de entrada.
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).
}
Como você pode ver, determinar o estado de um único botão é simples, mas às vezes você pode querer determinar se vários botões são pressionados ou liberados, ou se um conjunto de botões é organizado de uma maneira específica - alguns pressionados, outros não. Testar vários botões é mais complexo do que testar botões únicos, especialmente com o potencial de estado de botão misto, mas há uma fórmula simples para esses testes que se aplica a testes de botão único e múltiplo.
O exemplo a seguir determina se os botões A e B do gamepad são pressionados:
if ((GamepadButtons::A | GamepadButtons::B) == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A and B buttons are both pressed.
}
O exemplo a seguir determina se os botões A e B do gamepad são liberados:
if ((GamepadButtons::None == (reading.Buttons & GamepadButtons::A | GamepadButtons::B))
{
// The A and B buttons are both released (not pressed).
}
O exemplo a seguir determina se o botão A do gamepad é pressionado enquanto o botão B é liberado:
if (GamepadButtons::A == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A button is pressed and the B button is released (B is not pressed).
}
A fórmula que todos esses cinco exemplos têm em comum é que a disposição dos botões a serem testados é especificada pela expressão no lado esquerdo do operador de igualdade, enquanto os botões a serem considerados são selecionados pela expressão de mascaramento no lado direito.
O exemplo a seguir demonstra essa fórmula mais claramente reescrevendo o exemplo anterior:
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).
}
Essa fórmula pode ser aplicada para testar qualquer número de botões em qualquer arranjo de seus estados.
Obtenha o estado da bateria
Para qualquer controlador de jogo que implemente a interface IGameControllerBatteryInfo , você pode chamar TryGetBatteryReport na instância do controlador para obter um objeto BatteryReport que fornece informações sobre a bateria no controlador. Você pode obter propriedades como a taxa de carregamento da bateria (ChargeRateInMilliwatts), a capacidade de energia estimada de uma nova bateria (DesignCapacityInMilliwattHours) e a capacidade de energia totalmente carregada da bateria atual (FullChargeCapacityInMilliwattHours).
Para controladores de jogos que dão suporte a relatórios detalhados de bateria, você pode obter essas e mais informações sobre a bateria, conforme detalhado em Obter informações sobre a bateria. No entanto, a maioria dos controladores de jogos não suporta esse nível de relatório de bateria e, em vez disso, usa hardware de baixo custo. Para esses controladores, você precisará ter em mente as seguintes considerações:
ChargeRateInMilliwatts e DesignCapacityInMilliwattHours sempre serão NULL.
Você pode obter a porcentagem da bateria calculando RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours. Você deve ignorar os valores dessas propriedades e lidar apenas com a porcentagem calculada.
A porcentagem do marcador anterior sempre será uma das seguintes:
- 100% (Completo)
- 70% (Médio)
- 40% (Baixo)
- 10% (Crítico)
Se o código executar alguma ação (como desenhar a interface do usuário) com base na porcentagem de vida útil restante da bateria, verifique se ele está em conformidade com os valores acima. Por exemplo, se você quiser avisar o jogador quando a bateria do controle estiver fraca, faça-o quando ela atingir 10%.