Maneira correta de abstrair um controlador XBox

12

Eu tenho um controlador XBox360 que gostaria de usar como entrada para um aplicativo.

O que não consigo descobrir é a melhor maneira de expor isso por meio de uma interface.

Nos bastidores, a classe que lida com o (s) controlador (es) depende do estado do botão de busca.

Inicialmente, tentei algo link:

Event ButtonPressed() as ButtonEnum

onde ButtonEnumestava ButtonRed, ButtonStart, etc ...

Isso é um pouco limitado, pois suporta apenas pressionamentos de botão, não mantém / padrões (pressione duas vezes, etc.)

A próxima idéia era simplesmente expor o estado do botão ao aplicativo, por exemplo

Property RedPressed as Boolean
Property StartPressed as Boolean
Property Thumb1XAxis as Double

Isso é muito flexível, mas na verdade força muito trabalho no aplicativo e exige que ele seja pesquisado - eu preferiria o evento, se possível.

Eu considerei adicionar vários eventos, por exemplo:

Event ButtonPressed(Button as ButtonEnum)
Event ButtonPressedTwice(Button as ButtonEnum)
Event ButtonHeldStart(Button as ButtonEnum)
Event ButtonHeldEnd(Button as ButtonEnum)

mas isso parece um pouco desajeitado e foi uma dor real na tela "Botão de ligação".

Alguém pode me indicar a maneira "correta" de lidar com as entradas dos controladores.

NB: Estou usando o SlimDX dentro da classe que implementa a interface. Isso me permite ler o estado com muita facilidade. Quaisquer alternativas que resolveriam meu problema também são apreciadas

Basic
fonte

Respostas:

21

Não existe um mapeamento perfeito que ofereça uma abstração específica da plataforma, porque obviamente a maioria dos identificadores que fazem sentido para um controle 360 ​​estão errados para um controle PlayStation (A em vez de X, B em vez de Círculo). E, claro, um controle do Wii é outra coisa.

A maneira mais eficaz que encontrei para lidar com isso é usar três camadas de implementação. A camada inferior é totalmente específica da plataforma / controlador e sabe quantos botões digitais e eixos analógicos estão disponíveis. É essa camada que sabe pesquisar o estado do hardware, e é essa camada que lembra o estado anterior o suficiente para saber quando um botão acabou de ser pressionado, ou se está pressionado por mais de um tique ou se não está pressionado. . Fora isso, é burro - uma classe de estado puro que representa um único tipo de controlador. Seu valor real é abstrair o âmago da questão de consultar o estado do controlador longe da camada intermediária.

A camada intermediária é o mapeamento de controle real, desde botões reais até conceitos de jogos (por exemplo, A -> Jump). Nós os chamamos de impulsos em vez de botões, porque eles não estão mais ligados a um tipo de controlador específico. É nessa camada que você pode remapear os controles (durante o desenvolvimento ou em tempo de execução, a pedido do usuário). Cada plataforma possui seu próprio mapeamento de controles para impulsos virtuais . Você não pode e não deve tentar fugir disso. Cada controlador é único e precisa de seu próprio mapeamento. Os botões podem mapear para mais de um impulso (dependendo do modo de jogo), e mais de um botão pode mapear para o mesmo impulso (por exemplo, A e X aceleram, B e Y desaceleram). O mapeamento define tudo isso,

A camada superior é a camada do jogo. É preciso impulsos e não se importa como eles foram gerados. Talvez eles tenham vindo de um controlador, ou uma gravação de um controlador, ou talvez eles tenham vindo de uma IA. Nesse nível, você não se importa. O que importa é que exista um novo impulso de salto, ou que o impulso de aceleração tenha continuado, ou que o impulso de mergulho tenha um valor de 0,35 neste tique.

Com esse tipo de sistema, você escreve a camada inferior uma vez para cada controlador. A camada superior é independente da plataforma. O código para a camada intermediária precisa ser gravado apenas uma vez, mas os dados (o remapeamento) precisam ser refeitos para cada plataforma / controlador.

MrCranky
fonte
Parece uma abordagem muito limpa e elegante. Obrigado!
Básico
1
Muito agradável. Muito melhor que o meu: P
Jordaan Mylonas
3
Abstração muito agradável. Mas tenha cuidado ao implementar: não crie e destrua um novo objeto de impulso para cada ação do usuário. Use pool. Coletor de lixo vai agradecer.
Grega g
Absolutamente. Uma matriz estática dimensionada para o número máximo de impulsos simultâneos é quase sempre a melhor opção; já que quase sempre você deseja apenas uma instância de cada impulso ativo por vez. Na maioria das vezes, essa matriz possui apenas alguns itens, por isso é rápido para iterar.
precisa saber é o seguinte
@ Grega obrigado a vocês dois - eu não tinha pensado nisso.
Básico
1

Honestamente, eu diria que a interface ideal dependeria muito do uso no jogo. No entanto, para um cenário de uso geral, sugiro uma arquitetura de duas camadas que separa abordagens baseadas em eventos e em pesquisas.

A camada inferior pode ser considerada a camada "baseada em pesquisa" e expõe o estado atual de um botão. Uma dessas interfaces poderia simplesmente ter 2 funções, GetAnalogState(InputIdentifier)e GetDigitalState(InputIdentifier)onde InputIdentifierestá um valor enumerado que representa qual botão, gatilho ou stick você está checando. (Executar GetAnalogState para um botão retornaria 1.0 ou 0.0 e executar GetDigitalState para um stick analógico retornaria true se acima de um limite predefinido ou false caso contrário).

A segunda camada usaria a camada inferior para gerar eventos após a mudança de estado e permitir que os elementos se registrassem para retornos de chamada (os eventos C # são gloriosos). Esses retornos de chamada incluem eventos de imprensa, liberação, toque, pressão longa, etc. Para um stick analógico, você pode incluir gestos, por exemplo, QCF para um jogo de luta. O número de eventos expostos dependeria do detalhamento de um evento que você deseja despachar. Você poderia simplesmente disparar um simples ButtonStateChanged(InputIdentifier)se quisesse lidar com todo o resto em uma lógica separada.

Portanto, se você precisou verificar o estado atual de um botão de entrada ou controle, verifique a camada inferior. Se você deseja simplesmente disparar uma função em um evento de entrada, registre-se para um retorno de chamada da segunda camada.

Jordaan Mylonas
fonte