Bonjour! Je m'appelle Andrey Makhorin, développeur de serveurs chez Pixonic (MY.GAMES). Dans cet article, je vais partager comment mon équipe et moi avons créé une solution universelle pour le développement backend. Vous découvrirez le concept, son résultat et la façon dont notre système, appelé Singularity, a fonctionné dans des projets du monde réel. J'aborderai également en profondeur les défis auxquels nous avons été confrontés.
Lorsqu'un studio de jeux démarre, il est crucial de formuler et de mettre en œuvre rapidement une idée convaincante : des dizaines d'hypothèses sont testées et le jeu subit des changements constants ; de nouvelles fonctionnalités sont ajoutées et les solutions infructueuses sont révisées ou rejetées. Cependant, ce processus d’itération rapide, associé à des délais serrés et à un horizon de planification court, peut conduire à l’accumulation de dette technique.
Avec la dette technique existante, la réutilisation d’anciennes solutions peut s’avérer compliquée puisqu’il faut résoudre divers problèmes avec celles-ci. Ce n’est évidemment pas optimal. Mais il existe une autre solution : un « cadre universel ». En concevant des composants génériques et réutilisables (tels que des éléments de mise en page, des fenêtres et des bibliothèques qui implémentent des interactions réseau), les studios peuvent réduire considérablement le temps et les efforts nécessaires au développement de nouvelles fonctionnalités. Cette approche réduit non seulement la quantité de code que les développeurs doivent écrire, mais garantit également que le code a déjà été testé, ce qui réduit le temps consacré à la maintenance.
Nous avons discuté du développement de fonctionnalités dans le contexte d'un jeu, mais regardons maintenant la situation sous un autre angle : pour tout studio de jeux, la réutilisation de petits morceaux de code au sein d'un projet peut être une stratégie efficace pour rationaliser la production, mais à terme, ils Je devrai créer un nouveau jeu à succès. La réutilisation des solutions d’un projet existant pourrait, en théorie, accélérer ce processus, mais deux obstacles importants se posent. Tout d’abord, les mêmes problèmes techniques de dette s’appliquent ici, et deuxièmement, toutes les anciennes solutions étaient probablement adaptées aux exigences spécifiques du jeu précédent, ce qui les rendait mal adaptées au nouveau projet.
Ces problèmes sont aggravés par d'autres problèmes : la conception de la base de données peut ne pas répondre aux exigences du nouveau projet, les technologies peuvent être obsolètes et la nouvelle équipe peut ne pas posséder l'expertise nécessaire.
De plus, le système de base est souvent conçu pour un genre ou un jeu spécifique, ce qui rend difficile son adaptation à un nouveau projet.
Encore une fois, c'est là qu'un cadre universel entre en jeu, et même si créer des jeux très différents les uns des autres peut sembler un défi insurmontable, il existe des exemples de plateformes qui ont réussi à résoudre ce problème : PlayFab, Photon Engine et des plateformes similaires. ont démontré leur capacité à réduire considérablement le temps de développement, permettant aux développeurs de se concentrer sur la création de jeux plutôt que sur l'infrastructure.
Maintenant, passons à notre histoire.
Pour les jeux multijoueurs, un backend robuste est essentiel. Exemple concret : notre jeu phare, War Robots. Il s'agit d'un jeu de tir PvP mobile, il existe depuis plus de 10 ans et il accumule de nombreuses fonctionnalités nécessitant un support backend. Et même si le code de notre serveur était adapté aux spécificités du projet, il utilisait des technologies devenues obsolètes.
Quand est venu le temps de développer un nouveau jeu, nous avons réalisé qu'essayer de réutiliser les composants du serveur de War Robots serait problématique. Le code était trop spécifique au projet et nécessitait une expertise technologique qui manquait à la nouvelle équipe.
Nous avons également reconnu que le succès du nouveau projet n'était pas garanti et que, même s'il réussissait, nous devions éventuellement créer un autre nouveau jeu, et nous serions confrontés au même problème de « table rase ». Pour éviter cela et assurer une certaine pérennité, nous avons décidé d'identifier les composants essentiels requis pour le développement de jeux, puis de créer un cadre universel qui pourrait être utilisé dans tous les projets futurs.
Notre objectif était de fournir aux développeurs un outil qui leur éviterait d'avoir à concevoir à plusieurs reprises des architectures backend, des schémas de bases de données, des protocoles d'interaction et des technologies spécifiques. Nous voulions libérer les gens du fardeau de la mise en œuvre des autorisations, du traitement des paiements et du stockage des informations utilisateur, leur permettant de se concentrer sur les aspects essentiels du jeu : le gameplay, la conception, la logique métier, et bien plus encore.
De plus, nous souhaitions non seulement accélérer le développement avec notre nouveau framework, mais également permettre aux programmeurs clients d'écrire une logique côté serveur sans connaissance approfondie des réseaux, des SGBD ou de l'infrastructure.
Au-delà de cela, en standardisant un ensemble de services, notre équipe DevOps serait en mesure de traiter tous les projets de jeux de la même manière, seules les adresses IP changeant. Cela nous permettrait de créer des modèles de scripts de déploiement et des tableaux de bord de surveillance réutilisables.
Tout au long du processus, nous avons pris des décisions architecturales qui ont pris en compte la possibilité de réutiliser le backend dans les futurs jeux. Cette approche garantissait que notre cadre serait flexible, évolutif et adaptable aux diverses exigences du projet.
(Il convient également de noter que le développement du cadre n'était pas une île : il a été créé en parallèle avec un autre projet.)
Nous avons décidé de doter Singularity d'un ensemble de fonctions indépendantes du genre, du décor ou du gameplay principal d'un jeu, notamment :
Ces fonctions sont fondamentales pour tout projet mobile multi-utilisateurs (à tout le moins, elles sont pertinentes pour les projets développés dans Pixonic).
En plus de ces fonctions de base, Singularity a été conçu pour accueillir davantage de fonctionnalités spécifiques au projet, plus proches de la logique métier. Ces fonctionnalités sont construites à l'aide d'abstractions, ce qui les rend réutilisables et extensibles dans différents projets.
Voici quelques exemples :
Techniquement, la plateforme Singularity se compose de quatre composants :
Ensuite, examinons chacun de ces composants.
Certains services, comme le service de profil et le matchmaking, nécessitent une logique métier spécifique au jeu. Pour répondre à cela, nous avons conçu ces services pour qu'ils soient distribués sous forme de bibliothèques. En s'appuyant ensuite sur ces bibliothèques, les développeurs peuvent créer des applications intégrant des gestionnaires de commandes, une logique de mise en relation et d'autres composants spécifiques au projet.
Cette approche est analogue à la création d'une application ASP.NET, où le framework fournit des fonctionnalités de protocole HTTP de bas niveau, tandis que le développeur peut se concentrer sur la création de contrôleurs et de modèles contenant la logique métier.
Par exemple, disons que nous souhaitons ajouter la possibilité de modifier les noms d'utilisateur dans le jeu. Pour ce faire, les programmeurs devront écrire une classe de commandes incluant le nouveau nom d'utilisateur et un gestionnaire pour cette commande.
Voici un exemple de ChangeNameCommand :
public class ChangeNameCommand : ICommand { public string Name { get; set; } }
Un exemple de ce gestionnaire de commandes :
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; } }
Dans cet exemple, le gestionnaire doit être initialisé avec une implémentation IProfile, qui est gérée automatiquement via l'injection de dépendances. Certains modèles, tels que IProfile, IWallet et IInventory, sont disponibles pour une mise en œuvre sans étapes supplémentaires. Cependant, ces modèles peuvent ne pas être très pratiques à utiliser en raison de leur nature abstraite, fournissant des données et acceptant des arguments qui ne sont pas adaptés aux besoins spécifiques du projet.
Pour faciliter les choses, les projets peuvent définir leurs propres modèles de domaine, les enregistrer de la même manière auprès des gestionnaires et les injecter dans les constructeurs selon les besoins. Cette approche permet une expérience plus personnalisée et plus pratique lorsque vous travaillez avec des données.
Voici un exemple de modèle de domaine :
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; } }
Par défaut, le profil du joueur ne contient pas la propriété Level. Cependant, en créant un modèle spécifique au projet, ce type de propriété peut être ajouté et on peut facilement lire ou modifier les informations au niveau du joueur dans les gestionnaires de commandes.
Un exemple de gestionnaire de commandes utilisant le modèle de domaine :
class LevelUpCommandHandler : ICommandHandler<LevelUpCommand> { private WRProfile Profile { get; } public LevelUpCommandHandler(WRProfile profile) { Profile = profile; } public void Handle(ICommandContext context, LevelUpCommand command) { Profile.Level += 1; } }
Ce code démontre clairement que la logique métier d'un jeu spécifique est isolée des couches de transport ou de stockage de données sous-jacentes. Cette abstraction permet aux programmeurs de se concentrer sur les mécanismes de base du jeu sans se soucier de la transactionnalité, des conditions de concurrence ou d'autres problèmes courants du backend.
De plus, Singularity offre une grande flexibilité pour améliorer la logique du jeu. Le profil du joueur est une collection de paires de « valeurs saisies par clé », permettant aux concepteurs de jeux d'ajouter facilement n'importe quelle propriété, comme ils l'imaginent.
Au-delà du profil, l’entité joueur dans Singularity est composée de plusieurs composants essentiels destinés à maintenir une flexibilité. Cela inclut notamment un portefeuille qui suit le montant de chaque devise qu'il contient ainsi qu'un inventaire qui répertorie les objets du joueur.
Il est intéressant de noter que les éléments de Singularity sont des entités abstraites similaires aux profils ; chaque élément possède un identifiant unique et un ensemble de paires de valeurs saisies par clé. Ainsi, un objet ne doit pas nécessairement être un objet tangible comme une arme, un vêtement ou une ressource dans le monde du jeu. Au lieu de cela, il peut représenter n'importe quelle description générale délivrée uniquement aux joueurs, comme une quête ou une offre. Dans la section suivante, je détaillerai comment ces concepts sont implémentés dans un projet de jeu spécifique.
Une différence clé dans Singularity est que les éléments stockent une référence à une description générale dans le bilan. Bien que cette description reste statique, les propriétés de chaque élément émis peuvent changer. Par exemple, les joueurs peuvent avoir la possibilité de changer de skin d’arme.
De plus, nous disposons d’options robustes pour migrer les données des joueurs. Dans le développement back-end traditionnel, le schéma de base de données est souvent étroitement associé à la logique métier, et les modifications apportées aux propriétés d'une entité nécessitent généralement des modifications directes du schéma.
Cependant, l'approche traditionnelle n'est pas adaptée à Singularity car le framework ne connaît pas les propriétés commerciales associées à une entité de joueur et l'équipe de développement du jeu n'a pas d'accès direct à la base de données. Au lieu de cela, les migrations sont conçues et enregistrées en tant que gestionnaires de commandes qui fonctionnent sans interaction directe avec le référentiel. Lorsqu'un joueur se connecte au serveur, ses données sont récupérées dans la base de données. Si des migrations enregistrées sur le serveur n'ont pas encore été appliquées à ce lecteur, elles sont exécutées et l'état mis à jour est enregistré dans la base de données.
La liste des migrations appliquées est également stockée en tant que propriété du joueur, et cette approche présente un autre avantage non négligeable : elle permet d'étaler les migrations dans le temps. Cela nous permet d'éviter les temps d'arrêt et les problèmes de performances que des modifications massives des données pourraient autrement provoquer, par exemple lors de l'ajout d'une nouvelle propriété à tous les enregistrements de joueurs et de sa définition d'une valeur par défaut.
Singularity offre une interface simple pour l'interaction back-end, permettant aux équipes de projet de se concentrer sur le développement de jeux sans se soucier des problèmes de protocole ou de technologies de communication réseau. (Cela dit, le SDK offre la possibilité de remplacer les méthodes de sérialisation par défaut pour les commandes spécifiques au projet si nécessaire.)
Le SDK permet une interaction directe avec l'API, mais il comprend également un wrapper qui automatise les tâches de routine. Par exemple, l'exécution d'une commande sur le service de profil génère un ensemble d'événements qui indiquent des changements dans le profil du joueur. Le wrapper applique ces événements à l'état local, garantissant que le client conserve la version actuelle du profil.
Voici un exemple d'appel de commande :
var result = _sandbox.ExecSync(new LevelUpCommand())
La plupart des services de Singularity sont conçus pour être polyvalents et ne nécessitent pas de personnalisation pour des projets spécifiques. Ces services sont distribués sous forme d'applications prédéfinies et peuvent être utilisés dans divers jeux.
La suite de services prêts à l'emploi comprend :
Certains services sont fondamentaux à la plateforme et doivent être déployés, comme le service d'authentification et la passerelle. D'autres sont facultatifs, comme le service amis et le classement, et peuvent être exclus de l'environnement des jeux qui ne les nécessitent pas.
J'aborderai plus tard les problématiques liées à la gestion d'un grand nombre de services, mais pour l'instant, il est essentiel de souligner que les services optionnels doivent rester facultatifs. À mesure que le nombre de services augmente, la complexité et le seuil d'intégration des nouveaux projets augmentent également.
Bien que le cadre de base de Singularity soit tout à fait performant, des fonctionnalités importantes peuvent être implémentées indépendamment par les équipes de projet sans modifier le noyau. Lorsque la fonctionnalité est identifiée comme potentiellement bénéfique pour plusieurs projets, elle peut être développée par l'équipe du framework et publiée sous forme de bibliothèques d'extensions distinctes. Ces bibliothèques peuvent ensuite être intégrées et utilisées par des gestionnaires de commandes dans le jeu.
Quelques exemples de fonctionnalités qui pourraient s'appliquer ici sont les quêtes et les offres. Du point de vue du framework de base, ces entités sont simplement des éléments attribués aux joueurs. Cependant, les bibliothèques d'extensions peuvent conférer à ces éléments des propriétés et un comportement spécifiques, les transformant en quêtes ou en offres. Cette fonctionnalité permet une modification dynamique des propriétés des objets, permettant le suivi de la progression de la quête ou l'enregistrement de la dernière date à laquelle une offre a été présentée au joueur.
Singularity a été implémenté avec succès dans l'un de nos derniers jeux disponibles mondialement, Little Big Robots, et cela a donné aux développeurs clients le pouvoir de gérer eux-mêmes la logique du serveur. De plus, nous avons pu créer des prototypes qui utilisent les fonctionnalités existantes sans avoir besoin de l'assistance directe de l'équipe de la plateforme.
Cependant, cette solution universelle n’est pas sans défis. À mesure que le nombre de fonctionnalités a augmenté, la complexité de l’interaction avec la plateforme a également augmenté. Singularity est passé d'un simple outil à un système sophistiqué et complexe, semblable à certains égards à la transition d'un téléphone à bouton-poussoir de base à un smartphone complet.
Bien que Singularity ait évité aux développeurs d'avoir à se plonger dans les complexités des bases de données et de la communication réseau, il a également introduit sa propre courbe d'apprentissage. Les développeurs doivent désormais comprendre les nuances de la singularité elle-même, ce qui peut constituer un changement important.
Ces défis concernent des personnes allant des développeurs aux administrateurs d'infrastructure. Ces professionnels possèdent souvent une expertise approfondie dans le déploiement et la maintenance de solutions bien connues telles que Postgres et Kafka. Cependant, Singularity est un produit interne, ce qui nécessite qu'ils acquièrent de nouvelles compétences : ils doivent apprendre les subtilités des clusters de Singularity, faire la différence entre les services obligatoires et facultatifs et comprendre quelles métriques sont essentielles à la surveillance.
S'il est vrai qu'au sein d'une entreprise, les développeurs peuvent toujours s'adresser aux créateurs de la plateforme pour obtenir des conseils, mais ce processus demande inévitablement du temps. Notre objectif est de minimiser autant que possible les barrières à l’entrée. Y parvenir nécessite une documentation complète pour chaque nouvelle fonctionnalité, ce qui peut ralentir le développement, mais est néanmoins considéré comme un investissement dans le succès à long terme de la plateforme. De plus, une couverture robuste de tests unitaires et d’intégration est essentielle pour garantir la fiabilité du système.
Singularity s'appuie fortement sur des tests automatisés, car les tests manuels nécessiteraient le développement d'instances de jeu distinctes, ce qui n'est pas pratique. Les tests automatisés peuvent détecter la grande majorité, soit 99 %, des erreurs. Cependant, il existe toujours un petit pourcentage de problèmes qui ne deviennent évidents que lors de tests de jeu spécifiques. Cela peut avoir un impact sur les délais de publication, car l'équipe Singularity et les équipes de projet travaillent souvent de manière asynchrone. Une erreur de blocage peut être trouvée dans un code écrit il y a longtemps et l'équipe de développement de la plateforme peut être occupée par une autre tâche critique. (Ce défi n'est pas propre à Singularity et peut également se produire dans le développement backend personnalisé.)
Un autre défi important consiste à gérer les mises à jour dans tous les projets qui utilisent Singularity. En règle générale, il existe un projet phare qui pilote le développement du framework avec un flux constant de demandes de fonctionnalités et d'améliorations. L'interaction avec l'équipe de ce projet est étroite ; nous comprenons leurs besoins et comment ils peuvent tirer parti de notre plateforme pour résoudre leurs problèmes.
Alors que certains projets phares sont étroitement impliqués dans l'équipe du framework, d'autres jeux aux premiers stades de développement fonctionnent souvent de manière indépendante, en s'appuyant uniquement sur les fonctionnalités et la documentation existantes. Cela peut parfois conduire à des solutions redondantes ou sous-optimales, car les développeurs peuvent mal comprendre la documentation ou abuser des fonctionnalités disponibles. Pour atténuer ce problème, il est crucial de faciliter le partage des connaissances par le biais de présentations, de rencontres et d'échanges d'équipes, même si de telles initiatives nécessitent un investissement de temps considérable.
La singularité a déjà démontré sa valeur dans nos jeux et est sur le point d'évoluer davantage. Bien que nous prévoyions d'introduire de nouvelles fonctionnalités, notre objectif principal pour le moment est de garantir que ces améliorations ne compliquent pas la convivialité de la plateforme pour les équipes de projet.
En outre, il est nécessaire d’abaisser la barrière à l’entrée, de simplifier le déploiement, d’ajouter de la flexibilité en termes d’analyse, permettant aux projets de connecter leurs solutions. C'est un défi pour l'équipe, mais nous pensons et voyons en pratique que les efforts investis dans notre solution porteront certainement leurs fruits !