Créez un jeux en ligne (MMO) en HTML5 avec Phaser et Socket.IO – partie 1

Sommaire

Objectif

Ce tutorial est le premier d’une série sur la création d’un jeux en ligne ( MMO) tout en Javascript, notre jeux seras divisé principalement de deux parties :

Client:
c’est ce que le joueur verra sur son navigateur, notre client sera développé autour de Phaser.IO.
Serveur:
C’est ce qui va gérer la communication entre les joueurs, pour faire le tout en Javascript on va utiliser du NodeJS avec Express et Socket.IO.

Pour faciliter le processus de développement on va utiliser brunch.IO, c’est un super outils de build pour les projets webs, vous pouvez avoir plus de détails sur ce lien.

Dans ce tuto je présume que vous connaissez un minimum concernant le framework phaser.io et que vous avez déjà installé NodeJS sur votre pc.

Etape 1 : partie client avec phaser

Pour la première partie on va s’occuper de la partie client, on va poser la structure du code, et s’assurer que notre personnage puisse bouger, pour  rendre les mouvements plus sympa on va faire en sorte que utiliser l’algorithme A*, voici une démo du résultat souhaité :

Le code source sur GitHub

structure du projet

image structure projet

Structure du projet

  • Client : Le code source du client
    • assets : contient toutes les resources statiques ( images sons …)
    • gameObjects : objets contenant la mechanique du jeu ( voir plus bas)
    • gameSprites: contient les objets qui gèrent les sprites des objets
    • states : Contient tout les états du jeux
    • utils : tout ce qui est réutilisable dans notre code sera placé ici
  • Server : pour le moment on ne met rien ici, ce dossier est prévu pour la partie serveur
  • Vendor: va contenir les librairies qu’on va utiliser ( phaser, astar et socket.io)

Le fichier package.json:

Notre fichier package.json ressemblera à ça :

{
  "name": "mmo_demo",
  "version": "1.0.0",
  "description": "mmo example with phaser express and socket.io",
  "author": "Marwane",
  "license": "ISC",
  "devDependencies": {
    "auto-reload-brunch": "^1.8.0",
    "brunch": "^1.8.5",
    "javascript-brunch": "^1.7.1",
    "less-brunch": "^1.8.1"
  },
  "dependencies": {
    "express": "^4.13.3",
    "morgan": "^1.6.1",
    "path": "^0.12.7",
    "socket.io": "^1.3.6",
    "uglify-js-brunch": "^1.7.8"
  }
}

Voici les dépendances :

  • brunch: notre outil de build
  • javascript-brunch : plugin qui compile le code javascript du dossier source ( client) vers la déstination ( public).
  • less-brunch: optionnel , ça sert à compiler les fichiers de style de type less en css.
  • express, morgan et socket.io : on verra dans la partie 2 🙂
  • uglify-js-brunch : plugin qui minifie le code si on souhaite compiler une version de production (avec la commande brunch -P).

Configuration de brunch:

On va utiliser brunch pour compiler le code de la partie client, le but est de compiler tout le code se trouvant dans le dossier client/ et vendor/ dans le dossier public/ , voici la configuration de brunch:

module.exports = config:
  paths:
    "watched": ["client", "vendor"]
    "public": "public"
  files:
    javascripts:
      joinTo:
        'js/client.js': /^client/
        'js/vendor.js': /^vendor/

      order:
        before: [
          'vendor/phaser.js',
          'vendor/easystar-0.2.1.min.js',
          'vendor/phaser_pathfinding.min.js',
        ]

    stylesheets: joinTo: 'styles/client.css'

  server:
    path: './server/httpServer.js'
    run: yes
    port: 9192

  plugins:
    autoReload:
      port: 9193
    uglify:
      mangle: false

Durant la phase de développement on utilisera la commande suivante :

brunch w

Brunch va lancer un serveur web sur le port 9192, on pourra ainsi tester notre code en utilisant l’URL suivante : http://localhost:9192

Avec cette commande brunch va observer les dossiers client/ et vendor/, si un changement est détécté brunch va effectuer les taches suivantes :

  1. compiler le code se trouvant dans les dossiers client/ et vendor
  2. Si vous avez déjà ouvert la page http://localhost:9192 brunch va recharger la page

Page html

