JeuWeb - Crée ton jeu par navigateur

Version complète : Seelies, un jeu de stratégie persistant
Vous consultez actuellement la version basse qualité d’un document. Voir la version complète avec le bon formatage.
Pages : 1 2 3 4 5 6
Bonjour !


Je relance un projet qui me trotte dans la tête depuis des années, que j'ai maintes fois tenté et maintes fois échoué.

[Image: seelies.jpg]

Illustration d'une Seelies par Harparine.

Seelies est un jeu de stratégie proche d'un jeu de plateau dans lequel plusieurs équipes s'affrontent jusqu'à ce qu'il n'en reste plus qu'une. Une partie peut donc durer plusieurs semaines.


Le plateau de jeu forme un graphe : les nœuds sont des territoires et les arêtes des routes entre ceux-ci. Les polygones formés par les arêtes du graphes sont des zones. Chaque zone dispose de plusieurs gisements de ressources que seuls les territoires adjacents  peuvent exploiter (et donc concourir à épuiser). Les ressources végétales se renouvellent en permanence alors que seuls des événements aléatoires permettent de renouveler les ressources minérales.


Les territoires peuvent être aménagés et les unités équipées, mais aucun des deux n'appartiennent à une équipe : elles sont temporairement sous le contrôle de l'équipe qui possède le territoire. Les bâtiments peuvent faciliter la défense du territoire, augmenter sa capacité de stockage ou améliorer la production des unités.


Pour exploiter les ressources, les déplacer et pour combattre, les joueurs commandent des unités, qu'ils peuvent réaffecter à l'envie selon leurs forces et leurs faiblesses à la tâche (efficacité à collecter tel ou tel type de ressources, rapidité à transporter, grande capacité de transport, efficacité au combat, etc.). Ces unités apparaissent dans les zones : pour les recruter, les joueurs peuvent les appâter avec de la nourriture : l'unité rejoint alors le territoire le plus offrant.


Les combats sont automatisés : les unités agissent par ordre de rapidité, les joueurs peuvent seulement indiquer pour chaque unité quelles cibles elle doit attaquer en priorité.


On gagne par la conquête militaire, mais le commerce et la diplomatie peuvent être mis à profit : on peut échanger des ressources, capturer les convois adverses ou au contraire laisser passer ceux d'équipes alliées (en taxant éventuellement les ressources transportées), etc. Les interactions sont notifiées aux joueurs des équipes concernées pour leur permettre d'intervenir, et des règles peuvent être programmées pour réagir en cas d'absence.


Côté ambiance, l'action se déroule dans un univers végétal fantastique. Les joueurs y incarnent des Seelies : des créatures magiques de la forêt. Les unités contrôlées sont des insectes.


Sur le plan technique, le serveur est codé en Elixir et utilise une architecture CQRS/ES. Je pratique le développement piloté par test, donc je m'assure que tous les mécanismes fonctionnent en isolation. L'application prend ensuite la forme d'un daemon, qu'on peut
piloter par un REPL ; sans aucun lien avec un serveur Web. Le code est disponible sur GitHub.

