INFO505 Programmation C
|
L'objectif de ce TP est de rendre "graphique" votre Tetris "texte" mis au point lors du TP précédent (TP2 - Mini-projet Tetris simplifié).
On va donc se servir d'une interface graphique classique, appelée GTK (http://www.gtk.org). Cette interface, écrite en C, mais avec une philosophie objet, a été écrite pour réaliser le projet GIMP, qui est une très bonne alternative libre au logiciel de retouche d'image Photoshop. GTK est aussi à la base de l'environnement graphique GNOME.
On se servira aussi de Cairo (http://www.cairographics.org/) qui est une bibliothèque très polyvalente pour du dessin vectoriel dans n'importe quel tampon (image, fichier svg, buffer OpenGL, et fenêtre). C'est depuis GTK+ 2.8 la façon officielle de faire du dessin dans les fenêtres.
Note importante. Ce TP vous guide pas à pas pour que vous ayez les meilleures chances possibles de réussir un tetris graphique simplifié. Si vous trouvez ce canevas trop rigide, libre à vous de faire votre propre tetris graphique en suivant votre propre cheminement.
Pointeurs utiles:
On va réutiliser votre travail de la dernière fois. Pour bien faire les choses, on va faire des modules distincts. Le premier module sera la partie logique du Tetris, c'est-à-dire en gros votre dernier TP. Ce travail sera placé dans le module tetris
, constitué du fichier entête tetris.h
et du fichier source tetris.c
. Le deuxième module sera la partie graphique du tetris et contiendra le programme principal. On créera donc un fichier gtktetris.c
.
Vous récupérez votre TP de la dernière fois (version tableau ou liste, peu importe, du moment qu'elle fonctionne). Vous allez écrire un fichier tetris.h
ne contenant que les prototypes des fonctions que vous avez écrite la dernière fois ainsi que les types de données.
Vous placerez ensuite les fonctions (prototypes et corps) dans un fichier tetris.c
. Il faudra aussi enlever la fonction main
car un programme C n'a qu'un seul main
. Une façon élégante de le faire est de tout simplement renommer votre fonction main
, par exemple en main_console
. Enfin, il faudra inclure le fichier entête tetris.h
au début de votre fichier tetris.c
, juste après les inclusions d'entêtes systèmes.
La commande make
est un outil classique pour développer et compiler des programmes, notamment sur les systèmes UNIX. Cette commande regarde dans le répertoire où elle est lancé la présence d'un fichier Makefile
ou makefile
, et suit les directives écrites dedans. Créer donc le fichier Makefile
avec:
CC=gcc CFLAGS=-g -Wall -pedantic -std=c99 all: tetris.o tetris.o: tetris.h tetris.c $(CC) -c $(CFLAGS) tetris.c
Celui-ci compile simplement le fichier tetris.c
en son fichier objet tetris.o
. On voit que l'on peut définir des variables pour ensuite les utiliser.
On rajoute maintenant de quoi compiler notre futur programme gtktetris
. Recopier ce mini-fichier source gtktetris-1.c
.
Pour compiler tetris.c
et gtktetris.c
dans le même programme, on utilise maintenant le makefile
suivant. On note qu'il nous permettra de compiler notre programme avec l'interface graphique GTK (ici version 3), même si pour l'instant on ne s'en sert pas.
CC=gcc LD=gcc CFLAGS=-g -Wall -pedantic -std=c99 GTKCFLAGS:=-g $(shell pkg-config --cflags gtk+-3.0) GTKLIBS:=$(shell pkg-config --libs gtk+-3.0) all: gtktetris gtktetris: gtktetris.o tetris.o $(LD) gtktetris.o tetris.o -o gtktetris $(GTKLIBS) gtktetris.o: gtktetris.c $(CC) -c $(CFLAGS) $(GTKCFLAGS) gtktetris.c tetris.o: tetris.h tetris.c $(CC) -c $(CFLAGS) tetris.c clean: rm -f gtktetris gtktetris.o tetris.o
Vérifier maintenant que la compilation fonctionne. Tapez maintenant make
dans votre terminal. Il devrait afficher quelque chose ressemblant à ci-dessous. Vous pouvez vérifier que le programme gtktetris
a bien été compilé. Si vous l'exécutez, cela affiche la grille sur le terminal, comme d'habitude.
La compilation est plus complexe car GTK utilise beaucoup de bibliothèques différentes (X, mais ausii cairo, pango, gdk, Xi, Xext, ...). Mais tout ceci se fait tout seul.
gtktetris.c
est correct pour un type Grille
qui est un tableau bidimensionnel. Si vous avez choisi une structure pour le type Grille
, il faut passer la grille par adresse.Pour créer une interface graphique, GTK propose un système classique à base de composants. On dispose du type générique GtkWidget pour les désigner. La fenêtre (GtkWindow) est le composant dans lequel on place les autres. Certains composants peuvent contenir d'autres composants, on les appelle des conteneurs (GtkWindow est un conteneur ne contenant pas plus d'un composant, le GtkBox est un conteneur permettant de stocker autant d'autres composants que l'on souhaite). On dispose de plus de beaucoup de composants classiques pour les IHM : boutons, menus, checkbox, roulettes, zones de dessin, onglets, etc.
Ensuite, on associe à ces composants des réactions à certains événements. On pourra ainsi spécifier que la fonction quitter
sera appelée lorsqu'on clique sur un bouton.
Une fois ces composants créés puis assemblés dans le bon ordre, on peut lancer l'application en donnant la main à GTK (appel de gtk_main
). Ce sera GTK qui appelera automatiquement nos réactions selon les événements induits par le comportement de l'utilisateur. On parle de programmation événementielle.
Voilà un programme minimaliste pour créer une fenêtre GTK.
On note qu'il faut un pointeur (de type GtkWidget*
) pour pointer vers les composants créés. Chaque composant est créé avec une fonction de la forme gtk_xxxx_new( ... )
, où xxxx désigne le type de composant voulu. Enfin, on doit appeler la fonction gtk_widget_show
sur chaque composant créé pour le rendre visible. De la même façon, vous pouvez rendre invisible tout composant (et les composants qu'il contient) en appelant la fonction gtk_widget_hide
.
Une fois tout en place, on appelle la boucle d'attente d'événements de GTK avec gtk_main
. Compilez et exécutez ce code. Notez qu'il faut interrompre le programme car rien n'est prévu pour quitter la boucle.
On va créer un bouton (cliquable) avec gtk_button_new_with_label
. Rajouter les lignes suivantes dans votre programme avant l'appel au gtk_main
.
Vous avez maintenant une fenêtre composée d'un seul bouton. Si vous cliquez dessus, le programme se termine.
G_CALLBACK
). En fait, le programme passe l'adresse de la fonction comme argument. De son côté, la fonction g_signal_connect
récupère cette adresse et la place dans un pointeur pour ensuite appeler cette fonction en réaction à l'événement "clic sur le bouton". On parle alors de pointeur de fonction, plutôt que de pointeur tout court, car le pointeur ne pointe pas sur des données mais sur l'adresse du code assembleur de la fonction.Comme une fenêtre ne contient qu'un composant au maximum, on place un autre composant (la boîte) dans la fenêtre et c'est dans cette boîte qu'on mettra plusieurs composants.
Les boîtes permettent de ranger des composants, mais ne rajoutent pas de décor autour des composants. Elles servent à mettre en page votre IHM. Il y a les boîtes horizontales (créées avec gtk_box_new(GTK_ORIENTATION_HORIZONTAL,...)
pour placer les composants horizontalement et les boîtes verticales (créées avec gtk_box_new(GTK_ORIENTATION_VERTICAL,...)
) pour placer les composants en vertical.
Ajouter maintenant aussi le bouton button_quit
dans la boîte hbox1
, ajouter la boîte dans la fenêtre window
. N'oubliez pas de rendre visible tous les composants !
gtk_widget_show
. On peut rendre visible tous les composants contenus dans un conteneur avec gtk_widget_show_all
.GTK_CONTAINER( hbox1 )
transforme un pointeur GtkWidget*
en un pointeur GtkContainer*
: c'est valide car c'est bien ainsi que le composant a été créé. Cela permet à GTK de simuler l'héritage en C, et de vérifier à l'exécution que le transtypage est valide.On se propose de faire la mise en page suivante pour notre IHM. Il vous faut créer autant de hbox et vbox nécessaires pour structurer cette mise en page. On note qu'il y aura des boutons (New, Quit et les 3 flèches), des labels (Score et Delay) et des labels qu'on va modifier (la valeur du score et le temps restant). Il y aura enfin une zone de dessin (GtkDrawingArea).
On va aussi créer des boutons flèches. On pourrait utiliser des petits icones, mais le plus simple est de faire des labels "<"
, "v"
, ">"
. Pour faire un bouton flèche gauche, on écrit les lignes suivantes.
Pour créer une zone de dessin de taille donnée, on fait ainsi.
Vous définirez une constante TAILLE_CARRE qui vaut 16 et qui donne la taille de chaque carré de vos pièces de tetris. Il faudra donc créer une zone de dessin de largeur TAILLE_CARRE*(LARGEUR+4) et de hauteur TAILLE_CARRE*(HAUTEUR+7), car il faut prévoir la place pour le décor autour de la grille et pour la pièce qui arrive en haut.
Pour faire les choses un peu plus proprement, vous créerez une fonction creerIHM()
pour créer tous ces composants et les mettre en page. Pour simplifier, les variables pointant vers les composants "utiles" pourront être globales au module (fenêtre, zone de dessin, label pour la valeur du score, label pour la valeur du delay).
On peut écrire ses propres fonctions réactions pour les événements. Par exemple, si on veut que le bouton "gauche" appelle la fonction Gauche
lorsqu'il est cliqué, on connecte le composant à cette fonction grâce à g_signal_connect
. On note qu'on peut passer un pointeur en paramètre de plus.
Ecrivez des réactions similaires pour les boutons flèches gauche, bas, droite et "New". On les réécrira plus tard pour nos besoins. Il est important de remarquer que dans l'exemple ci-dessus, la valeur affichée est celle de global_val0
lorsque le bouton sera cliqué, il est donc primordial que cette variable existe encore à ce moment là. On note que ce mécanisme de réaction
est basé sur la notion de pointeur de fonction.
global_val0
doit exister lorsque Gauche
est appelée, ce ne peut être une variable définie dans le fonction creerIHM
. &global_val0
doit désigner soit l'adresse d'une variable globale, soit l'adresse d'une variable définie dans la fonction appelante, i.e. la fonction main
.Nous allons avoir besoin d'accéder à la partie logique du jeu (grille tetris ainsi que pièces, score, autres) à partir de l'IHM. Or, dans les réactions que l'on va associer aux événements, une seule donnée (un pointeur) est envoyée en même temps. On va donc se créer un type qui rassemble toutes nos données en un seul endroit et on passera l'adresse d'une variable de ce type à toutes nos réactions.
Il faudra donc rajouter un paramètre à creerIHM
. Son prototype deviendra void creerIHM( Jeu* ptrJeu )
.
GtkWidget
dans la structure Jeu
. Et ptrJeu
sera la donnée que vous passerez aux différentes réactions/callbacks.On va se servir de Cairo (http://www.cairographics.org/) pour dessiner notre grille tetris ainsi que les pièces.
Lorsqu'un composant est placé dans une fenêtre affichée à l'écran, il reçoit des signaux pour lui dire quand se réafficher. Cela arrive bien sûr la première fois que l'IHM est affichée, mais aussi par la suite si jamais la fenêtre a subi une occlusion partielle et doit donc être redessinée. Le composant GtkDrawingArea est par défaut rempli de gris, c'est tout ! On va donc se connecter aux signaux "realize" (appelé une seule fois lors du premier affichage) et "draw" (appelé régulièrement par le serveur de fenêtre) pour tracer notre tetris. On part donc du squelette suivant.
Maintenant, vous devriez voir à l'écran ceci lors de l'exécution.
Vous allez maintenant dessiner la grille en faisant un décor qui remplace les tubes '|' et en affichant un carré de couleur pour les pièces. Si cela vous amuse, vous pouvez les faire de différentes couleurs suivant vos choix pour les pièces (genre rouge pour '#', vert pour '@', etc). Les carrés seront donc de la taille TAILLE_CARRE * TAILLE_CARRE pixels. Pour plus de commandes sous cairo, allez voir http://cairographics.org/tutorial .
Vous allez modifier ce qui est entre (A) et (B) au-dessus.
Pour découper votre code, faites les fonctions
On note qu'on récupère le pointeur vers le jeu dans la fonction on_draw
en convertissant le pointeur data
ainsi:
Voilà un petit snapshot du jeu maintenant.
cairo_set_source_rgb(cr, r, g, b)
, où r,g,b sont les intensités (entre 0.0 et 1.0) des couleurs rouge, vert et bleu.On voit que, dans la structure Jeu, on mémorise la pièce courante ainsi que sa colonne courante. On va faire une fonction qui met à jour une structure Jeu en choisissant aléatoirement une nouvelle pièce courante et la place arbitrairement dans la colonne (LARGEUR-piece.largeur)/2. Appelons-là nouvellePiece
. Elle sera appelée en début de partie et à chaque fois que le joueur vient de poser une pièce.
Ecrivez ensuite la fonction dessinePiece( cairo_t* cr, Jeu* pJeu
)
qui l'affiche, et qui sera appelée depuis on_draw
(comme tous les dessins dans la zone graphique).
On peut maintenant écrire proprement les réactions Gauche
et Droite
. Elles doivent recevoir la donnée jeu
aussi. Ensuite, leur seul rôle est de changer la colonne courante de la pièce, en la bloquant sur les bords. Il faut ensuite redessiner la zone_de_dessin. On procède alors de la même manière que dans realize_evt_reaction
, c'est-à-dire avec la commande
Chaque clic sur les flèches gauche ou droite doit maintenant réafficher votre pièce déplacée dans la bonne direction.
Il ne vous reste plus qu'à écrire la réaction au clic sur la flèche vers le bas, qui ne fait qu'appeler vos fonctions écrites dans le TP précédent (hauteurMax
et ecrirePiece
).
N'oubliez pas de rappeler ensuite nouvellePiece
puis d'invalider la zone de dessin pour qu'elle soit réaffichée. Il faudrait aussi détecter les lignes complètes et les enlever.
Vous avez maintenant le même jeu de tetris qu'en mode texte !
En général, le tetris donne un temps limité pour chaque pièce, sinon la pièce est placée pour vous (en fait elle descend d'un cran de plus en plus rapidement). On va faire quelque chose de similaire. On va juste laisser un temps à l'utilisateur au bout duquel, si l'utilisateur n'a pas appuyer sur la flèche bas, le jeu fait comme si vous avez appuyé sur la flèche vers le bas.
Il existe une façon très simple en GTK pour appeler une fonction spécifiée au bout d'un certain délai. Par exemple la fonction tic
ci-dessous sera appelée toutes les 20ms, et recevra à chaque fois le pointeur vers le jeu.
Pour simplifier, on va supposer que le label pour le délai a été placé dans une variable globale du genre
En utilisant gtk_label_set_text
( GTK_LABEL
( valeur_delai
), str
), vous remplacez le texte stocké dans ce label par le contenu de la chaine de caractère str
. On va donc stocker le délai courant dans la structure Jeu ainsi que le délai maximum redonné à chaque fois. Votre fonction tic
doit décrémenter ce compteur délai et l'afficher. Lorsqu'il arrive à zéro, la réaction Bas
doit être appelée et le compteur réinitialisé au délai maximum. Eventuellement, vous pouvez vous amuser à diminuer progressivement le délai maximum, avec le même timer ou un autre timer.
sprintf( buffer, "%d", delai)
, où buffer est un tableau de caractère suffisament grand.Le tetris sera beaucoup plus sympa à jouer si au lieu de cliquer sur les boutons, vous utilisez des touches pour déplacer les pièces. On peut mettre des événements "touches pressées" en Gtk/Gdk.
Il suffit de récupérer les symboles désignant les touches possibles en ajoutant #include <gdk/gdkkeysyms.h>
, de connecter une réaction à l'événement "key_press_event"
, et dans la réaction de gérer les touches et d'appeler les bonnes fonctions.
Il ne reste plus (!) qu'à fignoler le jeu.
make
dans le terminal.