Port d’un moteur de jeu en C vers html5 via Emscripten

Bien qu’ayant utilisé Windows et Visual studio pour porter le code de mon moteur de jeu, la plupart des instructions listées ici sont valables sous d’autres platformes.

Logiciels utilisés

  • Emscripten 1.27.2
  • emsdk pour Windows
  • Emscripten Visual studio 2010 plugin
  • Chrome / Firefox pour les tests

Si vous n’avez jamais installé Emscripten avant, allez sur le site officiel et suivez les instructions disponibles ici :

Site de Emscripten (Anglais)

Choses à vérifier avant de tenter un port

La chose la plus importantes avant de tenter un port de votre moteur est de s’assurer qu’il est bien multiplateforme. Emscripten utilise clang pour compiler le code C, clang peut être considéré comme un remplaçant de GCC. Si votre base de code est uniquement fonctionnelle sous Windows, vous devez la modifier afin de la rendre multiplateforme.

Ce qui ne fonctionne PAS sous Emscripten:

  • Les Threads. Si jamais vous chargez vos ressources en arrière plan, vous devez modifier cette partie de votre code. Il est possible d’utiliser les webworker pour contourner cette limitation, mais cela ne remplace pas un système de multi-threading classique.
  • Les systèmes de plugin via dlopen / dlsym
  • Liens dynamiques des librairies, chaque librairie doit être compilée comme bitcode statique LLVM (.bc) et lié au moment de la compilation de votre jeu

Il s’agit des principales limitations que j’ai pu rencontrer en portant mon moteur. Il existe d’autres limitations listées sur le site de Emscripten :

Porting guidelines (Anglais)

Librairies utilisées

Voici la liste des librairies que j’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.

  • glfw 3 (Gestion entrée clavier / souris. Gestion fenêtre OpenGL)
  • chipmunk 6.2.1 (Moteur physique)
  • freetype 2.4.11 (Support police de caractère au format ttf)
  • lua 5.2.3 (Gestion des scripts)
  • libogg 1.3.2 (Décodage Ogg)
  • libpng 1.6.2 (Support image png)
  • libvorbis 1.3.4 (Décodage Vorbis)
  • libsndfile 1.0.25 (Lecture fichiers sonore)
  • protobuf-c 0.15 (Gestion fichiers binaires spécifique (données niveaux / données animation etc.))
  • sqlite 3 (Système de BDD SQL dans un fichier, utilisé pour les données de sauvegarde et de traduction)
  • libzip 0.11 (lecture archive zip)
  • zlib 1.2.8 (Décodage zip)
  • portaudio v19 (Lecture données son)

Port des librairies

Toutes les librairies sont compilés sous forme de bitcode statique LLVM. Ce sont ces fichiers que l’on précise comme librairies en entrée quand on compile le code de notre jeu.

GLFW
Emscripten inclus déjà glfw 2 et 3, il n’est donc pas nécessaire de compiler une librairie annexe. utiliser l’option « -s USE_GLFW=3 » (ou 2 pour GLFW 2) dans les options de liens de votre jeu.

Chipmunk
Chipmunk fonctionne presque tout seul, moyennant la correction d’un bug :

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éer une fonction spécifique avec un pointer de fonction cpHashSetIteratorFunc spécifique remplaçant le type void* avec le bon nom de structure. Il devient alors nécessaire de créer un fichier d’en-tête spécifique avec la définition de structure afin que les fichiers cpHashSetEach.c et cpBBTree.c puisse y accéder.

fichiers patch (au format winmerge)

cpBBTree.patch

cpHashSet.patch

emscripten.h

Il est aussi recommandé d’utiliser -s FORCE_ALIGNED_MEMORY=1 dans les options de liens EMCC.

Freetype
Fonctionne, mais certaines police de caractères font planter le moteur (mes tests avec une police de 20px font planter la librairie, alors qu’une police de 8px fonctionne parfaitement.)