Pour le client, je ne sais pas encore. Probablement dans le navigateur (car la gestion du réseau en Lua/LÖVE m'a un peu gonflée). Peut-être quelque chose en mode texte initialement, juste pour voir le système "vivre".
Pour la première itération, le but est d'avoir 3 mécanismes :

- Sur un territoire, affecter des unités à l'exploitation d'un gisement de ressource et les désaffecter ;
- Résoudre la récolte de ressources au fil du temps ;
- Convoyer des ressources d'un territoire à un autre grâce aux unités ;

Pour mettre cela en place, on a besoin d'une structure de données qui représente une partie. Cette structure contient :

- une description du plateau de jeu : territoires, zones, routes entre ces territoires, voisinages entre les territoires et les zones, gisements disponibles sur les zones ;
- les unités qui existent : soit en attente sur un territoire, soit en train de collecter sur un gisement, soit dans un convoi ;

Les territoires, les zones et les gisements sont identifiés de manière unique. Cela nous permet de représenter le monde ainsi :
- Le territoire T1 est relié aux territoires T2 (à une distance de 9) et T3 (à une distance de 12) ;
- Le territoire T1 a accès aux zones Z1 et Z2.
- La zone Z1 dispose de gisements de ressource R1 et R2.

Les unités sont également identifiées de maière unique afin de pouvoir donner leur état :
- L'unité U1 est sur la route depuis le territoire T1 vers T2, elle a parcouru une distance de 3 (sur 9) ;
- L'unité U2 est en attente sur le territoire T2 ;
- L'unité U3 est en train de collecter sur le gisement R1 de la zone Z1 pour le territoire T1, elle a commencé à T+3 ;

La collecte de ressource a lieu chaque minute. Si la collecte d'une unité est arrêtée avant le tick, ce que l'unité a récolté entre cette interruption et le précédent tick est calculé.

Pour les déplacements, le temps de trajet du convoi est calculé d'après la vitesse de l'unité la plus lente.
La capacité de chargement du convoi est calculée selon le type des unités qui la comportent. A son arrivée à destination,

On note qu'il n'y a pas encore de notion de joueur ou d'équipe à cette étape.
Citation :que j'ai maintes fois tenté et maintes fois échoué

C'est mon cas aussi et j'aimerais bien me lancer dans mon autocritique, mais ça sera plus facile si quelqu'un d'autre se tape le boulot avant ; par conséquent, première question, pourquoi penses-tu avoir échoué ?

Par exemple, mon équivalent de Seelies a.k.a "le big projet pour lequel on a plein d'idées mais qui est un peu trop gros", j'ai fait plusieurs prototypes, et mes plus grands élans de motivation ont été de recommencer de zéro avec une optique ou un environnement différent. Pourquoi, à plusieurs reprises, as-tu estimé que c'était un échec et qu'il fallait recommencer autrement ?



Citation :une architecture CQRS/ES

Tu m'en as parlé mais c'était pas vraiment clair. Pourrais-tu présenter cette architecture (autre topic éventuellement), notament dans l'optique de faire un jeu (avantages, inconvénients, … tu me suis).



Citation :On note qu'il n'y a pas encore de notion de joueur ou d'équipe à cette étape.

J'aime bien créer les choses dans ce sens également. Cela laisse libre champ à la création d'une IA, permet de créer des tests et faire des simulations sans s'embarasser de la notion de joueur ou d'interaction, ce qui amène ensuite à la possibilité de scripter des comportements ou des suites d'interactions. C'est typiquement un genre de jeu que j'aime imaginer et que j'aimerais créer : la définition d'un système aux comportements complexes et multiples sur lesquels des joueurs peuvent interagir, ou laisser faire, de façon limitée.
Par exemple sur mon "big projet" ou des joueurs peuvent être en guerre j'avais imaginé qu'à un certain stade de la partie, plusieurs joueurs pourraient contrôler la même entité, les mêmes unités ; en gros fusionner. Cela demandait une gestion des droits avancés comme des grades, des priorités, etc. compliqué pour une première version mais toujours intéressant.
(07-25-2019, 08:48 PM)niahoo a écrit : [ -> ]
Citation :que j'ai maintes fois tenté et maintes fois échoué

C'est mon cas aussi et j'aimerais bien me lancer dans mon autocritique, mais ça sera plus facile si quelqu'un d'autre se tape le boulot avant ; par conséquent, première question, pourquoi penses-tu avoir échoué ?

Par exemple, mon équivalent de Seelies a.k.a "le big projet pour lequel on a plein d'idées mais qui est un peu trop gros", j'ai fait plusieurs prototypes, et mes plus grands élans de motivation ont été de recommencer de zéro avec une optique ou un environnement différent. Pourquoi, à plusieurs reprises, as-tu estimé que c'était un échec et qu'il fallait recommencer autrement ?

J'ai toujours été une girouette en matière de développement : développer des trucs m'amuse et est souvent l'occasion d'apprendre. Une fois l'objectif d'apprentissage passé et l'expérimentation terminée (concluante ou non), l'envie retombe généralement. Parfois, ce sont les interruptions liés aux aléas de la vie qui cassent l'élan au point qu'il est difficile de reprendre.

J'ai essayé seul, en équipe, en PHP, en Ruby, en étant étudiant, en étant ingénieur. La dernière tentative date de 2013.

Je n'ai pas essayé en Elixir en étant prof ! :p Entre temps, j'ai su aller au bout de quelques projets, j'ai crée des jeux de moindre envergure, solo et multijoueur. J'accepte plus facilement l'idée de commencer petit, quitte à ce que le gameplay soit incomplet.


(07-25-2019, 08:48 PM)niahoo a écrit : [ -> ]
Citation :une architecture CQRS/ES

Tu m'en as parlé mais c'était pas vraiment clair. Pourrais-tu présenter cette architecture (autre topic éventuellement), notament dans l'optique de faire un jeu (avantages, inconvénients, … tu me suis).

Ouaip, je ferai une petite présentation (et pointer vers des articles plus complets) et expliquerai ce qui m'a tenté dans cette architecture.


(07-25-2019, 08:48 PM)niahoo a écrit : [ -> ]
Citation :On note qu'il n'y a pas encore de notion de joueur ou d'équipe à cette étape.

J'aime bien créer les choses dans ce sens également. Cela laisse libre champ à la création d'une IA, permet de créer des tests et faire des simulations sans s'embarasser de la notion de joueur ou d'interaction, ce qui amène ensuite à la possibilité de scripter des comportements ou des suites d'interactions. C'est typiquement un genre de jeu que j'aime imaginer et que j'aimerais créer : la définition d'un système aux comportements complexes et multiples sur lesquels des joueurs peuvent interagir, ou laisser faire, de façon limitée.
Par exemple sur mon "big projet" ou des joueurs peuvent être en guerre j'avais imaginé qu'à un certain stade de la partie, plusieurs joueurs pourraient contrôler la même entité, les mêmes unités ; en gros fusionner. Cela demandait une gestion des droits avancés comme des Tgrades, des priorités, etc. compliqué pour une première version mais toujours intéressant.

Initialement mon but avec Seelies était de limiter les joueurs à une catégorie parmi cinq (exploitation des ressources, conquête, commerce, diplomatie et défense) pour encourager le jeu en équipe, pousser à la réflexion dans la composition de l'équipe, en le justifiant par une carte énorme difficile à gérer seul. Cela dit, je ne suis plus très sûr, et je préfère commencer en mettant ça de côté.

Et en composant complexe, je pensais permettre un partage des territoires, avec exploitation des ressources au prorata, avec toutefois l'option de dépasser son quota alloué, s'attirant éventuellement les foudres des "colocataires". Cependant, je trouve ça trop compliqué pour pas forcément grand chose, donc j'ai abandonné cette idée pour un contrôle exclusif (mais temporaire) d'un territoire.

On n'a pas besoin de ça pour faire capoter nos projets ! :p
(07-25-2019, 10:06 PM)Sephi-Chan a écrit : [ -> ]Initialement mon but avec Seelies était de limiter les joueurs à une catégorie parmi cinq (exploitation des ressources, conquête, commerce, diplomatie et défense) pour encourager le jeu en équipe, pousser à la réflexion dans la composition de l'équipe, en le justifiant par une carte énorme difficile à gérer seul. Cela dit, je ne suis plus très sûr, et je préfère commencer en mettant ça de côté.

Ça voudrais dire qu'un joueur n'aurait accès qu'à 1/5ème du jeu, ce qui risque de rendre le jeu un peu pauvre.

Après oui, je suis d'accord que la spécialisation c'est bien pour forcer la coopération entre joueurs, mais je pense qu'il faut quand même que les joueurs aient accès à tous les aspects du jeu (même s'ils sont mauvais dans certains aspects).

