JeuWeb - Crée ton jeu par navigateur
Comment construire une application avec Erlang - Version imprimable

+- JeuWeb - Crée ton jeu par navigateur (https://jeuweb.org)
+-- Forum : Discussions, Aide, Ressources... (https://jeuweb.org/forumdisplay.php?fid=38)
+--- Forum : Programmation, infrastructure (https://jeuweb.org/forumdisplay.php?fid=51)
+--- Sujet : Comment construire une application avec Erlang (/showthread.php?tid=6100)



Comment construire une application avec Erlang - Sephi-Chan - 26-04-2012

Ce sujet est issu de la séparation de la discussion Mon apprentissage de Erlang.

Merci pour la réponse Niahoo. Smile

(26-04-2012, 03:42 PM)niahoo a écrit :
(26-04-2012, 02:05 PM)Sephi-Chan a écrit : Comme on peut le voir, quand j'envoie un message contenant l'atom endive, les messages introduce suivant ne sont pas traités. Si l'on n'avait pas créé de règle pour les messages d'une autre forme, le message endive n'aurait pas été traité, mais la réception des messages suivants n'aurait pas été interrompue pour autant (je ne suis pas encore capable d'en expliquer la raison).

Si tu n'avais pas utilisé un catch-all (je te suggère d'ailleurs d'utiliser _ au lieu de Any), le process serait resté bloqué sur receive, en attente d'autres messages, et aurait pu les traiter au fur et à mesure qu'ils arrivent, les messages non reconnus de type endive seraient simplement ignorés jusqu'au prochain appel de receive.

J'ai utilisé Any après avoir lu The Eight Myths of Erlang Performance, je trouvais ça un peu plus parlant (et ça m'évitait d'expliquer _). J'ai également appris qu'en nommant la variable _Any, le compilateur n'avertit plus la non-utilisation de la variable.

Effectivement, je suis allé lire la documentation de receive et on peut y lire que receive ne foire jamais et que l'exécution est suspendue jusqu'à ce qu'un message corresponde (et qu'il passe le guard).


Merci pour l'exemple en tout cas. D'ailleurs, j'ai quelques questions pour toi sur la façon de faire un système de jeu.
  • Que fait-on généralement pour communiquer avec un serveur Web (qui sert juste de point d'accès) si on développe un serveur de jeu (qui s'occupe de l'état du jeu et des joueurs) ?
  • Comment penses-tu le code métier (systèmes de quêtes, occupation des territoires, etc.) ?
  • Comment utilises-tu (ou non) les processus. Imagine Risk en Erlang, est-ce que c'est raisonnable de créer un processus par joueur, un processus par partie, un processus par territoire, etc.



RE: Mon apprentissage de Erlang - niahoo - 26-04-2012

Citation :Que fait-on généralement pour communiquer avec un serveur Web (qui sert juste de point d'accès) si on développe un serveur de jeu (qui s'occupe de l'état du jeu et des joueurs) ?

On utilise un serveur web en erlang (yaws, mochiweb, cowboy, …) ou un framework web qui intègre ce serveur, on démarre ça et le serveur de jeu sur le même node comme ça il n'y a plus qu'a appeller le serveur de jeu dans tes controlleurs.

Pour l'instant je bosse que sur mon serveur de jeu, et lentement, donc j'ai pas testé, mais disons que tu ferais dans tes controlleurs la même chose que tu fais dans ton shell pour tester ton serveur.

Pour communiquer avec ton serveur de jeu depuis un controlleur Rails, tu peux faire du TCP/IP ou bien du JSON/HTTP si t'es un flemmard. Ou bien tu peux simuler un node erlang avec Ruby je crois qu'il y a des bibliothèques pour ça.

Citation :Comment penses-tu le code métier (systèmes de quêtes, occupation des territoires, etc.) ?

Heu … ben je sais pas trop, mais tout ça c'est de la base de données. Quand tu parles à un PNJ, tu check en base les quêtes qu'il propose et celles que tu n'as pas déjà fait, et voilà.
Quand tu prends une quête tu l'indiques dans la base de données, etc… sur ce point y a pas trop de changement, si ce n'est que la base de données peut servir simplement de backup alors que tu gardes ces infos en RAM pour les persos connectés.


Citation :Comment utilises-tu (ou non) les processus. Imagine Risk en Erlang, est-ce que c'est raisonnable de créer un processus par joueur, un processus par partie, un processus par territoire, etc.

Bon ben déjà j'utilise le framework OTP, j'intègre tous mes processus dans un arbre de supervision, ce qui permet de récupérer mes morceaux en cas de crash.

Je t'avais dit que je fesais toujours du TDD mais j'ai un peu menti, avec erlang j'ai eu la flemme de ce côté là jusqu'à maitenant, je m'y remet peu à peu sur du code de test (sans OTP)
Enfin, il y a des frameworks pour les tests unitaires …

Un processus par joueur pour garder son State depuis le serveur ça me paraît pas mal. Ça te permet de créer un objet (au sens primaire) pour encapsuler ce State et gérer les interactions du joueur avec l'extérieur.
Le plus chiant étant d'éviter les deadlock quand tu vas demander à ton process de discuter avec d'autres process.

Par partie également, puisque tu vas garder dans ta partie la liste des joueurs, des infos les concernant comme leur score, la liste des territoires, par qui ils sont occupés et par quoi. (Et donc pas de process par territoire).

Ce n'est pas de la programamtion orientée objet, au début on a tendance à penser objet et à faire des process pour tout et pour rien. Mais les process sont faits pour modéliser ce qui est concurrent.

Comme tu as plusieurs joueurs connectés, plusieurs parties en même temps, c'est logique d'utiliser un process, mais pour les territoires, c'est une simple table, donc autant utiliser une liste de tuples

en anglais on dit territory ?

Code :
Territoires = [Territoire]
Territoire = {territory, AreaID, Owner, Troups}
    AreaID = int()
    Owner = PlayerID
    Troups = [Troup]
        PlayerID = int()
        Troup = {troup, TroupTypeID, Amount}
            TroupTypeID = int()
            Amount = int()

Je te conseilles de regarder les records (et si tu surmontes la syntaxe tu es bon pour erlang), car taper des tuples c'est un peu chiant.

Tu ne stockes pas le PID du player à la places de son id car si son process crash, quand le superviseur le relancera il aura changé de pid. Il te faut donc un système qui relie les id aux pid et qui soit tenu à jour
edit concernant le serveur, j'ai quand même déjà fait une appli erlang qui est standalone et je l'ai couplée à une interface web sur un framework Erlang et ça fonctionne bien. En fait c'est transparent.


RE: Mon apprentissage de Erlang - Sephi-Chan - 26-04-2012

(26-04-2012, 05:04 PM)niahoo a écrit : Quand tu prends une quête tu l'indiques dans la base de données, etc… sur ce point y a pas trop de changement, si ce n'est que la base de données peut servir simplement de backup alors que tu gardes ces infos en RAM pour les persos connectés.

Tu utilises quoi comme base de données ? Mnesia ? Tu as des articles sur la question ? Tu écris les données en base au fur et à mesure ou bien une fois de temps en temps (en récupération d'erreur, notamment) ?


(26-04-2012, 05:04 PM)niahoo a écrit : Bon ben déjà j'utilise le framework OTP, j'intègre tous mes processus dans un arbre de supervision, ce qui permet de récupérer mes morceaux en cas de crash.

Je t'avoue que je n'ai pas encore abordé OTP et les outils qu'il fournit. Smile


(26-04-2012, 05:04 PM)niahoo a écrit : Un processus par joueur pour garder son State depuis le serveur ça me paraît pas mal. Ça te permet de créer un objet (au sens primaire) pour encapsuler ce State et gérer les interactions du joueur avec l'extérieur.
Le plus chiant étant d'éviter les deadlock quand tu vas demander à ton process de discuter avec d'autres process.

Quels deadlocks ? Erlang n'est pas supposé éviter ça ?


(26-04-2012, 05:04 PM)niahoo a écrit : Je te conseilles de regarder les records (et si tu surmontes la syntaxe tu es bon pour erlang), car taper des tuples c'est un peu chiant.

Ouais j'ai vu ça. Mais du coup je me demande quand utiliser de simples records, des record Mnesia, ou des dictionnaires, voir même d'autres trucs qui existent.


(26-04-2012, 05:04 PM)niahoo a écrit : Tu ne stockes pas le PID du player à la places de son id car si son process crash, quand le superviseur le relancera il aura changé de pid. Il te faut donc un système qui relie les id aux pid et qui soit tenu à jour

Qu'est-ce que tu suggères pour s'occuper de ça ?


RE: Mon apprentissage de Erlang - niahoo - 26-04-2012

(26-04-2012, 07:22 PM)Sephi-Chan a écrit : Tu utilises quoi comme base de données ? Mnesia ? Tu as des articles sur la question ? Tu écris les données en base au fur et à mesure ou bien une fois de temps en temps (en récupération d'erreur, notamment) ?

J'utilise postgresql avec un petit ORM nommé boss_db, mais il n'est pas très complet.
J'utilise également bitcask pour stocker sur un node n'importe quel terme erlang. quand tu as un process qui crève, lorsqu'il sera redémarré il récupère où il en était comme si de rien n'était. J'ai pas trop expérimenté là dessus ceci-dit, je me suis juste créé le code pour pouvoir le faire mais ça n'a pas été éprouvé.


(26-04-2012, 07:22 PM)Sephi-Chan a écrit : Je t'avoue que je n'ai pas encore abordé OTP et les outils qu'il fournit. Smile
C'est un peu plus dur que le langage de base car il faut comprendre la logique. Mais une fois pigé, ça simplifie pas mal de choses. ça demande par contre pas mal de code "boilerplate", un peu chiant. Mais c'est du solide.


(26-04-2012, 07:22 PM)Sephi-Chan a écrit : Quels deadlocks ? Erlang n'est pas supposé éviter ça ?
Et bien, imagine deux process A et B. Le process A envoie un message à B et se met en attente de réponsede la part de B. pendant la construction de sa réponse, B a besoin d'informations de A, il lui envoie donc un message et se met en attente de réponse.
B ne recevra jamais de réponse de la part de A, car A est bloqué dans un receive qui ne terminera jamais puisque lui aussi attend une réponse de B avant de pouvoir faire autre chose.
Il est donc possible d'envoyer les messages sans attendre la réponse, et la traiter quand elle reviendra, mais le design que j'ai évoqué est donc à éviter. je n'ai pas eu pour le moment besoin de faire des allers-retours de ce type, mais ça pourrait bien arriver. Il y a pas mal de mécanismes pour les mettre en place mais il faut simplement y penser.
Or, comme montré dans l'autre topic, le côté asynchrone est masqué, on oublie rapidement qu'un petit appel de fonction va en fait passer des messages dans plusieurs processus.
Erlang n'évite pas les deadlocks mais permet facilement de les éviter.



(26-04-2012, 07:22 PM)Sephi-Chan a écrit : Ouais j'ai vu ça. Mais du coup je me demande quand utiliser de simples records, des record Mnesia, ou des dictionnaires, voir même d'autres trucs qui existent.

Mnesia utilise les records classiques en fait. Les records c'est bien quand tu as des données membres à récuperer rapidement, comme dans l'état d'un process. un joueur gardera son ID, son nom, son email, le record de son perso, etc..
Un dict ça sera bien quand tu a besoin de stocker des clés indéfiniment. Comme en ruby en fait : tu fais des objets avec leurs variables membres et si tu veux passer une liste d'options, un hash convient.
(J'utilise pas vraiment les dict mais plutot les proplists : [{name, "niahoo"},{age,99.4},{country,"France"}])


(26-04-2012, 07:22 PM)Sephi-Chan a écrit : Qu'est-ce que tu suggères pour s'occuper de ça ?

je me suis fait un petit bout de code qui utilise 'global', un storage de processus.
global te permet d'associer un Pid à une clé, et quand le process creve, la clé est supprimée.

Code :
-module(pidstore).
-export([get_pid/2,get_pid/1,register/2]).

%% sert à démarrer des process s'ils ne sont pas enregistrés.
%% on se sert du module 'global' comme process registry
%% mais le présent module sert justement à faire abstraction

get_pid(Key) -> global:whereis_name(Key).

get_pid(Key, MFA) ->
    case get_pid(Key)
        of undefined -> newpid(Key, MFA)
         ; Pid -> {ok, Pid}
    end.



newpid(Key, {M,F,A}=_MFA) when is_list(A) ->
    Ret = erlang:apply(M,F,A),
    case Ret
        of {ok, Pid} ->
            ok = ?MODULE:register(Key, Pid)
         ; Any -> 'youbraaad!'
    end,
    Ret.


register(Key, Pid) when is_pid(Pid) ->
    io:format("registering ~p as ~p on pidstore ~n", [Key,Pid]),
    yes = global:register_name(Key, Pid),
    ok.

On lui fournit un clé et un MFA (module, function, args), si 'global' ne renvoie pas de Pid (==undefined) alors j'applique apply(M,F,A) qui doit normalement me renvoyer {ok, Pid}, je réengegistre le Pid avec la même clé et c'est reparti.

Comme tu peux le lire ce n'est pas fini mais ça fonctionne comme ça.

Mais là, si deux process demandent la même clé en même temps et qu'elle n'est pas enregistrée, deux process vont être créés avec la même clé, le premier sera écrasé par le suivant mais (je crois) que global s'en fout, il ne va pas le 'kill'.

Ma prochaine étape est donc de mettre tout ceci au sein d'un process pour qu'il réponde aux demandes à la queue.





j'ai pas demandé l'avis d'experts, s'il le faut, c'est pourri. Il existe nprocreg et GProc mais ils sont destinés à remplacer les dictionnairs des processus, or je veux du code qui fasse uniquement associer un Pid à une clé, et c'est tout ce que global fait. Mais je ne sais pas si utiliser global est déconseillé.


RE: Comment construire une application avec Erlang - niahoo - 26-04-2012

Citation : Tu écris les données en base au fur et à mesure ou bien une fois de temps en temps (en récupération d'erreur, notamment) ?

J'oubliais ce point. Ce que je décidé pour le moment, c'est d'utiliser un timeout. quand un process reçoit un timeout, il fait ses sauvegardes en base de données puis il se termine. il sera redémarré si besoin par mon pidstore.

Mais ça pose un problème : s'il est contacté pendant la sauvegarde, il ne sera pas encore terminé, global va donc renvoyer son pid en échange de la clé, mais mon process ne répondra pas au message puisqu'il sera en train de terminer, donc le message sera perdu. je pourrais spawn un process qui s'occupera de la sauvegarde, ce qui minimiserait le risque de race condition car le process terminera beaucoup plus rapidement, ne s'occupant plus de la sauvegarde, mais la race condition sera toujours possible. de plus, s'il la clé est demandée et qu'on crée un nouveau process avant que la sauvegarde soit terminée, le process pourrait charger des données en base pas encore modifiées. (sauf si l'ORM met tous les select / insert / update à la queue mais j'en doute, ça ferait un gros bottleneck. faut voir)

Donc ce problème n'est pas résolu. en attendant je peux le minimiser en faisant des sauvegardes non seulement lors des timeout mais aussi après des opérations importantes.


RE: Comment construire une application avec Erlang - niahoo - 27-04-2012

J'ai posé la question sur stack overflow, personne répond pour le moment, je mets le lien ici si ça peut intéresser du monde http://stackoverflow.com/questions/10350947/handle-saving-of-transient-gen-servers-states-when-using-a-key-to-pid-mechanism


D'ailleurs si des gens ici pouvant perler anglais correctement voulaient bien le lire ça serait sympa de me donner les erreurs.


RE: Comment construire une application avec Erlang - srm - 27-04-2012

Récris ta question et trouve une solution pour l'expliquer en 5 lignes, ça doit être faisable, car là déjà 80% des gens vont pas t'aider car la question est trop longue.