Compartilhar via


Práticas de entrada para jogos

Este tópico descreve padrões e técnicas para usar efetivamente dispositivos de entrada em jogos da Plataforma Universal do Windows (UWP).

Lendo este tópico, você aprenderá:

  • como acompanhar os 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 disposições complexas de botão 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, FlightStick e Gamepad. Como você decide qual API usar para seu jogo?

Você deve escolher qualquer API que lhe dê a entrada mais apropriada para o 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 por meio de outras classes. Isso restringiria o jogo a dar 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, é recomendável enumerar todos os objetos RawGameController como base para garantir que ofereçam suporte a qualquer dispositivo de nicho que 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 curada. Por exemplo, se o dispositivo também for um gamepad, talvez você queira ajustar a interface de mapeamento de botões para refletir isso e fornecer alguns mapeamentos de botões padrões e intuitivos para escolher. (Isso contrasta com a necessidade de o jogador configurar manualmente as entradas do gamepad se você estiver usando apenas RawGameController.)

Como alternativa, você pode analisar a ID do fornecedor (VID) e a ID do produto (PID) de um RawGameController (usando HardwareVendorId e HardwareProductId, respectivamente) e fornecer sugestões de mapeamento de botões para dispositivos populares, e ainda assim manter a compatibilidade com dispositivos desconhecidos que são lançados no futuro por meio de mapeamentos manuais pelo jogador.

Manter 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 de nome semelhante em seu próprio tópico).

No entanto, o que acontece quando o player desconecta o controlador ou conecta um novo? Você precisa lidar com esses eventos e atualizar sua lista adequadamente. Consulte Adicionar e remover gamepads para obter mais informações (novamente, cada tipo de controlador tem uma seção de 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, deverá colocar um bloqueio em torno dele para que apenas um thread possa acessá-lo 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 leva um segundo ou dois 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 "atualiza" o gamepad principal (em um jogo de jogador único; jogos multijogador exigirão soluções mais sofisticadas). Em seguida, você 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 seguinte simplesmente retorna o primeiro gamepad da lista (ou nullptr se a lista estiver vazia). Em seguida, você só precisa se lembrar de verificar se há nullptr sempre que fizer qualquer coisa com o controlador. Cabe a você bloquear a jogabilidade quando não houver nenhum controlador 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);
    }
}

Acompanhamento de usuários e seus dispositivos

Todos os dispositivos de entrada são associados a um user para que sua identidade possa ser vinculada à sua jogabilidade, realizações, 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 permaneça conectado ao sistema após a saída do usuário anterior. Quando um usuário entra ou sai, 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 usuário também é a forma como um dispositivo de entrada está associado ao controlador de navegação da interface do usuário correspondente.

Por esses motivos, a entrada do jogador deve ser monitorada 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.

Detectar transições de botão

Às vezes, você deseja saber exatamente quando um botão é pressionado ou liberado pela primeira vez; isto é, quando o estado do botão muda 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 foi alterado.

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 um controle de arcade, um volante de corrida 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 newReading (a leitura do gamepad da iteração de loop anterior) para oldReading, em seguida, preenche newReading com uma nova leitura de 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 booliano da seleção do newReading botão e oldReading, em seguida, executam a lógica booliana para determinar se a transição de destino ocorreu. Essas funções retornarão 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, retornarão false.

Detectando disposições de botões complexas

Cada botão de um dispositivo de entrada fornece uma leitura digital que indica se ele é pressionado (para baixo) ou liberado (para cima). Para eficiência, as leituras de botão não são representadas como valores boolianos individuais; Em vez disso, todos eles são 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 o bit correspondente é definido; caso contrário, ele será liberado (para cima).

Lembre-se de como os botões únicos 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, volante de corrida e outros tipos de dispositivo 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 soltados, ou se um conjunto de botões está disposto de uma forma 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 de gamepad A e B 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 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.

Obter 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 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 dão suporte a relatórios detalhados de bateria, você pode obter essa e mais informações sobre a bateria, conforme detalhado em Obter informações sobre a bateria. No entanto, a maioria dos controladores de jogo não dá suporte a esse nível de relatórios de bateria e, em vez disso, usam hardware de baixo custo. Para esses controladores, você precisará ter as seguintes considerações em mente:

  • ChargeRateInMilliwatts e DesignCapacityInMilliwattHours sempre serão NULL.

  • Você pode obter a porcentagem da bateria ao dividir RemainingCapacityInMilliwattHours / por FullChargeCapacityInMilliwattHours. Você deve ignorar os valores dessas propriedades e lidar apenas com a porcentagem calculada.

  • A porcentagem do ponto 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 no percentual de duração da bateria restante, verifique se ele está em conformidade com os valores acima. Por exemplo, se você quiser avisar o player quando a bateria do controlador estiver baixa, faça isso quando atingir 10%.

Consulte também