La page html de notre jeu se trouve dans client/assets/ , ce fichier sera copié dans le dossier public/ lors de la compilation de notre code, c’est pourquoi tout les chemins dans ce fichiers pointent vers les fichiers « compilé » :

» Afficher le fichier : index.html


<!doctype html>
<!--[if lt IE 7]>      <html class="lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]>         <html class="lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]>         <html class="lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html> <!--<![endif]-->
<head>
    <title>MMO demo</title>
    <link href="styles/client.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="game"></div>

<script src="js/vendor.js"></script>
<script src="js/client.js"></script>


<script>
    require('client/game').init();
</script>
</body>
</html>

» Cacher

Notez que dans notre fichier html on a une balise script contenant la ligne suivante :


require('client/game').init();

ceci va démarrer le jeux, on verra la source du point d’entrée du jeux un peu plus bas.

Préparation des sprites

pour la gestion de nos sprites, on va utiliser texture packer, ça va nous permettre de regrouper toutes nos images dans une seule grande image, on gagne ainsi en temps de chargements durant le démarrage de notre jeux.

Sprite utilisée

Sprite utilisée

les sprites utilisées sont accessible sur le code source de cet exemple sur github, je vous met quand même les images individuelles des sprites si vous voulez les compiler vous même.

Création du niveaux

Pour la création du niveau on va utiliser un éditeur de niveaux cross platform tu nom de Tiled, c’est un outil très puissant qui permet de construire des niveaux en 2D, les niveaux sont exportables vers le format JSON et sont très bien gérés par Phaser.JS.

On va construire le niveau sous forme de couches ( layers ), ça permet de superposer les tiles durant la création du niveau, mais pas seulement, pour gérer plus tard le système de mouvement, on va définir dans Tiled une couche spéciale nommé “walkables”, comme indiqué sur l’image ci dessous, on définit les obstacles en rouge et le reste en vert, on va voir plus tard comment utiliser ces informations dans phaser.

map dans Tiled

map dans Tiled

map avec la couche "walkables"

map avec la couche « walkables »

Vous pouvez trouver le fichier source de notre map dans le code source de l’exemple dans le dossier : client/assets/gameAssets/map

Commençons à déveloper notre client

On a préparé notre environnement de développement, on peux donc commencer à coder notre jeu, le jeux sera composé de deux états ( écrans) , qu’on va nommer Boot et Play., le point d’entrée de notre jeux sera le fichier game.js

Game

Ici on configure la taille de l’ecran ainsi que les états de notre jeux :

» Afficher le fichier : game.js

"use strict";

var gameBootstrapper = {
    init: function(){
        console.log('**** Game bootstrap ****');
        var game = new Phaser.Game(800, 460, Phaser.AUTO, 'game');

        game.state.add('boot', require('./states/boot'));
        game.state.add('play', require('./states/play'));

        game.state.start('boot');
    }
};

module.exports = gameBootstrapper;

» Cacher

notez que ce module contient la fonction init qui se charge de démarrer notre jeux, cette fonction sera appelée tout en bas du fichier html.

Boot:

c’est notre écran de chargement, ici on charge toutes les resources necesaires à notre jeux, elle peuvent être sous forme d’images, maps …

 

» Afficher le fichier : Boot.js

'use stric';

function Boot(){}

Boot.prototype = {
    preload: function(){
        this.game.stage.disableVisibilityChange = true;
        this.game.stage.backgroundColor = 0x3b0760;
        this.load.onLoadComplete.addOnce(this.onLoadComplete, this);

        this.showLoadingText();
        this.loadAssets();
    },

    onLoadComplete: function(){
        this.game.state.start('play');
    },

    loadAssets: function(){
        this.game.load.tilemap('map', 'gameAssets/map/map.json', null, Phaser.Tilemap.TILED_JSON);
        this.game.load.image('tiles', 'gameAssets/map/tile1.png');
        this.game.load.image('walkables', 'gameAssets/map/walkable.png');

        this.load.atlasJSONArray('sprites', 'gameAssets/sprites/sprites.png', 'gameAssets/sprites/sprites.json');
    },

    showLoadingText: function(){
        var loadingText = "- Loading -";
        var text = this.game.add.text(this.game.world.centerX, this.game.world.centerY, loadingText);
        //  Centers the text
        text.anchor.set(0.5);
        text.align = 'center';

        //  Our font + size
        text.font = 'Arial';
        text.fontWeight = 'bold';
        text.fontSize = 70;
        text.fill = '#ffffff';
    }
};

