Présentation de l'architecture du jeu NProject

Introduction

Je présenterai dans cet article le jeu sur lequel je travail sur mon temps libre d'un point de vue technique. Celui-ci est un fan-game basé sur l'univers de Pokémon, fait pour être massivement multijoueurs, réalisé avec un ami. Le code source n'est pas public.

Il m'a toujours tenu à coeur de réaliser un jeu s'inspirant de ce monde que j'avais pu parcourir étant enfant, tout en le transformant entièrement afin de lui ajouter une touche plus réaliste et adulte, et jouable entre amis.

Dans ce jeu, qui entre dans sa deuxième année de développement, j'ai souhaité axer l'expérience joueur sur l'aspect interaction entre les joueurs. Que ce soit des joueurs pouvant bloquer d'autres joueurs, la capacité de faire des équipes de deux afin de progresser ensemble, mais également la possibilité d'endosser le rôle de maître d'arêne, j'ai voulu mettre au coeur du gameplay le sentiment d'être dans un monde vivant où les échanges entre joueurs sont primordiaux.

Rendu du jeu - Minuit

J'aborderai dans la suite de ce post plusieurs aspects liés au jeu, principalement du point de vue technique.

Réalisation - Architecture

Ce jeu est développé en Go, un langage moderne créé par Google, et dont la première version publique date de 2012. L'un des trois inventeurs n'est autre que Ken Thompson, l'un des créateurs du langage C.

L'architecture est très classique en ce qui concerne les micro-services, mais très innovante par rapport à ce que j'avais pu concevoir lors de la création de jeux précédents.

Présentation simplifiée du backend

La partie backend est constituée de quatre micro-services, à savoir:

  • Login: le service en charge d'authentifier un utilisateur, et de lui attribuer une session identifiée par son "sessionId"
  • Database: le service en charge de garantir l'échange de données entre le cache Redis, et la base de données persistées MongoDB
  • World: le service en charge du traitement des messages envoyés par les clients dans les files de messages RabbitMQ (je reviens là dessus un peu plus loin)
  • Ticker: le service ayant pour rôle d'envoyer tous les messages d'une map aux clients se trouvant dessus à intervalles réguliers afin d'améliorer les performances (diminution significative de l'utilisation réseau avec le compactage)

On retrouve également un service Registration permettant de se créer un compte sur le jeu.

Outillage

Ainsi, rien qu'avec cette petite présentation, on dégage trois acteurs en plus de ces micro-services

  • Redis: il s'agit d'un base de données NoSQL clef/valeur stockant ses valeurs dans la RAM. Cela permet un accès extrêmement rapide aux données (<1ms). Son principe de fonctionnement mono-threadé garantit par ailleurs l'atomicité de ses opérations, et permet donc de mettre très simplement en place un système de transactions. Cette base sert à stocker l'état courant du jeu, qui sera régulièrement persisté dans MongoDB à intervalles réguliers:
127.0.0.1:6379> KEYS *
  1) "world:state:map:78:environment"
  2) "world:state:map:29:environment"
  3) "world:state:map:51:monster:648827654"
  4) "world:state:map:28:environment"
  5) "world:state:map:69:environment"
  6) "world:state:map:51:monster:2569512326"
  7) "world:state:map:51:monster:3683265345"
  8) "world:state:map:4:monster:3252174890"
  9) "world:state:map:69"
 10) "world:state:map:51:monster:1433057"
...
127.0.0.1:6379> GET world:state:map:57:environment
"\x00\x06(\x02\n\x00\x00\x00\x009\x00\x00\x00\x11\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00^A\xd2\xca\x00\x00\x00\x00^@3\xcf\x19\x02\x00\x00\x00\x00\x009\x00\x00\x00\x0b\x00\x00\x00\x0e\x00\x00\x00\x01\x00\x00\x00\x00^A\xbc$\x00\x00\x00\x00^@3\xcf\"\x02\x00\x00\x00\x00\x009\x00\x00\x00\x03\x00\x00\x00\x0e\x00\x00\x00\x01\x00\x00\x00\x00^A\xbc.\x00\x00\x00\x00^@3\xcf\x17\x02\x00\x00\x00\x00\x009\x00\x00\x00\x0f\x00\x00\x00\x0b\x00\x00\x00\x01\x00\x00\x00\x00^A\xbc8\x00\x00\x00\x00^@3\xcf.\x02\x10\x00\x00\x00\x009\x00\x00\x00\r\x00\x00\x00\x0e\x00\x00\x00\x01\x00\x00\x00\x00^A\xca\x84\x00\x00\x00\x00^@3\xcf\x03\x02\x00\x00\x00\x00\x009\x00\x00\x00\x0b\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00^@A\xe4\x00\x00\x00\x00^>\xa9\xfe"

