Jeux Libres
       
           

» Les Tutoriels » Création d'un jeu vidéo » Création d'un personnage

Création d'un personnage


Notre personnage peut être vu comme un objet 3D. Pour simplifier les choses, il ne sera pas déformable. On va donc créer une classe Personnage qui héritera de la classe Objet3DStatique.

Je vous invite dans un premier temps à créer un personnage au format O3S. Pour cela, je vous laisse vous débrouiller seul une fois de plus. Petite indication : Il est très facile de se procurer le patron d'un personnage 3D en tapant "cube craft" sur google image.



Pour ma part, j'ai commencé à modéliser une tortue ninja que je ne l'ai pas terminé, mais qui fera l'affaire. Mon modèle 3D s'appelle donc raphael.o3s et il est accompagné de son fichier raphael.bmp.





Chapitre précédent     Sommaire     Chapitre suivant


La classe personnage


Dans un premier temps, il n'y a pas grand chose à faire.

Voici ma classe :
1
2
3
4
5
6
7
8
9
10
11
class Personnage : public Objet3DStatique
{
   private:
 
 
 
   public:
       Personnage(float16 positionX, float16 positionY, float16 positionZ,
                       float16 angleHorizontal, std::string nomFichier);
       void avancer(float16 distance);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
Personnage::Personnage(float16 positionX, float16 positionY, float16 positionZ,
float16 angleHorizontal, std::string nomFichier) : Objet3DStatique(nomFichier)
{
   this->positionX = positionX;
   this->positionY = positionY;
   this->positionZ = positionZ;
   this->angleHorizontal = angleHorizontal;
}
 
void Personnage::avancer(float16 distance)
{
 
}

Pour l'instant on a le nécessaire pour instancier un personnage pour bien le modéliser. Il ne peut pas encore avancer.


Affichage du personnage


Pour afficher le personnage, on l'instancie dans la scène puis, dans la méthode Scene::dessiner() ont fait appel à la méthode Personnage::dessiner() qui existe par héritage. On s'arrange avec le tag rst du fichier O3S pour que, lorsqu'il est placé en (0;0), le personnage apparaisse bien en (0;0).


N'hésitez pas à bouger la caméra pour vérifier que votre personnage est bien placé.


Pour observer la scène à la vertical, vous devrez définir la vertical de votre camera suivant un autre axe : les X négatifs par exemple.

1
gluLookAt(0,0,3,0,0,1,-1,0,0);

On fait également attention à l'orientation. Quand l'angle horizontal du personnage vaut zero, on va dire qu'il regarde vers le nord : les x négatifs. L'angle sera alors l'azimut du personnage.

Mon résultat :


Voilà, c'est mieux !


Déplacer le personnage


Avancer



Avancer, ça veut dire quoi ?


Du point de vue du personnage, avancer veux dire : "se déplacer vers l'azimut".

Si on doit avancer d'une unité, de combien doit-on se déplacer suivant X et suivant Y ?


On prend un papier et un crayon, puis on fait un petit dessin et quelques calcules.


Et pour finir, on code la méthode :
1
2
3
4
5
void Personnage::avancer(float16 distance)
{
   this->positionY -= distance * sin(this->angleHorizontal * M_PI / 180.0);
   this->positionX -= distance * cos(this->angleHorizontal * M_PI / 180.0);
}

les fonctions sin() et cos() prennent en paramètre un angle en radian.


Pour vérifier, on fait appel à la méthode avancer() dans la méthode animer(). Pour la distance, j'ai mis 0.01 unité. Il s'agit de la distance entre chaque image.

Le résultat :


Le personnage doit avancer droit devant lui, en direction de son azimut, quel qu'il soit.

La gestion des évènements



Dans les jeux de type FPS, il est coutume d'utiliser les touches Z, Q, S et D pour le déplacement du personnage :

  • Z : Avancer
  • Q : Gauche
  • S : Reculer
  • D : Droite

Et pour tourner la tête, on utilise la souris.

La gestion des évènements peut se fait grâce à la bibliothèque SDL. Un évènement c'est : l'enfoncement d'une touche, le relâchement...

