Configurando un juego multijugador con Socket.io | hyperPad Documentation

Loading...

Logo

z-f2VkDQ.jpg

Parte 1: Introducción

Prerequisitos Preferidos

En este tutorial, nos enfocaremos en cómo configurar un servidor y conectar tu juego a él para que puedas habilitar funciones de multijugador para tu proyecto a través de comportamientos de Socket.io. Vamos a cubrir funcionalidades y comportamientos avanzados en hyperPad y se recomienda encarecidamente revisar algunos otros tutoriales primero y tener una buena comprensión del software antes de continuar.

Este tutorial asume que tienes una buena comprensión de la funcionalidad principal de hyperPad, así como un entendimiento mínimo de la programación escrita, ya que nos enfocaremos en scripting de Javascript para construir un servidor de juego. Este tutorial también asumirá un entendimiento básico de redes y las diferencias entre un servidor y sus clientes.

El objetivo principal de este tutorial es enseñarte lo que necesitas hacer para añadir funcionalidad de multijugador a tu juego; no para crear el juego en sí. Exploraremos la relación entre los clientes de Socket.io y el servidor, y cómo se comunican entre sí con los comportamientos de Socket.io.

Requisitos

Para crear y alojar el servidor, necesitarás una computadora capaz de ejecutar Node.JS (Mac, Windows, etc). La instalación de Node.JS y su configuración para nuestro propósito se cubrirán en este tutorial. Si estás alojando un servidor en tu red doméstica, probablemente necesitarás cambiar la configuración de reenvío de puertos de tu puerta de enlace si deseas tener conexiones entrantes desde fuera de tu red local.

Para este tutorial, ya hemos creado un juego de demostración sencillo. En él, tenemos dos jugadores jugando a un juego de etiqueta en un pequeño laberinto. Exploraremos cómo se utilizan los comportamientos de Socket.io en el proyecto de ejemplo, por lo que se recomienda que descargues y abras el proyecto en hyperPad.

El proyecto de ejemplo completado en hyperPad puede ser descargado aquí: Multiplayer tag tutorial.tap

Visión General

En este tutorial, repasaremos lo básico de crear un servidor para tu juego. El servidor manejará la mayoría de los detalles en el juego, como la puntuación, los lobbys de juego, etc. Luego, crearemos comportamientos que envíen información de nuestro juego al servidor y viceversa.

Aquí está el flujo general de nuestro juego:

1. Desde el menú principal, nos conectaremos a nuestro servidor y dejaremos que el usuario decida crear o unirse a un juego.

a. Si se crea un juego, carga la sala de espera.

b. Si se une a un juego, carga una lista de salas de juego disponibles.

c. Cuando dos jugadores están en una sala de espera, el juego se lanza.

2. En un juego, el servidor coloca aleatoriamente a los jugadores en una de las cuatro áreas y asigna a uno de ellos como "Es".

a. Etiquetar al otro jugador aleatoriza sus ubicaciones, intercambia el estado de "Es" y añade un punto al jugador que etiquetó al otro.

b. Si no se producen etiquetas dentro de un cierto período de tiempo, el servidor eliminará 1 punto del jugador que actualmente es "Es" e intercambiará el estado de "Es" antes de colocar a los jugadores en las zonas de aparición nuevamente.

3. Una vez que un jugador ha alcanzado un cierto número de puntos, se cargará una superposición declarando un ganador. Luego, los jugadores se desconectarán de la sala y regresarán al menú principal.

Parte 2: Configurando el Servidor

Crear un servidor de Socket.io requiere que ejecutemos una aplicación de Javascript utilizando la biblioteca de Socket.io, escuchando las conexiones de los jugadores. Socket.io es una biblioteca de redes de Javascript que simplifica gran parte de los trabajos internos de la construcción de una aplicación en red para nosotros. Más detalles a seguir.

Descargar e Instalar Node.JS

Para empezar, dirígete a Node.JS y descárgalo en la computadora (Mac, Windows, etc.) en la que deseas alojar el servidor. Cuando termine la descarga, ejecuta el instalador y sigue sus instrucciones. Todas las opciones durante la instalación pueden dejarse por defecto. Node.JS nos brinda la capacidad de ejecutar aplicaciones Javascript por sí solas sin la necesidad de un navegador web.

Descargando y Ejecutando el Servidor de Ejemplo

A continuación, descarga el Ejemplo de Servidor Multijugador de este tutorial en GitHub:

https://github.com/hyperPad/multiplayerServerExample

Haz clic en el botón "Clonar o descargar" y selecciona "Descargar ZIP". Esto descargará una copia del código para el ejemplo del servidor.