Par exemple si on prends la catégorie "exploitation de ressources", on peut imaginer que le joueur aurait accès à des unités de combat plus faibles, mais avec la capacité unique de piller des ressources lors des batailles.
(07-26-2019, 11:15 AM)Thêta Tau Tau a écrit : [ -> ]
(07-25-2019, 10:06 PM)Sephi-Chan a écrit : [ -> ]Initialement mon but avec Seelies était de limiter les joueurs à une catégorie parmi cinq (exploitation des ressources, conquête, commerce, diplomatie et défense) pour encourager le jeu en équipe, pousser à la réflexion dans la composition de l'équipe, en le justifiant par une carte énorme difficile à gérer seul. Cela dit, je ne suis plus très sûr, et je préfère commencer en mettant ça de côté.

Ça voudrais dire qu'un joueur n'aurait accès qu'à 1/5ème du jeu, ce qui risque de rendre le jeu un peu pauvre.

Après oui, je suis d'accord que la spécialisation c'est bien pour forcer la coopération entre joueurs, mais je pense qu'il faut quand même que les joueurs aient accès à tous les aspects du jeu (même s'ils sont mauvais dans certains aspects).

Par exemple si on prends la catégorie "exploitation de ressources", on peut imaginer que le joueur aurait accès à des unités de combat plus faibles, mais avec la capacité unique de piller des ressources lors des batailles.