  Les évènements SDL imposent une gestion non-continue des touches, à savoir que si vous voulez faire avancer un personnage en testant SDL_KEYDOWN (par exemple), le mouvement sera saccadé.

Il existe deux solutions pour pallier ce problème.
      Laurent Gomila


Les deux solutions : Comment savoir si une touche reste enfoncée ?

Personnellement, je préfère la première solution car elle est plus simple à implémenter.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Dans la boucle de jeu principale... */
 
Uint8* keys = SDL_GetKeyState(NULL);
 
if (keys[SDLK_UP])
{
   /* Faire avancer personnage par exemple */
   Personnage.Position += 5;
}
if (keys[SDLK_DOWN])
{
   /* Faire reculer personnage */
   Personnage.Position -= 5;
}

Par soucis de simplicité, j'ai fait la gestion des évènements directement dans animer(). Il faudra revoir ça pour détacher la partie gestion des évènements de la partie animation. Voici mon code :
1
2
3
4
5
6
7
8
9
10
11
void Scene::animer(void)
{
   // Lecture de l'tat des touches
   Uint8* touches = SDL_GetKeyState(NULL);
 
   if (touches[SDLK_w]) // Touche Z
   {
       // Avancer
       this->personnage->avancer(0.01);
   }
}

Les touches sont basées sur les claviers QWERTY.
La touche Z de mon clavier AZERTY correspond donc à la touche W.


Reculer, gauche, droite



Pour la méthode reculer, je vous laisse faire. Personnellement j'ai choisi de ne pas la développer car reculer c'est comme avancer négativement.

Pour le déplacement vers la gauche, vous pouvez reprendre votre papier et votre crayon. Sinon, la solution astucieuse est de se dire que se déplacer vers la gauche c'est comme tourner sur soit même de 90° vers la gauche, avancer, puis retourner sur soit même de 90° vers sa droite. En réutilisant la méthode avancer(), vous pouvez faire ça en 3 instructions. Idem pour la droite.

Tourner



Pour tourner, on crée une méthode qui prend en paramètre l'angle à incrémenter à l'azimut du personnage.
1
2
3
4
5
6
7
8
9
10
11
12
13
void tournerHorizontalement(float16 angle)
{
   this->angleHorizontal += angle;
 
   while (this->angleHorizontal >= 180.0) // Lorsqu'on dpasse la limite (1/2 tour)
   {
       this->angleHorizontal -= 360.0;
   }
   while (this->angleHorizontal < -180.0) // Idem aprs 1/2 tours vers la droite
   {
       this->angleHorizontal += 360.0;
   }
}

Afin d'éviter les pertes de précision pour de grande valeur d'angle en variable réelle, je bride la valeur à l'intervalle [-180;180[.


Pour les geeks : Les variables réelles ont un nombre constant du chiffres significatifs (la mantisse). Si votre valeur d'angle est très grande (ex : 5.4795418 x 1048) et que vous ajoutez un petit angle (4° par exemple), le résultat restera inchangé car la petite valeur n'affectera pas la mantisse de votre flottant. (5.47954180000000000...004 sera tronqué à 5.4795418)


Tourner au mouvement de la souris



La technique utilisée pour tourner à la souris est la suivante : au début, la souris est au centre de l'écran. Lorsque la souris se déplace, par exemple vers la droite, on récupère le mouvement relatif en X de la souris pour faire tourner le personnage, puis on repositionne la souris au centre de l'écran pour le prochain calcul.

Le type d'évènement SDL associé à un mouvement de souris est SDL_MOUSEMOTION. Dans la boucle d'évènement, on ajoute donc ceci :
1
case SDL_MOUSEMOTION:

Après avoir ajouté ceci :
1
2
            case SDL_MOUSEMOTION:
               this->personnage->tournerHorizontalement(-evenement.motion.xrel);

Nous obtenons cela :


Le personnage tourne bien en fonction du mouvement gauche/droite de la souris. Lorsque la souris sort de l'écran, la position de la souris n'est plus mise à jour, le personnage ne tourne plus. On repositionne donc la souris au centre de l'écran juste après.
1
2
                // On replace la souris au milieu de l'ecran
               SDL_WarpMouse( (LARGEUR_FENETRE/2), (HAUTEUR_FENETRE/2) );

Le fait de repositionner la souris au centre de l'écran produit un mouvement relatif qui annule le précédent. Pour ne pas le prendre en compte, on peut faire le test suivant avant de traiter l'évènement :

1
2
                    // Si le mouvement de la souris est physique (pas par SDL_WarpMouse)
                   if ((LARGEUR_FENETRE/2) != evenement.motion.x || (HAUTEUR_FENETRE/2) != evenement.motion.y)

Attention : Avant de vous retrouver avec la souris bloqué au centre de l'écran et ne plus pouvoir quitter, pensez à mettre en place un moyen de quitter votre jeu.

1
2
3
4
5
            case SDL_KEYDOWN:
               if (evenement.key.keysym.sym == SDLK_ESCAPE)
               {
                   this->continuer = FALSE;
               }

Voici le résultat :


En bougeant énormément, on réussi à faire sortir la souris de l'écran et à perdre le contrôle du personnage. Pour empêcher la souris de sortir, nous utiliserons la fonction SDL_WM_GrabInput(SDL_GRAB_ON);

Ce bridage sera effectué au lancement de la scène, dans la constructeur. On en profite également pour initialiser la position de la souris puis on cache la souris.
1
2
3
4
5
6
7
8
    // Maintien de la souris dans la fenetre
   SDL_WM_GrabInput(SDL_GRAB_ON);
 
   // La souris est au centre de l'ecran
   SDL_WarpMouse( (LARGEUR_FENETRE/2), (HAUTEUR_FENETRE/2) );
 
