mardi 30 décembre 2008

Tablette graphique

Je suis maintenant l'heureux propriétaire d'une tablette graphique Wacom (modèle Bamboo Fun)! Après avoir passé pas mal de temps à la configurer sous mon OS favori, me voilà me réjouissant du dessin à main levée et des niveaux de pression.

Sous Gimp, aucun doute, la tablette change la vie. Pouvoir enfin dessiner comme sur du papier se ressent immédiatement comme un avantage considérable, et permet des trais impossibles à la souris.

Sous Inkscape, je n'ai pas encore trouvé comment utiliser les niveaux de pression, ni la gomme. Cependant, le placement des formes est grandement amélioré, et je pense que l'outil s'avèrera particulièrement utile pour encrer des dessins à la courbe de Bézier (sans s - le correcteur orthographique connait apparemment la ville, mais pas l'ingénieur ENSAM/Supelec...).

Sous Blender, c'est nettement plus bof. Ceci dit, je fais très peu de main levée dans Blender. Peut-être certains modes de type sculpture en feraient une meilleure utilisation?

lundi 22 décembre 2008

Techniquement, c'est bon...

Vive le Blender Book! En quelques minutes, j'avais compris comment ajouter une texture, en l'occurence un bandeau rouge au travers de mon wagon.



Ayant exporté le wagon au format Obj, osgviewer se fit un plaisir de me l'afficher.



Bien sûr, maintenant, la vraie difficulté est de dessiner une texture qui aille un peu plus loin que le "programmer's art"...

Un train avec Blender

Un wagon rapidement modélisé avec Blender. La prochaine étape, c'est d'y ajouter de jolies textures!

mardi 2 décembre 2008

Je n'aime pas les "bindings"

C'est bête, et pourtant, Je sens comme une gêne confuse à utiliser une bibliothèque qui a été développé dans un autre langage que celui que j'utilise. D'un côté, je suis bien évidemment ravi de trouver la bibliothèque qui fait ce dont j'ai besoin. Mais de l'autre, si j'utilise un langage, c'est que j'en admire ses qualités. Pourquoi donc ne pourrait-on pas implémenter cette bibliothèque si utile en natif?

D'accord, dans certains cas, plus particulièrement lorsque l'on est près du matériel, c'est stupide. Qui va s'amuser à ré-implémenter OpenGL en Perl natif? Mais quand il n'y a rien qui semble particulièrement bloquant, pourquoi s'embêter avec les usages ésotériques, la difficulté à débugger, sans parler du coup de boule dans le paradigme. Ah, et j'oubliais les bindings qui ne sont pas maintenus à jour pendant que la bibliothèque sous-jacente propose de nouvelles fonctionnalités.

Si l'on peut trouver une bonne raison de réinventer la roue, que ce soit celle là.

dimanche 2 novembre 2008

Apache ActiveMQ, premiers pas

J'ai donc tripatouillé la bête afin de trouver ce qu'elle a dans le bide. De manière générale, ce que j'y ai vu m'a plu: on y retrouve tous les concepts des solutions propriétaires, et peut-être même un peu plus. Administration via navigateur, messages transactionnels, filtrage et autres, tout est là. Enfin, la mise en place est triviale, ce qui est toujours agréable.

Par contre, la chose est très centrée sur Java, et cela se sent: leur API C++ est une horreur sans nom. Classes dérivées de partout, factories qui ne servent à rien, pointeurs à poil du début jusqu'à la fin, et utilisation abusive des threads, autant dire à des kilomètres du C++ moderne.

Si j'en ai besoin un jour, il faudra que je commence par coder une jolie petite couche d'abstraction histoire de couvrir les endives d'une belle couche de jambon béchamel.

vendredi 31 octobre 2008

Apache ActiveMQ

Voilà un soft intéressant, et que j'ai bien l'intention de creuser. Les queues de message sont un très bon moyen pour construire des applications fortement distribuées, en permettant un traitement asynchrone et transactionnel.

C'est écrit en Java (personne n'est parfait!), mais des bindings existent dans tous les languages.

J'essaie de faire quelque chose avec ça, et je vous tient au courant!

dimanche 19 octobre 2008

Ne me dérangez pas, je joue!

Vous ne vous étonnerez pas de mon silence assourdissant sur ce blog: la version 1 (ou plutôt la version 1.003) de Mount & Blade, le jeu de combat médiéval, est enfin sortie. La page principale du site n'est pas à jour, mais la page de téléchargement l'est. Par rapport à la version précédente, c'est surtout toute une série de finitions très agréables. Parmi celles qui m'ont le plus plu:

- Les tournois dans les villes sont maintenant à thèmes: parfois uniquement arme de jet, parfois à la lance, parfois à l'épée, ce qui rend les joutes plus intéressantes et chaque combat plus homogène.
- Plus de missions, même si assez rapidement je les zappe pour me concentrer sur le combat
- Plus de variété dans les villages, les donjons, les arènes
- Les batailles sont plus grosses!
- Peut-être pas une nouveauté, mais je ne l'avais jamais vu avant: l'on peut joindre un siège du côté des défenseurs, et étriper la piétaille au fur et à mesure qu'elle débarque de la tour

Comme d'habitude, les sauvegardes des versions précédentes ne sont pas supportés, mais l'on peut transférer sont personnage en l'exportant et le réimportant. Par contre, ma renommée est repartie de zéro, il va donc me falloir de longues heures d'exploits avant de retrouver une armée qui me permette de prendre un château!

mercredi 1 octobre 2008

Comment découper son code

Codons! Mais comment diviser son code en bibliothèques? À plus forte raison s'il y a plusieurs applications partageant le code (client serveur, par exemple)?

J'ai tout essayé. Du monolithique à la myriade de bibliothèques hyper-spécialisées. Et j'ai atteint un équilibre qui pour l'instant ne fonctionne pas trop mal.



Game contient le plus de code possible, à l'exception:
- Du code de GUI, qui va dans Client
- Du code réseau et connexion à la base de données, qui va dans Serveur
- Du code suffisamment générique pour ne pas être spécifique à l'application que je développe, qui va dans Common

Je m'interdis d'ailleurs d'inclure les en-têtes de connexion à la BDD, du réseau, et de la GUI, afin de forcer cette distinction.

Ainsi, j'ai par exemple tous mes objets du jeu dans Game, mais chaque objet a des méthodes virtuelles pures pour envoyer un paquet sur le réseau, ou sauver son état dans la BDD. Dans Serveur, ces objets sont dérivés, et le code spécifique y est ajouté.

Je me suis rendu compte que cette séparation était de fait très pratique, puisqu'elle est relativement simple à mettre en œuvre (pas besoin de se casser la tête pour décider d'où mettre le code) et qu'elle rend le code plus propre en séparant la logique de la partie purement communications.

dimanche 28 septembre 2008

Transactions

Pfiou, c'était du sport!

J'ai enfin implémenté le système de transactions dans le cœur du programme.

Voilà le problème: de nombreuses actions des utilisateurs doivent être persistées de manière transactionnelle dans la base. Par exemple, effectuer un achat revient à ajouter le nouvel élément dans la base, et à mettre à jour le solde. Si l'application s'arrête (crash, coupure de courant...) entre les deux, il faut que la base revienne à l'état d'avant la première opération.

Ces transactions sont parfois complexes: par exemple, dans le cas d'un contrat entre deux joueurs, il va falloir mettre à jour les deux soldes, plus les deux inventaires pour les contrats, plus peut-être une autre table pour les modalités du contrat.

J'ai donc implémenté ma solution en passant une liste d'actions à effectuer lors de chaque opération se passant au niveau de la représentation du joueur sur le serveur. L'objet ajoute l'opération à la liste. Lorsque l'opération est finie, et que tous les joueurs ont rempli la liste, je l'exécute.

Voilà un exemple de log lors de l'achat d'un bâtiment: on voit la transaction démarrer, le solde se mettre à jour, la liste des bâtiments du joueur se mettre à jour également, et la transaction est commitée!


Starting transaction
Executing insert into Balance(operation_id, account_id, change, balance) values (default, $1, $2, $3)
Affected 1 rows in 0 ms
Executing insert into CorpBuilding(corpbuilding_id, account_id, building_id, level) values ($1, $2, $3, $4)
Affected 1 rows in 0 ms
Transaction finished in 0 ms

mardi 23 septembre 2008

Encore des icônes avec Inkscape

Des formes géométriques simples, des gradiants, et voilà le travail!

dimanche 21 septembre 2008

Asio et les timers - Une classe qui fait tout le boulot