lua
Fonctionne si LUA_COMPAT_ALL est précisé dans les définitions de préprocesseur quand on compile lua. l’options de lien EMCC « -s FORCE_ALIGNED_MEMORY=1 » est ici requise.

libogg
Fonctionne sans modifications

libvorbis
Fonctionne sans modifications

libsndfile
Le code de la librairie doit être modifié pour enlever toutes références à FLAC (je n’ai inclus que le support oggvorbis) Il suffit d’exclure flac.c de votre projet et de commenter toutes les références à FLAC.

Il est également nécessaire d’utiliser un fichier de config spécifique, certains type sous Emscripten ont une taille différente de ce que libsndfile utilise généralement sur PC, particulièrement le type off_t, voici les fichiers modifiés:

config.h

sndfile.h

Enfin, ouvrez le fichier sndfile.c et commentez la ligne suivant dans la fonction sf_open (approximativement ligne 312) :

assert (sizeof (sf_count_t) == 8) ;

Dans notre cas, sf_count_t a une taille de 4 et non de 8, principalement à cause du type off_t qui a une taille différente sous emscripten.

protobuf-c
Fonctionne sans modifications

sqlite
Fonctionne sans modifications

libzip
Compile sans modifications (non testé dans mon cas, j’utilise le système de fichiers virtuel de Emscripten plutôt qu’une archive zip)

zlib
Fonctionne sans modifications

portaudio
La seule librairie que j’ai remplacé pour être compatible Emscripten. à la place, j’utilise la librairies SDL_audio disponible avec Emscripten. Celle-ci utilise webaudio qui est le meilleur moyen d’avoir du son sur un jeu HTML5 à l’heure actuelle. Si vous utilisez déjà l’interface asynchrone de portaudio, le portage est relativement aisé. Je me suis contenté de reprendre mon code existant et de l’adapter à deux / trois endroits pour être compatible avec l’interface de SDL_audio.

Fonction de rappel de portaudio

static int Callback(const void *input,
             void *output,
             unsigned long frameCount,
             const PaStreamCallbackTimeInfo* paTimeInfo,
             PaStreamCallbackFlags statusFlags,
             void *userData)

Fonction de rappel de SDL_audio
static void sdl_audio_callback(void* userData,Uint8* _stream,int _length)

La variable frameCount avec SDL_audio peut être calculé de la manière suivante :
(sf_count_t)((_length / sizeof(Sint16)) / num_channels);

(dans ce cas, je récupère les données audio sous forme d’entier court (SInt16). utiliser le type de données correspondant à votre code)

La variable ouput devient _stream avec SDL_audio.

Pour les autres variables tels que input / paTimeInfo et statusFlags, vous pouvez utiliser le pointer userData pour les passer à votre fonction.

Dernière chose à prendre en compte, la gestion multi-thread. Sur mon code original, j’utilise des variables modifié de manière atomique pour gérer l’accès aux fonctions sonores. Avec la version Emscripten, j’ai remplacé l’ensemble de ce code avec l’appel à SDL_LockAudio() / SDL_UnlockAudio(). La taille du tampon des données sonore doit également être modifiée. Dans mon cas, je suis passé d’une taille de 4096 octets à 1024 octets (soit 512 octets par canal audio), ce qui reste un compromis acceptable entre qualité et latence audio. Je n’ai de glitch sonores que sous Chrome, mais rien de problématique.

Port du moteur

