dimanche 24 février 2008

Presque Bérénice

Avec l'ajout du support pour les messages "whois" (identification des joueurs, qui permet l'affichage des noms dans le chat), la nouvelle mouture d'AdH a rattrapé l'ancienne, ce qui veut dire que l'on va enfin commencer à attaquer de nouvelles fonctionnalités (après un bon coup de nettoyage dans le code, quand même). Il ne reste qu'un seul bug ennuyeux, qui est que l'on ne peut voir les autres avatars qu'a partir du moment où ils commencent à bouger, et il faut que je trouve une manière élégante de corriger cela.

Pour la prochaine fonctionnalité, je me voyais bien m'attaquer à la création du monde. L'idée est de permettre à certains avatars de créer et de développer des zones, le tout depuis le jeu. Ces avatars pourraient à partir d'une bibliothèque d'objets et de quelques outils architecturaux créer des mondes entiers. La première raison pour une telle fonctionnalité est de n'avoir pas de génération de carte séparée: après tout, la représentation la plus proche de ce que verrons les joueurs est le jeu lui même! La deuxième raison est qu'une telle fonctionnalité, utilisée avec discernement, peut être formidable pour le gameplay, en proposant par exemple un niveau à partir duquel les joueurs, élevés au rang des dieux, peuvent créer de nouveaux mondes.

jeudi 21 février 2008

Un écosystème

Plus qu'un serveur causant à une base de données, c'est un écosystème entier qui peut se greffer sur l'architecture d'Adh. Maintenant que le système est au point (je viens de rajouter dans la base le support pour le déplacement des avatars), il est possible de connecter à peu près n'importe quel module additionnel sur la base pour aller y faire tout un tas de choses intéressantes. Il suffit de savoir causer le SQL.

On peut commencer simple, avec des systèmes de feed vers la base: par exemple, en envoyant la météo, afin de modifier l'environnement du jeu en temps réel. Ou encore, en permettant aux joueurs de recevoir des nouvelles depuis le forum d'un site web.

Après, on peut installer des feeds en sens inverse: faire remonter vers le site tout un tas de statistiques sur le monde, les derniers prix de marchandises, les actes de bravoure des joueurs...

Poussons plus loin, et ayons un système qui cause dans les deux sens! Pourquoi ne pas permettre aux joueurs d'accéder à leur personnage et d'effectuer des actions simples sans avoir à se connecter via le client lourd? Ainsi, le joueur pourrait aller enguirlander ses fermiers, commercer, ou encore préparer une autre conspiration, tout cela depuis le boulot / l'université / la salle info de l'école maternelle, à la pause déjeuner (et seulement à la pause déjeuner, n'est-ce pas, bande de petits canaillous?).

Mais ce n'est pas fini! Modifions la base pour permettre l'ajout de modules spécifiques qui iront modifier l'expérience de jeu. Il serait possible de développer un module d'artisanat, de combat, de sorts, de gestion de ses troupes, et beaucoup d'autres choses, de manière totalement indépendante. Par exemple, il suffit d'ajouter une simple table des ordres, et l'on peut avoir un module d'artisanat qui lit les ordres, et transforme les objets contenus dans l'inventaire de l'avatar. Une autre table d'ordres liée à un module de jeux de hasard, et voilà un casino!

Au final, ce code "hors serveur" doit surtout être utilisé pour les actions un peu particulières et qui n'arrivent pas trop souvent (l'on pourrait faire un module qui gère les déplacements, mais bonjour les performances!), mais permettrait d'ajouter assez facilement des tonnes de fonctionnalités pour un gameplay poussé.

Il n'y a plus qu'à s'y mettre!

mardi 19 février 2008

Inventaire

J'ai étendu le principe développé dans le post précédent à l'inventaire. C'est assez similaire:

- L'inventaire est modifié
- Une trigger ajoute l'identifiant d'avatar à la liste des inventaires modifiés
- Une autre trigger réveille le serveur
- Le serveur récupère les nouveaux inventaires et les envoie à qui de droit

Il y a cependant une petite différence. Un des avantages du système est que je n'ai plus à me préoccuper de redistribuer les informations d'inventaire: même si l'on ajoute un composant totalement indépendant qui va modifier la base de données, toute modification sera immédiatement répercutée sur les utilisateurs. L'on peut donc par exemple penser à un système de commerce séparé, pourquoi pas dans un navigateur internet, qui irait en temps réel répercuter les actions dans le jeu. Ou pourquoi pas un composant "artisanat" qui permette de transformer certains matériaux en objets? Ou encore un composant "La main des Dieux" qui permette au maitre du jeu de faire apparaitre des artéfacts de quête dans le sac du joueur.

Un petit point d'optimisation ici est l'envoi juste avant la requête, dans une table temporaire, des avatars actuellement connectés. Ainsi, la requête est faite en joignant la table de l'inventaire, la table des avatars dont l'inventaire a été modifié, et la table des avatars actuellement connectés, de manière à n'envoyer vers le serveur que les inventaires qui doivent être réellement distribués (l'inventaire a été modifié ET le joueur est connecté). Si l'on a par exemple 1000 joueurs connectés, et que l'on modifie l'inventaire de 1000 joueurs, mais que seuls 50 d'entre eux étaient connectés, l'on envoie seulement vers le serveur 50 inventaires. L'écriture des données sur la table des avatars actuellement connectés est très rapide, puisque c'est une copie directe, et que l'on a donc pas besoin de faire une ribambelle d'INSERT qui ralentiraient le tout.

