Agregar soporte a controlador en los juegos HTML5 con la API Gamepad
() translation by (you can also view the original English article)
A medida que los juegos basados en la web se vuelven más populares, uno de los mayores puntos de adherencia para los jugadores es el control de entrada. Mientras que mis primeros juegos de FPS eran puramente de ratón y basado en el teclado, ahora tengo mucho más utilizado para un controlador de consola adecuado que prefiero usarlo para todo, incluidos los juegos basados en web.
Por suerte, el HTML5 Gamepad API existe para permitir a los desarrolladores web acceso programático a los controladores de juegos. Desafortunadamente, aunque esta API ha existido durante mucho tiempo, es sólo ahora, poco a poco, moviéndose a las versiones más recientes de los navegadores de escritorio. Se languideció durante mucho tiempo en una versión de Firefox (no una versión más alta—ninguna compilacion nightly) y fue problemática en Chrome. Ahora—bien—no es perfecto, pero un poco menos problemático y realmente bastante fácil de usar.
En
este artículo, voy a discutir las diversas características de la API,
cómo conseguir que funcione en Firefox y Chrome, y mostrar un juego real
(si es simple) y lo fácil que es agregar soporte de mando de videojuegos a ella.
Lo basico
El API Gamepad comprende las siguientes características:
- La capacidad de escuchar eventos
connect
ydisconnect
. - La capacidad de reconocer múltiples mandos de juegos. (En teoría, podría conectar tantos mandos de jeugos como cuantos puertos USB tengas.)
- La capacidad de inspeccionar estos gamepads y reconocer cuántos ejes tienen (joysticks), cuántos botones tienen (¿has jugado una consola de juegos moderna últimamente?), Y en qué estado cada uno de estos elementos individuales se encuentran.
Comencemos por discutir cómo puede detectar el soporte para un gamepad a un nivel alto.
Tanto
Firefox como Chrome soportan un método en navigator
, getGamepads()
, que devuelve una matriz de todos los dispositivos de gamepad conectados. Podemos usar esto como un método simple para detectar si la API Gamepad está presente. Aquí está una función simple para esa comprobación:
1 |
function canGame() { |
2 |
return "getGamepads" in navigator; |
3 |
}
|
Hasta aquí todo bien. Ahora para la parte funky. La API de Gamepad tiene soporte para eventos que detectan cuándo un gamepad está conectado y desconectado. La API de Gamepad tiene soporte para eventos que detectan cuándo un gamepad está conectado y desconectado. Normalmente, la página web esperará a que el usuario haga algo, cualquier cosa realmente, con el gamepad real. Esto significa que tenemos que proporcionar algún tipo de mensaje al usuario que les permite saber que necesitan "despertar" el soporte para el mando de juego si está conectado. Podrías decirles que toquen cualquier botón o muevan la palanca.
Para hacer las cosas aún más interesantes, este cheque en particular no parece necesario cuando se vuelve a cargar la página. Verá que una vez que haya utilizado la API de Gamepad en una página y luego la vuelva a cargar, la página reconoce este hecho y automáticamente lo considera conectado.
Pero espera—se pone mejor. Chrome no admite los eventos conectados (o desconectados) en este momento. El trabajo típico para esto (y el que se demuestra en los buenos documentacion MDN para la API) es configurar una encuesta y ver si un mando de juego "aparece" en la lista de dispositivos conectados.
¿Confuso? Comencemos con un ejemplo que sólo es compatible con Firefox:
1 |
<!DOCTYPE html>
|
2 |
<html>
|
3 |
<head>
|
4 |
<meta charset="utf-8"> |
5 |
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
6 |
<title></title>
|
7 |
<meta name="description" content=""> |
8 |
<meta name="viewport" content="width=device-width"> |
9 |
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script> |
10 |
|
11 |
</head>
|
12 |
<body>
|
13 |
|
14 |
<div id="gamepadPrompt"></div> |
15 |
|
16 |
<script>
|
17 |
function canGame() { |
18 |
return "getGamepads" in navigator; |
19 |
}
|
20 |
|
21 |
$(document).ready(function() { |
22 |
|
23 |
if(canGame()) { |
24 |
|
25 |
var prompt = "To begin using your gamepad, connect it and press any button!"; |
26 |
$("#gamepadPrompt").text(prompt); |
27 |
|
28 |
$(window).on("gamepadconnected", function() { |
29 |
$("#gamepadPrompt").html("Gamepad connected!"); |
30 |
console.log("connection event"); |
31 |
});
|
32 |
|
33 |
$(window).on("gamepaddisconnected", function() { |
34 |
console.log("disconnection event"); |
35 |
$("#gamepadPrompt").text(prompt); |
36 |
});
|
37 |
|
38 |
}
|
39 |
|
40 |
});
|
41 |
</script>
|
42 |
</body>
|
43 |
</html>
|
En el ejemplo anterior, comenzamos por comprobar si el navegador admite la API del Gamepad. Si
lo hace, primero actualizamos una div con instrucciones para el usuario
y luego comenzamos a escuchar inmediatamente los eventos connect
y disconnect
.
Si ejecutas esto con Firefox y conectas tu mando de juego, deberías tener que pulsar un botón, momento en el que se dispara el evento y ya está listo.
De nuevo, sin embargo, en mis pruebas, cuando vuelvo a cargar la página, el evento connection
es inmediato. Esto crea un ligero efecto de "parpadeo" que puede ser indeseable. Usted
podría utilizar un intervalo para establecer las direcciones para algo
como 250ms después de que el DOM ha cargado y sólo se preguntan si una
conexión no se produjo en el ínterin. Decidí mantener las cosas simples para este tutorial.
Nuestro código funciona para Firefox, pero ahora vamos a agregar soporte para Chrome:
1 |
<!DOCTYPE html>
|
2 |
<html>
|
3 |
<head>
|
4 |
<meta charset="utf-8"> |
5 |
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
6 |
<title></title>
|
7 |
<meta name="description" content=""> |
8 |
<meta name="viewport" content="width=device-width"> |
9 |
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script> |
10 |
|
11 |
</head>
|
12 |
<body>
|
13 |
|
14 |
<div id="gamepadPrompt"></div> |
15 |
|
16 |
<script>
|
17 |
var hasGP = false; |
18 |
|
19 |
function canGame() { |
20 |
return "getGamepads" in navigator; |
21 |
}
|
22 |
|
23 |
$(document).ready(function() { |
24 |
|
25 |
if(canGame()) { |
26 |
|
27 |
var prompt = "To begin using your gamepad, connect it and press any button!"; |
28 |
$("#gamepadPrompt").text(prompt); |
29 |
|
30 |
$(window).on("gamepadconnected", function() { |
31 |
hasGP = true; |
32 |
$("#gamepadPrompt").html("Gamepad connected!"); |
33 |
console.log("connection event"); |
34 |
});
|
35 |
|
36 |
$(window).on("gamepaddisconnected", function() { |
37 |
console.log("disconnection event"); |
38 |
$("#gamepadPrompt").text(prompt); |
39 |
});
|
40 |
|
41 |
//setup an interval for Chrome
|
42 |
var checkGP = window.setInterval(function() { |
43 |
if(navigator.getGamepads()[0]) { |
44 |
if(!hasGP) $(window).trigger("gamepadconnected"); |
45 |
window.clearInterval(checkGP); |
46 |
}
|
47 |
}, 500); |
48 |
}
|
49 |
|
50 |
});
|
51 |
</script>
|
52 |
</body>
|
53 |
</html>
|
El código es un poco más complejo, pero no terriblemente. Cargue la demostración en Chrome y vea qué sucede.
Tenga
en cuenta que tenemos una nueva variable global, hasGP
, que usaremos
como indicador general para tener un mando de juego conectado. Como
antes, tenemos dos oyentes de eventos, pero ahora tenemos un nuevo
intervalo configurado para comprobar si existe un mando de juego. Esta
es la primera vez que ves getGamepads
en acción, y lo describiremos un
poco más en la siguiente sección, pero por ahora sabemos que sólo
devuelve una matriz y si el primer elemento existe, podemos usarlo como Una forma de saber que un mando de juego está conectado.
Utilizamos jQuery para disparar el mismo evento que Firefox habría recibido, y luego borrar el intervalo. Tenga en cuenta que este mismo intervalo se disparará una vez en Firefox, así, que es un poco derrochador, pero honestamente, pensé que era una pérdida de tiempo añadiendo soporte adicional para probar Chrome frente a Firefox. Una pequeña llamada como esta desperdiciada en Firefox no debería importar en absoluto.
Ahora que tenemos un gamepad conectado, ¡vamos a trabajar con él!
El objeto Gamepad
Para darle una idea de cuántos años tengo - aquí está el joystick de última generación que usé para mi primer sistema de juegos.



