¡War Robots celebra su décimo aniversario este abril! Y hasta el día de hoy, continuamos desarrollándolo y brindándole soporte, no solo lanzando nuevas funciones para nuestros jugadores sino también mejorándolo técnicamente.
En este artículo, discutiremos nuestros muchos años de experiencia en el desarrollo técnico de este gran proyecto. Pero primero, aquí hay una instantánea del proyecto tal como está:
Para mantener la funcionalidad de este tipo de proyectos y garantizar un mayor desarrollo de alta calidad, no basta con trabajar únicamente en las tareas inmediatas del producto; También es clave mejorar su condición técnica, simplificar el desarrollo y automatizar los procesos, incluidos los relacionados con la creación de nuevos contenidos. Además, debemos adaptarnos constantemente al mercado cambiante de dispositivos de usuario disponibles.
El texto se basa en entrevistas con Pavel Zinov, jefe del departamento de desarrollo de Pixonic (MY.GAMES), y Dmitry Chetverikov, desarrollador principal de War Robots.
Volvamos a 2014: el proyecto War Robots fue creado inicialmente por un pequeño equipo, y todas las soluciones técnicas encajan en el paradigma de rápido desarrollo y entrega de nuevas funciones al mercado. En ese momento, la empresa no contaba con grandes recursos para alojar servidores dedicados, y así, War Robots ingresó al mercado con un multijugador en red basado en lógica P2P.
Photon Cloud se utilizó para transferir datos entre clientes: cada comando de jugador, ya sea moverse, disparar o cualquier otro comando, se envió al servidor de Photon Cloud utilizando RPC separados. Como no había un servidor dedicado, el juego tenía un cliente maestro, que era responsable de la confiabilidad del estado del partido para todos los jugadores. Al mismo tiempo, los jugadores restantes procesaron completamente el estado de todo el juego en sus clientes locales.
Como ejemplo, no hubo verificación de movimiento: el cliente local movió su robot como mejor le pareció, este estado se envió al cliente maestro, y el cliente maestro creyó incondicionalmente este estado y simplemente lo reenvió a otros clientes en la coincidencia. Cada cliente mantuvo de forma independiente un registro de la batalla y lo envió al servidor al final del partido, luego el servidor procesó los registros de todos los jugadores, otorgó una recompensa y envió los resultados del partido a todos los jugadores.
Usamos un servidor de perfiles especial para almacenar información sobre los perfiles de los jugadores. Este consistía en muchos servidores con una base de datos Cassandra. Cada servidor era un servidor de aplicaciones simple en Tomcat y los clientes interactuaban con él a través de HTTP.
El enfoque utilizado en el juego tenía una serie de desventajas. El equipo, por supuesto, sabía de esto, pero debido a la velocidad de desarrollo y entrega del producto final al mercado, hubo que hacer una serie de concesiones.
Entre estas desventajas se encontraba, en primer lugar, la calidad de la conexión del cliente maestro. Si el cliente tenía una mala red, entonces todos los jugadores de un partido experimentaban un retraso. Y, si el cliente maestro no se ejecutaba en un teléfono inteligente muy potente, entonces, debido a la gran carga que tenía, el juego también experimentaba un retraso en la transferencia de datos. Entonces, en este caso, además del cliente maestro, otros jugadores también sufrieron.
El segundo inconveniente era que esta arquitectura era propensa a tener problemas con los tramposos. Dado que el estado final fue transferido desde el cliente maestro, este cliente podría cambiar muchos parámetros del partido a su discreción, por ejemplo, contando el número de zonas capturadas en el partido.
En tercer lugar, existe un problema con la funcionalidad de emparejamiento en Photon Cloud: el jugador de mayor edad en la sala de emparejamiento de Photon Cloud se convierte en el cliente principal, y si algo les sucede durante el emparejamiento (por ejemplo, una desconexión), la creación del grupo se ve afectada negativamente. Dato curioso relacionado: para evitar que el emparejamiento en Photon Cloud se cierre después de enviar un grupo a una partida, durante algún tiempo incluso configuramos una PC simple en nuestra oficina, y esto siempre admitía el emparejamiento, lo que significa que el emparejamiento con Photon Cloud no podía fallar. .
En algún momento, la cantidad de problemas y solicitudes de soporte alcanzó una masa crítica, y comenzamos a pensar seriamente en evolucionar la arquitectura técnica del juego; esta nueva arquitectura se llamó Infraestructura 2.0.
La idea principal detrás de esta arquitectura fue la creación de servidores de juegos dedicados. En ese momento, el equipo del cliente carecía de programadores con amplia experiencia en desarrollo de servidores. Sin embargo, la empresa estaba trabajando simultáneamente en un proyecto de análisis de alta carga basado en servidor, al que llamamos AppMetr. El equipo había creado un producto que procesaba miles de millones de mensajes por día y tenía una amplia experiencia en el desarrollo, configuración y arquitectura correcta de soluciones de servidor. Y así, algunos miembros de ese equipo se sumaron al trabajo de Infraestructura 2.0.
En 2015, en un período de tiempo bastante corto, se creó un servidor .NET, incluido en el marco Photon Server SDK que se ejecuta en Windows Server. Decidimos abandonar Photon Cloud, manteniendo solo el marco de red Photon Server SDK en el proyecto del cliente y creamos un servidor maestro para conectar clientes y los servidores correspondientes para el emparejamiento. Parte de la lógica se trasladó del cliente a un servidor de juegos dedicado. En particular, se introdujo la validación básica de daños, además de colocar cálculos de resultados de partidos en el servidor.
Después de crear con éxito la Infraestructura 2.0, nos dimos cuenta de que necesitábamos seguir avanzando hacia la disociación de las responsabilidades de los servicios: el resultado de esto fue la creación de una arquitectura de microservicios. El primer microservicio creado en esta arquitectura fueron los clanes.
A partir de ahí, apareció un servicio de comunicación separado, que era responsable de transferir datos entre microservicios. Se enseñó al cliente a establecer una conexión con el "hangar" responsable de la metamecánica en el servidor del juego y a realizar solicitudes de API (un punto de entrada único para interactuar con los servicios) utilizando UDP sobre Photon Cloud. Poco a poco, la cantidad de servicios creció y, como resultado, refactorizamos el microservicio de emparejamiento. Con esto se creó la funcionalidad de noticias, Inbox.
Cuando creamos clanes, estaba claro que los jugadores querían comunicarse entre sí en el juego: necesitaban algún tipo de chat. Esto se creó originalmente en base a Photon Chat, pero todo el historial de chat se borró tan pronto como el último cliente se desconectó. Por lo tanto, convertimos esta solución en un microservicio independiente basado en la base de datos de Cassandra.
La nueva arquitectura de microservicios nos permitió gestionar cómodamente una gran cantidad de servicios independientes entre sí, lo que significó escalar horizontalmente todo el sistema y evitar tiempos de inactividad.
En nuestro juego, las actualizaciones se produjeron sin necesidad de pausar los servidores. El perfil del servidor era compatible con varias versiones de clientes, y en cuanto a los servidores de juegos, siempre tenemos un grupo de servidores para las versiones antiguas y nuevas del cliente, este grupo cambió gradualmente después del lanzamiento de una nueva versión en las tiendas de aplicaciones, y Los servidores de juegos desaparecieron con el tiempo de la versión anterior. El esquema general de la infraestructura actual se denominó Infraestructura 4.0 y tenía este aspecto:
Además de cambiar la arquitectura, también nos enfrentamos a un dilema sobre dónde debían ubicarse nuestros servidores, ya que se suponía que cubrirían a jugadores de todo el mundo. Inicialmente, muchos microservicios se ubicaban en Amazon AWS por varias razones, en particular, por la flexibilidad que ofrece este sistema en términos de escalamiento, ya que esto era muy importante en momentos de aumento del tráfico de juegos (como cuando aparecía en tiendas y para impulsar la AU). Además, en aquel entonces era difícil encontrar un buen proveedor de hosting en Asia que proporcionara una buena calidad de red y conexión con el mundo exterior.
El único inconveniente de Amazon AWS fue su alto costo. Por lo tanto, con el tiempo, muchos de nuestros servidores se trasladaron a nuestro propio hardware: servidores que alquilamos en centros de datos de todo el mundo. Sin embargo, Amazon AWS siguió siendo una parte importante de la arquitectura, ya que permitía el desarrollo de código que cambiaba constantemente (en particular, el código de los servicios de Ligas, Clanes, Chats y Noticias) y que no estaba suficientemente cubierto por la estabilidad. pruebas. Pero, en cuanto nos dimos cuenta de que el microservicio era estable, lo trasladamos a nuestras propias instalaciones. Actualmente, todos nuestros microservicios se ejecutan en nuestros servidores de hardware.
En 2016, se realizaron cambios importantes en la representación del juego: apareció el concepto de Ubershaders con un único atlas de texturas para todos los mechs en batalla, lo que redujo en gran medida la cantidad de llamadas de sorteo y mejoró el rendimiento del juego.
Quedó claro que nuestro juego se jugaba en decenas de miles de dispositivos diferentes y queríamos brindar a cada jugador las mejores condiciones de juego posibles. Por lo tanto, creamos Quality Manager, que es básicamente un archivo que analiza el dispositivo de un jugador y habilita (o deshabilita) ciertas características del juego o de renderizado. Además, esta característica nos permitió administrar estas funciones según el modelo de dispositivo específico para que pudiéramos solucionar rápidamente los problemas que estaban experimentando nuestros jugadores.
También es posible descargar la configuración de Quality Manager desde el servidor y seleccionar dinámicamente la calidad en el dispositivo según el rendimiento actual. La lógica es bastante simple: todo el cuerpo del Gerente de Calidad está dividido en bloques, cada uno de los cuales es responsable de algún aspecto del desempeño. (Por ejemplo, la calidad de las sombras o el suavizado). Si el rendimiento de un usuario se ve afectado, el sistema intenta cambiar los valores dentro del bloque y seleccionar una opción que conduzca a un aumento en el rendimiento. Volveremos a la evolución del Quality Manager un poco más adelante, pero en esta etapa su implementación fue bastante rápida y proporcionó el nivel de control requerido.
También fue necesario trabajar en Quality Manager a medida que continuaba el desarrollo de gráficos. El juego ahora presenta sombras dinámicas en cascada, implementadas íntegramente por nuestros programadores de gráficos.
Poco a poco, aparecieron bastantes entidades en el código del proyecto, por lo que decidimos que sería bueno empezar a gestionar su vida útil, así como delimitar el acceso a diferentes funcionalidades. Esto fue especialmente importante para separar el hangar y el código de combate porque muchos recursos no se borraban cuando cambiaba el contexto del juego. Comenzamos a explorar varios contenedores de gestión de dependencias de IoC. En particular, analizamos la solución StrangeIoC. En ese momento, la solución nos pareció bastante engorrosa, por lo que implementamos nuestra propia DI simple en el proyecto. También introdujimos contextos de hangar y batalla.
Además, comenzamos a trabajar en el control de calidad del proyecto; FirebaseCrashlytics se integró en el juego para identificar fallos, ANR y excepciones en el entorno de producción.
Utilizando nuestra solución analítica interna llamada AppMetr, creamos los paneles necesarios para el seguimiento periódico del estado del cliente y la comparación de los cambios realizados durante los lanzamientos. Además, para mejorar la calidad del proyecto y aumentar la confianza en los cambios realizados en el código, comenzamos a investigar pruebas automáticas.
El aumento en la cantidad de activos y errores únicos en el contenido nos hizo pensar en organizar y unificar activos. Esto fue especialmente cierto para los mechs porque su construcción era única para cada uno de ellos; para esto creamos herramientas en Unity, y con estas hicimos mech dummies con los principales componentes necesarios. Esto significó que el departamento de diseño del juego podía editarlos fácilmente, y así fue como comenzamos a unificar nuestro trabajo con los mechs.
El proyecto siguió creciendo y el equipo empezó a pensar en lanzarlo en otras plataformas. Además, como otro punto, para algunas plataformas móviles en ese momento, era importante que el juego admitiera tantas funciones nativas de la plataforma como fuera posible, ya que esto permitía que el juego se presentara. Entonces, comenzamos a trabajar en una versión experimental de War Robots para Apple TV y una aplicación complementaria para Apple Watch.
Además, en 2016, lanzamos el juego en una plataforma que era nueva para nosotros: Amazon AppStore. En cuanto a características técnicas, esta plataforma es única porque tiene una línea unificada de dispositivos, como Apple, pero la potencia de esta línea está al nivel de los Android de gama baja. Teniendo esto en cuenta, durante el lanzamiento en esta plataforma, se realizó un trabajo importante para optimizar el uso de la memoria y el rendimiento, por ejemplo, trabajamos con atlas y compresión de texturas. El sistema de pago de Amazon, un análogo de Game Center para inicio de sesión y logros, se integró en el cliente y, por lo tanto, se rehizo el flujo de trabajo con el inicio de sesión del jugador y se lanzaron los primeros logros.
También vale la pena señalar que, simultáneamente, el equipo del cliente desarrolló por primera vez un conjunto de herramientas para Unity Editor, que hicieron posible grabar videos del juego en el motor. Esto facilitó a nuestro departamento de marketing trabajar con recursos, controlar el combate y utilizar la cámara para crear los videos que tanto amaban a nuestra audiencia.
Debido a la ausencia de Unity, y en particular del motor de física en el servidor, la mayoría de las partidas continuaron emulándose en los dispositivos de los jugadores. Debido a esto, persistieron los problemas con los tramposos. Periódicamente, nuestro equipo de soporte recibió comentarios de nuestros usuarios con videos de tramposos: aceleraron en un momento conveniente, volaron por el mapa, mataron a otros jugadores a la vuelta de una esquina, se volvieron inmortales, etc.
Lo ideal sería transferir todas las mecánicas al servidor; sin embargo, además del hecho de que el juego estaba en constante desarrollo y ya había acumulado una buena cantidad de código heredado, un enfoque con un servidor autorizado también podría tener otras desventajas. Por ejemplo, aumentando la carga sobre la infraestructura y la demanda de una mejor conexión a Internet. Dado que la mayoría de nuestros usuarios jugaban en redes 3G/4G, este enfoque, aunque obvio, no fue una solución eficaz al problema. Como enfoque alternativo para combatir a los tramposos dentro del equipo, se nos ocurrió una nueva idea: crear un "quórum".
El quórum es un mecanismo que te permite comparar múltiples simulaciones de diferentes jugadores al momento de confirmar el daño causado; Recibir daño es una de las principales características del juego, de la que depende prácticamente el resto de su estado. Por ejemplo, si destruyes a tus oponentes, no podrán capturar balizas.
La idea de esta decisión era la siguiente: cada jugador seguía simulando el mundo entero (incluido disparar a otros jugadores) y enviar los resultados al servidor. El servidor analizó los resultados de todos los jugadores y tomó una decisión sobre si el jugador finalmente resultó dañado y en qué medida. En nuestros partidos participan 12 personas, por lo que para que el servidor considere que se ha hecho daño, basta con que este hecho quede registrado dentro de las simulaciones locales de 7 jugadores. Estos resultados luego se enviarían al servidor para su posterior validación. Esquemáticamente, este algoritmo se puede representar de la siguiente manera:
Este esquema nos permitió reducir significativamente la cantidad de tramposos que se volvían imposibles de matar en la batalla. El siguiente gráfico muestra la cantidad de quejas contra estos usuarios, así como las etapas para habilitar las funciones de cálculo de daños en el servidor y habilitar el quórum de daños:
Este mecanismo de infligir daño resolvió los problemas de las trampas durante mucho tiempo porque, a pesar de la falta de física en el servidor (y por lo tanto de la imposibilidad de tener en cuenta cosas como la topografía de la superficie y la posición real de los robots en el espacio), los resultados de La simulación para todos los clientes y su consenso dieron una comprensión inequívoca de quién causó daño a quién y bajo qué condiciones.
Con el tiempo, además de las sombras dinámicas, agregamos efectos posteriores al juego usando la pila de posprocesamiento de Unity y el renderizado por lotes. En la siguiente imagen se pueden ver ejemplos de gráficos mejorados como resultado de la aplicación de efectos de posprocesamiento:
Para comprender mejor lo que estaba sucediendo en las compilaciones de depuración, creamos un sistema de registro e implementamos un servicio local en la infraestructura interna que podría recopilar registros de los evaluadores internos y facilitar la búsqueda de las causas de los errores. El control de calidad todavía utiliza esta herramienta para las pruebas de juego y los resultados de su trabajo se adjuntan a los tickets en Jira.
También agregamos al proyecto un Administrador de paquetes escrito por nosotros mismos. Inicialmente queríamos mejorar el trabajo con varios paquetes y complementos externos (de los cuales el proyecto ya había acumulado una cantidad suficiente). Sin embargo, la funcionalidad de Unity no era suficiente en ese momento, por lo que nuestro equipo interno de Plataforma desarrolló su propia solución con control de versiones de paquetes, almacenamiento usando NPM y la capacidad de conectar paquetes al proyecto simplemente agregando un enlace a GitHub. Como todavía queríamos utilizar una solución de motor nativa, consultamos con colegas de Unity y varias de nuestras ideas se incluyeron en la solución final de Unity cuando lanzaron su Administrador de paquetes en 2018.
Continuamos ampliando la cantidad de plataformas donde nuestro juego estaba disponible. Finalmente apareció Facebook Gameroom, una plataforma que admite teclado y mouse. Se trata de una solución de Facebook (Meta), que permite a los usuarios ejecutar aplicaciones en una PC; Básicamente, es un análogo de la tienda Steam. Por supuesto, la audiencia de Facebook es bastante diversa y Facebook Gameroom se lanzó en una gran cantidad de dispositivos, principalmente aquellos que no son de juegos. Entonces, decidimos mantener los gráficos móviles en el juego para no sobrecargar las PC de la audiencia principal. Desde un punto de vista técnico, los únicos cambios significativos que requirió el juego fueron la integración del SDK, el sistema de pago y la compatibilidad con teclado y mouse utilizando el sistema de entrada nativo Unity.
Técnicamente, difería poco de la versión del juego para Steam, pero la calidad de los dispositivos fue significativamente menor porque la plataforma se posicionó como un lugar para jugadores ocasionales con dispositivos bastante simples que no podían ejecutar nada más que un navegador, con esto en Eso sí, Facebook esperaba entrar en un mercado bastante grande. Los recursos de estos dispositivos eran limitados y la plataforma, en particular, tenía problemas persistentes de memoria y rendimiento.
Más tarde, la plataforma cerró y transferimos nuestros jugadores a Steam, donde era posible. Para eso, desarrollamos un sistema especial con códigos para transferir cuentas entre plataformas.
Otro evento notable de 2017: el lanzamiento del iPhone X, el primer teléfono inteligente con muesca. En ese momento, Unity no admitía dispositivos con muescas, por lo que tuvimos que crear una solución personalizada basada en los parámetros UnityEngine.Screen.safeArea, que obliga a la interfaz de usuario a escalar y evitar diferentes zonas seguras en la pantalla del teléfono.
Además, en 2017 decidimos probar algo más: lanzar una versión VR de nuestro juego en Steam. El desarrollo duró unos seis meses e incluyó pruebas en todos los cascos disponibles en ese momento: Oculus, HTC Vive y Windows Mixed Reality. Este trabajo se llevó a cabo utilizando la API de Oculus y la API de Steam. Además, se preparó una versión de demostración para auriculares PS VR, así como soporte para MacOS.
Desde un punto de vista técnico, era necesario mantener un FPS estable: 60 FPS para PS VR y 90 FPS para HTC Vive. Para lograr esto, se utilizó Zone Culling autoguiado, además de Frustum y Occlusion estándar, así como reproyecciones (cuando algunos fotogramas se generan en base a otros anteriores).
Para resolver el problema del mareo, se adoptó una interesante solución creativa y técnica: nuestro jugador se convirtió en piloto de robot y se sentó en la cabina. La cabina era un elemento estático, por lo que el cerebro la percibía como una especie de constante en el mundo y, por tanto, el movimiento en el espacio funcionaba muy bien.
El juego también tenía un escenario que requería el desarrollo de varios desencadenantes, guiones y líneas de tiempo caseras independientes, ya que Cinemachine aún no estaba disponible en esa versión de Unity.
Para cada arma, la lógica para apuntar, bloquear, rastrear y apuntar automáticamente se escribió desde cero.
La versión se lanzó en Steam y fue bien recibida por nuestros jugadores. Sin embargo, después de la campaña de Kickstarter, decidimos no continuar con el desarrollo del proyecto, ya que el desarrollo de un shooter PvP completo para realidad virtual estaba plagado de riesgos técnicos y la falta de preparación del mercado para tal proyecto.
Mientras trabajábamos para mejorar el componente técnico del juego, así como para crear nuevas mecánicas y plataformas y desarrollar las existentes, nos encontramos con el primer error grave en el juego, que afectó negativamente los ingresos al lanzar una nueva promoción. El problema fue que el botón "Precio" estaba localizado incorrectamente como "Premio". La mayoría de los jugadores estaban confundidos por esto, hicieron compras con moneda real y luego solicitaron reembolsos.
El problema técnico fue que, en aquel entonces, todas nuestras localizaciones estaban integradas en el cliente del juego. Cuando surgió este problema, entendimos que no podíamos posponer más el trabajo en una solución que nos permitiera actualizar las localizaciones. Así fue como se creó una solución basada en CDN y las herramientas que la acompañan, como proyectos en TeamCity, que automatizaban la carga de archivos de localización en la CDN.
Esto nos permitió descargar localizaciones de los servicios que utilizamos (POEditor) y cargar datos sin procesar en la CDN.
Después de eso, el perfil del servidor comenzó a configurar enlaces para cada versión del cliente, correspondientes a los datos de localización. Cuando se inició la aplicación, el servidor de perfiles comenzó a enviar este enlace al cliente y estos datos se descargaron y almacenaron en caché.
Avancemos hasta 2018. A medida que el proyecto crecía, también lo hacía la cantidad de datos transferidos del servidor al cliente. En el momento de conectarse al servidor, fue necesario descargar una gran cantidad de datos sobre el saldo, la configuración actual, etc.
Los datos actuales se presentaron en formato XML y se serializaron mediante el serializador estándar de Unity.
Además de transferir una cantidad bastante grande de datos al iniciar el cliente, así como al comunicarse con el servidor de perfil y el servidor del juego, los dispositivos de los jugadores gastaron mucha memoria almacenando el saldo actual y serializándolo/deserializándolo.
Quedó claro que para mejorar el rendimiento y el tiempo de inicio de la aplicación, sería necesario realizar I+D para encontrar protocolos alternativos.
Los resultados del estudio sobre los datos de nuestras pruebas se muestran en esta tabla:
Protocolo | tamaño | asignar | tiempo |
---|---|---|---|
XML | 2,2MB | 18,4 MB | 518,4 ms |
MessagePack (sin contrato) | 1,7 megas | 2 MB | 32,35 ms |
MessagePack (teclas str) | 1,2 megas | 1,9 MB | 25,8 ms |
MessagePack (claves int) | 0,53MB | 1.9 | 16.5 |
FlatBuffers | 0,5 megas | 216B | 0 ms/12 ms/450 KB |
Como resultado, elegimos el formato MessagePack ya que la migración era más económica que cambiar a FlatBuffers con resultados de salida similares, especialmente cuando se usaba MessagePack con claves enteras.
Para FlatBuffers (y con buffers de protocolo) es necesario describir el formato del mensaje en un lenguaje separado, generar código C# y Java, y usar el código generado en la aplicación. Como no queríamos incurrir en este costo adicional de refactorizar el cliente y el servidor, cambiamos a MessagePack.
La transición fue casi perfecta y, en las primeras versiones, admitimos la capacidad de volver a XML hasta que estuvimos convencidos de que no había problemas con el nuevo sistema. La nueva solución cubrió todas nuestras tareas: redujo significativamente el tiempo de carga del cliente y también mejoró el rendimiento al realizar solicitudes a los servidores.
Cada característica o nueva solución técnica de nuestro proyecto se publica bajo una "bandera". Esta bandera se almacena en el perfil del jugador y llega al cliente cuando comienza el juego, junto con el saldo. Como regla general, cuando se lanza una nueva funcionalidad, especialmente una técnica, varias versiones de clientes contienen ambas funcionalidades, la antigua y la nueva. La activación de nuevas funciones se produce estrictamente de acuerdo con el estado de la bandera descrito anteriormente, lo que nos permite monitorear y ajustar el éxito técnico de una solución particular en tiempo real.
Poco a poco, llegó el momento de actualizar la funcionalidad de inyección de dependencia en el proyecto. El actual ya no podía hacer frente a una gran cantidad de dependencias y, en algunos casos, introducía conexiones no obvias que eran muy difíciles de romper y refactorizar. (Esto fue especialmente cierto para las innovaciones relacionadas con la interfaz de usuario de una forma u otra).
A la hora de elegir una nueva solución, la elección fue sencilla: el conocido Zenject, que ya había sido probado en otros proyectos nuestros. Este proyecto ha estado en desarrollo durante mucho tiempo, cuenta con soporte activo, se están agregando nuevas funciones y muchos desarrolladores del equipo estaban familiarizados con él de alguna manera.
Poco a poco, empezamos a reescribir War Robots usando Zenject. Todos los módulos nuevos se desarrollaron con él y los antiguos se refactorizaron gradualmente. Desde que usamos Zenject, hemos recibido una secuencia más clara de carga de servicios dentro de los contextos que discutimos anteriormente (combate y hangar), y esto permitió a los desarrolladores sumergirse más fácilmente en el desarrollo del proyecto, así como desarrollar nuevas funciones con más confianza. dentro de estos contextos.
En versiones relativamente nuevas de Unity, fue posible trabajar con código asincrónico mediante async/await. Nuestro código ya usaba código asincrónico, pero no había un estándar único en todo el código base, y dado que el backend de secuencias de comandos de Unity comenzó a admitir async/await, decidimos realizar investigación y desarrollo y estandarizar nuestros enfoques para el código asincrónico.
Otra motivación fue eliminar el infierno de devolución de llamadas del código: esto es cuando hay una cadena secuencial de llamadas asincrónicas y cada llamada espera los resultados de la siguiente.
En aquel momento, existían varias soluciones populares, como RSG o UniRx; Los comparamos todos y los compilamos en una sola tabla:
| RSG.Promesa | TPL | TPL con espera | UniTarea | UniTask con asíncrono |
---|---|---|---|---|---|
Tiempo hasta terminar, s | 0.15843 | 0.1305305 | 0,1165172 | 0.1330536 | 0,1208553 |
Primer fotograma Tiempo/Auto, ms | 105,25/82,63 | 13,51/11,86 | 21,89/18,40 | 28,80/24,89 | 19,27/15,94 |
Asignaciones del primer cuadro | 40,8MB | 2,1 megas | 5,0MB | 8,5MB | 5,4MB |
Segundo fotograma Tiempo/Auto, ms | 55,39/23,48 | 0,38/0,04 | 0,28/0,02 | 0,32/0,03 | 0,69/0,01 |
Asignaciones del segundo cuadro | 3,1 megas | 10,2 KB | 10,3KB | 10,3KB | 10,4 KB |
Al final, decidimos utilizar async/await nativo como estándar para trabajar con código C# asincrónico con el que la mayoría de los desarrolladores están familiarizados. Decidimos no utilizar UniRx.Async, ya que las ventajas del complemento no cubrían la necesidad de depender de una solución de terceros.
Elegimos seguir el camino de la funcionalidad mínima requerida. Dejamos de usar RSG.Promise o titulares porque, en primer lugar, era necesario capacitar a nuevos desarrolladores para trabajar con estas herramientas, generalmente desconocidas, y en segundo lugar, era necesario crear un contenedor para el código de terceros que utiliza tareas asíncronas en RSG.Promise o titulares. La elección de async/await también ayudó a estandarizar el enfoque de desarrollo entre los proyectos de la empresa. Esta transición resolvió nuestros problemas: terminamos con un proceso claro para trabajar con código asincrónico y eliminamos del proyecto el infierno de devolución de llamadas difícil de soportar.
La interfaz de usuario se mejoró gradualmente. Dado que en nuestro equipo la funcionalidad de las nuevas funciones la desarrollan los programadores del cliente, y el diseño y el desarrollo de la interfaz los lleva a cabo el departamento UI/UX, necesitábamos una solución que nos permitiera paralelizar el trabajo en la misma función, de modo que El diseñador de diseño podría crear y probar la interfaz mientras el programador escribe la lógica.
La solución a este problema se encontró en la transición al modelo MVVM de trabajo con la interfaz. Este modelo permite al diseñador de interfaces no sólo diseñar una interfaz sin involucrar a un programador, sino también ver cómo reaccionará la interfaz ante ciertos datos cuando aún no se han conectado datos reales. Después de investigar un poco sobre soluciones disponibles en el mercado, así como de crear rápidamente un prototipo de nuestra propia solución llamada Pixonic ReactiveBindings, compilamos una tabla comparativa con los siguientes resultados:
| tiempo de CPU | Memoria. Alloc | Memoria. Uso |
---|---|---|---|
Enlace de datos de menta | 367 ms | 73KB | 17,5 megas |
DisplayFab | 223 ms | 147KB | 8,5MB |
Soldadura de unidad | 267 ms | 90KB | 15,5 megas |
Enlaces reactivos Pixonic | 152 ms | 23KB | 3 megas |
Tan pronto como el nuevo sistema demostró su eficacia, principalmente en términos de simplificar la producción de nuevas interfaces, comenzamos a utilizarlo para todos los proyectos nuevos, así como para todas las funciones nuevas que aparecían en el proyecto.
En 2019, se lanzaron varios dispositivos de Apple y Samsung que eran bastante potentes en términos de gráficos, por lo que a nuestra empresa se le ocurrió la idea de una actualización significativa de War Robots. Queríamos aprovechar el poder de todos estos nuevos dispositivos y actualizar la imagen visual del juego.
Además de la imagen actualizada, también teníamos varios requisitos para nuestro producto actualizado: queríamos admitir un modo de juego de 60 FPS, así como diferentes calidades de recursos para diferentes dispositivos.
El actual responsable de calidad también tuvo que reelaborar, ya que dentro de los bloques crecía el número de calidades que se cambiaban sobre la marcha, lo que reducía la productividad, además de crear una gran cantidad de permutaciones, cada una de las cuales debía probarse, lo que imponía medidas adicionales. costos en el equipo de control de calidad con cada lanzamiento.
Lo primero que comenzamos al reelaborar el cliente fue refactorizar el rodaje. La implementación original dependía de la velocidad de renderizado del cuadro porque la entidad del juego y su representación visual en el cliente eran el mismo objeto. Decidimos separar estas entidades para que la entidad de juego de la toma ya no dependiera de sus imágenes, y esto logró un juego más justo entre dispositivos con diferentes velocidades de fotogramas.
El juego trata con una gran cantidad de disparos, pero los algoritmos para procesar los datos de estos son bastante simples: moverse, decidir si el enemigo fue alcanzado, etc. El concepto Entity Component System encajaba perfectamente para organizar la lógica del movimiento del proyectil. en el juego, así que decidimos seguir con ello.
Además, en todos nuestros nuevos proyectos, ya estábamos usando ECS para trabajar con lógica, y era hora de aplicar este paradigma a nuestro proyecto principal de unificación. Como resultado del trabajo realizado, nos dimos cuenta de un requisito clave: garantizar la capacidad de ejecutar imágenes a 60 FPS. Sin embargo, no todo el código de combate fue transferido a ECS, por lo que no se pudo aprovechar plenamente el potencial de este enfoque. Al final, no mejoramos el rendimiento tanto como nos hubiera gustado, pero tampoco lo degradamos, lo cual sigue siendo un indicador importante con un cambio de paradigma y arquitectura tan fuerte.
Al mismo tiempo, comenzamos a trabajar en nuestra versión para PC para ver qué nivel de gráficos podíamos alcanzar con nuestra nueva pila de gráficos. Comenzó a aprovechar Unity SRP, que en ese momento todavía estaba en la versión preliminar y cambiaba constantemente. La imagen que salió para la versión Steam fue bastante impresionante:
Además, algunas de las características gráficas que se muestran en la imagen de arriba podrían transferirse a potentes dispositivos móviles. Esto fue especialmente para los dispositivos Apple, que históricamente tienen buenas características de rendimiento, excelentes controladores y una estrecha combinación de hardware y software; esto les permite producir una imagen de muy alta calidad sin necesidad de soluciones temporales de nuestra parte.
Rápidamente comprendimos que cambiar la pila de gráficos por sí solo no cambiaría la imagen. También se hizo evidente que, además de dispositivos potentes, necesitaríamos contenido para dispositivos más débiles. Entonces, comenzamos a planificar el desarrollo de contenido para diferentes niveles de calidad.
Esto significó que los mechs, armas, mapas, efectos y sus texturas tuvieron que separarse en calidad, lo que se traduce en diferentes entidades para diferentes calidades. Es decir, los modelos físicos, las texturas y el conjunto de texturas mecánicas que funcionarán en calidad HD serán diferentes de los mecanismos que funcionarán en otras calidades.
Además, el juego dedica una gran cantidad de recursos a los mapas, que también hay que rehacer para que cumplan con las nuevas cualidades. También se hizo evidente que el actual Gerente de Calidad no cubría nuestras necesidades ya que no controlaba la calidad de los activos.
Entonces, primero tuvimos que decidir qué cualidades estarían disponibles en nuestro juego. Teniendo en cuenta la experiencia de nuestro actual Gerente de Calidad, nos dimos cuenta de que en la nueva versión queríamos varias calidades fijas que el usuario pueda cambiar de forma independiente en la configuración dependiendo de la calidad máxima disponible para un dispositivo determinado.
El nuevo sistema de calidad determina el dispositivo que posee un usuario y, en función del modelo de dispositivo (en el caso de iOS) y del modelo de GPU (en el caso de Android), nos permite establecer la calidad máxima disponible para un reproductor concreto. En este caso se dispone de esta calidad, así como de todas las anteriores.
Además, para cada calidad, hay una configuración para cambiar el FPS máximo entre 30 y 60. Inicialmente, planeamos tener alrededor de cinco calidades (ULD, LD, MD, HD, UHD). Sin embargo, durante el proceso de desarrollo, quedó claro que tal cantidad de cualidades llevaría mucho tiempo desarrollarse y no nos permitiría reducir la carga de control de calidad. Con esto en mente, al final terminamos con las siguientes cualidades en el juego: HD, LD y ULD. (Como referencia, la distribución actual de la audiencia que juega con estas calidades es la siguiente: HD - 7%, LD - 72%, ULD - 21%).
Tan pronto como comprendimos que necesitábamos implementar más cualidades, comenzamos a pensar en cómo clasificar los activos según esas cualidades. Para simplificar este trabajo, acordamos el siguiente algoritmo: los artistas crearían activos de máxima calidad (HD) y luego, utilizando guiones y otras herramientas de automatización, partes de estos activos se simplificarían y utilizarían como activos para otras calidades.
En el proceso de trabajo en este sistema de automatización, desarrollamos las siguientes soluciones:
Se ha trabajado mucho para refactorizar los recursos mecánicos y de mapas existentes. Los mechs originales no fueron diseñados con diferentes calidades en mente y, por lo tanto, se almacenaron en casas prefabricadas individuales. Después de la refactorización, se extrajeron de ellos las partes básicas, que contenían principalmente su lógica, y utilizando Prefab Variants, se crearon variantes con texturas para una calidad específica. Además, agregamos herramientas que podrían dividir automáticamente un mecanismo en diferentes calidades, teniendo en cuenta la jerarquía de las carpetas de almacenamiento de texturas según las calidades.
Para entregar activos específicos a dispositivos específicos, era necesario dividir todo el contenido del juego en paquetes y paquetes. El paquete principal contiene los recursos necesarios para ejecutar el juego, recursos que se utilizan en todas las capacidades, así como paquetes de calidad para cada plataforma: Android_LD, Android_HD, Android_ULD, iOS_LD, iOS_HD, iOS_ULD, etc.
La división de activos en paquetes fue posible gracias a la herramienta ResourceSystem, creada por nuestro equipo de Plataforma. Este sistema nos permitió recopilar activos en paquetes separados y luego incrustarlos en el cliente o tomarlos por separado para cargarlos en recursos externos, como una CDN.
Para entregar contenido utilizamos un nuevo sistema, también creado por el equipo de la plataforma, el llamado DeliverySystem. Este sistema le permite recibir un manifiesto creado por ResourceSystem y descargar recursos de la fuente especificada. Esta fuente podría ser una compilación monolítica, archivos APK individuales o una CDN remota.
Inicialmente, planeamos utilizar las capacidades de Google Play (Play Asset Delivery) y AppStore (Recursos bajo demanda) para almacenar paquetes de recursos, pero en la plataforma Android hubo muchos problemas asociados con las actualizaciones automáticas del cliente, así como restricciones en el número de recursos almacenados.
Además, nuestras pruebas internas mostraron que un sistema de entrega de contenido con CDN funciona mejor y es más estable, por lo que abandonamos el almacenamiento de recursos en tiendas y comenzamos a almacenarlos en nuestra nube.
Cuando se completó el trabajo principal de la remasterización, llegó el momento del lanzamiento. Sin embargo, rápidamente comprendimos que el tamaño original planificado de 3 GB era una mala experiencia de descarga a pesar de que varios juegos populares en el mercado requerían que los usuarios descargaran entre 5 y 10 GB de datos. Nuestra teoría era que los usuarios estarían acostumbrados a esta nueva realidad cuando lanzáramos nuestro juego remasterizado al mercado, pero ¡ay!
En otras palabras, los jugadores no estaban acostumbrados a este tipo de archivos grandes en juegos móviles. Necesitábamos encontrar una solución a este problema rápidamente. Decidimos lanzar una versión sin calidad HD varias iteraciones después de esto, pero aún así la entregamos a nuestros usuarios más tarde.
Paralelamente al desarrollo de la remasterización, trabajamos activamente para automatizar las pruebas del proyecto. El equipo de control de calidad ha hecho un gran trabajo garantizando que se pueda realizar un seguimiento automático del estado del proyecto. Por el momento disponemos de servidores con máquinas virtuales donde se ejecuta el juego. Al utilizar un marco de prueba escrito por nosotros mismos, podemos presionar cualquier botón del proyecto con scripts, y los mismos scripts se utilizan para ejecutar varios escenarios cuando se realizan pruebas directamente en dispositivos.
Seguimos desarrollando este sistema: ya se han escrito cientos de pruebas en ejecución constante para verificar la estabilidad, el rendimiento y la ejecución lógica correcta. Los resultados se muestran en un panel especial donde nuestros especialistas de control de calidad, junto con los desarrolladores, pueden examinar en detalle las áreas más problemáticas (incluidas capturas de pantalla reales de la ejecución de prueba).
Antes de poner en funcionamiento el sistema actual, las pruebas de regresión requerían mucho tiempo (alrededor de una semana en la versión anterior del proyecto), que también era mucho menor que la actual en términos de volumen y cantidad de contenido. Pero gracias a las pruebas automáticas, la versión actual del juego se prueba sólo durante dos noches; Esto se puede mejorar aún más, pero actualmente estamos limitados por la cantidad de dispositivos conectados al sistema.
Hacia la finalización de la remasterización, el equipo de Apple se puso en contacto con nosotros y nos dio la oportunidad de participar en una presentación de nuevo producto para demostrar las capacidades del nuevo chip Apple A14 Bionic (lanzado con los nuevos iPads en el otoño de 2020) utilizando nuestro nuevos gráficos. Durante el trabajo en este mini proyecto, se creó una versión HD completamente funcional, capaz de ejecutarse en chips Apple a 120 FPS. Además, se agregaron varias mejoras gráficas para demostrar la potencia del nuevo chip.
¡Como resultado de una selección competitiva y bastante dura, nuestro juego fue incluido en la presentación de otoño del Apple Event! Todo el equipo se reunió para ver y celebrar este evento, ¡y fue realmente genial!
Incluso antes del lanzamiento de la versión remasterizada del juego en 2021, había surgido una nueva tarea. Muchos usuarios se habían quejado de problemas de red, cuya raíz era nuestra solución actual en ese momento, la capa de transporte Photon, que se utilizaba para trabajar con Photon Server SDK. Otro problema fue la presencia de una cantidad grande y no estandarizada de RPC que se enviaban al servidor en orden aleatorio.
Además, también hubo problemas con la sincronización de mundos y el desbordamiento de la cola de mensajes al comienzo del partido, lo que podría causar un retraso significativo.
Para remediar la situación, decidimos mover la pila de partidas de la red hacia un modelo más convencional, donde la comunicación con el servidor se produce a través de paquetes y recepción del estado del juego, no a través de llamadas RPC.
La nueva arquitectura de partidos en línea se llamó WorldState y tenía como objetivo eliminar Photon del juego.
Además de sustituir el protocolo de transporte de Photon a UDP basado en la biblioteca LightNetLib, esta nueva arquitectura también implicó optimizar el sistema de comunicación cliente-servidor.
Como resultado de trabajar en esta característica, redujimos los costos en el lado del servidor (cambiando de Windows a Linux y abandonando las licencias del SDK de Photon Server), terminamos con un protocolo que es mucho menos exigente para los dispositivos finales de los usuarios, reduciendo la cantidad de problemas. con desincronización de estado entre el servidor y el cliente, y creó una oportunidad para desarrollar nuevo contenido PvE.
Sería imposible cambiar todo el código del juego de la noche a la mañana, por lo que el trabajo en WorldState se dividió en varias etapas.
La primera etapa fue un rediseño completo del protocolo de comunicación entre el cliente y el servidor, y el movimiento de los mechs se trasladó a nuevos rieles. Esto nos permitió crear un nuevo modo para el juego: PvE. Poco a poco, las mecánicas comenzaron a trasladarse al servidor, en particular las más recientes (daño y daño crítico a los mechs). Continúa el trabajo en la transferencia gradual del código antiguo a los mecanismos de WorldState y también tendremos varias actualizaciones este año.
En 2022, lanzamos una plataforma nueva para nosotros: Facebook Cloud. El concepto detrás de la plataforma era interesante: ejecutar juegos en emuladores en la nube y transmitirlos al navegador de teléfonos inteligentes y PC, sin la necesidad de que el jugador final tenga una PC o un teléfono inteligente potente para ejecutar el juego; sólo se necesita una conexión a Internet estable.
Desde el lado del desarrollador, se pueden distribuir dos tipos de compilaciones como la principal que utilizará la plataforma: la compilación de Android y la compilación de Windows. Tomamos el primer camino debido a nuestra mayor experiencia con esta plataforma.
Para iniciar nuestro juego en Facebook Cloud, necesitábamos realizar varias modificaciones, como rehacer la autorización en el juego y agregar control del cursor. También necesitábamos preparar una compilación con todos los recursos integrados porque la plataforma no admitía CDN y necesitábamos configurar nuestras integraciones, que no siempre podían ejecutarse correctamente en los emuladores.
También se trabajó mucho en el lado de los gráficos para garantizar la funcionalidad de la pila de gráficos porque los emuladores de Facebook no eran dispositivos Android reales y tenían sus propias características en términos de implementación de controladores y gestión de recursos.
Sin embargo, vimos que los usuarios de esta plataforma encontraron muchos problemas, tanto conexiones inestables como funcionamiento inestable de los emuladores, y Facebook decidió cerrar su plataforma a principios de 2024.
Lo que sucede constantemente por parte de los programadores y que no se puede discutir en detalle en un artículo tan breve es el trabajo regular con los riesgos técnicos del proyecto, el seguimiento regular de las métricas técnicas, el trabajo constante con la memoria y la optimización de recursos, la búsqueda de problemas en soluciones de terceros de SDK de socios, integraciones de publicidad y mucho más.
Además, continuamos la investigación y el trabajo práctico para solucionar fallos críticos y ANR. Cuando un proyecto se ejecuta en una cantidad tan grande de dispositivos completamente diferentes, esto es inevitable.
Por otra parte, nos gustaría destacar la profesionalidad de las personas que trabajan para encontrar las causas de los problemas. Estos problemas técnicos suelen ser de naturaleza compleja y es necesario superponer datos de varios sistemas analíticos, así como realizar algunos experimentos no triviales antes de encontrar la causa. Con frecuencia, los problemas más complejos no tienen un caso consistentemente reproducible que pueda ser probado, por lo que a menudo se utilizan datos empíricos y nuestra experiencia profesional para solucionarlos.
Cabe decir algunas palabras sobre las herramientas y recursos que utilizamos para encontrar problemas en un proyecto.
Esta es sólo una pequeña lista de las mejoras técnicas que ha sufrido el proyecto durante su existencia. Es difícil enumerar todo lo que se ha logrado ya que el proyecto está en constante desarrollo y los responsables técnicos trabajan constantemente en la elaboración e implementación de planes para mejorar el rendimiento del producto tanto para los usuarios finales como para quienes trabajan con él dentro del estudio, incluido el los propios desarrolladores y otros departamentos.
Todavía tenemos muchas mejoras que tendrá el producto en la próxima década y esperamos poder compartirlas en nuestros próximos artículos.
¡Feliz cumpleaños, War Robots, y gracias al enorme equipo de especialistas técnicos que hacen todo esto posible!