Conception de ofLive : Interaction entre JavaScript et C

J’ai récemment mis en ligne ofLive, un outil permettant de coder avec OpenFrameworks directement dans un navigateur via l’éditeur de code HTML5 ACE.

Lors de la création de cet outil, le principal problème à résoudre était comment faire communiquer le code généré par Emscripten avec du code JavaScript classique. J’explique ci-après comment je m’y suis pris via la création d’une librairie JavaScript pour Emscripten.

Pour réaliser ofLive, j’utilise les librairies suivantes :

J’ai également créé à cette occasion un site web en PHP/MySQL, les sources sont disponible sur le dépôt GitHub de ofLive

Conception d’une librairie, les bases

Pour créer ma librairie JavaScript compatible Emscripten, je suis d’abord parti de la base de code suivante :

var LibraryOfLive = {
$OFLIVE: {
},

editor_init: function()
{
},
}

autoAddDeps(LibraryOfLive, '$OFLIVE');
mergeInto(LibraryManager.library, LibraryOfLive);

Le tableau $OFLIVE est prévu pour contenir toute les variables qui doivent être accessibles en dehors de Emscripten, il est notamment possible d’y stocker des pointeurs vers des fonctions C. La fonction editor_init est prise en compte par Emscripten lors de la compilation, ce qui permet de l’appeler dans le code C afin d’invoquer du code JavaScript. Cette fonction doit avoir une déclaration correspondante dans la partie native de l’application au niveau d’un fichier d’en-tête .h :

#pragma once

extern "C" {
	//fonctions JavaScript utilisées depuis C
	extern void editor_init();
}

La signature de la fonction C doit correspondre à la signature de la fonction en JavaScript. Ceci fait, nous pouvons alors ajouter une nouvelle fonction à notre librarie, il s’agira cette fois d’une fonction C utilisable par JavaScript. Voici le code de la librairie mis à jour :

var LibraryOfLive = {
    $OFLIVE: {
        backend_loadlua: null,
     },

    editor_init: function()
    {
        //bind c glue functions
        OFLIVE.backend_loadlua = Module.cwrap('backend_loadlua','number',['string']);
    },

}

autoAddDeps(LibraryOfLive, '$OFLIVE');
mergeInto(LibraryManager.library, LibraryOfLive);

Ici, nous utilisons la fonction Module.cwrap pour obtenir un pointeur vers une fonction C nommée ‘backend_loadlua’, les paramètres qui suivent nous permettent de spécifier le type retour (‘number’ dans ce cas) et la liste des paramètres de la fonction C (ici un seul paramètre ‘string’). Le résultat est stocké dans notre variable backend_loadlua du tableau $OFLIVE, ce qui nous permet d’appeler cette fonction directement en JavaScript avec le code suivant :

OFLIVE.backend_loadlua('ici notre code lua')

Pour finir, nous devons bien évidemment ajouter cette fonction C dans notre code natif, voici l’en-tête mis à jour :

#pragma once

extern "C" {
	//fonctions JavaScript utilisées depuis C
	extern void editor_init();

	//fonctions C utilisées depuis JavaScript
	int backend_loadlua(const char* scriptcontent_from_js);
}

et le contenu de la fonction dans le fichier source :

int backend_loadlua(const char* scriptcontent_from_js)
{
       std::string script_content(scriptcontent_from_js);
	ofLogError() << script_content;
}

C’est tout pour la partie code, il nous faut maintenant préciser à Emscripten où se trouve notre librairie et les fonctions à exporter. Le paramètre « –js_library » suivi du chemin vers notre librairie permet de l’intégrer à l’application Emscripten finale. Le paramètre « -s EXPORTED_FUNCTIONS » permet de spécifier la liste des fonctions C à exporter vers JavaScript.

N’oubliez pas d’ajouter un tiret-bas en face du nom de chaque fonctions et aussi d’ajouter à la liste la fonction « main », que Emscripten n’ajoute pas automatiquement dans notre cas. (Sinon notre application ne pourra pas démarrer au chargement de la page Web)


--js-library ".\library_editor.js" -s EXPORTED_FUNCTIONS='["_main","_backend_loadlua"]'

Voici le code final de la librairie et l’en-tête .h tels qu’ils apparaissent dans ofLive :

