Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Este tópico descreve padrões e técnicas para o uso eficaz de dispositivos de entrada em jogos da Plataforma Universal do Windows (UWP).
Ao ler este tópico, você aprenderá:
- Como rastrear jogadores e quais dispositivos de entrada e navegação eles estão usando atualmente
- Como detetar transições de botão (pressionado para solto, solto para pressionado)
- Como detetar arranjos de botões complexos com um único teste
Escolhendo uma classe de dispositivo de entrada
Há muitos tipos diferentes de APIs de entrada disponíveis para você, como ArcadeStick, FlightSticke Gamepad. Como você decide qual API usar para o seu jogo?
Você deve escolher qual API lhe dá a entrada mais apropriada para o seu jogo. Por exemplo, se você estiver fazendo um jogo de plataforma 2D, provavelmente pode usar a classe Gamepad e não se preocupar com a funcionalidade extra disponível através de outras classes. Isso restringiria o jogo a suportar apenas gamepads e forneceria uma interface consistente que funcionará em muitos gamepads diferentes sem necessidade de código adicional.
Por outro lado, para simulações complexas de voo e corrida, pode querer enumerar todos os objetos RawGameController como um padrão de referência para garantir que suportem qualquer dispositivo de nicho que os jogadores entusiastas possam ter, incluindo dispositivos como pedais separados ou acelerador que ainda são utilizados por um único jogador.
A partir daí, pode usar o método FromGameController de uma classe de entrada
Como alternativa, pode-se verificar o ID do fornecedor (VID) e o ID do produto (PID) de um RawGameController (usando HardwareVendorId e HardwareProductId, respectivamente) e fornecer mapeamentos de botões sugeridos para dispositivos populares, enquanto mantém a compatibilidade com dispositivos desconhecidos que possam surgir no futuro através de mapeamentos manuais feitos pelo jogador.
Manter o controle dos 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 desliga o comando ou liga 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 controle 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 dele para que apenas um thread possa acessá-lo de cada vez. Isso pode ser feito com o Concurrency Runtime, 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 segundo ou dois para ser preenchida. Então, 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 single-player; jogos multiplayer exigirão soluções mais sofisticadas). Em seguida, deve chamar esse método nos manipuladores de eventos de adição e remoção 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. Depende de você se deseja bloquear a jogabilidade quando não há um controle conectado (por exemplo, pausando o jogo) ou simplesmente fazer com que a jogabilidade 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 User para que sua identidade possa ser vinculada à jogabilidade, conquistas, alterações de configurações e outras atividades. Os utilizadores podem iniciar ou terminar a sessão à vontade, e é comum que um utilizador diferente inicie sessão num dispositivo de entrada que permanece conectado ao sistema depois que o utilizador anterior terminou a sessão. Quando um utilizador inicia ou termina a sessão, o evento IGameController.UserChanged é acionado. 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 utilizador é também a forma como um dispositivo de entrada se associa ao controlador de navegação da UI correspondente .
Por estas razões, a entrada do jogador deve ser rastreada e correlacionada com a propriedade User da classe do 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.
Deteção de transições de botões
Às vezes, você quer saber quando um botão é pressionado ou liberado pela primeira vez; ou seja, precisamente quando o estado do botão transita de liberado para pressionado ou de pressionado para liberado. Para determinar isso, você precisa lembrar a 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 apresentados aqui, mas os princípios são os mesmos para joystick de arcade, volante de corrida e os 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 newReading (a leitura do gamepad da iteração de loop anterior) para oldReadinge, em seguida, preenche newReading com uma nova leitura do gamepad para a iteração atual. Isso fornece as informações necessárias para detetar transições de botões.
O exemplo a seguir demonstra uma abordagem básica para detetar 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ões de newReading e oldReadinge, em seguida, executam a lógica booleana para determinar se a transição de destino ocorreu. Essas funções retornam verdadeiro somente se a nova leitura contiver o estado de destino (pressionado ou liberado, respectivamente) e a leitura antiga não contiver o estado de destino; caso contrário, elas retornam falso.
Deteção de arranjos de botões complexos
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ões não são representadas como valores booleanos individuais; em vez disso, todos eles são embalados 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 em que você está interessado. Um botão é pressionado (para baixo) quando seu bit correspondente é definido; caso contrário, é liberado (para cima).
Lembre-se de como botões individuais são determinados para serem pressionados ou liberados; Os gamepads são mostrados aqui, mas os princípios são os mesmos para arcade stick, racing wheel e os 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 está 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 do 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 de gamepad A e B 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 os cinco exemplos têm em comum é que o arranjo dos botões a serem testados é especificado 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).
}
Esta fórmula pode ser aplicada para testar qualquer número de botões em qualquer arranjo de seus estados.
Obter o estado da bateria
Para qualquer controlador de jogo que implemente a interface IGameControllerBatteryInfo, pode-se 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 que a bateria está carregando (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 suportam 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:
TaxaDeCargaEmMiliwatts e CapacidadeDeProjetoEmMiliampereHoras será sempre NULL.
Você pode obter a porcentagem da bateria calculando CapacidadeRestanteEmMilliwattHoras / CapacidadeTotalDeCargaEmMilliwattHoras. Você deve ignorar os valores dessas propriedades e lidar apenas com a porcentagem calculada.
A percentagem do ponto anterior será sempre uma das seguintes:
- 100% (completo)
- 70% (Médio)
- 40% (Baixo)
- 10% (Crítica)
Se o seu código executar alguma ação (como desenhar a interface do usuário) com base na porcentagem de vida útil da bateria restante, certifique-se de que ele esteja em conformidade com os valores acima. Por exemplo, se quiser avisar o leitor quando a bateria do comando estiver fraca, faça-o quando atingir os 10%.