paint-brush
Singularidad: agilización del desarrollo de juegos con un marco de backend universalby@makhorin

Singularidad: agilización del desarrollo de juegos con un marco de backend universal

Andrei Makhorin13m2024/07/25
Read on Terminal Reader

¿Qué es un “marco universal” en términos de diseño de juegos? ¿Por qué son necesarios o útiles? ¿Cómo pueden ayudar a agilizar el desarrollo? Respondemos a todas esas preguntas (y más) y mostramos nuestra solución, Singularity.
featured image - Singularidad: agilización del desarrollo de juegos con un marco de backend universal
Andrei Makhorin HackerNoon profile picture


¡Hola! Soy Andrey Makhorin, desarrollador de servidores de Pixonic (MY.GAMES). En este artículo, compartiré cómo mi equipo y yo creamos una solución universal para el desarrollo backend. Aprenderá sobre el concepto, su resultado y cómo nuestro sistema, llamado Singularity, se desempeñó en proyectos del mundo real. También profundizaré en los desafíos que enfrentamos.

Fondo

Cuando un estudio de juegos está comenzando, es crucial formular e implementar rápidamente una idea convincente: se prueban docenas de hipótesis y el juego sufre cambios constantes; Se agregan nuevas funciones y se revisan o descartan soluciones fallidas. Sin embargo, este proceso de rápida iteración, sumado a plazos ajustados y un horizonte de planificación corto, puede conducir a la acumulación de deuda técnica.


Con la deuda técnica existente, reutilizar soluciones antiguas puede resultar complicado, ya que es necesario resolver varios problemas con ellas. Obviamente esto no es óptimo. Pero hay otra manera: un “marco universal”. Al diseñar componentes genéricos y reutilizables (como elementos de diseño, ventanas y bibliotecas que implementan interacciones de red), los estudios pueden reducir significativamente el tiempo y el esfuerzo necesarios para desarrollar nuevas funciones. Este enfoque no solo reduce la cantidad de código que los desarrolladores necesitan escribir, sino que también garantiza que el código ya haya sido probado, lo que resulta en menos tiempo dedicado al mantenimiento.


Hemos discutido el desarrollo de funciones dentro del contexto de un juego, pero ahora veamos la situación desde otro ángulo: para cualquier estudio de juegos, reutilizar pequeños fragmentos de código dentro de un proyecto puede ser una estrategia eficaz para agilizar la producción, pero eventualmente, Necesitarás crear un nuevo juego exitoso. Reutilizar soluciones de un proyecto existente podría, en teoría, acelerar este proceso, pero surgen dos obstáculos importantes. En primer lugar, aquí se aplican los mismos problemas de deuda técnica y, en segundo lugar, es probable que las soluciones antiguas se hayan adaptado a los requisitos específicos del juego anterior, lo que las hace inadecuadas para el nuevo proyecto.


Estos problemas se ven agravados por otros: el diseño de la base de datos puede no cumplir con los requisitos del nuevo proyecto, las tecnologías pueden estar desactualizadas y el nuevo equipo puede carecer de la experiencia necesaria.


Además, el sistema central suele estar diseñado teniendo en cuenta un género o juego específico, lo que dificulta la adaptación a un nuevo proyecto.


Una vez más, aquí es donde entra en juego un marco universal, y si bien crear juegos que sean muy diferentes entre sí puede parecer un desafío insuperable, hay ejemplos de plataformas que han abordado con éxito este problema: PlayFab, Photon Engine y plataformas similares. han demostrado su capacidad para reducir significativamente el tiempo de desarrollo, lo que permite a los desarrolladores centrarse en crear juegos en lugar de infraestructura.


Ahora, saltemos a nuestra historia.

La necesidad de la singularidad

Para los juegos multijugador, un backend robusto es esencial. Un buen ejemplo: nuestro juego estrella, War Robots. Es un juego de disparos PvP móvil, existe desde hace más de 10 años y ha acumulado numerosas funciones que requieren soporte de backend. Y aunque el código de nuestro servidor se adaptó a las características específicas del proyecto, utilizaba tecnologías que habían quedado obsoletas.


Cuando llegó el momento de desarrollar un nuevo juego, nos dimos cuenta de que intentar reutilizar los componentes del servidor de War Robots sería problemático. El código era demasiado específico para el proyecto y requería experiencia en tecnologías de las que carecía el nuevo equipo.