module.exports = Boot;

» Cacher

showLoadingText : Affichage du texte « Loading » durant le chargement des resources.

loadAssets: ici on va charger les resources qu’on va utiliser plus tard dans le jeux, dans notre cas ça sera la map du jeux, les tilesSets qui la composent, et la texture des sprites.

onLoadComplete: passage au jeux après le chargement de toutes les resources.

Play:

Après le chargement on passe à l’ecran principal, ici on va mettre en place les éléments de notre jeu, pour le moment le code va effectuer les taches suivantes :

  • Mise en place du niveau ( créé avec Tiled)
  • Initialiser l’objet pathfinder qui sera utilisé
  • Afficher un carré qui s’affiche là ou la souris est survolée
  • Ajouter le joueur dans le niveau

» Afficher le fichier : Play.js

'use stric';

var CharacterObj = require('client/gameObjects/CharacterObj');
var Pathfinder = require('client/utils/Pathfinder');

function Play(){}

Play.prototype = {
    create: function(){
        this.game.stage.backgroundColor = 0xFFFFFF;
        this.game.physics.startSystem(Phaser.Physics.ARCADE);

        this.initMap();
        this.initPathfinder();
        this.initCursor();
        this.addMainPlayer();

    },
    initMap: function(){
        this.map = this.game.add.tilemap('map');
        this.map.addTilesetImage('tiles', 'tiles');
        this.map.addTilesetImage('collision', 'walkables');

        this.walkableLayer = this.map.createLayer('collision');


        this.map.createLayer('ground');
        this.map.createLayer('obstacles');
        this.map.createLayer('obstacles2');

        this.walkableLayer.resizeWorld();
    },

    initPathfinder: function(){

        Pathfinder.init(this.game,
                        this.walkableLayer,
                        this.map.layers[3].data, // the layer containing the walkable tiles
                        [2017], // ID of the walkable tile ( the green one )
                        32
        );
    },

    initCursor: function(){
        this.cursors = this.game.input.keyboard.createCursorKeys();
        this.marker = this.game.add.graphics();
        this.marker.lineStyle(2, 0x000000, 1);
        this.marker.drawRect(0, 0, Pathfinder.tileSize, Pathfinder.tileSize);

        this.input.onDown.add(function(event){
            this.updateCursorPosition();
            this.player.moveTo(this.marker.x, this.marker.y, function(path){

            });
        }, this);

    },

    updateCursorPosition: function(){
        this.marker.x = this.walkableLayer.getTileX(this.game.input.activePointer.worldX) * 32;
        this.marker.y = this.walkableLayer.getTileY(this.game.input.activePointer.worldY) * 32;
    },

    addMainPlayer: function(){
        this.game.world.setBounds(0, 0, 1600, 1600);
        this.player = new CharacterObj(this.game, 200, 200, true);
        this.game.camera.follow(this.player.sprite);
    },

    update: function(){
        this.updateCursorPosition();
    }
};

module.exports = Play;

» Cacher

Explication du code

initMap: Charge la map créé avec Tiled ainsi que les images utilisée pour l’affichage des tiles (céllules).
après avoir chargé la map on affiche les couches (layers) avec createLayer, ici l’ordre est important, il faut ajouter la couche « walkables » avant les autres couches pour qu’elle soit invisible.

initPathfinder: Comme dit précédemment, les mouvements seront effectué avec avec l’algorithme A*, et pour ce faire on va utiliser la librairie Astar, ainsi que le plugin Phaser qui failite son intégration.
Ici on délègue toute la partie de Pathfinding dans un Objet qu’on appèle « Pathfinder », l’objectif est de séparer la logique du pathfinder du reste du code pour garder notre code propre et lisible.

initCursor: Ici on va créer un carré qui suivra la l’emplacement de la souris, on fait aussi en sorte que le joueur se dirige vers la case cliquée par le joueur.

Pathfinder

L’objectif de ce fichier est de séparer tout les appels au plugin du pathfinder, ça va nous permettre de garder le reste du code plus propre et plus lisible.

» Afficher le fichier : Pathfinder.js

'use strict';

var pathfinder;

