Image reproduite avec l'aimable autorisation de Joy Way

Un développeur de STRIDE chez Joy Way partage ses pratiques pour gérer le multijoueur et le multiplateforme en VR

Artem Tarasov, responsable du multijoueur chez Joy Way
Bonjour, je suis Artem Tarasov, responsable du multijoueur chez Joy Way.

Basée à Chypre, Joy Way est une entreprise de développement et d'édition de jeux en VR. Depuis nos débuts il y a environ six ans, nous avons travaillé sur plusieurs projets en VR autonome et PC sous SteamVR, Meta Quest, PlayStation VR, Pico et autres plateformes.

Le développement de STRIDE, notre jeu d'action et de parkour en VR, a représenté un réel défi pour l'équipe, en particulier ses modes multijoueur en multiplateforme. Entres autres tâches, nous avons dû gérer plusieurs plateformes, divers périphériques d'entrée, le déplacement basé sur la physique et la correction de l'enregistrement des impacts.

Ce blog décrit les réponses apportées à certains des défis que nous avons rencontrés en développant les modes multijoueur de STRIDE.
 

Vous pouvez vous inspirer de ce blog pour tirer pleinement parti de l'Unreal Engine dans le cadre du développement d'un jeu multiplateforme que vous pouvez connecter à des services back-end personnalisés. Ce blog sera utile pour les développeurs Unreal travaillant en C++ comme avec Blueprint.

L'écriture de code spécifique à une plateforme

La nécessité d'écrire du code spécifique à une plateforme est le premier défi qu'un développeur rencontre lorsqu'il travaille sur un jeu multiplateforme.

Le plus souvent, on utilise des plug-ins et des modules spécifiques à la plateforme. Dans l'Unreal Engine, on peut inscrire les plateformes dans une liste blanche ou une liste noire pour chacun des modules :
En ce qui concerne les plug-ins, on peut procéder de la même manière avec BlacklistPlatforms, SupportedTargetPlatforms et WhitelistPlatforms (il faut sélectionner un ou plusieurs paramètres).
Après avoir inscrit les plateformes sur liste blanche ou noire pour utiliser des modules en tant que dépendances en C++, on doit inclure le nom des modules dans PublicDependencyModuleNames et/ou dans PrivateDependencyModuleNames. Dès lors, les choses se corsent un peu.

Premièrement, il faut exclure les modules non pris en charge par certaines plateformes. Pour ce faire, le plus simple consiste à utiliser des déclarations "if/else" dans le fichier .Build.cs des modules.

Par exemple :
Deuxièmement, il faut utiliser des indicateurs de compilation spécifiques à chaque plateforme. Par exemple :
Pour en savoir plus sur les directives du préprocesseur C++, cliquez ici.
Une carte multijoueur pour STRIDE

La gestion du multiplateforme à partir d'un seul système d'exploitation

Lors du développement de STRIDE, nous avons été confrontés au fait que certaines des plateformes de VR cibles (Pico et Meta Quest) tournaient sous Android. Cela a soulevé plusieurs problèmes, car nous ne pouvions plus utiliser les configurations de la plateforme Unreal Engine pour distinguer les valeurs de recoupement de Pico et de Meta Quest. Par défaut dans le code, on ne peut pas identifier la plateforme d'exécution. Voici nos solutions.

Pour résoudre le problème des valeurs de configuration, on injecte des arguments -ini dans l'outil Unreal Build. On peut utiliser cette technique dans les scripts d'assemblage CI/CD et pousser les configurations -ini dans l'outil d'automatisation du moteur pour remplacer les configurations. On les pousse dans le format suivant :
Pour en savoir plus sur les injections de configurations, cliquez ici. De plus, il est possible d'appliquer des injections de configurations sur un jeu assemblé en modifiant l'Unreal Automation Tool (UAT). Pour en savoir plus, cliquez ici.

Dès lors, on peut pousser une valeur de configuration vers l'UAT pour identifier la plateforme d'exécution.

