dimanche 13 décembre 2009

Une ville

Quelques clics et un peu de Blender plus tard...

Une interview du créateur d'Infinity sur Gamedev

Infinity, Quest for Earth est probablement l'archétype du projet indépendant incroyablement ambitieux, mais qui sortira un jour, lui. La liste des fonctionnalités est à tomber par terre: Une simulation spatiale réaliste façon Elite, massivement multi-joueur, avec un univers gigantesque, la possibilité d'explorer les planètes ou les ceintures d'astéroïdes, avec des graphismes à se mettre à genoux devant, et les vidéos disponibles sur le site prouvent que le repas sera à la hauteur du menu.

Son concepteur, Flavien Brebion, répond à quelques questions sur Gamedev.

Ma foi, je n'ai plus qu'à ronger mon frein jusqu'à la beta!

dimanche 6 décembre 2009

Interfaces et OpenSceneGraph

J'ai ressorti quelques vieux bouts de code qui combinent OpenSceneGraph et wxWidgets, et je les ai dépoussiérés en utilisant la bonne vieille wxAui pour permettre de docker et de faire flotter les composants d'interface autour de la fenêtre graphique. Tout cela marche plutôt bien, et en voici quelques captures d'écran, avec un arbre docké, puis flottant:




La combinaison des évênements wxWidgets avec ceux d'OpenSceneGraph marche très bien, et permet de gérer efficacement la navigation à la souris et au clavier dans la fenêtre graphique. Le rafraîchissement se fait dans l'événement OnIdle, ce qui fonctionne plutôt bien: le rendu se fait aussi vite que possible, mais les autres composants de l'interface sont fluides.

J'attends également avec impatience le support de de l'anti-crénelage, qui devrait apparaître normalement dans wxWidgets 2.9.

mardi 24 novembre 2009

Rails et routes

J'ai passé un peu de temps à chercher à comment modéliser des routes et des rails, et j'ai trouvé un post très intéressant ici. Ça cause courbes de Bézier et arcs de cercle, et j'ai déjà commencé à gribouiller quelques feuilles pour tenter de gérer mes courbes.

A voir également, la manière dont Cities XL gère la chose, s'affranchissant complètement (du moins en apparence) des grilles.

À ruminer!

samedi 14 novembre 2009

Enfin, le bump mapping!

C'est avec l'aide de cette page, de nombreuses simplifications, et beaucoup de bidouillages, que j'ai enfin réussi à faire du bump mapping qui ressemble à quelque chose.

L'ensemble de la scène est assez complexe.


  • Tout d'abord, il y a l'élévation, générée à partir d'une texture, au niveau du vertex shader

  • Ensuite, il y a l'ombrage général de la scène, généré également avec une texture, mais au niveau du fragment

  • Ensuite, il y a le calcul de la tangente, au niveau du vertex shader, qui est ensuite passée au fragment pour générer le bump mapping, à la fois en diffus, et en reflets, pour donner les effets "mouillés"



L'ensemble tourne à un très correct 1700 images par secondes en 1280x1024 plein écran.






Dans la dernière image, la couleur diffuse est en rose, afin que les reflets soient encore plus visibles.

jeudi 12 novembre 2009

La gueulante du jour

Mais pourquoi les commits sur SourceForge sont-ils si lents???

De une à deux secondes par fichier, c'est pas la mer à boire, mais quand on veux commiter souvent, on a l'impression de passer sa vie à attendre que le serveur daigne répondre.

Ça me dépasse.

samedi 31 octobre 2009

Mount & Blade - Rebelle

Cette enflure de King Harlaus ayant refusé de me donner un château que je venais de conquérir, je lui ai dit d'aller se mettre son royaume là où le soleil de brille pas, et je suis passé rebelle.

La déception, c'est que sans l'utilisation des mods, la partie devient assez injouable. En effet, il devient impossible d'aller un tout petit peu loin sans se retrouver avec la moitié du monde attaquant nos châteaux. L'on se retrouve donc principalement à crapahuter entre ses différents fiefs, à recruter dans les villages voisins, chasser les armées belliqueuses, et se battre contre les gardes pour entrer dans les villes et y vendre sa camelote.

Avec du temps, j'imagine qu'il est possible de correctement remplir ses châteaux, et donc espérer les voir résister à de plus grands assauts, mais il faut alors payer sa gigantesque armée, ce qui est loin d'être évident.

Peut-être vais-je essayer le mod de "gestion de royaume", alors.

samedi 24 octobre 2009

Cellules sélectionnées d'une wxGrid

J'ai bien cru péter un plomb! J'ai une wxGrid, et je veux savoir quelles sont les cellules sélectionnées. Quelque soit la méthode que j'utilise (GetSelectionBlockTopLeft / GetSelectionBlockBottomRight, GetSelectedCells, GetSelectedRows, GetSelectedCols), il m'en manquait toujours.

Tombant finalement sur le rapport de bug de chez wxWidgets, l'explication est arrivée: pour des raisons d'optimisation mémoire, différentes structures seront maintenues en fonction de la manière dont on sélectionne. Par exemple, lorsque l'on sélectionne une colonne entière, la grille gardera juste l'index de la colonne, lorsque l'on sélectionne un bloc, la grille sauvera le coin en haut à gauche et le coin en bas à droite, etc. Ce qui veux dire en fait qu'il faut utiliser les 4 méthodes pour trouver l'ensemble des sélections!

Cela donne quelque chose d'un peu comme ça:


std::set<std::pair<int, int> > getGridSelection(wxGrid & grid)
{
std::set<std::pair<int, int> > selection;

wxGridCellCoordsArray topLeft = grid.GetSelectionBlockTopLeft();
wxGridCellCoordsArray bottomRight = grid.GetSelectionBlockBottomRight();
for(size_t i = 0; i < std::min(topLeft.GetCount(), bottomRight.GetCount()); i++)
{
for(int row = topLeft.Item(i).GetRow(); row <= bottomRight.Item(i).GetRow(); row++)
{
for(int col = topLeft.Item(i).GetCol(); col <= bottomRight.Item(i).GetCol(); col++)
{
selection.insert(std::make_pair(row, col));
}
}
}

wxGridCellCoordsArray cellSelection = grid.GetSelectedCells();
for(size_t i = 0; i < cellSelection.GetCount(); i++)
{
selection.insert(std::make_pair(cellSelection.Item(i).GetRow(), cellSelection.Item(i).GetCol()));
}

wxArrayInt selectedRows = grid.GetSelectedRows();
for(size_t i = 0; i < selectedRows.GetCount(); i++)
{
for(int col = 0; col < grid.GetNumberCols(); col++)
{
selection.insert(std::make_pair(selectedRows.Item(i), col));
}
}

wxArrayInt selectedCols = grid.GetSelectedCols();
for(size_t i = 0; i < selectedCols.GetCount(); i++)
{
for(int row = 0; row < grid.GetNumberRows(); row++)
{
selection.insert(std::make_pair(row, selectedCols.Item(i)));
}
}

return selection;
}

mardi 20 octobre 2009

De retour sur le trading

Ma phase corporate étant passée, je laisse Médoc de côté (en tant que développeur, car en tant qu'utilisateur, je continue à faire chauffer le chameau!), et je retourne à mon module de trading. J'espère bientôt pouvoir sortir une démo, même si le problème de l'hébergement va se poser de plus en plus.

dimanche 11 octobre 2009

Médoc - Stable

Médoc est fini! Enfin, c'est un bien grand mot, mais disons qu'il est maintenant à un stade où il est utile pour moi, ce qui n'est déjà pas si mal. En dehors de fonctions de recherche plus avancées au niveau de l'interface web, qui seront aisées à ajouter, la partie la moins jolie est sans doute la petite application pour enregistrer le document dans la base. Puisque l'ensemble du projet est maintenant hébergé par OcamlCore, j'ai voulu écrire l'interface en utilisant Lablgtk, qui ne se prête pas vraiment à l'écriture d'une petite appli rapide, qui n'est qu'en fait qu'un front-end pour les programmes scanimage et convert, qui font le gros du boulot. Ce n'est donc ni très beau, ni très robuste.



L'interface web, elle, est utilisable, efficace (du moins pour un nombre réduit d'utilisateurs simultanés), et le système de tag fonctionne gentiment. J'ajoute presque chaque courrier administratif reçu au système, maintenant. On verra à l'usage!

jeudi 8 octobre 2009

Ping

Voulez vous tout savoir sur Ping?

mardi 22 septembre 2009

mlBlocks