vendredi 15 février 2008

Des requêtes, et des requêtes

Je suis en train de rapidement rattraper le niveau de fonctionnalités de Bérénice, rajoutant les requêtes faites par le client. Ce soir, c'était la description des objets du monde. Très sommaire pour l'instant (id, nom, description), mais ça sera simple à enrichir.

Un problème intéressant que celui de ces requêtes est la synchronisation. A la connexion, le client envoie en effet un lot de requêtes au serveur pour récupérer les informations importantes sur l'avatar incarné, la liste des types d'objets, l'inventaire... Mais si par exemple l'inventaire est reçu avant la liste d'objets, les noms ne pourront pas être affichés. De même, si la requête de mon inventaire est traitée par le serveur plus rapidement que la demande d'incarnation de mon avatar, le serveur va trouver que je ne suis pour l'instant lié à aucun avatar, et va refuser ma demande.

Dans certains cas, il existe une solution élégante au problème Pour l'inventaire, par exemple, utiliser le numéro d'identification tant que l'on a pas d'informations sur le nom de l'objet. Quand la liste des noms d'objets arrive, renommer tous les objets de l'inventaire avec la nouvelle liste. Le message arrivera de toutes façons si vite que le joueur ne s'apercevra même pas que les noms étaient faux au départ! Cela améliore la robustesse de l'application, qui devient capable de gérer les messages dans un ordre moins strict.

Dans d'autres cas, il n'y a pas de solution miracle, et il va falloir synchroniser à la main... Et le client attendra donc qu'une requête ait été confirmée pour envoyer la suivante. Il n'y a plus qu'à trouver un design à peu près propre pour supporter cela.

jeudi 14 février 2008

Chat de bout en bout

C'est fait, le système fonctionne complètement! L'on peut voir dans les logs les différentes requêtes appelées pour faire fonctionner le tout. La récupération des messages et la suppression est faite dans une même transaction sérializable, afin qu'un message qui arriverait entre les deux ne soit pas supprimé sans avoir été envoyé. La ligne arrive au client une fraction de seconde après avoir été tapée, les requêtes à la base de données prennent très peu de temps (après un peu d'échauffement, la partie upstream prend moins d'une milliseconde).


2008-02-13 23:25:46 - Debug - 11 - Request "select * from chat(1, 3, 'Je ne suis ni Athénien,
ni Grec, mais un citoyen du monde.')" executed in 1 ms
2008-02-13 23:25:46 - Debug - 2 - Request "begin transaction isolation level serializable"
executed in 0 ms
2008-02-13 23:25:46 - Debug - 2 - Request "select * from getChatUpstream()" executed in 4 ms
2008-02-13 23:25:46 - Debug - 2 - Request "select * from clearChatUpstream()" executed in 4 ms
2008-02-13 23:25:46 - Debug - 2 - Request "commit transaction" executed in 0 ms
2008-02-13 23:27:21 - Info - 9 - Disconnected


dimanche 10 février 2008

Les callback

La décision de pousser les états dans la base de données apporte au moins un problème particulier: comment prévenir les clients que quelque chose vient de se passer.

J'ai discuté dans les posts précédents la tentative de se débarrasser de (presque) tous les états dans le code, et de ne les avoir que dans la base de données elle même. Presque? En effet, je garde cependant une structure de données partagée et protégée par des sections critiques: le catalogue des connections actuellement valides, et les identifiant de comptes et d'avatars correspondants. Mais à part ça, tout est dans la base.

Que ce passe-t-il donc, lorsque par exemple un joueur veut parler dans la zone? Il envoie son message au serveur, et un thread le traite et le sauve dans la base de données. Et c'est là que l'on se rend compte que la base commence à avoir besoin d'un moyen de prévenir le serveur que quelqu'un a dit quelque chose, et qu'il faudrait peut-être penser à envoyer ledit message à ses voisins. Et heureusement, postgres a pensé à nous.

Le système de notification est fait exactement pour ça. Lorsqu'un processus lance une commande NOTIFY dans la base, toutes les connections qui écoutent vont la recevoir. En ajoutant un thread qui attend les notifications, l'on peut traiter les changements dans la base et expédier aux joueurs.

Le système pourrait fonctionner comme cela:

- Lorsqu'un message d'une conversation est reçu, il est ajouté à une table de la base de données.
- Cet ajout déclenche une "trigger", qui duplique le message dans une table secondaire, et lance un "notify"
- Le thread de notification reçoit le message, lit tous les messages de la table secondaire, les supprime de la base (seulement de la table secondaire, l'on veut quand même garder une trace de ce qui s'est dit), et envoie les messages à leurs destinataires.