También reconocimos que el éxito del nuevo proyecto no estaba garantizado y, incluso si tuviera éxito, eventualmente necesitaríamos crear otro juego nuevo y nos enfrentaríamos al mismo problema de "pizarra en blanco". Para evitar esto y prepararnos para el futuro, decidimos identificar los componentes esenciales necesarios para el desarrollo de juegos y luego crear un marco universal que podría usarse en todos los proyectos futuros.


Nuestro objetivo era proporcionar a los desarrolladores una herramienta que les ahorrara la necesidad de diseñar repetidamente arquitecturas de backend, esquemas de bases de datos, protocolos de interacción y tecnologías específicas. Queríamos liberar a la gente de la carga de implementar autorizaciones, procesamiento de pagos y almacenamiento de información del usuario, permitiéndoles centrarse en los aspectos centrales del juego: jugabilidad, diseño, lógica empresarial y más.


Además, no solo queríamos acelerar el desarrollo con nuestro nuevo marco, sino también permitir a los programadores de clientes escribir lógica del lado del servidor sin conocimientos profundos de redes, DBMS o infraestructura.


Más allá de eso, al estandarizar un conjunto de servicios, nuestro equipo de DevOps podría tratar todos los proyectos de juegos de manera similar, cambiando solo las direcciones IP. Esto nos permitiría crear plantillas de secuencias de comandos de implementación reutilizables y paneles de control.


A lo largo del proceso, tomamos decisiones arquitectónicas que tuvieron en cuenta la posibilidad de reutilizar el backend en juegos futuros. Este enfoque aseguró que nuestro marco fuera flexible, escalable y adaptable a los diversos requisitos del proyecto.


(También vale la pena señalar que el desarrollo del marco no fue una isla: se creó en paralelo con otro proyecto).

Creando la plataforma

Decidimos darle a Singularity un conjunto de funciones independientes del género, escenario o jugabilidad principal de un juego, que incluyen:

  • Autenticación
  • Almacenamiento de datos del usuario
  • Configuración del juego y análisis del equilibrio
  • Procesando pago
  • Distribución de pruebas AB
  • Integración del servicio de análisis
  • Panel de administración del servidor


Estas funciones son fundamentales para cualquier proyecto móvil multiusuario (como mínimo, son relevantes para proyectos desarrollados en Pixonic).


Además de estas funciones principales, Singularity fue diseñado para acomodar características más específicas del proyecto más cercanas a la lógica empresarial. Estas capacidades se crean mediante abstracciones, lo que las hace reutilizables y extensibles en diferentes proyectos.


Algunos ejemplos incluyen:

  • Misiones
  • Ofertas
  • Lista de amigos
  • Casamentero
  • Tablas de calificación
  • Estado en línea de los jugadores.
  • Notificaciones en el juego



Técnicamente, la plataforma Singularity consta de cuatro componentes:

  • SDK de servidor: se trata de un conjunto de bibliotecas a partir de las cuales los programadores de juegos pueden desarrollar sus servidores.
  • SDK de cliente: también un conjunto de bibliotecas, pero para desarrollar una aplicación móvil.
  • Un conjunto de microservicios listos para usar: son servidores listos para usar que no requieren modificación. Entre ellos se encuentran el servidor de autenticación, el servidor de equilibrio y otros.
  • Bibliotecas de extensiones: estas bibliotecas ya implementan varias funciones, como ofertas, misiones, etc. Los programadores de juegos pueden habilitar estas extensiones si su juego lo requiere.


A continuación, examinemos cada uno de estos componentes.


SDK de servidor

Algunos servicios, como el servicio de perfiles y el emparejamiento, requieren una lógica empresarial específica del juego. Para dar cabida a esto, hemos diseñado estos servicios para que se distribuyan como bibliotecas. Luego, basándose en estas bibliotecas, los desarrolladores pueden crear aplicaciones que incorporen controladores de comandos, lógica de emparejamiento y otros componentes específicos del proyecto.


Este enfoque es análogo a la creación de una aplicación ASP.NET, donde el marco proporciona funcionalidad de protocolo HTTP de bajo nivel; mientras tanto, el desarrollador puede centrarse en crear controladores y modelos que contengan la lógica empresarial.


