Programmation Fonctionnelle, TP2

Figure
Version & licenses
Creative Commons License

Programmation Fonctionnelle, TP2 : Mise en orbite !

Guyslain Naves

Aujourd'hui, nous programmons un petit jeu dans lequel on contrôle un petit satellite tournant autour d'une étoile. Commencez par créer un nouveau répertoire, et y extraire cette archive.

Si vous êtes sur votre ordinateur personnel, il vous faudra installer les paquets vg, lwt et js_of_ocaml(ce dernier en version 2.8.4, sinon il faudra peut-être modifier les sources fournies, priori simplement en ajoutant le package js_of_ocaml-lwt dans le fichier _tags, mais je ne garantis rien). Vous pouvez forcer la version de js_of_ocaml avec la commande opam pin add js_of_ocaml 2.8.4 (et attendre la réinstallation).

En plus des fichiers de configurations, on trouve dans cette archive :

  • Body permet de représenter un corps sphérique en dimension 2, avec une position et une vitesse. Il contient aussi des fonctions sur les corps.
  • Space contient une description d'un système solaire rudimentaire.
  • Engine contient un mini-moteur de jeu.

Les deux premiers modules ne sont pas très compliqués, vous pouvez regarder rapidement le code et vous convaincre qu'avec un peu plus de temps, vous auriez pu le faire. Ils utilisent une fonctionnalité pas encore introduite en cours : les enregistrements. Ils s'utilisent d'une façon assez proche des structures en C. Par exemple, voici le type Body.t :

  1. type t = {
  2. position : Gg.p2;
  3. velocity : Gg.v2;
  4. mass : float;
  5. radius : float;
  6. }

Pour créer une valeur de ce type, il suffit de donner une valeur à chaque champ :

  1. let a_body =
  2. { position = Gg.P2.(v 100. (-10.));
  3. velocity = Gg.V2.zero;
  4. mass = 100.;
  5. radius = 4.
  6. }

Pour accéder à la valeur d'un champ :

  1. let its_mass = a_body.mass

Il n'est pas possible de modifier les valeurs d'une structure, mais on peut créer une copie en changeant uniquement un ou plusieurs champs ainsi :

  1. let a_bigger_body =
  2. { a_body with
  3. mass = 200.;
  4. radius = 64.
  5. }

Un jeu pas intéressant du tout.

Pour commencer, on va créer l'architecture du jeu, mais au début, il ne se passera rien dans le jeu. Un jeu (dans notre moteur simplifié) est décrit par :

  • un état initial (le modèle)
  • une liste d'événements (clavier ou souris) et la façon dont chaque événement modifie l'état,
  • une fonction produisant une image à partir de l'état du jeu,
  • une durée, qui précise la fréquence à laquelle le jeu est mis à jour,
  • une fonction déterminant si le jeu est terminé.

Tout cela est défini par le type paramétré par le modèle :

  1. (* in engine.mli *)

  2. type 'model app =
  3. { model : 'model
  4. ; event_handlers : (Event.t * ('model -> 'model)) list
  5. ; show : 'model -> Vg.image
  6. ; dt : float (** in second *)
  7. ; stop : 'model -> bool
  8. }

Il nous faut donc définir une telle description du jeu. Ensuite, la fonction run de Engine se charge du reste.

Nous compilerons le jeu en javascript, il s'exécutera dans un navigateur en ouvrant la page index.html.

Créez un nouveau fichier game.ml.

Définissez un type model, un enregistrement qui pour l'instant ne contient qu'un seul champ : un entier. Définisser une valeur model valant 1.

Définissez une valeur event_handlers qui est la liste vide.

Définissez une fonction show prenant un modèle, et dessinant un disque centré en 0, de rayon égal à l'entier du modèle. Pour cela, on utilise la librairie Vg dont la documentation est ici. Par exemple, pour créer un disque :

  1. open Vg

  2. let circle_shape =
  3. P.empty
  4. |> P.circle (P2.v 0.5 0.5) 0.4
  5. let circle_image =
  6. I.const Gg.Color.black
  7. |> I.cut circle_shape

Définissez dt valant 0.02 (50 franes par secondes).

Définissez stop une fonction du modèle retournant vrai si l'entier est au moins 10.

Finalement, définissez l'application et le main ainsi :

  1. let app =
  2. Engine.(
  3. { model;
  4. event_handlers;
  5. show;
  6. dt;
  7. stop;
  8. })

  9. let main =
  10. Engine.run
  11. ~width:800
  12. ~height:800
  13. app

Pour compiler :

  1. ocamlbuild -use-ocamlfind -plugin-tag "package(js_of_ocaml.ocamlbuild)" game.js

Vérifier que index.html pointe vers le bon fichier javascript, et ouvrez-le dans un navigateur. Un cercle devrait s'afficher.

Gestion des événements.

Pour l'instant le jeu n'est pas intéressant, il ne se passe rien. On va donc ajouter des événements.

Dans evant_handlers ajoutez une paire dans la liste. La première composante est l'événement tick défini dans Engine.Event. On lui associe une fonction qui prend le modèle et augmente le champ entier de 1.

Ajoutez une deuxième paire dans la liste, pour l'événement click, décrémentant le modèle de 1 s'il est positif.

