Imagen cortesía de Joy Way

Joy Way, la desarrolladora de STRIDE, explica sus métodos de RV multijugador multiplataforma.

Artem Tarasov, director de multijugador de Joy Way
Hola, soy Artem Tarasov, director de multijugador de Joy Way.

Joy Way es una compañía chipriota de desarrollo y distribución de videojuegos de RV. Empezamos hace unos seis años y hemos trabajado en varios proyectos para PC y RV individual en SteamVR, Meta Quest, PlayStation VR, Pico, y otros.

El desarrollo de STRIDE, nuestro juego de acción de parkour en RV, fue un desafío para el equipo, sobre todo los modos multijugador, que permiten el juego multiplataforma. Tuvimos que trabajar con varias plataformas, dispositivos de entrada, locomoción basada en físicas, un registro de impactos adecuado y otras tareas.

Esta entrada de blog tratará sobre cómo solucionamos algunas dificultades que surgieron durante el desarrollo de los modos de juego multijugador de STRIDE.
 

Esta entrada de blog puede usarse como guía para usar Unreal Engine y desarrollar un juego multiplataforma y conectarlo con servicios backend personalizados. Este blog será útil para los programadores de Unreal que trabajan tanto con C++ como con blueprints.

Redacción de código para plataformas específicas

El primer desafío al que se enfrenta todo desarrollador al desarrollar un juego multiplataforma es la necesidad de redactar código específico para cada plataforma.

El primer caso y el más común es usar complementos y módulos de plataformas específicas. En Unreal, puedes añadir plataformas a una lista blanca o negra para cada módulo por separado.
Con respecto a los complementos, se puede hacer lo mismo con BlacklistPlatforms, SupportedTargetPlatforms, y WhitelistPlatforms (escogiendo uno o varios parámetros).
Tras añadir plataformas a una lista blanca o negra para usar los módulos como dependencias de C++, es necesario incluir el nombre del módulo en PublicDependencyModuleNames y/o PrivateDependencyModuleNames. A partir de ese momento, las cosas dejan de ser triviales.

Lo primero es excluir los módulos que no son compatibles con ciertas plataformas. La forma más sencilla de hacerlo es usar condiciones «if/else» en el archivo de módulos .Build.cs.

Por ejemplo:
Lo segundo es usar marcas de compilación de plataformas específicas. Por ejemplo:
Más información sobre directivas de preprocesador C++ aquí.
Un mapa multijugador de STRIDE

Gestionar el uso de un solo SO en diversas plataformas.

Al desarrollar STRIDE, nos enfrentamos al problema de que algunas de las plataformas de RV a las que estaba destinado (Pico y Meta Quest) funcionaban con Android. Esto dio pie a varios problemas: a partir de ese punto, no podíamos usar las configuraciones de plataformas de Unreal para separar los valores de configuración interconectados de Pico y Quest. De forma predeterminada, en el código no se puede determinar en qué plataforma estás. Estas fueron nuestras soluciones.

El problema de los valores de configuración se puede resolver introduciendo argumentos «-ini» en la herramienta Unreal Build. Se puede implementar en los scripts de compilación de integración y entrega continuas y pasar las configuraciones «-ini» a la herramienta de automatización de Unreal para anular las configuraciones. Se pasa en este formato:
Más información sobre introducciones de configuración aquí. También hay una forma de aplicar los valores de configuración introducidos en un juego en paquete modificando la herramienta de automatización (UAT) de Unreal. Más información aquí.

Ahora se puede pasar un valor de configuración a UAT para determinar en qué plataforma estás.