Ces données sont tous ce que peut voir le joueur sur la carte, depuis les monstres jusqu'aux baies, ainsi que les autres joueurs connectés.

  • MongoDB: il s'agit d'une base de données NoSQL stockant ses valeurs au format BSON. Cette base de données est celle qui sert à persister l'état du jeu lors de l'arrêt du serveur. Elle contient les mêmes informations que le cache Redis, mais est bien plus lente d'accès (~30ms)
> db.account.find().pretty()
{
	"_id" : ObjectId("5e11f2e7a24c297725a50c67"),
	"username" : "paprika",
	"opad" : "TRIiyAlQ",
	"ipad" : "T8g6BoSr",
	"password" : "e0305ebdc9c455cffa0bf1d76e9b44d1e7cbb924fb11a10b159d213841ea8e226bc9877fb994daa8ffab96aba15506130ca71467f71e4a68c5f936f270238b17",
	"email" : "test@test.com",
	"firstName" : "test",
	"lastName" : "test",
	"registrationDate" : ISODate("2020-01-05T14:29:59.857Z"),
	"status" : "active",
	"authorization" : "normal",
	"credit" : 0
}
  • RabbitMQ: c'est la pierre angulaire de ce projet, autour de laquelle toute l'interaction client/services est construite. MQ signifie Messages Queue, et fait référence à la capacité de RabbitMQ d'accepter en entrée des messages, et de les router via des exchanges (des "règles" de routage) dans des files d'attente. Charge ensuite au service concerné de récupérer le message et de le traiter. Cet échange a lieu dans les deux sens (queues in/out):
Illustration de la page de monitoring de RabbitMQ avec queue in et out

Monitoring

Il existe également deux outils permettant de surveiller en temps réel l'état du service, Prometheus et Grafana.

  • Prometheus: Il s'agit d'un aggrégateur de métriques. Derrière ces termes barbares, on trouve simplement un programme ayant pour rôle d'aller interroger des points de terminaison HTTP, à la recherche de ces informations:
$ curl http://127.0.0.1:16580/metrics
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 7.39e-06
go_gc_duration_seconds{quantile="0.25"} 1.8428e-05
go_gc_duration_seconds{quantile="0.5"} 3.2967e-05
go_gc_duration_seconds{quantile="0.75"} 7.7261e-05
go_gc_duration_seconds{quantile="1"} 0.000109317
go_gc_duration_seconds_sum 0.001191266
go_gc_duration_seconds_count 26
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 26
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.13.1"} 1

...

Ces informations sont donc des métriques qui peuvent prendre plusieurs formes, et qui ont pour rôle d'exprimer à un instant T l'état du service (sa charge, sa consommation mémoire, son utilisation réseau, etc). Dans le cas de l'exemple précédent, la métrique go_gc_duration_seconds est de type summary et sert à exprimer la durée des dernières collectes mémoire ayant eu lieu. La métrique go_goroutines, quant-à elle, est de type jauge, et permet de connaître à l'instant T le nombre de Go-Routine (une variante de Thread propre au langage Go).

Une métrique telle quelle n'a pas réellement de sens pour un être humain lorsqu'il s'agit d'histogramme ou de summary. C'est pour cela que vient s'ajouter un outil nommé Grafana, servant à traduire en graphes l'ensemble des métriques.

  • Grafana: grafana est un outil extrêmement puissant permettant de transformer, avec une grande liberté de personnalisation, des données aggregées provenant de différentes sources de données, dont prometheus, pour en faire des graphes percutants et aggréables à lire.
Illustration des métriques présentes sur le projet concernant le service "world"

Client/Editeur de carte

Le client de jeu est réalisé grâce à la librairie incroyable pixelgl permettant de créer des interfaces graphiques basées sur OpenGL en Go.

Client NProject
Baies, pousse accelérée
Effet poison en jeu

On retrouve également un éditeur de carte, réalisé grâce à cette même librairie.

Editeur de cartes NProject
Navigation sur la carte du monde dans l'éditeur

Il s'agit là d'une présentation globale et simplifiée de l'architure du projet. J'expliquerai plus en détails dans de prochains articles le fonctionnement et le rôle de chacun de ces composants. Une fois cette présentation faite, je me consacrerai à détailler les challenges rencontrés, et la façon dont je les ai surmontés.