Make Something Horrible (3)

Make Something Horrible est une Game Jam organisée régulièrement par le magazine papier (et Web maintenant) Canard PC. Le concours consiste à créer un jeu original, surtout drôle et forcément laid car créé par des gens sans talents graphiques. Moi compris ! Cette année j’ai décidé de participer en ayant comme objectif secondaire la découverte de certaines technologies et la mise en pratique d’architectures. Voici le quatrième chapitre.

Galères et bugs

Alors dans les chapitres précédents, j’expliquais que je voulais développer le back-end en C++. Malheureusement, j’ai eu des gros soucis de programmation Socket sous Windows (alors que sous Linux ça fonctionne au poil). Bon, pour ne pas perdre trop de temps dans le développement, je suis passé à Electron.js : le back-end se fera donc sous Node.js.

J’ai développé un serveur HTTP vite-fait bien fait, sans utiliser de framework pour éviter les dépendances, uniquement chargé de répondre aux requêtes sur une API REST.

On construit les cartes

Attaquons nous maintenant aux cartes. Côté serveur, je dispose d’un fichier JSON flexible listant toutes les cartes du jeu. Voici l’exemple du carte, modélisée par les paramètres suivants :

{
    "title": "Tente",
    "category": "defense",
    "picture": "tent",
    "text": "Planter sa tente marque le territoire du Campeur. You shall not pass, comme dirait l'autre.",
    "value": "1"
}

Là, on entre dans le coeur du Game Play : forcément, les paramètres ont un impact sur les règles du jeu, pas encore totalement définies à ce moment là du développement. J’ai décidé de rester simple :

  • On définit trois catégories : défense (à jouer sur le plateau), attaque (à jouer contre l’adversaire ou le plateau) et bibine (pour remplir son niveau d’énergie)
  • Une carte a une valeur, un chiffre dont la signification dépendra de la catégorie
  • Le reste c’est du graphisme : un titre, un texte et une illustration

Normalement, je ne devrais pas passer beaucoup de temps dans le moteur de jeu, mais je ne sais absolument pas ce que donnera le jeu en terme de “jouerie” : facile ? difficile ? Ennuyant ? C’est ma première GameJame, je vous avoue que je n’avais pas anticipé cette crainte, de devoir attendre très tardivement dans le développement du jeu pour … jouer !

Au niveau du front-end, je dispose déjà d’un composant Card.js bien séparé (règle d’or : dans le doute, toujours créer un composant dédié !!). La vue générale passera l’objet JSON regroupant les propriété d’une carte à chaque carte affichée. Le composant Card.js se chargera de gérer l’affichage au bon endroit !

image

On construit la grille

Attaquons nous maintenant à l’autre élément central qui n’existe toujours pas : la grille repréentant le Camping. Normalement, cela devrait aller vite car j’ai déjà passé du temps sur le moteur de ‘drop’ générique. Je vais donc définir N rectangles.

Astuce : commencez par dessiner un rectangle en affichant ses contours, puis positionnez le au pif, ajustez les tailles et les positions. Ensuite, constuisez dans la fonction ‘created()’ du composant GameView.js la matrice en mémoire :

// Create the grid
for (let i = 0 ; i < 9; i++) {
    for (let j = 0 ; j < 3; j++) {
    this.grid.push( {
        state: 0,
        camp: "transparent",
        x: 765 + i*123,
        y: 190 + j*175
    });
    }
}

Pour chaque case, on maintient un statut (la valeur de défense de l’emplacement) et à qui appartient l’emplacement. Ici, astuce, on va utiliser cette propriété pour afficher la couleur du rectangle :

  • transparent : l’emplacement est à personne
  • blue : l’emplacement est au joueur humain
  • red: l’emplacement est au joueur adverse (ordinateur)

L’affichage de la grille est réalisée en utilisant le système de template efficace de Vue.js :

  <template v-for="g in grid">
    <rect ry="10" :x="g.x" :y="g.y" width="110" height="155" v-bind:style="{ fill: g.camp }" class="droppable"/>
    <text :x="g.x + 50" :y="g.y + 70" 
        font-family="Badaboom" 
        font-size="40">
        
    </text>
  </template>

Bingo c’est terminé ! Chaque carré aura une classe “droppable” comme vu au chapitre précédent.

image

On filtre les drop