chrome_2019-08-05_14-22-14.png

Extrae el ZIP descargado. Dentro encontrarás algunos archivos pequeños, pero el archivo más notable aquí es el archivo "index.js" el cual es el código para nuestro servidor. A continuación, abre tu línea de comandos/terminal dentro de la carpeta del ejemplo del servidor.

Escribe "npm install" y presiona enter, luego déjalo correr. npm es un gestor de paquetes que lee el archivo package.json y descarga los paquetes necesarios para nuestro servidor. (¡Incluyendo Socket.io!)

cmd_2019-08-05_14-55-19.png

Cuando el comando haya terminado, tenemos todo lo que necesitamos para ejecutar el servidor. En la terminal, escribe "node ." y el servidor comenzará.

cmd_2019-08-05_15-02-38.png

¡Eso es todo! Nuestro servidor ahora está escuchando en el puerto 3000 para conexiones Socket.io entrantes.

"node ." lanza Node.JS para el directorio actual, donde buscará el archivo índice del directorio y lo ejecutará. Por defecto, este es el archivo "index.js", en el cual se localiza la mayor parte del código de nuestro servidor y que será analizado a lo largo de este tutorial.

Deja la terminal abierta, ya que cerrarla también cerrará el servidor. Verás que imprime mensajes cuando los jugadores se conectan o desconectan, y cuando se crean o destruyen las salas, y otros eventos.

Nota: Para permitir conexiones entrantes desde fuera de una red doméstica, probablemente necesitarás abrir el puerto 3000 en tu puerta de enlace doméstica. Este proceso varía de red a red, pero generalmente se puede encontrar una guía haciendo una búsqueda en internet para una guía de reenvío de puertos para el módem/router de tu hogar. Es posible que necesites cerrar y reiniciar el servidor de Node.JS cuando cambies la configuración de reenvío de puertos.

Parte 3: Conectándose al Servidor

Una vez que tengas lo básico del servidor iniciado, ahora podemos conectarnos a él en un juego de hyperPad. Esto debe configurarse como lo primero que ocurre en tu proyecto, independientemente de qué escena estés (ya que todo necesita comunicarse con el servidor). Es mejor adjuntar este comportamiento a un objeto en la Capa Global para que se aplique en todas las escenas.

Untitled.jpg

En el proyecto de ejemplo, la etiqueta de la Capa Global "Servidor" contiene lo anterior. (La descarga del proyecto se puede encontrar en la Parte 1 de este tutorial en la sección de Requisitos).

Estos dos comportamientos son realmente todo lo que necesitas para conectarte al servidor. Primero, en la pestaña personalizada, agarra el comportamiento del Cliente de Socket.io y colócalo. Aquí, introducirás la URL de tu servidor bajo la pestaña URL en su ventana de propiedades. En la imagen de arriba, la URL de nuestro servidor estaba en "http://192.168.0.191:3000", incluyendo el protocolo y el puerto. Querrás cambiar esto para que coincida con la URL de tu servidor o es probable que no funcione cuando inicies el juego.

Ahora tenemos la información del servidor que queremos, pero aún necesitamos conectarnos a él. Entonces, todo lo que necesitamos es el comportamiento de Conectar a Socket. Coloca uno, abre sus propiedades y selecciona el cliente en el cuadro vacío y establece la propiedad Función en "Conectar".

Ahora, cuando nuestro proyecto se carga, nos conectaremos automáticamente a nuestro servidor.

mceclip1.png

Parte 4: Crear y Unirse a Salas

El siguiente paso es crear nuestros lobbys de juego donde los jugadores pueden unirse y jugar juntos.

mceclip2.png

Aquí está nuestra sencilla pantalla de Menú Principal. Todo lo que se necesita hacer es tocar uno de los botones para iniciar una sala o buscar salas disponibles.

Creando Salas

Veamos cómo nos comunicamos con el servidor para crear nuestra sala.

mceclip3.png

¿No es bastante sencillo? Entonces, para repasar esto rápidamente, una vez que toquemos nuestro botón, se nos pedirá que escribamos un nombre para la sala. Una vez que esto esté hecho, ese nombre se emitirá al servidor donde se crea la sala y cargamos la escena de sala de espera.

A partir de aquí, utilizaremos Emit a Socket a menudo. Eso es porque es nuestra forma principal de enviar datos al servidor. Piénsalo simplemente como la "versión en línea" de Broadcast Message y Receive Message (Emit a Socket realiza ambas acciones a la vez).

Ahora, necesitamos añadir algo de información a nuestro servidor para que cree la sala una vez que reciba el mensaje de nuestro comportamiento Emit.