module.exports = {
  init: function(game, walkableLayer, walkableLayerData, walkableTiles, tileSize){

      this.walkableLayer = walkableLayer;
      this.tileSize = tileSize;
      pathfinder = game.plugins.add(Phaser.Plugin.PathFinderPlugin);
      pathfinder.setGrid(walkableLayerData, walkableTiles);
      
  },
    calculatePath: function(fromX, fromY, toX, toY, onPathReadyCallback){
        var fromTiles = [this.getTileX(fromX), this.getTileY(fromY)];
        var toTiles = [this.getTileX(toX), this.getTileY(toY)];
        pathfinder.preparePathCalculation (fromTiles, toTiles,onPathReadyCallback );

        pathfinder.calculatePath();
    },

    getTileX: function(value){
        return this.walkableLayer.getTileX(value);
    },
    getTileY: function(value){
        return this.walkableLayer.getTileY(value);
    }
};

» Cacher

cet Objet va contenir les fonctions suivantes :

init: fonction d’initialisation du plugin pathfinder, cette fonction prend les paramètres suivants :

  • game: référence vers l’objet principale du jeux
  • walkableLayer: réference vers la couche nommé walkable
  • walkableLayerData: Tableau contenant pour chaque case dans la map le numéro vers l’image à utiliser.
  • walkableTiles: numéro de la case de couleur verte dans notre map, pour trouver la bonne valeur à mettre il faut regarder dans le fichier map.json, dans notre cas c’est « 2017 »
  • tileSize: Taille des cases de notre map, pour notre cas ça sera 32

calculatePath: cette fonction seras appelé pour calculer le chemin que le joueur devra suivre pour ces mouvements, cette fonction fait appel au plugin Pathfinder, le calcul du chemin est fait en asynchrone, à la fin du calcule le plugin executera la fonction « onPathReadyCallback » passé en paramètre.

Les objets CharacterObj et CharacterSpr

Pour garder le code simple et lisible j’ai décidé de séparer le code du joueur en deux Objets:

  • CharacterSpr: Tout se qui se rapporte avec la gestion de la partie visible du joueur sera faite ici (sprite, animations …)
  • CharacterObj: Tout ce qui n’est pas directement lié au sprites et animations sera dans cet objet, ça peut donc contenir les caractéristiques du joueur, les mouvements, les actions qu’il peux réaliser, ces interactions avec les autres joueurs …

L’avantage de cette séparation est qu’on aura la libérté de changer toute la partie graphique du jeu sans pour autant modifier tout le code.

Voici donc le code de ces deux objets :

CharacterSpr

» Afficher le fichier : CharacterSpr.js

'use strict';

var CharacterSpr = function(game, x, y, isCollisionEnabled) {
    Phaser.Sprite.call(this, game, x, y, 'sprites');
    if(isCollisionEnabled){
        this.enableCollision();
    }
    this.setupAnimations();
};

CharacterSpr.prototype = Object.create(Phaser.Sprite.prototype);
CharacterSpr.prototype.constructor = CharacterSpr;

CharacterSpr.prototype.enableCollision = function() {
    this.game.physics.enable(this);
    this.body.fixedRotation = true;
};

CharacterSpr.prototype.setupAnimations = function() {
    this.anchor.setTo(0.5, 0.5);

    this.animations.add('walk_down', [
        "walk/down/0.png",
        "walk/down/1.png",
        "walk/down/0.png",
        "walk/down/2.png"
    ], 60, true);
    this.animations.add('walk_up', [
        "walk/up/0.png",
        "walk/up/1.png",
        "walk/up/0.png",
        "walk/up/2.png"
    ], 60, true);

    this.animations.add('walk_side', [
        "walk/side/0.png",
        "walk/side/1.png",
        "walk/side/0.png",
        "walk/side/2.png"
    ], 60, true);

};

CharacterSpr.prototype.walkDown = function(){
    this.animations.play("walk_down",6,true);
};

CharacterSpr.prototype.walkUp = function(){
    this.animations.play("walk_up",6,true);
};

CharacterSpr.prototype.walkLeft = function(){
    this.scale.x = 1;
    this.animations.play("walk_side",6,true);
};

CharacterSpr.prototype.walkRight = function(){
    this.scale.x = -1;
    this.animations.play("walk_side",6,true);
};

CharacterSpr.prototype.stopAnimation = function(){
    this.animations.stop();
};

module.exports = CharacterSpr;
 

» Cacher