Por ejemplo, digamos que queremos agregar la posibilidad de cambiar los nombres de usuario dentro del juego. Para hacer esto, los programadores necesitarían escribir una clase de comando que incluya el nuevo nombre de usuario y un controlador para este comando.


Aquí hay un ejemplo de un comando ChangeName:

 public class ChangeNameCommand : ICommand { public string Name { get; set; } }


Un ejemplo de este controlador de comandos:

 class ChangeNameCommandHandler : ICommandHandler<ChangeNameCommand> { private IProfile Profile { get; } public ChangeNameCommandHandler(IProfile profile) { Profile = profile; } public void Handle(ICommandContext context, ChangeNameCommand command) { Profile.Name = command.Name; } }


En este ejemplo, el controlador debe inicializarse con una implementación de IProfile, que se maneja automáticamente mediante inyección de dependencia. Algunos modelos, como IProfile, IWallet y IInventory, están disponibles para su implementación sin pasos adicionales. Sin embargo, puede que no sea muy conveniente trabajar con estos modelos debido a su naturaleza abstracta, ya que proporcionan datos y aceptan argumentos que no se adaptan a las necesidades específicas del proyecto.


Para facilitar las cosas, los proyectos pueden definir sus propios modelos de dominio, registrarlos de manera similar a los controladores e inyectarlos en los constructores según sea necesario. Este enfoque permite una experiencia más personalizada y conveniente al trabajar con datos.


A continuación se muestra un ejemplo de un modelo de dominio:

 public class WRProfile { public readonly IProfile Raw; public WRProfile(IProfile profile) { Raw = profile; } public int Level { get => Raw.Attributes["level"].AsInt(); set => Raw.Attributes["level"] = value; } }


De forma predeterminada, el perfil del jugador no contiene la propiedad Nivel. Sin embargo, al crear un modelo específico del proyecto, se puede agregar este tipo de propiedad y se puede leer o cambiar fácilmente la información a nivel de jugador en los controladores de comandos.


Un ejemplo de un controlador de comandos que utiliza el modelo de dominio:

 class LevelUpCommandHandler : ICommandHandler<LevelUpCommand> { private WRProfile Profile { get; } public LevelUpCommandHandler(WRProfile profile) { Profile = profile; } public void Handle(ICommandContext context, LevelUpCommand command) { Profile.Level += 1; } }


Ese código demuestra claramente que la lógica empresarial de un juego específico está aislada de las capas subyacentes de transporte o almacenamiento de datos. Esta abstracción permite a los programadores centrarse en la mecánica central del juego sin preocuparse por la transaccionalidad, las condiciones de carrera u otros problemas comunes del backend.


Además, Singularity ofrece una gran flexibilidad para mejorar la lógica del juego. El perfil del jugador es una colección de pares de "valores clave escritos", que permiten a los diseñadores de juegos agregar fácilmente cualquier propiedad, tal como lo imaginan.


Más allá del perfil, la entidad del jugador en Singularity se compone de varios componentes esenciales diseñados para mantener la flexibilidad. En particular, esto incluye una billetera que rastrea la cantidad de cada moneda que contiene, así como un inventario que enumera los artículos del jugador.


Curiosamente, los elementos de Singularity son entidades abstractas similares a los perfiles; cada elemento tiene un identificador único y un conjunto de pares de valores escritos por clave. Por lo tanto, un elemento no tiene por qué ser necesariamente un objeto tangible como un arma, ropa o recurso en el mundo del juego. En cambio, puede representar cualquier descripción general emitida exclusivamente para los jugadores, como una misión u oferta. En la siguiente sección, detallaré cómo se implementan estos conceptos dentro de un proyecto de juego específico.


Una diferencia clave en Singularity es que los artículos almacenan una referencia a una descripción general en el balance. Si bien esta descripción permanece estática, las propiedades del artículo individual emitido pueden cambiar. Por ejemplo, a los jugadores se les puede dar la posibilidad de cambiar los diseños de las armas.


Además, tenemos opciones sólidas para migrar datos de jugadores. En el desarrollo backend tradicional, el esquema de la base de datos suele estar estrechamente vinculado con la lógica empresarial y los cambios en las propiedades de una entidad normalmente requieren modificaciones directas del esquema.