mceclip4.png

socket.on('createRoom', (roomName, callback) => {

Esta línea crea un evento de socket (como una expresión lambda) en el servidor, escuchando Emisiones con el evento 'createRoom'.

El primer parámetro es el valor que pasamos a través del comportamiento Emit a Socket, en este caso, el nombre de la sala que el usuario escribió. Aquí, hemos nombrado a ese parámetro 'roomName'.

El segundo parámetro es una función que llamamos más tarde en el evento de socket para señalar al cliente. El comportamiento Emit a Socket solo continuará la ejecución si se llama a la función de retorno (callback) en el servidor. Aquí, hemos nombrado ese parámetro 'callback'.

const room = {
id: uuid(),
name: roomName,
sockets: []
};

Esto creará una instancia de una estructura que contendrá información esencial sobre una sala.

'id' será un identificador único generado por la función de utilidad 'uuid()'. Esto nos ayudará a identificar específicamente esta sala contra una lista de muchas otras salas creadas más tarde.

'name' se establecerá con el nombre que el usuario escribió anteriormente.

'sockets' se inicializará como un arreglo vacío. Más adelante, esto servirá para rastrear los sockets de los jugadores conectados a esa sala.

rooms[room.id] = room;

'rooms' es una lista global de salas que están actualmente activas. Como estamos creando una nueva sala, la almacenaremos en la lista por su ID.

joinRoom(socket, room);

Esto llamará a la función global 'joinRoom' (línea 29), que añadirá el socket del jugador a la lista 'sockets' de la sala. Dado que el jugador creó la sala, esto también lo unirá a ella.

callback();
});

Por último, invocamos la función de retorno para que el cliente sea notificado que la sala se ha creado. Esto permitirá que Emit a Socket continúe la ejecución, lo que cargará a continuación la escena de la Sala de Espera. Esto también marca el final del evento de socket 'createRoom'.

Uniéndose a Salas

Unirse a salas es un poco diferente ya que necesitaremos acceder a la información desde una escena diferente desde el botón.

mceclip5.png

Para unirse a salas, hemos colocado el comienzo de nuestros comportamientos en nuestra capa global junto a donde nos conectamos al servidor. De esta manera, podemos acceder a la información desde cualquiera de nuestras escenas, en este caso, nuestra lista de salas. Para el botón, tocarlo simplemente cargará al jugador en la escena de lista de salas.

Aquí tenemos nuestro segundo comportamiento para comunicarnos con el servidor. El Evento de Socket se puede pensar como Recibir Mensaje, ya que solo se activará una vez que el mensaje establecido haya sido emitido desde el servidor.

La mejor forma de pensar al usar el Evento de Socket y Emit a Socket es que el Evento de Socket reacciona solo a la información proveniente del servidor, mientras que Emit a Socket se llama en reacción a cosas realizadas localmente.

En cuanto a nuestra lógica, una vez que nos conectemos al servidor, emitiremos la solicitud 'getRoomNames' para obtener los nombres de las salas disponibles.

mceclip19.png

Luego, estableceríamos una etiqueta que el jugador pueda tocar para entrar.

mceclip6.png

Esta es nuestra escena de Lista de Salas. Aquí es donde se cargarán y mostrarán todas las salas de juego disponibles. Simplemente tocando el nombre de la sala se cargará al jugador. Gracias a nuestros comportamientos previos, la lista se cargará automáticamente con cualquier sala abierta y generará las etiquetas de nombres de sala en pantalla. Si no aparece nada, tenemos nuestro botón de Actualizar Lista que simplemente repite los comportamientos para cargarla una vez más.

mceclip7.png mceclip8.png

Aquí tenemos la lógica para configurar la lista. No nos vamos a adentrar demasiado aquí porque solo queremos saber cómo esto se conecta con nuestro servidor.

Para empezar, tenemos nuestro comportamiento Emit a Socket. Esto solicita al servidor que envíe información sobre las salas disponibles. Desde allí, tenemos un comportamiento Obtener Valor de Arreglo. Todos los datos que provengan del servidor serán enviados como un Arreglo y la información necesaria estará en los primeros valores. Así que, establecemos nuestro Obtener Valor de Arreglo para obtener el valor en el índice 0. Desde allí, nuestros comportamientos extraerán esos datos y crearán una etiqueta para cada sala, generándolas en nuestra pantalla.

A continuación, vamos a comprobar el objeto al que necesitamos tocar, en nuestra escena es la etiqueta llamada Nombre de Sala.

mceclip9.png

