{"id":285,"date":"2015-01-21T18:05:08","date_gmt":"2015-01-21T17:05:08","guid":{"rendered":"http:\/\/www.uncovergame.com\/?p=285"},"modified":"2023-02-12T17:52:41","modified_gmt":"2023-02-12T16:52:41","slug":"porting-a-complete-c-game-engine-to-html5-through-emscripten","status":"publish","type":"post","link":"https:\/\/uncovergame.com\/fr\/2015\/01\/21\/porting-a-complete-c-game-engine-to-html5-through-emscripten\/","title":{"rendered":"Port d&rsquo;un moteur de jeu en C vers html5 via Emscripten"},"content":{"rendered":"<p>Bien qu&rsquo;ayant utilis\u00e9 Windows et Visual studio pour porter le code de mon moteur de jeu, la plupart des instructions list\u00e9es ici sont valables sous d&rsquo;autres platformes.<\/p>\n<h3>Logiciels utilis\u00e9s<\/h3>\n<ul>\n<li>Emscripten 1.27.2<\/li>\n<li>emsdk pour Windows<\/li>\n<li>Emscripten Visual studio 2010 plugin<\/li>\n<li>Chrome \/ Firefox pour les tests<\/li>\n<\/ul>\n<p>Si vous n&rsquo;avez jamais install\u00e9 Emscripten avant, allez sur le site officiel et suivez les instructions disponibles ici  : <\/p>\n<p><a href=\"http:\/\/kripken.github.io\/emscripten-site\/docs\/getting_started\/index.html\" target=\"blank_\" rel=\"noopener\">Site de Emscripten (Anglais)<\/a><\/p>\n<h3>Choses \u00e0 v\u00e9rifier avant de tenter un port<\/h3>\n<p>La chose la plus importantes avant de tenter un port de votre moteur est de s&rsquo;assurer qu&rsquo;il est bien multiplateforme. Emscripten utilise clang pour compiler le code C, clang peut \u00eatre consid\u00e9r\u00e9 comme un rempla\u00e7ant de GCC. Si votre base de code est uniquement fonctionnelle sous Windows, vous devez la modifier afin de la rendre multiplateforme.<\/p>\n<p>Ce qui ne fonctionne PAS sous Emscripten:<\/p>\n<ul>\n<li>Les Threads. Si jamais vous chargez vos ressources en arri\u00e8re plan, vous devez modifier cette partie de votre code. Il est possible d&rsquo;utiliser les webworker pour contourner cette limitation, mais cela ne remplace pas un syst\u00e8me de multi-threading classique.<\/li>\n<li>Les syst\u00e8mes de plugin  via dlopen \/ dlsym<\/li>\n<li>Liens dynamiques des librairies, chaque librairie doit \u00eatre compil\u00e9e comme bitcode statique LLVM (.bc) et li\u00e9 au moment de la compilation de votre jeu<\/li>\n<\/ul>\n<p>Il s&rsquo;agit des principales limitations que j&rsquo;ai pu rencontrer en portant mon moteur. Il existe d&rsquo;autres limitations list\u00e9es sur le site de Emscripten :<\/p>\n<p><a href=\"http:\/\/kripken.github.io\/emscripten-site\/docs\/porting\/guidelines\/index.html\">Porting guidelines (Anglais)<\/a><\/p>\n<h3>Librairies utilis\u00e9es<\/h3>\n<p>Voici la liste des librairies que j&rsquo;utilise pour la version PC\/mobile de mon moteur. Quasiment toutes ces librairies fonctionnent directement sous Emscripten, moyennant quelques adaptations de configuration et des changements de code mineur. <\/p>\n<ul>\n<li>glfw 3 (Gestion entr\u00e9e clavier \/ souris. Gestion fen\u00eatre OpenGL)<\/li>\n<li>chipmunk 6.2.1 (Moteur physique)<\/li>\n<li>freetype 2.4.11 (Support police de caract\u00e8re au format ttf)<\/li>\n<li>lua 5.2.3 (Gestion des scripts)<\/li>\n<li>libogg 1.3.2 (D\u00e9codage Ogg)<\/li>\n<li>libpng 1.6.2 (Support image png)<\/li>\n<li>libvorbis 1.3.4 (D\u00e9codage Vorbis)<\/li>\n<li>libsndfile 1.0.25 (Lecture fichiers sonore)<\/li>\n<li>protobuf-c 0.15 (Gestion fichiers binaires sp\u00e9cifique (donn\u00e9es niveaux \/ donn\u00e9es animation etc.))<\/li>\n<li>sqlite 3 (Syst\u00e8me de BDD SQL dans un fichier, utilis\u00e9 pour les donn\u00e9es de sauvegarde et de traduction)<\/li>\n<li>libzip 0.11 (lecture archive zip)<\/li>\n<li>zlib 1.2.8 (D\u00e9codage zip)<\/li>\n<li>portaudio v19 (Lecture donn\u00e9es son)<\/li>\n<\/ul>\n<h3>Port des librairies<\/h3>\n<p>Toutes les librairies sont compil\u00e9s sous forme de bitcode statique LLVM. Ce sont ces fichiers que l&rsquo;on pr\u00e9cise comme librairies en entr\u00e9e quand on compile le code de notre jeu.<\/p>\n<p>\t<u>GLFW<\/u><br \/>\n\tEmscripten inclus d\u00e9j\u00e0 glfw 2 et 3, il n&rsquo;est donc pas n\u00e9cessaire de compiler une librairie annexe. utiliser l&rsquo;option \u00ab\u00a0-s USE_GLFW=3\u00a0\u00bb (ou 2 pour GLFW 2) dans les options de liens de votre jeu.<\/p>\n<p>\t<u>Chipmunk<\/u><br \/>\n      Chipmunk fonctionne presque tout seul, moyennant la correction d&rsquo;un bug :<\/p>\n<p>       Celui ci se trouve dans le fichier  cpHashSetEach.c. Emscripten ne convertit pas bien le pointer de fonction cpHashSetIteratorFunc ce qui produit un crash du jeu. Pour corriger cela, il suffit de cr\u00e9er une fonction sp\u00e9cifique avec un pointer de fonction cpHashSetIteratorFunc sp\u00e9cifique rempla\u00e7ant le type void* avec le bon nom de structure. Il devient alors n\u00e9cessaire de cr\u00e9er un fichier d&rsquo;en-t\u00eate sp\u00e9cifique avec la d\u00e9finition de structure afin que les fichiers   cpHashSetEach.c et  cpBBTree.c puisse y acc\u00e9der.<\/p>\n<p>\tfichiers patch (au format winmerge)<\/p>\n<p>      <a href=\"https:\/\/uncovergame.com\/emscripten_code\/Chipmunk\/cpBBTree.patch\" target=\"blank_\" rel=\"noopener\">cpBBTree.patch<\/a><\/p>\n<p>       <a href=\"https:\/\/uncovergame.com\/emscripten_code\/Chipmunk\/cpHashSet.patch\" target=\"blank_\" rel=\"noopener\">cpHashSet.patch<\/a><\/p>\n<p>\t <a href=\"https:\/\/uncovergame.com\/emscripten_code\/Chipmunk\/emscripten.h\" target=\"blank_\" rel=\"noopener\">emscripten.h<\/a><\/p>\n<p>\tIl est aussi recommand\u00e9 d&rsquo;utiliser -s FORCE_ALIGNED_MEMORY=1 dans les options de liens EMCC. <\/p>\n<p>\t<u>Freetype<\/u><br \/>\n       Fonctionne, mais certaines police de caract\u00e8res font planter le moteur (mes tests avec une police de 20px font planter la librairie, alors qu&rsquo;une police de 8px fonctionne parfaitement.)<\/p>\n<p>\t<u>lua<\/u><br \/>\n      Fonctionne si  LUA_COMPAT_ALL est pr\u00e9cis\u00e9 dans les d\u00e9finitions de pr\u00e9processeur quand on compile lua.  l&rsquo;options de lien EMCC \u00ab\u00a0-s FORCE_ALIGNED_MEMORY=1\u00a0\u00bb est ici requise.<\/p>\n<p>\t<u>libogg<\/u><br \/>\n\tFonctionne sans modifications<\/p>\n<p>\t<u>libvorbis<\/u><br \/>\n\tFonctionne sans modifications<\/p>\n<p>\t<u>libsndfile<\/u><br \/>\n         Le code de la librairie doit \u00eatre modifi\u00e9 pour enlever toutes r\u00e9f\u00e9rences \u00e0 FLAC (je n&rsquo;ai inclus que le support oggvorbis) Il suffit d&rsquo;exclure flac.c de votre projet et de commenter toutes les r\u00e9f\u00e9rences \u00e0 FLAC.<\/p>\n<p>       Il est \u00e9galement n\u00e9cessaire d&rsquo;utiliser un fichier de config sp\u00e9cifique, certains type sous Emscripten ont une taille diff\u00e9rente de ce que libsndfile utilise g\u00e9n\u00e9ralement sur PC, particuli\u00e8rement le type off_t, voici les fichiers modifi\u00e9s:<\/p>\n<p>        <a href=\"https:\/\/uncovergame.com\/emscripten_code\/libsndfile\/config.h\" target=\"blank_\" rel=\"noopener\">config.h<\/a><\/p>\n<p>\t<a href=\"https:\/\/uncovergame.com\/emscripten_code\/libsndfile\/sndfile.h\" target=\"blank_\" rel=\"noopener\">sndfile.h<\/a><\/p>\n<p>         Enfin, ouvrez le fichier sndfile.c et commentez la ligne suivant dans la fonction sf_open (approximativement ligne 312) :<\/p>\n<p>       <code>assert (sizeof (sf_count_t) == 8) ;<\/code><\/p>\n<p>       Dans notre cas, sf_count_t a une taille de 4 et non de 8, principalement \u00e0 cause du type off_t qui a une taille diff\u00e9rente sous emscripten.<\/p>\n<p>\t<u>protobuf-c<\/u><br \/>\n\tFonctionne sans modifications<\/p>\n<p>\t<u>sqlite<\/u><br \/>\n\tFonctionne sans modifications<\/p>\n<p>\t<u>libzip<\/u><br \/>\n\tCompile sans modifications (non test\u00e9 dans mon cas, j&rsquo;utilise le syst\u00e8me de fichiers virtuel de Emscripten plut\u00f4t qu&rsquo;une archive zip)<\/p>\n<p>\t<u>zlib<\/u><br \/>\n\tFonctionne sans modifications<\/p>\n<p>\t<u>portaudio<\/u><br \/>\n        La seule librairie que j&rsquo;ai remplac\u00e9 pour \u00eatre compatible Emscripten. \u00e0 la place, j&rsquo;utilise la librairies SDL_audio disponible avec Emscripten. Celle-ci utilise webaudio qui est le meilleur moyen d&rsquo;avoir du son sur un jeu HTML5 \u00e0 l&rsquo;heure actuelle. Si vous utilisez d\u00e9j\u00e0 l&rsquo;interface asynchrone de portaudio, le portage est relativement ais\u00e9. Je me suis content\u00e9 de reprendre mon code existant et de l&rsquo;adapter \u00e0 deux \/ trois endroits pour \u00eatre compatible avec l&rsquo;interface de SDL_audio.<\/p>\n<p>       Fonction de rappel de portaudio<\/p>\n<pre class=\"lang:c decode:true\" title=\"fonction de rappel\">static int Callback(const void *input,\r\n             void *output,\r\n             unsigned long frameCount,\r\n             const PaStreamCallbackTimeInfo* paTimeInfo,\r\n             PaStreamCallbackFlags statusFlags,\r\n             void *userData)<\/pre>\n<p>        Fonction de rappel de SDL_audio<br \/>\n\t<code>static void sdl_audio_callback(void* userData,Uint8* _stream,int _length)<\/code><\/p>\n<p>      La variable frameCount avec SDL_audio peut \u00eatre calcul\u00e9 de la mani\u00e8re suivante :<br \/>\n\t<code>(sf_count_t)((_length \/ sizeof(Sint16)) \/ num_channels);<\/code><\/p>\n<p>      (dans ce cas, je r\u00e9cup\u00e8re les donn\u00e9es audio sous forme d&rsquo;entier court (SInt16). utiliser le type de donn\u00e9es correspondant \u00e0 votre code)<\/p>\n<p>        La variable ouput devient _stream avec SDL_audio.<\/p>\n<p>      Pour les autres variables tels que input \/ paTimeInfo et statusFlags, vous pouvez utiliser le pointer userData pour les passer \u00e0 votre fonction.<\/p>\n<p>       Derni\u00e8re chose \u00e0 prendre en compte, la gestion multi-thread. Sur mon code original, j&rsquo;utilise des variables modifi\u00e9 de mani\u00e8re atomique pour g\u00e9rer l&rsquo;acc\u00e8s aux fonctions sonores. Avec la version Emscripten, j&rsquo;ai remplac\u00e9 l&rsquo;ensemble de ce code avec l&rsquo;appel \u00e0 SDL_LockAudio() \/ SDL_UnlockAudio(). La taille du tampon des donn\u00e9es sonore doit \u00e9galement \u00eatre modifi\u00e9e. Dans mon cas, je suis pass\u00e9 d&rsquo;une taille de 4096 octets \u00e0 1024 octets (soit 512 octets par canal audio), ce qui reste un compromis acceptable entre qualit\u00e9 et latence  audio. Je n&rsquo;ai de glitch sonores que sous Chrome, mais rien de probl\u00e9matique.<\/p>\n<h3>Port du moteur<\/h3>\n<p>Tant que votre moteur compile sous GCC, et n&rsquo;utilise pas de muti-threading, le port devrait se faire sans trop de probl\u00e8me. La partie la plus importante est probablement le code de rendu si il n&rsquo;est pas d\u00e9j\u00e0 compatible OpenGL ES 2.0 (WebGL utilise les m\u00eame fonctions que OpenGL ES) . Le plus gros du travail se situe sur la suppression de toutes les fonctions fixes OpenGL (glcolor4f \/ glPushMatrix \/ etc.) et leurs remplacement par des fonctions compatibles avec OpenGL core 3.0 afin de faciliter la compatibilit\u00e9 OpenGL ES \/ WebGL. Il est par exemple impossible d&rsquo;utiliser les tableaux de vertices directement, vous devez utiliser les VBO \u00e0 la place. Pour plus d&rsquo;info sur OpenGL 3.0, voir les tutos sur le site d\u00e9veloppez.com : <a href=\"http:\/\/jeux.developpez.com\/tutoriels\/OpenGL-moderne\/tutoriel-1-ouvrir-une-fenetre\/\" target=\"_blank\" rel=\"noopener\">tutos OpenGL<\/a> <\/p>\n<p>Si jamais vous n&rsquo;utilisez pas d\u00e9j\u00e0 de shaders, il vous faudra les cr\u00e9er pour \u00eatre compatible WebGL, la section suivante aborde la question du port de shaders.<\/p>\n<h3>A propos des shaders et de WebGL<\/h3>\n<p>WebGL ne supporte que les shaders   #version 100, ce qui peut donc n\u00e9cessiter de les r\u00e9\u00e9crire.<\/p>\n<p>Par exemple ce code:<\/p>\n<pre class=\"lang:c decode:true\" title=\"GLSL\">#version 150\r\n\r\nin vec2 inPos;\r\nin vec2 inTex;\r\n\r\nout vec4 color;\r\nout vec2 texcoord;<\/pre>\n<p>en version 150 devient ce code<\/p>\n<pre class=\"lang:c decode:true\" title=\"GLSL\">#version 100\r\n\r\nattribute vec2 inPos;\r\nattribute vec2 inTex;\r\nvarying mediump vec4 color;\r\nvarying mediump vec2 texcoord;<\/pre>\n<p>en version 100.<\/p>\n<p>Il est possible d&rsquo;utiliser le pr\u00e9processeur pour garder votre code dans un seul fichier de shaders (avec #ifdef GL_ES pour le code sp\u00e9cifique  GLES\/WebGL)<br \/>\nDans mon cas, j&rsquo;ai pr\u00e9f\u00e9rer utiliser des fichiers s\u00e9par\u00e9s pour bien faire la diff\u00e9rence entre un shader WebGL et un shader PC.<\/p>\n<h3>Boucle de jeu sp\u00e9cifique \u00e0 Emscripten<\/h3>\n<p>Plut\u00f4t que d&rsquo;utiliser une boucle while(), avec Emscripten vous devez utiliser la ligne ci-dessous pour d\u00e9finir votre boucle de jeu principale :<\/p>\n<p><code>emscripten_set_main_loop(renderoneframe,0,1);<\/code><\/p>\n<p>o\u00f9 renderoneframe  est votre fonction o\u00f9 se trouve le code de jeu pour une frame. Pour plus de d\u00e9tails, voir la documentation de Emscripten :<\/p>\n<p><a href=\"http:\/\/kripken.github.io\/emscripten-site\/docs\/api_reference\/emscripten.h.html#id3\">emscripten_set_main_loop (Anglais)<\/a><\/p>\n<h3>Compilation du code du jeu utilisant notre moteur<\/h3>\n<p>Une autre chose que j&rsquo;ai eu \u00e0 changer est la mani\u00e8re dont je charge ma DLL de jeu. Je dispose d&rsquo;un syst\u00e8me de module utilisant dlopen\/dlsym pour acc\u00e9der \u00e0 des fonctions sp\u00e9cifique \u00e0 mon jeu. Comme ce syst\u00e8me n&rsquo;est pas support\u00e9 par Emscripten, une solution est de compiler une librairie LLVM statique de ma DLL de jeu, r\u00e9f\u00e9rencer le header de cette DLL dans mon code d&rsquo;application et remplacer mon code de r\u00e9solution de fonctions comme suit :<\/p>\n<p>Version normale<br \/>\n<code>mngr->game_init_objects = (game_init_objects_t)dlsym(mngr->game_module,\"game_init_objects\");<\/code><\/p>\n<p>Version Emscripten<br \/>\n<code>mngr->game_init_objects = (game_init_objects_t)game_init_objects;<\/code><\/p>\n<p>Cela ne s&rsquo;applique bien sur que au cas o\u00f9 on ne charge qu&rsquo;une seule DLL externe. Pour un syt\u00e8me de plugin plus complexe, il est n\u00e9cessaire de r\u00e9\u00e9crire le code existant.<\/p>\n<p>Commande finale utilis\u00e9e par l&rsquo;\u00e9diteur de liens :<\/p>\n<p><code>-s USE_GLFW=3 -s TOTAL_MEMORY=67108864 -s NO_EXIT_RUNTIME=1 -s FORCE_ALIGNED_MEMORY=1<\/code><\/p>\n<h3>Acc\u00e9der au fichiers de ressources du jeu<\/h3>\n<p>Emscripten dispose d&rsquo;un syst\u00e8me de fichiers virtuels qui remplace le syst\u00e8me de fichiers classique de Windows. Celui-ci fonctionne de la m\u00eame mani\u00e8re pour la gestion des fichiers via fopen \/ fread. Il est n\u00e9cessaire de cr\u00e9er une archive des fichiers du jeu pour que notre jeu HTML5 puisse y acc\u00e9der : <\/p>\n<p><code>python <chemin vers votre r\u00e9pertoire  Emscripten>\\emscripten\\1.25.0\\tools\\file_packager.py emscripten_data.data --preload assets scripts locale.db save.db config.lua > emscripten_data.js<\/code><\/p>\n<p>cela g\u00e9n\u00e8re deux fichiers emscripten_data.data et emscripten_data.js. Le premier est l&rsquo;archive contenant toutes nos ressources et le second permet d&rsquo;indexer nos fichiers afin que notre code de jeu les retrouvent. Dans cet exemple, les r\u00e9pertoires \u00ab\u00a0assets\u00a0\u00bb  et \u00ab\u00a0scripts\u00a0\u00bb ainsi que les fichiers \u00ab\u00a0locale.db\u00a0\u00bb \u00ab\u00a0save.db\u00a0\u00bb et \u00ab\u00a0config.lua\u00a0\u00bb sont archiv\u00e9s dans le fichier emscripten_data.data.<\/p>\n<p>Cette commande doit \u00eatre ex\u00e9cut\u00e9e depuis le r\u00e9pertoire courant de l&rsquo;application. Dans cet exemple, le fichier locale.db est accessible via cette ligne de code :<br \/>\n<code>fopen(\"locale.db\",\"rb\")<\/code><br \/>\nSi vous souhaitez ouvrir un fichier dans le r\u00e9pertoire assets, utilisez :<br \/>\n<code>fopen(\"assets\/file.png\",\"rb\").<\/code><\/p>\n<p>Enfin, n&rsquo;oubliez pas d&rsquo;inclure le fichier .js dans le fichier .html g\u00e9n\u00e9r\u00e9 par Emscripten :<\/p>\n<p><code><script async type=\"text\/javascript\" src=\"emscripten_data.js\"><\/script><\/code><\/p>\n<p>afin que votre code de jeu puisse charger vos ressources.<\/p>","protected":false},"excerpt":{"rendered":"<p>Bien qu&rsquo;ayant utilis\u00e9 Windows et Visual studio pour porter le code de mon moteur de jeu, la plupart des instructions list\u00e9es ici sont valables sous d&rsquo;autres platformes. Logiciels utilis\u00e9s Emscripten 1.27.2 emsdk pour Windows Emscripten Visual studio 2010 plugin Chrome &hellip; <a href=\"https:\/\/uncovergame.com\/fr\/2015\/01\/21\/porting-a-complete-c-game-engine-to-html5-through-emscripten\/\">Continuer la lecture <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[6,7,8],"tags":[],"_links":{"self":[{"href":"https:\/\/uncovergame.com\/fr\/wp-json\/wp\/v2\/posts\/285"}],"collection":[{"href":"https:\/\/uncovergame.com\/fr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/uncovergame.com\/fr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/uncovergame.com\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/uncovergame.com\/fr\/wp-json\/wp\/v2\/comments?post=285"}],"version-history":[{"count":46,"href":"https:\/\/uncovergame.com\/fr\/wp-json\/wp\/v2\/posts\/285\/revisions"}],"predecessor-version":[{"id":517,"href":"https:\/\/uncovergame.com\/fr\/wp-json\/wp\/v2\/posts\/285\/revisions\/517"}],"wp:attachment":[{"href":"https:\/\/uncovergame.com\/fr\/wp-json\/wp\/v2\/media?parent=285"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/uncovergame.com\/fr\/wp-json\/wp\/v2\/categories?post=285"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/uncovergame.com\/fr\/wp-json\/wp\/v2\/tags?post=285"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}