var LibraryOfLive = {
    $OFLIVE: {
        editor: null,
        backend_loadlua: null,
        backend_newscript: null,
        backend_openscript: null,
        backend_savescript: null,
        opened_script: "",
        readonly_script: false,
     },

    editor_init: function()
    {
        OFLIVE.editor = ace.edit("editor");
        OFLIVE.editor.setTheme("ace/theme/monokai");
        OFLIVE.editor.getSession().setMode("ace/mode/lua");
        
        //mount read/write filesystem
        FS.mkdir('/oflivescripts');
        FS.mount(IDBFS,{},'/oflivescripts');
        Module.print("ofLive: Start scripts sync...");
        Module.syncdone = 0;
        
        FS.syncfs(true,function(err) {
            assert(!err);
            Module.print("OfLive: End scripts sync");
            Module.syncdone = 1;
       });
       
       //check if we load a shared script, in this case readonly mode
       if($.trim($('#share_content').text()))
       {
           OFLIVE.opened_script =  $('#name_script').text();
           OFLIVE.readonly_script = true;
           $('#save_script').attr('class','disabled_link');
           $('#shared_script').attr('class','disabled_link');
       }
        
        //bind c glue functions
        OFLIVE.backend_loadlua = Module.cwrap('backend_loadlua','number',['string']);
        OFLIVE.backend_newscript = Module.cwrap('backend_newscript','number',['string']);
        OFLIVE.backend_openscript = Module.cwrap('backend_openscript','number',['string','number','string']);
        OFLIVE.backend_savescript = Module.cwrap('backend_savescript','number',['string','string']);
        
        //custom commands
        OFLIVE.editor.commands.addCommand({
            name: 'saveScript',
            bindKey: {win: 'Ctrl-S', mac: 'Command-S'},
            exec: function(editor) {
                //check first if the script is read only and if a name exist
                if(OFLIVE.readonly_script == true)
                    return;
                
                //no name, open name input popup
                if(OFLIVE.opened_script == "") {
                     $("#save_script_name").click();
                }
                else {
                    OFLIVE.backend_savescript($('#name_script').text(),editor.getValue());
                    OFLIVE.backend_loadlua(editor.getValue());
                }
            },
            readOnly: false
        });
    },


    editor_loadscript: function(scriptptr) 
    {
        var scriptcontent = Pointer_stringify(scriptptr);
        OFLIVE.editor.setValue(scriptcontent);
    },
    
    editor_isshare: function()
    {
        if($.trim($('#share_content').text()))
            return 1;
           
        return 0;
    },

}

autoAddDeps(LibraryOfLive, '$OFLIVE');
mergeInto(LibraryManager.library, LibraryOfLive);

Dernière précisions : Il est possible d’utiliser du code jQuery dans notre librairie mais SEULEMENT à l’intérieur d’une fonction. Les fonctions editor_* ne sont PAS accessibles depuis JavaScript, seule les fonctions que l’on souhaite appeler via C sont définies à ce niveau.

#pragma once

extern "C" {
	//functions calling javascript library from c
	extern void editor_init();
	extern void editor_loadscript(const char* scriptcontent);
	extern int editor_isshare();

	//functions calling c code from javascript
	int backend_loadlua(const char* scriptcontent);
	int backend_newscript(const char* script_name);
	int backend_openscript(const char* script_name,int isExample,const char* type);
	int backend_savescript(const char* script_name,const char* scriptcontent);
}

Plus de détails sur l’utilisation de fonctions C en JavaScript dans la documentation officielle (EN) .

Publié dans Emscripten, HTML5, OpenFrameworks | Commentaires fermés sur Conception de ofLive : Interaction entre JavaScript et C

Persistance de données avec Emscripten

Lors du portage d’un jeu vers le web via Emscripten, On remarque rapidement que le système de fichiers virtuel utilisé ne persiste pas les données par défaut. Toutes les modifications sur un fichier sont systématiquement perdues d’une session à une autre. Heureusement, Emscripten dispose d’un système de fichiers spécifique, nommé IDBFS, qui permet de persister les données d’un fichier en utilisant l’API Indexed Db de HTML5. Il est important de garder en tête que la persistance de données se fait manuellement en appelant des fonctions de synchronisation dédiées, ses fonctions de synchronisation étant asynchrones, il faut également prendre en compte le délai entre l’appel de fonction et le moment où les données sont effectivement sauvegardées. Pour plus d’informations sur l’API fichiers de Emscripten, voir la documentation sur le site officiel (en anglais) :
Emscripten API