Compiler et vérifiez ce qu'il se passe.

Un jeu plus intéressant.

Ce premier exemple vous a permis de comprendre comment fonctionne ce moteur de jeu. On peut maintenant faire un vrai jeu avec. Celui-ci consistera en contrôler un satellite en rotation autour d'une étoile.

Créer un fichier satellite.ml et définir un type t. Un satellite est un corps physique, le type doit donc avoir un champ de type Body.t. Ajoutez-lui aussi une couleur.

Ajoutez des fonctions pour :

  • déplacer le satellite (en utilisant la fonction appropriée de Body) de type dt:float -> Satellite.t -> Satellite.t. Le satellite doit être soumis à la gravitation de l'étoile définie dans Space (pour cela, les fonctions nécessaires sont définies dans Body).
  • dessiner le satellite, de type Satellite.t -> Vg.image. Pour cela, on le dessine en position (0,0) et pointant vers la droite, puis on le déplace avec la fonction Vg.I.move jusqu'à sa position.
  • créer un satellite. On le positionnera en orbite autour de l'étoile.

Modifiez le modèle du jeu pour que ce soit un champ contenant un satellite. Supprimer l'événement click. Modifier l'événement tick pour que le satellite se déplace.

Enfin modifiez la fonction d'affichage du jeu, pour afficher l'étoile et le satellite, sur fond noir. Utilisez Vg.I.blend pour coller les images les unes sur les autres.

Un satellite un peu plus joli.

On voudra pouvoir contrôler le satellite, donc il sera intéressant de pouvoir connaître sa direction.

Ajoutez un champ direction au satellite. La direction est un vecteur unitaire, on utilise le type Gg.v2. Au début, la direction est la même que la vélocité.

Voici le schéma du satellite, la grille étant alignés sur les vecteurs horizontaux et verticaux (vous pouvez personnaliser votre satellite si vous vous sentez un peu artiste) :

le satellite.

On dessine le satellite en position (0,0) allant vers la droite, puis on le tourne, puis on le déplace.

Les modules Gg et Vg contiennent les fonctions nécessaires :

  1. val Gg.V2.angle : Gg.v2 -> float
  2. (* gets the angle made by a vector with the x-axis *)
  3. val Gg.Float.pi_div_2 : float
  4. (* pi /. 2. *)
  5. val Vg.I.scale : float -> Vg.image -> Vg.image
  6. (* scale the dimension of an image *)
  7. val Vg.I.rot : float -> Vg.image -> Vg.image
  8. (* rotate an image by an angle in radian *)
  9. val Vg.I.move : Gg.v2 -> Vg.image -> Vg.image
  10. (* move an image *)

À nouveau, testez !

Ajouter le contrôle.

On veut pouvoir contrôler les mouvements du satellite, soit en activant son moteur, soit en tournant le satellite sur lui-même.

Ajoutez des champs au satellite :

  • sa vitesse angulaire angular_speed, en radians par seconde, par exemple $\pi/2$.
  • son stock de carburant fuel, en nombre de seconde de moteur disponible.
  • sa puissance thrust
  • un champ booléen indiquant si le moteur est allumé,
  • un champ indiquant si le satellite tourne sur lui-même, en utilisant le type :
  1. type rotation =
  2. | Left
  3. | Right
  4. | Stay

Pour manipuler les valeurs de ce type, on peut utiliser cette construction (c'est une exemple, ne convertissez par les rotations en chaînes de caractères) :

  1. let to_string rotation =
  2. match rotation with
  3. | Left -> "counterclockwise"
  4. | Right -> "clockwise"
  5. | Stay -> "not rotating"

Ajoutez ensuite trois fonctions :

  • accelerate : dt:float -> Satellite.t -> Satellite.t, qui retourne une version modifiée du satellite : sa vélocité est augmenté par le produit de sa puissance fois dt divisé par sa masse, fois sa direction. Son fuel est diminué de dt.
  • rotate_left : dt:float -> Satellite.t -> Satellite.t, qui modifie l'angle du vecteur de direction par le produit de la vitesse angulaire avec dt.
  • rotate_right, similaire mais dans l'autre direction.

Puis ajoutez des fonctions permettant d'allumer/éteindre le moteur, et initier ou terminer une rotation.

Il faut maintenant lier les actions du joueur sur le clavier avec ces fonctions. Utilisez les événements claviers Engine.Event.key_up et Engine.Event.key_down, en donnant en argument le code de la touche ('a' pour la touche A par exemple). (Dans la console javascript accessible via Ctrl-Maj-i, vous pouvez voir la chaîne encodant une touche).

Testez !

Modifier la condition de fin de jeu.

Modifier la fonction de test de fin du jeu. Le jeu s'arrête si le satellite rentre dans l'étoile, ou quitte la fenêtre du jeu.

À vous de jouer...

Le jeu est encore très basique, mais vous trouverez facilement des idées pour le rendre plus intéressant. Vous pouvez aussi :

  • ajouter un score,
  • ajouter la possibilité de mettre le jeu en pause,
  • recharger le carburant du satellite, selon sa proximité au soleil et l'orientation de ses panneaux solaires
  • ajouter un deuxième satellite controlé par un deuxième joueur.