1942 Shooter

Créations

clement

il y a 6 ans

Tuto creation d'un jeu : 1942Shooter

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.

  • Le joueur sera à gauche les ennemis arriverons de la droite.
  • Le joueur pourra bouger de bas en haut
  • Les ennemis bougeront de droite à gauche
  • Le joueur perds si un ennemis arrive jusqu’à lui ou si un ennemi le tue
  • Le joueur devra tirer sur les ennemis pour les arrêter

Prérequis

  • Avoir une Gamebuino META
  • Avoir fait l'atelier Installation de la Gamebuino META
  • Avoir un minimum de connaissance en programmation


But :

  • Structurer un programme pour avoir un code propre et réutilisable.
  • Utiliser la notion d’état pour passer d’un état à l’autre de notre jeu
  • Séparation logique /graphique


1 le squelette de l’application

Nous allons initialiser les différents états de notre jeu : 

  • Le spash screen  lors du démarrage
  • L’état in Game qui est le jeu à proprement parler


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


2 in game

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


3 IA (de ouf)

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

 4 Un peu de pixel art

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



Barricade

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 

  • polish du tuto 
  • Barriere de protection pour le joueur 
  • Balle ennemies
  • Ennemies plus "inteligent"
  • Vague d'ennemies
  • gestion de niveau
  • Ajout de graphisme

Voir la création

jicehel

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  ;)

Nux

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

jicehel

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 

clement

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

jicehel

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.

clement

NEW il y a 5 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).



Max

NEW il y a 5 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 5 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

alxm

il y a 5 ans

Nux

NEW il y a 5 ans

Max Max

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 5 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.



Max

il y a 5 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 :)

clement

NEW il y a 5 ans

Nux Nux

+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.



Max

NEW il y a 5 ans

Nux Nux

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 :)

ripper121

NEW il y a 5 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.

Aurélien Rodot

il y a 5 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.

Aurélien Rodot

NEW il y a 5 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.

clement

il y a 5 ans

ok c'est noté. 

je prendrais en compte cette remarque sr ce jeux 

Aurélien Rodot

NEW il y a 5 ans

ripper121 ripper121

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.

clement

NEW il y a 5 ans

Aurélien Rodot Aurélien Rodot

ok c'est noté. 

je prendrais en compte cette remarque sr ce jeux