Sur l’ensemble de mes jeux écrit en C existant, j’utilise SQLite pour la sauvegarde des données joueurs. Les avantages principaux de SQLite sont l’utilisation du langage SQL pour la manipulation de données, l’absence de serveur en arrière plan et le stockage de données dans un seul fichier.

En utilisant IDBFS, il est possible de conserver le fonctionnement de SQLite tel quel, en y ajoutant l’appel de fonction de synchronisation pour sauvegarder les modification du fichier de base de données.

Initialisation du système de fichiers IDBFS :

EM_ASM(
			//Création de notre dossier qui contiendra l'ensemble des données à      persister
			FS.mkdir('/persistent_data');
			//initialisation de IDBFS sur notre dossier
			FS.mount(IDBFS,{},'/persistent_data');

			Module.print("start file sync..");
			//valeur de vérification de fin de synchro
			Module.syncdone = 0;

			//effectue la synchronisation, si des fichiers existe déjà, il sont    ajouté à notre dossier "persistent_data"
			//premier paramètre = "true" signifie synchronisation depuis Indexed Db vers Emscripten, 
			//"false" signifie synchronisation depuis Emscripten vers Indexed Db
			//second paramètre = fonction appelée lorsque les données sont synchronisées
			FS.syncfs(true, function(err) {
				assert(!err);
				Module.print("end file sync..");
				Module.syncdone = 1;
			});
		);

(La macro EM_ASM permet d’intégrer du code JavaScript directement en C)

Le dossier « persistent_data » est toujours vide au début, Il n’est pas possible de monter un dossier qui est déjà monté avec le système de fichiers par défaut de Emscripten.

Une fois la synchronisation effectuée, on peut commencer à utiliser les fichiers du répertoire « persistent_data », ou ajouter un fichier de sauvegarde initial si le joueur lance notre jeu pour la première fois.

if(!save_mngr.ready)
	{
		if(emscripten_run_script_int("Module.syncdone") == 1) //vérification de la valeur de fin de synchro		{
			FILE* file = fopen(save_path,"r"); //vérification de le fichier sqlite existe, sinon on le copie dans notre dossier persistant

			if(file == NULL)
			{
				logprint("save.db file doesn't exist in file system, copying it...");
				size_t size;
				//copie du fichier
				//dans ce cas, on copie un fichier sqlite de base dans le dossier  persistant depuis le dossier "base_data"
				unsigned char* buffer = get_resx_content(&game_state->resx_mngr,"/base_data/save.db",&size,NULL);

				file = fopen(save_path,"w");

				fwrite(buffer,sizeof(unsigned char),size,file);
				fclose(file);
                                
				//persistance des données depuis Emscripten vers Indexed Db
				EM_ASM(
					Module.print("Start File sync..");
					Module.syncdone = 0;
					FS.syncfs(false, function(err) {
						assert(!err);
						Module.print("End File sync..");
						Module.syncdone = 1;
					});
				);
			}
			else
			{
				fclose(file);
			}

			save_mngr_init(&save_mngr,save_path);  //chargement du fichier SQLite, la variable "ready_flag" a maintenant la valeur "true"
		}
		else
		{
			return;  //tant que les données de sauvegarde ne sont pas chargées, on empêche l'exécution du reste de la fonction
		}
	}

(La variable save_mngr gère dans ce cas l’ensemble des données de sauvegarde via SQLite)
(Comme Emscripten ne nous permet pas d’utiliser une boucle while ou la fonction sleep pour attendre l’exécution d’une fonction asynchrone, Le code est ajouté au début de notre fonction principale de jeu appelée en boucle)

Une fois les données de sauvegarde créées / chargées, il est possible de persister les modifications de notre fichier SQLite avec le code suivant :

EM_ASM(
			//persistance des données
			FS.syncfs(false,function (err) {
				assert(!err);
			});
		);

Le code d’initialisation chargera ainsi les données modifiées la prochaine fois que le joueur lancera notre jeu. Via Chrome, il est possible de voir le contenu synchronisé dans Indexed Db, Aller à Outils de développements > Onglet Ressources > Indexed Db.