L'étape suivante consiste à ajouter la définition des plateformes Pico et Meta Quest. On peut constater la différence entre les GlobalDefinitions des Target Rules et les PublicDefinitions des Module Rules. Par défaut, l'UAT produit deux assemblages : l'un pour l'éditeur (pour exécuter les Commandlets pendant l'assemblage) et l'autre pour la plateforme cible. Les GlobalDefinitions s'appliquent à l'ensemble de la cible. Ainsi, si l'on définit une plateforme Pico/Quest dans les GlobalDefinitions, alors l'assemblage de l'éditeur (nécessaire pour exécuter les Commandlets) comprend également la définition de plateformes non souhaitées. Pour ces raisons, il faut inclure la définition de la plateforme dans les PublicDefinitions des Module Rules. Voici comment procéder :
On peut maintenant utiliser les directives habituelles du préprocesseur pour identifier la plateforme d'exécution du code C++ et utiliser BlueprintFunctionLibrary pour exposer les vérifications de plateforme aux blueprints.

La connexion du jeu avec des services back-end personnalisés

Cette partie du blog décrit notre approche dans l'intégration de services back-end. Notre back-end comprend plusieurs API HTTP et une API WebSocket. L'implémentation des requêtes HTTP et des événements WebSocket est assez simple. Je vais donc me concentrer sur notre approche pour enchaîner des appels asynchrones.
L'emplacement du centre multiplateforme de STRIDE
D'abord, nous avons implémenté les appels d'API à l'aide de rappels de fonctions lambda en réponses. Toutefois, nous avons rapidement reçu un grand nombre d'appels imbriqués et le code est devenu difficile à maintenir. Nous avons donc décidé de faire correspondre à chaque requête une UBlueprintAsyncActionBase distincte.

Les nœuds UBlueprintAsyncActionBase sont générés automatiquement dans les blueprints et sont assez faciles à utiliser. Le guide de programmation est disponible ici.
 
Get Stride Net User Data

Néanmoins, nous avions toujours un important problème à résoudre : où appeler ces nœuds ? Dans certains cas, on peut faire des appels au sein des entités du jeu. C'est une bonne approche. Mais comment gérer les appels au niveau de la GameInstance ? Notre solution consiste à utiliser un UObject étendu pour l'entité «Worker ».

Les Workers forment une classe dérivée de UObject et servent à contrôler le cycle d'utilisation des GameInstanceSubsystems. Pour en savoir plus sur les sous-systèmes de programmation, cliquez ici. Il n'est pas nécessaire d'utiliser des GameInstanceSubsystems. D'autant que l'utilisation de LocalPlayerSubsystems est probablement une meilleure solution.

Les Workers facilitent beaucoup la maintenance des chaînes d'appels.
À présent, je vais démystifier l'extension des UObjects et partager quelques astuces bien cachées.

Voici le premier élément à étendre absolument. GetWorld pour appeler les fonctions globales avec WorldContext. Voici un extrait de code pour remplacer GetWorld par Worker.
Observez bien la première vérification du CDO. Ce sera plus clair.

Voici un exemple de code avec une création de Worker.
Résumons. On hérite de UObject, on remplace GetWorld, puis on hérite du Worker en C++. Enfin, on instancie le bon objet au cœur du cycle de vie du Worker. Voici comment obtenir une classe depuis les blueprints pour instancier le bon objet.
Parlons maintenant du CDO. On constate que la variable TSubclassOf est assignée à la construction de GameInstanceSubsystem. On peut alors rencontrer un problème à moins d'effectuer une vérification du CDO. Sans vérification, nous avons subi des plantages de l'éditeur et d'importants soucis avec le système des ressources. Vous pourriez en subir également.

Séparer chaque requête et exposer les chaînes de requêtes aux graphiques de Blueprint nous ont aidés à nous débarrasser du "code spaghetti" et ont facilité la maintenance du code du back-end. Nous avons ainsi accéléré nos itérations et subi moins de bugs.
Si vous souhaitez mieux connaître nos jeux et les coulisses du développement de jeux en VR, suivez Joy Way sur Twitter et rejoignez notre serveur Discord.

    Obtenez l'Unreal Engine dès maintenant !

    Procurez-vous l'outil de création le plus ouvert et le plus avancé au monde.
    L'Unreal Engine est prêt à l'emploi, avec toutes les fonctionnalités et un accès complet au code source.