Fiche pratique : le rendu 2D
Bonjour,
En complément des exercices que je rédige en ce moment sur le Wiki (http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux), voici une petite fiche pratique concernant les affichages et rendus des jeux en 2D.
Bien sur j'utilise ma propre méthode, il en existe surement d'autres et je ne suis pas à l’abri d'erreurs (merci de me corriger le cas échéant), cependant je pense que cela fera un bon complément par exemple pour l'exercice du Rogue Like (http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/exercice_-_roguelike_partie_1).
Voici les différents types de rendus que j'ai recensé, saurez-vous les reconnaître ?
J'ai trouvé intéressant de vous parler spécifiquement des différents types de rendu graphiques que proposent les jeux vidéo. Le rendu d'un jeu est une chose différente des calculs, qu'ils soient de physique ou d'effets, lorsqu'on à compris ça beaucoup de choses deviennent limpides et nos programmes (les miens du moins) maigrissent rapidement tout en gagnant en précision et en rapidité.
On va donc faire un test sur les différents types de vues proposées dans les jeux vidéo 2D (j'exclu la 3D pour le moment), utiliser le même programme pour gérer tous les calculs de physique, la même map pour décrire le monde, puis générer un rendu différent à chaque fois.
Une même structure
Je vais prendre un exemple Tile Based, c'est à dire une grille composée de tuiles avec un moteur de déplacement classique (écouteurs sur les touches et 4 directions possibles), des feuilles de sprites externes et une gestion de collision simple.
Si vous ne savez pas ce qu'est le Tile Based, allez voir ici : http://ressources.mediabox.fr/tutoriaux/flashplatform/jeux/arcade-base
Si vous ne savez pas jongler avec les grilles, allez voir ici : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/exercice_-_le_taquin
Si vous ne savez pas gérer des écouteurs clavier, c'est ici : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/gestion_touche
Si vous ne savez pas utiliser les tableaux allez voir par là : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/programmation/generalites/tableaux
Concernant la structure, j'ai plusieurs fichiers externes (AS) chacun comprenant une petite partie du programme (map, feuilles de sprites, collisions, contrôleurs,….), je les importes en début de programme afin de conserver toujours la même structure (ou presque).
Je n'utilise pas de classe, là on parle juste de découper le programme en petits bouts qu'on réparti pour alléger le code et se concentrer sur ce qui est important, mais chaque fichier AS peut bien sur être une classe indépendante si vous le souhaitez, la transformation ne sera pas bien difficile vu que c'était en fait déjà des classes au départ
Cet ensemble est le moteur physique des jeux, il défini le monde à l'aide d'une map et de normes (taille des tuiles, vitesse, taille du monde,…), y déplace un objet en fonction du clavier et teste les collisions en fonction de la surface occupée par cet objet dans le monde (la map).
Mon objectif n'est pas de vous expliquer comment tout ça fonctionne, j'en parlerai sans doute dans une autre fiche ou vous pouvez vous lancer sur ce tutoriel (http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/arcade-base) et les exercices du Wiki (http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux) pour vous entraîner sur cette partie.
Deux choses vont principalement nous intéresser, les graphismes et leur rendu à l'écran.
Charger les graphismes
Il y a différentes écoles, quand on travaille avec l'IDE de Flash et qu'on dispose d'une bibliothèque bien pratique, on a tendance à l'utiliser et à placer les graphismes dans des Clips. Mais quand ce n'est pas le cas, autrement dit la plupart du temps, on se débrouille pour charger les images depuis un dossier externe. Un des gros avantages de cette technique c'est qu'elle permet de modifier le look du jeu sans retoucher au code ou avoir à le recompiler, un des désavantages c'est qu'on ne travaille qu'avec des Bitmaps et plus des animations vectorielles comme le permet Flash.
Les jeux 2D utilisent la plupart du temps ce que l'on appelle des feuilles de sprites, ou SpriteSheets, ce sont de grandes images regroupant les textures de chaque tuile, police, objet ou personnage.
Feuille du décor du niveau 1 de Chaos Engine sur Amiga
Feuille d'un personnage de Chaos Engine sur Amiga
Rendu final recomposé avec différentes feuilles de sprites.
Généralement on utilise plusieurs feuilles au sein d'un jeu, une différente pour chaque décor, une autre pour chaque personnage, une pour les objets, une pour chaque animation, une pour l'interface, etc…. On charge la feuille correspondante à ce qu'on à besoin d'afficher.
Voici le code que j'utilise dans cette fiche (“SpriteSheets.as”) :
// variables var chargeur:Loader = new Loader(); var textures:Array = []; var url:String = "sprites/"; var tailleTuile:int = 64; var colonneFeuille:int; var ligneFeuille:int; var departTuile:Point = new Point(0, 0); var textureTuile:BitmapData; var surfaceTuile:Rectangle; // Charge la feuille de sprites function SpritesSheet(feuille:String, echelle:int){ tailleTuile = echelle; chargeur.contentLoaderInfo.addEventListener(Event.COMPLETE, finCharge); chargeur.load(new URLRequest(url + feuille)); } // A la fin du chargement, découpe et création des tuiles function finCharge(e:Event):void{ chargeur.contentLoaderInfo.removeEventListener(Event.COMPLETE, finCharge); colonneFeuille = chargeur.content.width / tailleTuile; ligneFeuille = chargeur.content.height / tailleTuile; // Boucle sur le nombre total de tuiles for (var i:int=0; i<colonneFeuille*ligneFeuille; i++){ textureTuile = new BitmapData(tailleTuile,tailleTuile); surfaceTuile = new Rectangle(i % colonneFeuille * tailleTuile, int(i / colonneFeuille) * tailleTuile, tailleTuile, tailleTuile); textureTuile.copyPixels(Bitmap(chargeur.content).bitmapData, surfaceTuile, departTuile); textures.push(new Bitmap(textureTuile)); } init(); }
Je commence par charger l'image, notez que l'image à charger et la taille des tuiles à découper sont passées en paramètre de la fonction SpriteSheet afin de pouvoir découper des feuilles différentes avec la même fonction.
Une fois l'image chargée, je récupère le nombre de colonnes et de lignes à découper en fonction de la taille de l'image que j'ai chargé et de la taille des tuiles que je veux découper.
Je boucle sur le nombre total de tuiles que peut contenir la feuille. Pour chacune je crée une nouvelle texture vierge de la taille d'une tuile, je récupère ensuite les dimensions de la surface que je dois découper, attention sa position est différente pour chaque tuile. Enfin, je copie tous les pixels de la zone que je viens de récupérer, à l'intérieur de ma texture vierge. Pour finir je range ma nouvelle texture dans un tableau afin de disposer à tout moment de la liste des textures du jeu.
Notez que la fonction se termine par l'appel à une fonction “init()”, par défaut dans les exemples de cette fiche c'est la fonction qui initialise le jeu.
Nous allons vérifier que le chargement de la feuille et sa découpe se sont bien passées avec ce petit code à placer dans un FLA vierge, vous devez également créer un dossier “sprites” à la racine du projet et y placer une feuille de sprites au format PNG de 512*512 pixels, chaque texture devra faire 64*64 pixels.
include "SpriteSheets.as"; SpritesSheet("pieces.png",64); function init():void{ var C:int = colonneFeuille; var L:int = ligneFeuille; var T:int = tailleTuile; var ecran:BitmapData = new BitmapData(512,512,false,0); var p:Point = new Point(0,0); var tiles:Rectangle = new Rectangle(0,0,T,T); for(var i=0; i<C*L; i++){ p.x = int(i%C)*T p.y = int(i/L)*T ecran.copyPixels(textures[i].bitmapData,tiles,p); } graphics.clear(); graphics.beginBitmapFill(ecran,null,true,false); graphics.drawRect(0,0,512,512); }
Je commence par importer et lancer la découpe de ma feuille de sprites.
Lorsque la découpe est terminée, je lance la fonction init, je crée un BitmapData qui va servir pour l'affichage du jeu, on va le considérer comme une grosse texture unique dans laquelle je vais dessiner. Elle fait la taille de ma scène, un point de départ pour le remplissage (par défaut le coin haut gauche de ma scène), et un rectangle qui détermine la surface à tracer.
Pour chaque tuile que j'ai à afficher, je récupère sa position et je copie la texture correspondante dans mon écran (la texture d'affichage du jeu) et je trace le tout.
Avec ce petit bout de code, on charge une feuille de sprites, on la découpe et on affiche une seule image de rendu à l'écran recomposant la feuille de sprites dans l'ordre, c'est la base de tout ce que nous allons voir par la suite sauf qu'on utilisera une map pour disposer les tuiles dans un ordre précis.
Pour charger plusieurs feuilles il suffit simplement d'utiliser des tableaux de stockages différents
Types d'affichages
Si on part du principe que toutes les méthodes de rendu passent par le dessin d'une image en temps réel à l'écran, la technique la plus “optimisée” est de faire tout simplement ce qui s'en rapproche le plus… en supprimant tous les intermédiaires. Oubliez donc les MovieClip, Sprites et autres conteneurs, d'ailleurs en fait Flash ne sert plus vraiment à grand chose, hormis de compilateur, à ce stade….
Le principe est simple, on dessine une grosse texture (ecran) en fonction de textures plus petites (feuilles de sprites) et d'une carte détaillée du monde (map). Le reste est l'affaire du moteur du jeu qui se charge des diverses transformations mathématiques (rotation, position, effets, zoom, ….) utiles pour obtenir les paramètres du rendu souhaité.
Bien que certains aient pu m'échapper, j'ai noté six types d'affichages principaux utilisés dans les jeux vidéo :
2D à plat
Ce rendu propose le tracé d'une map à plat, les jeux peuvent être de différents types comme ici Zelda (JDR), ou Mario (profil), ou 1942 (scrolling vertical), …
Isométrique
Contrairement à ce que l'on croit il s'agit également du tracé d'une map à plat, mais cette fois les tuiles sont rangées d'une manière spéciale qui donne cet aspect de fausse 3D.
Perspective
Cette fois il s'agit d'une déformation d'un bitmap qui donne un effet de perspective, très employé dans les jeux de course comme Mario Kart par exemple.
2.5D
Ca se complique un peu, on est toujours en 2D mais grâce à quelques artifices et formules on arrive à élever des murs à partir d'une simple map 2D, on appelle ça la 2.5D car on est pas encore vraiment en 3D mais plus vraiment sur un affichage 2D à plat.
Voxel
Toujours de la 2.5D, cette fois on ajoute une notion de volume, on est toujours avec une map en 2D, mais chaque case à une hauteur qui lui est propre et se défini comme un cube de taille fixe et de couleur dépendant de la hauteur. Ce qui nous donne des terrains et des dénivelés.
3D
Là on passe dans un autre univers, la 3D, on ne gère plus les choses de la même manière et on oublie les maps 2D.
Je ne vais pas attaquer la 3D et les Voxels dans cette fiche, c'est un peu plus complexe et ça sort un peu du cadre 2D simple que je souhaite aborder (bien que les Voxels soient de la 2.5D).
2D à plat
Utilisez les flèches du clavier pour vous déplacer.
Le plus simple est certainement le rendu 2D à plat, qu'il soit vu du dessus, horizontal, vertical, fixe ou scrollé, le concept est toujours le même, dessiner la carte à plat à l'écran. On se contente donc de parcourir la carte (la grille, la map, peu importe) et d'afficher les sprites en fonction de la référence et position (virtuelle) de chaque case.
Avec ce petit code vous avez déjà beaucoup d'astuces pour ce type d'affichage
include "Map.as"; include "SpriteSheets.as"; include "Collisions.as"; include "Controles.as"; SpritesSheet("pieces.png",64); var T:int = 64; var C:int = 64; var W:int = 512; var H:int = 512; var X:Number = 0; var Y:Number = 0; var Xmax:int = C*T-W; var Ymax:int = map.length/C*T-H; var V:int = 8; var ecran:BitmapData = new BitmapData(W,H,false,0); var tiles:Rectangle = new Rectangle(0,0,T,T); var pX:int = 1; var pY:int = 1; var px:Number = pX*T; var py:Number = pY*T; function init():void{ stage.addEventListener(Event.ENTER_FRAME, main); stage.addEventListener(KeyboardEvent.KEY_DOWN,appuie); stage.addEventListener(KeyboardEvent.KEY_UP, relache); } function main(e:Event):void{ graphics.clear(); graphics.beginBitmapFill(ecran,null,true,false); graphics.drawRect(0,0,W,H); deplace(); } function deplace():void{ var bouge:Array = collisions(droite-gauche,bas-haut,px,py,T,map,V,C,C); var dessine:Boolean = true; var scroller:Boolean = true; if(!bouge[2]){ px = bouge[0]; py = bouge[1]; pX = px/T; pY = py/T; } else { scroller=false; } if(scroller){ if((pX<C-3 && gauche) || (pX>3 && droite)){ scrollTo(X+V*(droite-gauche),Y); dessine=false; } if((pY<C-3 && haut) || (pY>3 && bas)) { scrollTo(X,Y+V*(bas-haut)); dessine=false; } } if(dessine) scrollTo(X,Y) } // scrolling function scrollTo(x:int,y:int):void{ // limites et déplacement x<0 ? x=0 : x>Xmax ? x=Xmax : X=x; y<0 ? y=0 : y>Ymax ? y=Ymax : Y=y; var colA:int = x/T; var rowA:int = y/T; var colB:int = (x+W-1)/T; var rowB:int = (y+H-1)/T; var startX:int = -x%T; var p:Point = new Point(startX,-y%T); ecran.lock(); for (var r:int=rowA; r<=rowB; r++){ p.x = startX; for (var c:int=colA; c<=colB; c++){ ecran.copyPixels(textures[map[c+C*r]].bitmapData,tiles,p); p.x += T; } p.y += T; } ecran.copyPixels(textures[2].bitmapData,tiles,new Point(px-x,py-y)); ecran.unlock(); }
Qu'est ce que ça nous dit tout ça ?
include "Map.as"; include "SpriteSheets.as"; include "Collisions.as"; include "Controles.as"; SpritesSheet("pieces.png",64);
J'importe la map (carte), la découpe de la feuille de sprites, la gestion des collisions et la gestion des touches, puis je découpe la feuille de sprites pour récupérer la liste des textures de mon level. Notez qu'en dehors de la découpe des sprites je ne touche pas aux graphismes, la map est un tableau, les déplacements sont retranscrits en formules et calculs de surfaces utilisés pour la détection de collisions dans la map, bref tout ça c'est le moteur du jeu qui s'en charge.
Ca c'est fait !
var T:int = 64; var C:int = 64; var W:int = 512; var H:int = 512; var X:Number = 0; var Y:Number = 0; var Xmax:int = C*T-W; var Ymax:int = map.length/C*T-H; var V:int = 8; var ecran:BitmapData = new BitmapData(W,H,false,0); var tiles:Rectangle = new Rectangle(0,0,T,T); var pX:int = 1; var pY:int = 1; var px:Number = pX*T; var py:Number = pY*T;
Je déclare quelques variables utiles comme la taille des tuiles affichées, la taille de l'écran de jeu, les limites de la map, etc… et je crée ma texture d'affichage (l'écran, on à vu ça avec la découpe de la feuille de sprites).
pX et pY réprésentent la position stricte (ligne et colonne) du joueur dans la map.
px et py représentent la position réelle (libre) du joueur dans la map.
function init():void{ stage.addEventListener(Event.ENTER_FRAME, main); stage.addEventListener(KeyboardEvent.KEY_DOWN,appuie); stage.addEventListener(KeyboardEvent.KEY_UP, relache); } function main(e:Event):void{ graphics.clear(); graphics.beginBitmapFill(ecran,null,true,false); graphics.drawRect(0,0,W,H); deplace(); }
Je pose les écouteurs des touches et le pilote du programme (main).
La fonction “main” est exécutée à la cadence du projet, elle nettoie l'affichage, retrace la texture et vérifie si le joueur se déplace. Notez que pour cet exemple il n'est pas nécessaire de nettoyer l'affichage car la map rempli toute la zone, tous les pixels seront donc retracés directement dans la texture, cependant cela peut vous servir si votre map ne rempli pas tout l'affichage, nous allons le voir dans le prochain paragraphe justement.
function deplace():void{ var bouge:Array = collisions(droite-gauche,bas-haut,px,py,T,map,V,C,C); var dessine:Boolean = true; var scroller:Boolean = true; if(!bouge[2]){ px = bouge[0]; py = bouge[1]; pX = px/T; pY = py/T; } else { scroller=false; } if(scroller){ if((pX<C-3 && gauche) || (pX>3 && droite)){ scrollTo(X+V*(droite-gauche),Y); dessine=false; } if((pY<C-3 && haut) || (pY>3 && bas)) { scrollTo(X,Y+V*(bas-haut)); dessine=false; } } if(dessine) scrollTo(X,Y) }
Je regarde si il y a collision, pour ça je fait appel à ma petite fonction de collisions, elle regarde si à son prochain mouvement la surface du joueur entre en contact avec un mur (1 dans la map). Si c'est le cas la position du joueur sera limitée au bord de la tuile touchée, sinon le joueur peut avancer à cette nouvelle position. La fonction renvoie un tableau contenant la nouvelle position sur chaque axe et un indicateur de collision (vrai ou faux).
Si il n'y a pas collision (donc faux), la surface du joueur change de coordonnées, sinon le scrolling doit s'arrêter.
Si le scrolling peut entrer en action, pour chaque axe je vérifie si la surface du joueur ne se trouve pas trop près du bord de la map, auquel cas je dois arrêter le scrolling et permettre au joueur de se déplacer librement, cette petite astuce donne un joli effet et permet de conserver le décor en plein écran sans interdire l'accès à certaines zones au joueur.
Si le scrolling doit entrer en action j'appelle la fonction “scrollTo” qui se charge de l'affichage et du rendu, sinon j'appelle quand même cette fonction mais avec des paramètres neutres (on ne change pas de position) afin que mon jeu continue à se tracer (l'affichage peut être fixe mais certaines choses peuvent y bouger il est donc nécessaire de retracer l'écran à tout moment).
// scrolling function scrollTo(x:int,y:int):void{ // limites et déplacement x<0 ? x=0 : x>Xmax ? x=Xmax : X=x; y<0 ? y=0 : y>Ymax ? y=Ymax : Y=y; var colA:int = x/T; var rowA:int = y/T; var colB:int = (x+W-1)/T; var rowB:int = (y+H-1)/T; var startX:int = -x%T; var p:Point = new Point(startX,-y%T); ecran.lock(); for (var r:int=rowA; r<=rowB; r++){ p.x = startX; for (var c:int=colA; c<=colB; c++){ ecran.copyPixels(textures[map[c+C*r]].bitmapData,tiles,p); p.x += T; } p.y += T; } ecran.copyPixels(textures[2].bitmapData,tiles,new Point(px-x,py-y)); ecran.unlock(); }
Et voilà le rendu avec scrolling.
“x” et “y” représentent le point de départ de la surface à tracer au sein de la map.
Je commence par limiter le défilement de la map afin qu'elle remplisse toujours l'écran.
Je récupère la colonne et la ligne de départ de la zone à tracer, je récupère aussi la colonne et la ligne d'arrivée, ceci me donne la surface complète correspondant à la zone que je veux tracer de la map.
Je récupère également le point de départ de mon tracé à l'écran.
Je bloque le rafraîchissement de l'affichage (évite les artefacts), puis je vais faire deux boucles imbriquées sur les lignes et les colonnes à tracer, pour chaque case je copie tous les pixels de la texture correspondante à l'écran.
Lorsque tout le décor est tracé j'ajoute le rendu du joueur, je viens donc redessiner sa texture par dessus mon décor, et c'est tout
C'est court (et je pense qu'on peut encore faire plus simple), efficace (vous pouvez le passer en plein écran sur n'importe quelle machine sans que ça rame) et vous n'avez qu'un seul objet d'affichage pour gérer tout votre jeu.
Avec cette base vous pouvez déjà faire une foultitude de jeux bien sympas
Isométrique
Utilisez les flèches du clavier pour vous déplacer.
Même chose que pour le rendu 2D à plat, la seule différence est dans le positionnement des tuiles et leur aspect. Chaque sprite représente une tuile en vue isométrique, c'est à dire vulgairement “tournée de 45 degrés et aplatie à la moitié de sa hauteur”, vulgairement car en fait n'est pas une règle d'or…
include "Map.as"; include "Collisions.as"; include "SpriteSheets.as"; include "Controles.as"; SpritesSheet("pieces_iso.png",64); var T:int = 64; var C:int = 64; var W:int = 512; var H:int = 512; var X:Number = 0; var Y:Number = 0; var Xmax:int = C*T-W; var Ymax:int = map.length/C*T-H; var V:int = 64; var ecran:BitmapData = new BitmapData(W,H,false,0); var tiles:Rectangle = new Rectangle(0,0,T,T); var pX:int = 1; var pY:int = 1; var px:Number = pX*T; var py:Number = pY*T; function init():void{ stage.addEventListener(Event.ENTER_FRAME, main); stage.addEventListener(KeyboardEvent.KEY_DOWN,appuie); stage.addEventListener(KeyboardEvent.KEY_UP, relache); } function main(e:Event):void{ ecran.fillRect(new Rectangle(0,0,W,H),0x00000000); graphics.clear(); graphics.beginBitmapFill(ecran,null,true,false); graphics.drawRect(0,0,W,H); deplace(); } function deplace():void{ var bouge:Array = collisions(droite-gauche,bas-haut,px,py,T,map,V,C,C); var dessine:Boolean = true; var scroller:Boolean = true; if(!bouge[2]){ px = bouge[0]; py = bouge[1]; pX = px/T; pY = py/T; } else { scroller=false; } if(scroller){ if((pX<C-4 && gauche) || (pX>3 && droite)) { scrollTo(X+V*(droite-gauche),Y); dessine=false; } if((pY<C-4 && haut) || (pY>3 && bas)){ scrollTo(X,Y+V*(bas-haut)); dessine=false; } } if(dessine) scrollTo(X,Y); } // scrolling function scrollTo(x:int,y:int):void{ x<0 ? x=0 : x>Xmax ? x=Xmax : X=x; y<0 ? y=0 : y>Ymax ? y=Ymax : Y=y; var colA:int = x/T; var rowA:int = y/T; var colB:int = (x+W-1)/T; var rowB:int = (y+H-1)/T; var startX:int = -x%T+256-T/2; var p:Point = new Point(startX,128-T/4); ecran.lock(); for (var r:int=rowA; r<=rowB; r++){ for (var c:int=colA; c<=colB; c++){ ecran.copyPixels(textures[map[c+C*r]].bitmapData,tiles,p); if(c==pX && r==pY) ecran.copyPixels(textures[2].bitmapData,tiles,p); p.y += T/4; p.x += T/2; } p.y -= 2*T-T/4; p.x -= 4*T+T/2; } ecran.unlock(); }
C'est exactement le même code que pour le scrolling 2D à quelques différences près.
function main(e:Event):void{ ecran.fillRect(new Rectangle(0,0,W,H),0x00000000); graphics.clear(); graphics.beginBitmapFill(ecran,null,true,false); graphics.drawRect(0,0,W,H); deplace(); }
Ici je retrace l'écran en noir avant d'y réafficher la nouvelle texture, comme la zone visible des tuiles ne remplit plus tout l'écran il faut que je “gomme” les zones que le programme ne retrace pas.
var startX:int = -x%T+256-T/2; var p:Point = new Point(startX,128-T/4);
Le centre de ma zone d'affichage a changé, il se place à présent au centre de ma scène sur X et à 1/4 du haut sur Y, toutes les tuiles subissent ce décalage.
ecran.lock(); for (var r:int=rowA; r<=rowB; r++){ for (var c:int=colA; c<=colB; c++){ ecran.copyPixels(textures[map[c+C*r]].bitmapData,tiles,p); if(c==pX && r==pY) ecran.copyPixels(textures[2].bitmapData,tiles,p); p.y += T/4; p.x += T/2; } p.y -= 2*T-T/4; p.x -= 4*T+T/2; } } ecran.unlock();
C'est là que tout se joue, je trace toujours des tuiles droites, elles ne subissent aucune déformation, mais les sprites utilisés sont au format isométrique et en partie transparents, je peux donc superposer mes tuiles affichées sans que cela se voie.
La position de chaque tuile dépend de la précédente, pour chaque colonne la tuile suivante soit nécessairement se trouver une demi tuile plus à droite et un quart de tuile plus bas que la précédente afin de respecter la vue Isométrique.
Pour chaque ligne il faut faire un retour en arrière car la nouvelle rangée de tuiles doit se trouver une demi tuile avant quart de tuile plus bas que la première de la rangée supérieure, toujours pour respecter la vue Isométrique.
Le truc le plus important pour un jeu de ce type c'est qu'on ait l'impression que le joueur puisse passer derrière le décor, je vous rassure pas besoin de zSorting ou d'algorithme de tri quelconque, ça se règle avec une simple petite vérification.
if(c==pX && r==pY) ecran.copyPixels(textures[2].bitmapData,tiles,p);
Au moment où je trace mes tuiles, je vérifie si le joueur se trouve sur la case que je suis en train de tracer, si c'est le cas je trace la texture du joueur par dessus la texture de décor et le tour est jouée, je me base simplement sur l'ordre de tracé des tuiles pour reproduire en fin de compte se qu'il se passe dans la réalité lorsqu'un objet passe derrière un décor.
Dans cet exemple je me suis simplifié la vie, le joueur avance de case en case, avec une vitesse variable cela devient légèrement plus compliqué car on ne peut plus prendre en compte le retraçage du joueur seulement lorsqu'il est pile poil sur une tuile, mais je vous laisses approfondir cette partie
Perspective
Utilisez les flèches du clavier pour vous déplacer.
La perspective a été très longtemps utilisée, et l'est même encore pour certains jeux 3D, pour simuler un affichage en vue subjective, mais pas seulement. Il s'agit en fait de déformer un plan à l'aide d'une transformation affine, je ne vais pas trop m'étendre dessus, vous avez un tutoriel qui traite du Mode 7 (c'est le nom communément utilisé, a tort, de ce type de vue) ici : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/mode7
include "Map.as"; include "Collisions.as"; include "SpriteSheets.as"; include "Controles.as"; var T:int = 64; var C:int = 64; var W:int = 512; var H:int = 512; var X:Number = 0; var Y:Number = 0; var Xmax:int = C*T-W; var Ymax:int = map.length/C*T-H; var V:int = 8; var ecran:BitmapData = new BitmapData(W,H,false,0); var tiles:Rectangle = new Rectangle(0,0,T,T); var i:int; var pX:int = 1; var pY:int = 1; var px:Number = pX*T; var py:Number = pY*T; var rot:Number = 0; var texture:Bitmap = new Bitmap(ecran); var view:Sprite = new Sprite(); var plane:Sprite = new Sprite(); var projection:PerspectiveProjection=new PerspectiveProjection(); projection.fieldOfView = 60; projection.projectionCenter = new Point(256,256); view.transform.perspectiveProjection=projection; plane.rotationX = -38; plane.z = 80 plane.x = -16 plane.y = -50 addChild(view); plane.addChild(texture); view.addChild(plane); SpritesSheet("pieces.png",64); function init():void{ stage.addEventListener(Event.ENTER_FRAME, main); stage.addEventListener(KeyboardEvent.KEY_DOWN,appuie); stage.addEventListener(KeyboardEvent.KEY_UP, relache); } function main(e:Event):void{ deplace(); } function deplace():void{ var bouge:Array = collisions(droite-gauche,bas-haut,px,py,T,map,V,C,C); var dessine:Boolean = true; var scroller:Boolean = true; if(!bouge[2]){ px = bouge[0]; py = bouge[1]; pX = px/T; pY = py/T; } else { scroller=false; } if(scroller){ if((pX<C-3 && gauche) || (pX>3 && droite)){ scrollTo(X+V*(droite-gauche),Y); dessine=false; } if((pY<C-3 && haut) || (pY>3 && bas)) { scrollTo(X,Y+V*(bas-haut)); dessine=false; } } if(dessine) scrollTo(X,Y) } // scrolling function scrollTo(x:int,y:int):void{ // limites et déplacement x<0 ? x=0 : x>Xmax ? x=Xmax : X=x; y<0 ? y=0 : y>Ymax ? y=Ymax : Y=y; var colA:int = x/T; var rowA:int = y/T; var colB:int = (x+W-1)/T; var rowB:int = (y+H-1)/T; var startX:int = -x%T; var p:Point = new Point(startX,-y%T); ecran.lock(); for (var r:int=rowA; r<=rowB; r++){ p.x = startX; for (var c:int=colA; c<=colB; c++){ ecran.copyPixels(textures[map[c+C*r]].bitmapData,tiles,p); p.x += T; } p.y += T; } ecran.copyPixels(textures[2].bitmapData,tiles,new Point(px-x,py-y)); ecran.unlock(); }
Je me suis simplifié la vie encore une fois, ce que je vous propose là n'est pas réellement la vue à laquelle on s'attend avec du Mode7, le plan est mal orienté et ne suis pas les mouvements du joueur. La vue en perspective n'est pas très adapté aux jeux de labyrinthes mais plutôt aux jeux de voiture, ou les vues en extérieur, ici en fait je me contente d'utiliser les outils de 3D natifs du langage pour effectuer des rotations sur un plan et m'éviter de fastidieuses formules de transformation, d'autant que les maths et moi …..
C'est toujours le même code que le premier, ou presque, voici les différences :
var texture:Bitmap = new Bitmap(ecran); var view:Sprite = new Sprite(); var plane:Sprite = new Sprite(); var projection:PerspectiveProjection=new PerspectiveProjection(); projection.fieldOfView = 60; projection.projectionCenter = new Point(256,256); view.transform.perspectiveProjection=projection; plane.rotationX = -38; plane.z = 80 plane.x = -16 plane.y = -50 addChild(view); plane.addChild(texture); view.addChild(plane);
Je crée une vue (un Sprite conteneur) qui servira à la projection 3D, un plane (surface) qui servira de surface d'affichage (un peu comme un panneau que je pose derrière l'écran) et une texture qui remplira cette surface.
Je crée ensuite une nouvelle projection, je choisi la perspective puisque c'est le but de la vue que je souhaite mettre en place, je règle le champ de vision et la position du centre de ma projection à l'écran, et je passe la projection à ma vue 3D.
J'oriente mon plane dans l'espace 3D puis j'ajoute le tout dans le bon ordre dans la liste d'affichage.
function main(e:Event):void{ deplace(); }
Plus besoin de retracer quoi que ce soit à l'écran, tout se passe à présent dans la vue 3D donc la fonction “main” ne s'occupe plus que de déplacer le joueur, et c'est tout
2.5D Raycasting
Utilisez les flèches pour tourner, Z pour avancer, S pour reculer, Q pas chassé gauche, D pas chassé droite.
Pour ce type de vue le principe de rendu reste exactement le même à ceci près qu'il demande beaucoup plus de calculs en amont. Le but est toujours de ne tracer qu'une texture à l'écran, mais il faut pour cela prendre en compte aussi la profondeur dans une map en 2 dimensions. Pour trouver la profondeur on utilise ce que l'on appelle le “lancer de rayons” ou “Raycasting”, tout est expliqué en détail dans ce tutoriel : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/affichage/3d/raycasting
Du coup comme vous avez le détail je ne m'encombre pas d'explications supplémentaires
Notez qu'il s'agit toujours de la même map que nous utilisons depuis le départ et les mêmes calculs de collisions, en dehors de l'exemple du Raycasting que j'ai repris tel quel mais les principes sont les mêmes, seules les feuilles de sprites et le calcul du rendu sont différents.
Voxels et 3D
Là on commence à entrer dans un autre monde, bien que le principe de rendu soit là aussi le même, il s'agit juste de dessiner une grosse texture à l'écran, les calculs sont bien plus complexes, certains dirons qu'il s'agit juste de rajouter un axe pour passer en trois dimensions, mais une dimension de plus ouvre des perspectives parfois effrayantes. Je laisse donc le soin à d'autres de vous initier sur ces points et je vous recommande de commencer par ce très bon tutoriel de Tlecoz à propos de drawTriangles : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/math_physique/utilisation_de_drawtriangles
Conclusion
Il existe différentes méthodes de rendu graphique, celle que je vous propose n'en est qu'une parmi d'autres, elle n'est pas totalement optimisée puisqu'on pourrait faire intervenir des opérateurs binaires un peu partout, mais elle me semble suffisamment propre pour convenir à la plupart de vos jeux et vous permettre un gain de puissance confortable en plus de réduire considérablement le code. Je me suis limité à l'affichage simpliste d'une grille mais vous pouvez bien entendu ajouter autant de couches d'effets que vous le souhaitez, le tout étant de garder à l'esprit que tout doit être calculé avant l'affichage et qu'on ne se sert que d'une passe pour afficher le rendu final à l'écran.
Les sources