Ce petit crobard représente Athos voulant envoyer un message à Porthos et Aramis. Le message est reçu par le serveur, qui l'insère dans la table principale. La trigger insère un duplicata du message dans une table secondaire, qui notifie un thread du serveur, lequel lit les messages actuellement dans la table secondaire, et les envoie à nos trois mousquetaires (Athos sera content de recevoir un accusé de réception).

Et les performances, dans tout ça? Pas très bonnes. Il y a beaucoup de communication entre divers processus (comptez les flèches!), incluant des sauts à travers le réseau (encore plus si la base de données vit la vie sur un serveur séparé). Mais bon, quand le seul problème restant sera celui des performances, l'on pourra toujours penser à réintégrer la base.

samedi 9 février 2008

Postgresql 8.3 - commit asynchrone


Postgresql 8.3 vient de sortir, et voilà une release qu'elle est excitante! Parmi la multitude de nouvelles fonctionnalités, notons plus particulièrement le commit asynchrone, qui devrait être particulièrement intéressant pour Adh.

Lorsque l'on modifie des données dans la base, le système ne retourne du commit que lorsque les données ont été effectivement écrites sur le disque, dans le wal (write ahead log). Ainsi, on a la garantie que s'il y a une coupure de courant, à partir du moment où le commit a été fait, nous n'avons rien perdu.

Bien sûr, cette garantie a un coût, puisqu'il va falloir accéder au disque. Mais par exemple dans un système financier, où il est hors de question de perdre la moindre transaction, cette fonctionnalité est d'une importance capitale.

Il existe cependant une option qui permet de se passer du wal, et de faire des commit en mémoire, l'information étant écrite sur le disque après coup. Autant ceci améliore grandement les performances, autant c'est plutôt dangereux pour des donnés sensibles. En effet, s'il y a coupure de courant, non seulement on ne sait pas quelles sont les transactions qui ont été effectivement écrites sur le disque, mais la base pourrait contenir des données corrompues! On ne peut donc utiliser ce mode que lorsque l'on a besoin de hautes performances, mais que les données en elles-mêmes ne sont pas capitales, par exemple pour enregistrer des données issues de capteurs durant une simulation. Si l'on perd la base, on la reconstruit, on réimplante la dernière sauvegarde, et on relance la simulation!

Le commit asynchrone de postgresql 8.3 propose une fonctionnalité intermédiaire, qui est de permettre de revenir du commit avant que les données soient sur le disque, mais garantit que s'il y a coupure, la base reste dans un état propre, ne perdant que les transactions qui ont été exécutées dans les derniers instants.

Adh peut bénéficier grandement de ce mode. En effet, en cas de coupure, un joueur qui par exemple commerce avec un autre en sera quitte pour recommencer sa négociation, sans risquer que l'argent soit parti mais l'objet jamais arrivé. Quelques secondes de perdue en cas de panne sont parfaitement acceptables si elles permettent d'améliorer la vitesse du jeu.

mercredi 6 février 2008

Logs

Voilà les logs du serveur, pour ce soir. J'ai enfin connecté de bout en bout le serveur, le buffer de messages, la queue de messages, le module d'authentification, et la base de données! Comme l'on peut le voir, c'est le thread numéro 6 qui a choppé la requête. Pendant qu'il bosse, d'autres peuvent prendre le relais. 4 ms linéaires, c'est beaucoup trop pour une bête authentification, mais si d'autres actions peuvent bosser en parallèle, c'est tout à fait acceptable.


******************************************
** AdH *
******************************************

db_host localhost
db_port 5432
db_name lucas
db_user adh_user
server_port 1789
server_log adh.log
server_tick 0.1
server_refresh 300.
actions 5
run as daemon no

2008-02-05 23:46:37 - Info - 0 - Ready
2008-02-05 23:46:37 - Info - 1 - Listening on port 1789
2008-02-05 23:46:37 - Info - 4 - Connected to database
2008-02-05 23:46:37 - Info - 6 - Connected to database
2008-02-05 23:46:37 - Info - 2 - Connected to database
2008-02-05 23:46:37 - Info - 3 - Connected to database
2008-02-05 23:46:37 - Info - 5 - Connected to database
2008-02-05 23:46:55 - Info - 4 - Connected
2008-02-05 23:46:55 - Info - 6 - Request "select * from login('Raoul', 'Raoul')" executed in 4 ms
2008-02-05 23:46:55 - Info - 6 - Connected!

mardi 5 février 2008

Modules d'actions

Un des intérêts liés à la nouvelle architecture est le faible couplage entre les modules d'actions, qui reçoivent des messages xml et les traitent dans la base de données, et le reste du système. A condition d'utiliser les fonctions de chargement de code dynamique, il serait possible de charger et de décharger des modules à chaud. Une nouvelle fonctionnalité? Hop, on charge le module correspondant, et elle peut commencer à être utilisée, aucun redémarrage du système nécessaire, aucune déconnexion à craindre.

Je ne mets pas trop d'énergie là dedans pour l'instant (si personne ne se connecte, je peux bien relancer le serveur autant de fois que je veux!), mais le design est clairement tourné dans cette direction. Peut-être qu'un jour, nous pourrons nous targuer d'avoir une disponibilité de 100% tout en assurant l'évolution du jeu!