   // La souris est invisible
   SDL_ShowCursor(FALSE);


Gestion de la vitesse de déplacement


Mesure du temps de construction de l'image



Pour le moment, la distance de déplacement est constante d'une image à l'autre. Or le nombre d'image par seconde n'est pas forcément constant en fonction de la configuration de la carte graphique (synchronisation verticale) et du temps de construction de l'image de l'ordinateur qui exécute le jeu. De plus, il est possible que le système d'exploitation ne vous redonne pas la main pendant un certain temps, ce qui peut vous faire rater des images et vous décaler dans le temps.

Pour remédier à ce problème, nous allons calculer la distance à parcourir en fonction du temps écoulé depuis la dernière image.

La SDL procède une fonction permettant de récupérer le temps en milliseconde écoulé depuis le début de l'exécution du programme : SDL_GetTicks().

Nous allons appeler "pas" le temps écoulé entre la construction de 2 images.

A la fin du pas, le temps de celui-ci est la différence entre l'heure actuelle est l'heure du dernier pas.
L'heure du dernier pas devient alors l'heure du pas précédent auquel s'ajoute le temps du dernier pas.

Dans la boucle d'exécution, après l'affichage, on ajoute donc ceci :
1
2
        tempsDernierPas = SDL_GetTicks() - heureDernierPas;
       heureDernierPas += tempsDernierPas;

Remarquez au passage que j'ajoute le temps du dernier pas plutôt que de re-récupérer l'heure actuelle. Je fais cela pour éviter de perdre une miliseconde entre deux appels à la fonction SDL_GetTicks(). Ainsi, le jeu ne se désynchronise pas.


Afin que ce calcul fonctionne immédiatement, on initialisera les variables juste avant de commencer la boucle d'exéction :
1
2
    tempsDernierPas = 0;
   uint32 heureDernierPas = SDL_GetTicks();

La seule information qui nous sera utile, ce sera la variable tempsDernierPas. En effet, l'heure du dernier pas ne nous servira à rien pour la suite car la construction d'une image ne se fait qu'en fonction de l'image précédente. On met donc la variable tempsDernierPas en attribut de notre classe de façon à pouvoir y accéder.

Voici la description de notre nouvelle méthode executer() :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void Scene::executer()
{
   // Initialisation des variables de gestion du temps
   this->tempsDernierPas = 0;
   uint32 heureDernierPas = SDL_GetTicks();
   
   this->continuer = TRUE;
 
   while(this->continuer)
   {
       gererEvenements();
       animer();
       dessiner();
       afficher();
       
       // On calcule le temps de construction de la derniere image
       this->tempsDernierPas = SDL_GetTicks() - heureDernierPas;
       heureDernierPas += this->tempsDernierPas;
   }
}

Déplacement à vitesse constante



Si nous voulons que la vitesse de déplacement du personnage soit constante, nous devons déplacer le personnage d'une distance qui soit proportionnelle au temps de construction de l'image courante. Nous ne savons pas exactement le temps que va prendre la construction de l'image. Or d'une image à l'heure, le temps de construction diffère peu. En reprenant le temps de construction de l'image précédente, le temps du dernier pas, l'erreur est alors quasiment nulle. De plus, cette méthode aura au moins le mérite de rattraper le retard dans le cas ou l'image précédente aurait été construite et affichée avec du retard (image précédente périmée à l'affichage).

Calcule de la distance à parcourir



Nous allons supposer à la louche que nous voulons que le personnage se déplace à vitesse constante d'une unité par seconde. On reprend notre papier et notre crayons et on calcul la distance à parcourir en fonction du temps de construction de l'image pour obtenir la vitesse constante d'une unité par seconde.


Notre code devient alors :
1
2
#define VITESSE_DEPLACEMENT (1.0f)
this->personnage->avancer((float)tempsDernierPas * VITESSE_DEPLACEMENT / 1000.0f);

VITESSE_DEPLACEMENT est en unité par seconde.




Ce chapitre touche à sa fin. Il nous a permis de refaire un peu de trigonométrie et de physique. Nous savons maintenant créer un personnage et faire interagir le joueur avec la scène sans dépendre de la vitesse d'exécution de la machine.

Tout n'est pas très propre mais pour l'instant nous sommes obligé de faire comme ça.



Chapitre précédent     Sommaire     Chapitre suivant



Rédigé par David
Consulté 41723 fois



Hébergeur du site : David
Version PHP : 5.4.45-0+deb7u2
Uptime : 31 jours 5 heures 4 minutes
Espace libre : 2115 Mo
Dernière sauvegarde : inconnue
Taille de la sauvegarde : 1100 Mo


4007131 pages ont été consultées sur le site !
Dont 1685 pages pendant les 24 dernières heures.

Page générée en 0.47 secondes


Nos sites préférés
- Création d'un jeu de plateforme de A à Z avec SDL
- Zelda ROTH : Jeux amateurs sur le thème de Zelda
- Zeste de Savoir : la connaissance pour tous et sans pépins
- YunoHost : s'héberger soi-même en toute simplicité
- Site de Fvirtman : recueil de projets et de codes en C et C++


  © 2005-2017 linor.fr - Toute reproduction totale ou partielle du contenu de ce site est strictement interdite.