Bonito - simple - y dolia como mucho después de una hora de jugar. Consolas modernas tienen mandos de juegos mucho más complejos. Considere el controlador PS4:



Este controlador tiene dos palos, una almohadilla direccional, cuatro botones principales, cuatro más en la parte posterior, un botón Share y Options, un botón PS, control táctil, un altavoz y una luz. También probablemente tiene un condensador de flujo y un fregadero de la cocina.
Afortunadamente, tenemos acceso a esta bestia a través del objeto Gamepad. Las propiedades incluyen:
-
id
: Este es el nombre del controlador. No esperes un nombre amigable aqui. Mi DualShock 4 fue reportado como54c-5c4-Wireless Controller
en Firefox, mientras que Chrome llamó al mismo controladorWireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4)
. -
index
: Dado que la API de Gamepad admite varios controladores, éste le permite determinar qué controlador numerado es. Podría ser usado para identificar al jugador uno, dos, y así sucesivamente. -
mapping
: Mapping no es algo que vamos a cubrir aquí, pero esencialmente esto es algo que el navegador puede hacer para ayudar a asignar su controlador particular a una configuración de controlador "estándar". Si has jugado varias consolas, sabes que tienen algunas similitudes en términos de control, y la API intenta "aplastar" tu controlador en un estándar. No tiene que preocuparse por esto por ahora, pero si desea obtener más detalles, consulte la sección mapping de los documentos de la API. -
connected
: Un booleano que indica si el controlador sigue conectado. -
buttons
: una matriz de valores de botón. Cada botón es una instancia de GamepadButton. Tenga en cuenta que el objetoGamepadButton
admite tanto una propiedad booleana simple (pressed
) como una propiedadvalue
para botones analógicos. -
axes
: Una matriz de valores que representan las diferentes palancas en el mando de juegos. Dado un gamepad con tres palos, tendrás una matriz de seis elementos, donde cada palo está representado por dos valores de matriz. El primero del par representa X, o movimiento de izquierda / derecha, mientras que el segundo representa Y, movimiento de arriba / abajo. En todos los casos el valor oscilará entre-1
y1
: para los valores izquierdo / derecho,-1
es izquierdo y1
es derecho; Para los valores arriba / abajo,-1
es arriba y1
es abajo. Según la API, la matriz se clasifica de acuerdo con la "importancia", por lo que en teoría, puede centrarse en losaxes[0]
y losaxes[1]
para la mayoría de las necesidades de juego. Para hacer las cosas más interesantes, utilizando mi DualShock 4, Firefox informó tres ejes (que tiene sentido—vea la imagen de arriba), pero Chrome informó dos. Parece como si el stick d-pad se informa en Firefox como un eje, pero no parece que los datos salgan de ella. En Chrome, el d-pad apareció como botones adicionales, y se leyó correctamente. -
timestamp
: Por último, este valor es una marca de tiempo que representa la última vez que se comprobó el hardware. En teoría, esto probablemente no es algo que usarías.
De acuerdo, así que es mucho para digerir. En el ejemplo siguiente, simplemente hemos añadido un intervalo para obtener e inspeccionar, el primer mando de juego e imprimir el ID y luego los botones y los ejes:
1 |
<!DOCTYPE html>
|
2 |
<html>
|
3 |
<head>
|
4 |
<meta charset="utf-8"> |
5 |
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
6 |
<title></title>
|
7 |
<meta name="description" content=""> |
8 |
<meta name="viewport" content="width=device-width"> |
9 |
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script> |
10 |
|
11 |
</head>
|
12 |
<body>
|
13 |
|
14 |
<div id="gamepadPrompt"></div> |
15 |
<div id="gamepadDisplay"></div> |
16 |
|
17 |
<script>
|
18 |
var hasGP = false; |
19 |
var repGP; |
20 |
|
21 |
function canGame() { |
22 |
return "getGamepads" in navigator; |
23 |
}
|
24 |
|
25 |
function reportOnGamepad() { |
26 |
var gp = navigator.getGamepads()[0]; |
27 |
var html = ""; |
28 |
html += "id: "+gp.id+"<br/>"; |
29 |
|
30 |
for(var i=0;i<gp.buttons.length;i++) { |
31 |
html+= "Button "+(i+1)+": "; |
32 |
if(gp.buttons[i].pressed) html+= " pressed"; |
33 |
html+= "<br/>"; |
34 |
}
|
35 |
|
36 |
for(var i=0;i<gp.axes.length; i+=2) { |
37 |
html+= "Stick "+(Math.ceil(i/2)+1)+": "+gp.axes[i]+","+gp.axes[i+1]+"<br/>"; |
38 |
}
|
39 |
|
40 |
$("#gamepadDisplay").html(html); |
41 |
}
|
42 |
|
43 |
$(document).ready(function() { |
44 |
|
45 |
if(canGame()) { |
46 |
|
47 |
var prompt = "To begin using your gamepad, connect it and press any button!"; |
48 |
$("#gamepadPrompt").text(prompt); |
49 |
|
50 |
$(window).on("gamepadconnected", function() { |
51 |
hasGP = true; |
52 |
$("#gamepadPrompt").html("Gamepad connected!"); |
53 |
console.log("connection event"); |
54 |
repGP = window.setInterval(reportOnGamepad,100); |
55 |
});
|
56 |
|
57 |
$(window).on("gamepaddisconnected", function() { |
58 |
console.log("disconnection event"); |
59 |
$("#gamepadPrompt").text(prompt); |
60 |
window.clearInterval(repGP); |
61 |
});
|
62 |
|
63 |
//setup an interval for Chrome
|
64 |
var checkGP = window.setInterval(function() { |
65 |
console.log('checkGP'); |
66 |
if(navigator.getGamepads()[0]) { |
67 |
if(!hasGP) $(window).trigger("gamepadconnected"); |
68 |
window.clearInterval(checkGP); |
69 |
}
|
70 |
}, 500); |
71 |
}
|
72 |
|
73 |
});
|
74 |
</script>
|
75 |
</body>
|
76 |
</html>
|
Puedes probar la demo en Chrome o Firefox.
Supongo que todo esto es bastante autoexplicativo; La única parte realmente difícil era manejar los ejes. Hago un bucle sobre la matriz y cuento por dos para representar los valores de izquierda / derecha, arriba / abajo a la vez. Si abres esto en Firefox y conectas un DualShock, puede ver algo como esto.



