Jour 14 - Carré rouge ← Précédent
OK, nous allons continuer là où se termine la première partie du tutorial de plateforme, le 13e jour. En fait, j’ai commencé avec le jeu fini du jour 13 et j’ai ajouté ce qui me semblait être le plus utile. J’espère que vous le pensez aussi. Mais avant d’ajouter ces fonctionnalités supplémentaires, j’ai changé quelques petites choses pour rendre le code plus lisible. Premièrement, j’ai ajouté des #define pour les points fixes...
#define norm_fix(x) ((x)<<8) #define fix_norm(x) ((x)>>8) //xxx tronqué au lieu des cercles #define fix_mult(x,y) (((x)*(y))>>8) #define ratio_fix(x,y) ((256*(x))/(y)) #define fixed_fraction(x) ((x)&&0xff)
bitshifting) dans le code parceque je trouve que c’est plus facile à lire.OK, nous avons vu les changements basiques. Maintenant, voyons le premier changement majeur : faire une carte de collision.
La deuxième chose que j’ai décidé de changer était le code pour la détection de collision. Actuellement, le code de détection de collision contient beaucoup de “nombres magiques” et fonctionnera seulement avec le sprite du joueur. J’ai décidé de le changer car nous allons avoir besoin que les ennemis se déplacent tout comme le joueur. Pour ce faire, je stocke les quatre points de collision du joueur dans une structure appelée ‘hitboxinfo’ qui contient quatres structures ‘pointinfo’.
typedef struct{ s8 x, y, flipx; }pointinfo; typedef struct{ pointinfo left, right, up, down; u8 flipped; }hitboxinfo;
‘pointinfo’ possède les informations suivantes d’un point :
‘hitboxinfo’ contient quatre de ces pointinfo : un pour la gauche (left), la droite (right), le haut (up), et le bas (down). left est le point utilisé pour une collision sur le côté gauche, right pour le côté droit, up pour le haut, et down pour le bas. J’ai placé les infos du hitbox de mario ici...
mario.info.hitbox.left.x=2; mario.info.hitbox.left.y=8; mario.info.hitbox.right.x=29; mario.info.hitbox.right.y=8; mario.info.hitbox.down.x=10; mario.info.hitbox.down.y=31; mario.info.hitbox.up.x=5; mario.info.hitbox.up.y=0; mario.info.hitbox.left.flipx=0; mario.info.hitbox.right.flipx=0; mario.info.hitbox.up.flipx=10; mario.info.hitbox.down.flipx=11;
As you can see I get the postions for x and why from the old methods in platform demo 3... Comme vous pouvez le voir, j’obtient les postions pour x et pourquoi pas les vieilles méthodes dans la démo de plateforme 3...
DOIT ÊTRE AJOUTE
Maintenant nous avons besoin des méthodes employées pour manipuler ce nouveau type de données. Nous utiliserons toujours GetTile, mais au lieu d’avoir quatre méthodes différentes de collision nous n’en avons plus qu’une...
//Vérifie une collision avec une tile au pointx, pointy, et ajouter flipx si le sprite est renversé u8 TileCollision(pointinfo point, u8 flipped){ return (GetTile(fix_norm(mario.x)+point.x + (flipped*point.flipx), fix_norm(mario.y)+point.y)); }
Comme vous pouvez le voir, cette méthode utilise un pointinfo. Le pointinfo indique quel point vérifier sur Mario. En plus de cette méthode, vous pouvez aussi vérifier tous les côtés à la fois en utilisant notre objet de type hitboxinfo.
u8 anycollision(u8 tile, hitboxinfo hitbox){ if(TileCollision(hitbox.left, hitbox.flipped)==tile)return 1; if(TileCollision(hitbox.right, hitbox.flipped)==tile)return 2; if(TileCollision(hitbox.up, hitbox.flipped)==tile)return 3; if(TileCollision(hitbox.down, hitbox.flipped)==tile)return 4; else return 0; }
Le tile que vous passez en paramètre est le tile que vous voulez testé pour une éventuelle collision, et hitbox (passé en paramètre) est la variable hitboxinfo que vous lui passez. Cette méthode renvoie 1 pour une collision sur la gauche, 2 pour la droite, 3 pour le haut, et 4 pour le bas. En utilisant ce code nous devons maintenant changer la méthode de collision.
void CheckCollisions(void){ while(anycollision(1, mario.info.hitbox)==1){ // Collision sur le côté gauche du sprite... mario.x+= norm_fix(1); // Move by 1 pixel... } while(anycollision(1, mario.info.hitbox)==2){ // Collision sur le côté droit du sprite... mario.x-= norm_fix(1); // Move by 1 pixel... } while(anycollision(1,mario.info.hitbox)==3){ // Collision avec quelque chose au-dessus du sprite... mario.y+=norm_fix(1); mario.vy=0; } while(anycollision(1,mario.info.hitbox)==4){ // Collision sous le sprite... mario.y -= norm_fix(1); // Move by 1/2 pixel... } if( (TouchingGround()==1 || TouchingGround()==2) && mario.vy>0){ //if he's touching the ground and moving down he can't hit ground while moving up mario.vy = 0; mario.jumping=0; }
1 est le numéro du tile sur lequel nous voulons que mario soit arrêté (souvenez-vous comment nous avons mis ce carré vert dans le coin en haut à gauche, c’était pour çà, et pour être sur que le vert est le tile numéro 1. Note trad : c’est évoqué juste après... c’est ce qu’il appelle ancienne carte de collision, mais il n’en avais pas encore parlé), et mario.info.hitbox est la structure hitbox de mario dont nous voulons tester une éventuelle collision. Ce qui se produit lors d’une collision est exactement identique (à ce qui a déjà été fait).
Actuellement, nous sommes parvenus à ajouter une carte de collision mais nous n’avons réellement rien ajouté au jeu. Cependant, comme nous avons paramétré une carte de collision, il est maintenant très facile d’ajouter d’autres types de tiles. Nous commencerons simple et ajouterons le type de bloc à travers lequel vous pouvez sauter et marcher. Pour ce faire, nous allons retourner à notre carte de collision et ajouter un autre type de tiles. Voici la nouvelle carte de collision...
Et voilà l’ancienne carte de collision.
Comme vous pouvez le voir, il y a deux différences. Premièrement, nous avons ajouté un nouveau type de tile, les tiles numéro 2, en vert foncé. Nous savons que c’est le tile numéro 2 parce que nous l’avons placée à droite de la tile vert clair, la tile 1, dans le coin supérieur gauche. Nous avons placé cette tile à des endroits où nous voulons que le joueur puisse sauter à travers et attérir dessus. La deuxième chose que vous noterez est que nous avons enlevé des secteurs vert clair. Celà signifie que Mario ne heurtera pas ces secteurs. Si vous essayez la démo en ce moment, vous verrez qu’elle se joue comme un autre jeu Mario.
Donc maintenant, nous avons changé la carte, mais notre code ne l’utilise pas encore. Heureusement, c’est incroyablement facile d’ajouter ces nouvelles fonctionalités à notre code. Tout que nous devons faire est de changer un petit peu notre méthode de collision. C’est ça que nous devons changer.
while(anycollision(1,mario.info.hitbox)==4 || anycollision(2,mario.hitbox)==4){ // Collision on the bottom of the sprite... mario.y -= norm_fix(1); // Move by 1/2 pixel... } if( (TouchingGround()==1 || TouchingGround()==2) && mario.vy>0){ //if he's touching the ground and moving down he can't hit ground while moving up mario.vy = 0; mario.jumping=0; }
Comme vous pouvez le voir, voici tout ce que nous avons ajouter : || anycollision(2,mario.hitbox)==4 et || TouchingGround()==2. What this does is says that the player is colliding with something underneath him if the tile returned is number 2, our dark green tile, he is also touching the ground if he is standing on tile number 2 Celà signifie que le joueur rentre en collision vers le base si la tile retournée est la numéro 2, notre tile vert foncé, et qu’il touche également la terre s’il se tient sur la tuile le numéro 2. Nous n’ajoutons pas || anycollision(2,mario.hitbox) aux autres côtés parce que Mario ne peut heurter ces tiles que par le bas. ça lui permet ainsi de passer à travers ces tiles par le haut, puis de retomber dessus. Nous venons d’accomplir la partie 4 de cette démo de plateforme.
En regardant le code de la démo 4 du jeu de plateforme, vous avez peut être remarqué deux méthodes dont je n’ai pas encore parlé...
u8 getsprite(){ int i; for(i=0;i<128;i++){ if(sprite[i]==0){ sprite[i]=1; return i; } } return -1; } void deletesprite(u8 spritenumber){ sprite[spritenumber]=0; PA_DeleteSprite(0,spritenumber); }
Je suis sur que beaucoup d’entre vous ont réalisé ce qui arrive, et se souvienne que la numération de chaque nouveau sprite devient vraiment difficile. Ces deux nouvelles méthodes qui possède un tableau de u8 appelé sprite résoud le problème. Elles deviennent incroyablement simple à utilisé, et m’ont épargné un tas de problème. Lorsque l’on crée un objet qui possède des sprites, on fixe ces variables de sprites en utilisant getsprite(); comme cela :
mario.sprite=getsprite();
Ensuite, on crée les sprite en utilisant la variable mario.sprite comme cela :
PA_CreateSprite(0, mario.sprite,(void*)mario_Sprite, OBJ_SIZE_32X32,1, 0, fix_norm(mario.x), fix_norm(mario.y));
De cette façon, vous n’avez pas à vous souciez du numéro des sprites, et vous êtes assuré de ne pas utilisé le même numéro deux fois de suite. Pour insérer un sprite, la fonction parcourt le tableau de sprites jusqu’à ce qu’elle arrive sur un sprite vide (à 0). Après en avoir trouver un, elle change sa valeur à 1 pour montrer qu’il a été remplit et retourne le numéro. deletesprite prends le sprite que vous lui donnez (en paramètre), et le remets à zéro. Elle supprime ensuite les sprites de l’écran. Ces deux fonctions seront vraiment très utile par la suite.
Notre prochaine tâche consiste à créer des pièces qui auront le même comportement que les pièces du jeu mario. Nous nous en occuperons en utilisant la carte des collisions. Pour chaque endroit où vous souhaiterez placer une pièce, tout ce que vous aurez à faire c’est de placer un carré jaune de 8×8 (1 tile). Nous devrons ensuite faire une méthode qui scan tout le niveau, et récupére les coordonnées de toutes les pièces. Et enfin, une méthode qui efface les sprites des pièces quand elles disparaissent de l’écran, et les remets lorsqu’elles apparaissent.
Nous ajoutons les carrés jaunes où nous voulons que les pièces soient ...
Maintenant dans le jeu, partout où nous avons placé ces carrés jaunes, notre programme saura qu’il doit mettre une pièce. Encore une fois, nous en mettons un dans le coin supérieur gauche pour savoir que c’est le tile 3.
Malheureusement, çà demande un peu plus de boulot avec le code qu’avec la carte, mais c’est bon... ce n’est pas si compliqué. Nous allons déjà créer une structure pour nos pièces :
typedef struct { s32 x,y; u8 alive; u8 sprite; } coininfo; coininfo coin[maxcoins];
x et y sont les coordonnées x et y de la piècealive permet de savoir si le joueur l’a récupéré ou nonsprite est le numéro du sprite, une sprite de 0 signifie qu’elle n’est pas sur l’écran, parce que le joueur a toujours une sprite à 0coin[maxcoins] est un tableau qui cointient toutes les pièces. maxcoins est un #define où vous fixer le nombre maximum de pièces du niveau.Nous avons ensuite besoin d’un code pour placer les pièces :
//places les pièces aux endroits où se trouve du jaune void placecoins(){ int i; int j; int coinnumber=0; for(i=0;i<levellength;i++){ for(j=0;j<levelheight;j++){ if(GetTile(i*8,j*8)==3){ coin[coinnumber].x=i*8; coin[coinnumber].y=j*8; coin[coinnumber].alive=1; PA_OutputText(1,1,coinnumber,"x: %d, y: %d",coin[coinnumber].x, coin[coinnumber].y); if(coin[coinnumber].x<=fix_norm(mario.scrollx)+256 && coin[coinnumber].x>=fix_norm(mario.scrollx)){ coin[coinnumber].sprite=getsprite(); PA_CreateSprite(0, coin[coinnumber].sprite,(void*)coin_Sprite, OBJ_SIZE_8X8,1, 0, coin[coinnumber].x, coin[coinnumber].y); } coinnumber++; } } } }
Voici ce que fais ce code : Il parcourt tous les tiles du niveau, et vérifie s’il trouve des tiles numéro 3. Si c’est le cas, il place une pièce aux coordonnées x, y. La variable coinnumber sert juste à connaître le nombre de pièces que nous avons placer. Comme il n’y qu’une pièce par tile, nous devons juste vérifier chaque tile une fois. Puisqu’un tile fait 8×8, nous multiplions nos x et y (i et j) par 8. Lorsque nous trouvons un tile égale à 3 ( GetTile(i*8,j*8)==3 ), nous fixons coin[coinnumber].x et coin[coinnumer].y] à la position actuel, et nous fixons la pièce comme étant présente (valeur de alive). J’ai affiché la position de la pièce juste pour être sûr que notre code fonctionne. La ligne suivante vérifie si la pièce est sur l’écran.
getsprite(), et nous créons son sprite grâces à ces variables x et y.
A la fin de la boucle, nous ajoutons 1 à la variable coinnumber, donc à la prochaine itération, la pièce suivante du tableau coin[] sera utilisé. Cette méthode devra être appelée une seule fois avant de démarrer la boucle principale. Et maintenant, comment faire défiler les pièces correctement :
void scroll(){ if (((fix_norm(mario.x-mario.scrollx)) > 160) && (fix_norm(mario.x) < 1024-128)){ // Scroll more... mario.scrollx = mario.x - norm_fix(160); } else if ((((mario.x-mario.scrollx)>>8) < 64) && ((mario.x>>8) > 64)){ mario.scrollx = mario.x - norm_fix(64); } //automatic scrolling // mario.scrollx+=ratio_fix(1,2); // mario.scrolly+=ratio_fix(1,2); PA_ParallaxScrollXY(0, fix_norm(mario.scrollx),fix_norm(mario.scrolly)); //move player PA_SetSpriteXY(0, mario.sprite, fix_norm(mario.x-mario.scrollx), fix_norm(mario.y-mario.scrolly)); //scroll the coins int i; for(i=0;i<maxcoins;i++){ if(coin[i].alive){ if(coin[i].x<=fix_norm(mario.scrollx)+256 && coin[i].x>=fix_norm(mario.scrollx)-8){ //if it is then move it to the correct position if(coin[i].sprite!=0){ PA_SetSpriteXY(0,coin[i].sprite,coin[i].x-fix_norm(mario.scrollx),coin[i].y-fix_norm(mario.scrolly)); } //if it's not then create a sprite for it else{ coin[i].sprite=getsprite(); PA_CreateSprite(0, coin[i].sprite,(void*)coin_Sprite, OBJ_SIZE_8X8,1, 0, coin[i].x-fix_norm(mario.scrollx), coin[i].y-fix_norm(mario.scrolly)); } } //if the coin is offscren delete it else{ //don't delete already gone stuff if(coin[i].sprite!=0){ deletesprite(coin[i].sprite); coin[i].sprite=0; } } } } //PA_OutputText(1, 2, 11, "X : %d ", fix_norm(mario.x)); //PA_OutputText(1, 2, 10, "Y : %d ", mario.y); //PA_OutputText(1, 2, 12, "Scroll : %d ", fix_norm(mario.scrollx)); }
Beaucoup de choses de cette méthode devraient vous paraître familier, et nous avons ajouté la partie pour le défilement des pièces. Nous faisons d’abord une boucle sur notre tableau de pièces. Cela pourrait probablement être plus efficace, mais j’essaye de faire en sorte que çà soit simple. Tout d’abord, nous vérifions que la pièce est toujours présente (variable alive), si c’est le cas, nous faisons le défilement, sinon nous ne faisons rien. Ensuite, nous vérifions que la pièce est à l’écran : ( if(coin[i].x⇐fix_norm(mario.scrollx)+256 && coin[i].x>=fix_norm(mario.scrollx)-8) ). Si c’est le cas, nous vérifions si elle possède un sprite. Si elle ne possède pas de sprite, alors sa variable sprite sera 0. Si elle possède une sprite, tout ce que nous avons à faire c’est de la déplacer à la bonne position en utlisant PA_SetSpriteXY(0,coin[i].sprite,coin[i].x-fix_norm(mario.scrollx),coin[i].y-fix_norm(mario.scrolly));. Cependant, si elle n’est pas encore sur l’écran, nous avons besoin de lui créer un sprite. Nous fixons la variable sprite en utilisant getsprite, ainsi que les variables x, y, et sprite de la pièce. Nous allons maintenant voir ce qui se passe, lorsqu’une pièce n’est pas à l’écran. Si la pièce est en dehors de l’écran, et que la variable sprite est 0, nous ne faisons rien, nous ne voulons pas que les sprites qui ne sont pas à l’écran aient un sprite. Si elle possède un sprite, nous devons nous en débarasser. Nous le faisons en utilisant notre fonction deletesprite en lui passant le numéro de sprite de la pièce. Nous fixons ensuite, le numéro du sprite à 0, ce qui nous permettra de savoir qu’elle n’a pas de sprite associé. Et c’est tout, maintenant nous devrions avoir les pièces qui défilent, et notre prochaine et dernière tâche consistera à faire fonctionner les collisions.
u8 collision(s32 x2, s32 y2, u8 h2, u8 w2){ if ( (fix_norm(mario.x)>x2+w2) || (fix_norm(mario.x)+30<x2) || (fix_norm(mario.y)>y2+h2) || (fix_norm(mario.y)+30<y2)){ return 0; } else{ return 1; } }
Pour commencer la détection de collisions, j’ai décidé d’écrire une méthode générique pour savoir si quelque chose a rencontré mario. Vous lui passez en paramètres des coordonnées x, y, une hauteur et une largeur. Avec ces informations, nous pouvons retourner 1 s’il y a une collision avec mario, et 0 sinon. Maintenant, voyons comment l’utiliser pour vérifier les collisions avec les pièces...
void CheckCollisions(void){ while(anycollision(1, mario.hitbox)==1){ // Collision on the left of the sprite... mario.x+= norm_fix(1); // Move by 1 pixel... } while(anycollision(1, mario.hitbox)==2){ // Collision on the right of the sprite... mario.x-= norm_fix(1); // Move by 1 pixel... } while(anycollision(1,mario.hitbox)==3){ // Collision with the something on top mario.y+=norm_fix(1); mario.vy=0; } while(anycollision(1,mario.hitbox)==4 || anycollision(2,mario.hitbox)==4){ // Collision on the bottom of the sprite... mario.y -= norm_fix(1); // Move by 1/2 pixel... } if( (TouchingGround()==1 || TouchingGround()==2) && mario.vy>0){ //if he's touching the ground and moving down he can't hit ground while moving up mario.vy = 0; mario.jumping=0; } //coin collisions int i; for(i=0;i<maxcoins;i++){ if(coin[i].sprite!=0){ if(collision(coin[i].x,coin[i].y,8,8)){ coin[i].alive=false; deletesprite(coin[i].sprite); coin[i].sprite=0; } } } }
Presque tout le code est le même, seul une petite partie sous le commentaire des pièces est nouveau. Dans cette partie, nous faisons encore une boucle sur toutes les pièces. Nous vérifions d’abord qu’elles possèdent un sprite, parce que si elles n’en n’ont pas, pourquoi vérifier une collisions avec elles ? Si elles ont un sprite, nous passons à la méthode de collisions les coordonnées x et y de la pièce, ainsi que 8 pour sa hauteur et largeur. Si elle retourne vraie (1 en fait), nous fixons la variable alive de la pièce à false (valeur 0), puis nous supprimons le sprite de la pièce, et fixons le sprite de la pièce à 0. Si la méthode des collisions retourne false (0), alors nous n’avons rien besoin de faire. Et c’est tout, vous avez maintenant ajouté les pièces à votre programme.
— dustin rhodes 29/06/2006 18:24
Vous pouvez récupérer la rom/le code source avec les pièces implémentées ici... Voici la demo 5 du jeu de plateforme
Nous ne pouvons pas aller plus loin sans que notre code ne deviennent vraiment fouillis et ingérable, ou utilisant d’autres techniques plus avancées. Fondamentallement, nous allons devoir utiliser les pointeurs que j’essaierai d’expliquer aussi bien que je le peux.
Un pointeur est exactement ce qu’il dit être. Tout ce qui est dans votre programme est stocké quelque part dans la mémoire, n’est-ce pas ? Et bien un pointeur est un pointeur à cet endroit de la mémoire. Ces pointeurs peuvent pointer sur tout et n’importe quoi, comme des structures, des tableaux, ou des routines/méthodes. Ce que nous allons faire avec, c’est de laisser chaque objet connaître son propre sprite (un sprite est en fait juste un tableau), et aussi laisse chaque objet avoir sa propre méthode de défilement, et méthode d’ia (”intelligence artificielle”). De cette façon, notre code sera mieux, et plus lisible. J’en ai assez dit sur le pourquoi nous allions utiliser les pointeurs, je vais expliquer à présent la syntaxe des pointeurs.
Pour déclarer un pointeur d’un certain type de donnée, vous déclarez le type de données que vous souhaitez pointer suivit d’une * puis d’un espace, et enfin le nom de votre pointeur. Par exemple, si vous voulez un pointeur appellé pointer1 qui pointe sur un u8, vous le déclarerez :
u8* pointer1;
Maintenant que nous l’avons déclaré, nous devons lui dire ce qu’il doit pointer. Pour ce faire, vous devez utiliser le symbole &. Le symbole & signifie : “donne moi une adresse de cette variable particulière”, donc vous pourriez faire quelque chose comme çà :
pointer1 = &mario.x;
Ceci fixera pointer1 à l’adresse de mario.x, en d’autres termes pointer1 pointe maintenant sur mario.x. Bien qu’intéressant, ce n’est pas particulière utilisé, nous allons utilisé les pointeurs pour pointer sur des tableaux, méthodes et structures.
La principale utilisation d’un pointeur sur structure est de l’utiliser dans une méthodes. Habituellement, lorsqu’une structure est passé dans une méthode, la structure complète est copié en mémoire. Cela signifie que de la mémoire supplémentaire est utilisé, et si vous modifiez la structure dans la méthode les changements ne seront pas sauvegardé. Dans le but de modifier la structure dans une méthode, et prendre en compte ses changements, vous devez passer à la méthode un pointeur de cette structure, de cette façon :
void coinscroll(objectinfo* mover)
Comme vous pouvez le remarquer, le * suis le type de la variable. La seule différence que vous avez besoin de connaître, c’est qu’au lieu d’utiliser le . vous devez utiliser → donc ca devrait ressembler à çà : mover→x au lieu de mover.x Cependant si vous avez plus d’une “couche”, vous aurez : mover→info.x et non pas : not mover→info→x ou mover.info.x J’espère que c’est clair, maintenant pour notre utilisation finale, les pointeurs de méthodes.
Créer un pointeur sur un tableau est simple, voici tout ce que vous avez à faire...
Nous créons le pointeur et le tableau comme d’habitude, mais nous fixons le pointeur égal au tableau. Nous n’avons pas à utiliser le symbole & parce que le nom d’un tableau est déjà un pointeur qui pointe sur le premier élement du tableau. Si çà vous semble confus, vous pouvez également fixé le pointeur sur tableau égal à &sprite[0].
Les pointeurs sur les routines/methodes sont vraiment très utilisé car ils permettent aux méthodes/routines d’être stocké comme une variable. Pour initialiser un pointeur sur une méthode, vous avez simplement à faire ceci...
void (*ai)(u8);
void est le type retourné, * permet d’indiquer que c’est un pointeur, et ai est le nom de ce pointeur. Le (u8) est le type de variables que vous devez passer à la méthode. Pour fixer la variable égale à quelque chose ...
ai=&enemyai;
Nous utiliser le & pour prendre l’adresse et enemyai est le nom de la méthode que nous voulons pointer. Finallement, pour appeler cette méthode en utilisant le pointeur ...
(ai)(3);
(ai) est le pointeur de la méthode que nous appelons, et 3 est l’(u8) que nous lui passons. C’est tout sur les pointeurs, maintenant nous allons les utiliser dans notre code. — dustin rhodes 01/07/2006 01:17
Avant de commencer à coder en utilisant nos pointeurs, je vais vous expliquer comment notre nouveau code va fonctionner, et avec de la chance pourquoi il devrait être plus simple. Chaque objet (le joueur, les ennemies, et les items) sera une structure qui contiendra toutes les informations nécessaire pour le dessiner (x, y, pallet, nombre, taille, un pointeur sur le sprite), et aussi un méthode d’ia qui devra indiquer à l’objet ce qu’il devra faire à chaque tour, une méthode de défilement pour indiquer comment l’objet devra défiler, et une méthode de collision qui devra détecter les collisions avec le joueur et réagir en conséquence. Nous aurons de gros de tableaux avec tous ces objets dedans, à chaque tour l’ia, le défilement, et la collision seront appelés. Cela permettra de créer plus facilemet de nouveaux types d’objets. Pour le moment, disons que nous allons introduire un ennemi qui bouge de haut en bas dans notre jeu. Tout ce que nous à faire, c’est de fixer les coordonnées x et y en utilisant quelque chose d’assez similaire à ce que nous avons fait pour les pièces, ensuite de fixer son sprite égal à l’image de l’ennemi, son IA égal à y++ ou quelque chose dans le genre, et sa collision avec le joueur tuera le joueur. Vraiment très simple. — dustin rhodes 01/07/2006 01:23
Bien, il semble que vous sachiez ce que nous allons faire alors allons-y et regardons le code.
Ok, la première chose que nous avons à nous occuper c’est la structure pour nos objets.
struct objectinfo{ void (*ai)(objectinfo*); void (*scroll)(objectinfo*); void (*collision)(objectinfo*); hitboxinfo hitbox; s32 x,y; s8 sprite; u8 lastframe; u8 alive; const u8* spriteimage; u8 pallete; u8 size; s8 variables; };
Maintenant, tout ceci vous est familier, c’est ce que nous avons utilisé pour la structure de nos pièces, mais il est maintenant un peu plus générique, donc il peut contenir n’importe quel type d’objets ou d’ennemis. Nous allons expliquer le plus compliquée de cette première partie. Les trois premières lignes portent probablement à confusion, mais elles sont assez simple après des explications. La petite * devrait vous indiquer qu’elles ont quelques choses à voir avec les pointeurs. Les 3 sont des pointeurs sur des méthodes. void est le type retourné par la méthode, scroll est le nom de notre pointeur, et * indique que c’est un pointeur. objectinfo* est le paramètre de la méthode. Parce que nous utilisons un objectinfo dans notre structure d’information de notre objet, nous devons le déclarer au préalable en tapant
typedef struct objectinfo objectinfo;
Avant même la déclaration de nos méthodes. Cela signifie que lorsque nous créons notre structure, nous n’indiquons plus typedef struct , nous utilisons simplement struct, puis objectinfo. Cela signifie aussi que nous n’avons plus à mettre le nom à la fin de la structure. Ok, donc maintenant nous savons ce que veulent dire les 3 premières lignes, avec de la chance. hitboxinfo stockera les points de collisions de notre objet. x et y devrait être assez clair, ce sont les coordonnées de l’objet. sprite contiendra le numéro de sprite de l’objet. lastframe est la dernière image qui sera utilisée dans l’animation normale de l’objet. alive nous indiquera si l’objet est en vie ou non, 0 = non, 1 = oui. Maintenant, nous allons passer à une chose un peu plus compliquée...
const u8* spriteimage;
C’est simplement un pointeur sur un tableau d’un const u8, le type utilisé pour stocker les images, nous l’utiliserons à chaque fois que nous voudrons dessiner notre objet. palette est utilisé pour stocker la palette que l’objet utilisera, et size est la taille de l’objet. 0 indiquera une taille de 8×8, 1 sera une taille de 16×16, et ainsi de suite. Finalement, variables est une variable générique qui sera utilisée pour les objets d’ia.
Maintenant, je vais vous montrer toutes les nouvelles/modifications des méthodes que nous devons créer pour faire notre nouveau système facile à utiliser. Techniquement, vous n’avez pas besoin de savoir comment elles fonctionnent, il vous serait possible de les utiliser comme çà mais je pense qu’en général il est préférable de les comprendre.
Cette méthode est utilisé à chaque fois que nous voulons placer un nouvel objet sur la carte...
void newobject(s32 x, s32 y, objectinfo* object, objectdata* data){ object->spriteimage=data->spriteimage; object->lastframe=data->lastframe; object->pallete=data->pallete; object->size=data->size; object->ai=data->ai; object->x=norm_fix(x); object->y=norm_fix(y); object->alive=1; object->sprite=-1; object->scroll=data->scroll; // fixe la méthode de défilement des pièces object->hitbox=data->hitbox; object->collision=data->collision; object->variables=data->variables; }
Comme vous pouvez le voir, la méthode prends en paramètre x et y, donc elle sait où mettre l’objet, ainsi qu’un pointeur sur un objectinfo et un objectdata. Nous utilisons ces pointeurs pour deux raisons... 1. Passer des pointeurs est plus rapide que de passer des strutures complètes. 2. Si nous passons l’objet, c crée une copie de cet objet pour l’utiliser dans cette méthode, et donc aucun des changements que nous pourrions faire dans cette méthode ne serait pris en compte en dehors de cette méthode. Notre méthode utilise un struct d’objectdata pour fixer toutes les variables dans le struct d’objectinfo. C’est beaucoup mieux, et plus soigneux que d’avoir à le faire manuellement à chaque fois que nous voulons créer un objet. Après avoir vu le struct d’objectinfo, objectdata sera très commun. Le struct d’objetdata ressemble à ceci ...
struct objectdata{ void (*ai)(objectinfo*); void (*scroll)(objectinfo*); void (*collision)(objectinfo*); const u8* spriteimage; u8 h,w; u8 lastframe; u8 pallete; u8 size; hitboxinfo hitbox; u8 variables; s16 tile; };
La petite chose que vous auriez pu noté à propos de ceci, c’est la variable tile. Nous l’utilisons pour savoir quand placer l’objet. Par exemple, fi nous voulons placer l’objet sur tous les tiles numéro 4, nous devrons fixer cette variable tile à 4. C’est mis en place dans la méthode placeobjets.
if(GetTile(i*8,j*8)==data[k].tile){ newobject(i*8,j*8, &coin[getobject()], &data[k]); }
Comme vous pouvez le voir maintenant lorsque nous ajoutons un nouvel objet nous n’avons plus à modifier cette méthode.
Notre deletesprite sera utilisé pour supprimer les sprites, et être sur que toutes les variables sont correctement fixé lorsque nous l’appelons... Our deletesprite will be used for deleting sprites and making sure all our variables get set correctly when we do so...
void deletesprite(objectinfo* todelete){ sprite[todelete->sprite]=0; PA_DeleteSprite(0,todelete->sprite); PA_StopSpriteAnim(0,todelete->sprite); todelete->sprite=-1; }
Cette méthode prends aussi un pointeur sur un objectinfo. En premier lieu, elle met un 0 dans notre tableau à la position de l’objet sprite, donc notre progamme sait que nous pouvons réutiliser ce sprite. Ensuite, nous supprimons complètement le sprite en utilisant PA_DeleteSprite. Puis, nous arrêtons l’animation du sprite pour plus de sureté. Et pour finir, nous mettons l’objet sprite supprimé à -1, ce qui nous permet de savoir qu’il ne sera plus affiché. Peut être avez vous noté que lorsque nous avons un pointeur sur tableau au lieu d’un simple tableau, vous utilisez → au lieu de .
Elle sera utilisée à chaque fois que nous voulons créer un sprite. Elle est particulièrement propre, parce que vous lui passez simplement une structure d’ objectinfo et le sprite qui doit être créé. Cette méthode explique pourquoi nous avons mis toute l’information dans notre structure.
void createsprite(objectinfo* todraw){ todraw->sprite=getsprite(); PA_CreateSprite(0, todraw->sprite,todraw->spriteimage, 0,todraw->size,1, todraw->pallete, fix_norm(todraw->x), fix_norm(todraw->y)); if(todraw->lastframe)PA_StartSpriteAnim(0,todraw->sprite,0,todraw->lastframe,6); }
Tout comme deletesprite, cette méthode prends un pointeur sur objectinfo. Premièrement, nous récupérons la variable du sprite, et lui attribuons (au struct objectinfo). Ensuite, nous dessinons le sprite, ce qui est un peu plus compliqué que d’habitude. Nous prenons l’écran 0, comme toujours. Puis, nous lui passons le numéro du sprite de l’objet à l’aide de todraw→spriteimage. todraw→spriteimage est le pointeur de l’image pour cet objet en particulier, et nous lui passons ensuite. 0 montre que c’est un sprite de type 256 couleurs, todraw→size indique à CreateSprite la taille du sprite, et 1 montre que le sprite est carré. Enfin, todraw→pallete donne la bonne palette, et x et y donne ces coordonnées. Après avoir créé le sprite, nous vérifions que cet objet possède une dernière frame (çà signifie qu’il possède une animation), et si c’est le cas, nous démarrons l’animation.
Cette méthode ne devrait pas énormément changer, mais on doit maintenant la faire fonctionner pour les objets autres que mario. Pour ce faire, nous devons passer à la fonction TileCollision les coordonnées x et y du point que nous devons vérifier. On fait comme ceci ...
u8 TileCollision(pointinfo* point, u8 flipped, s32 x, s32 y){ return (GetTile(fix_norm(x)+point->x + (flipped*point->flipx), fix_norm(y)+point->y)); }
Maintenant, nous avons juste besoin de changer notre méthode anycollision pour qu’elle fonctione avec les modifications effectuées. Tout ce que nous avons à faire, c’est passer à anycollision le tile de collision, ainsi qu’un objet, et nous pourrons voir si l’objet est en collision avec le tile.
//checks if any of the collisions are true for that tile and returns what side u8 anycollision(u8 tile, objectinfo* object){ if(TileCollision(&object->hitbox.left, object->hitbox.flipped, object->x, object->y)==tile)return 1; if(TileCollision(&object->hitbox.right, object->hitbox.flipped, object->x, object->y)==tile)return 2; if(TileCollision(&object->hitbox.up, object->hitbox.flipped, object->x, object->y)==tile)return 3; if(TileCollision(&object->hitbox.down, object->hitbox.flipped, object->x, object->y)==tile)return 4; else return 0; }
Delete sprite était déjà utilisé, et nous allons juste lui ajouté des fonctionnalités, donc nous n’avons pas à l’utiliser à d’autres endroits. createsprite est basiquement un remplacement de PA_CreateSprite donc j’ai cherché dans le programme, et remplacer tous les occurences de PA_CreateSprites par createsprite. createobject est utilisé là ou nous avons précédement créé les pièces. Donc maintenant, au lieu de se compliquer la tâche en écrivant toutes les informations partout où nous trouvons un bloc jaune, nous utilisons simplement la méthode createobject qui le fait pour nous. Elle peut le faire, parce que nous lui avons indiqué toutes les données des pièces, ainsi que les struct(ures) objectdata
//set up the coin data data[0].spriteimage=coin_Sprite; data[0].lastframe=0; data[0].pallete=0; data[0].size=0; data[0].ai=&noai; data[0].scroll=&objectscroll; //set the scroll method of the coin data[0].collision=&coincollision; data[0].hitbox.right.x=8; data[0].hitbox.right.y=4; data[0].hitbox.left.x=0; data[0].hitbox.left.y=4; data[0].hitbox.up.x=4; data[0].hitbox.up.y=0; data[0].hitbox.down.x=4; data[0].hitbox.down.y=8; data[0].tile=4;
Comme vous pouvez le constater, spriteimage pointe sur coin_Sprite, la dernière frame (image) est à 0 parce que les pièces n’ont pas d’animations, la palette est à 0, la taille est 0 ce qui implique une taille de 8×8. Les pièces n’ont pas d’IA, donc nous lui donnons l’adresse de notre méthode noai, une pièce défile normalement donc nous lui passons la méthode de défilement des objets, et finallement pour les collisions, les pièces posssèdent la méthode coincollision qui permet de supprimer les pièces si mario les touches. Les quatres lignes suivantes sont simplement les informations d’hitbox des pièces.
Maintenant, vous vous demandez sûrement quel est l’avantage de ce nouveau système. On s’est donné tout ca mal pour l’implémenter mais maintenant nous en sommes encore là où nous avons commencé, avec un seul type d’objet. Maintenant je vais faire tout le processus pour créer un ennemi et en le faisant, je vais expliquer ce que font l’ia, les collisions, et le scrolling...
Avant d’apprendre comment écrire ces trois types de méthodes, je vais vous montrer où elles sont appelées.
u16 i; for(i=0;i<maxcoins;i++){ //scroll all the coins if(coin[i].alive){ (coin[i].scroll)(&coin[i]); (coin[i].ai)(&coin[i]); PA_OutputText(1,1,i,"x:%d, y:%d",fix_norm(coin[i].x), fix_norm(coin[i].y)); } }
Comme vous pouvez le voir, nos objets sont toujours dans un tableau appelé “coin” (NDT: pièce), mais ne laissez pas cela vous troubler. Cette partie du code est dans notre méthode de scroll pour qu’après que le scrolling de mario et du background aient été effectués, ce bout de code soit atteint. Il fait une boucle sur tous les objets dans le tableau “coin” et pour chacun d’entre eux appelle la méthode pointée par le pointer “scroll” de l’objet et passe à cette méthode l’adresse de l’objet courant.
(coin[i].scroll)(&coin[i]);
Après avoir fait ça, il fait la même chose pour l’ia...
(coin[i].ai)(&coin[i]);
Les méthodes de collision sont appelées dans la méthode check collisions. Après que les collisions de Mario aient été testées, on teste encore tous les objets dans le tableau “coin”.
int i; for(i=0;i<maxcoins;i++){ if(coin[i].sprite!=-1 && coin[i].alive){ //coincollision(&coin[i]); (coin[i].collision)(&coin[i]); } }
Pour gagner du temps, on ne teste les collisions que si l’objet est sur l’écran (sprite différent de -1) et que l’objet est en vie. Maintenant que nous savons où ces trois méthodes sont appelées, intéressons nous à leur écriture.
L’ia, la détection de collisions, et le scrolling des pièces sont très faciles donc je vais vous montrez ceux-là en premier.
Une chance pour vous, une pièce n’a pas d’ia, elle ne bouge pas du tout par elle-même, donc sa méthode d’ia est...
void noai(objectinfo* bady){ }
notez que même si la méthode est vide, elle doit quand même exister. Elle doit exister car on va passer par tous les objets et appeller ces méthodes d’ia. Si un objet n’a pas de méthode d’ia, alors notre programme va freezer. Et aussi, les méthodes pour l’ia, les collisions, et le scroll doivent toutes prendre en paramètre un pointer sur un type objectinfo ou notre programme freezera.
La détection de collision pour nos pièces est un petit peu subtile, mais pas trop...
void coincollision(objectinfo* object){ if(boxcollision(object->x,object->y,8+object->size*8,8+object->size*8)){ object->alive=false; deletesprite(object); } }
Comme vous pouvez le voir, cette méthode a le même prototype que notre méthode d’ia, son type de retour est void, et elle prend en paramètre un pointeur sur objectinfo. Tout ce que fait cette méthode est tester s’il y a une collision aux coordonnées x et y de notre pièce, avec un objet de size*8+8 de large et de size*8+8 de haut. Cela signifie que si la taille est de 0, ça testera une boite de 8×8, si la taille est de 1, ça testera une boite de 16×16, et ainsi de suite. S’il y a collision, on met la variable alive à false et on efface le sprite en utilisant deletesprite.
La méthode de scroll va être la même pour beaucoup de choses. L’exemple de quelque chose aui aurait une méthode de scroll différente de la normale pourrait être les lakitus qui volent dans les nuages. Ils ne scrollent pas avec le background mais rebondissent en haut de l’écran. Cependant, si vous avez un doute, faite juste pointer le pointer “scroll” sur cette méthode.
void objectscroll(objectinfo* mover){ //check if coin is onscreen if(fix_norm(mover->x)<=fix_norm(mario.scrollx)+256 && fix_norm(mover->x)>=fix_norm(mario.scrollx)-8){ //if it is then move it to the correct position if(mover->sprite!=-1){ PA_SetSpriteXY(0,mover->sprite,fix_norm(mover->x)-fix_norm(mario.scrollx),fix_norm(mover->y)-fix_norm(mario.scrolly)); } //if it's not then create a sprite for it else{ createsprite(mover); } } //if the coin is offscren delete it else{ //don't delete already gone stuff if(mover->sprite!=-1){ deletesprite(mover); } } }
Ce code devrait vous dire quelque chose car on l’a déjà utilisé avant. Tout ce qui a été fait, c’est copier et coller ce bout de code de la méthode playerscroll dans sa propre méthode. — dustin rhodes 24/07/2006 19:32
OK, maintenant nous allons écrire un nouveau type d’objet. Cette fois nous allons faire un goomba pour que vous puissiez voir à quel point il est facile d’ajouter des nouveaux types d’objet. La premère chose que nous allons faire est changer notre tableau de objectinfo (celui qui s’appelle actuellement ‘coin’ (NDT: pièces) pour lui donner un nom plus approprié. A partir de maintenant, le tableau coin[] sera le tableau object[]. C’est simplement pour éviter les confusions maintenant qu’il va contenir des pièces mais aussi notre nouveau type d’objet.
La première tâche comme d’habitude est de mettre une nouvelle couleur dans la carte de collision pour notre nouvel objet. On fera ça exactement comme on l’a toujours fait...
Comme vous pouvez le voir, notre méthode placeobject récupère ces informations sur notre nouveau type d’objet de data[1] donc nous devons régler les variables appropriées.
//set up the goomba data data[1].spriteimage=goomba_Sprite; data[1].lastframe=1; data[1].pallete=0; data[1].size=1; data[1].ai=&simpleai; data[1].scroll=&objectscroll; data[1].collision=&badycollision; data[1].variables=1; data[1].hitbox.right.x=16; data[1].hitbox.right.y=8; data[1].hitbox.left.x=0; data[1].hitbox.left.y=8; data[1].hitbox.up.x=8; data[1].hitbox.up.y=0; data[1].hitbox.down.x=8; data[1].hitbox.down.y=16;
Ce morceau de code va au début de notre méthose main juste après avoir rempli les variables de data[0]. Vous devriez vous souvenir de ce que toutes ces variables signifient pour notre coin object, je vais donc juste expliquer les nouvelles. Last frame est mis à 1 au lieu de 0 ca notre goomba a 2 frames d’animation, 0 et 1, qui vont faire une boucle. size est à 1 au lieu de 0 car le goomba est 16×16 au lieu de 8×8. Les méthodes d’ia et de collision sont différentes mais la méthode de scroll est exactement la même que pour la pièce. C’est un autre avantage de ce système, on peut facilement réutiliser n’importe laquelle de nos méthodes d’ia, de scroll, ou de collision.
Vu que la méthode de scroll est exactement la même, je ne vais pas m’étendre plus sur le sujet. Cependant, l’ia et les collisions sont différentes, nous allons donc y jeter un oeil.
OK, voici l’ia de notre goomba. Il ne fait que tomber sur le sol, et marcher dans une direction jusqu’à ce qu’il rencontre un obstacle. A ce moment, il se tourne et marche dans l’autre direction.
void simpleai(objectinfo* bady){ bady->x+=norm_fix(1)*bady->variables; bady->vy += GRAVITY; bady->y += bady->vy; while(anycollision(1, bady)==1){ // Collision on the left of the sprite... bady->x+= norm_fix(1); // Move by 1 pixel... bady->variables=bady->variables*-1; } while(anycollision(1, bady)==2){ // Collision on the right of the sprite... bady->x-= norm_fix(1); // Move by 1 pixel... bady->variables=bady->variables*-1; } while(anycollision(1,bady)==3){ // Collision with the something on top bady->y+= norm_fix(1); } while(anycollision(1,bady)==4 || anycollision(2,bady)==4){ // Collision on the bottom of the sprite... bady->y -= norm_fix(1); // Move by 1/2 pixel... bady->vy=0; } }
La première ligne de code ne fait que bouger le goomba d’un pixel, à gauche ou à droite. Si variables est égal à 1 alors c’est sur la droite. Si c’est -1, alors c’est à gauche. Ensuite on ajoute la vitesse y de l’ennemi à sa position y, et on ajoute ensuite la gravité à sa vitesse y exactement comme pour le joueur. Tout le reste de l’ia devrait vous sembler assez familier. C’est la meme chose que pour le joueur, à quelques détails près. Au lieu de passer le joueur en paramètre à anycollision, nous lui passons le méchant. Si le méchant rentre en collision avec quelque chose sur la droite ou la gauche, variable est multipliée par -1. Cela signifie que si le méchant rentre en collision avec quelque chose, il va se tourner. Et c’est tout. Maintenant, la méthode de collision.
La méthode de collision n’est pas trop compliquée. Tout ce qu’elle fait est tester pour une collision et s’assurer que le joueur est au dessus du goomba. Si le joueur n’est pas au dessus du goomba, alors il meurt.
void badycollision(objectinfo* object){ if(boxcollision(object->x,object->y,8+object->size*8,8+object->size*8)){ if(fix_norm(mario.info.y)+25<fix_norm(object->y)){ object->alive=0; deletesprite(object); } else{ //player dies } } }
Le if du début teste si une boite de 16×16 (car notre goomba est de taille 1, 1*8+8 donne 16) à l’endroit ou se trouve le goomba est rentrée en collision avec le joueur. Si c’est le cas, alors on teste si la coordonnée en y du joueur plus sa hauteur (donc la position de ses pieds) est au dessus de la coordonnée y du goomba (le dessus de la tête du goomba). Si c’est le cas, le goomba meurt (son alive est réglé à 0) et on l’efface. Si ce n’est pas le cas, et bien en fait pour le moment rien ne se passe, mais si vous vouliez vraiment que le joueur meurt ici comme dans un vrai jeu Mario, alors on mettrait sa variable alive à 0 et on effacerait son sprite.
Voila le source de notre dernière version du code. Nous n’avons pas tout vu, mais vous devriez être capables de comprendre par vous même. Download
J’espère que ce guide à réussi à vous montrer comment vous rapprocher d’un vrai jeu de plate-forme. Je sais que certaines parties, surtout le petit bout à propos des pointeurs, sont troublantes, mais je vous suggère de jouer avec le code si vous ne le comprenez pas. Mais maintenant il est temps que j’aille travailler pour finir mon propre jeu de plate-forme. Bonne chance à tous dans la poursuite de votre code, à plus tard.
Dustin
— dustin rhodes 24/07/2006 19:46