J'ai vérifié de nouveau que mon vieux clone de Tetris en Ocaml marchait encore. Ce fut également l'occasion pour mettre quelques captures sur la section Web de la forge Ocaml.

Ocaml n'est certainement pas prêt pour les blockbusters en 3D, mais pour coder de petits jeux, il est très certainement à la hauteur!



lundi 21 septembre 2009

Le système de tags

Voilà, le système de tags est ajouté. L'on peut insérer de nouveaux tags, ou les supprimer d'un clic. Pour les tags qui n'existent pas encore, il est possible de taper leur nom directement.



Ce fut une belle plongée dans les CSS, et je suis maintenant à peu près satisfait de l'apparence générale de l'application. Il me reste maintenant à améliorer l'ajout de nouveaux documents, qui repose pour l'instant sur un script un petit peu fragile.

vendredi 11 septembre 2009

Redirections à travers Ocaml

Mes remerciements les plus vifs à un japonais inconnu dont le post(?) sur un blog(?) m'a débloqué! J'essayais de gérer les redirections entre des programmes démarrés à travers Ocaml, et j'avais un mal fou à rediriger les flux. Je n'ai rien compris à son post, juste vu le code, et j'ai compris que je m'y prenais comme un pied.

Maintenant, j'ai une fonction toute bête pour prendre un JPEG en entrée et ressortir le texte en sortie:


let gocr file =
let i_file = Unix.openfile file [Unix.O_RDONLY] 0 in
let gocr_in, jpg_out = Unix.pipe() in
let result, gocr_out = Unix.pipe() in
let pid0 =
Unix.create_process
"jpegtopnm"
[|"jpegtopnm"|] i_file jpg_out Unix.stderr in
Unix.close jpg_out;
let pid1 =
Unix.create_process
"gocr"
[|"gocr"; "-"|] gocr_in gocr_out Unix.stderr in
Unix.close gocr_out;
ignore(Unix.waitpid [] pid0);
ignore(Unix.waitpid [] pid1);
let buffer = Buffer.create 4096 in
let read_buffer = String.create 512 in
let r = ref true in
while !r do
let len = Unix.read result read_buffer 0 512 in
if len > 0 then
Buffer.add_substring buffer read_buffer 0 len
else
r := false
done;
Buffer.contents buffer


L'on ouvre donc le fichier d'entrée, un pipe entre jpegtopnm et gocr, et un pipe entre gocr et notre sortie (si quelqu'un a une idée pour se débarrasser du deuxième pipe, je suis preneur!). L'on chaîne les processus avec les descripteurs de fichiers ainsi récupérés, et, très important, on ferme le pipe d'output après l'avoir donné à un processus. Pour la fin, je lis bêtement depuis mon pipe jusque dans un buffer, jusqu'à ce que le pipe soit vide, et je renvoie le contenu du buffer.

Ceci m'approche d'autant plus du moment où je vais enfin pouvoir faire la reconnaissance de caractères directement depuis le serveur web.

mardi 1 septembre 2009

Ocsigen - Images dynamiques

Voilà un sujet sur lequel il m'a fallu batailler ferme! C'est tout l'intérêt d'Ocaml: la difficulté est d'arriver à écrire du code qui compile. Une fois que le compilo est content, le code fonctionnera comme convenu. C'est exactement ce qui s'est passé avec mes images dynamiques.

Les documents et miniatures sont tous sauvés dans la base de données de Médoc. Quand bien même balancer ce genre de gros blobs à travers du SQL n'est pas franchement recommandé, je pense qu'il s'agissait là d'un des cas de figure autorisés. En effet, sauver ces images dans la base permet de centraliser toutes les données, ce qui simplifie la sauvegarde et la restauration et permet de laisser à la base le soin de chiffrer les données.

Cela veut dire qu'il faut pouvoir générer une image à partir d'un buffer binaire récupéré de la base. Ça tombe bien, Ocsigen sait envoyer du binaire via un Streamlist. Écrivons par exemple un service Ocsigen qui affiche une image à partir d'une chaîne binaire:


(* Petites déclarations ennuyeuses *)
open Lwt
open XHTML.M
open Eliom_services
open Eliom_parameters
open Eliom_sessions
open Eliom_predefmod.Xhtml

(* Déclaration du service, appelé "view", et qui prend *)
(* l'identifiant entier de l'image en paramètre *)
let view = Eliom_services.new_service ["view"] (int "id") ()

(* Initialisation du service *)
let _ = Eliom_predefmod.Streamlist.register view
(fun sp id () ->
return
([fun () ->
return (Ocsigen_stream.of_string (get_data id))],
"image/png"))

Il suffira d'écrire la fonction get_data qui prend dans notre cas l'identifiant entier de l'image à afficher, et qui retournera la chaîne contenant les données binaires de l'image.

Ce service peut ensuite être utilisé dans la déclaration d'un élément img:
 <img src="view?id=2"/>
pour afficher des images à l'intérieur d'une page (attention aux performances quand même, quand on va taper dans la base pour chaque image!).

lundi 31 août 2009

Premiers pas avec Ocsigen

Je voulais m'y mettre depuis longtemps, à ce serveur web. Or, justement, j'avais un petit projet en tête pour me délasser d'AdH: un système de documentation.


Ocsigen est donc un serveur web écrit en Ocaml, qui charge des modules basés sur le framework Eliom. Ocsigen est performant, mais surtout force un typage extrêmement fort du HTML en lui-même, et des liens entre les différents services. Enfin, l'on pourra réutiliser tout le code Ocaml existant, à condition de le faire rentrer dans le modèle de thread, basé sur une bibliothèque de threads coopératifs, Lwt.

Le gros problème d'Ocsigen, c'est qu'en dehors de la documentation, point de salut, ou si peu.

La documentation en elle-même est complète, mais manque furieusement d'exemples si l'on sort un tant soit peu des sentiers battus, et il faut pas mal d'efforts pour commencer à comprendre la manière dont les différents éléments s'enchaînent. L'inférence de types de nos amis Hindley et Milner vient heureusement à notre secours, et il serait une très mauvaise idée d'essayer d'écrire les interfaces avec des types aussi biscornus.

La communauté, elle, est inexistante (ou du moins, très bien cachée), et les sites publics qui tournent sous Ocsigen sont rares. Heureusement, un Wiki a été écrit pour Ocsigen pour le Google Summer of Code, ce qui aide beaucoup à voir comment, par exemple, utiliser Postgresql à travers Lwt.

Il est particulièrement intéressant d'écrire des pages à travers Ocsigen, c'est une manière d'appréhender la programmation Web tout à fait différente. On finit par se faire aux types ésotériques, et j'ai rapidement écrit quelques fonctions pour faciliter l'utilisation d'éléments récurrents.

Mon système de documentation, répondant au doux nom de Médoc (Mes docs :) ), avance tout gentiment, en quelques jours j'ai déjà une base de données (avec chiffrage des données les plus sensibles!), un outil en ligne de commande pour y sauver les documents, et une page web qui permet d'effectuer des recherches et d'afficher les documents. En voici une petite capture:



La page affiche tous les documents correspondant à un terme, en se basant sur le "Full Text Search" de Postgresql. L'on affiche également les miniatures de chaque page, cliquables et qui mènent au document grandeur nature.

Un peu de CSS sera nécessaire pour rendre l'ensemble plus joli, mais le système est déjà utile, ce qui est un bon début!

vendredi 28 août 2009

Postgresql Full Text Search - Planquez ces chiffres!

Admettons que l'on veuille sauver des textes cryptés dans la colonne d'une table Postgresql. Rien de plus facile en utilisant les fonctions pgp_sym_encrypt et pgp_sym_decrypt. Jusque là, tout va bien.

Mais admettons maintenant que l'on veuille également pouvoir faire des recherches rapides sur ces textes en utilisant le Full Text Search. L'on rajoute une colonne de type tsvector, et l'on parse le texte là dedans. Patatras, votre vilain espion peut maintenant recréer le document en regardant ce champ, qui, lui, ne peut pas être crypté!

L'on peut bien sûr monter ses données sur une partition cryptée. Mais c'est compliqué, et pas facile à installer chez l'utilisateur, sans compter que tout est en clair tant que la machine est allumée.