Este texto actúa como nuestro botón cuando se genera, pero aún necesitamos adjuntar el ID de la sala a la que queremos conectarnos. Para hacerlo, primero debemos obtener el ID de la sala, y lo hacemos con el comportamiento Obtener Atributo y lo establecemos como dinámico. Luego, emitimos al servidor que queremos unirnos a esta sala y cargar a la sala de espera.

mceclip10.png

socket.on('joinRoom', (roomId, callback) => {

Este es el punto de entrada para el evento de socket 'joinRoom'. El valor del primer parámetro sería el ID de la sala a la que queremos unirnos que se emitió al servidor. Aquí, nombramos al parámetro 'roomId'.

const room = rooms[roomId];
joinRoom(socket, room);

Utilizando el roomId dado por el cliente, podemos encontrar la instancia de sala correcta en el servidor. Con eso, invocamos la función global 'joinRoom' (línea 29) con el socket del jugador que quiere conectarse, y la instancia de la sala. Echemos un vistazo a la función 'joinRoom' en un momento.

callback();
});

Por último, invocamos la función de retorno para notificar al cliente que continúe cargando la escena de la Sala de Espera, marcando el final del evento de socket 'joinRoom'.

Entonces, ¿qué está sucediendo exactamente en la función 'joinRoom'? Veamos.

mceclip11.png

room.sockets.push(socket);

Como se mencionó anteriormente, el arreglo 'room.sockets' rastrea los sockets conectados en una sala. Esta línea hace exactamente eso al agregar el socket al arreglo.

socket.join(room.id, () => {
socket.roomId = room.id;
console.log(socket.id, "Unido", room.id);
});

Esta es la llamada oficial para conectar al cliente a una sala. Primero le decimos al socket que se una a una sala por su ID. Cuando eso se complete, se invoca el siguiente callback, donde adjuntamos el ID de la sala al socket. Por último, registramos en la consola que un jugador se ha unido a una sala.

mceclip12.png

La Sala de Espera

La sala de espera es simplemente una escena a la que cargamos a los jugadores mientras esperan que otros jugadores se unan o para que comience un juego.

mceclip13.png

Al entrar en la sala, emitiremos un mensaje 'ready' al servidor. Una vez que el servidor haya recibido dos de estos, enviará un mensaje de que está comenzando el juego ('initGame'). Captamos ese mensaje con nuestro Evento de Socket y así, cargamos nuestro nivel del juego. En cuanto a nuestros comportamientos, eso es todo. Puedes agregar un botón que te desconecte de la sala y te envíe de regreso al menú principal si lo deseas.

En el lado del servidor, analicemos el código para ver qué está ocurriendo allí.

mceclip14.png

Este es el evento 'ready' que se llama cuando un cliente se ha unido a una sala y está listo para conectarse.

const room = rooms[socket.roomId];

Dado que hemos adjuntado el ID de la sala al socket, podemos obtener la sala para verificar si el juego puede comenzar.