Vous remarquez que dans cet objet on se limite à définir les différentes animations à utiliser, toute la logique du jeu seras déléguée à l’objet CharacterObj.

CharacterObj

» Afficher le fichier : CharacterObj.js


'use stric';

var CharacterSpr = require('client/gameSprites/CharacterSpr');
var Pathfinder = require('client/utils/Pathfinder');


var CharacterObj = function(game, x, y, isMainPlayer) {
    this.configure(game, isMainPlayer);
    this.setupSprite(x, y);
    this.resetCurrentTweens();
};

CharacterObj.prototype.configure = function(game, isMainPlayer){
    this.game = game;
    this.isMainPlayer = isMainPlayer;
    this.moveDuration = 500;
    this.info = {};

    this.currentTweens = [];
    this.moving = false;
    this.tweenInProgress = false;
};

CharacterObj.prototype.setupSprite = function(x, y){
    this.sprite = new CharacterSpr(this.game, x, y, this.isMainPlayer);
    this.game.add.existing(this.sprite);

    this.sprite.walkDown();
};

CharacterObj.prototype.moveTo = function(targetX, targetY, pathReadyCallback){
    var me = this;

    Pathfinder.calculatePath(
        this.sprite.position.x,
        this.sprite.position.y,
        targetX,
        targetY,
        function(path) {
            if (pathReadyCallback !== undefined || typeof pathReadyCallback === "function") {
                pathReadyCallback(path);
            }
            me.onReadyToMove(me, path);
        }
    );
};


CharacterObj.prototype.onReadyToMove = function(me, listPoints){
    this.resetCurrentTweens();
    this.prepareMovement(listPoints);
    this.moveInPath();
};

CharacterObj.prototype.resetCurrentTweens  = function(){
    var me = this;
    this.currentTweens.map(function(tween){
        me.game.tweens.remove(tween);
    });
    this.currentTweens = [];
    this.moving = false;
    this.sprite.stopAnimation();
};

CharacterObj.prototype.prepareMovement = function(listPoints){
    listPoints = listPoints || [];
    this.currentTweens = [];
    var me = this;

    listPoints.map(function(point){
        me.currentTweens.push(me.getTweenToCoordinate(point.x, point.y));
    });

};

CharacterObj.prototype.getTweenToCoordinate = function(x, y){
    var tween = this.game.add.tween(this.sprite.position);

    x = (x * Pathfinder.tileSize) + Pathfinder.tileSize / 2;
    y = (y * Pathfinder.tileSize) + Pathfinder.tileSize / 2;
    tween.to({ x:x, y:y }, this.moveDuration);
    return tween;
};

CharacterObj.prototype.moveInPath = function() {
    if(this.currentTweens.length === 0){
        return;
    }
    var index = 1, me = this;
    this.moving = true;


    moveToNext(this.currentTweens[index]);


    function moveToNext(tween){

        index ++;
        var nextTween = me.currentTweens[index];
        if(nextTween != null){

            tween.onComplete.add(function(){
                me.tweenInProgress = false;
                moveToNext(nextTween);
            });
        }else{
            // if i am the last tween
            tween.onComplete.add(function(){
                me.onStopMovement();
            });
        }

        tween.start();
        me.faceNextTile(tween);

        me.tweenInProgress = true;
    }
};

CharacterObj.prototype.faceNextTile = function(tween){

    var isVerticalMovement = tween.properties.y == this.sprite.position.y;

    if(isVerticalMovement) {
        if(tween.properties.x &amp;gt; this.sprite.position.x){
            this.sprite.walkRight();
        } else {
            this.sprite.walkLeft();
        }
    } else {
        if(tween.properties.y &amp;gt; this.sprite.position.y){
            this.sprite.walkDown();
        } else {
            this.sprite.walkUp();
        }

    }
};



CharacterObj.prototype.onStopMovement = function(){
    this.resetCurrentTweens();

};

CharacterObj.prototype.setPosition = function(x, y){
    this.sprite.position.x = x;
    this.sprite.position.y = y;
};


CharacterObj.prototype.getInfo = function(){
  this.info.x = this.sprite.position.x;
  this.info.y = this.sprite.position.y;
  this.info.uid = this.uid;

  return this.info;
};

module.exports = CharacterObj;

» Cacher