El siguiente paso es añadir definiciones de plataformas para los dispositivos Pico y Quest. Hay que tener en cuenta la diferencia entre GlobalDefinitions de Target Rules y PublicDefinitions de Module Rules. Por defecto, UAT produce dos versiones ejecutables: una para el editor (para ejecutar commandlets durante la compilación) y una para la plataforma objetivo. GlobalDefinitions son definiciones para el objetivo en conjunto. Eso significa que, si pones una definición para la plataforma Pico/Quest en GlobalDefinitions, la compilación del editor (necesaria para ejecutar commandlets) también incluirá la definición de la plataforma, cosa que no queremos. Teniendo en cuenta esto, conviene poner la definición de la plataforma en PublicDefinitions, en Module Rules. Esta es la manera de hacerlo:
Ahora puedes usar directivas de preprocesador conocidas para determinar en qué plataforma estás en código C++ y usar BlueprintFunctionLibrary para exponer comprobaciones de plataformas a blueprints.

Conectar el juego a servicios backend personalizados

Esta parte del blog mostrará nuestro enfoque a la hora de integrar servicios backend. Nuestro backend consiste en varias API HTTP y una API WebSocket. La implementación de solicitudes HTTP y eventos WebSocket es relativamente sencilla, así que me centraré en cómo encadenamos llamadas asíncronas.
Ubicación del centro multiplataforma en STRIDE
Al principio, empezamos implementando llamadas API usando llamadas a la función lambda para obtener respuestas. Sin embargo, pronto nos encontramos con una gran cantidad de llamadas anidadas y el código se volvió muy difícil de mantener. Así que decidimos hacer que cada solicitud fuera una UBlueprintAsyncActionBase separada.

Usar nodos de UBlueprintAsyncActionBase generados automáticamente en blueprints es bastante sencillo. Aquí está la guía de programación.
 
Get Stride Net User Data

Sin embargo, aún hay un problema importante que resolver: ¿dónde llamamos a esos nodos? En algunos casos, se pueden hacer llamadas a entidades dentro del juego, lo cual es una buena ubicación. Pero, ¿qué hay de las llamadas a nivel de GameInstance? Nuestra solución es usar un UObject extendido para la entidad «Worker».

Las entidades Workers son clases derivadas de UObject para controlar el ciclo vital del que usamos GameInstanceSubsystems. Más información sobre subsistemas de programación aquí. No es necesario usar GameInstanceSubsystems. Además, seguramente, usar LocalPlayerSubsystems sería una solución mejor.

Workers simplifica mucho el mantenimiento de cadenas de llamadas.
Ahora desmitificaré la extensión de UObject y daré algunos consejos no muy obvios.

En primer lugar, solo hace falta extender una cosa. GetWorld para llamar a funciones globales con WorldContext. Aquí hay un ejemplo de sustitución de código de Worker GetWorld.
Presta atención a la primera comprobación en CDO. Tendrá sentido más adelante.

Aquí está el ejemplo del código con la creación de Worker.
Resumiendo lo anterior. Hay que heredar de UObject, sustituir GetWorld, heredar de C++ Worker, y por último, citar el objeto correcto en el núcleo de ciclo vital de Worker. Así es como se obtiene una categoría de blueprints para citar el objeto correcto.
Ahora pasamos al CDO. Como ves, asignamos la variable TSubclassOf en la construcción de GameInstanceSubsystem. Ahí puede surgir un problema si no se comprueba el CDO. A nosotros nos falló el editor y tuvimos problemas serios con recursos del sistema sin comprobar el CDO, así que quizás a ti también te pase.

Al separar cada solicitud y exponer cadenas de solicitudes a gráficas de blueprints logramos librarnos del «código espagueti» y pudimos hacer código relacionado con backend mucho más sencillo de mantener, lo cual generó iteraciones más rápidas y menos errores.
Si quieres obtener más información sobre nuestros juegos y presenciar el desarrollo de juegos de RV, sigue a Joy Way en Twitter y únete a nuestro servidor de Discord.

    ¡Hazte ya con Unreal Engine!

    Consigue la herramienta de creación más abierta y avanzada del mundo.
    Unreal Engine incluye todas las funciones y acceso ilimitado al código fuente,
    ¡listo para usar!