if (room.sockets.length == 2) {

Aquí verificamos si ahora hay dos jugadores esperando en la sala. Supongamos que eso es cierto, y continuamos para comenzar el juego.

for (const client of room.sockets) {
client.emit('initGame');
}

Ahora que hay dos jugadores, iteramos a través de cada socket y emitimos el evento 'initGame' para que cada cliente cargue la escena del Nivel, como se mostró anteriormente.

Parte 5: Jugabilidad

Ahora, para entrar en la parte importante. Aquí es donde se realizará el 90% de nuestro trabajo. A continuación, está nuestro nivel de juego diseñado para este tutorial.

mceclip15.png

Antes de sumergirnos en eso, tenemos una etiqueta titulada "Lógica de Juego", echemos un vistazo.

mceclip16.png

¡Vaya, eso es un montón de comportamientos! No te preocupes, esto es simplemente cómo hacemos aparecer a nuestros jugadores en el juego. Veamos más de cerca;

mceclip17.png

Comenzamos con un comportamiento Emit a Socket que le dice al servidor que nuestro juego ha comenzado, con el evento 'startGame'. Luego agarramos el arreglo que devuelve el servidor, tomamos su primer valor y con el Obtener Valor de Diccionario, obtenemos los diversos atributos que nuestro objeto necesitará. Un árbol separado hace lo mismo, excepto para el jugador oponente. Por último, emitimos el mensaje 'init' a nuestro objeto jugador para comenzar.

Veamos qué está sucediendo en el lado del servidor cuando emitimos el evento 'startGame'.

mceclip20.png

La primera mitad de este evento de socket que ves aquí está configurando algunos valores iniciales en cada cliente, luego añade a cualquier cliente que no sea el cliente emisor al arreglo local 'otros'.

mceclip21.png

En la segunda mitad, se crea una lista de diccionario local llamada 'ack'. En ella, tenemos información sobre nosotros mismos y el otro cliente(s). Luego enviamos esa información de vuelta al cliente pasando nuestro diccionario 'ack' a la función de retorno, que se convierte en el valor resultante del comportamiento Emit a Socket que llamamos.

Después de eso, se hace una llamada de timeout de 5 segundos para finalmente comenzar la ronda, ahora que todos tienen toda la información que necesitan para jugar el juego. La función 'beginRound' (línea 99) controla lógica específica del juego para este proyecto. No profundizaremos demasiado en ello, pero esencialmente maneja dónde hacer aparecer a los jugadores, verifica las puntuaciones y también informa a los clientes quién es "Es".

Como se mencionó antes, el mensaje 'init' se llama en nuestra etiqueta "Lógica de Juego" cuando todo está listo. En el objeto jugador, ahora veremos los comportamientos de él donde recibe el mensaje 'init'.

mceclip22.png

Aquí puedes ver que tenemos varios árboles de comportamiento en nuestro jugador. Primero, comenzaremos con el árbol superior izquierdo.

mceclip23.png

Este es el comportamiento que prácticamente inicia todo lo demás en nuestro juego.

Primero, recibimos el mensaje 'init' enviado desde la etiqueta "Lógica de Juego". A partir de ahí, obtenemos el ID del servidor de nuestro objeto y activamos uno de nuestros Eventos de Socket así como configuramos nuestra pantalla de juego para seguir con precisión a nuestro personaje.

Sincronización de Movimiento

mceclip24.png

Aquí hay uno de nuestros comportamientos más importantes. Este pequeño árbol está diseñado para actualizar la posición de nuestro jugador en el servidor cada vez que movemos el joystick. Probablemente hayas notado que también está haciendo referencia a algunos valores de diccionario. Obtenemos esos valores de un comportamiento de diccionario independiente que contiene las posiciones X e Y de nuestro jugador.

Veamos el evento 'moved' en el servidor.

mceclip25.png

data = JSON.parse(data);

Los diccionarios, cuando son enviados desde un cliente a un servidor, tienen que ser analizados para poder leer los datos fácilmente. Esto se debe a que los diccionarios en hyperPad se codifican en una estructura JSON cuando se emiten a un servidor. Esta línea analiza la estructura en forma de cadena JSON y almacena la lista de diccionario de vuelta en la misma variable local 'data'.

socket.x = data.x;
socket.y = data.y;

Aquí, actualizamos las posiciones X e Y almacenadas en el socket con los nuevos valores proporcionados por el cliente.

for (const client of room.sockets) {
if (client == socket) {
continue;
}
client.emit(socket.id, {
x: socket.x,
y: socket.y,
score: socket.score,
isIt: socket.isIt
});
}

A continuación, iteramos a través de todos los clientes para actualizarles sobre nuestra nueva posición y otros detalles, excluyendo a nosotros mismos (es decir, el cliente emisor no necesita conocer su propia posición).

mceclip26.png

Este árbol aquí controla la mayor parte de nuestro juego. Usamos un Evento de Socket cuando el servidor está verificando qué jugador está etiquetado como "Es", que es emitido por 'beginRound' (línea 99) en el servidor.

Luego, obtenemos el ID del servidor de nuestro objeto del arreglo y usamos el Valor de Diccionario para desglosarlo en las diversas piezas de datos que contiene. Desde allí, obtenemos nuestra Puntuación, si estamos etiquetados como "Es", así como las posiciones x e y de nuestro objeto y aplicamos esos atributos al objeto. El resto de los comportamientos es para establecer y controlar la interfaz de usuario en nuestro juego.

Conclusión

Hubo mucho que asimilar aquí, pero espero que si llegaste tan lejos, deberías tener una comprensión de cómo utilizar los comportamientos de Socket.io para crear experiencias multijugador para tus jugadores.

¡Inténtalo tú mismo! Toma un juego existente que hayas creado e intenta darle alguna funcionalidad en línea, como una escena de tabla de puntajes que se conecte a un servidor y solicite una lista de los 10 mejores puntajes con nombres de jugadores para mostrar.

Es difícil enseñar un lenguaje de scripting como Javascript en un solo artículo. Afortunadamente, si tienes problemas hay muchos otros recursos para ayudarte a escribir aplicaciones Javascript para Node.JS y Socket.io;

Aprende Javascript - https://developer.mozilla.org/bm/docs/Web/JavaScript

Aprende Socket.io - https://socket.io/docs/

Aprende Node.JS - https://nodejs.org/en/docs/