Cependant, admettons encore (ça fait beaucoup, je sais!) que les données sensibles soient plutôt des nombres (codes secrets, identification, numéros de compte, sommes d'argent). Sans ces nombres, les documents deviennent plutôt inoffensifs. Et de plus, il est peu probable que l'on aie besoin de ces nombres pour faire des recherches. C'est parfois utile, certes ("quels sont les documents qui contiennent mon numéro de compte?"), mais on pourra s'en passer.

Ça tombe bien, il est possible de créer une nouvelle configuration qui escamotera ce genre de données. Il suffit d'enlever la correspondance entre les types de tokens et le parseur.

Ajoutons donc la configuration french_safe:

create text search configuration french_safe ( copy = pg_catalog.french );
alter text search configuration french_safe drop mapping for int;
alter text search configuration french_safe drop mapping for uint;
alter text search configuration french_safe drop mapping for float;
alter text search configuration french_safe drop mapping for numword;

Alors que de la configuration de base, tout un tas de données sensibles auraient pu être extraites:


select * from to_tsvector(
'french',
'mon code secret est le 12345 et j''ai 217.45 € sur mon compte')
=> "'12345':6 '217.45':10 'cod':2 'compt':13 'secret':3"


la nouvelle configuration, elle, garde nos petits secrets bien au chaud!

select * from to_tsvector(
'french_safe',
'mon code secret est le 12345 et j''ai 217.45 € sur mon compte')
=> "'cod':2 'compt':11 'secret':3"


Faites donc un peu tourner des phrases sous ts_debug, et voyez comment les nombres et les mélanges chiffres lettres (mots de passes!) sont gentiment effacés.

lundi 17 août 2009

Trading

Petit aperçu de ce que va donner la GUI de trading



L'idée est de permettre au joueur de choisir lui-même la disposition des éléments d'interface, à travers l'utilisation du wxAuiNotebook de chez wxWidgets. Pour l'instant, il n'existe que deux types de composants: la liste des marchandises, et l'inventaire. Je vais y ajouter le carnet d'ordres, et enfin la visualisation des prix du marché en temps réel.

Le serveur est séparé du serveur de jeu AdH, ce qui me permet d'être plus léger, et d'identifier les composants partagés pour les mettre dans une bibliothèque à part. A terme, je devrais avoir migré de nombreux composants, rendus génériques, vers cette bibliothèque, ce qui devrait permettre de créer très facilement des serveurs spécifiques, voire de monter des clusters. Mais pour le moment, j'essaie de trouver un gameplay sympa pour la partie trading. Je compte bien que ça soit le souk, dans le sens premier du terme!

lundi 10 août 2009

Postgresql 8.4 et les requêtes récursives

La fonctionnalité de Postgres 8.4 que je préfère, c'est les requêtes récursives. Voilà vraiment quelque chose qui va permettre de résoudre bon nombre de problèmes de manière beaucoup plus élégante, et notamment tout ce qui concerne les arbres, les graphes, et les hiérarchies de toutes sortes.

Voici par exemple un arbre où chaque élément pointe vers son parent.


create table h(entry int not null, parent int null);

insert into h values(1, null);
insert into h values(2, 1);
insert into h values(3, 1);
insert into h values(4, 1);
insert into h values(5, 1);
insert into h values(6, 2);
insert into h values(7, 2);
insert into h values(8, 6);
insert into h values(9, 8);
insert into h values(10, 4);


Maintenant, répondons à la question: Quels sont tous les éléments enfants de l'élément 2? Avec le SQL récursif, c'est trivial:


with recursive deep(n) as(
select entry from h where entry = 2
union
select entry from h join deep on h.parent = deep.n
)
select * from deep;


Le premier élément de l'union est l'amorce de la récursion: l'on démarre donc avec l'élément 2. Le second élément est la récursion en elle-même: l'on retourne l'ensemble des éléments qui sont parents de l'élément n.

La documentation contient quelques exemples beaucoup plus compliqués, où l'on explore des graphes cycliques, avec un SQL qui devient franchement ésotérique. Mais pour des cas simples, c'est un très bon moyen de se débarrasser des couches de pl/pgsql ou autres, et de revenir en pur SQL, ce qui pourrait entre autres améliorer les performances en permettant au planificateur de travailler directement sur toutes les tables.

A propos de planificateur, le nouveau pgadmin3 (malheureusement pas encore dans Squeeze, mais cela ne saurait tarder) montre de bien jolies choses sur ces requêtes récursives. Voici donc l'explain de ma requête.



En faisant quelques tests supplémentaires, je me suis rendu compte que Postgres utilisait gentiment les index qu'on lui donnait sur les colonnes "entry" et "parent", ce qui permettait de gagner environ 25% de temps sur de très grosses requêtes (arbre de 100 000 éléments). Elle est pas belle, la vie?

jeudi 6 août 2009

Postgresql 8.4 débarque dans Debian Squeeze

C'est en faisant mon "aptitude update; aptitude dist-upgrade" hebdomadaire que j'ai vu avec joie que Postgresql 8.4 allait arriver. Vu qu'apparemment, la 8.3 allait jarter du même coup, je suis allé faire mon petit backup, et j'ai lancé la mise à jour.

Tout c'est bien passé, sauf pour Mediawiki qu'il a fallu un poil bidouiller. En effet, en recréant l'utilisateur wikiuser (souvenez-vous du mot de passe!), l'on perd le "search path" vers le schéma "mediawiki", et l'application se met à se plaindre qu'elle ne trouve pas ses tables. Heureusement, la résolution est aisée:


alter user wikiuser set search_path = mediawiki,public;


Et voilà!

vendredi 31 juillet 2009

Développer son MMORPG - Les bibliothèques

4éme opus, où l'on parlera de tout ce qu'il ne faut surtout pas coder soi-même. Moins on écrit de code, et plus on peut utiliser celui des autres (à condition que cela soit de la qualité!), et mieux on se porte.

Combien de lignes de code, pour un MMORPG? Des dizaines de milliers? Des centaines de milliers? Il est hors de question de tout écrire soi-même. Si l'on veut avoir la moindre chance de sortir quelque chose un jour, il va falloir lourdement utiliser toutes les bibliothèques possibles, et se servir au maximum de tout ce code écrit par d'autres. Vu que choisir ses bibliothèques est à peine aussi cornélien que de choisir son langage, voici quelques pointeurs (C++! Hein, pointeurs, C++! Bon, heu, je prends mon manteau, hein?).

À travers le spectre
Pourquoi donc ne pas avoir une seule bibliothèque (on parlerait d'ailleurs plutôt de plate-forme, d'ailleurs) qui soit spécialisée dans le MMO? Ça tombe bien, cela existe, mais il faudra alors entrer dans le moule: ces plate-formes proposent en fait la modification et l'extension d'un jeu de base, fonctionnant sur certains principes immuables, et il sera probablement impossible, par exemple, de transformer une plate-forme fantasy en un space opera. d

D'un autre côté, l'on peut s'orienter vers des petites bibliothèques très spécialisées, donnant une grande liberté, mais ajoutant autant au travail d'intégration.

Enfin, au milieu, il y a les bibliothèques de jeux intégrées (comme par exemple Ogre) qui proposent un moteur graphique complet, un support des sons et des entrées utilisateurs, avec en général une bonne portabilité sur de nombreux systèmes d'exploitations.

Pour AdH, l'on a beaucoup tergiversé entre les deux dernières options, pour au final s'orienter vers des bibliothèques spécialisées, les moteurs de jeux complètement intégrés s'étant avérés trop contraignants. Point bonus, il est plus facile de changer une bibliothèque qui ne convient pas que d'avoir à remplacer tout un framework.

Qu'est-ce qu'il ne faut surtout pas faire soi-même

  • Les couches bas-niveau de portabilité - Le genre de code qui vous permet d'afficher une fenêtre graphique et de lire le clavier à la fois sous Linux et sous Windows est généralement ennuyeux au possible à coder, en plus d'être difficile. Et puis, ce n'est pas cela qui va limiter votre imagination. Regardez la bonne vielle SDL, qui a des bindings dans à peu près tous les langages.

  • La bibliothèque graphique - N'écrivez pas votre graphe de scène, malheureux! Ni les routines de chargement de ressources (images, modèles 3D). Du peu que je connais de DirectX, c'est probablement utilisable directement, mais pour OpenGl, trouvez-vous un bon moteur graphique. AdH utilise OpenSceneGraph, mais il en existe des tonnes dans tous les langages.

  • La couche réseau - Il va falloir pouvoir compter dessus, donc inutile de se fatiguer avec les sockets. Utilisez la couche de haut niveau de votre plate-forme, voire une bibliothèque client-serveur complète

  • Le XML - Si vous en mettez, n'écrivez surtout pas votre parseur! Aujourd'hui, il y a pléthore de générateurs de code, ou encore de sérialiseurs qui peuvent par exemple utiliser la réflexion


Pour bien choisir

Car en plus, il y a du choix! Voici les critères, par ordre de priorité, que nous retenons pour faire notre choix:

  • La licence - Si vous ne pouvez pas légalement utiliser la bibliothèque, inutile d'aller plus loin! En fonction de vos choix, vous pourrez utiliser du GPL, du LGPL, du BSD, ou encore sortir votre porte-monnaie pour acheter une licence commerciale.

  • Portable - Puisque nous voulons tourner sur le maximum de plate-formes, il est hors de question de choisir une bibliothèque qui ne tourne que sous un système d'exploitation

  • Mature - Vous n'allez pas attendre l'année prochaine que la fonctionnalité dont vous avez absolument besoin sorte enfin. Un tiens vaut mieux que deux tu l'auras, choisissez donc une bibliothèque qui fait déjà tout ce dont vous avez besoin

  • Simple - Inutile d'essayer d'intégrer une usine à gaz. Les meilleures bibliothèques sont celles qui peuvent se marier très facilement à votre système, sans forcer une architecture. La bibliothèque doit faire exactement ce qu'on lui demande, avec le minimum d'intégration

  • Maintenue - Rien de plus ennuyeux que d'utiliser des vieilles versions de bibliothèques, utilisant des compilateurs antédiluviens, et présentant des bugs qui ne seront jamais résolus. Au contraire, une bibliothèque bien maintenue va évoluer avec les outils du jour, et les petits problèmes rencontrés ont une plus grande chance de se voir résolus un jour. N'hésitez pas à contribuer si vous avez trouvé un défaut!


L'équipe AdH utilise une règle simple et qui marche la plupart du temps: n'utiliser que les bibliothèques se trouvant dans les dépôts Debian testing. Ansi, c'est l'assurance que la bibliothèque est suffisamment mature, qu'elle est maintenue, qu'elle est libre, et qu'elle sera facile à installer! Il ne reste plus qu'à vérifier l'existence d'une version pour Windows, et roule ma poule.

samedi 18 juillet 2009

Création dynamique des îles

C'est fait, l'on peut maintenant créer les îles dynamiquement! Dans la console, l'on tape par exemple:

/island create here 1457 Minos
et l'île Minos, créée à partir de la racine 1457, apparaît sous vos pieds.

Il me reste cependant à gérer l'UTF-8, car pour l'instant, l'on ne peut pas créer l'île de Ré!

mercredi 8 juillet 2009

Vive les en-têtes précompilées!

J'adore cette fonctionnalité de gcc, d'ailleurs présente, si mes souvenirs sont justes, dans le compilo de Microsoft, bien avant son arrivée dans le compilateur GNU.

L'idée est simple: dans n'importe quel programme d'une taille un peu conséquente, beaucoup de fichiers en-tête sont inclus dans pratiquement toutes les unités de compilation (pensez à la bibliothèque standard, par exemple). Le compilateur passe donc beaucoup de temps à lire et à parser les mêmes fichiers. Pourquoi donc ne pas mouliner ces en-têtes souvent utilisées, une seule fois, vers un format efficace, utilisé ensuite dans chaque unité de compilation.

Dans gcc, c'est d'une simplicité enfantine.

  • Créez un fichier en-tête (par exemple All.h), mettez-y toutes vos en-têtes souvent utilisées.

  • Dans tous vos fichiers source, incluez en premier All.h, et enlevez toutes les inclusions déjà présentes dans All.h

  • Vérifiez que tout compile gentiment

  • Puis, compilez All.h comme un bête fichier source, avec les mêmes options de compilation que tout le rèste. Par exemple, pour le c++:
    g++ -c All.h

  • Admirez le fichier All.h.gch qui vient d'être généré, et compilez vos sources normalement!



Voilà ce que je mets dans mon All.h, pour le serveur AdH:

#ifndef __ALL_H__
#define __ALL_H__

#include <istream>
#include <ostream>
#include <stdexcept>
#include <cmath>
#include <sstream>
#include <vector>
#include <set>
#include <iostream>

#include <boost/shared_ptr.hpp>
#include <boost/scoped_ptr.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/static_assert.hpp>
#include <boost/operators.hpp>
#include <boost/optional.hpp>
#include <boost/scoped_array.hpp>
#include <boost/foreach.hpp>
#include <boost/noncopyable.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/bind.hpp>
#include <boost/variant.hpp>
#include <boost/scoped_array.hpp>
#include <boost/timer.hpp>
#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/asio.hpp>
#include <boost/array.hpp>
#include <boost/random.hpp>
#include <boost/function.hpp>
#include <boost/program_options.hpp>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/identity.hpp>
#include <boost/multi_index/member.hpp>
#include <boost/regex.hpp>

#include <pqxx/pqxx>

#endif


Il me semble avoir déjà dit ici que j'aimais bien Boost.

Voilà les temps de compilation du serveur.
  • Tout seul: 26.16 secondes

  • Avec l'en-tête précompilée:12.92 secondes


Probablement un peu injuste, puisque par exemple je n'inclurais pas boost/regex.hpp dans chaque fichier. Mais la différence est quand même significative, ne serais-ce que pour ne pas avoir à s'embêter à choisir soigneusement les fichiers à inclure dans chaque source, et se retrouver avec des listes d'inclusions standards longues comme le bras dans chaque fichier.

Petite remarque: ma préférence quand à choisir ce qui va dans les en-têtes précompilées est d'y mettre toutes les dépendances extérieures. Les entêtes libpqxx ou wxWidgets y appartiennent, mais ma bibliothèque d'utilitaires non!

Et une dernière pour la route: assurez vous qu'il y a de l'espace disque. Mon gch fait 113 megs!

mardi 7 juillet 2009

Rhaaaaa!

C'est le cri primal du programmeur qui a passé de trop nombreuses heures à traquer des bugs stupides.

C'est en effet le pompon: en 3 jours, pas moins de 3 bugs majeurs qui sont venus tout bousiller. Reprenons dans l'ordre:

- Tout d'abord, ce stupide état OpenGL qui me cassait la couleur de mes îles et leur réflexion, décrit dans le post précédent

- Ensuite, le plan de clipping qui commençait à faire des siennes, coupant mon décor de manière différente en fonction de la taille de la mer (je ne vois toujours pas le rapport!), qu'il a fallu calmer à coups de osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR

- Enfin, un bug encore plus stupide: j'ai voulu charger dynamiquement les îles en threadant le calcul des altitudes et des normales, parce que cela prend un bon bout de temps. Mais surprise, les performances étaient épouvantables! Après un bon moment et beaucoup de recherches, j'ai fini par me rendre compte que tous les threads restaient sur le même CPU, et que cela était dû à OpenSceneGraph qui force l'affinité des threads graphiques, c'est à dire indique à chaque thread quel processeur utiliser, améliorant les performances de l'ordonnanceur du système d'exploitation qui n'a pas à se poser de questions. Mais là où tout part en vrille, c'est que les threads fils du thread principal se retrouvent avec la même affinité, et tout le monde se retrouve sur le même CPU, délaissant totalement les autres!

Ces bugs sont résolus, mais de nombreux problèmes subsistent. En premier, l'agrandissement de la zone d'eau cause maintenant une pixelisation excessive de la texture de reflet, ce qui, plus simplement, veut dire que c'est moche. De toutes façons, mon eau, si elle rendait plutôt bien à courte distance, est bien trop réfléchissante lorsque l'on prend de l'altitude. Ce ne sont plus des îles en Méditerranée, ce sont des tas de farine sur un miroir!

samedi 4 juillet 2009

Archipels

Pfiou! Il a fallu se battre avec OpenSceneGraph, mais voilà enfin un archipel digne de ce nom.

Tout d'abord, il semblerait que charger un fichier 3D mette la machine à états OpenGL dans une configuration bizarre, qui me cassait à la fois mes couleurs et ma réflexion. Après de longues batailles, j'ai fini par contourner le problème en m'assurant que tous mes objets "spéciaux" seraient affichés en premier.

Ensuite, il m'a fallu me battre avec mes îles, et la taille de la grille pour les accueillir.

Mais bon, on s'approche de la création dynamique des îles, et bientôt, les Dieux feront surgir des îles fertiles des océans furieux!

jeudi 2 juillet 2009

Permissions

Je viens juste d'ajouter un petit système de permissions, afin de contrôler les commandes des joueurs.

Comme créer un éditeur de monde me semble au dessus de nos forces, la création du monde se fera en temps réel à partir du client, à travers certains avatars possédant des pouvoirs supplémentaires, tels des dieux, des demi-dieux, ou autres créatures magiques. La première commande permet à l'avatar de savoir où il se situe dans les coordonnées du monde:

/where
renvoie par exemple:
1@-57.446,-418.098,66.9607
pour l'avatar possédant la permission "locateSelf".

Plus tard, il sera possible de créer des îles en définissant ses coordonnées et quelques paramètres de base, et tous les joueurs de la zone devraient voir apparaître le nouvel archipel.

mardi 30 juin 2009

Îles - Gestion du niveau de détail

C'était un peu confus dans ma tête, mais il me semblait bien que mon design séparant la description mathématique de mon île et la génération de la grille d'une résolution donnée allait beaucoup m'aider dans ma gestion du niveau de détails. Voilà le code: je génère une fois la description mathématique de mon île, créée à partir de la racine 37 (et placée à l'origine). Ensuite, j'utilise un osg::LOD auquel j'ajoute des osg::Drawable créés à partir de ma description de l'île et d'une résolution: 200, puis 100, puis 50, puis 20. Une fois toutes les grilles calculées, l'on peut se promener dans un monde qui va ajuster tout seul (à travers la magie d'OpenSceneGraph) le niveau de détail des îles.