Como se puede ver, el botón 2 fue presionado cuando tomé mi captura de pantalla. (En caso de que tengas curiosidad, ese era el botón X.) Tenga en cuenta las palancas; Mi gamepad estaba sentado en mi portátil y esos valores estaban fluctuando constantemente. No de una manera que implicaría que los valores eran malos, per se—si tome el mando del juego y empujé todo el camino en una dirección, vi el valor correcto. Pero creo que lo que estaba viendo era lo sensible que es el controlador para el entorno. O tal vez gremlins.
A continuación, se muestra un ejemplo de cómo Chrome lo muestra:



Yo estaba, otra vez, presionando el botón de X—pero observa cómo el índice del botón es diferente aquí. Como usted puede decir, usted va a necesitar hacer un poco de... masaje si quieres utilizar esta API para un juego. Me imagino que podría comprobar ambos botones 1 y 2 para el "disparo" y haces seguimiento con una buena cantidad de pruebas.
Poniendolo todo junto
Entonces, ¿qué tal una demo real? Al igual que la mayoría de los codificadores que comenzaron su vida jugando videojuegos, soñé con ser un creador de videojuegos importante cuando crecí. Resulta que las matemáticas se ponen muy difíciles después "cálculo", y al parecer esta cosa "web" tiene un futuro, así que mientras que ese futuro no se desarrolló para mí, todavía me gustaría imaginar que un día podría convertir estas habilidades estándares web en un juego jugable. Hasta ese día, lo que tengo hoy es una versión tonta de pong en canvas. Pong de un solo jugador. Como he dicho, tonto.
El juego simplemente hace una paleta y una bola, y le da el control del teclado sobre la pelota. Cada vez que te pierdas la pelota, la puntuación sube. Lo que tiene sentido para el golf en lugar de pong, supongo, pero no nos preocupemos demasiado. El código se puede encontrar en game1.html y puede jugar la demo en tu navegador.
No voy a pasar por todo el código aquí, pero veamos algunos fragmentos. En primer lugar, aquí está la función de bucle principal que maneja todos los detalles de la animación:
1 |
function loop() { |
2 |
draw.clear(); |
3 |
ball.move(); |
4 |
ball.draw(); |
5 |
paddle.draw(); |
6 |
paddle.move(); |
7 |
draw.text("Score: "+score, 10, 20, 20); |
8 |
}
|
La paleta es manejada por el teclado usando dos simples manejadores de eventos:
1 |
$(window).keydown(function(e) { |
2 |
switch (e.keyCode) { |
3 |
case 37: input.left = true; break; |
4 |
case 39: input.right = true; break; |
5 |
}
|
6 |
});
|
7 |
|
8 |
$(window).keyup(function(e) { |
9 |
switch (e.keyCode) { |
10 |
case 37: input.left = false; break; |
11 |
case 39: input.right = false; break; |
12 |
}
|
13 |
});
|
La variable de entrada input
es una variable global que es recogida por un método de movimiento move
de objeto paddle:
1 |
this.move = function() { |
2 |
if(input.left) { |
3 |
this.x -= this.speed; |
4 |
if(this.x < 0) this.x=0; |
5 |
}
|
6 |
if(input.right) { |
7 |
this.x += this.speed; |
8 |
if((this.x+this.w) > canvas.width) this.x=canvas.width-this.w; |
9 |
}
|
10 |
}
|
Una vez más, nada demasiado complejo. Aquí hay una captura de pantalla del juego en acción. (Ya sé—no debería dejar mi trabajo diario.)