C’est bien beau, mais pour le moment tous les zones de drop s’affichent. On va filter selon le type de carte sélectonnée. Pour cela, nous allons utiliser la propriété ‘data-xxx’ de chaque zone et la comparer avec le type de carte.

Nous modifions la fonction de sélection de zone en ajoutant l’index de la carte en cours de drag :

app.isSelected(d3.event.x, d3.event.y, parseInt(c.attr('index')));
//...
// On vérifie si la carte peut être jouée ici:
if (type == app.cards[card_id].category) {
    accept_drop = true;
} else if (type == 'trash') {
    accept_drop = true;
}

Et plus loin on teste donc le nom de cette zone par rapport à la propriété de la carte (le deck du joueur étant toujours accessible dans les data() du composant Vue.js). On accèpte toutes les cartes dans la poubelle du Camping, bien évidemment. Notre belle fonction se termine par retourner non seulement ce statut de drop autorisé ou non mais aussi la destination choisie par le joueur : joueur, grille, poubelle ou adversaire.

Côté serveur, on initialise le jeu et le protocole

Nous allons maintenant commencer à déclarer toutes les variables de notre jeu côté serveur : la grille, le niveau de Pastis de chaque joueur, les cartes de chaque joueur.

Côté protocole réseau, on reste dans le simple :

image

Pour chaque carte jouée par le joueur humain, on met à jour les différentes variables, on fait jouer l’IA et on renvoit tout le status du jeu : la grille, les cartes et les niveaux de Pastis.

Le front-end, à la réception, copiera ces statuts en local (section data()) et grâce au système réactif de Vue.js on n’a rien à faire, le framework va répercuter graphiquement tous les changements.

On s’évitera également côté serveur tout un tas de vérifications déjà bloquées par le front-end, on va rester simple et efficace pour cette Game Jam ; évidemment, dans le cadre pro ne faites pas ça …

Regardoons ce qu’il se passe dans le cas où l’on joue une carte dans la poubelle :

// On joue cette carte (pas de vérification côté serveur on est en Game Jam :)
if (destination == 'trash') {
    // on vire cette carte et on en tire une autre
    player_cards.splice(index, 1);
    player_cards.push(cards[getRandomInt(cards.length)]);
}

Et hop on revoie tout au client :

// On renvoit un objet contectant tout le statut du jeu que le front-end mettra à jour graphiquement
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify({ grid: grid, cards: player_cards }));

Là par exemple, on ne gère toujours pas le niveau de Pastis :(

On met à jour la grille

On continue sur notre belle lancée. Maintenant, si le joueur joue sur la grille on va la mettre à jour. La règle est la suivante :

  • Une carte défense jouée sur un emplacement vide : ok, on le prend
  • Une carte défense jouée sur un emplacement à nous : la valeur de l’emplacement augmente d’autant
  • Une carte attaque jouée sur un emplacement adverse : la valeur de l’emplacement réduit d’autant
  • Une carte attaque n’est valide que sur un emplacement pris par l’adversaire (le rouge dans notre cas)

On effectue une vérification locale pour filtrer les zones de drop acceptées ou non, puis lorsque la carte est lâchée (fin du drop), on l’envoie au serveur.

Côté serveur, on effectue la même vérification, on s’arrange et on anticipe : notre fonction de vérification de la carte jouée aura en paramètre le camp qui joue : camp rouge (joueur adverse, l’ordinateur) ou bleu (joueur humain). On réutilisera cette fonction lors du développement de notre IA.

Notre fonction de dialogue avec le serveur est assez simple : on utilise une Promesse, on envoie la carte jouée et on reçoit une mise à jour de tous les éléments du jeu que l’on va recopier en local. Vue.js fait le reste et met tout à jour graphiquement.

dropCard(dest, card_idx) {
    // On envoie au serveur notre carte jouée, en retour on reçoit la conséquence et le jeu de l'adversaire
    Api.sendCard({card_idx: card_idx, dest: dest }).then((action) => {
        console.log("Received: " + JSON.stringify(action));

        // Update the grid
        for (let i = 0 ; i < 9*3; i++) {
            this.grid[i].camp = action.grid[i].camp;
            this.grid[i].state = action.grid[i].state;
        }

        // Update the player's cards
        this.cards = action.cards;
    });
},

Voilà le résultat pour l’équipe Bleue, on commence à avoir du Game Play !!

image

On met à jour le niveau de Pastis

Là c’est tout simple, on a déjà l’aspect graphique de développé. Il suffit de communiquer avec notre composant enfant et lui envoyer la nouvelle valeur du niveau de Pastis :

Parent :

this.$refs.bibineBlue.updatePastisLevel(action.bibine); // le joueur humain, le bleu
this.$refs.bibineRed.updatePastisLevel(action.opponent); // le joueur ordi, le rouge

Enfant :

updatePastisLevel(newLevel) {
    // newLevel entre 0 et 10
    newLevel = newLevel * 10;
    this.pastisLevel = 100 - newLevel; // ça fonctionne à l'envers
},

On fait jouer l’adversaire

Concernant l’adversaire, on va se contenter d’une seule IA pour le moment, une IA très agressive :

  • On scanne nos cartes en main
  • Si le niveau de Pastis de l’adversaire est non nul et que l’on peut l’attaquer, on le fait
  • Sinon on attque un emplacement, si on a de quoi

Si rien de tout cela fonctionne :

  • On remplit notre bibine
  • Sinon on ajoute un emplacement aléatoire (en privilégiant le nombre)

Et si rien ne va, on envoie une carte à la poubelle.

Si j’ai le temps, j’améliorerai l’IA avec quelques subtilités :)