Oui, c'est pour ça que j'avais mis l'idée de côté. Les unités spécifiques au joueur ne sont pas dans l'esprit du jeu. En revanche, je me suis dit que le personnage du joueur pourrait intervenir physiquement à un endroit pour fournir des bonus liées à son domaine

L'effet serait assez évident dans le cas du combat (des buffs de vitesse/puissance/défense) et de l'exploitation de ressources (buff de production). Pour la diplomatie je pensais à quelque chose de plus subtil : le convoi accompagné d'une Seelies diplomate qui atteint un territoire ennemi ne déclencherait ni les règles de résolution automatique, ni les notification aux joueurs adverses. Pour le commerce ça pourrait être des marchandises supplémentaires ou des sommes réduites concédées lors des échanges (le joueur adverse serait "arnaqué" sans s'en rendre compte).
Je vais parler un peu d'implémentation ! 2


Dans l'architecture CQRS/ES choisie, on raisonne en commandes et en événements.

Pour envoyer une unité exploiter une parcelle de ressources, on envoie une commande UnitStartsExploitingDeposit à notre aggregate Game.

Grâce à un module qu'on appelle un routeur, on définit que les commandes UnitStartsExploitingDeposit doivent être transmises à un aggegate Game qui contient l'état d'une partie.
Le système fera donc tourner autant d'aggregate Game qu'il y a de partie, et il les identifiera grâce à la clé "game_id" de chaque commande (cela fait partie des informations que l'on doit donner au routeur).

En soit, une commande n'est qu'un "value objet" contenant quelques clés. Dans le cas de la commande UnitStartsExploitingDeposit, on a trois clés : game_id, unit_id et deposit_id.

Très concrètement, un aggegate est un module qui implémente une fonction de décision (execute) et une fonction de mutation (apply) :
- la fonction de décision execute reçoit l'état de l'aggregate et une action et retourne soit une erreur, soit une liste d'événements (potentiellement aucun) à émettre ;
- la fonction de mutation apply reçoit pour chaque événement émis l'état de l'aggregate et un événement et retourne le nouvel état de l'aggregate.

Ici, la fonction de décision execute est appelée et effectue quelques vérifications métier grâce aux arguments qui lui sont fournis, à savoir (l'état de l'aggregate (l'état de la partie, donc) et la commande UnitStartsExploitingDeposit :
- est-ce que l'unité choisie a bien accès au gisement demandé ?
- est-ce que ce type d'unité est capable d'exploiter ce type de gisement ?
- est-ce qu'elle n'est pas déjà en train d'exploiter ce gisement ?

Si tout va bien, la fonction retourne un seul événement UnitStartedExploitingDeposit, avec quelques informations (unit_id, deposit_id, timestamp).
Cet événement est alors ajouté de manière permanente et immuable dans l'historique de notre application.

Ensuite, la fonction de mutation apply est exécutée : elle reçoit à son tour l'état de l'aggregate ainsi que l'événement UnitStartedExploitingDeposit. On modifie l'état de la partie.

Cette architecture (l'event sourcing) est complexe mais apporte de nombreux avantages dans le cas d'un jeu :
- en terme de compréhension : il est facile de visualiser les transitions entre un état et un autre puisque ça ne passe que par les fonctions de mutation ;
- de tracabilité : on sait exactement ce qui se passe (et s'est déjà produit) dans l'application, on peut même ajouter des fonctionnalités a posteriori (analyse statistiques, trophées, etc.) ;
- de débogage : là aussi, si un bug survient, on peut remonter l'historique des événements pour savoir où ça a cloché, il suffit de voir quel élément de code a émis l'événement foireux (ou n'a pas émis l'événement attendu) ;

En plus de l'event sourcing, CQRS (dont on parlera plus tard) apporte également son lot d'avantages et d'inconvénients. Un autre article traitera des avantages et inconvénients de l'architecture CQRS/ES.

L'état de la partie, c'est une structure de données qui contient tout ce dont on a besoin pour la faire évoluer et prendre les décisions à la réception des commandes suivantes.
Dans le cas de l'aggregate Game de Seelies, il s'agit d'une map avec quelques clés, j'y reviendrai un autre jour.
Merci, j'attends avec impatiences tes futurs écrits sur CQRS. D'après ce que je viens de rechercher en ligne je ne pense pas que ça soit super adapté à ce que je veux faire car il ne me semble pas pouvoir mettre tout mon jeu dans un seul aggregate. Par contre ça m'a l'air très intéressant pour des jeux plus petits avec des parties type board game.

Pourras-tu parler de comment tu gères le fait que tes modifications de state et sa lecture sont séparées dans le temps, ce qui fait qu'un joueur, après avoir appelé une commande, ne voit pas forcément le dernier état du jeu ?
(07-26-2019, 04:06 PM)niahoo a écrit : [ -> ]Merci, j'attends avec impatiences tes futurs écrits sur CQRS. D'après ce que je viens de rechercher en ligne je ne pense pas que ça soit super adapté à ce que je veux faire car il ne me semble pas pouvoir mettre tout mon jeu dans un seul aggregate. Par contre ça m'a l'air très intéressant pour des jeux plus petits avec des parties type board game.

Tu peux avoir un niveau de granularité plus fin. Ça se trouve pour moi non plus ça va pas coller ! :p


(07-26-2019, 04:06 PM)niahoo a écrit : [ -> ]Pourras-tu parler de comment tu gères le fait que tes modifications de state et sa lecture sont séparées dans le temps, ce qui fait qu'un joueur, après avoir appelé une commande, ne voit pas forcément le dernier état du jeu ?

Les commandes n'auront un retour direct qu'en cas d'erreur. Le nouvel état — ou plus certainement un sous-ensemble — est envoyé au moyen d'un push depuis un event handler. L'eventual consistency ne devrait pas poser problème.
Effectivement j'ai pensé au push en écrivant mon message.

Pour la granularité plus fine, je pensais à quelque chose de transactionnel. Par exemple tu veux modéliser du commerce entre deux entités. Mais tu as genre 100K joueurs sur un serveur unique (et pas une "partie") donc tu n'as pas tout dans le même state – forcément. Tu peux donc envoyer une commande à deux states de joueurs, l'une qui sera traitée en vendeur (parce que command.vendor_id === player.id) et l'autre en acheteur. Ensuite tu génères deux events qui vont, pour l'un, augmenter sa thune et diminuer son stock d'herbes magiques, l'autre l'inverse.

Si un des deux events est rejetté mais pas l'autre, le state global et morcelé de ton jeu n'est plus cohérent.

D'après ce que j'ai lu, CQRS sert a améliorer perf et scalabilité mais à condition de pouvoir toujours modifier de façon unitaire chaque élément. Est-ce que tu connais une solution fréquemment utilisée pour ça ?

Perso j'ai fait un système de queue tout simple qui gère ça, et je modifie mon state selon le principe suivant : d'abord je check que tout est bon, et si oui alors j'écris sans gestion d'erreurs puisque ça passe. Mais il n'est pas question de ségrégation des queries.
Pages : 1 2 3 4 5 6