Entonces, ¿cómo añadimos soporte de mando de juegos? Afortunadamente, ya tenemos el código hecho. En la demostración anterior, hicimos todo lo necesario para comprobar y recibir actualizaciones del código. Podemos tomar ese código y simplemente añadirlo al código existente del juego.
Dado que es (prácticamente) el mismo, no lo repetiré (aunque el listado completo está disponible si lo desea), pero compartiré el código modificado cada 100ms una vez que se detecte un mando de juegos:
1 |
function checkGamepad() { |
2 |
var gp = navigator.getGamepads()[0]; |
3 |
var axeLF = gp.axes[0]; |
4 |
if(axeLF < -0.5) { |
5 |
input.left = true; |
6 |
input.right = false; |
7 |
} else if(axeLF > 0.5) { |
8 |
input.left = false; |
9 |
input.right = true; |
10 |
} else { |
11 |
input.left = false; |
12 |
input.right = false; |
13 |
}
|
14 |
}
|
Una vez más, puede probar la demostración en cualquiera de los navegadores.
Al igual que con el ejemplo anterior, hemos asumido que sólo nos interesa un mando de juego. Dado que nuestro juego sólo tiene una paleta y sólo se mueve horizontalmente, podemos obtener por sólo verificando el primer eje. Recuerde, de acuerdo con la API esto debe ser el "más importante", y en mis pruebas fue la palanca izquierda, que es bastante estándar para los juegos.
Dado
que nuestro juego utiliza una variable global, input
, para representar
el movimiento izquierdo y derecho, todo lo que tengo que hacer es
modificar ese valor basado en el valor del eje. Ahora, tenga en cuenta que no sólamente comprobamos "menor que cero" y "mayor que cero". ¿Por qué? Si
te acuerdas en la demo anterior, el mando de juego era muy sensible, y a menudo
reportarían valores incluso cuando no pensaba que realmente había
movido la palanca. El uso de un valor límite de 0.5
da al control un poco más de estabilidad. (Y, obviamente, este es el tipo de cosa que tendrías que ajustar para ver que "se siente" bien.)
En general, he añadido aproximadamente 25 líneas de código a mi juego para agregar soporte de mando de juegos. Eso es genial.
¡A jugar!
Esperamos
que haya visto que, aunque definitivamente hay algunas idiosincrasias,
la API Gamepad ahora tiene soporte en dos grandes navegadores, y es
algo que creo que los desarrolladores realmente deberían empezar a
considerar para sus juegos.
Recursos
Estos son algunos recursos adicionales que le ayudarán a aprender más sobre la API Gamepad.
- Uso de la API Gamepad
- Especificaciones Gamepad
- Gamepad.js - Una biblioteca Javascript para permitir el uso de mandos de juegos y joysticks en el navegador.
- Controles de Gamepad para juegos HTML5
- Versión Wii U (totalmente diferente de la especificación - buen trabajo, Nintendo!)
Referencias
- Crédito de imagen: Video Game Controller diseñado por Uriel Sosa del Proyecto Noun