Condition de victoire

Pour tester la condition de victoire, je fais mal jouer l’IA : elle se contentera de jeter la première carte venue à la poubelle ce qui me laissera le champs libre pour gagner et tester la victoire.

Lorsque toutes les cases de la grille sont remplient d’une couleur, le jeu s’arrête et on affiche une popup de victoire ou de défaite.

L’algorithme est simple, on écrit une fonction qui teste si le camp passé en paramètre est gagnant, que l’on appelle après que chaque joueur ait joué :

function hasWon(camp) {
  let won = false;

  let count = 0;

  for (let i = 0 ; i < grid.length; i++) {
    if (grid[i].camp == camp) {
      count++;
    }
  }

  if (count >= (9*3)) {
    won = true;
  }

  return won;
}

// ...
playCard(action, 'blue');

if (hasWon('blue')) {
    game_result = 'victory';
} else {
    // On fait jouer l'ordinateur
    playComputerIA();
}

if (hasWon('red')) {
    game_result = 'lost';
}

Conclusion et reste à faire

Nous voilà dans la dernière ligne droite du concours. Il reste une semaine avant la dead-line. Vu que cette version est jouable, je décide de la diffuser en beta-test sur itch.io. Au pire, si tout se passe mal, c’est cette version qui sera jugée. Si je parviens à faire mieux, alors tant mieux !

Ce qu’il reste à faire d’ici une semaine :

  • Déclencher le coup spécial
  • Ajouter du du son (son du coup spécial, musique d’ambiance, son poubelle)
  • Ajouter les portrait des joueurs
  • Faire quelques cartes supplémentaires
  • Ajouter le menu d’aide avec l’explication des cartes

S’il me reste du temps :

  • Faire parler les joueurs avec une bulle style BD
  • Encore plus de carte
  • Des effets sur les cartes lors des drops
  • Choix du coup spécial au démarrage de la partie
  • Améliorer la Popup de Victoire avec une image sympa :)

Make Something Horrible (3)

Make Something Horrible est une Game Jam organisée régulièrement par le magazine papier (et Web maintenant) Canard PC. Le concours consiste à créer un jeu original, surtout drôle et forcément laid car créé par des gens sans talents graphiques. Moi compris ! Cette année j’ai décidé de participer en ayant comme objectif secondaire la découverte de certaines technologies et la mise en pratique d’architectures. Voici le troisième chapitre.

Empaquetage et distribution

Pour le moment, nous n’avons que des pages web propulsées par Vue.js. Il faut quand même lancer un serveur pour que les pages s’affichent. Pour distribuer le jeu sous la forme d’un exécutable nous allons avoir besoin de fournir un tel serveur et un navigateur. Je pourrais utiliser Electron, qui est fait pour ça, mais il est très connoté Node.js. Moi, je suis avant tout un programmeur embarqué c/C++ donc mes back-ends je les fait en C moi môssieur.

