Écrit par Victor D.
Un jeu en CSS ? mais ça n’a aucun sens !
Une fois n’est pas coutume, nous allons aujourd’hui repousser les limites des CSS afin d’en comprendre les aspects les plus intéressants en créant un jeu ! Il est assez évident que CSS n’est pas un langage de programmation comme Ruby ou JavaScript et qu’il nous offre des possibilités limitées. Mais ça ne me fait pas peur, avec quelques astuces je suis sûr que nous pourrons créer un jeu simple mais qui saura nous donner du fil à retordre et du fun.
Le but du jeu
L’idée de notre jeu est simple, nous utilisons le curseur comme “vaisseau” et devons parcourir un couloir sinueux en évitant ses murs. Lorsque l’un des murs est touché nous revenons au point de départ et ce, avec une vie et un niveau. Pour le confort de l’exercice, ce niveau dure environ 10 secondes, mais comme vous pourrez le voir il est extensible à l’infini.
Quelques lignes de HTML
Commençons par placer les éléments qui composent notre interface de jeu. Nous mettons en place une page HTML basique avec doctype, head, un lien vers une feuille de style et un body. Dans celui-ci nous ajoutons une DIV avec l’ID game
qui contient un #start-spot
, 2 murs que nous appelons .wall-left
et .wall-right
, un écran de #gameover
et un écran de succès que nous appelons #win
. Ces 2 derniers écrans contiennent chacun un texte pour indiquer au joueur s’il a perdu ou gagné.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CSS Game</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="game"> <div id="start-spot"></div> <div class="wall-left"></div> <div class="wall-right"></div> <div id="gameover"><h1>You lose</h1></div> <div id="win"><h1>Congratulations !<br/>You win</h1></div> </div> </body> </html>
On attaque les CSS
Afin de mettre en place le jeu, voici quelques règles primordiales que nous verrons :
- clip-path pour donner la forme que l’on veut à chacun des murs
- transform pour le déplacement et l’effet de 3d
- animation et @keyframes pour les déplacements et l’animation des éléments
L’espace de jeu
Dans un premier temps définissons l’espace qui contiendra le jeu ; on applique un ensemble de règles pour réinitialiser les conteneurs HTML
et BODY
:
html, body { margin: 0; padding: 0; height: 100%; width: 100%; overflow:hidden; text-align:center; /* Quelques règles pour la forme */font-family:sans-serif; background: #333; color: #fff; text-transform:uppercase; }
Dans un second temps on ajoute des styles pour définir l’espace de jeu, un écran de 800 par 600px pour un maximum de sensations ! Une box-shadow
pour faire ressortir le-dit écran et une règle transform
qui nous permet de donner une impression de 3d pour rendre le jeu un poil plus immersif. Le translate
permet de contre-carrer le déplacement de l’écran dans la page dû à la rotation en X.
#game { position:relative; width: 800px; height: 600px; margin: 20px auto 40px; box-shadow: 0 1px 15px #222; overflow:hidden; -webkit-transform: perspective(200px) rotateX(15deg) translateY(-90px); transform: perspective(200px) rotateX(15deg) translateY(-90px); }
Les différents écrans de jeu
Nous avons deux écrans différents qui marquent les étapes du jeu, le #gameover
et le #win
. Tous deux viendront se placer au dessus de l’ensemble des éléments en prenant toute la surface du jeu. L’écran de Game Over se verra affublé d’un fond coloré pour que le joueur comprenne qu’il a perdu. L’écran de succès quant à lui aura une opacité nulle qui nous permettra de faire une transition plus douce en fin de jeu. Le pointer-events: none;
nous permet de l’avoir constamment au dessus du reste sans qu’il n’influence les contrôles du jeu. Ces 2 écrans ne sont donc pas affichés par défaut.
#gameover,#win { position:absolute; top: 0; left: 0; height: 100%; width: 100%; } #gameover { display:none; background: tomato; } #win { opacity: 0; pointer-events:none; }
Nous stylons aussi simplement le point de départ qui indique au joueur où placer son curseur en début de partie. C’est un simple indicateur puisqu’il suffit en réalité de survoler le bloc #game
comme nous le verrons plus tard.
#start-spot { position:absolute; bottom: 10px; left: 50%; width: 20px; height: 20px; -webkit-transform: translateX(-50%); transform: translateX(-50%); background: #fff; }
CSS clip-path pour former les murs
Mettons en place les styles de base de chacun de nos murs. Afin de pouvoir voir l’ensemble du niveau mettons la hauteur des murs à 100%. Par la suite nous les étirerons afin d’avoir un effet de déroulé bien plus intéressant :
.wall-left,.wall-right { position:absolute; bottom: 0; width: 100%; height: 100%; /* Nous changerons cette valeur pour 400% une fois le niveau dessiné */background: linear-gradient(#fff,#777); }
Afin de ne pas former 2 murs parallèles qui seraient très ennuyeux à éviter (forcément…) nous allons utiliser une règle puissante des CSS, à savoir clip-path. Cette règle permet de définir un masque précis où l’on définit les coordonnées de chacun des points d’un polygone. Cette même règle permet aussi plus simplement de définir des ellipses ou des rectangles ou encore utiliser du SVG pour créer des courbes plus complexes dans votre éditeur favori. Pour bien comprendre comment fonctionne un clip-path
nous allons définir notre polygone point par point. Pour le mur de gauche :
.wall-left { left: 0; -webkit-clip-path: polygon( 0 0 /* Une premier point en haut à gauche de l’écran */, 40% 0 /* Le second point en haut à droite de la forme */, 35% 15% /* Ce point comme les suivants crée les « zig-zag » */, 55% 35%, 45% 45%, 60% 60%, 30% 80%, 40% 100% /* Le point à gauche du point de #start-spot */, 0 100% /* Ce dernier point en bas à gauche de l’écran %/ ); }
Nous créons de la même manière le mur de droite, je vous laisse le loisir de jouer avec ses dimensions tout en sachant qu’il ne faut évidemment pas que les murs se chevauchent auquel cas le jeu deviendrait impossible !
.wall-right { right: 0; -webkit-clip-path: polygon(60% 0, 100% 0, 100% 100%, 60% 100%, 44% 81%, 70% 60%,57% 44%, 65% 35%, 46% 15%); }
Il est possible d’ajouter beaucoup plus de points et/ou de les séparer les uns des autres afin de faire voyager le joueur de gauche à droite et rendre le jeu plus difficile.
Okay visuellement tout est là, dynamisons l’ensemble !
Il nous faut définir les animations dont nous aurons besoin (par soucis de simplicité je n’écris pas les versions préfixées, mais n’oublions pas de les ajouter). Pour les murs, rien de plus simple, nous partons d’une keyframe win
0% où il ne se passe rien à une keyframe 100% où le niveau sera entièrement déroulé :
@keyframes wallScroll { 0% { transform:none; } 100% { transform: translateY(100%); } }
Nous ajoutons ensuite une keyframe qui affichera l’écran de fin si le joueur arrive jusqu’au bout de l’animation (et donc du niveau) :
@keyframes win { 0% { opacity: 0; } 99% { opacity: 0; /* on attend le tout dernier moment de l'animation */ } 100% { opacity: 1; } }
Maintenant que nous avons nos animations il est temps de les activer au moment opportun. Tout d’abord dès que nous entrons dans l’espace de jeu, les murs commencent à se déplacer :
#game:hover .wall-left,#game:hover .wall-right { -webkit-animation: walls 10s ease-in forwards; animation: walls 10s ease-in forwards; }
Ensuite, dès le début du niveau, nous lançons l’animation de fin avec le même timing, ainsi dès qu’on arrive à la fin on affiche le message de félicitation :
#game:hover #win { -webkit-animation: win 10s linear forwards; animation: win 10s linear forwards; }
En passant on retire le #start-point
qui ne sera pas utile lors de notre parcours :
#game:hover #start-spot { display:none; }
Et enfin le plus important ! Dès que l’on survole l’un des murs, nous affichons l’écran de Game Over au dessus de l’ensemble. On applique la même règle au survol de l’écran de Game Over pour s’assurer qu’il reste bien présent jusqu’à ce que l’utilisateur recommence une partie. Malgré l’écran de défaite affiché, les animations en arrière plan se déroulent toujours mais nous ne pouvons plus les voir. Pour revenir au début du jeu il faut alors sortir de l’écran et l’ensemble du niveau (et donc des animations) reviendra au début./* on utilise ici le ‘~’ pour sélectionner un élément au même niveau plus loin dans le DOM */.wall-left:hover ~#gameover,.wall-right:hover ~#gameover,#gameover:hover { display:block; z-index: 1; }
Pour ajouter quelques points de charisme à notre jeu nous allons centrer les textes des écrans de jeu avec une astuce relativement simple qui consiste à positionner l’élément à 50% des bords haut et gauche de l’écran puis à le déplacer lui-même de 50% de sa propre taille dans les mêmes directions :#gameover h1,#win h1 { position:absolute; top: 50%; left: 50%; –webkit–transform: translate3d(-50%, -50%, 0); transform: translate3d(-50%, -50%, 0); }
Bonus
Notre jeu est fonctionnel, mais encore bien trop simple à mon goût. Voyons ensemble comment nous pourrions modifier le niveau pour faire perdre un peu de cheveux à nos joueurs. La modification des murs nous permettra ainsi de voir une technique dont j’ai parlé brièvement et qui est bien plus pratique pour rendre notre environnement plus intéressant, le SVG.
Un peu de level design
Pour l’exemple vous pouvez télécharger un SVG que j’ai déjà crée ou le fichier sketch mais je vous incite à ouvrir votre éditeur vectoriel favori (InkScape, Sketch ou Illustrator par exemple) pour créer 2 formes qui composeront chacun de nos murs dans un espace de 800px (la largeur de notre écran) par 600px * 4
soit 2400px qui nous permettra de scroller sur 4 fois la hauteur de notre écran. Grâce au SVG vous pourrez utiliser des courbes, et l’édition du niveau sera bien plus aisée qu’avec l’utilisation de polygon()
.
Une fois le SVG créé nous devons l’intégrer dans notre page HTML. Sketch par exemple exporte un SVG avec beaucoup de données inutiles, nous allons donc déclarer un SVG à la main que nous viendrons compléter avec les courbes définies dans le SVG. Dans mon exemple vous trouverez deux éléments path
, #path-1
et #path-2
que l’on copie dans deux éléments clipPath
auxquels on attribue un ID que nous réutiliserons dans les CSS.<svg><!– On déclare un SVG (en général juste après <body>, en début de document). –> <defs><!– On crée un bloc de définition, –> <clipPath id=”left”><!– un premier clip path –> <path id=”path-1″ d=”M310.664062,2494.08984 L312.664062,2248.46875 L237.390625,2194.82812 L137.777344,2030.35938 L237.390625,1931.83594 L201.117188,1718.30469 L512.40625,1671.47266 L625.328125,1623.67187 L554.5,1528.82813 L554.671875,1367.17188 L505.5,1496.0625 L389.328125,1318.78125 L308.054687,1456.08984 L292.945312,1260.08984 L139.835938,1239.41797 L193.140625,968.167969 L594.265625,834.582031 L589.878906,810.363281 L294.828125,639.890625 L466.5625,514 L286.109375,421.609375 L403.671875,259.953125 L259.140625,-1.77635684e-15 L0,-1.77635684e-15 L0,2400 L310.664062,2494.08984 Z”></path> </clipPath> <clipPath id=”right”><!– et un second clip path. –> <path id=”path-2″ d=”M507.574219,2400 L477.292969,2217.58203 L322.253906,2136.07031 L277.097656,2050.32031 L335.421875,1981.23438 L322.253906,1799.02734 L555.867188,1774.47266 L740.628906,1650.70703 L634.464844,1501 L606.171875,1092.44531 L489.804688,1280.52734 L455.144531,1174.73438 L363.847656,1236.04297 L393.441406,1148.53906 L268.179688,1157.70312 L292.765625,1043.58203 L708.695312,919.421875 L669.171875,751.890625 L469.5,639.890625 L641.234375,514 L460.78125,421.609375 L578.34375,259.953125 L433.8125,-1.77635684e-15 L800,-1.77635684e-15 L800,2400 L507.574219,2400 Z”></path> </clipPath> </defs> </svg>
Lions maintenant notre SVG à nos déclarations clip-path
dans bloc déjà existant :.wall-left { left: 0; –webkit–clip–path: url(‘#left’); clip–path: url(‘wall.svg#left’); } .wall-right { right: 0; –webkit–clip–path: url(‘#right’); clip–path: url(‘wall.svg#right’); }
Si on teste maintenant dans Chrome on peut remarquer un “saut” des murs en début de partie, c’est probablement dû à un bug de Webkit que l’on peut corriger en appliquant la règle suivante :.wall-left,.wall-right { transform: TranslateZ(1px); }
Un vaisseau spatial c’est quand même plus sympa qu’un curseur !
Une petite astuce qui vous permettra d’ajouter une cerise sur le gâteau de votre satisfaction, nous allons modifier le curseur de la souris pour en faire un vaisseau spatial flambant neuf à chaque partie. C’est relativement simple, il nous suffit d’ajouter une règle dans notre espace de jeu et d’ajouter un fichier image (de préférence avec des bords transparents) à la racine du projet comme ceci :#game { cursor: url(‘vessel.png’),auto; /* }
Si tout s’est bien passé, vous devriez obtenir quelque chose qui ressemble à ce jeu.
Conclusion
Vous l’aurez compris, il y a très peu d’intérêt à créer un jeu avec CSS mais je pense qu’il est toujours utile de jouer avec nos outils et de les pousser dans leurs retranchements (les hacker en quelque sorte). Ça nous permet d’être chaque jour plus à l’aise avec leurs comportements les plus complexes et parfois peu instinctifs.
Bien sûr je vous incite à rendre le jeu plus difficile, plus long ou mieux dessiné et à nous partager ensuite vos créations dans les commentaires.