Tant que votre moteur compile sous GCC, et n’utilise pas de muti-threading, le port devrait se faire sans trop de problème. La partie la plus importante est probablement le code de rendu si il n’est pas déjà compatible OpenGL ES 2.0 (WebGL utilise les même 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é OpenGL ES / WebGL. Il est par exemple impossible d’utiliser les tableaux de vertices directement, vous devez utiliser les VBO à la place. Pour plus d’info sur OpenGL 3.0, voir les tutos sur le site développez.com : tutos OpenGL

Si jamais vous n’utilisez pas déjà de shaders, il vous faudra les créer pour être compatible WebGL, la section suivante aborde la question du port de shaders.

A propos des shaders et de WebGL

WebGL ne supporte que les shaders #version 100, ce qui peut donc nécessiter de les réécrire.

Par exemple ce code:

#version 150

in vec2 inPos;
in vec2 inTex;

out vec4 color;
out vec2 texcoord;

en version 150 devient ce code

#version 100

attribute vec2 inPos;
attribute vec2 inTex;
varying mediump vec4 color;
varying mediump vec2 texcoord;

en version 100.

Il est possible d’utiliser le préprocesseur pour garder votre code dans un seul fichier de shaders (avec #ifdef GL_ES pour le code spécifique GLES/WebGL)
Dans mon cas, j’ai préférer utiliser des fichiers séparés pour bien faire la différence entre un shader WebGL et un shader PC.

Boucle de jeu spécifique à Emscripten

Plutôt que d’utiliser une boucle while(), avec Emscripten vous devez utiliser la ligne ci-dessous pour définir votre boucle de jeu principale :

emscripten_set_main_loop(renderoneframe,0,1);

où renderoneframe est votre fonction où se trouve le code de jeu pour une frame. Pour plus de détails, voir la documentation de Emscripten :

emscripten_set_main_loop (Anglais)

Compilation du code du jeu utilisant notre moteur

Une autre chose que j’ai eu à changer est la manière dont je charge ma DLL de jeu. Je dispose d’un système de module utilisant dlopen/dlsym pour accéder à des fonctions spécifique à mon jeu. Comme ce système n’est pas supporté par Emscripten, une solution est de compiler une librairie LLVM statique de ma DLL de jeu, référencer le header de cette DLL dans mon code d’application et remplacer mon code de résolution de fonctions comme suit :

Version normale
mngr->game_init_objects = (game_init_objects_t)dlsym(mngr->game_module,"game_init_objects");

Version Emscripten
mngr->game_init_objects = (game_init_objects_t)game_init_objects;

Cela ne s’applique bien sur que au cas où on ne charge qu’une seule DLL externe. Pour un sytème de plugin plus complexe, il est nécessaire de réécrire le code existant.

Commande finale utilisée par l’éditeur de liens :

-s USE_GLFW=3 -s TOTAL_MEMORY=67108864 -s NO_EXIT_RUNTIME=1 -s FORCE_ALIGNED_MEMORY=1

Accéder au fichiers de ressources du jeu

Emscripten dispose d’un système de fichiers virtuels qui remplace le système de fichiers classique de Windows. Celui-ci fonctionne de la même manière pour la gestion des fichiers via fopen / fread. Il est nécessaire de créer une archive des fichiers du jeu pour que notre jeu HTML5 puisse y accéder :

python \emscripten\1.25.0\tools\file_packager.py emscripten_data.data --preload assets scripts locale.db save.db config.lua > emscripten_data.js

cela génère deux fichiers emscripten_data.data et emscripten_data.js. Le premier est l’archive contenant toutes nos ressources et le second permet d’indexer nos fichiers afin que notre code de jeu les retrouvent. Dans cet exemple, les répertoires « assets » et « scripts » ainsi que les fichiers « locale.db » « save.db » et « config.lua » sont archivés dans le fichier emscripten_data.data.

Cette commande doit être exécutée depuis le répertoire courant de l’application. Dans cet exemple, le fichier locale.db est accessible via cette ligne de code :
fopen("locale.db","rb")
Si vous souhaitez ouvrir un fichier dans le répertoire assets, utilisez :
fopen("assets/file.png","rb").

Enfin, n’oubliez pas d’inclure le fichier .js dans le fichier .html généré par Emscripten :

afin que votre code de jeu puisse charger vos ressources.

Ce contenu a été publié dans Emscripten, HTML5, WebGL. Vous pouvez le mettre en favoris avec ce permalien.

Les commentaires sont fermés.