Mes outils :

  • Gulp (optionnel) qui permet de regrouper les fichiers Javascript et éventuellement de les compresser (uglify)
  • Qt pour le navigateur et un peu tout ce qui concerne le système
  • Et on zippe le tout hein,on va pas s’embêter avec un installeur sauf si je m’ennuie.

Etape 1 : empaquetage du Web

On commence avec Gulp. Le but ici va être de générer un répertoire de livraison nommé “dist” qui va contenir tous les Javascript ; d’une part les librairies externes, celles-ci seront seulement copiées, et une librairie applicative contenant tous nos fichiers Vue.js amalgamés et compressés. On parle ici de compression au sens Javascript, c’est à dire que c’est une compression textuelle : les variables sont remplacées par des lettres uniques, les espaces sont supprimés etc. Le but sera de réduire la taille de téléchargement. Le serveur Web lui rajoutera une couche de compression binaire, Gzip.

Tout d’abord, on install gulp et les plug-ins utilisés par notre script :

npm install gulp -g
npm install

Puis, on invoke dans un stript nos tâches gulp :

gulp bmen-lib
gulp bmen-css

Deux fichiers vont être générés dans /dist : bmen.min.js et bmen.min.css. Maintenant, on va utiliser un autre script, en Perl cette fois, pour générer une arborescence de serveur de fichiers :

perl ./embed.pl dist images i18n fonts sounds index.html favicon.ico > src/embedded_files.c

Allez hop, on dump tous les fichiers et répertoires nécessaires à notre WebApp pour pouvoir fonctionner, ce qui génère un GROS fichier .c contenant des tableaux en “const char …”. La fin du fichier liste toute notre arborescence virtuelle, la taille et le type de chaque fichier.

static const struct embedded_file {
  const char *name;
  const char *mime;
  const unsigned char *data;
  size_t size;
} embedded_files[] = {
  {"/dist/bmen.min.js", "application/javascript", v0, sizeof(v0) - 1},
  {"/dist/style.min.css", "text/css", v1, sizeof(v1) - 1},
  {"/images/background.svg", "image/svg+xml", v2, sizeof(v2) - 1},
  {"/images/card.svg", "image/svg+xml", v3, sizeof(v3) - 1},
  {"/images/cover.png", "image/png", v4, sizeof(v4) - 1},
  {"/images/logo.png", "image/png", v5, sizeof(v5) - 1},
  // ...
};

Attention à bien séparer les répertoires contenant les librairies tierces, les sources brutes (images, sons) et le code source de votre site proprement dit.

Etape 2 : Qt à la rescousse

Nous sommes prêts pour la seconde étape qui consistera à compiler tous ces fichiers C dans une application executable. On utilisera donc la librairie Qt qui dispose d’un composant QWebEngine : il s’agit du moteur de rendu Chromium embarqué à la sauce Qt, c’est-à-dire avec une API génial et simple d’utilisation ainsi que les passerelles nécessaires pour partager des données entre QWebEngine et le reste des classes Qt.

On ajoute au projet un serveur de fichier, notre fichier généré contenant un système de fichier “virtuel”.

Le code QML est quant à lui assez léger, il se contente d’instancier une vue Web et de charger l’adresse locale du serveur :

Window {
    width: 1024
    height: (width*9)/16
    visible: true

    WebEngineView {
        id: webView
        anchors.fill: parent
        url: "http://127.0.0.1:8081"
    }
}

Côté serveur en C++ : on démarre un serveur HTTP et on sert les pages web embarquées précédemment sous forme de fichier .c. Nous créons une classe appelée “BMen” qui sera le coeur de notre back-end.

  BMen bmen;
  tcp::TcpServer tcpServer(bmen);

  if (bmen.Initialize())
  {
      if (tcpServer.Start(100, true, 8081, 8083))
      {
          bmen.Start();
      }
  }

  QQmlApplicationEngine engine;
  engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

Voilà le résultat, nous avons une fenêtre seule contenant le navigateur et le code back-end, le tout dans un exécutable (avec ses DLL autour, ce qui n’est pas un détail avec Qt).

image

Nous n’allons pas plus loin pour le moment, nous verrons par la suite comment, toujours avec Qt, facilement distribuer notre application.

On continue le front-end : vive les drop

Nous pouvons effectuer un “drag”, essayons maintenant de détecter un “drop” ; nous allons définir des zones sensibles sur lesquelles le joueur pourra interagir avec sa carte.