IslandGenerator islandGenerator;
boost::shared_ptr t = islandGenerator.generateIsland(0, 0, 37);

osg::LOD * islandLod = new osg::LOD;

osg::Drawable * grid1 = createGrid(*t, 200, 1, 0.05, 20);
osg::Geode * geode1 = new osg::Geode;
geode1->addDrawable(grid1);
islandLod->addChild(geode1, 0, 500);

osg::Drawable * grid2 = createGrid(*t, 100, 1, 0.05, 20);
osg::Geode * geode2 = new osg::Geode;
geode2->addDrawable(grid2);
islandLod->addChild(geode2, 500, 1000);

osg::Drawable * grid3 = createGrid(*t, 50, 1, 0.05, 20);
osg::Geode * geode3 = new osg::Geode;
geode3->addDrawable(grid3);
islandLod->addChild(geode3, 1000, 2000);

osg::Drawable * grid4 = createGrid(*t, 20, 1, 0.05, 20);
osg::Geode * geode4 = new osg::Geode;
geode4->addDrawable(grid4);
islandLod->addChild(geode4, 2000, 10000);

lightsource->addChild(islandLod);


Voilà le résultat:



La transition entre 40000 polygones et 10000 polygones (regardez l'ombre du petit pic de droite)




La transition entre 10000 polygones et 2500 polygones (le petit pic de droite est sacrément diminué)




La transition entre 2500 polygones et 400 polygones (sacrément raboté!)


Bien sûr, j'ai fortement exagéré les niveaux de détail pour apercevoir la transition, mais en étant moins agressif, l'on peut obtenir une bonne réduction du niveau de détail sans que le passage d'un niveau à l'autre soit trop visible. Ainsi, il devient possible d'afficher des centaines d'îles simultanément!

lundi 29 juin 2009

Génération automatique de terrain - Des îles!

J'ai commencé à bidouiller la génération d'îles, et les résultats sont encourageants. J'ai ressorti du placard mon système de collines, en y ajoutant la génération des normales par calcul du gradient de mon exponentielle, ce qui devrait donner de meilleures ombres.

La génération se contente donc d'ajouter plusieurs collines en même temps, et est basée sur un unique nombre qui est la racine de mon générateur aléatoire. L'on donne son entier, et hop, l'archipel surgit des flots!

Un avantage supplémentaire est la séparation de la définition de l'île, comme étant une liste de collines, et de sa représentation graphique, qui est une grille. En faisant jouer la résolution, l'on peut très facilement créer des niveaux de détail différents, et les changer de manière dynamique, permettant d'afficher efficacement les îles lointaines, et de réserver les détails aux îles plus proches.

Voici quelques exemples d'îles générées:


L'île n°5. Les petites bosses correspondent aux toutes petites collines qui ajoutent au relief général.



L'île n°17. Remarquez le petit étang! Certaines collines sont générées inversées, ce qui peut créer des lacs.



L'île n°190. Vue de plus bas, pour le reflet dans la mer.



L'île n°2009. Sacrément biscornue!


Le relief n'est pas la seule chose à générer: je veux pouvoir créer des textures afin d'avoir des couleurs et des aspects différents selon les îles. Ensuite, il faudra ajouter quelques outils au client afin que le maître du jeu puisse créer une île à un endroit donné, juste à partir de son numéro. En essayant plusieurs numéros, il tombera bien sur une île qui lui plaît, laquelle sera alors ajoutée à la carte du monde au niveau du serveur, et envoyée chez tout le monde.

vendredi 26 juin 2009

AdH - rapport d'activité

Voici un extrait du rapport d'activité pour le CA de l'association Reves d'Autres Mondes.

Où en sommes-nous?

AdH est actuellement un client, un serveur et un lanceur, lesquels compilent et fonctionnent sous Windows et sous Linux, nonobstant quelques bugs graphiques dus aux différences entre les cartes au niveau des shaders.

Le lanceur est probablement la grande nouveauté par rapport aux moutures précédentes de l'authentification et de la mise à jour des ressources.
- Le lanceur s'occupe de la gestion des comptes: création d'un login, création d'un avatar
- Le lanceur vérifie la présence des ressources, et télécharge celles qui manquent. Grâce à ce mécanisme, il suffit de permettre aux joueurs de télécharger les deux exécutables (lanceur et client), de quelques mégaoctets, et de laisser le lanceur s'occuper de tout le reste. Le système de téléchargement de ressources est bête comme chou, puisqu'il ne s'agit que de copier les fichiers de ressources et un fichier de description des ressources sur un site Web.
- Le lanceur gère les serveurs multiples: il est possible de voir en même temps l'état (ping et nombre de joueurs) de tous les serveurs référencés d'AdH qui tournent, et de choisir celui sur lequel on veut aller.

Tel qu'il est, le lanceur est considéré "feature complete", et devrait très peu changer, à part peut-être pour la partie "création d'avatar".

Le client fonctionne raisonnablement bien, cependant qu'en termes d'interface il reste très limité. L'utilisation des shaders a permis une très bonne amélioration du rendu, notamment sur les effets d'océan, mais cela veut dire que la qualité de la carte graphique devient particulièrement importante pour avoir de bonnes performances. Il est possible de voir les autres joueurs évoluer dans le monde, avec, contrairement à Bérénice, le support de l'orientation. Les performances réseau sont plutôt bonnes, avec un cout de 5 kb/s par client qui se déplace.

Le serveur est robuste et sobre, sans aucun souci en vue pour la montée en charge, qui sera probablement limitée d'abord par la bande passante. La persistance est encore très limitée, et un redémarrage du serveur perdra les positions des joueurs (mais pas les comptes, qui sont sauvés à la création dans la base de données). A part gérer les conversations et les positions des joueurs, le serveur ne fait pas grand chose!

La prochaine étape

Tout d'abord, une release! Les ressources ont été ajoutées à aubedesheros.net, il va nous falloir remettre Lea sur pied, et nous pourrons sortir une version jouable permettant à tout un chacun de se connecter pour crapahuter dans le monde. Cela nous donnera une première idée des problèmes de compatibilité graphique et de performances.

A long terme

Le modèle basé sur des îles permet de beaucoup simplifier le système de terrain, notamment en permettant d'avoir un niveau de détail différent par île sans tomber dans les soucis de bordures de zones. Je suis en train de remettre en place un système de terrain procédural, avec multitexturing à base de shaders. Il semble hors de notre portée de développer un éditeur de monde, mais quelques fonctions "god-mode" dans le client devraient faire l'affaire.

Les gros morceaux à venir sont le système physique pour les déplacements, sur lequel on pourra ajouter un système basique de combat. A ce stade, AdH sera orienté exploration / baston. Et sur cette base, nous pourrons ajouter les autres systèmes.

Les constantes

Au vu de nos capacités techniques, voilà certains points qui ne changeront probablement pas:
- Animations extrêmement limitées. Ne vous attendez pas à voir des gens bouger, c'est totalement au dessus de notre portée, aussi bien en terme de moteur graphique que de capacités à créer et animer des modèles. Ce sera déjà une victoire si les monstres volants battent des ailes!
- Beaucoup d'eau, quelques îles, plutôt Micronésie qu'Australie. Les grands terrains nécessitent de bons systèmes de niveau de détails, que nous n'implémenterons pas.
- Plutôt plusieurs serveurs séparés qu'un grand système avec tout le monde. J'aimerais être l'Eve-online antique, mais 100 joueurs par serveur semble un maximum. Je suppose qu'un modèle comme le nôtre se dirigera plus vers une liste de serveurs indépendants, qui peut-être se fédèreront pour maintenir une liste commune. Le lanceur supporte totalement ce modèle, puisqu'il permet au joueur de visualiser toute une série de serveurs d'un coup d'oeil, et de rapidement créer un compte pour commencer à jouer dans un autre monde.

dimanche 21 juin 2009

Développer son MMORPG - Persistance!

3ème opus de la série, où l'on parlera de la partie "persistance" dans "univers persistants". Que ce soit pour définir le monde ou pour récupérer l'état du serveur après une panne ou une séance de maintenance, pouvoir compter sur sa couche de persistance est indispensable.

Base de données relationnelle? Listes de clés-valeurs? XML? Gros fichiers binaires? Une chose est sûre, il faudra bien d'une manière ou d'une autre sauver ses précieuses données. AdH a fait très tôt le choix de la base de données relationnelle (Postgresql, pour être précis, mais cela aurait pu être Mysql, ou encore Oracle si on avait les sous).

Voici quelques avantages des bases de données relationnelles:

  • Les données sont accessibles directement à partir des outils fournis avec le système. Changer une position ou réparer un inventaire, c'est juste quelques lignes de SQL.

  • Les données sont en sécurité! Ces systèmes sont stables, utilisés dans d'innombrables projets à travers le monde. Les données sont centralisées, et les outils sont fournis pour sauvegarder et restaurer la base.

  • Il existe probablement déjà une bibliothèque pour accéder à la base depuis votre langage de programmation préféré.

  • Les données sont accessibles pour d'autres applications. Il est très aisé d'extraire des statistiques pour les afficher sur une page Web, ou encore de générer des rapports sur l'activité des joueurs.

Et quelques inconvénients:
    Ecrire le schéma prend du temps, et écrire la couche relationnelle dans le code en prend également.
  • Les changements de schéma peuvent être lourds: une colonne qui change dans une table, et il faut potentiellement changer ses index, ses procédures stockées, son SQL dans le code, et enfin le code lui-même.

  • Il faut installer une base pour faire tourner le serveur, en s'assurant que les binaires sont bien à la même version que le SQL.

Ces derniers temps, l'on a beaucoup parlé des bases de données de type clé-valeur, souvent utilisées dans le Web. Très dynamiques, elles permettent d'étendre le modèle de données très simplement, et sont d'une extensibilité sans pareille. Rajouter des paramètres à ses objets devient un jeu d'enfant. Par contre, être aussi dynamique a un coût: la base peut vite devenir bordélique, et les clés et les valeurs de plus en plus complexes et difficiles à interpréter. Sauver du XML en place des valeurs peut être une bonne manière de permettre de sauver simplement des données complexes, mais on se retrouve alors à définir un schéma et donc perdre tous les avantages du dynamisme.

Enfin, reste la solution du bon vieux fichier. Facile à lire, un peu plus difficile à modifier si l'on veut juste changer un petit quelque chose au milieu, il a l'avantage de ne nécessiter l'installation d'aucun logiciel supplémentaire. Par contre, la protection des données, la définition du schéma, leur accès en dehors de l'application, tout cela est entre vos mains. Ça fait un peu beaucoup, non?

Une fois que l'on est content de sa couche de persistance, encore faut-il décider de quand y accéder. D'un côté, il est possible de tout charger en mémoire au démarrage, et de tout sauver de temps en temps (mettons toutes les 10 minutes, dans un thread séparé pour un maximum de performances). C'est rapide. Mais cela veut dire qu'il faut mapper strictement toutes les données de la couche de persistance vers les objets du code. C'est du boulot! Enfin, il faut trouver le bon équilibre pour les sauvegardes: trop souvent, et les performances souffrent. Trop rarement, et un crash inopiné zappe une bonne partie de l'avancement des joueurs, à leur grande colère. Et enfin, arrive un moment où tout ne tient plus en mémoire... De l'autre côté, l'on peut accéder la couche de persistance uniquement quand on a besoin d'une donnée, et l'écrire à chaque fois qu'elle change. On ne perd rien en cas de crash, par contre, le système peut devenir très lent.

Il s'agit de trouver le bon équilibre entre les deux: quelles données peuvent être chargées à la volée? Peut-on les cacher en mémoire? Quelles donnés ont besoin d'être écrites immédiatement, quelles autres peuvent attendre? Comment s'assurer que tout est synchronisé? Typiquement, on fera la distinction entre les données importantes (une transaction entre deux joueurs, un avancement de quête) et celles qui le sont moins (monstre tué, ressources prises). Personne ne se plaindra si après un crash, une créature n'est pas exactement à la même position, ou si la mine contient encore les minerais que l'on a extrait (mais attention quand même, si un joueur découvre comment faire crasher le serveur, il pourrait se fabriquer une mine infinie?).

Enfin, quand il s'agit de coder tout ça, rappelez vous que moins on en écrit, mieux on se porte. Profitez au maximum des bibliothèques, écrivez du code générique pour binder vos données le plus efficacement possible, ou, encore mieux, utilisez des couches de persistance toutes faites (Hibernate?). Et sauvegardez. Tout. Tout le temps.

On se risque sur le bizarre?

Un petit test à partir d'une grille de 2 millions de triangles. La vitesse d'affichage demeure raisonnable, avec manifestement un goulot d'étranglement au niveau des entrées sorties de la carte: le processeur tourne presque à vide, passant la majorité de son temps à attendre.

De toutes façons, là n'était pas le but. La grille tient le coup, mais maintenant, il reste à voir si j'arrive à faire fonctionner les textures multiples comme je le sens.





Et puis, ça m'aura fait réviser les gradients.

lundi 15 juin 2009

Développer son MMORPG - Communications client serveur

Le combat épique, le duel au sommet: binaire ou XML? AdH a beaucoup changé non seulement sur ce choix de base, mais également sur la manière de l'implémenter.

Récapitulons:


  • Performances: Le binaire est le grand vainqueur. Difficile de faire mieux, puisqu'il n'y a généralement aucune transformation entre les données internes du programme et les données envoyées sur le fil. De son côté, XML devra transformer toutes les données en texte et rajouter des balises un peu partout, et ça prend du temps.

  • Taille: Le XML est généralement plus gros, mais l'on peut parfois avoir des surprises! Par exemple, un petit entier au format texte prendra moins de place qu'en binaire. Comparez: <l>1,2,1,1,1,2,5,2,1,2</l> fait 27 octets, mais passer ces 10 entiers 32 bits en binaire prendrait 40 octets! Certes, les puristes me feront remarquer que séparer les entiers par des virgules n'est pas franchement du XML. Et enfin, compressez!

  • Compatibilité Voilà un sujet dans lequel le XML est roi. Ce n'est que du texte, dans un encodage défini dans le message, il est très difficile de se tromper. Alors qu'en binaire, l'on va devoir parler de grand endian contre petit endian, de taille des types de base, de conventions pour passer la taille d'une chaine ou d'un tableau. Et si ça marche bien chez vous, qui sait ce que ça donnera quand vous enverrez le message à un Mac 64 bits?

  • Lisibilité: Le XML est le gagnant toutes catégories. On dira ce que l'on voudra sur la lecture de gros fichiers XML, mais c'est quand même mieux que de regarder des colonnes d'hexadécimal.

  • Maintenance: C'est sur ce point capital qu'avoir une bonne stratégie fera toute la différence, que ce soit en XML ou en binaire. Tout d'abord en XML: n'écrivez pas votre parseur, malheureux! N'écrivez pas non plus le code qui traduit les structures XML en structures de votre langage. Gé - Né - Rez. De nos jours, pléthore d'outils, tels Codesynthesis XSD et gSoap pour le C++, Microsoft XSD pour .NET, JAXB pour Java, et même mlxsd pour Ocaml (non, je déconne, n'utilisez surtout pas mlxsd pour quoi que ce soit de serieux!), vous font de belles classes qui savent se sérialiser toutes seules à partir d'un beau schéma. Changement de données? On change le schéma, on génère, on compile, on résout toutes les erreurs de compilation, on recompile (car seuls les cons pilent, ah ah!), et voilàte, ça marche! Et pour le binaire, alors? Attendez, on y arrive.


Par chance, comme pour la plupart des problèmes récurrents, il existe multitudes de frameworks qui vous les résoudront, tout en apportant une panoplie de difficultés nouvelles. Mais au moins, l'on ressent un certain soulagement à changer sa peine.

Blague à part, ICE, Corba, gsoap et beaucoup d'autres utilisent l'idée que les messages doivent être définis de manière centralisée via un langage de description, utilisé ensuite par des générateurs de code. Et ça, c'est un concept qui marche aussi bien en binaire qu'en XML. En plus, pour ceux qui auraient quand même décidé de coder le client et le serveur dans des langages différents, il suffit que le générateur de code puisse causer dans ces deux langages, et on est sortis d'affaire.

AdH a démarré sur du binaire, puis est passé au XML, revenu sur le binaire puis fait un dernier petit tour en XML avant de revenir fermement en binaire. N'ayant pas à supporter plusieurs langages, j'ai été faible, et j'ai décidé d'écrire moi-même mon générateur de code. L'avantage principale de mon approche est que je passe directement dans la description les types C++ supportés par mon sérializeur, ce qui veux dire que je peux passer des types très forts, tel mon AvatarId dans l'exemple du post en lien. Tous les autres frameworks m'auraient probablement obligé à passer un entier et perdre le typage, ou à décrire chaque type fort séparément, ce qui aurait sérieusement alourdi la chose.

En conclusion, choisir du XML ou du binaire a beaucoup moins d'importance que la manière dont on va maintenir et utiliser sa structure de messages. De plus en plus, il me semble indispensable d'avoir un bon framework qui pourra générer sans erreurs des brouettes de code ennuyeux. Choppez donc en un, et faites le bosser! Et si vous décidez de l'écrire vous-même, assurez vous que les fonctionnalités que vous rajoutez par rapport à un framework existant vous feront vraiment gagner du temps. Depuis, j'ai rajouté à mon générateur de code l'option de lier certains champs à la base de données, me permettant en quelques lignes de code d'extraire mon objet de la base et de l'envoyer sur le réseau!

samedi 13 juin 2009

Développer son MMORPG - Choisir un language

Premier épisode d'une petite série qui durera ce qu'elle durera! A travers ces années à essayer un peu tout et n'importe quoi, voilà quelques idées sur le sujet des langages de programmation pour les MMO.

Les forums de développement de jeu regorgent de questions relatives aux langages qui sont les mieux adaptés à tel ou tel type de jeu. Lorsque l'on rajoute par dessus les besoins de performance et la complexité du développement d'un MMO, la question du langage devient particulièrement épineuse.

Avec AdH, on a un peu tout essayé: serveur en ocaml et c++, client en ocaml et c++, outils en ocaml, c++, java. Rajoutez quelques essais web avec PHP et .NET, une couche de SQL d'une épaisseur plus que changeante. Voilà donc ce qui a donné l'impression de marcher:


  • Il n'y a pas de langage parfait, et entre deux choix raisonnables, le mieux est de choisir quelque chose qui soit bien connu par les membres de l'équipe, et qui soit suffisamment utilisé par ailleurs pour trouver tout un tas de ressources sur le sujet. Développer son Tetris ou son Pacman pour apprendre un nouveau langage, c'est une excellente idée. Démarrer un WOW-killer à cloche pied avec l'idée d'apprendre en route, beaucoup moins! Également, il est préférable d'éviter les langages trop ésotériques, et choisir des technologies stables, matures et pour lesquels il sera facile de trouver de l'aide.


  • Choisir un langage, c'est aussi choisir un écosystème. Pour cette raison, il est important d'identifier les bibliothèques disponibles. Pour les graphismes, une implémentation OpenGL bas niveau ne suffit généralement pas: avez vous vraiment envie de développer un graphe de scène, d'écrire le code de chargement pour les objets 3D, ou de devoir convertir toutes vos images dans un seul format? Vous aurez suffisamment à coder comme ça pour vous embêter à écrire une couche de base de données, un moteur graphique, une couche réseau haut niveau, un bon support des threads, des bibliothèques pour charger et jouer les sons, charger les images, et j'en oublie.


  • Un même langage pour le client et le serveur. C'est un point très discuté, et pour cause. Avec un serveur et un client qui ont fondamentalement des buts très différents (graphismes et sons, portabilité, performances brutes pour le client - réseaux, bases de données, montée en charge pour le serveur), il est tentant de choisir deux langages. Notre expérience est qu'avec deux langages et une petite équipe, on se heurte à tout une série de problèmes:

    • Souvent, les membres de l'équipe se spécialisent, et il est difficile de faire bouger des gens du serveur au client et vice-versa

    • Tous les utilitaires bas-niveau doivent être ré-écrits deux fois

    • La communication entre le client et le serveur devient plus complexe, il y a d'avantages de risques liés à la sérialisation des données

    • De manière générale, une perte de temps bien supérieure aux gains potentiels


  • Un langage plus agile pour les outils. L'émergence de nouveaux langages de script tels Python, Ruby, Perl et autres peuvent fournir des opportunités pour développer rapidement des outils. La difficulté est de bien s'assurer qu'ils seront peu couplés avec l'application principale: par exemple, un éditeur de monde avancé aura probablement besoin des mêmes structures que le client et le serveur, et le choix d'un langage différent pourrait au final causer une perte de temps. En revanche, la création rapide de donnés, d'un panneau administratif, ou encore la génération de code, peuvent bénéficier des langages de script. Par exemple, dans AdH, le langage Ocaml est utilisé pour générer le code C++ de sérialisation des messages, ce qui a sauvé beaucoup de temps de développement.


Allez hop, maintenant on s'y met!

jeudi 11 juin 2009

Functions, requêtes préparées, ou les deux?

Comme souvent, mes choix technologiques correspondent souvent à de grands coups de volant d'un extrême l'autre. Découvrant les fonctions postgresql, je me suis précipité dessus en voulant les utiliser partout. Un peu plus tard, je découvre qu'elles ne sont pas la panacée:

- Pas de protection particulière contre l'injection, puisque l'appel à la fonction est fait d'un appel SQL normal
- Le plan est compilé une fois pour toutes. Si l'on créé une fonction alors que la table est vide, le planificateur peut choisir d'ignorer les index, ce qui provoquera des performances épouvantables si la table devient tout d'un coup énorme
- Enfin, la modification de la signature d'une fonction entraîne un changement dans la base, ainsi qu'un changement dans le code.

Utilisons les requêtes préparées, donc! Compilées au démarrage de l'appli, lors de leur enregistrement, elles permettent un plan plus optimal, protègent de l'injection, et un changement de requête se fait dans le code, au plus près de leur utilisation.

C'était sans compter les requêtes complexes que nécessitent mon système d'authentification... Les requêtes préparées devenaient alors trop compliquées, forçant beaucoup d'allers-retours entre l'application et la base de données. Et libpqxx, pour être une bibliothèque très bien conçue, nécessite beaucoup de code pour faire tenir une requête dans une transaction et pour en récupérer les résultats.

Je me suis donc remis à écrire des fonctions, au moins pour le système de gestion de compte (création et vérification d'un utilisateur, création et vérification d'un avatar). J'appellerai les fonctions via des requêtes préparées, histoire d'améliorer la sécurité. J'espère ne pas trop avoir à modifier mes fonctions, et je les recompilerai autant de fois qu'il le faudra pour que le planificateur prenne des décisions qui montent en charge!

Ma règle devient donc d'utiliser les requêtes préparées quand je le peux, et des fonctions quand je le dois! Certes, avec du recul, c'est évident, mais ça va mieux en le disant.

D'ailleurs, en parlant de choses qui vont mieux en le disant, il est maintenant beaucoup plus facile de rajouter le support du langage pl/pgsql dans sa base (au moins à partir de Postgresql 8.3). Alors que jusqu'à présent j'indiquais systématiquement le chemin vers la bibliothèque, il suffit en fait de faire:


create language plpgsql

C'est quand même plus simple et mnémotechnique, non?

mardi 26 mai 2009

Cargo

Une petite capture pour se remettre de l'updator.

lundi 25 mai 2009

Une update de l'updator

Je me suis concentré ce week-end sur la vérification et le téléchargement des fichiers, et l'ensemble commence à ressembler à quelque chose.

Au moment où l'utilisateur tente de se connecter à un serveur, l'updator télécharge un fichier XML dont l'adresse lui a été indiquée par le serveur. Ce fichier XML contient la liste des ressources, et leur signature SHA1.

L'updator regarde alors les fichiers présents localement.

- Si le fichier est présent en distant, mais pas en local, il le télécharge.
- Si le fichier est présent en distant et en local, il calcule le SHA1 du fichier local et la compare avec la signature du fichier qu'il est censé recevoir. Si les signatures diffèrent, il télécharge le fichier distant et écrase le fichier local.
- Enfin, si le fichier est présent en local mais pas en distant, il le supprime.

Voilà par exemple ce que pourrait voir l'utilisateur lors de la première connexion:



L'utilisateur se connecte pour la deuxième fois, et tous les fichiers étant déjà là, le processus est beaucoup plus rapide, il suffit de vérifier les signatures.



Allons donc bidouiller le fichier cannon.obj, par exemple en supprimant une ligne. A la prochaine mise à jour, l'updator détecte que les signatures sont différentes, et télécharge à nouveau le fichier.



Le tout est fait dans un petit thread d'arrière plan, qui envoie régulièrement des messages à la GUI pour mettre à jour les logs et la barre de progression, avec l'aide des "custom events" de wxWidgets.

A priori, il est possible d'obtenir la taille du fichier avant téléchargement, ce qui me permettra d'ajouter une autre barre de progression pour chaque fichier, avec pourquoi pas la vitesse de téléchargement.

Il faudra probablement se battre un peu pour que tout cela tourne gentiment à la fois sous Linux et sous Windows, mais une fois que ce sera fait, il deviendra possible de faire une vraie release d'AdH!

dimanche 17 mai 2009

Updator - Graphismes et fonctionnalités

Dites bonjour à l'updator! Tyrion avait bien commencé le back-end, chargé d'aller télécharger les ressources sur un serveur afin que le client soit toujours à jour, et aujourd'hui, comme mes tentatives de donner une GUI digne de ce nom au client ont lamentablement échouées, je me suis dit que j'allais lui donner une petite GUI multi-serveurs et connecter la chose. Maintenant, l'updator peut donc obtenir les informations du serveur, incluant celles visibles sur la capture, mais aussi par exemple l'URL pour la mise à jour.



En jouant un peu avec les threads, j'ai obtenu une GUI répondant comme je le souhaite, avec des informations qui se mettent à jour rapidement. L'on voit très vite si des joueurs sont en train de se connecter, et si la connexion tombe, l'updator va réessayer jusqu'à ce que ça marche.

Maintenant que tous les bouts sont disponibles, il ne s'agit plus que de brancher la partie qui retrouve les informations du serveur avec la partie qui met à jour les données, et l'updator première version sera prêt!

dimanche 10 mai 2009

Ça bouge!

Grâce aux efforts redoublés de Tyrion, le build Win32 est bien avancé, et en dehors de petits soucis avec les fontes et certains formats d'images, utilisable. Entre temps, nous sommes passés à SFML, ce qui m'a forcé à débloquer le taux de rafraîchissement. Ceci dit, c'est probablement mieux pour nos tests de savoir à quelle vitesse on tourne!

Bien sûr, envoyer sa nouvelle position à chaque image a fait exploser la bande passante, chaque client mangeant 120 kB/s. Un petit changement pour envoyer sa nouvelle position seulement 10 fois par secondes, et nous sommes revenus à un 3 kB/s beaucoup plus raisonnable. Le mouvement des autres entités devient un petit peu saccadé, mais ce n'est que partie remise, un poil d'interpolation et il n'y paraîtra plus!


Tyrion barbote

jeudi 7 mai 2009

SFML

SFML, c'est la petite bibliothèque multimédia façon SDL, mais en C++, et dont la version 1.4 vient enfin d'atterrir dans Debian Testing (ce qui veut donc dire que je peux commencer à l'utiliser!). Ce que j'aime bien:

- La propreté du design, en bon C++ moderne
- Des modules bien séparés, dont on prend juste ce dont on a besoin
- Un support de l'Unicode nettement plus poussé que SDL
- Le son basé sur OpenAL, avec le support d'un nombre invraisemblable de format, et le support de la spatialisation des sons

En plus, toute la doc et les tutos sont en français, ce qui est sympa.

Voilà, peut-être bientôt dans AdH!

lundi 4 mai 2009

Un coffre

Puisqu'il va bien falloir un jour penser à l'inventaire, voilà un petit coffre fait à partir de Blender. Je pars d'un cylindre, en vire la partie inférieure, extrude, et rajoute deux surfaces pour faire le fond du coffre. Un petit UV unwrap, et il ne reste qu'à exprimer mon talent d'artiste dans mon Gimp préféré :)



Bon, la texture n'est pas top, mais vu de loin, ça passera.

Et maintenant?

Je suis assailli par le doute.

Jusqu'à présent, le but était clair: faire un chat 3D (je parle du canal de discussion, pas du félin). Bon, bah ça y est, on peut causer, bouger, et voir les autres bouger.

Et maintenant, il y a tellement de directions à prendre que je ne suis pas sûr de laquelle choisir!

- Développer le combat? PvP ou PvE?
- Ajouter des fonctionnalités d'artisanat, de commerce?
- En rester là pour l'instant sur le code, et créer un monde plus étendu, avec plus de décors, que le joueur va avoir envie d'explorer?

Mhh!

dimanche 3 mai 2009

Les avatars

Aujourd'hui, étape importante: il est enfin possible de voir les autres avatars se balader dans le monde! Il a fallu gérer une quantité importante de concepts:

- Le client doit tout d'abord demander au serveur quelle est son identité, afin de ne pas s'afficher lui-même lorsqu'il reçoit les mises à jour des positions
- Une fois l'identité reçue, le client indique au serveur qu'il est prêt à démarrer
- Le serveur enregistre la position du client, et lui envoie les informations de tous les avatars dans la zone. En même temps, le serveur envoie à tous les avatars de la zone les informations du nouveau client
- Quand le client se déplace, tous les avatars de la zone sont notifiés
- Enfin, si le client quitte la zone (par exemple en se déconnectant), les avatars de la zone sont notifiés que le client a disparu



Sur la capture, 3 autres avatars sont représentés par des ULM (le quatrième ULM, c'est juste de la déco). Lorsqu'un ULM se déplace, les autres clients le voient se déplacer. L'on envoie la matrice de position, donc l'orientation de l'avatar est correctement représentée.

Maintenant, il reste à correctement gérer l'identité des avatars, afin d'afficher leur nom. Enfin viendra la gestion des actions, mais ce ne sera pas pour tout de suite!

lundi 13 avril 2009

Océan - Une vidéo

Finalement, je suis tombé sur "gtk-recordmydesktop", lequel a été capable de me sortir une vidéo potable. Voilà donc l'animation de l'océan, basée sur C++, SDL, OpenSceneGraph(OSG), et une épaisse couche de shaders GLSL:

dimanche 12 avril 2009

La couleur de l'eau

J'ai tout d'abord réessayé le bump mapping, ce qui a donné des résultats tout à fait ridicules. Je me suis ensuite basé sur une texture de flotte assez standard, pour des résultats qui m'ont immédiatement plus plu. Cependant, restait le problème de savoir comment mélanger le reflet et la texture d'eau.

- Addition -> Joli, mais les reflets apparaissent beaucoup plus clairs que l'objet original
- Multiplication -> L'eau est presque noire!
- Max -> A peu près potable si la couleur de l'eau est sombre, fait disparaître des éléments qui devraient être reflétés sinon
- Min -> Situation inverse de max, la couleur de l'eau doit être claire, sinon on perd des reflets

En fait, la couleur de l'eau "sans rien" est vraiment la couleur du ciel réfléchie. Ce que l'on veut vraiment, c'est la couleur du reflet si il existe, et l'océan (de fait, le ciel!) sinon. En modifiant ma caméra de reflet pour qu'elle retourne une couleur de fond transparente, j'ai ensuite combiné mon reflet à mon océan en utilisant la valeur alpha dans mon shader:

gl_FragColor = ocean * (1. - refl.a) + refl * refl.a;

Ainsi, mon océan n'est visible que si mon reflet est transparent (c'est à dire correspond à la couleur de fond), et disparaît lorsqu'un reflet existe.

J'ai même la sourde impression que ce code devrait fonctionner même le jour où je voudrais réfléchir des objets semi-transparents, puisqu'effectivement c'est le ciel qui apparaîtrait au travers.

Et voilà le résultat!