Publié dans Emscripten, HTML5, WebGL | Commentaires fermés sur Persistance de données avec Emscripten

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.

Publié dans Emscripten, HTML5, WebGL | Commentaires fermés sur Port d’un moteur de jeu en C vers html5 via Emscripten

Reckpunk est disponible !

Reckpunk est disponible ! La version complète est désormais téléchargeable via le site officiel :

https://dlan.itch.io/reckpunk

Une démo, ainsi que la bande son du jeu sont également disponible sur le site. Voici le trailer de lancement, le jeu à pas mal évolué en un an 😉 :

Publié dans Reckpunk | Marqué avec | Commentaires fermés sur Reckpunk est disponible !

(2/2) Création d’une video de gameplay: Utilisation de VirtualDub et BulkRenameUtility

J’ai récemment conçu une vidéo de gameplay de Reckpunk  (visible ici)

Les séquences de gameplay ont été capturées en uitlisant la technique décrite dans mon précédent post sur apitrace. Cependant, une vidéo de gameplay nécessite d’inclure un maximum de séquences provenant de différents niveaux, et de les regrouper ensemble tout en soignant les transitions, pour cela, il faut disposer d’outils qui permettent de:

  1. sélectionner uniquement une partie de la capture du jeu
  2. renuméroter les fichier png afin qu’ils soit dans l’ordre
  3. réaliser les transitions
  4. Compresser le fichier vidéo avec un bon rapport qualité d’image / taille du fichier.

Pour les points 1 et 2, après avoir supprimer les fichiers png inutiles via l’explorateur Windows, j’ai eu recours à  BulkRenameUtility pour réaliser la renumérotation, voici les paramètres que j’ai utilisés :

bulkrename

En gros, il s’agit de supprimer le nom initial de chaque fichier en premier ( la groupbox « remove » ), et de le remplacer avec un nom de notre choix (groupbox « add » ),  tout en commençant avec le bon numéro de fichier (si la précédent séquence comporte 150 fichier, il faut saisir la valeur 151 dans le champ « start » de la groupbox « numbering »)

Ceci fait,  il est possible d’ajouter une transition entre les séquences. La transition la plus classique et l’effet de fondu (remplissage de l’écran avec une couleur, généralement on utilise la couleur noir)

Virtualdub est parfait pour cette tâche. Tout d’abord, ouvrons le premier fichier de notre séquence d’image png. N’oubliez pas de définir le bon framerate pour la séquence (Video > Framerate). Pour ma vidéo, j’ai mis la valeur 60 dans « Source framerate » et 30 dans « frame rate conversion ».

Maintenant que les paramètres de base sont sauvegardés, (file> save processing settings) , ajoutons le filtre qui va nous permettre d’obtenir un effet de fondu. Allez sur video > filter, et ajoutez le filtre « fill ». Dans la partie configuration, sélectionnez la couleur noir.

filter_virtualdub

Cela nous permets de remplir l’écran avec une seule couleur, mais comment faire pour remplir l’écran uniquement à un moment donné ? et comment effectuer ce remplissage de manière graduelle d’une frame à l’autre?

Pour cela, nous allons avoir besoin du « curve editor ». Pour l’afficher , allez sur view>curve editor.

curve_editor

Comme visible sur cette capture, Il est possible de sélectionner dans le menu déroulant tout en haut le filtre que l’on souhaite modifier. (Ici, seul le filtre « fill » est disponible, et nous pouvons agir sur son opacité)

Shift+click sur la partie noir fait apparaitre un point et une ligne qui représente notre valeur d’opacité.

Quand la ligne est tout en haut, l’opacité est à 100%, quand elle est tout en base, l’opacité est à 0%. Les nombres tout en bas représentent les frames de la vidéo. Il est possible ainsi de contrôler précisément le fonctionnement du filtre frame par frame.

shift + click sur la ligne permet d’ajouter un nouveau point de contrôle. ctrl + click permet de le supprimer. Un simple glisser/déposer permet de le déplacer à l’endroit souhaité.

Pour le moment, ajoutez un seul point de controle, et déplacer le tout en bas, ainsi le filtre n’est pas actif. Cela permet de faire apparaitre à nouveau nos séquences de gameplay  dans la fenêtre de droite.