Nous allons développer un code générique : pour transformer n’importe quel composant SVG en zone “draggable”, on va :

  1. Le définir en tant que classe ‘draggable’
  2. Au démarrage, scanner tous les composants ayant cette classe, puis créer un rectangle SVG par dessus
  3. Lorsque le drag est en cours, utiliser les coordonnées du curseur de la souris pour détecter si l’on est au dessus d’une telle zone
  4. Si oui, alors on change une propriété de type ‘data-xxxxx’ du rectangle en question
  5. Un CSS dédié à cette propriété permettra de mettre en surbrillance cette zone

Exemple pour la poubelle, que l’on définit en tant que draggable :

<Trash
    x="200" 
    y="400"
    class="droppable"
  >
</Trash>

Le code au démarrage, création des carrés en surbrillance, avec un data-state vide par défaut :

d3.selectAll(".droppable").each(function(d,i) {
  let rect = d3.select(this);
  d3.select('#mainsvg')
    .append('rect')
    .attr('x', rect.attr('x'))
    .attr('y', rect.attr('y'))
    .attr('width', rect.attr('width'))
    .attr('height', rect.attr('height'))
    .attr('class', 'drop-area')
    .attr('data-state', '');
});

Le code CSS correspondant, lorsque le curseur au dessus de la zone est détecté, on affiche un contour et un remplissage semi-transparent :

.drop-area {
  fill: transparent;
  stroke-width:0;
}

.drop-area[data-state='ok'] {
  fill:rgba(255,255,255,0.5);
  stroke-width:10;
  stroke: green;
}

Maintenant, le code de détection : nous appelons une fonction classique de détection de collision à chaque fois que la souris est bougée :

.on("drag", function () {
        d3.select(this)
            .attr("x", d3.event.x + deltaX)
            .attr("y", d3.event.y + deltaY);
        app.isSelected(d3.event.x, d3.event.y);
    })

isSelected: function(x, y) {

  d3.selectAll(".drop-area").each(function(d,i) {
    let rect = d3.select(this);
    let xmin = parseInt(rect.attr('x'));
    let ymin = parseInt(rect.attr('y'));
    let xmax = xmin + parseInt(rect.attr('width'));
    let ymax = ymin + parseInt(rect.attr('height'));
    
    if ((x >= xmin) && (x < xmax) && (y >= ymin) && (y < ymax)) {
      console.log("Detected !!");
      d3.select(this).attr('data-state', 'ok');
    } else {
      d3.select(this).attr('data-state', '');
    } 

  });

}

Et voilà notre super détection de zone en action :

image

C’est tout pour cette fois-ci, au prochain numéro !

Make Something Horrible (2)

Make Something Horrible est une Game Jam organisée régulièrement par le magazine papier (et Web maintenant) Canard PC. Le concours consiste à créer un jeu original, surtout drôle et forcément laid car créé par des gens sans talents graphiques. Moi compris ! Cette année j’ai décidé de participer en ayant comme objectif secondaire la découverte de certaines technologies et la mise en pratique d’architectures. Voici le second chapitre.

Quelques ajustements et tests

Jusqu’à présent, nous n’avons pas fait grand chose mais cela n’empêche pas d’avoir des problèmes. Le premier a été de tester le jeu avec le navigateur Chrome, test cross-browser que tout développeur Web doit réaliser. Evidemment, le menu ne fonctionna pas. Après quelques recherches, j’ai pu déterminer les causes ce qui a rendu le code plus simple et plus propre.

Autre ajustement, j’ai ajouté une musique d’ambiance sur la page d’accueil. J’ai puisé sur le site opengameart.org qui possède tout ce qu’il faut pour le développeur amateur de jeu. J’ai converti le fichier OGG au format WebM, ce qui a réduit la taille de moitié en plus de rendre la chose plus multiplateforme, a priori.

image

Ensuite, j’ai ajouté un favicon.ico à la racine, les navigateurs en font par défaut la requête. Pour l’icône, j’ai là encore fait fans le rapide, une capture d’écran de la lettre ‘B’ du titre, arrangée sur Paint.net au niveau du remplissage et de la taille. Plus qu’à utiliser un convertisseur en ligne pour générer un .ico.

Phase 2 : le gameplay prend forme

Donc, c’est un jeu de cartes. Avant de créer les cartes proprement dites, je souhaitais dessiner un peu le plateau de jeu pour me fixer les idées sur les mécaniques de jeu qui n’existent toujours pas à l’heure où j’écris ces lignes. Dans tous les cas, il faut simplifier les choses au maximum car tout élément dynamique ou interactif nécessitera du code derrière …

Un jeu de cartes à la Magic ? Oui, pourquoi pas, mais l’aspect camping est alors relayé au second plan. Je lance Inkscape en ayant dans l’idée de créer un décors camping. Uniquement un décors, sans interaction. Je récupère un plan de camping en SVG, vous savez celui avec les emplacements numérotés et les blocs sanitaires ?

image

Oui, ça peut faire une bonne illustration, mais alors l’image est un peu chargée, avec les cartes par dessus … puis je trouve que les emplacements pourraient faire un bon jeu de bataille de territoire, à la Crusader King. Mais oui ! Je tiens le truc, on pourrait faire un jeu de gestion carrément complexe avec gestion des ressources pour chaque emplacement … mais je m’emballe. On va simplifier la carte et créer une grille d’emplacements, un peu un camping imaginaire.

image

Et voilà, je crée au pif un certain nombre d’emplacements, je ne sais pas si ça va tenir la route …

Au niveau du code, je m’imagine déjà une matrice en mémoire représentant cette grille. Une carte sera mise sur une case par un glissé-déoposé.

Pour simplifier encore, et réutiliser le glisser déposé, j’imagine deux autres zones : une poubelle (la poubelle du camping bien sûr !) permettant de défausser une carte et une “réserve de Bibine”, équivalent à une réserve de Mana. Si une carte est envoyée dessus, la réserve de Bibine augmente. Une fois le maximum atteint (représenté par un verre de Pastis bien entendu), le pouvoir unique du joueur pourra se déclencher.

On résume :

  • Tout fonctionne par drag and drop, avec une librairie comme Draggable.js ou même D3.js ça peut être super simple
  • Plusieurs types de cartes :
    • Des cartes “Bibine” pour recharger
    • Des cartes “Emplacement” à poser sur une case et restent en place le temps qu’il faut
    • Des cartes “Coups spéciaux”, événement éphémère

La fin de la partie : lorsque le joueur a conquis tous les emplacaments. Oui, ça me plaît, à la fois simple dans l’action, mais compliqué dans la gestion des cartes, il va falloir faire un système bien générique et pouvoir faire des cartes rapidement, à la volée. J’imagine un fichier de configuration listant toutes les cartes, type JSON.

L’IA

Franchement, j’ai la flemme de créer une IA, et en plus je n’y connais rien. Je pense que dans un premier temps, le joueur adverse jouera au hasard, et si j’ai du temps j’ajouterai quelques if. J’essaierai de tricher pour rendre l’IA plus dure : elle connaitra les cartes en main du joueur humain.

Dans tous les cas, il faudra se réserver du temps pour cette partie

Création du HUD

Maintenant que nous savons à peu près où nous allons, ajoutons quelques éléments graphiques. Il nous faut :

  • Un menu permettant de quitter le jeu et revenir au menu principal (réutilisation du composant MenuItem.js)
  • Une jauge de Bibine, avec le portrait et le nom de nos héros du jour (le joueur humain et l’ordinateur)
  • La poubelle (défausse)

La jauge de Bibine sera un verre de Pastis bien connu qui se remplira au fur et à mesure de certaines cartes jouées. Aux choix du joueur : soit il remplit sa jauge pour espérer arriver à 100% et déclancher son pouvoir spécial, soit il joue sa carte ailleurs (défausse, sur le camping, même sur l’adversaire pourquoi pas ?).

Pour cela, j’ai besoin d’une image SVG dont je remplierai le contenant par du fameux contenu jaune anis. Je prends une photo sur Internet que je détoure avec deux mains gauches :

image

Ok, passons maintenant un peu au code parce que c’est marrant. On va créer un nouveau composant, appelé PlayerIcon.js. Ce composant graphique contienra :

  • L’image d’un verre de Pastis, une fois rempli il déclenchera le pouvoir spécial
  • Le portrait du joueur et son nom

Les éléments graphiques sont tous du SVG. Concernant le verre de Pastis, je donne un nom au chemin représentant l’intérieur du verre et lui associe un clipping :

