Make Something Horrible (5)

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 cinquième et dernier chapitre.

Finitions et ajustements

La première livraison en “démo” d’un jeu est importante : vous peaufinez vos scripts de livraison et surtout cela vous permet d’avoir des premiers retours utilisateurs. Pour ma part, j’en ai eu plusieurs par le biais du forum de Canard PC, dans la section dédiée à la Game Jam. Très bien, il me reste une semaine, avec ces remarques et ma TODO list du précédent article j’ai de quoi remplir mes soirées.

Pouvoir spécial

Je savais qu’il manquait une chose essentielle : le pouvoir spécial de chaque joueur. Voici ce que j’ai ajouté : avant de commencer une partie, le joueur choisit son pouvoir spécial parmi trois :

  • Tempête sur le Camping : effacez des emplacements adverses
  • Mal de tête : bloquez le joueur adverse pendant plusieurs tours
  • Pétanque Master : videz la Bibine adverse

image

Le développement est assez simple côté serveur.

Effets visuels et sonores

Maintenant que l’architecture est stable, je peux ajouter des effets pour rendre le jeu plus sympatique. Je souhaitais ajouter des effets graphiques. Je pourrais le faire en SVG, mais je sais que les jeux HTML5 sont plus souvent réalisés en utilisant la balise ‘canvas’. Donc, j’aurais plus de chance de trouver du code source d’exemple d’effets graphiques.

J’ajoute donc une couche au sandwich HTML avec une balise canvas prennant tout l’écran.

image

Comme d’habitude, je crée un composant pour cela ce qui permet de découpler un peu les choses. Le premier effet que je mets est la puie dans le cas de l’enclenchement du pouvoir “Tempête”. Je récupère du code (libre) sur Internet et l’effet est lancé en parallèle avec un effet sonore de tempête. C’est l’effet sonore qui arrêtra l’effet graphique, toujours à l’aide de la librairie Howler.js :

if (action.effects[e] == 'rain') {
    this.$refs.canvasLayer.startRain();
    new Howl({
        src: ['sounds/rain.webm'],
        autoplay: true,
        onend : () => {
        this.$refs.canvasLayer.stopRain();
        }
    });
}

Efficace et simple. Pour information, c’est le serveur qui envoie l’ordre d’effet dans un tableau d’événements liés aux actions effectuées par la carte envoyée.

Ajustements de GamePlay

Pour rendre la jouerie plus sympa et moins longue, j’effectue les modifications suivantes :

  • Limite des emplacements à 5 points
  • L’IA choisit sa défense aléatoirement
  • Stratégie IA aléatoire (choix stratégiques)

Ainsi, les emplacements sont plus facilement prenables et moins défendable, pour le coup. Egalement, cela permettra de créer des cartes plus fortes, genre une attaque à 5 points, mais plus rares !

La modification fondamentale de GamePlay a été de complexifier la carte du Camping. En effet, en l’état, il n’y a pas de dimension stratégique : on conquiert les emplacements où l’on veut.

Je décide donc d’autoriser la pose des cartes uniquement sur des cartes adjacentes (horizontales ou verticales) à d’autres cartes du même camp (sauf le premier coup, bien entendu !!).

Evolutions graphiques

Attaquons nous maintenant à quelques ajouts graphiques finaux. Dans un premier temps, j’améliore la Police de caractères utilisée pour les nombres du Camping ; plus grosse et plus explicite, surtout entre le chiffre 1 et 7. Merci le forum Canard PC ! Nous avons maintenant par exemple le portrait des joueurs.

De nouvelles cartes sont développées à l’arrache pour rendre le jeu plus sympa.

Conclusion

Le compte à rebours est terminé, il est temps de livrer la version finale du jeu, non sans défauts. Il n’est pas très sympa à jouer à vrai dire, notamment parcequ’il est compliqué de le terminer. Bon, j’ai tout de même eu du plaisir à le développer, j’ai pu tester la technologie Web avec satisfaction pour créer un jeu en “local”. Bonne chance à tous pour cette GameJam !

Make Something Horrible (4)

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

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 !