Maintenant, ajoutez un second point à l’endroit où vous souhaitez que l’écran soit totalement noir, et déplacer le tout en haut de l’écran de l’éditeur. Une ligne va relier les deux points, et avec ça, notre effet changera de la frame où est placé le premier point jusqu’à la frame où est placé le deuxième point, remplissant ainsi notre écran de manière graduelle. Il est évidemment possible d’ajouter autant de point de contrôle que vous voulez pour améliorer le rendu final du filtre.

Enfin, voyons comment exporter et compresser notre séquence d’images png en un fichier avi prêt à être envoyé sur youtube :

Il vous faut pour ça le lossless codec de CamStudio, comme youtube réalise sa propre compression, il est inutile d’utiliser un codec qui compresse de manière plus « agressive » tel que x264, vu que nous souhaitons garder la meilleure qualité possible. Pour information, lorsque j’ai exporté ma vidéo de gameplay sans compression, le fichier final faisait 1.5 Go pour seulement 30 seconds en 1024 x 768. Un fois compressé via le codec de CamStudio, le fichier ne fait plus que 150 Mo, pour une qualité similaire.

Après l’installation du codec, allez sur « video>compress » et sélectionner le codec CamStudio.

Pour ajouter une bande son, il suffit d’aller sur « audio>audio from other file » et de sélectionner le fichier son correspondant.

Sauvegarder le tout en avi, attendez que Virtualdub finisse l’export et vous obtiendrez un fichier vidéo près à l’envoi !

Publié dans video capture | Commentaires fermés sur (2/2) Création d’une video de gameplay: Utilisation de VirtualDub et BulkRenameUtility

(1/2) Créer une vidéo de gameplay : utilisation d’apitrace

Un petit post pour expliquer l’utilisation de apitrace pour capturer des vidéos de gameplay, l’idée vient de ce post: http://kivy.org/planet/2011/06/recording-opengl-output-to-h264-video/

Pourquoi utiliser apitrace plutôt que des logiciels tels que FRAPS ?

FRAPS et autre logiciels ont tendance à rendre compliquer la capture de séquence de gameplay en haute qualité, mon objectif était aussi de pouvoir capturer des séquences sans avoir besoin de logiciels payant, ou d’un ordinateur plus puissant.

Un autre problème de FRAPS est par défaut le manque de fidélité des couleurs (il existe une option pour changer cela, mais au prix d’encore plus de puissance, surtout pour capturer un jeu en 60 fps) et l’algorithme de compression qui réduit la qualité finale de la capture. Vu que Reckpunk est un jeu dont les graphismes repose sur un style avec des bordure de 1 pixel, le résultat avec FRAPS ou PlayClaw est plutôt mauvais, ce qui m’as poussé à chercher comment capturer des séquences de gameplay sous forme de fichiers png.

A propos de apitrace

Je reprend ici la description du site officiel :

apitrace est un ensemble d’outils pour:

  • enregistrer des appel aux API OpenGL, OpenGL ES, D3D et DDraw dans un fichier
  • rejouer les appel aux API depuis un fichier, sur n’importe quelle machine, et pour OpenGL et OpenGL ES, sur n’importe quel système d’exploitation.

En résumé, il faut créer tout d’abord un fichier trace, et puis le rejouer pour générer des images png pour chaque frames, que nous pouvons ensuite joindre dans une vidéo via un logiciel comme Virtual dub

Comment capturer une séquence de gameplay

Tout d’abord, téléchargez apitrace depuis le site officiel, http://apitrace.github.io/  Dans le répertoire bin, nous retrouvons divers programmes, apitrace et glretrace sont les seuls que nous allons utiliser.

Réaliser la capture:

apitrace trace [chemin vers l'exécutable]

Cela génère un fichier trace dans le répertoire courant, avec le même nom que l’exécutable.

Générer une séquence de fichiers png:

glretrace -sr [chemin vers le fichier trace] --call-nos=false --snapshot-format=RGB

cela génère un fichier png pour chaque frames, et les stockent dans l’ordre (avec l’option –call-nos=false, par défaut, le nom du fichier utilise un nombre généré, ce que l’on ne veut pas ici)

Créer un fichier vidéo

Chargez le premier fichier png de la séquence avec Virtual dub, les fichiers étant dans l’ordre, Virtual dub va automatiquement importer l’ensemble de la séquence. Nous pouvons également sélectionner une image au milieu de la séquence, Virtual dub chargera alors uniquement les fichiers qui suivent l’image sélectionnée.

N’oubliez pas de changer le framerate source, dans video>framerate dans source rate, mettre la valeur 60 (si votre jeu tourne à 60 fps).

Si vous voulez générer un fichier gif, il faut utiliser la conversion de framerate, juste au dessous. une valeur de 30 dans convert fps fonctionne plutôt bien, vous pouvez aussi utiliser l’option « Process every other frame (decimate by 2) » ( le résultat est relativement similaire, à tester si la première option ne donne pas un résultat fluide).

Si vous ne voulez qu’une partie de l’image, utiliser le filtre null transform pour rogner le résultat final (plutôt utile pour limiter la taille d’un gif).

Exportez en avi, et nous avons votre vidéo avec une excellent fluidité ! Avec les paramètres par défaut, le résultat final reste plutôt moyen niveau qualité de rendu. La prochaine fois, j’aborderai la partie concernant la génération d’une vidéo HD depuis notre séquence de gameplay.

Publié dans video capture | Commentaires fermés sur (1/2) Créer une vidéo de gameplay : utilisation d’apitrace

Reckpunk : version 2 : 2 mois plus tard

(° Ɐ°)

Après avoir joué un peu avec mon code de rendu, et ajouté un renderer D3D11, j’ai finalement pu réaliser une nouvelle version de Reckpunk ! La version Windows restera en OpenGL, mais pour une futur version mobile, le renderer D3D11 sera plutôt utile.

Télécharger ReckpunkA2 – Windows – 2.1 MBTélécharger

Nouveau dans cette version

  1. 10 niveaux supplémentaires
  2. Un sélecteur de niveau
  3. Des effets graphiques supplémentaires et un nouveau jeu de couleur pour les décors
  4. une options pour changer le comportement du curseur (Fixe ou suivant le scrolling)
  5. La distance de vue à été légèrement augmentée

Capture d’écrans de cette version :

Nouveau jeu de couleur
Exemple de décor de fond

Le prochain objectif est de distribuer une version linux, J’espère avant 2 mois 🙂

Publié dans Uncategorized | Commentaires fermés sur Reckpunk : version 2 : 2 mois plus tard

Reckpunk : version jouable

Une version jouable apparaît!

Télécharger ReckpunkV1 – Windows – 1,87 MBTélécharger

La fin novembre approche, du coup impossible de savoir si je pourrai ajouter beaucoup plus de contenu à cette version. Quoi qu’il en soit, je prévois au moins d’ajouter quelques animations au niveau du décor afin de donner un peu plus de vie à l’ensemble. Quelque effets sonore ne serait pas de trop, ça devrait aller, je m’y prend toujours à la dernière minute 🙂

Quelques images supplémentaires:




Publié dans Uncategorized | Commentaires fermés sur Reckpunk : version jouable

October challenge => November challenge

Au final.. octobre est terminé, mais mon jeu pour l’october challenge n’est pas terminé 🙁

Le concept fonctionne plutôt bien, notamment sur mobile. Plutôt que d’abandonner le jeu en cours, j’ai décidé de continuer à avancer dessus pour le mois de novembre au moins. Les base du gameplay sont là, le plus important maintenant étant d’ajouter du contenu (je prévois d’avoir 20 niveaux). Et bien sur, avancer aussi sur le sound design!

Quelques screenshots :

début

avant un checkpoint

presque à la fin

surf sur un boite

mort

Publié dans Uncategorized | Commentaires fermés sur October challenge => November challenge

October challenge !

Quoi de mieux quand on a déjà un projet sur les bras ? un second project ! Mon objectif est de participer à l’October Challenge organisé par le site ludum dare, le principe est simple :

Finir un jeu – le distribuer – gagner 1 dollar

J’ai donc avancé sur un nouveau jeu, il s’agira d’une reprise de reckless adventurer en version plateforme, et adaptée aux mobiles. Je devrais normalement le distribuer uniquement sur le play store, la version pc devrait être gratuite 🙂

pour cela, je vais quand même utiliser le moteur et l’éditeur de Token, cela devrait me permettre d’accélérer le développement et de faciliter la distribution, vu que le moteur tourne également sous android.

résultat du jour :

capture niveau 1

Publié dans Uncategorized | Commentaires fermés sur October challenge !