Imagina un personaje de juego llamado "Bob el Carnicero" parado solo en una habitación oscura mientras hordas de zombis de salchichas mutantes comienzan a entrar por las puertas y ventanas rotas. En este punto, sería una buena idea que Bob comience a convertir a los zombis de salchicha en pequeños trozos de carne, pero ¿cómo hará eso Bob en un juego multiplataforma? ¿El jugador del juego tendrá que presionar una o más teclas en un teclado, hacer clic con el mouse, tocar la pantalla o presionar un botón en un gamepad??
Cuando programes un juego multiplataforma, este es el tipo de cosas con las que probablemente pasarás mucho tiempo luchando si no estás preparado para ello. Si no eres cuidadoso, podrías terminar con un espagueti masivo Si
declaraciones o cambiar
Declaraciones para tratar con todos los diferentes dispositivos de entrada..
En este tutorial, vamos a simplificar mucho las cosas mediante la creación de una clase única que unificará múltiples dispositivos de entrada. Cada instancia de la clase representará una acción o comportamiento específico del juego (como "disparar", "correr" o "saltar") y se le puede decir que escuche varias teclas, botones y punteros en múltiples dispositivos de entrada.
Nota: El lenguaje de programación usado en este tutorial es JavaScript, pero la técnica que se usa para unificar múltiples dispositivos de entrada se puede transferir fácilmente a cualquier otro lenguaje de programación multiplataforma que proporcione API para dispositivos de entrada.
Antes de comenzar a escribir el código para la clase que vamos a crear en este tutorial, echemos un vistazo rápido a cómo podría usarse la clase..
// Crear una entrada para la acción "disparar". disparar = nuevo GameInput (); // Dile a la entrada a qué reaccionar. shoot.add (GameInput.KEYBOARD_SPACE); shoot.add (GameInput.GAMEPAD_RT); // Durante cada actualización del juego, verifique la entrada. función de actualización () si (disparar.valor> 0) // ¡Dile a Bob que dispare a los zombies de salchichas mutantes! else // Dile a Bob que deje de disparar.
GameInput
es la clase que crearemos, y puedes ver cuánto más simple hará las cosas. los disparar.valor
la propiedad es un número y será un valor positivo si el barra espaciadora en un teclado se presiona o la gatillo derecho En un gamepad se presiona. Si no se presiona la barra espaciadora ni el disparador derecho, el valor será cero.
Lo primero que debemos hacer es crear una función de cierre para el GameInput
clase. La mayoría del código que escribiremos no es en realidad parte de la clase, pero debe ser accesible desde dentro de la clase mientras permanezca oculto de todo lo demás. Una función de cierre nos permite hacer eso en JavaScript..
(En un lenguaje de programación como ActionScript o C #, simplemente puedes usar miembros de clase privados, pero eso no es un lujo que tenemos en JavaScript, desafortunadamente).
(function () // el código va aquí) ();
El resto del código en este tutorial reemplazará el comentario "el código va aquí".
El código solo necesita que se definan un puñado de variables fuera de las funciones, y esas variables son las siguientes.
var TECLADO = 1; var PUNTERO = 2; var GAMEPAD = 3; var DISPOSITIVO = 16; var CODE = 8; var __pointer = currentX: 0, currentY: 0, previousX: 0, previousY: 0, distanceX: 0, distanceY: 0, identifier: 0, moved: false, presionado: false; var __keyboard = ; var __inputs = []; var __channels = []; var __mouseDetected = false; var __touchDetected = false;
La constante-como TECLADO
, PUNTERO
, JUEGO
, DISPOSITIVO
y CÓDIGO
los valores se utilizan para definir canales de dispositivo de entrada, como GameInput.KEYBOARD_SPACE
, y su uso quedará claro más adelante en el tutorial..
los __puntero
objeto contiene propiedades que son relevantes para los dispositivos de entrada de ratón y pantalla táctil, y el __teclado
objeto se utiliza para realizar un seguimiento de los estados clave del teclado. los __inputs
y __canales
arrays se utilizan para almacenar GameInput
instancias y cualquier canal de dispositivo de entrada agregado a esas instancias. Finalmente, el __mouseDetected
y __tocadoDetectado
indicar si se ha detectado un ratón o una pantalla táctil.
Nota: Las variables no necesitan estar prefijadas con dos guiones bajos; esa es simplemente la convención de codificación que elegí usar para el código en este tutorial. Ayuda a separarlos de las variables definidas en funciones..
Aquí viene la mayor parte del código, por lo que es posible que desee tomar un café o algo antes de comenzar a leer esta parte.!
Estas funciones se definen después de las variables en la sección anterior de este tutorial, y se definen en orden de aparición.
// Inicializa el sistema de entrada. function main () // Exponer el constructor GameInput. window.GameInput = GameInput; // Añadir los oyentes del evento. addMouseListeners (); addTouchListeners (); addKeyboardListeners (); // Algunas acciones de UI que deberíamos evitar en un juego. window.addEventListener ("contextmenu", killEvent, true); window.addEventListener ("selectstart", killEvent, true); // Iniciar el ciclo de actualización. window.requestAnimationFrame (actualizar);
los principal()
La función se llama al final del código, es decir, al final del código Cierre de funciones que creamos anteriormente. Hace lo que dice en la lata y hace que todo funcione para que el GameInput
clase puede ser utilizada.
Una cosa que debería llamar su atención es el uso del requestAnimationFrame ()
Función, que es parte de la especificación de temporización de animación W3C. Los juegos y aplicaciones modernos utilizan esta función para ejecutar sus bucles de actualización o representación porque se ha optimizado altamente para ese propósito en la mayoría de los navegadores web.
// Actualiza el sistema de entrada. function update () window.requestAnimationFrame (update); // Actualizar primero los valores del puntero. updatePointer (); var i = __inputs.length; var input = null; var canales = nulo; while (i -> 0) input = __inputs [i]; canales = __canales [i]; if (input.enabled === true) updateInput (input, canales); else input.value = 0;
los actualizar()
La función rueda a través de la lista de activos GameInput
Instalar y actualizar los que estén habilitados. El seguimiento updateInput ()
la función es bastante larga, así que no agregaré el código aquí; Puede ver el código completo descargando los archivos de origen.
// Actualiza una instancia de GameInput. función updateInput (entrada, canales) // nota: vea los archivos de origen
los updateInput ()
La función examina los canales del dispositivo de entrada que se han agregado a un GameInput
instancia y funciona lo que el valor
propiedad de la GameInput
la instancia se debe establecer en. Como se ve en el Disparando las salchichas código de ejemplo, el valor
la propiedad indica si se está activando un canal de dispositivo de entrada, y eso permite que un juego reaccione en consecuencia, tal vez diciéndole a Bob que dispare a los zombies de salchichas mutantes.
// Actualiza el valor de una instancia de GameInput. función updateValue (entrada, valor, umbral) si (umbral! == no definido) si (valor < threshold ) value = 0; // The highest value has priority. if( input.value < value ) input.value = value;
los updateValue ()
función determina si el valor
propiedad de un GameInput
La instancia debe ser actualizada. los límite
se utiliza principalmente para evitar que los canales de entrada de dispositivos analógicos, como los botones y las palancas del gamepad, que no se reinician correctamente disparen constantemente GameInput
ejemplo. Esto sucede muy a menudo con gamepads defectuosos o sucios.
Como el updateInput ()
función, la siguiente updatePointer ()
La función es bastante larga, así que no agregaré el código aquí. Puede ver el código completo descargando los archivos de origen.
// Actualiza los valores del puntero. función updatePointer () // nota: vea los archivos de origen
los updatePointer ()
función actualiza las propiedades en el __puntero
objeto. En pocas palabras, la función fija la posición del puntero para asegurarse de que no abandona la ventana de visualización del navegador web y calcula la distancia que se ha movido desde la última actualización..
// Llamado cuando se detecta un dispositivo de entrada de mouse. función mouseDetected () if (__mouseDetected === false) __mouseDetected = true; // Ignorar eventos táctiles si se usa un mouse. removeTouchListeners (); // Llamado cuando se detecta un dispositivo de entrada de pantalla táctil. función touchDetected () if (__touchDetected === false) __touchDetected = true; // Ignore los eventos del mouse si se está utilizando una pantalla táctil. removeMouseListeners ();
los mouseDetected ()
y touchDetected ()
Las funciones le dicen al código que ignore un dispositivo de entrada u otro. Si se detecta un mouse antes de una pantalla táctil, la pantalla táctil se ignorará. Si se detecta una pantalla táctil antes de un mouse, el mouse será ignorado.
// Llamado cuando se presiona un dispositivo de entrada tipo puntero. function pointerPressed (x, y, identifier) __pointer.identifier = identifier; __punto.presionado = verdadero; punteroMoved (x, y); // Llamado cuando se libera un dispositivo de entrada tipo puntero. function pointerReleased () __pointer.identifier = 0; __punto.presionado = falso; // Llamado cuando se mueve un dispositivo de entrada tipo puntero. función punteroMoved (x, y) __pointer.currentX = x >>> 0; __punto.currenteY = y >>> 0; if (__pointer.moved === false) __pointer.moved = true; __pointer.previousX = __pointer.currentX; __pointer.previousY = __pointer.currentY;
los puntero presionado ()
, puntero liberado ()
y puntero
Las funciones manejan la entrada desde un mouse o una pantalla táctil. Las tres funciones simplemente actualizan las propiedades en el __puntero
objeto.
Después de esas tres funciones, tenemos un puñado de funciones de manejo de eventos de JavaScript estándar. Las funciones se explican por sí mismas, así que no agregaré el código aquí; Puede ver el código completo descargando los archivos de origen.
// Agrega un canal de dispositivo de entrada a una instancia de GameInput. función inputAdd (entrada, canal) var i = __inputs.indexOf (entrada); if (i === -1) __inputs.push (entrada); __channels.push ([canal]); regreso; var ca = __channels [i]; var ci = ca.indexOf (canal); if (ci === -1) ca.push (canal); // Elimina un canal de dispositivo de entrada a una instancia de GameInput. función inputRemove (input, channel) var i = __inputs.indexOf (input); if (i === -1) return; var ca = __channels [i]; var ci = ca.indexOf (canal); if (ci! == -1) ca.splice (ci, 1); if (ca.length === 0) __inputs.splice (i, 1); __channels.splice (i, 1); // Restablece una instancia de GameInput. function inputReset (input) var i = __inputs.indexOf (input); if (i! == -1) __inputs.splice (i, 1); __channels.splice (i, 1); input.value = 0; input.enabled = true;
los inputAdd ()
, inputRemove ()
y inputReset ()
Las funciones son llamadas desde un GameInput
instancia (ver más abajo). Las funciones modifican la __inputs
y __canales
Arreglos dependiendo de lo que se necesita hacer..
UNA GameInput
instancia se considera activa, y se añade a la __inputs
matriz, cuando se ha agregado un canal de dispositivo de entrada al GameInput
ejemplo. Si un activo GameInput
instancia tiene todos sus canales de dispositivo de entrada eliminados, el GameInput
instancia considerada inactiva y eliminada de la __inputs
formación.
Ahora llegamos a la GameInput
clase.
// Constructor GameInput. function GameInput () GameInput.prototype = value: 0, enabled: true, // Agrega un canal de dispositivo de entrada. agregar: función (canal) inputAdd (este, canal); , // Elimina un canal de dispositivo de entrada. remove: function (channel) inputRemove (this, channel); , // Elimina todos los canales del dispositivo de entrada. reset: function () inputReset (this); ;
Sí, eso es todo lo que hay, es una clase súper liviana que esencialmente actúa como una interfaz para el código principal. los valor
La propiedad es un número que va desde 0
(cero) hasta 1
(uno). Si el valor es 0
, significa que el GameInput
La instancia no recibe nada de ningún canal de dispositivo de entrada que se le haya agregado..
los GameInput
La clase tiene algunas propiedades estáticas, por lo que agregaremos esas ahora.
// La posición X del puntero dentro de la ventana de visualización. GameInput.pointerX = 0; // La posición Y del puntero dentro de la ventana de visualización. GameInput.pointerY = 0; // La distancia que el puntero tiene que moverse, en píxeles por cuadro, para // hacer que el valor de una instancia de GameInput sea igual a 1.0. GameInput.pointerSpeed = 10;
Canales de dispositivo de teclado:
GameInput.KEYBOARD_A = TECLADO << DEVICE | 65 << CODE; GameInput.KEYBOARD_B = KEYBOARD << DEVICE | 66 << CODE; GameInput.KEYBOARD_C = KEYBOARD << DEVICE | 67 << CODE; GameInput.KEYBOARD_D = KEYBOARD << DEVICE | 68 << CODE; GameInput.KEYBOARD_E = KEYBOARD << DEVICE | 69 << CODE; GameInput.KEYBOARD_F = KEYBOARD << DEVICE | 70 << CODE; GameInput.KEYBOARD_G = KEYBOARD << DEVICE | 71 << CODE; GameInput.KEYBOARD_H = KEYBOARD << DEVICE | 72 << CODE; GameInput.KEYBOARD_I = KEYBOARD << DEVICE | 73 << CODE; GameInput.KEYBOARD_J = KEYBOARD << DEVICE | 74 << CODE; GameInput.KEYBOARD_K = KEYBOARD << DEVICE | 75 << CODE; GameInput.KEYBOARD_L = KEYBOARD << DEVICE | 76 << CODE; GameInput.KEYBOARD_M = KEYBOARD << DEVICE | 77 << CODE; GameInput.KEYBOARD_N = KEYBOARD << DEVICE | 78 << CODE; GameInput.KEYBOARD_O = KEYBOARD << DEVICE | 79 << CODE; GameInput.KEYBOARD_P = KEYBOARD << DEVICE | 80 << CODE; GameInput.KEYBOARD_Q = KEYBOARD << DEVICE | 81 << CODE; GameInput.KEYBOARD_R = KEYBOARD << DEVICE | 82 << CODE; GameInput.KEYBOARD_S = KEYBOARD << DEVICE | 83 << CODE; GameInput.KEYBOARD_T = KEYBOARD << DEVICE | 84 << CODE; GameInput.KEYBOARD_U = KEYBOARD << DEVICE | 85 << CODE; GameInput.KEYBOARD_V = KEYBOARD << DEVICE | 86 << CODE; GameInput.KEYBOARD_W = KEYBOARD << DEVICE | 87 << CODE; GameInput.KEYBOARD_X = KEYBOARD << DEVICE | 88 << CODE; GameInput.KEYBOARD_Y = KEYBOARD << DEVICE | 89 << CODE; GameInput.KEYBOARD_Z = KEYBOARD << DEVICE | 90 << CODE; GameInput.KEYBOARD_0 = KEYBOARD << DEVICE | 48 << CODE; GameInput.KEYBOARD_1 = KEYBOARD << DEVICE | 49 << CODE; GameInput.KEYBOARD_2 = KEYBOARD << DEVICE | 50 << CODE; GameInput.KEYBOARD_3 = KEYBOARD << DEVICE | 51 << CODE; GameInput.KEYBOARD_4 = KEYBOARD << DEVICE | 52 << CODE; GameInput.KEYBOARD_5 = KEYBOARD << DEVICE | 53 << CODE; GameInput.KEYBOARD_6 = KEYBOARD << DEVICE | 54 << CODE; GameInput.KEYBOARD_7 = KEYBOARD << DEVICE | 55 << CODE; GameInput.KEYBOARD_8 = KEYBOARD << DEVICE | 56 << CODE; GameInput.KEYBOARD_9 = KEYBOARD << DEVICE | 57 << CODE; GameInput.KEYBOARD_UP = KEYBOARD << DEVICE | 38 << CODE; GameInput.KEYBOARD_DOWN = KEYBOARD << DEVICE | 40 << CODE; GameInput.KEYBOARD_LEFT = KEYBOARD << DEVICE | 37 << CODE; GameInput.KEYBOARD_RIGHT = KEYBOARD << DEVICE | 39 << CODE; GameInput.KEYBOARD_SPACE = KEYBOARD << DEVICE | 32 << CODE; GameInput.KEYBOARD_SHIFT = KEYBOARD << DEVICE | 16 << CODE;
Canales de dispositivo puntero:
GameInput.POINTER_UP = POINTER << DEVICE | 0 << CODE; GameInput.POINTER_DOWN = POINTER << DEVICE | 1 << CODE; GameInput.POINTER_LEFT = POINTER << DEVICE | 2 << CODE; GameInput.POINTER_RIGHT = POINTER << DEVICE | 3 << CODE; GameInput.POINTER_PRESS = POINTER << DEVICE | 4 << CODE;
Canales de dispositivos gamepad:
GameInput.GAMEPAD_A = GAMEPAD << DEVICE | 0 << CODE; GameInput.GAMEPAD_B = GAMEPAD << DEVICE | 1 << CODE; GameInput.GAMEPAD_X = GAMEPAD << DEVICE | 2 << CODE; GameInput.GAMEPAD_Y = GAMEPAD << DEVICE | 3 << CODE; GameInput.GAMEPAD_LB = GAMEPAD << DEVICE | 4 << CODE; GameInput.GAMEPAD_RB = GAMEPAD << DEVICE | 5 << CODE; GameInput.GAMEPAD_LT = GAMEPAD << DEVICE | 6 << CODE; GameInput.GAMEPAD_RT = GAMEPAD << DEVICE | 7 << CODE; GameInput.GAMEPAD_START = GAMEPAD << DEVICE | 8 << CODE; GameInput.GAMEPAD_SELECT = GAMEPAD << DEVICE | 9 << CODE; GameInput.GAMEPAD_L = GAMEPAD << DEVICE | 10 << CODE; GameInput.GAMEPAD_R = GAMEPAD << DEVICE | 11 << CODE; GameInput.GAMEPAD_UP = GAMEPAD << DEVICE | 12 << CODE; GameInput.GAMEPAD_DOWN = GAMEPAD << DEVICE | 13 << CODE; GameInput.GAMEPAD_LEFT = GAMEPAD << DEVICE | 14 << CODE; GameInput.GAMEPAD_RIGHT = GAMEPAD << DEVICE | 15 << CODE; GameInput.GAMEPAD_L_UP = GAMEPAD << DEVICE | 16 << CODE; GameInput.GAMEPAD_L_DOWN = GAMEPAD << DEVICE | 17 << CODE; GameInput.GAMEPAD_L_LEFT = GAMEPAD << DEVICE | 18 << CODE; GameInput.GAMEPAD_L_RIGHT = GAMEPAD << DEVICE | 19 << CODE; GameInput.GAMEPAD_R_UP = GAMEPAD << DEVICE | 20 << CODE; GameInput.GAMEPAD_R_DOWN = GAMEPAD << DEVICE | 21 << CODE; GameInput.GAMEPAD_R_LEFT = GAMEPAD << DEVICE | 22 << CODE; GameInput.GAMEPAD_R_RIGHT = GAMEPAD << DEVICE | 23 << CODE;
Para finalizar el código, simplemente necesitamos llamar al principal()
función.
// Inicializar el sistema de entrada. principal();
Y eso es todo el código. Una vez más, todo está disponible en los archivos de origen.
Antes de concluir el tutorial con una conclusión, veamos un ejemplo más de cómo GameInput
clase puede ser utilizada. Esta vez, le daremos a Bob la capacidad de moverse y saltar porque las hordas de zombis de salchichas mutantes podrían ser demasiado para él solo..
// Crear las entradas. var jump = nuevo GameInput (); var moveLeft = nuevo GameInput (); var moveRight = nuevo GameInput (); // Indica a las entradas a qué reaccionar. jump.add (GameInput.KEYBOARD_UP); jump.add (GameInput.KEYBOARD_W); jump.add (GameInput.GAMEPAD_A); moveLeft.add (GameInput.KEYBOARD_LEFT); moveLeft.add (GameInput.KEYBOARD_A); moveLeft.add (GameInput.GAMEPAD_LEFT); moveRight.add (GameInput.KEYBOARD_RIGHT); moveRight.add (GameInput.KEYBOARD_D); moveRight.add (GameInput.GAMEPAD_RIGHT); // Durante cada actualización del juego, revisa las entradas. función update () if (jump.value> 0) // Dile a Bob que salte. else // Dile a Bob que deje de saltar. if (moveLeft.value> 0) // Dígale a Bob que se mueva / corra hacia la izquierda. else // Dígale a Bob que deje de moverse a la izquierda. if (moveRight.value> 0) // Dígale a Bob que se mueva / corra a la derecha. else // Dile a Bob que deje de moverse a la derecha.
Bonito y fácil. Tenga en cuenta que la valor
propiedad de GameInput
las instancias van desde 0
a través de 1
, así que podríamos hacer algo como cambiar la velocidad de movimiento de Bob usando ese valor si uno de los canales del dispositivo de entrada activo es analógico.
if (moveLeft.value> 0) bob.x - = bob.maxDistancePerFrame * moveLeft.value;
Que te diviertas!
Todos los juegos multiplataforma tienen una cosa en común: todos tienen que lidiar con una multitud de dispositivos de entrada (controladores), y tratar con esos dispositivos de entrada puede convertirse en una tarea desalentadora. Este tutorial ha demostrado una forma de manejar múltiples dispositivos de entrada con el uso de una API simple y unificada..