Le role de cet objet est de gérer les interactions avec le joueur, pour le moment la seule intéraction est le mouvement, voyons ce qui se passe quand le joueur clique sur une case de la map :

  1. Appel de la fonction moveTo:on passe ici les coordonnées de la position du curseur de la souris au moment du click.
  2. Appel du pathfinder: calcul du chemin à utiliser en utilisant les coordonées actuelles de l’objet et les coordonnées de destination ( position de la souris ), quand le calcul est terminé on fait appel à la fonction onReadyToMove
  3. préparation du mouvement : le plugin pathfinder renvoi un tableau avec toutes les cases que le joueur devra traverser avant d’arriver à sa destination, il faut donc diviser le mouvement en un ensemble de déplacement entre tout les points composant le chemin, et ceci est fait dans la fonction prepareMovement qui va utiliser des tween pour configurer les mouvements du joueur.
  4. execution du mouvement : on appel finalement la fonction moveInPath pour executer le mouvement, ceci va déclencher les tween qu’on a configuré précédement.

Conclusion

Je termine ici la première partie de ce tuto assez chargé, j’ai essayé de ne pas trop rentrer dans les détails pour simplifier , je vais ajouter quelques liens utiles sur ce qui a été abordé dans cet article, évidement n’hésitez pas à poser vos questions et suggestions dans la section commentaires.

Dans la prochaine partie on va aborder la partie serveur et implémenter les méchaniques du jeux.

Le code source sur GitHub

Liens utiles :

Partie 2 : Création d’un serveur avec Socket.io

9 commentaires

  • Bravo pour ce travaille, j’ai enfin trouvé un bon tutoriel sur phaser en francais, j’attend avec impatience la suite !

  • sidney sissaoui

    Merci beaucoup pour ce tutoriel. Je me posais une question, est-ce-que Phaser est assez rapide pour faire tourner des applications nécessitant d’effectuer de nombreux calculs du coté client ? En testant la démo, je trouvais que la vitesse de déplacement du personnage est assez lente.

    • En fait Phaser n’est que du Javascript, donc la performance de phaser dépend surtout du navigateur web utilisé, en matière de jeux HTML5 il est préférable d’utiliser un navigateur récent et à jour (chrome par exemple), et surtout d’éviter Internet explorer 🙂

      Sinon pour ce qui est de la vitesse de déplacement je l’ai configuré exprès pour être lente 🙂 pour accélérer le mouvement du personnage il faut modifier le fichier CharacterObj.js et modifier la valeur suivante :

      this.moveDuration = 500; // correspond au temps qu'il va prendre pour bouger d'une cellule à une autre

      en diminuant cette valeur le mouvement devient plus rapide.

  • Costa Alexandre

    Depuis le temps que je voulais un petit tuto pour Phaser en français ! En tout cas super article, j’espère que tu continueras sur cette lancé ! J’attend avec impatiente la suite 😀

  • PhaserCoder

    Bonjour,

    J’ai commencé à lire le tutoriel, merci pour celui-ci ça donne une très bonne idée de ce que l’on peut faire en multijoueurs avec Phaser.

    J’ai néanmoins une question: comment est géré le require(‘client/game’).init(); présent dans index.html ?

    A priori il s’agit d’une fonction utilisée par express / nodejs, mais pas EcmaScript; pourtant en regardant le code du jeu qui tourne au chapitre suivant elle semble bien interprétée par le navigateur…
    Pourriez-vous m’expliquer comment cela fonctionne ? Y a t il une librairie qui permette de l’interpréter côté navigateur ?

    Je connais bien Phaser en revanche pas du tout ni nodejs ni express.

    • Oui alors le require viens du fait que j’utilise brunch.io durant mes développement, pour faire simple, brunch va ajouter un gros tas de Javascript pour que ton navigateur comprenne utilise le require utilisé dans NodeJS, je t’invite à regarder le fichier final lu par le navigateur, tu trouvera que tout les fichiers javascript sont regroupés dans 1 seul fichier, chaque fichier est alors concidéré comme un module par la fonction require. la ligne présente dans le fichier index.html appèle la fonction require générée par brunch lors de la compilation de mon code Javascript, j’espère que j’ai été assez clair, je t’invite à regarder mon article sur Brunch pour avoir une petite idée de comment ça fonctionne.

  • camille

    Bonjour,

    Pourriez vous faire des tuto pour creer un rpg solo avec phaser ?

    merci 😉

  • seb

    Un immense merci, j’attends la suite avec impatience

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *