Le JS13K est un concours annuel de développement organisé sur un mois. Le but est de développer un jeu faisant moins de 13kb une fois zippé sur un thème donné. J’avais participé à l’édition de 2015 avec Super peach world, et j’ai réitéré en septembre dernier pour la 6ème édition portant sur le thème « lost ».
Je souhaitais partager ici comment j’en suis arrivé au résultat final: « Lonely soul » . Je vous invite d’abord à tester le jeu (http://js13kgames.com/entries/lonely-soul), à le liker et partager si vous avez aimé.
Phase de recherche
Le thème « lost » peut être interprété de la façon que chacun souhaite. J’ai rapidement pensé à « lost soul », une âme en peine comme personnage. Un jeu nommé « suicide guy » a été testé par At0mium, où le but est de faire mourir son personnage dans ses rêves pour qu’il se réveille, et à chaque niveau trouver un moyen de mourir, donc de perdre. Je pensais donc partir sur un jeu de plateforme 2D où on incarnerait une âme en peine qui cherche un moyen de quitter cet entre-deux mondes.
Partant de cette idée j’ai cherché des idées de design. Pour rendre la mort moins violente j’ai pensé à un jeu en noir et blanc, avec les objets qui nous tueraient en rouge, un peu façon Madworld
Pour la détection des collisions j’ai réalisé il y a quelques mois un tuto pour gérer les collisions sur un canvas grâce à une couleur au pixel près : cela correspondant parfaitement à mon besoin.
Puis en cherchant quel décor je pourrais utiliser je suis tombé sur un décor de grotte en 2D, et je me suis demandé s’il était possible de générer ce genre de décor de façon aléatoire. Et en le faisant défiler j’ai gardé le principe d’un défilement automatique et d’un side scroller dans le genre d’un jeu auquel je joue souvent dans le train : Zombie tsunami
Gameplay
3C
Les 3C représentent les éléments de base du jeu, à savoir :
- Caméra : ici une vue extérieure de côté en 2D
- Character (personnage): le fantôme/âme
- Controller : la saut via la touche espace ou en touchant l’écran plus ou moins longtemps.
Mécaniques
- Des enchaînements de plateformes défilent sous le joueur
- Il doit sauter pour éviter les trous et pièges
- S’il tombe, il réapparaît en haut de l’écran en perdant de la vie, pour ne pas perdre la partie dès le premier saut râté
- S’il marche sur des pics, il perd de la vie à chaque instant passé dessus
- Il peut collecter des diamants en passant dessus
- Au fil du temps, le champ de vision se réduit et la zone autour s’obscurcit, jusqu’à ne plus voir que de très prêt, conduisant à une défaite inévitable
Rejouabilité
Il me semblait important d’avoir une mécanique de scoring et de rejouabilité afin d’accrocher les joueurs et qu’ils restent plusieurs parties sur le jeu. Durant les parties, le joueur collecte des diamants et peut les dépenser pour :
- prendre moins de dégâts sur les pics
- prendre moins de dégâts en tombant
- avoir la lumière qui dure plus longtemps
J’ai ajouté une fausse boutique pour avoir des diamants gratuitement et tester le jeu avec les améliorations sans avoir à jouer des heures quand même. Le tout en échange d’une demande de like ou de partage.
Optimisations
Dès le début j’ai tenu à m’occuper de l’optimisation du code. J’ai mis notre boilerplate en base pour la gestion des vues, du sass, du js, mais aussi pour profiter du live reload et de la minification qu’on utilise habituellement sur les sites.
Pour obtenir le code le plus optimisé possible il faut écrire en procédural, avec des fonctions, plutôt qu’utiliser des objets. Les fonctions et leur appel peuvent-être obfusqué facilement, les variables aussi; alors que le nom des méthodes et propriétés des objets ne peut pas l’être.
Pour les images le but est d’en générer un maximum en programmant, pour le reste de les optimiser puis les encoder en base 64 pour ne pas avoir de fichier séparé.
J’ai séparé le code dans plusieurs fichiers pour que tout le reste lisible, avec des require, mais le code compilé restait assez verbeux par tous les ajouts faits par require, ce n’était donc pas optimisé. J’ai donc ajouté une tâche locale dans gulp-tasks/ pour mon build pour juste concaténer mes fichiers JS en un seul et minifier le résultat.
J’ai ajouté une seconde tâche spécifique au JS13K pour faire un « inline » (inclure le JS et le CSS dans le fichier html directement plutôt que pointer sur des fichiers externes), et créé un zip du fichier en m’indiquant sa taille et une erreur si la taille limite autorisée est dépassée. Avec les images en base 64, le JS et le CSS simplifiés de la sorte, je n’ai au final qu’un fichier html qui tient sur une seule très longue ligne, parfait pour profiter d’un maximum d’espace pour coder!
Génération des assets
Pour faire un décor de grotte j’ai fait quelques recherches, pour trouver ce projet de grotte en 2D qui m’a fortement inspiré pour Lonely Soul. J’ai commencé à le coder en JS. Le principe a été de remplir un rectangle dans un canvas, puis passer le globalCompositeOperation en mode destination-out.
On dessine chaque moitié en déplaçant un peu la coordonnée x à chaque fois. Sur les premiers déplacements on déplace le y jusqu’à atteindre la hauteur souhaitée, ensuite le y prend une valeur aléatoire pour donner l’aspect crénelé. Pour le fond le plus en avant (à droite sur l’image ci-dessous), l’écart aléatoire est accentué au début et à la fin pour donner un aspect stalactite.
Les plateformes et les pics sont générés avec un procédé similaire.
Je génère 3 fois un motif aléatoire différent puis je les met bout à bout pour créer l’image de fond. Initialement j’avais prévu que les 3 images se déplacent en JS, puis quand la première image arrive hors de l’écran je la replace à la fin en générant un nouveau motif, pour éviter toute répétition durant les parties. Mais pour avoir un code plus simple j’ai simplement fait une animation CSS qui déplace le background et se répète à l’infini. Les 2 images se déplaçant à des vitesses différentes pour donner l’effet de parallaxe.
Au début, je voulais que le jeu s’accélère avec le temps, ça aurait été plus facile avec la solution JS, car en CSS il aurait fallu mettre à jour la durée de l’animation à chaque frame pour qu’elle augmente progressivement. Si l’on met à jour la vitesse d’un coup, on verra une saccade vu que la durée change en cours d’animation, ce qui décalerait l’image.
Création du personnage
Avec l’idée de départ de « tuer » son personnage je voulais qu’il soit plutôt mignon pour avoir un contraste entre le personnage et ce qu’il fait. Je cherchais dans un style Kirby, j’ai fini par dessiner ce petit fantôme.
Au départ ses yeux étaient sur l’image mais je les ai retiré pour les dessiner par dessus dans le canvas. Ce qui a permis d’ajouter le clignement des yeux, pour ajouter un peu d’âme au personnage. A intervalle régulier avec un modulo et pour une certaine durée, je dessine les yeux en petit ou en grand. C’est le genre de détail qui augmente la qualité d’un jeu.
J’ai par la suite ajouté une lanterne pour rendre crédible l’effet de lumière. J’ai essayé de dessiner une flamme à l’intérieur, mais trop petit pour être visuellement bien fait, j’ai juste mis un aplat de couleur jaune qui devient de plus en plus transparent alors que la lumière faiblit.
De gauche à droite : l’image dans le jeu, optimisée puis encodée en 64 bits pour prendre le moins de place. Puis le rendu en jeu, et à droite avec le clignement des yeux et la lanterne éclairant moins.
J’aurais aimé dessiner le personnage avec des formes en canvas, ça aurait sans doute été plus optimisé en poids, mais ça aurait surtout évité un élément en pixel art par rapport au reste qui était plutôt vectoriel, ce qui m’a été reproché par le jury d’ailleurs.
Déplacement
Le personnage reste toujours au même endroit de l’écran, il peut juste sauter et se déplacer sue l’axe des « y ». les plateformes défilent sous lui ce qui donne l’impression d’avancer. J’ai placé le personnage à 25% de l’écran et pas au centre, vu que les éléments déjà passés n’ont plus d’utilité de gameplay, et ne pas avoir 50% de l’écran inutile.
La gestion des sauts est plutôt basique :
- une force de gravité attire le personnage vers le bas quand il n’est pas sur une plateforme (GRAVITY dans player.js) à chaque frame
- quand le personnage commence un saut, on lui donne une impulsion (JUMP_IMPULSE dans player.js) qui va être réduite de la valeur de la gravité à chaque frame, jusqu’à retomber à 0 plus haut du saut avant de commencer à redescendre
- si le joueur laisse la touche de saut appuyée l’impulsion sera donnée à chaque frame jusqu’à atteindre une valeur maximale afin d’avoir des sauts plus ou moins longs
- si le personnage se retrouve en bas de l’écran je le renvoie en haut, pour éviter de mourir dès la première chute.
Je souhaitais avoir 3 niveaux de hauteur sur le jeu avec les sauts longs, et il fallu jouer sur l’impulsion et la gravité pour connaître la hauteur maximale des sauts pour que le personnage puisse aller sur ces niveaux facilement et ne sorte pas de l’écran. J’ai aussi dû gérer la vitesse de défilement du jeu pour que la distance parcourue lors d’un saut soit suffisamment grande, mais pas trop non plus pour pouvoir afficher les obstacles à venir.
Plateformes
Pour les plateformes la collision a été gérée assez simplement : juste quand le personnage tombe sur une plateforme. Il n’y a pas de collision sur les côtés ni vers le haut au moment où le personnage saute (ce qui permet au personnage de passer à travers une plateforme puis atterrir dessus).
Le jeu se compose de différents blocs qui contiennent des plateformes, des diamants et des pics. Pour les créer j’ai utilisé des tableaux qui contiennent la seconde de début, la seconde de fin et le niveau de la plateforme en y. Pareil pour les pics. Pour les diamants vu qu’ils correspondent à un point ils sont définis par le moment où ils sont situés en seconde et leur niveau en y.
J’ai fait en sorte que chaque bloc commence au même niveau, comme ça on peut les enchaîner aléatoirement sans crainte que le joueur soit obligé de tomber à la fin d’un bloc. 3 blocs s’enchaînent, et quand le premier est hors de l’écran on en génère un nouveau qui se place à la fin, permettant un enchainement à l’infini.
Pour positionner les éléments j’ai utilisé comme unité la seconde plutôt que des pixels, car la vitesse de défilement devant augmenter avec la durée de la partie, la taille des blocs aurait varié avec ce paramètre. La conversion et la création des blocs était plus simple de cette manière.
Level design
Pour que la mécanique du jeu soit prise en main rapidement, dès l’écran sur lequel on arrive on voit le décor défiler, et si on reste longtemps la lumière se réduit. Ca donne des indications au jour sur ce qu’il va lui arriver durant la partie.
La première plateforme est toujours la même. Le personnage tombe dessus vu qu’il est plus haut sur l’écran d’accueil, ce qui donne une idée de la vitesse de chute. Puis on a 2 séries de pièces à attraper, suivant la courbure d’un saut court et d’un saut long, ce qui indique au joueur qu’il peut varier la hauteur de son saut sans avoir à l’indiquer.
Améliorations
Beaucoup d’idées que j’ai eu au début ou pendant le dév n’ont pas été livrées dans la version finale, par manque de temps ou par peur de manquer de place si je les ajoutais.
L’idée principale de départ était d’être poursuivi par une faucheuse qui nous rattrape au fil du temps (l’image est toujours commentée dans mes sources), et on perd si elle nous touche. Cela aurait demandé de nombreux ajouts :
- ajouter une barre pour voir notre avance par rapport à la faucheuse.
- ajouter un bouton pour faire un sprint pendant quelques temps, mais pour mobile ça aurait été compliqué de pouvoir taper n’importe où pour sauter et sur un bouton pour le sprint
- des bonus dans la boutique pour avoir plus de distance d’avance au début de la partie ou augmenter la réserve de sprint
- ajouter un effet visuel de sprint et l’effet sur la vitesse de défilement
Je souhaitais également augmenter la vitesse du jeu au fil du temps. Le code était déjà prévu pour ça, mais pour l’équilibrage du jeu il aurait fallu passer plus de temps pour gérer la courbe de difficulté. En l’état, avec la lumière qui se réduit, la courbe de difficulté est déjà correcte pour avoir des parties entre 30 et 90s suivant les bonus utilisés.
A vous de jouer maintenant! N’hésitez pas à partager votre meilleur score.
Et rendez-vous en septembre pour la prochaine édition !