<!-- En faisant varier y de 100 (verre vide) à 0 (verre plein), on monte le niveau de lique en faisant monter le cliping -->
<clipPath id="pastisClip">
   <rect x="0" :y="pastisLevel" width="100" height="158" style="fill: white; stroke-width: 0"/>
</clipPath>

L’explication est donnée, y’a plus qu’à faire varier la variable du composant Vue.js pour faire bouger le niveau de pasis.

J’instantie deux fois ce composant, bleu étant la couleur du joueur humain, rouge étant la couleur de l’adversaire :

  <PlayerIcon 
    x="100" 
    y="800"
    color="blue">
  </PlayerIcon>

  <PlayerIcon 
    x="1400" 
    y="800"
    color="red">
  </PlayerIcon>

Et voici le résultat. Je ferai évoluer plus tard le composant pour y ajouter le portrait du joueur et son nom, plus certains effets graphiques. Pour le moment, j’ai le minimum pour prototyper la deuxième phase du game play : les cartes.

image

Modélisation des cartes

L’atout (!) principal du jeu n’est pas encore modélisé : les cartes ! Voyez-vous, je ne suis pas un expert et j’ai probablement fait une erreur de débutant en tant que développeur de jeu vidéo. J’ai commencé par le menu principal alors que je pense que dans un studio réel, c’est le genre de partie qui est effectuée que bien plus tard.

Reste que développer le menu m’a permis de tester certaines technologies (SVG, l’intégration avec Vue.js, le son …). Bref, développer seul change un peu la stratégie de développement.

Bon, une fois de plus, sortons Inkscape. Je ne sais pas encore quelle sera la taille d’une carte, mais j’imagine que le joueur aura en gros 5 cartes en main en permanence. On peut par exemple avoir la séquence de jeu suivante :

  1. Le joueur tire une carte de son deck (ah je viens de penser que l’on ne voit pas de deck sur le plateau … c’est pas grave, on n’a pas le temps, la carte apparaîtra magiquement)
  2. Il doit jouer une carte à destination :
    • du terrain de camping
    • de la poubelle
    • de lui
    • de l’adversaire

Donc, 5 cartes sur une largeur de … 1920 moins deux blocs HUD de 400 de large plus les marges de chaque côté cela nous laisse environ 800 de large pour toutes les cartes soit 160 pixels pour une carte, si on ne les chevauche pas. Au niveau des proportions, on copie la proportion des cartes existantes, 65mm x 100mm.

On copie-colle le code SVG dans le template Vue.js, et on instancie 5 cartes en bas de l’écran.

   <Card x="550" y="800"></Card>
   <Card x="700" y="800"></Card>
   <Card x="850" y="800"></Card>
   <Card x="1000" y="800"></Card>
   <Card x="1150" y="800"></Card>

image

Hou ! Les belles cartes !

Le drag & drop

Bon, je gameplay central de notre jeu se base sur le drag et le drop des cartes. On va tester cela, pas besoin de nouvelle librairie, D3.js sait tout faire !

Je prends le code à partir d’un petit tutorial sur Internet, a priori il n’y a pas grand chose à faire. J’autorise tous les tags qui ont la class ‘card’ à être draggable.

    let deltaX, deltaY;
    let dragHandler = d3.drag()
    .on("start", function () {
      let current = d3.select(this);
      deltaX = current.attr("x") - d3.event.x;
      deltaY = current.attr("y") - d3.event.y;
    })
    .on("drag", function () {
        d3.select(this)
            .attr("x", d3.event.x + deltaX)
            .attr("y", d3.event.y + deltaY);
    });

    dragHandler(d3.selectAll(".card"));

Trop simple, je peux maintenant bouger toutes les cartes sur le plateau de jeu :

image

Et le ‘drop’ ? Nous verrons cela au prochain numéro !

Conclusion

Voilà nous avons terminé pour cette partie. Evidemment, rien n’est totalement finalisé, mais nous avons tous les objets de base au niveau graphisme pour avancer dans le jeu de la carte proprement dit. Pour faire bien BD, nous ajouterons à la fin des bulles pour faire parler nos personnages et leur faire dire des bêtises. On laisse tout le rafinement pour la fin, ne pas oublier qu’un mois c’est court pour faire un jeu complet.

Au prochain numéro, nous nous attaquerons au code qui ne se voit pas : le moteur de jeu. On fera donc du C++, du réseau, bref c’est la rigolade.