Sin embargo, el enfoque tradicional no es adecuado para Singularity porque el marco carece de conocimiento de las propiedades comerciales asociadas con una entidad de jugador y el equipo de desarrollo del juego carece de acceso directo a la base de datos. En cambio, las migraciones se diseñan y registran como controladores de comandos que operan sin interacción directa con el repositorio. Cuando un jugador se conecta al servidor, sus datos se obtienen de la base de datos. Si alguna migración registrada en el servidor aún no se ha aplicado a este reproductor, se ejecuta y el estado actualizado se guarda en la base de datos.


La lista de migraciones aplicadas también se almacena como propiedad del jugador y este enfoque tiene otra ventaja importante: permite escalonar las migraciones a lo largo del tiempo. Esto nos permite evitar tiempos de inactividad y problemas de rendimiento que podrían causar cambios masivos de datos, como cuando se agrega una nueva propiedad a todos los registros del reproductor y se establece en un valor predeterminado.

SDK del cliente

Singularity ofrece una interfaz sencilla para la interacción backend, lo que permite a los equipos de proyecto centrarse en el desarrollo de juegos sin preocuparse por cuestiones de protocolo o tecnologías de comunicación de red. (Dicho esto, el SDK proporciona la flexibilidad de anular los métodos de serialización predeterminados para comandos específicos del proyecto si es necesario).


El SDK permite la interacción directa con la API, pero también incluye un contenedor que automatiza las tareas rutinarias. Por ejemplo, ejecutar un comando en el servicio de perfil genera un conjunto de eventos que indican cambios en el perfil del jugador. El contenedor aplica estos eventos al estado local, asegurando que el cliente mantenga la versión actual del perfil.


A continuación se muestra un ejemplo de una llamada de comando:

 var result = _sandbox.ExecSync(new LevelUpCommand())


Microservicios listos para usar

La mayoría de los servicios de Singularity están diseñados para ser versátiles y no requieren personalización para proyectos específicos. Estos servicios se distribuyen como aplicaciones prediseñadas y se pueden utilizar en varios juegos.


El conjunto de servicios listos para usar incluye:

  • Una puerta de entrada para las solicitudes de los clientes.
  • Un servicio de autenticación
  • Un servicio para analizar y almacenar configuraciones y tablas de equilibrio.
  • Un servicio de estado en línea
  • un servicio de amigos
  • Un servicio de clasificación


Algunos servicios son fundamentales para la plataforma y deben implementarse, como el servicio de autenticación y la puerta de enlace. Otros son opcionales, como el servicio de amigos y la tabla de clasificación, y pueden excluirse del entorno de juegos que no los requieran.

Hablaré de las cuestiones relacionadas con la gestión de una gran cantidad de servicios más adelante, pero por ahora, es esencial enfatizar que los servicios opcionales deben seguir siendo opcionales. A medida que crece el número de servicios, también aumentan la complejidad y el umbral de incorporación para nuevos proyectos.


Bibliotecas de extensión

Si bien el marco central de Singularity es bastante capaz, los equipos de proyecto pueden implementar características importantes de forma independiente sin modificar el núcleo. Cuando la funcionalidad se identifica como potencialmente beneficiosa para múltiples proyectos, el equipo del marco puede desarrollarla y publicarla como bibliotecas de extensión separadas. Estas bibliotecas luego se pueden integrar y utilizar controladores de comandos en el juego.


Algunos ejemplos de características que podrían aplicarse aquí son misiones y ofertas. Desde la perspectiva del marco central, estas entidades son simplemente elementos asignados a los jugadores. Sin embargo, las bibliotecas de extensiones pueden dotar a estos elementos de propiedades y comportamientos específicos, transformándolos en misiones u ofertas. Esta capacidad permite la modificación dinámica de las propiedades de los elementos, permitiendo el seguimiento del progreso de la misión o registrando la última fecha en que se presentó una oferta al jugador.


Resultados hasta ahora

Singularity se ha implementado con éxito en uno de nuestros últimos juegos disponibles a nivel mundial, Little Big Robots, y esto ha dado a los desarrolladores del cliente el poder de manejar ellos mismos la lógica del servidor. Además, hemos podido crear prototipos que utilizan la funcionalidad existente sin la necesidad de soporte directo del equipo de la plataforma.


Sin embargo, esta solución universal no está exenta de desafíos. A medida que se ha ampliado el número de funciones, también lo ha hecho la complejidad de interactuar con la plataforma. Singularity ha evolucionado de una herramienta simple a un sistema sofisticado e intrincado, similar en algunos aspectos a la transición de un teléfono básico con botón a un teléfono inteligente con todas las funciones.


Si bien Singularity ha aliviado la necesidad de que los desarrolladores se sumerjan en las complejidades de las bases de datos y la comunicación en red, también ha introducido su propia curva de aprendizaje. Los desarrolladores ahora necesitan comprender los matices de Singularity en sí, lo que puede suponer un cambio significativo.


Los desafíos los enfrentan personas que van desde desarrolladores hasta administradores de infraestructura. Estos profesionales suelen tener una gran experiencia en la implementación y el mantenimiento de soluciones conocidas como Postgres y Kafka. Sin embargo, Singularity es un producto interno que requiere que adquieran nuevas habilidades: necesitan aprender las complejidades de los clústeres de Singularity, diferenciar entre servicios requeridos y opcionales y comprender qué métricas son críticas para el monitoreo.


Si bien es cierto que dentro de una empresa, los desarrolladores siempre pueden pedir consejo a los creadores de la plataforma, este proceso inevitablemente requiere tiempo. Nuestro objetivo es minimizar la barrera de entrada tanto como sea posible. Lograr esto requiere documentación completa para cada característica nueva, lo que puede ralentizar el desarrollo, pero de todos modos se considera una inversión en el éxito a largo plazo de la plataforma. Además, una cobertura sólida de pruebas unitarias y de integración es esencial para garantizar la confiabilidad del sistema.


Singularity depende en gran medida de las pruebas automatizadas porque las pruebas manuales requerirían desarrollar instancias de juego separadas, lo cual no es práctico. Las pruebas automatizadas pueden detectar la gran mayoría (es decir, el 99%) de los errores. Sin embargo, siempre hay un pequeño porcentaje de problemas que sólo se hacen evidentes durante las pruebas específicas del juego. Esto puede afectar los cronogramas de lanzamiento porque el equipo de Singularity y los equipos de proyecto a menudo trabajan de forma asincrónica. Es posible que se encuentre un error de bloqueo en un código escrito hace mucho tiempo y que el equipo de desarrollo de la plataforma esté ocupado con otra tarea crítica. (Este desafío no es exclusivo de Singularity y también puede ocurrir en el desarrollo de backend personalizado).


Otro desafío importante es gestionar las actualizaciones en todos los proyectos que utilizan Singularity. Normalmente, hay un proyecto emblemático que impulsa el desarrollo del marco con un flujo constante de solicitudes de funciones y mejoras. La interacción con el equipo de este proyecto es muy unida; Entendemos sus necesidades y cómo pueden aprovechar nuestra plataforma para resolver sus problemas.


Si bien algunos proyectos emblemáticos están estrechamente relacionados con el equipo del marco, otros juegos en etapas iniciales de desarrollo a menudo funcionan de forma independiente, basándose únicamente en la funcionalidad y la documentación existentes. En ocasiones, esto puede dar lugar a soluciones redundantes o subóptimas, ya que los desarrolladores pueden malinterpretar la documentación o hacer un mal uso de las funciones disponibles. Para mitigar esto, es crucial facilitar el intercambio de conocimientos a través de presentaciones, reuniones e intercambios de equipos, aunque tales iniciativas requieren una inversión considerable de tiempo.

El futuro

Singularity ya ha demostrado su valor en nuestros juegos y está preparado para seguir evolucionando. Si bien planeamos introducir nuevas funciones, nuestro enfoque principal en este momento es garantizar que estas mejoras no compliquen la usabilidad de la plataforma para los equipos de proyecto.

Además de esto, es necesario reducir la barrera de entrada, simplificar la implementación, agregar flexibilidad en términos de análisis, permitiendo que los proyectos conecten sus soluciones. Este es un desafío para el equipo, pero creemos y vemos en la práctica que los esfuerzos invertidos en nuestra solución definitivamente darán sus frutos.