J'avais précédemment causé des timers d'asio ici, mais vu que je viens de finir une importante sous-partie de mon sous-projet en cours (on a les jalons que l'on peut!), je vais m'étendre sur une petite classe outil qui m'a beaucoup simplifié la tâche.

Le code de la classe en question est un simple en-tête, disponible ici. L'idée est de dériver de la classe outil ainsi fournie en lui passant un paramètre template permettant de définir l'objet de callback, puis d'implémenter la méthode de callback. Voilà un programme minimaliste permettant d'utiliser ladite classe:


/* This program is free software. It comes without any warranty, to
* the extent permitted by applicable law. You can redistribute it
* and/or modify it under the terms of the Do What The Fuck You Want
* To Public License, Version 2, as published by Sam Hocevar. See
* http://sam.zoy.org/wtfpl/COPYING for more details. */

// Timer.cpp

#include <set>
#include <asio/io_service.hpp>
#include <asio/placeholders.hpp>
#include <asio/deadline_timer.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/foreach.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/bind.hpp>

#include "Timer.h"

class MyTimer : public Common::Timer<std::pair<int, std::string> >
{
public:
MyTimer(asio::io_service & io_service):
Common::Timer<std::pair<int, std::string> >(io_service)
{
add_async_event(boost::posix_time::milliseconds(500),
std::make_pair(500, "Every 500 ms"));
add_async_event(boost::posix_time::milliseconds(1300),
std::make_pair(1300, "Every 1300 ms"));
add_async_event(boost::posix_time::milliseconds(1400),
std::make_pair(1400, "Every 1400 ms"));
}

void onTimer(const std::pair<int, std::string> & data)
{
std::cout << data.second << std::endl;
add_async_event(boost::posix_time::milliseconds(data.first),
data);
}
};

int main()
{
asio::io_service io_service;
MyTimer myTimer(io_service);
io_service.run();
}


Certes, il y a beaucoup d'inclusions: elles ne sont pas avec le fichier d'en-tête parce que je me sers de la pré-compilation d'en-têtes, et que donc tous mes fichiers sources ont un #include "All.h" qui contient tout le monde. Mais venons-en au fait.

Je déclare donc ma classe MyTimer, qui dérive de ma classe utilitaire Timer à qui je passe, par exemple, une paire comprenant un entier et une chaîne. J'implémente onTimer prenant une référence sur ce même type, et basta!

En l'occurrence, j'enclenche trois évènements, que je répète quand ils arrivent à expiration dans ma classe.

Une petite compile plus tard...


g++ -o Timer Timer.cpp -lpthread


Et voilà le résultat:


Every 500 ms
Every 500 ms
Every 1300 ms
Every 1400 ms
Every 500 ms
Every 500 ms
Every 500 ms
Every 1300 ms
Every 1400 ms
Every 500 ms
Every 500 ms
Every 1300 ms


Ainsi, chaque classe peut gérer elle-même ses évènements asynchrones. Si par exemple le service est également utilisé pour des connexions réseau, l'on peut gérer ensemble à la fois les interactions venues de l'extérieur, et les minuteries internes.

vendredi 19 septembre 2008

Une nouvelle icône

Promis, je vais finir par arrêter de poster mes icônes une par une. Mais pour l'instant, voilà mon symbole "recherche", dessiné une fois de plus avec Inkscape. Je demeure ébahi par la facilité avec laquelle il est possible de faire des icônes simples avec cet outil:

* Un triangle et deux rectangles pour la bouteille, avec un dégradé vert et un bord épais noir arrondi
* Des bulles à partir du même modèle, disque au contour noir, avec dégradé central blanc -> transparent, légèrement décalé. La bulle est alors joyeusement dupliquée

mercredi 17 septembre 2008

Postgresql - Optimisation d'une requête

Schématiquement, j'ai une table des opérations, qui maintient pour chaque opération un identifiant unique (incrémental), et l'utilisateur qui a amorcé l'opération. Vu que chaque opération maintient l'état final du compte de l'utilisateur, je n'ai besoin de charger au démarrage que la dernière opération pour chaque utilisateur.

Je fais tourner le code suivant dans pgAdmin3.

Tout d'abord, je créé la table des opérations, et je la remplis avec des données aléatoires. J'ai donc 10 millions d'opérations, réparties sur 10 000 utilisateurs.


create table data(id int not null, utilisateur int not null);
insert into data select generate_series(1, 10000000), (random()*10000)::int;


La requête qui m'intéresse est simple:


select max(id), utilisateur from data group by utilisateur;


Arg, 6.3 secondes! Il y a forcément moyen de faire mieux. Un petit tour auprès du plan (F7) nous indique qu'effectivement, la requête balaye toute la table. Mais bien sûr, rajoutons un index! Si l'index est sur l'utilisateur, puis l'id, le plan devrait être trivial et bien plus efficace.


create index data_idx on data(utilisateur, id);
vacuum full analyze;


On relance la requête... Et aucune différence. Pourtant, on sent bien que l'index devrait aider! Mais Postgresql est historiquement un peu faible au niveau des performances des agrégats. Il va donc falloir forcer un peu le plan. Par exemple, en forçant la base de données à commencer à lire la liste des utilisateurs, puis en allant chercher l'identifiant le plus haut. Cela tombe bien, dans mon modèle, il y aura une liste séparée des utilisateurs. Essayons:


create table utilisateurs(utilisateur int primary key);
insert into utilisateurs select distinct utilisateur from data;


Et maintenant, la requête modifiée:


select utilisateur, (select max(id) from data d where d.utilisateur = u.utilisateur)
from utilisateurs u;


Yeah, 370 ms! Bien mieux, et parfaitement en phase avec mes besoins.



En regardant le plan, l'on se rend compte qu'il se passe exactement ce que nous voulions: pour chaque utilisateur, notre index est utilisé pour trouver le maximum. L'on notera que Postgresql a de fait transformé notre requête en ceci:


select
utilisateur,
(select id from data d where d.utilisateur = u.utilisateur order by id desc limit 1)
from utilisateurs u;


Dans les versions précédentes, le planificateur ne savait pas optimiser les max et min, et la meilleure manière d'être efficace était alors d'ordonner les données et de limiter la sortie. Dans 8.3 (ou peut-être 8.2?), le planificateur le fait de lui-même.

Voilà notre requête environ 20 fois plus rapide, cela valait le coup de s'y accrocher, non?

Petit tutorial Inkscape - Le symbole "Radiations"

Simplissime tutorial Inkscape pour dessiner le symbole du danger de radiations

Tout d'abord, commençons par un camembert bien fait!



Dessinez un cercle (utilisez la touche Control pour le rendre régulier), passez en mode éditer les poignées, et déplacez la poignée ronde en gardant le pointeur à l'extérieur du cercle. Tout en maintenant la touche Ctrl, ouvrez votre camembert de 4 graduations, correspondant à 60 degrés.



De retour en mode sélection, cliquez deux fois sur la forme pour voir les poignées de rotation. Déplacez la petite croix représentant le centre de rotation au bout du camembert. Dupliquez l'objet (Ctrl-D) et effectuez une rotation de 120 degrés en maintenant la touche Ctrl. Dupliquez à nouveau, et ré-effectuer une rotation. La forme devrait maintenant ressembler à ceci.



Groupez les trois camemberts (sélection, Ctrl-G), puis ajoutez un petit cercle noir au milieu (vous pourriez avoir à re-sélectionner l'icône "cercle entier"), puis un cercle jaune un peu plus grand, que vous baisserez juste en dessous du cercle noir (Pg Down), puis un grand cercle jaune enrobant toute la forme, que vous mettrez au niveau le plus bas (Répéter Pg Down). Ne vous occupez pas trop de centrer pour l'instant.



Sélectionnez tout, utilisez l'outil d'alignement, et alignez tout au centre dans l'axe vertical et l'axe horizontal.



Ajustez le grand cercle jaune vers le bas pour bien le centrer, et voilàte!

Voilà mon icône finie (avec le petit éclair pour les besoins du symbole).

jeudi 11 septembre 2008

Un peu de sel dans les passwords

Il a suffi d'un regard vers ma table de passwords pour me rendre compte du problème: tous mes passwords de test avaient le même hash. Normal, les passwords sont identiques...

Se contenter de hacher les mots de passes avec du md5 les rend particulièrement vulnérables à une attaque de type dictionnaire, où l'attaquant génère des hashs de mots connus et les compare à toutes les entrées dans la table. L'augmentation du nombre d'entrées augmente du même coup la probabilité de faire mouche.

La méthode standard pour renforcer le hachage est d'introduire un "salt", petit mot aléatoire au début du mot de passe. Je me suis donc attelé à la tâche, et voilà les deux fonctions pour Postgres qui génèrent et vérifient les mots de passe.


-- createPassword (login, password)
create function createPassword(varchar(255), varchar(255)) returns varchar(255) as
$$
select salt.s || md5(salt.s || $1 || $2) from
(select substr(md5(now()::text), 0, 5) as s) as salt
$$ language 'sql' volatile;

-- matchPassword (login, password, hash)
create function matchPassword(varchar(255), varchar(255), varchar(255)) returns boolean as
$$
select (salt.s || md5(salt.s || $1 || $2)) = $3 from
(select substr($3, 0, 5) as s) as salt;
$$ language 'sql' immutable;


Disclaimeur: Si vous faites quoi que ce soit de sérieux avec ces fonctions, prenez conseil auprès d'un pro de la cryptographie. Notamment, je suis sûr que générer le salt à partir d'un hash de l'horloge sera considéré comme une faiblesse

mardi 9 septembre 2008

Une planète

Maintenant, c'est Gimp que j'essaie de dompter. Un petit tutorial plus tard, voilà le travail:

samedi 6 septembre 2008

Une petite dernière

Une pièce avec Inkscape. C'est étonnant comme l'ajout d'un simple dégradé améliore énormément l'image! En l'occurence, je ne m'inquiète pas trop pour les détails, puisque la pièce est destinée à être vue en taille 32 par 32 pour des icônes.

Et une autre sphère!

Décidément, je commence à vachement accrocher sur Inkscape!

Programmer's art

Parce qu'il faut savoir varier les plaisirs, voilà un exemple d'orbe que je viens de dessiner avec Inskcape, basée sur ce tutorial, et cette traduction à laquelle l'auteur (béni soit-il!) a ajouté quelques explications supplémentaires. Je trouve le principe de superposer un dégradé semi-transparent au dessus du premier dégradé absolument génial. Voilà quelque chose que je n'aurais jamais trouvé tout seul.

mercredi 3 septembre 2008

Le Xml est comme la violence: si cela ne résoud pas votre problème, mettez en plus

Il me semblait bien avoir ici parlé de xsd, générateur de code C++ à partir d'un schéma xml, et qui a fait depuis peu son arrivée dans Debian, à ma plus grande joie.

En effet, je m'efforce depuis un certain temps à n'utiliser que des paquets directement disponibles sur les serveurs officiels de ma distribution. Auparavant, preux chevalier du ./configure make makeinstall, je remplissais ainsi mon /usr/local avec des trucs pas forcément faciles à retirer ensuite, provoquant d'innombrables soucis de compatibilité avec des logiciels plus récents qui s'installent via les mises à jour de la distribution. C'est maintenant fini, je n'installe que via aptitude. Et s'il y a un logiciel que je veux vraiment compiler moi-même, je le garde dans mon répertoire (./configure --prefix=~/local est mon ami), là où il est bien moins risqué de jouer du rm à tout va.

Mais je m'égare, et je reviens à xsd. Pour avoir le malheur d'utiliser tous les jours la Concurrence (non, pas de noms!), je puis vous dire que xsd est un plaisir à utiliser, et que j'apprécie tout particulièrement sa légèreté. On génère, on compile, la seule dépendance étant xerces. la Concurrence, donc, souvent requiert une lourde bibliothèque additionnelle, des DLLs, et génère des DLLs. Bref, l'horreur. Car, c'est bien connu, Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl DLL fhtagn, ce qui veux dire "Dans sa demeure de R'lyeh, le défunt Cthulhu rêve de DLLs et attend", ce qui prouve bien leur origine épouvantable.

Mais la raison première de ce post est de publier ces quelques petites lignes, qui, si elles ne servent à personne d'autre, seront au moins pour moi un bon pense-bête. C'est l'ajout d'une cible omake pour générer mon schéma.


.PHONY: schema
schema:
xsdcxx cxx-tree --root-element-all --generate-serialization --hxx-suffix .h
--cxx-suffix .cpp --namespace-map =Schema xsd/*.xsd


Attention, les deux lignes "xsdcxx" sont censées n'en être qu'une seule, mais sinon c'était trop long et c'était le bordel pour faire tenir tout ça sur le blog. Attention², xsdcxx est le nom de xsd sous Debian, puisqu'il existait déjà un utilitaire nommé xsd.

--root-element-all permet que chaque élément défini au plus haut de la hiérarchie puisse être un élément racine, c'est à dire ayant des fonctions pour sérialiser. Si vous n'avez qu'un seul document principal, vous pouvez vous en tamponner le coquillard, mais pour moi qui définit parfois plusieurs messages indépendants par fichier, c'est primordial.
--generate-serialization Plutôt utile pour faire quoi que ce soit avec le fichier sans avoir besoin de passer par les fonctions xerces.
--hxx-suffx et --cxx-suffix Moi, j'aime bien que mes fichiers soient appelés tous pareil. Mes en-têtes sont en .h, mes sources en cpp. Et pas autrement!
--namespace-map Les namespace XML, encore un concept tordu comme seul XML peut les corrompre. Cette commande permet de définir comment transformer un namespace XML en un namespace C++. Pour éviter de finir comme un héros Lovecraftien (mais le XML, c'est plus Dieux Extérieurs que Grands Anciens), je ne définis aucun namespace dans mon XML, mais j'indique à xsd que je veux avoir tout le code dans un namespace à moi.

À noter, la commande --output-dir qui permet d'éviter que tout le schéma ne se déverse dans le répertoire courant.

dimanche 31 août 2008

Icônes

M'intéressant maintenant à une barre d'outils digne de ce nom, je me suis attaqué à la création d'icônes. Malgré de très bon tutoriaux, mon oeuvre demeure du "programmer's art", mais au moins, la bonne idée à piocher, c'est d'utiliser Inkscape. Voilà donc une icône censée représenter un carnet:



Et voilà la grande taille, éditée avec Inkscape:

mercredi 20 août 2008

Être ou ne pas être thread safe

Je codais il y a de cela quelques jours une petite interface pour visualiser nos queues de messages. En programmeur soucieux de la réactivité de mon interface graphique, je me décidais de placer chaque connexion vers chaque queue dans son propre thread.

O tempora, O mores, que n'avais-je pas fait là! Après quelques minutes de tests, des crash intempestifs sont venus me rappeler que les threads ne pardonnent pas l'à-peu-près. En plongeant dans le débuggeur, je n'ai pas tardé à trouver que chaque connexion, apparemment indépendante de ses copines, se retrouvait de fait référencée dans une obscure map au fin fond d'un singleton (argh!), et que cela me mettait dedans.

En effet, la libraire d'accès aux queues, en C, avait été gentiment wrappée en C++, ce qui est bien, mais avec de discutables (et impossibles à désactiver) optimisations qui partagent la connexion sous-jacente, ce qui est moins bien. Kaboum, donc.

Je suis donc revenu à un design bâtard, où les connexions sont toutes dans le même thread, séparé du thread principal de l'interface. C'est donc moche, et vachement moins efficace. Grrr!

samedi 16 août 2008

rdbuf

Mieux vaut tard que jamais, après quand même un bon paquet d'années à codouiller en C++, l'on vient enfin de me faire découvrir une bonne manière de balancer un fichier dans une chaîne. Jusqu'ici, je faisais:


std::ifstream str("input.txt");
std::string s;
std::getline(str, s, '\0');

Ça fonctionne à peu près, sauf si le fichier contient un beau nul. Pour le texte, ça va, mais c'est encore un peu long. Entre le rdbuf!

std::string s =
boost::lexical_cast(std::ifstream("input.txt").rdbuf());

Voilà. C'est quand même plus simple, non?

mardi 12 août 2008

Postgis

J'ai pas mal taquiné Postgis, récemment. Postgis, c'est un ensemble d'add-ons à Postgres, qui permettent de charger et de traiter des données géographiques.



Voici par exemple ce que cela donne en sauvant des donnés d'Amérique du nord dans Postgres, et en les chargeant ensuite dans qgis.

L'add-on va fournir à Postgres des types supplémentaires, de nombreuses fonctions géométriques, et un autre type d'arbre permettant d'indexer très efficacement les données géographiques.

Je me suis dit que ce genre d'environnement me semblait particulièrement adapté à la programmation de wargames! En cherchant un peu sur le net, l'on peut trouver des cartes du monde tout à fait raisonnables. Ensuite, la base de données se charge de trouver, pour une position donnée, si elle est située dans un pays ou sur l'eau, si elle est à distance d'un "point d'intérêt" (une unité, par exemple, ou une ville?). De même, il est alors trivial de définir un chemin à faire suivre par une unité, et à détecter à quel moment l'unité passe la frontière, ou se retrouve sur l'eau.

Ajoutez une petite couche applicative, un client web ou lourd (j'aurais une petite préférence pour le lourd, mais le web rend l'ensemble bien plus accessible... Une applet Java, peut-être?), et l'on a un bon départ pour un jeu de stratégie léger et fun.

Connaissez-vous Colonial Conquest? J'y ai passé de nombreuses heures de ma tendre enfance, sur Atari ST.



Un des points forts du jeu était de pouvoir effectivement conquérir le monde, en plaçant sous son égide des territoires qui correspondaient à quelque chose. Pas très éthique, peut-être, mais très bon pour la géographie, en tous cas!

Postgis fournit la plupart des briques de base pour le back-end d'un jeu de ce type (le seul point difficile étant de gérer les pays limitrophes, et les pays ayant un accès à la mer, mais il doit y avoir moyen). Qui se lancera?

Peu de liens directs avec AdH, en tout état de cause. Il faut dire, ce n'est pas vraiment la même échelle! Cependant, l'on pourrait probablement l'utiliser pour gérer la carte. Dans tous les cas, c'était très instructif.

(Capture de Colonial Conquest piquée ici)

vendredi 8 août 2008

Retour de vacances



Je vais donc pouvoir me remettre doucement au système des marchés!

vendredi 25 juillet 2008

Pourquoi je m'embête avec les licenses

Certains auront pu remarquer que mes extraits de code contenaient une en-tête indiquant qu'ils étaient placés sous la WTFPL.

La raison en est que lorsque l'on veut utiliser un bout de code récupéré sur le Net, bien souvent il n'y a aucune indication sur ses conditions d'utilisation. Doit-on considérer qu'il est par défaut libre de droits (même si dans beaucoup de législations, on ne peut pas décider de "placer" son œuvre dans le domaine public)? Est-ce autorisé pour une utilisation non commerciale?

Bref, la plupart du temps, et moi le premier, on choppe le code, on l'utilise, et on oublie très vite d'où il vient.

Ce n'est cependant pas une raison pour que je fournisse du code avec la même désinvolture. Je le dis donc haut et clair, faites ce que vous voulez avec!

Comment afficher une image dans une wxGrid?

Ma grille de commerce sera bien plus jolie si elle contient des images. Cela tombe bien, wxGrid est très extensible, et tout plein de gens l'ont fait avant moi. Par contre, il m'a quand même fallu recoller les bouts!

La seule spécificité du code est le passage par un wxMemoryDC, qui permet donc de faire un blit exactement sur le rectangle au lieu de baver sur les autres cellules si l'on utilise tout bêtement un DrawBitmap.

Voilà le résultat:



Et voilà bien entendu le code.

wxBmpCellRenderer.h


/* This program is free software. It comes without any warranty, to
* the extent permitted by applicable law. You can redistribute it
* and/or modify it under the terms of the Do What The Fuck You Want
* To Public License, Version 2, as published by Sam Hocevar. See
* http://sam.zoy.org/wtfpl/COPYING for more details. */
#include
#include

class wxBmpCellRenderer : public wxGridCellRenderer
{
public:
wxBmpCellRenderer(const wxBitmap & bmp);
void Draw(wxGrid& grid,
wxGridCellAttr& attr,
wxDC& dc,
const wxRect& rect,
int row,
int col,
bool isSelected);

wxSize GetBestSize(wxGrid& grid,
wxGridCellAttr& attr,
wxDC& dc,
int row,
int col);

wxGridCellRenderer *Clone() const;
private:
wxBitmap m_bmp;
};


wxBmpCellRenderer.cpp

/* This program is free software. It comes without any warranty, to
* the extent permitted by applicable law. You can redistribute it
* and/or modify it under the terms of the Do What The Fuck You Want
* To Public License, Version 2, as published by Sam Hocevar. See
* http://sam.zoy.org/wtfpl/COPYING for more details. */
#include "wxBmpCellRenderer.h"

wxBmpCellRenderer::wxBmpCellRenderer(const wxBitmap & bmp):
m_bmp(bmp)
{
}

void wxBmpCellRenderer::Draw(wxGrid& grid,
wxGridCellAttr& attr,
wxDC& dc,
const wxRect& rect,
int row,
int col,
bool isSelected)
{
wxGridCellRenderer::Draw(grid,attr,dc,rect,row,col,isSelected);
wxMemoryDC memdc;
memdc.SelectObject(m_bmp);
dc.Blit(rect.GetX(), rect.GetY(), rect.GetWidth(), rect.GetHeight(),
&memdc, 0, 0);
}

wxSize wxBmpCellRenderer::GetBestSize(wxGrid& grid,
wxGridCellAttr& attr,
wxDC& dc,
int row,
int col)
{
return wxSize(m_bmp.GetWidth(), m_bmp.GetHeight());
}

wxGridCellRenderer * wxBmpCellRenderer::wxBmpCellRenderer::Clone() const
{
return new wxBmpCellRenderer(m_bmp);
}

jeudi 24 juillet 2008

Les ordres

C'est parti, j'ai commencé à coder le système d'ordres. Après avoir fortement typé mes prix et mes quantités, j'ai pu coder un ordre comme ayant un identifiant personnel, un identifiant d'objet, un prix et une quantité. Pour les actions sur les ordres, c'est on ne peut plus simple: on ajoute un ordre, ou on le supprime!

Maintenant, il me reste à trouver une bonne structure de données pour maintenir les ordres du côté du serveur (avec probablement de la persistance dans la base de données), et une bonne structure de données pour envoyer les données de marché vers les clients.

dimanche 20 juillet 2008

Des nombres fortement typés

Je viens de terminer mon dernier test unitaire (vive la Boost Test Library!) sur ma classe de nombres fortement typés. L'idée est, comme pour les identifiants, d'éviter de s'emmêler les pinceaux en comparant ou effectuant d'autres opérations sur des nombres qui n'ont rien à voir. C'est à dire que quand j'ajoute des choux et des carottes, je veux que le compilo gueule bien fort.

Le souci, c'est qu'il faut ajouter à notre classe de nombres forts tous les opérateurs généralement présents sur les nombres, et il y en a un paquet. Heureusement, vive la Boost Operators Library vient à notre rescousse, en déduisant tout un tas d'opérateurs à partir d'une liste restreinte à implémenter.


template<typename D, typename T>
class Number : boost::operators<Number<D, T> >
{
public:
Number();
Number(const Number & n);
Number(const T & value);

bool operator<(const Number & n) const;
bool operator==(const Number & n) const;
Number & operator+=(const Number & n);
Number & operator-=(const Number & n);
Number & operator*=(const Number & n);
Number & operator/=(const Number & n);
Number & operator%=(const Number & n);
Number & operator|=(const Number & n);
Number & operator&=(const Number & n);
Number & operator^=(const Number & n);
Number & operator++();
Number & operator--();

T getValue() const;

private:
T m_value;
};


Et avec ceci, les opérations +, -, *, / et les autres sont toutes définies gentiment.

jeudi 17 juillet 2008

Le marché - Flux de données

Pour avoir un peu côtoyé ce genre de code, je sais qu'il est parfois un poil complexe d'afficher tous les prix d'une marchandise donnée dans une liste achat/vente. La raison en est que de nouveaux prix vont devoir s'insérer, ou des prix visibles disparaitre en faisant remonter toute la pile. Il faut donc gérer au niveau du client un registre local des prix, mis à jour par le serveur, et qui peut afficher la profondeur de prix voulue par l'utilisateur. De plus, l'utilisateur peut reconnaître certains prix particuliers, comme les siens, ou ceux entrés par un groupe d'utilisateurs particuliers. Ceci veut dire que le serveur doit envoyer une vue légèrement différente à chaque utilisateur, en cachant les identités des prix pour lesquels l'utilisateur n'a pas de permissions.

Bien entendu, tout un tas de monde s'est heurté au même souci avant moi, et c'est pour cela que la plupart des logiciels utilisés dans les échanges séparent d'un côté les ordres de l'utilisateur, et de l'autre les données de marché. Ainsi, plutôt que de voir la liste des prix, avec par exemple ses propres prix surlignés, l'utilisateur voit le marché, qui est l'agrégation des prix les plus hauts (bas pour les ordres de vente), et la liste de ses propres ordres. Le serveur envoie donc la même vue du marché à tous, et seulement les mises à jour de leurs propres ordres à chaque utilisateur.

Vous l'aurez deviné, c'est bien cette deuxième méthode que j'ai l'intention d'utiliser dans AdH!

mardi 8 juillet 2008

Actions longues

Enfin, c'est implémenté! Ce système permet maintenant à une action de s'effectuer sur la durée. Le joueur démarre donc une recherche, et en reçoit le résultat 30 secondes plus tard. S'il démarre une autre recherche, ou change de lieu, la recherche courante est annulée.



Tout est dans la base de données, il est donc possible pour chaque lieu d'y ajouter la possibilité d'y rechercher des choses. Chaque recherche est définie par le temps passé à chercher, et d'une liste d'objets potentiellement trouvables avec leurs probabilités respectives. Il est ainsi possible de donner 1 chance sur 1000 de trouver une perle naturelle sur la plage!

dimanche 6 juillet 2008

Ils font rire les gosses mes static*

[*] Point bonus à ceux qui pigent la vanne

Le mot-clé "static" est une cochonnerie. Ou plutôt, un outil bien spécifique utilisé à tort et à travers dans le mauvais code. En fait, l'on pourrait probablement dire que "static", c'est le SUV du C++. Non, vous n'en avez pas besoin pour aller chercher le pain. Ni même pour aller en vacances à Biarritz. En revanche, si vous écumez les chemins de terre, on peut commencer à discuter.

Ecumons donc les chemins de terre!
Tout d'abord, et afin de tout compliquer, le mot-clé "static" a plusieurs utilisations.

  • Tout d'abord, et c'est un héritage du C, une variable globale "static" n'est visible que dans son unité de compilation. Cette utilisation est tombée en désuétude avec l'apparition des namespaces, et notamment des namespaces anonymes.

  • Ensuite, et c'est encore du C, une variable statique dans une fonction ou une méthode agit comme une variable globale visible uniquement dans son scope, initialisée la première fois que la fonction est appelée, et gardant sa valeur d'un appel à l'autre.

  • Enfin, et cette fois ci c'est du vrai C++, les méthodes et attributs d'une classe peuvent être déclarés static, c'est à dire relatifs à une classe et non à une instance.

J'ai trouvé que la variable statique dans une fonction était particulièrement utile pour débugger son programme. Par exemple, je cherche à afficher pour chaque appel de la fonction quelles étaient ses paramètres:


int f(int a, int b)
{
static n = 0;
n++;
std::cout << "Appel" << n << " - " << a << ", " << b << std::endl;
...
}


Ceci affichera, par exemple:

Appel 1 - 3, 7
Appel 2 - 7, 43
...
Appel 254 - 4, 25

Voilà, mais par contre, ce n'est pas particulièrement utile en production!

Une deuxième utilisation est d'utiliser une variable statique comme cache. Par exemple, imaginons que la fonction f soit très gourmande en calcul, mais que par ailleurs elle soit uniquement dépendante de ses paramètres, et qu'elle soit souvent appelée avec les mêmes. L'on peut faire la chose suivante:


int f(int a, int b)
{
typedef std::map<std::pair<int, int>, int> CacheT;
static CacheT cache;
CacheT::const_iterator it = cache.find(std::make_pair(a, b));
if(it != cache.end())
{
return it->second;
}
else
{
int result = calcul_long(a, b);
cache.insert(std::make_pair(std::make_pair(a, b), result));
return result;
}
}


Et voilà, un beau cache ajouté à la fonction, sans rien cochonner autour! Par contre, cette technique montre assez rapidement ses limites, particulièrement lorsque le code est inclus dans un service censé tourner 24h/24. Il est en effet impossible de vider le cache, ou d'en tirer des statistiques. Même si l'on implémente un cache plus malin qui ne garde que les n dernières entrées, la taille des entrées est fixée à la compilation. Ou alors, il faut la passer systématiquement en paramètre de la fonction. Si c'est le cas, pourquoi ne pas passer le cache lui-même, ce qui sera bien plus propre. Et que l'on ne vienne pas me parler de singleton, les variables globales, c'est le mal!


En parlant de variables globales, causons un peu des méthodes et attributs statiques. Sur le sujet, je dirais que c'est rarement nécessaire, et souvent dangereux, en dehors des cas où ils sont utilisés à travers des templates. Par exemple, lors de l'implémentation d'une factory qui prend mes messages ayant des identifiants.


class LoginMessage
{
...
static int getId() {return 1};
};

class LogoutMessage
{
...
static int getId() {return 2;}
};


Quand j'enregistre mon message dans ma factory en passant le message comme un paramètre template, je peux en extraire l'identifiant.


template<typename T>
registerMessage()
{
m_messageMap.insert(T::getId(), ...); // ... est par exemple un pointeur vers une fonction qui renvoie un nouvel objet T
}


Et je peux donc enregistrer mes messages ainsi:


factory.registerMessage<LoginMessage>();
factory.registerMessage<LogoutMessage>();


Voilà, j'en ai écrit un peu plus que ce que j'aurais imaginé. Probablement la frustration de devoir maintenir du code plein d'horreurs statiques dans tous les sens. Alors, "static", évitez-le quand vous le pouvez, utilisez-le quand vous le devez!

samedi 5 juillet 2008

Wrapper zlib, un panégyrique

Il semblerait bien que mon wrapper C++ de la zlib soit un particulièrement recherché, si j'en crois mes stats. Je tiens donc à préciser que le bout de code en question tourne joyeusement en production au boulot, et que je l'utilise également dans le code réseau d'AdH. Pour l'instant, pas de bugs en vue, et une utilisation plutôt sobre de la mémoire.

En regardant les autres implémentations sur le net, l'on s'aperçoit que beaucoup sont centrées autour des fichiers, ce que je trouve plutôt étonnant. Je ne vois en effet pas vraiment l'intérêt de s'amuser à compresser ou décompresser un fichier à partir d'un programme C++. Si par exemple l'on est inquiet de la taille que prennent les logs, pourquoi ne pas avoir une tâche quotidienne qui appelle un bon vieux tar bz2? Si ce sont des données binaires, sont-elles si grosses qu'elles vaillent la peine de se fatiguer à les compresser, pour un gain généralement assez faible? Si c'est du XML, est-ce la peine de perdre le gros avantage de pouvoir l'éditer à la main? Bien sûr, il y a des cas où utiliser directement zlib pour compresser un fichier aura un avantage, mais à ce point?

En l'occurrence, j'utilise mon wrapper dans deux cas. D'une part, pour sauver des fichiers XML dans une base de données. Nos utilisateurs veulent sauver des frames XML de tailles qui s'approchent du mégaoctet, et la base n'a pas beaucoup de place libre. La compression permet de gagner jusqu'à 20 fois l'espace, et nos utilisateurs, au lieu de faire sauter la base au bout de 1000 entrées, ce qui risque d'arriver dans l'année, pourront insérer jusqu'à 20 000 entrées, ce qui nous permet de souffler un peu. Et j'en conviens, avoir seulement 1 gigaoctet d'espace dans la base, c'est pas malin, mais que voulez-vous, c'est corporate...

D'autre part, et c'est là que la simplicité prime, compresser les paquets que l'on envoie à travers le réseau résulte en un très bon gain de performances, et permet de voir venir des structures beaucoup plus grosses. Bien sûr, n'oubliez pas que le gain dépend de la taille des données, et qu'il vous faut trouver la limite à partir de laquelle il est intéressant de compresser! Décidément, c'est la journée des backlinks, j'en avais causé ici.

Et dans ces deux cas, je ne peux m'empêcher que l'approche fonctionnelle, une fonction pour compresser, une autre pour décompresser, est de loin la plus simple et la plus élégante! Pourquoi aurait-on à créer un objet, et s'embêter avec deux ou trois couches d'abstraction? Ou pire, utiliser des méthodes statiques! Beurk...

Du simple (deux petites fonctions), du fonctionnel (fonctions libres, pas d'effets de bord), du standard (istream, ostream, vous pouvez même passer un fichier si ça vous fait plaisir).

Oh, et j'ai reçu mon WTF mug! Cool, non?

samedi 28 juin 2008

asio et les timers

Dans AdH, certaines actions s'effectuent dans la durée. Par exemple, un joueur peut décider d'aller à la plage pour ramasser des coquillages, et avoir par exemple une chance d'en trouver toutes les 10 secondes. S'il décide de s'en aller, l'action courante est automatiquement annulée.

Ce qui tombe bien, c'est que asio::deadline_timer gère tout cela (presque) tout seul. Je peux créer une action, et lui dire d'appeler une méthode de ma classe de session dans un temps donné. Après ce délai, la méthode est appelée, et je peux alors effectuer l'action, par exemple rajouter un coquillage à l'inventaire du personnage.

Si le joueur change de lieu, il est facile d'annuler l'action, simplement en appelant cancel. La méthode d'action est alors appelée avec un code d'erreur, et je peux alors en profiter pour renvoyer au joueur l'information que l'action a été annulée.

Le système gèrera ainsi une action courante pour chaque joueur, qui sera utilisée notamment par toute la partie artisanat.

vendredi 27 juin 2008

L'inventaire

Le voili, le voiça, l'inventaire!

Placé dans une belle grille (j'aime bien les grilles wxWidgets, elles sont très flexibles), et directement remplie par le serveur. Maintenant, pour la voir évoluer, il va falloir commencer à gérer les actions. La première action que je compte implémenter, c'est ramasser des coquillages sur la plage. Ces coquillages pourront ensuite être revendus à un bijoutier qui en fera des colliers.



Donc, je récapitule: actions, commerce, et artisanat! Encore quelques soirées à coder en perspective.

lundi 23 juin 2008

Memcached

Connaissez vous memcached? C'est un système de cache distribué, qui permet de sauver et de retrouver un bout de binaire à partir d'une clé, la clé étant un autre bout de binaire. Ainsi, l'on a accès à une quantité de mémoire limitée seulement par le nombre de machines qui font tourner le service, et à partir du moment où l'on connait la clé, toute application pouvant se connecter au cache peut récupérer les données. Rajoutez quelques détails comme la gestion de la durée de vie des objets, et c'est un service tout à fait redoutable qui se profile.

Memcached est principalement utilisé dans les applications web. J'avais installé Mediawiki en local, et voyant le pic CPU lors de la génération de chaque page, je me demandais quel type de hardware wikipedia pouvait bien avoir sous le capot pour gérer autant de requêtes à la fois. La réponse, c'est memcached! Au moment d'accéder à la base de données, Mediawiki regarde tout d'abord si les données sont accessibles dans le cache. Si oui, il les récupère, s'épargnant une couteuse requête auprès de la base de données. Lorsqu'un utilisateur modifie la page, l'entrée dans le cache est supprimée, pour être remplacée par la nouvelle page. En majorité, seules les requêtes en écriture vont effectivement arriver sur la base de données, ce qui améliore grandement les performances. De fait, de nombreux sites à fort débit utilisent memcached.

Récemment, je travaillais sur une application qui commence à poser problème parce qu'elle arrive aux limites des 2 gigaoctets d'espace d'adressage. L'on a bien passé Windows en mode 3 gigaoctets, au prix d'une stabilité réduite, et de soucis avec d'autres applications qui ne s'attendaient pas à ce que le noyau aie son propre espace aussi réduit. Je me disais que c'est probablement la seconde utilisation qui puisse être faite de memcached: au lieu de garder les objets directement dans la mémoire du processus, on peut les envoyer dans la mémoire d'un autre processus, potentiellement sur une autre machine, et libérer ainsi le précieux espace disque sans pour autant trop impacter les performances, en attendant que l'on passe tous enfin en 64 bits.

mardi 17 juin 2008

Ressources C++

J'avais fait cette petite liste pour aider deux collègues qui se verraient bien lâcher le VB et faire du C++. Voici donc les ressources (bouquins, sites web, bibliothèques) que je considère comme un très bon point de départ pour approfondir le langage en allant un peu plus loin que la syntaxe.

Livres

* Andrei Alexandrescu - Modern C++ Design - Un excellent bouquin qui ouvre vraiment les yeux sur ce qu'il est possible de faire avec le C++.
* GOF - Design Patterns - Plutôt une référence, ne se lit pas vraiment comme un roman! Utile, probablement, mais gare cependant avec les patterns. Comme pour épicer son repas, le tout est de savoir doser.
* Scott Meyers - Effective C++ - Une bonne lecture pour s'habituer à coder proprement. Très sain.
* Scott Meyers - More Effective C++ - La digne suite du précédent.


Sites web

* C++ FAQ Lite - Reprend point par point les petites bizarreries du C++. Un must pour préparer les interviews!
* SGI STL - La doc STL. S'il ne doit y avoir qu'un bookmark sur le C++ dans votre navigateur, c'est celui-là.
* Guru of the Week Archive - Très intéressantes discussions sur le C++
* Project Euler - Tout plein de problèmes mathématiques et algorithmiques. Un excellent entrainement à la maitrise d'un langage.

Bibliothèques

* Boost - Le complément indispensable à la STL, Boost est vraiment l'API du C++
* SDL - C'est du C, mais c'est quand même très utile pour coder des jeux en C++.
* SDL_net - Si vous êtes rebuté par boost::asio, la bibliothèque réseau de la SDL est très simple d'utilisation.
* wxWigets - Un framework pour coder des interfaces graphiques.
* libpqxx - La bibliothèque pour interfacer le C++ à la base de données Postgresql.

samedi 14 juin 2008

Quand on compresse d'un côté...

Il vaut mieux penser à décompresser de l'autre. Il m'aura fallu une bonne demie-heure pour comprendre pourquoi j'avais droit à un plantage magistral lorsque j'envoyais trop de texte.



Enfin, c'est résolu, et cela marche même plutôt bien avec les alphabets non romains. Ce n'est malheureusement pas non plus parfait, comme les sinophones (non, Firefox, pas les saxophones!) distingués auront pu le remarquer: la longueur des caractères chinois est mal calculée par la fenêtre, et le passage à la ligne se produit donc beaucoup trop loin, cachant toute une partie du texte. Peut-être un rapport de bug à remonter chez wxWidgets?

libpqxx - Un tutorial

Aucun doute, libpqxx est une bibliothèque très bien foutue. C'est un wrapper C++ autour de la libpq, qui est l'interface C de Postgres.

Cependant, j'ai trouvé que la documentation était trop orientée sur la description des interfaces, et trop peu sur l'utilisation en elle-même. Voici donc un petit topo sur les transactors.

Les transactors, ce sont des objets qui gèrent l'ensemble de la transaction, le support du rollback, et les essais multiples en cas de problème de connexion. Typiquement, un transactor qui fait simple SELECT pourrait ressembler à cela:


class SelectObjects : public pqxx::transactor<pqxx::transaction<> >
{
public:
SelectObjects(std::vector & objects):
m_objects(objects)
{
}

void operator()(argument_type & T)
{
std::ostringstream sql;
sql << "select name from objecttype";
pqxx::result result = T.exec(sql.str());

pqxx::result::const_iterator it = result.begin();
pqxx::result::const_iterator end = result.end();
for(; it != end; ++it)
{
m_objects.push_back(it->at(0).as());
}
}

private:
std::vector & m_objects;
};


A noter, la structure de données qui sera remplie est passée par référence. En effet, le transactor sera copié au moins une fois, et potentiellement plusieurs si la connexion est interrompue et que la commande doit être relancée.

L'on appellera le code ainsi:


// dbConn est une pqxx::connection valide
std::vector objects;
dbConn.perform(SelectObjects(objects));


L'on pourra ensuite facilement vérifier, à l'aide de notre ami Boost, que les données ont été récupérées:


BOOST_FOREACH(const std::string & obj, objects)
{
std::cout << obj << std::endl;
}


Allez, une autre pour la route: imaginons que l'on veuille insérer une valeur dans la table.


class InsertObject : public pqxx::transactor<pqxx::transaction<> >
{
public:
InsertObject(const std::string & value):
m_value(value)
{
}

void operator()(argument_type & T)
{
std::ostringstream sql;
sql << "insert into objecttype values "
<< "(5, '" << T.esc(m_value) << "', 0.7)";
T.exec(sql.str());
}

void on_commit()
{
std::cout << "Success!" << std::endl;
}

private:
std::string m_value;
};


L'on remarquera le T.esc() qui permet d'échapper la chaine en bon SQL. Tant qu'à faire, autant éviter l'injection! La méthode "on_commit" sera appelée automatiquement s'il n'y a pas eu de souci pendant la transaction.

Voilà, en espérant que ces quelques fragments soient utiles un jour!

lundi 9 juin 2008

Ça cause!

Quand je disais qu'avec la nouvelle architecture, tout devenait simple! En un peu plus d'une heure, le système de chat était de nouveau sur les rails.



Rien de très compliqué, mais c'est à peu près le niveau de chat disponible dans la version Castor. En dehors d'un peu de nettoyage de code et d'une refonte de l'interface, la prochaine étape n'est pas encore choisie. L'inventaire? Le système de commerce? Le système de villes? Ou simplement livrer cette version (ce qui veut dire une compilation sous Windows, youpie!), et tenter de la faire monter en charge?

dimanche 8 juin 2008

Authentification

Le système d'authentification est enfin fini. Il permet à l'utilisateur de se connecter à un serveur, de s'identifier, et de choisir l'avatar qu'il souhaite incarner. Petite visite guidée.

C'est le même écran que deux posts auparavant. L'utilisateur choisit son serveur, entre ses informations, et se connecte. En l'occurrence, mon compte n'existe pas encore, je coche donc pour le créer à la volée.



Voilà la liste de mes avatars. C'est un nouveau compte, aucun n'est donc disponible. Je créé donc Nestor.



La prochaine fois que je me connecte, mon cher Nestor apparait dans la liste des avatars disponibles. Je peux le sélectionner, ou en créer un autre.



Enfin, toutes les erreurs possibles sont attrapées. Si je retente de créer Nestor, un message d'erreur m'expliquera le problème.



L'architecture tient étonnamment le coup. Cela n'a pas été toujours sans soucis: par exemple, il m'a fallu jongler un peu avec les évènements wxWidgets: mes évènements réseau étant gérés à travers le même mécanisme, l'on casse tout si l'on tente par exemple d'afficher une boite de dialogue modale directement dans l'évènement, et il faut donc se rajouter à la queue d'évènements de la boite de dialogue courante.

La prochaine étape, porter le vieux système de chat vers la nouvelle architecture. Rien de trop complexe en théorie, mais la possibilité d'envoyer de gros messages va enfin mettre à contribution le système de compression des paquets. Restez branché!

vendredi 6 juin 2008

Ping!

Ce soir, ajout du ping! Le framework étant en place, ce fut à tout casser 45 minutes de boulot, donc
ça vaut amplement le coup pour le petit côté wizzz.

Petit récapitulatif de la manière dont fonctionne la boite de dialogue d'authentification:



Le client connait, à partir par exemple d'un fichier XML (ou pourquoi pas d'une ressource web centralisée), la liste des serveurs publiés. Il tente de se connecter à chacun des serveurs, en lui demandant ses infos personnelles. Dans la boite de dialogue, le premier serveur est fonctionnel, et a donc répondu en donnant son nom, sa description, sa langue, le nombre de joueurs connectés, et le ping. Le deuxième serveur n'existe pas, la connexion échoue donc, et le serveur demeure affiché en orange.

Le ping, c'est tout bête. Le client calcule le nombre de millisecondes depuis le début de la journée, l'envoie au serveur, qui se contente de copier l'info et de la renvoyer avec quelques informations supplémentaires (le nom, la description, et autres). Au retour, le client refait le même calcul, petite soustraction, et basta.

Rajout d'une petite horloge, qui renvoie le ping pour chaque connexion encore valide. Je reçois, je mets à jour. Byzance!

jeudi 5 juin 2008

Segfault

Un étrange crash que je viens de découvrir: définissant par erreur deux fois de suite la valeur à afficher dans un contrôle d'édition de wxWidgets, il se produit une segfault apparemment à cause d'un mutex dans un autre thread, dans la libglib. La correction a bien entendu été triviale (ne définir la valeur qu'une fois), mais ce problème m'interpelle, surtout que les conditions pour le reproduire ne semblent pas si ésotériques. Peut-être un souci d'évènements qui se télescopent?

jeudi 22 mai 2008

wxWidgets et les sockets

Me voilà très favorablement impressionné par le support réseau de wxWidgets, totalement intégrée dans la gestion d'évènements. Je trouve de manière générale qu'une des difficultés de l'intégration du réseau est de gérer correctement l'aspect asynchrone. L'on peut coder soi-même son petit thread et sa queue de messages, mais c'est vite lassant, et ne résout pas le problème de manière totalement satisfaisante. Doit-on aller regarder si un message attend dans la queue de temps en temps? Ou traiter le message de manière préemptive, au risque d'avoir à coller des mutex à travers tout le code? On se retrouve vite à coder une mauvaise implémentation d'un système d'évènements. Pourquoi donc ne pas utiliser wxSocket?

Dans l'utilisation que j'en fait, j'ai une liste de sockets que j'ouvre les uns après les autres, et qui vont aller sonner, de fait, chacun des serveurs de la liste, affichant dans le client l'état du serveur (s'il est up, le nombre de connectés, une courte description...). Je démarre donc toutes mes connections, et j'attends les évènements. Pas de threads, pas de select, juste une méthode qui sera appelée à chaque fois qu'il y a quelque chose d'intéressant. Si j'ai bien compris, l'on peut même dire au système: "Préviens moi quand tu as reçu tant d'octets", ce qui colle particulièrement bien avec mon protocole.

La seule chose qui me chagrine... Pour différencier les clients, qui sont passés à la méthode de gestion de l'évènement, il faut leur passer un "ClientData", horrible void * qu'il faut caster pour savoir de quoi il retourne. Mais je serais bien en peine de trouver un autre mécanisme plus élégant.

mercredi 21 mai 2008

Autosatisfecit

Petit post en forme d'autosatisfecit, mon wrapper de zlib arrive en deuxième position dans google France sur la recherche "zlib c++". Youpie!

J'en profite pour mentionner que le code est en prod, et que personne ne s'en est plaint jusqu'à présent. Le code est également présent dans AdH, et compresse les messages qui font plus de 100 octets. Autant la charge mémoire et CPU ne m'inquiète absolument pas, autant, avec les moyens du bord, c'est la bande passante qui va rapidement s'étioler...

dimanche 18 mai 2008

Grilles

Voilà une petite capture de ce à quoi ressemble la grille de commerce pour le moment. Désolé pour les en-têtes en anglais, je suis encore loin d'avoir décidé comment gérer la localisation.



Cette grille sera en lecture seule, et contiendra pour chaque marchandise les prix du marché. Le joueur peut y ajouter des prix, et quand le marché se croise, les marchandises et les sous sont échangés. Le tout devra fonctionner en temps réel, ce qui risque de créer d'intéressants problèmes si trop de joueurs sont actifs au même moment!

lundi 12 mai 2008

Boostons notre C++

Java a sa bibliothèque standard, C# a .NET, et C++ a boost.

Boost, c'est bien, mangez-en! Boost, c'est:

* exhaustif, la plupart des fonctionnalités dont on a toujours besoin y sont
* modulaire, on peut prendre ce qui nous intéresse, et laisser le rèste
* construit sur des concepts robustes, comme l'orienté fonctionnel, le typage fort
* géré par la communauté à travers un process de peer-review, quiconque peut proposer de nouveaux ajouts
* distribué sous une licence extrêmement permissive

Gare cependant à l'indigestion, on peut facilement s'y perdre. Voilà donc la petite liste des librairies de boost que j'utilise tout le temps.

* boost::shared_ptr - Les compteurs de référence sont très utiles, mais gare à les utiliser à bon escient, c'est à dire lorsque std::auto_ptr ou boost::scoped_ptr ne sont plus adaptés.
* boost::bind - Indispensable pour gérer ses closures dans les algorithmes de la bibliothèque standard. Par contre, se fait une spécialité de cracher les erreurs de compile les plus imbitables qui soient.
* boost::noncopyable - Bien pratique pour rendre une classe non copiable. Quand bien même c'est très facile de modifier soit-même sa classe, c'est encore plus simple (et plus lisible pour le suivant) de dériver de boost::noncopyable
* BOOST_FOREACH - Bien qu'étant une macro, c'est une reine de lisibilité. Très pratique lorsque j'ai 5 ou 6 conteneurs imbriqués les uns dans les autres.
* boost::lexical_cast - Transformer les chaines en flotants ou en entier sans avoir à se battre avec les vieilles atoi et atof, c'est bien vu. Encore mieux, étant une fonction template, l'on pourra parfaitement intégrer par exemple lexical_cast à sa classe de vecteurs 3D de types génériques.
* boost::posix_time - Je tremble à chaque fois que je dois gérer des dates dans mon code. posix_time et ses amis boost::gregorian et autres font leur boulot, et je tremble (un peu) moins.
* boost::optional - Un peu comme noncopyable, c'est surtout une bonne manière de commenter le code, en donnant plus de sens qu'un pointeur

Celles-là, je les utilise moins souvent, mais elles n'en sont pas moins utiles au moment où on en a besoin:
* boost::assign - Très pratique pour rapidement remplir des conteneurs, je l'utilise tout le temps dans mes tests unitaires
* boost::unit_test - En parlant de tests unitaires, voilà une belle bibliothèque qui, pour n'en être pas exhaustive, n'en remplit pas moins mes besoins. Bonus pour la quantité minimum de code de mise en place nécessaire grâce à auto_unit_test
* boost::asio - des concepts très intéressants dans cette petite nouvellement arrivée et qui propose un cadre plutôt bien conçu pour gérer ses connections réseaux et évènements asynchrones.

En revanche, je déconseille fortement boost::iostreams::zlib! Utilisez plutôt mon wrapper dans le post précédent :)

samedi 3 mai 2008

Zlib et C++

Depuis longtemps j'avais tenté d'éviter ce moment, mais finalement je m'y suis mis: écrire une couche d'abstraction c++ au dessus de zlib. En suivant pas à pas la documentation, et en remplaçant les concepts C par des concepts C++ (principalement l'utilisation des streams), j'ai fini par obtenir un bout de code qui me semble fonctionner correctement, sans trop avoir à se casser la tête.

Ce qui m'y a poussé, c'est d'abord que j'avais besoin d'un bout de code comme cela pour le boulot. Je me servais de boost::iostream, mais soit je l'utilise mal, soit il reste des bugs dans cette bibliothèque de boost (ce qui n'est impossible, en voyant la mailing-list), mais Purify m'indiquait de nombreuses fuites mémoires dues à la compression. Le code étant trop complexe pour un fix rapide, et ne pouvant de toutes façons pas patcher boost à travers l'ensemble de la boite, je me suis mis à écrire ce wrapper minimaliste.

C'est quand même une certaine déception que ces nouvelles bibliothèques de Boost aient autant de problèmes. Leur wrapper zlib fuit et n'accepte de toutes façons pas les valeurs négatives pour le paramètre "window" permettant de supprimer l'en-tête et le hash final, le rendant incompatible avec le cryptokit ocaml. Et la bibliotèque d'encodage en base 64 a des soucis de fin d'encodage (les '=' qui doivent indiquer comment se finit le flux). Quand on voit le temps qu'il faut pour compiler tout ça...

Comme cadeau bonus, voilà mon code, que vous pouvez bien entendu utiliser comme bon vous semble. Bien entendu, je ne garantis rien, donc si en l'exécutant, vous coupez l'alimentation oxygène de l'IIS, il ne faudra vous en prendre qu'à vous même. Na!


/* This program is free software. It comes without any warranty, to
* the extent permitted by applicable law. You can redistribute it
* and/or modify it under the terms of the Do What The Fuck You Want
* To Public License, Version 2, as published by Sam Hocevar. See
* http://sam.zoy.org/wtfpl/COPYING for more details. */

#include "zlib.h"
#include <string>
#include <istream>
#include <sstream>
#include <stdexcept>
#include <iostream>

namespace
{
const int chunk = 256 * 1024;
}

void compress(std::istream & input, std::ostream & output, int level = 8, int window = 15)
{
unsigned char in[chunk];
unsigned char out[chunk];
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;

if(deflateInit2(&strm,
Z_DEFAULT_COMPRESSION,
Z_DEFLATED,
window,
level,
Z_DEFAULT_STRATEGY) != Z_OK)
{
throw std::runtime_error("deflateInit");
}

int flush;

do
{
input.read(reinterpret_cast<char*>(in), chunk);
strm.avail_in = input.gcount();
flush = (input.eof() || input.fail()) ? Z_FINISH : Z_NO_FLUSH;
strm.next_in = in;
do
{
strm.avail_out = chunk;
strm.next_out = out;
deflate(&strm, flush);
output.write(reinterpret_cast<char*>(out), chunk - strm.avail_out);

}
while (strm.avail_out == 0);
}
while(flush != Z_FINISH);

deflateEnd(&strm);
}

void decompress(std::istream & input, std::ostream & output, int window = 15)
{
unsigned char in[chunk];
unsigned char out[chunk];
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.avail_in = 0;
strm.next_in = Z_NULL;
if(inflateInit2(&strm,
window))
{
throw std::runtime_error("inflateInit");
}

int ret;
do
{
input.read(reinterpret_cast<char*>(in), chunk);
strm.avail_in = input.gcount();
if(strm.avail_in == 0)
{
inflateEnd(&strm);
throw std::runtime_error("unexpected end of stream");
}
strm.next_in = in;
do
{
strm.avail_out = chunk;
strm.next_out = out;
ret = inflate(&strm, Z_NO_FLUSH);
if(ret == Z_STREAM_ERROR ||
ret == Z_NEED_DICT ||
ret == Z_DATA_ERROR ||
ret == Z_MEM_ERROR)
{
inflateEnd(&strm);
throw std::runtime_error("inflate");
}
output.write(reinterpret_cast<char*>(out), chunk - strm.avail_out);

}
while(strm.avail_out == 0);
}
while(ret != Z_STREAM_END);
inflateEnd(&strm);
}


int main()
{
std::istringstream input("aaa");
std::ostringstream output;
compress(input, output);
std::cout << output.str() << std::endl;

std::istringstream input2(output.str());
std::ostringstream output2;
decompress(input2, output2);
std::cout << output2.str() << std::endl;

return 0;
}

dimanche 13 avril 2008

Mount & Blade

Parmi les jeux prenants qui peuvent me durer des heures entières sans décoller le nez du PC, il y a Mount & Blade.

Le principal attrait du jeu est la qualité des combats: à l'épée ou à l'arc, à pied ou sur un canasson, l'on a une maitrise peu commune de son combattant. Une grande partie du combat dépend de la dextérité du joueur, à travers la précision des gestes, et donne vraiment l'impression de participer. Le développement des compétences à la manière d'un jeu de rôle permet de faire plus de dégâts, ou d'améliorer la précision de ses tirs. Les graphismes sont très beaux, et l'immersion excellente, lorsque l'on galope à travers les plaines à travers des batailles qui peuvent regrouper jusqu'à 70 combattants.

La stratégie est importante également, puisqu'il est possible de choisir son terrain de combat (Étant particulièrement performant en combat à la lance couchée, par exemple, je choisis en prédilection de me battre dans les plaines), mais aussi le contrôle assez fin de son armée, pouvant par exemple laisser ses archers en haut d'une plaine pour arroser l'adversaire, pendant que l'on charge accompagné de sa cavalerie pour massacrer la valetaille. L'équilibre des différents combattants est également plaisant, une trentaine de paysans déchaines peuvent venir à bout d'un chevalier qui se serait imprudemment coincé au milieu d'eux.

L'aspect rôle est plus limité, surtout les quêtes proposées par les seigneurs locaux qui deviennent assez rapidement répétitives. L'on peut quand même rejoindre quelques unes des nombreuses nations du jeu, et aller piller les caravanes et bruler les villages des nations ennemies.

Au final, le seul reproche que je pourrais mentionner est que les moments entre les batailles peuvent être un peu longs et répétitifs: retourner à la ville principale pour vendre le butin du dernier pillage et se débarrasser de ses prisonniers, faire le tour des villages pour recruter quelques paysans pour compléter son armée, et faire des tours en attendant de trouver un village bien prospère ou un groupe de brigands passant dans le coin. Mais dès que l'on est de retour sur le champ de bataille, c'est le bonheur absolu!

Le jeu est téléchargeable sur leur site, avec suffisamment de temps en démo pour se faire une bonne idée de la chose. Après deux après-midis à jouer comme un fou, je suis arrivé au bout de la démo, et je me suis précipité pour acheter la licence, pour un prix tout à fait modeste (25 $).

Mangez-en, c'est du bon!

jeudi 10 avril 2008

A bas les enchères!

Je reproduis un post que j'ai écrit sur le forum de jeuxonline.info.


- Le commerce de la main à la main nécessite soit d'avoir un système d'affichage de ses prix pour des marchands qui "campent" (à la Guild Wars), ou de submerger le chat avec ses prix (à la Project Entropia). Dans les deux cas, c'est une zone où l'on ne vient que pour commercer, parce que tout autre occupation devient assommante. Pourquoi donc ne pas fournir des outils qui institutionnalisent ce système, le rendant plus simple à utiliser?
- Toujours dans Project Entropia, on découvre que les joueurs, en plus des annoncent de vente, font aussi des annonces d'achat, ce qui n'est pas possible dans un système d'enchères. Ceci est tout bêtement une criée, ou un "pit" (voir http://en.wikipedia.org/wiki/Open_outcry, en anglais seulement malheureusement). Pourquoi donc ne pas s'inspirer des systèmes d'échanges électroniques qui ont remplacé ces "criées"? Voilà ce que cela donne:

Dans le marché du métal, joueur 1 veut acheter 10 unités à 17€ l'une. C'est son ordre, qu'il rentre dans le système. Tous les joueurs qui regardent le marché du métal dans la zone voient apparaître:




AchatVente
10 @ 17€


Un peu plus tard, d'autres joueurs ajoutent leurs ordres. J2 veut vendre ses unités à 20€, J3 veut acheter ses unités à 18 €.




AchatVente
10 @ 18€30 @ 20€
10 @ 17€


J4 veut absolument vendre ses 15 unités de métal à n'importe quel prix, il ajoute donc un ordre pour 15 unités à 1 €. Le système apparie les prix, créant donc un échange pour 10 unités à 18 € entre J2 et J4, et 5 unités à 17 € entre J1 et J4 (la priorité est aux "meilleurs" prix, c'est à dire les achats les plus hauts, ou les ventes les plus basses). Les marchandises sont échangés, et le système ressemble alors à cela:




AchatVente
5 @ 17€30 @ 20€
Dernier échange à 17 €


Avec ce système, les acheteurs et les vendeurs sont sur un pied d'égalité. Dans des marchés très liquides (avec beaucoup d'échanges), il est possible de commercer très efficacement.

Peut-être est-ce justement parce que la plupart des MMOs ont des systèmes de commerce trop peu liquide qu'ils n'implémentent pas ce système. En tous cas, mon jeu utilisera cela, et on verra bien si c'est utilisable!


L'on me fait très justement remarquer qu'Eve Online utilise un système similaire, ce qui n'est pas étonnant au vu de la richesse de son économie et des nombreux articles écrits à ce sujet. Il va falloir que je ressuscite mon compte à l'essai.

dimanche 23 mars 2008

Collisions

Un petit peu de géométrie en ce week-end de Pâques, afin de mettre au point mes routines de détection de collision. Je me suis beaucoup inspiré d'un très bon article de gamedev.net et les premiers résultats sont encourageants. Voici donc une petite vidéo qui montre ma petite planète tombant sur un cube.


video


Un peu après le démarrage de la vidéo, j'applique un vecteur vitesse vers le bas. La planète tombe donc sur ma surface, et bloque lorsqu'elle la rencontre. Pour l'instant, j'ai codé ma zone de collision comme étant un plan infini, il me reste donc encore à gérer le cas du triangle, qui est nettement plus complexe (mais les maths sont finies, ce n'est donc plus que du code).

Le système décrit dans l'article (et implémenté ici) gère le "glissement" le long de la surface de collision lorsque l'approche est oblique, ce qui est particulièrement adapté aux shooters, mais aussi (miracle!), aux MMOs en vue subjective!

jeudi 13 mars 2008

Et pourtant elle tourne

En vidéo, un petit exemple de ce que l'on peut faire avec un peu d'ocaml, et les librairies lablgl et ocamlsdl. Le code est composé d'un petit graphe de scènes purement fonctionnel (vu du dehors, du moins), qui permet de déterminer les transformations géométriques et de style des objets représentés. Un des nœuds se charge de compiler la géométrie pour obtenir de bonnes performances.


video

mardi 4 mars 2008

Blog en folie

Ouééé, je suis classé 19453ème sur Wikio! Pour la peine, je rajoute en bas à droite le classement, et un petit lien si quelqu'un a l'idée saugrenue de m'ajouter à son feed Wikio.

dimanche 2 mars 2008

Description des zones

Tout en faisant un peu de nettoyage dans le code (principalement centré autour de des binders pour les requêtes SQL), je m'attaque à la description des zones, comme indiqué dans le post précédent. L'intention est d'envoyer une liste des objets utilisés dans la zone, avec le nom du fichier de ressources correspondant, suivi d'une liste des positions, orientations et échelles des objets dans la zone.

Pour l'instant, je vais me contenter de renvoyer la description entière de la zone lorsqu'elle est modifiée. Si sa fonctionnalité d'édition dynamique des zones prend, il me faudra probablement aller vers l'envoi des différences.

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!