
Introduccion
La historia comenzó con un hackathon basado en blockchain. Al comienzo del evento, conocí a un hombre que crea juegos de mesa como un pasatiempo (estaba en la prueba de uno de esos juegos), nos unimos y encontramos un equipo con el que "cegaron" un simple juego estratégico durante el fin de semana. El hackatón pasó, pero el entusiasmo permaneció. Y se nos ocurrió la idea de un juego de cartas multijugador sobre la felicidad, la comunidad mundial y las elecciones.
En la serie de artículos, reflejaremos nuestro camino para crear un juego, con una descripción del rastrillo que ya hemos pisado y lo haremos a medida que avancemos.
Debajo del corte estará:
- Resumen del juego
- Cómo se tomó la decisión sobre qué hacer backend. ¿Dónde va a "vivir" para que no lo pague en la etapa de desarrollo?
- Primeros pasos en el desarrollo: autenticación de jugadores y organización de emparejamiento
- Planes adicionales
¿De qué trata el juego?
La humanidad está cansada de las guerras mundiales, el agotamiento de los recursos y la competencia constante. Las facciones clave acordaron usar tecnología moderna para seleccionar un solo liderazgo. A la hora señalada, el electorado mundial debe decidir la elección de una fracción que regirá el planeta durante el próximo milenio. Las facciones clave participan en una lucha de poder "honesta". En una sesión de juego, cada jugador representa una fracción.
Este juego de cartas trata sobre elecciones. Cada facción tiene un presupuesto para conducir la carrera electoral, fuentes de ingresos que aumentan el presupuesto y comienzan los votos. Al comienzo del juego, el mazo con cartas de acción se mezcla y se entregan 4 cartas a cada participante. Cada turno, los jugadores pueden realizar hasta dos acciones de juego. Para usar la tarjeta, el jugador la pone sobre la mesa y, si es necesario, designa la meta y deduce del presupuesto el costo de usar la tarjeta. Después del final de la ronda, el jugador solo puede quedarse con una de las cartas no utilizadas. Al comienzo de cada ronda, los jugadores obtienen cartas del mazo, de modo que al comienzo de cada ronda, cada jugador tiene 4 cartas de acción disponibles.
Al final de las rondas 3, 6 y 9, el jugador con el menor número de votos es eliminado del juego. Si varios jugadores tienen el mismo número mínimo de votos, entonces todos los jugadores con este resultado son eliminados del juego. Las voces de estos jugadores van al grupo general del electorado.
Al final de la ronda 12, el ganador es el que tiene más votos.
Elegir una herramienta para el backend
De la descripción del juego sigue:
- Esto es multijugador
- Es necesario identificar de alguna manera a los jugadores y administrar cuentas
- La presencia de un componente social beneficiaría al juego: amigos, comunidades (clanes), chats, logros (logros)
- Se requerirán tablas de clasificación y funcionalidad de emparejamiento.
- La funcionalidad de gestión de torneos será útil en el futuro
- Dado que el juego es un juego de cartas, debes administrar el catálogo de cartas, es posible que necesites almacenar cartas disponibles para el jugador y mazos compilados
- En el futuro, es posible que se requiera una economía en el juego, incluida la moneda del juego, el intercambio de bienes virtuales (tarjetas)
Mirando la lista de necesidades, inmediatamente llegué a la conclusión de que hacer mi propio backend en la etapa inicial no tiene sentido y busqué en Google qué otras opciones hay. Entonces descubrí que hay backends especializados en juegos en la nube, entre los que se destacan PlayFab (comprado por Microsoft) y GameSparks (comprado por Amazon).
En general, son funcionalmente similares y cubren necesidades básicas. Además, su arquitectura interna es muy diferente, las mismas tareas se resuelven de manera un poco diferente y las correspondencias explícitas en las características son difíciles de rastrear. A continuación se presentan las características positivas y negativas de cada plataforma y las consideraciones sobre el tema de elección.
Playfab
Características positivas:
- Las cuentas de diferentes juegos se combinan en una cuenta maestra
- La economía del juego se describe sin una sola línea de código, incluido el precio a una tienda virtual separada
- Interfaz de usuario amigable
- Microsoft adquiere producto después de la adquisición
- El costo de propiedad en la producción por suscripción de Indie Studio es de $ 99 (hasta 100k MAU), cuando el cambio a la suscripción Professional 1k MAU costará $ 8 (cuenta mínima $ 300)
Características negativas:
- El almacenamiento de datos del juego está estrictamente limitado, por ejemplo, en una suscripción gratuita para almacenar datos para una sesión de juego específica (si entiendo todo correctamente, los grupos de entidades se utilizan para esto) 3 ranuras de 500 bytes cada una están disponibles
- Para organizar el modo multijugador, debes conectar servidores de terceros que procesarán los eventos de los clientes y calcularán la lógica del juego. Este es Photon en su hardware o Azure Thunderhead, y necesita no solo organizar el servidor, sino también actualizar su suscripción al menos a Indie Studio
- Es necesario soportar el hecho de que el código de la nube sin autocompletar y no hay forma de dividirse en módulos (¿o no lo encontró?)
- No hay un depurador normal, solo puede escribir registros en CloudScript y ver
Gamesparks
Características positivas:
- Juego de almacenamiento de datos. No solo hay muchos lugares donde puede guardar datos (metadatos generales del juego, bienes virtuales, perfil de jugador, sesiones de jugadores múltiples, etc.), la plataforma también proporciona una base de datos completa como un servicio que no está conectado a nada, Además, tanto MongoDB como Redis están disponibles inmediatamente para diferentes tipos de datos. En el entorno de desarrollo puedes almacenar 10 MB, en la batalla 10 GB
- El modo multijugador está disponible en una suscripción gratuita (Desarrollo) con un límite de 10 conexiones simultáneas y 10 solicitudes por segundo.
- Trabajo conveniente con CloudCode, que incluye una herramienta integrada para pruebas y depuración (Test Harness)
Características negativas:
- La sensación de que desde la compra de Amazon (invierno de 2018) la herramienta se ha estancado, no hay innovaciones
- Nuevamente, después de la adquisición de Amazon, los aranceles empeoraron; antes era posible usar hasta 10,000 MAU en producción de forma gratuita
- El costo de propiedad de producción comienza en $ 300 (suscripción estándar)
Reflexiones
Primero tienes que verificar el concepto del juego. Para hacer esto, quiero construir un prototipo de palos y cinta adhesiva sin inversiones monetarias y comenzar las pruebas de juego de la mecánica del juego. Por lo tanto, en primer lugar al elegir, aprovecho la oportunidad para desarrollar y probar un mecánico con una suscripción gratuita.
GameSparks cumple este criterio, pero PlayFab no, porque necesitará un servidor que maneje los eventos de los clientes del juego y una suscripción de nivel de estudio independiente ($ 99).
Al mismo tiempo, acepto el riesgo de que Amazon no desarrolle GameSparks, lo que significa que puede "morir". Dado esto y aún el costo de propiedad en la producción, tengo en mente la necesidad potencial de moverme a otra plataforma o a mi propio backend.
Primeros pasos en el desarrollo.
Conexión y autenticación
Entonces, la elección recayó en GameSparks como un backend en la etapa de creación de prototipos. El primer paso es aprender cómo conectarse a la plataforma y autenticar al jugador. Un punto importante es que el usuario debería poder jugar sin registro y SMS inmediatamente después de instalar el juego. Para hacer esto, GameSparks ofrece la opción de crear un perfil anónimo llamando al método DeviceAuthenticationRequest, luego, sobre la base de un perfil anónimo, puede hacer uno completo conectándose, por ejemplo, con su cuenta de Google.
Dado que tengo un TDD cerebral, comencé creando una prueba para conectar al cliente al juego. Como en el futuro CloudCode deberá escribirse en JS, haré pruebas de integración en JS usando mocha.js y chai.js. La primera prueba resultó así:
var expect = require("chai").expect; var GameClientModule = require("../src/gameClient"); describe("Integration test", function () { this.timeout(0); it("should connect client to server", async function () { var gameClient = new GameClientModule.GameClient(); expect(gameClient.connected()).is.false; await gameClient.connect(); expect(gameClient.connected()).is.true; }); })
Por defecto, el tiempo de espera en mocha.js es de 2 segundos, inmediatamente lo hago infinito, porque las pruebas son de integración. En la prueba, creo un cliente de juego que aún no se ha implementado, verifico que no haya conexión con el servidor, llame al comando para conectarse al backend y verifique que el cliente se haya conectado correctamente.
Para que la prueba se vuelva verde, debe descargar y agregar el SDK de GameSparks JS al proyecto, así como conectar sus dependencias (crypto-js y ws) y, por supuesto, implementar GameClientModule:
var GameSparks = require("../gamesparks-javascript-sdk-2018-04-18/gamesparks-functions"); var config = new require("./config.json"); exports.GameClient = function () { var gamesparks = new GameSparks(); this.connected = () => (gamesparks.connected === true); this.connect = function () { return new Promise(function (resolve, reject) { gamesparks.initPreview({ key: config.gameApiKey, secret: config.credentialSecret, credential: config.credential, onInit: () => resolve(), onMessage: onMessage, onError: (error) => reject(error), logger: console.log }); }); } function onMessage(message) { console.log("GAME onMessage: " + JSON.stringify(message)); } }
En la implementación inicial del cliente del juego, las claves necesarias para la autorización técnica para crear una conexión desde la aplicación del cliente se leen desde la configuración. El método conectado envuelve el mismo campo del SDK. Lo más importante sucede en el método de conexión, devuelve una promesa con devoluciones de llamada para una conexión exitosa o un error, también vincula el controlador onMessage a la misma devolución de llamada. onMessage actuará como el administrador de procesamiento de mensajes desde el backend, por ahora permita que registre mensajes en la consola.
Parece que el trabajo se ha completado, pero la prueba sigue siendo roja. Resulta que el SDK de GameSparks JS no funciona con node.js; para él, ves, carece del contexto del navegador. Hagámosle pensar que ese nodo es Chrome en la amapola. Vamos a gamesparks.js y al principio agregamos:
if (typeof module === 'object' && module.exports) {
La prueba se puso verde, avanzando.
Como escribí anteriormente, un jugador debería poder comenzar a jugar inmediatamente tan pronto como ingrese al juego, mientras que quiero comenzar a acumular análisis en la actividad. Para hacer esto, nos unimos al identificador del dispositivo o a un identificador generado aleatoriamente. Comprueba que esta será una prueba:
it("should auth two anonymous players", async function () { var gameClient1 = new GameClientModule.GameClient(); expect(gameClient1.playerId).is.undefined; var gameClient2 = new GameClientModule.GameClient(); expect(gameClient2.playerId).is.undefined; await gameClient1.connect(); await gameClient1.authWithCustomId("111"); expect(gameClient1.playerId).is.equals("5b5f5614031f5bc44d59b6a9"); await gameClient2.connect(); await gameClient2.authWithCustomId("222"); expect(gameClient2.playerId).is.equals("5b5f6ddb031f5bc44d59b741"); });
Decidí verificar de inmediato a 2 clientes para asegurarme de que cada cliente cree su propio perfil en el back-end. Para hacer esto, el cliente del juego necesitará un método en el que pueda transferir un cierto identificador externo a GameSparks, y luego verificar que el cliente haya contactado con el perfil del jugador deseado. Perfiles preparados de antemano en el portal GameSparks.
Para la implementación en GameClient agregue:
this.playerId = undefined; this.authWithCustomId = function (customId) { return new Promise(resolve => { var requestData = { "deviceId": customId , "deviceOS": "NodeJS" } sendRequest("DeviceAuthenticationRequest", requestData) .then(response => { if (response.userId) { this.playerId = response.userId; resolve(); } else { reject(new Error(response)); } }) .catch(error => { console.error(error); }); }); } function sendRequest(requestType, requestData) { return new Promise(function (resolve) { gamesparks.sendWithData(requestType, requestData, (response) => resolve(response)); }); }
La implementación se reduce a enviar una solicitud de solicitud de autenticación de dispositivo, recibir el identificador del jugador de la respuesta y colocarlo en la propiedad del cliente. Inmediatamente, en un método separado, el ayudante envió solicitudes a GameSparks con un contenedor en una promis.
Ambas pruebas son verdes, queda por agregar el cierre de la conexión y refactorizar.
En GameClient, agregué un método que cierra la conexión al servidor (desconectar) y connectAsAnonymous combinando connect y authWithCustomId. Por un lado, connectAsAnonymous viola el principio de responsabilidad única, pero no parece violar ... Al mismo tiempo, agrega usabilidad, porque en las pruebas a menudo será necesario autenticar a los clientes. ¿Qué opinas sobre esto?
En las pruebas, agregó un método auxiliar de fábrica que crea una nueva instancia del cliente del juego y se agrega a la matriz de clientes creados. En el controlador especial de mocha, después de cada prueba en ejecución para clientes en la matriz, llamo al método de desconexión y borro esta matriz. Todavía no me gustan las "cadenas mágicas" en el código, así que agregué un diccionario con identificadores personalizados utilizados en las pruebas.
El código final se puede ver en el repositorio, un enlace que daré al final del artículo.
Organización de búsqueda de juegos (emparejamiento)
Comenzaré con la función de emparejamiento, que es muy importante para el modo multijugador. Este sistema comienza a funcionar cuando presionamos el botón "Buscar un juego" en un juego. Ella recoge rivales, o compañeros de equipo, o ambos (dependiendo del juego). Como regla general, en tales sistemas, cada jugador tiene un indicador numérico MMR (Proporción de emparejamiento), una calificación personal del jugador, que se utiliza para seleccionar a otros jugadores con el mismo nivel de habilidad.
Para probar esta funcionalidad, se me ocurrió la siguiente prueba:
it("should find match", async function () { var gameClient1 = newGameClient(); var gameClient2 = newGameClient(); var gameClient3 = newGameClient(); await gameClient1.connectAsAnonymous(playerCustomIds.id1); await gameClient2.connectAsAnonymous(playerCustomIds.id2); await gameClient3.connectAsAnonymous(playerCustomIds.id3); await gameClient1.findStandardMatch(); expect(gameClient1.state) .is.equals(GameClientModule.GameClientStates.MATCHMAKING); await gameClient2.findStandardMatch(); expect(gameClient2.state) .is.equals(GameClientModule.GameClientStates.MATCHMAKING); await gameClient3.findStandardMatch(); expect(gameClient3.state) .is.equals(GameClientModule.GameClientStates.MATCHMAKING); await sleep(3000); expect(gameClient1.state) .is.equals(GameClientModule.GameClientStates.CHALLENGE); expect(gameClient1.challenge, "challenge").is.not.undefined; expect(gameClient1.challenge.challengeId).is.not.undefined; expect(gameClient2.state) .is.equals(GameClientModule.GameClientStates.CHALLENGE); expect(gameClient2.challenge.challengeId) .is.equals(gameClient1.challenge.challengeId); expect(gameClient3.state) .is.equals(GameClientModule.GameClientStates.CHALLENGE); expect(gameClient3.challenge.challengeId) .is.equals(gameClient1.challenge.challengeId); });
Tres clientes están conectados al juego (en el futuro es un mínimo necesario para verificar algunos escenarios) y están registrados para buscar el juego. Después de registrar al tercer jugador en el servidor, se forma una sesión de juego y los jugadores deben conectarse a él. Al mismo tiempo, el estado de los clientes cambia y aparece el contexto de la sesión del juego con el mismo identificador.
Primero, prepara el backend. En GameSparks hay una herramienta lista para personalizar la búsqueda de juegos, disponible en la ruta "Configurador-> Partidos". Creo uno nuevo y procedo con la configuración. Además de los parámetros estándar como el código, el nombre y la descripción del partido, se indica el número mínimo y máximo de jugadores necesarios para un modo de juego personalizado. Asignaré el código "StandardMatch" al partido creado e indicaré el número de jugadores de 2 a 3.
Ahora debe configurar las reglas para seleccionar jugadores en la sección "Umbrales". Para cada umbral, se indican el tiempo de su acción, tipo (absoluto, relativo y en porcentaje) y límites.

Supongamos que un jugador con un MMR de 19 comienza a buscar. En el ejemplo anterior, los primeros 10 segundos serán la selección de otros jugadores con un MMR de 19 a 21. Si los jugadores no fueron seleccionados, el segundo borde de búsqueda se activa, lo que extiende el rango de búsqueda de 16 por los siguientes 20 segundos ( 19-3) a 22 (19 + 3). A continuación, se incluye el tercer umbral, dentro del cual se realizará una búsqueda durante 30 segundos en el rango de 14 (19-25%) a 29 (19 + 50%), mientras que el partido se considera completado si se ha acumulado el número mínimo requerido de jugadores (Aceptar marca mínima Jugadores).
De hecho, el mecanismo es más complicado, ya que tiene en cuenta el MMR de todos los jugadores que lograron unirse a un partido en particular. Analizaré estos detalles cuando llegue el momento de establecer el modo de calificación del juego (no en este artículo). Para el modo de juego estándar, donde todavía no planeo usar MMR, solo necesito un umbral de cualquier tipo.
Cuando todos los jugadores han sido seleccionados, debe crear una sesión de juego y conectar a los jugadores. En GameSparks, la función de sesión del juego es "Desafío". Como parte de esta entidad, los datos de la sesión del juego se almacenan y los mensajes se intercambian entre los clientes del juego. Para crear un nuevo tipo de Desafío, debe seguir el camino "Configurador-> Desafíos". Allí agrego un nuevo tipo con el código "StandardChallenge" e indico que este tipo de sesión de juego es por turnos, es decir Los jugadores se turnan, no simultáneamente. GameSparks al mismo tiempo toma el control de la secuencia de movimientos.
Para que un cliente se registre para buscar un juego, puede utilizar una solicitud del tipo MatchmakingRequest, pero no lo recomendaría, porque uno de los parámetros requiere el MMR del jugador. Esto puede conducir a un fraude por parte del cliente del juego, y el cliente no debe saber ningún MMR, este es un negocio de back-end. Para registrarme correctamente para la búsqueda del juego, creo un evento arbitrario desde el cliente. Esto se hace en la sección "Configurador-> Eventos". Llamo al evento FindStandardMatch sin atributos. Ahora necesita configurar la reacción a este evento, para esto voy a la sección del código de la nube "Configurador-> Código de la nube", allí escribo el siguiente controlador para FindStandardMatch en la sección "Eventos":
var matchRequest = new SparkRequests.MatchmakingRequest(); matchRequest.matchShortCode = "StandardMatch"; matchRequest.skill = 0; matchRequest.Execute();
Este código registra a un jugador en StandardMatch con un MMR de 0, por lo que cualquier jugador registrado para buscar un juego estándar será adecuado para crear una sesión de juego. En la selección de un partido de calificación, podría haber una apelación a los datos privados del perfil del jugador para obtener el MMR de este tipo de partido.
Cuando hay suficientes jugadores para comenzar una sesión de juego, GameSparks enviará un mensaje de MatchFoundMessage a todos los jugadores seleccionados. Aquí puede generar automáticamente una sesión de juego y agregarle jugadores. Para hacer esto, en "Mensajes de usuario-> MatchFoundMessage" agregue el código:
var matchData = Spark.getData(); if (Spark.getPlayer().getPlayerId() != matchData.participants[0].id) { Spark.exit(); } var challengeCode = ""; var accessType = "PRIVATE"; switch (matchData.matchShortCode) { case "StandardMatch": challengeCode = "StandardChallenge"; break; default: Spark.exit(); } var createChallengeRequest = new SparkRequests.CreateChallengeRequest(); createChallengeRequest.challengeShortCode = challengeCode; createChallengeRequest.accessType = accessType; var tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); createChallengeRequest.endTime = tomorrow.toISOString(); createChallengeRequest.usersToChallenge = []; var participants = matchData.participants; var numberOfPlayers = participants.length; for (var i = 1; i < numberOfPlayers; i++) { createChallengeRequest.usersToChallenge.push(participants[i].id) } createChallengeRequest.Send();
El código primero verifica que sea el primer jugador en la lista de participantes. A continuación, en nombre del primer jugador, se crea una instancia de StandardChallenge y se invita a los jugadores restantes. Los jugadores invitados reciben un mensaje ChallengeIssuedMessage. Aquí puede imaginar el comportamiento cuando se muestra una invitación para unirse al juego en el cliente y requiere confirmación enviando AcceptChallengeRequest, o puede aceptar la invitación en modo silencioso. Así que lo haré, para esto en "Mensajes de usuario-> ChallengeIssuedMessage" agregaré el siguiente código:
var challangeData = Spark.getData(); var acceptChallengeRequest = new SparkRequests.AcceptChallengeRequest(); acceptChallengeRequest.challengeInstanceId = challangeData.challenge.challengeId; acceptChallengeRequest.message = "Joining"; acceptChallengeRequest.SendAs(Spark.getPlayer().getPlayerId());
El siguiente paso, GameSparks distribuye el evento ChallengeStartedMessage. El controlador global de este evento ("Mensajes globales-> ChallengeStartedMessage") es un lugar ideal para inicializar una sesión de juego, me ocuparé de esto cuando implemente la lógica del juego.
Ha llegado el momento de la aplicación del cliente. Cambios en el módulo del cliente:
exports.GameClientStates = { IDLE: "Idle", MATCHMAKING: "Matchmaking", CHALLENGE: "Challenge" } exports.GameClient = function () { this.state = exports.GameClientStates.IDLE; this.challenge = undefined; function onMessage(message) { switch (message["@class"]) { case ".MatchNotFoundMessage": this.state = exports.GameClientStates.IDLE; break; case ".ChallengeStartedMessage": this.state = exports.GameClientStates.CHALLENGE; this.challenge = message.challenge; break; default: console.log("GAME onMessage: " + JSON.stringify(message)); } } onMessage = onMessage.bind(this); this.findStandardMatch = function () { var eventData = { eventKey: "FindStandardMatch" } return new Promise(resolve => { sendRequest("LogEventRequest", eventData) .then(response => { if (!response.error) { this.state = exports.GameClientStates.MATCHMAKING; resolve(); } else { console.error(response.error); reject(new Error(response)); } }) .catch(error => { console.error(error); reject(new Error(error)); }); }); } }
De acuerdo con la prueba, aparecieron un par de campos en el cliente: estado y desafío. El método onMessage ha adquirido un aspecto significativo y ahora responde a los mensajes sobre el inicio de una sesión de juego y a un mensaje de que no fue posible recoger un juego. También se ha agregado el método findStandardMatch, que envía la solicitud correspondiente al back-end. La prueba es verde, pero estoy satisfecho, la selección de juegos dominó.
Que sigue
En los siguientes artículos describiré el proceso de desarrollo de la lógica del juego, desde la inicialización de una sesión de juego hasta el procesamiento de movimientos. Analizaré las características de almacenar diferentes tipos de datos: una descripción de los metadatos del juego, las características del mundo del juego, los datos de las sesiones del juego y los datos sobre los jugadores. La lógica del juego se desarrollará a través de dos tipos de pruebas: unidad e integración.
Subiré las fuentes en github en porciones vinculadas a artículos.
Hay un entendimiento de que para avanzar efectivamente en la creación de un juego, debes expandir nuestro equipo de entusiastas. El artista / diseñador se unirá pronto. Y el gurú en, por ejemplo, Unity3D, que hará el frente para las plataformas móviles, aún no se ha encontrado.