il y a 6 ans
Je m'excuse par avance pour ceux qui aurons les yeux qui saigne a cause de mes fautes de "frappes" ;) .
Niveau intermédiaire
Création d’un jeu de shoot.
Prérequis
But :
Nous allons initialiser les différents états de notre jeu :
Pour chaque etat de notre jeux nous allons créer un fichier ino qui gerera uniquement cet etat.
Chaque fichier porte l’initialisation/les mise à jour et le graphisme uniquement pour lui.
Dans chaque fichier on a une method init [NomDuFIchier] (),update [NomDuFIchier] (),draw [NomDuFIchier] ()
Ceci nous permet d’avoir un fichier qui gère un seul état et une méthode qui gère un seul comportement.
//appeler une fois pour initialiser les etat de ce fichier // par exemple si on est sur le joueur on vas le positionner sur le point de depart void init() { } //appeler a toutes les frame , gere le comportement de ce fichier //par exemple si on est sur le fichier du joueur c'est ici que nous allons detecter les pression sur les boutons // pour deplacer le personnage void update() { } //permet de dessiner notre objet //le fait de bien separer le comportement des graphisme me permet de commencer ace de simple rectagle // puis d evoluer sur des sprite plus complexe sans avoir a retoucher aux methodes de comportement // souvent plus complexe et plus dure a maintenir void draw() { }
Source :
https://github.com/Clement83/1942Shooter/releases/tag/1_skeleton
https://github.com/Clement83/1942Shooter/tree/1_skeleton
Maintenant que le squelette est fait nous allons passer au jeu.
Pour commencer nous allons avoir besoins de trois nouveaux fichiers
Player.ino, ennemies.ino, et playerBullet.ino
Nous allons aussi déclarer les différentes structures dont nous aurons besoin et déclarer les variables globale dans 1942Shooter.ino
pour nos objets nous utilisons des stucture qui nous permettent de regrouper plusieur variable sous une meme variable
struct Player { int y; int life; boolean fire; };
Les ennemies, le joueur et , les balle serons utiliser à plusieurs endroit ils sont donc déclarer en globale
Dans inGame.ino on appelle les méthodes de nos nouveau fichiers (update/init /draw)
Comme énoncé plus haut pour les états , chacun de nos nouveau fichier n'aura qu'un type d'objet a géré. par exemple le fichier player.ino gerera les mouvements du players, l'affichage du player, et l'initialisation du player. et puis c est a peux pres tout :)
On ajoute dans inGame.ino la détection de la fin de la partie (soit le joueur a tué tout le monde soit un ennemi a franchi votre ligne) et le tour est joué.
chaque etat de notre jeux et le seul maitre de son evolution.
C est lui qui vas appeler les methodes init/update/draw des objet qu il gere
et c est lui qui sait a quel moment il vas changer d etat et dans quel etat il peux passer
par exemple pour l etat in game :
// une method nous permet de detecter la fin du jeux boolean isEndGame() { int dead = 0; for(int i = 0 ; i< NB_MAX_ENNEMIES; ++i) { if(ennemies[i].life>0) { if(ennemies[i].x< 2) { return true; } } else { ++dead; } } if(dead>=NB_MAX_ENNEMIES) { return true; } return false; } //dans l init il initialise tous les objets qu il manage void initInGame() { initPlayer(); initEnnemies(); initPlayerBullet(); } // de meme il update tous les objets void updateInGame() { updatePlayer(); updateEnnemies(); updatePlayerBullet(); if(isEndGame()) { //si la fin de parti est valider on passe sur l etat game over gameState = STATE_GAME_OVER; } } //on dessie tous les objet lié void drawInGame() { drawPlayer(); drawEnnemies(); drawPlayerBullet(); }
Source :
https://github.com/Clement83/1942Shooter/releases/tag/2_in_game
https://github.com/Clement83/1942Shooter/tree/2_in_game
Nous allons rendre les ennemies moins prévisible et plus armé.
Premièrement nous créons un fichier ennemiesBullet qui gérera les balles ennemies
Ce fichier est un copier collé du fichier playerBullet, et nous verrons rapidement que beaucoup des méthodes de ces deux fichier sont identique. Nous les factoriserons plus tard. Pour le moment on renomme les player par des Ennemies et on inverse le sens des balles :
ennemyBullet[i].x -= VELOCITY_ENNEMY_BULLET;//go bullet go!
Bien voire le -= a la place du +=
Dans 1942Shooter nous ajoutons les variables de gestion des balles ennemies
#define NB_ENNEMY_BULLET 10 Bullet ennemyBullet[NB_ENNEMY_BULLET];
Petite modification aussi dans la methode createNewEnnemy Bullet.
Cette method prend en paramètre un ennemy pour pouvoir positionner la nouvelle balle
void createNewEnnemyBullet(Ennemies ennemy)
Dans le fichier ennemies nous allons ajouter une method actionEnnemy qui prend en parametre un pointeur sur un ennemy.
void actionEnnemy(Ennemies *ennemy)
Cette methode vas modifier l’instance de l’ennemy c’est pour cela que nous envoyons un pointer (aussi appelé une référence) plutôt qu'une valeur. (étant développer web c#/php/javascript je ne suis plus tres au fait des pointeurs en C, escusez moi si je raconte des truc plus ou moins faut)
Avec un random et deux if on gere les action de nos ennemies de façon aleatoir .
Attention pour modifier l ennemy nous utilison la syntaxe -> car nous somme sur un pointeur
ennemy->x
pour le moment j ai mis : à chaque frame on a 3% de tirer et 27 % de chance d avancer
il y a donc 70% de chance de rien faire.
A ce niveau-là nous avons une IA bien plus intéressante
Release : https://github.com/Clement83/1942Shooter/releases/tag/3_ia_enn
Code : https://github.com/Clement83/1942Shooter/tree/3_ia_enn
Ce soir un peu de « déco » pour le jeu
Je fais un personnage unique qui servira pour le moment aux ennemies et au joueur
Ce personnage devra être capable de rester sans bouger, de courir et de tirer. Je prends 2 frame par animation (une seul pour le sans bougé)
Dans la structure Player je veux savoir combien de temps dure mon tire
Pour cela je change mon boolean fire en int. Cela devient un compteur de frame
Je ne pourrais tirer que si le compteur est à zéro. Au moment du tire je l initialise a 3 pour faire un tour de mon animation.
Evidemment je met ce nombre dans une constante pour pouvoir le modifier facilement
#define NB_FRAME_FIRE_ANIM 3
Dans le updatePlayer je mets à jour ce compteur :
if (soldat.fire>0) { --soldat.fire; }
Ne pas oublier de modifier dans playerBullet/updatePlayerBullet
if(soldat.fire == NB_FRAME_FIRE_ANIM) { createNewSoldatBullet(); }
De même il faut que je sache si mon personnage est en mouvement ou non.
Dans un jeu avec un minimum de physique nous aurions pu nous baser sur les variables de vélocité du personnage. Ici nous n’en avons pas il faut donc ajouter une variable qui nous indique l’état de notre personnage.
struct Player { int y; int life; int fire; boolean isRun; };
Le boolean isRun nous permettra de savoir quel sprite afficher je le met a false dans l'init du player et en début d update , si j’appuis sur haut ou bas je le met à true
Enfin je draw tout ca :
void drawPlayer() { gb.display.setColor(GREEN); if(soldat.fire == NB_FRAME_FIRE_ANIM) { gb.lights.fill(YELLOW); } if(soldat.fire>0) { gb.display.drawImage( SOLDAT_X , soldat.y , soldatFire); } else if(soldat.isRun == true){ gb.display.drawImage( SOLDAT_X , soldat.y , soldatRun); } else { gb.display.drawImage( SOLDAT_X , soldat.y , soldatIdl); } }
Je fais pareil pour les ennemies en changeant les nom des image par ennemiyMonNomDiMage .
J’y ajouter une subtilité, les ennemies ne font rien pendant le tire
Pour voir les personnages il faut ajouter un fond au monde.
Nous allons donc ajouter un fichier ino world.ino avec la même structure que les précèdent.
Pour le moment seul le draw aura une action (peindre le fond d’une couleur, en vert par exemple) mais dans la venir on pourra imaginer des animations de fond ou autre qui viendront s’ajouter ici
J’ajoute les appels à initWorld/updateWorld/drawWorld dans inGame.ino
J’en profite pour rappeler que l’ordre dans lequel sont appelées les méthodes est important.
Mettre draw world en dernier et votre écran sera complètement recouvert de vert .
Source :
Release : https://github.com/Clement83/1942Shooter/releases/tag/4_player_sprite
Code : https://github.com/Clement83/1942Shooter/tree/4_player_sprite
Je vais passer aux barricades pour protéger notre soldat.
Ces barricade aurons un certain nombre de point de vie et donc on les verra ce dégrader au fur et à mesure qu’elles se font tirer dessus.
Je passerais rapidement sur l’implementation des baricade car nous allons suivre le shema habituel
Création d’un barricade.ino avec les methodes init/update/draw.
A ce point du tuto on commence a ressentire les limite de cette architecture.
Avec de la programmation orienté objet nous aurions pu faire une arborescence d’héritage qui nous aurais permis d’éviter pas mal de copier-coller. C’est un problème qu’essaye de résoudre par exemple @ZappedCow avec GBX.
Ceci étant dit on passe à la suite.
Lors de la création du fichier barricade ne pas oublier d’ajouter l’appel des méthodes init/update/draw dans inGame.ino
Attention l’ordre du draw a une importance, je mets le draw des barricades avant le draw des balles mais après celui des ennemies.
Plus haut je dis que nous voyons ici les limites de cette architecture.
Mais nous voyons aussi toute la puissance de bien organiser son code.
En 30 minutes et sans aucun risque que plus rien ne marche j’ai pu ajouter la gestion des barricades
release: https://github.com/Clement83/1942Shooter/releases/tag/5_barricade
code: https://github.com/Clement83/1942Shooter/tree/5_barricade
A venir
NEW il y a 6 ans
Bon rien à branler... non je déconne c'est un clin d’œil au readme du github. Ce que j'aime: l'explication de comment structurer son programme. Ce que j'aime un peu moins c'est que les fonctions principales sont nommées mais pas expliquées. Bien sûr on comprend globalement que l'update va mettre à jour l'objet (position, comportement etc...) mais bon pour les grands débutants que certains dont moi sont, un peu plus d'explications seraient les bienvenues. Ce tuto se place après ceux publiés par Aurélien et permet de bien structurer un programme.
En tout cas merci pour ce programme et ces explications. Je vais essayer de les mettre en pratique ;)
NEW il y a 6 ans
Pour le moment je n'ai pas regarder mais le tuto a l'air vraiment sympas, je regarde ca dans la semaine
@jicehel: ton rien a branler m'a choqué sur le coup xD
NEW il y a 6 ans
C'est fait exprès... J'avoue que la lecture du commentaire de Clément sur l'utilisation de son source m'a fait rire. Pas très orthodoxe mais efficace alors je lui ais fait ce petit clin d'oeil un peu choquant... :D
NEW il y a 6 ans
Merci pour les retours ,
il y a encore beaucoup de travail pour arrivé un un tuto digne de ce nom.
+ d'images, + de codes, + d'explications
la licence je la ferais passer dans un fichier licence elle sera moins visible pour les ames sensible ;)
Au passage j ai fixé un bug sur l étape 2. le nouveau binaire
NEW il y a 6 ans
Clément, j'ai compil les sources, mais je passe directement du splash screen au gameover en appuyant sur le bouton A et de nouveau au splash screen puis encore au gameover.
A priori, c'est lié à la procédure isEndGame() qui renvoie vrai dès le début du jeu et donc on revient au SplachScreen. J'ai commenté dans updateInGame l'appel du test pour pouvoir tester le jeu et détruire les carrés rouges ennemis.
NEW il y a 6 ans
tu as fait avec les dernières sources ?
Le bug que tu as est lié au condition de retour de la method isEndGame il manque le return false; a la fin normalement c'est fixé (cf mon dernier com).
NEW il y a 6 ans
Hello,
Je tente de comprendre, j'ai une première question.
Je vois partout des int16_t, uint8_t, uint16_t, etc. : Qu'est-ce que c'est ? j'en vois dans tous les programmes...
Nux
il y a 6 ans
Si jamais je me trompe corrigez moi:
1/
int = integer
interger veux dire "nombre entier" en francais
2/
u = unsigned
Veux dire sans signe en francais (donc pas de chiffre négatif)
3/
le nombre (16,8,...) c'est la quantité de chiffre que la variable peut stocké en binaire
4/
Le _t est un mystere pour moi, je me demande si ca ne veux pas dire que la variable est un type et non une reference mais comme je ne suis pas sûr je n'expliquerais pas et la nuance peut être dure a saisir. Pour le moment n'y pense pas.
------------------------------------------
Voila avec ca on peut repondre a t'as question.
uint16_t = unsigned integer 16 binar = un chiffre entier positif qui peut stoker un chiffre d'un taille maximal de 2^16.
Donc ton nombre stoker doit être compris entre [ 0 ; 2^16 -1] = [ 0 ; 65 535 ]
int16_t c'est donc exactement comme le uint16_t mais on inclus les nombre negatif ce qui fait que l'on doit repartire nos 2^16 chiffre possible équitablement entre les + et les - . (note on mettra le 0 avec les chiffre positif)
Ce qui fait que ton nombre stoker doit être compris entre [ - 2^8 ; 2^8 -1 ] = [ -32 768 ; 32 767 ]
(note: 2^8 est égale a 2^16/2 operation que je fait car je fait 50-50 entre les plus et les moins)
Note Final
Le classique int que l'on utilise en c++ est un int32, à toi de trouvé les nombres possible
NEW il y a 6 ans
Si jamais je me trompe corrigez moi:
1/
int = integer
interger veux dire "nombre entier" en francais
2/
u = unsigned
Veux dire sans signe en francais (donc pas de chiffre négatif)
3/
le nombre (16,8,...) c'est la quantité de chiffre que la variable peut stocké en binaire
4/
Le _t est un mystere pour moi, je me demande si ca ne veux pas dire que la variable est un type et non une reference mais comme je ne suis pas sûr je n'expliquerais pas et la nuance peut être dure a saisir. Pour le moment n'y pense pas.
------------------------------------------
Voila avec ca on peut repondre a t'as question.
uint16_t = unsigned integer 16 binar = un chiffre entier positif qui peut stoker un chiffre d'un taille maximal de 2^16.
Donc ton nombre stoker doit être compris entre [ 0 ; 2^16 -1] = [ 0 ; 65 535 ]
int16_t c'est donc exactement comme le uint16_t mais on inclus les nombre negatif ce qui fait que l'on doit repartire nos 2^16 chiffre possible équitablement entre les + et les - . (note on mettra le 0 avec les chiffre positif)
Ce qui fait que ton nombre stoker doit être compris entre [ - 2^8 ; 2^8 -1 ] = [ -32 768 ; 32 767 ]
(note: 2^8 est égale a 2^16/2 operation que je fait car je fait 50-50 entre les plus et les moins)
Note Final
Le classique int que l'on utilise en c++ est un int32, à toi de trouvé les nombres possible
clement
il y a 6 ans
+1
je sais pas si sur la gamebuino meta les int8_t sont plus efficace que les int16_t, mais c était le cas sur la gamebuino classic.
du coup j ai pris l habitude d utiliser au max les "8 bit" si je peux et les "16 bit" si j ai besoin d aller plus loin dans mon incrémentation.
NEW il y a 6 ans
+1
je sais pas si sur la gamebuino meta les int8_t sont plus efficace que les int16_t, mais c était le cas sur la gamebuino classic.
du coup j ai pris l habitude d utiliser au max les "8 bit" si je peux et les "16 bit" si j ai besoin d aller plus loin dans mon incrémentation.
NEW il y a 6 ans
Merci pour toutes vos explications. J'ai une dernière question qui je pense sera complémentaire : à quoi ça sert ces uint_t8, int_t8, uint_t16 et int_t16 ? dans quels cas les utilise-t-on ? merci :)
NEW il y a 6 ans
It feels not good when many is written in France.
Would be nicer when most is written in English, then more people can understand it.
NEW il y a 6 ans
Vous pouvez utiliser du "int" tout court de partout, c'est du 32 bit signé, c'est pas moins performant (on a un processeur 32 bit maintenant), et ça sera plus lisible pour les débutants :)
Erratum: int c'est du 32 bit, pas du 16, mais ça fait pas grande différence. C'est pas plus lent, ça prend juste un poil de mémoire en plus.
NEW il y a 6 ans
You know that not all French people speak English, just like all English people don't speak French, right ?
The language is starting to be a real issue I have to agree, we are working on a solution.