TD3. Game On!
animations, plateformes et sauts !
L’objectif de cette troisième fiche est de produire des jeux TIC-80 aux mouvements fluides de type jeux de plateformes. En passant, on découvre les derniers éditeurs qui permettent de définir sons et musiques. Côté programmation, on s’interroge sur la gestion du mouvement et les collision. En Python, on aborde la notion d’objet pour factoriser le code et structurer les données.
Aujourd’hui, nous allons créer notre premier jeu de plateformes. Ne soyez pas déçus si les premiers résultats sont en deça de vos espérances : il est assez facile d’écrire un morceau de code qui marche… mais obtenir le comportement précis qu’on a en tête peut être très long et demande beaucoup de tests. Un jeu de plateformes c’est facile, un bon jeu de plateformes c’est subtil ! Commençons par quelque chose de correct mais basique.
Pendant cette séance, ne pas oublier de récupérer régulièrement les cartouches produites en utilisant save et get.
Ouvrir le lien GameOn TIC-80 dans une autre fenêtre ou un autre onglet du navigateur. C’est votre environnement de travail pour toute la séance ! Garder cette fenêtre ouverte.
Attention à bien utiliser la version de TIC-80 fournie pour ce TP pas une autre. Cette instance contient des éléments spécifiques pour vous aider pendant les séances. Bien sûr, les cartouches produites seront ensuite utilisables dans toute instance de TIC-80.
1 Lecture de code
Aujourd’hui, un premier code fonctionnel est fourni dans GameOn/TD3/microplat.tic !
Ouvrir la cartouche GameOn/TD3/microplat.tic grâce à la commande surf. Tester le jeu, observer le contenu des éditeurs de code, de sprites et de carte.
Se placer à la racine avec la commande cd, puis sauvegarder le jeu sous le nom plateforme.tic avec save plateforme.tic.
Le code source est organisé en plusieurs classes et fonctions, comme ceci (une partie du code a été retirée) :
1class player:
"le joueur et ses attributs"
(...)
2# les variables globales
t=0
p1=player()
grav=0.2
3def is_solid(dx,dy):
"""teste si le pixel en position (dx,dy)
par rapport au joueur est solide"""
(...)
4def update():
"mise a jour du jeu"
(...)
5def draw():
cls()
map()
p1.draw()
6def TIC():
global t
# cache la souris
poke(0x3ffb,0)
update()
draw()
print("Python platformer demo",20,20,12)
t+=1- 1
- La classe joueur, ses attributs et ses méthodes
- 2
- Les variables globales
- 3
- Fonction pour tester les collisions
- 4
- Fonction de mise à jour
- 5
- Fonction d’affichage
- 6
- Boucle principale
1.1 Les objets en Python
Nous abordons toujours Python avec une approche de type « observer, copier et modifier » en vous fournissant du code TIC-80 à modifier. Si cette approche ne vous convient pas, nous vous encourageons fortement à travailler en dehors des séances de TD avec futurecoder, un outil d’aide à l’apprentissage de Python.
Le début du code utilise le mot-clé class pour définir une structure de donnée player. On dit que player est une classe et p1 définit plus loin par p1=player() est une instance de cette classe.
Les objets représentent des entités de notre jeu, ici le joueur. Ils stockent des valeurs dans leurs attributs et définissent des comportements à l’aide de fonctions qui leurs sont propres appelées méthodes. Les attributs et les méthodes d’un objet lapin sont accessibles grâce à la notation pointée lapin.nom ou encore lapin.mange(carotte).
Une classe définit les méthodes disponibles sur ses instances. Afin de pouvoir accéder aux attributs de l’objet sur lequel est invoquée la méthode, celle-ci dispose d’un premier paramètre appelé self.
Une méthode spéciale, __init__ est exécutée lors de la création de l’objet. C’est cette méthode qui se charge d’initialiser l’objet, en particulier de fixer les valeurs initiales de ses attributs. Cette méthode est parfois appellée un constructeur1.
Regardons tout cela sur notre joueur :
class player:
"le joueur et ses attributs"
1 def __init__(self):
# coordonnees
self.x=8
self.y=120
# orientation (0: droite, 1: gauche)
self.dir=0
# deplacement
self.dx=0
self.dy=0
# les pieds au sol ?
self.isgrounded=False
# hauteur de saut
self.jumpvel=3.6
# gestion de l'animation
self.st=0
self.anim=[[1],[1,2],[3]]
2 def draw(self):
"dessine le joueur"
a=self.anim[self.st]
s=a[(t//10)%len(a)]
spr(s,int(self.x),int(self.y),0,flip=self.dir)- 1
-
Constructeur de la classe
player - 2
-
Méthode
drawpour dessiner le joueur à l’écran
Lors de la création d’une instance, la méthode __init__ se charge comme on le voit de définir et d’initialiser tout un tas d’attributs, ici x, y, dir, dx, dy, isgrounded, jumpvel, st et anim. Chaque instance de la classe possède ses propres attributs.
La classe player définit une méthode draw qui peut ensuite être invoquée pour toute instance p comme une fonction en appelant p.draw(). Lors de cet appel, la valeur self pointera sur l’instance p. Ainsi, dans le code, self.st fera référence à p.st, etc.
Ouvrir la cartouche GameOn/TD3/boules.tic. Lire et comprendre son code source.
Modifier le programme pour qu’il utilise 6 instances de la classe bestiole avec des couleurs dans l’intervalle 3-7.
1.2 Boucle principale et affichage
Le code responsable de l’affichage et la boucle de jeu sont très semblables à ce que nous avons vu dans le TD2 :
def draw():
cls()
map()
p1.draw()
def TIC():
global t
# cache la souris
poke(0x3ffb,0)
update()
draw()
print("Python platformer demo",20,20,12)
t+=1On notera l’utilisation d’une astuce pour cacher le curseur de la souris.
La boucle de jeu est classique : mise à jour et dessin alternent. La fonction de dessin efface l’écran, affiche la carte puis elle affiche le joueur.
Le programme n’utilise que 3 variables globales grâce à l’utilisation des objets.
# les variables globales
t=0
p1=player()
grav=0.2La valeur grav spécifie l’intensité de la force de gravitation.
La variable p1 est une instance de la classe player qui représente le joueur.
La variable t mesure le temps qui passe.
1.3 Sprites et décor
Comme dans le TD2, le décor est produit en utilisant une carte avec des tuiles dont certaines ont le drapeau 0 activé pour indiquer qu’elles sont infranchissables.
Comme dans le TD2, il faut être vigilant sur le système de coordonnées utilisé : les tuiles occupent 8x8 pixels et les sprites sont affichés en coordonnées écran lorsqu’on utilise spr.
1.4 Plateformes et mouvement
La méthode update se charge du gros du travail : gérer la physique du jeu, c’est-à-dire l’action de la gravité, les touches pressées par le joueur, ainsi que le caractère infranchissable des murs.
La fonction is_solid(dx,dy) permet de tester si un pixel situé autour du joueur est à l’intérieur d’une tuile infranchissable. Pour cela, il suffit d’interroger le drapeau 0 avec fget sur la tuile qui se trouve sous le pixel de coordonnées (p1.x+dx, p1.y+dy), en coordonnées tuiles.
def is_solid(dx,dy):
"""teste si le pixel en position (dx,dy)
par rapport au joueur est solide"""
h=mget(int(p1.x+dx)//8,int(p1.y+dy)//8)
return fget(h,0)Attention, le déplacement (0,0) correspond au pixel en haut à gauche du sprite du joueur. Ainsi, pour ce sprite, les pixels juste sous les pieds sont en (1,8) et (6,8).
Lire le code de la fonction update en prenant soin de faire correspondre, bloc par bloc, votre compréhension avec les commentaires.
def update():
"mise a jour du jeu"
global p1
# position initiale
startx=p1.x
1 # est-ce qu'il faut sauter ?
if p1.isgrounded and (btnp(0) or btnp(4) or btnp(5)):
p1.dy=-p1.jumpvel
p1.st=2
2 # est-ce que le joueur se deplace ?
p1.dx=0
if p1.isgrounded:
p1.st=0
if btn(2):
# un pas a gauche
p1.dx-=1
p1.dir=1
if p1.isgrounded: p1.st=1
if btn(3):
# un pas a droite
p1.dx+=1
p1.dir=0
if p1.isgrounded: p1.st=1
p1.x+=p1.dx
3 # collision avec un mur ?
xoffset=0
if p1.dx>0:
xoffset=7
if is_solid(xoffset, 7):
p1.x=startx
4 # la gravite agit
p1.dy+=grav
p1.y+=p1.dy
5 # est-ce qu'on s'enfonce dans le sol ?
p1.isgrounded=False
if p1.dy>=0 and (is_solid(1,8) or is_solid(6,8)):
p1.y=(int(p1.y)//8)*8
p1.dy=0
p1.isgrounded=True
6 # attention au plafond !
if p1.dy<=0 and (is_solid(1,0) or is_solid(6,0)):
p1.y=(int(p1.y+8)//8)*8
p1.dy=0- 1
-
Si le joueur est au sol et qu’une touche de saut est appuyée : ajoute une impulsion à
dy. - 2
-
Calcule le déplacement horizontal selon les touches enfoncées, met à jour
dxetx. - 3
- Si le déplacement horizontal nous a mené dans un mur, annule le déplacement.
- 4
- La gravité tire le joueur vers le bas.
- 5
- Si le joueur est en train de s’enfoncer dans une tuile solide, pose-le au-dessus de cette tuile.
- 6
- Si le joueur se cogne la tête dans une tuile solide, pose-le au-dessous de cette tuile.
1.5 Animation du personnage
La classe player se charge dans sa méthode draw de gérer l’animation du personnage.
Une animation est définie comme une suite de sprites à afficher en boucle.
Un attribut st permet de mémoriser l’animation courante. L’offset est calculé à l’aide de t.
(...)
1 self.st=0
self.anim=[[1],[1,2],[3]]
(...)
def draw(self):
"dessine le joueur"
2 a=self.anim[self.st]
s=a[(t//10)%len(a)]
spr(s,int(self.x),int(self.y),0,flip=self.dir)- 1
-
définition des différentes animation et de l’animation courante
st - 2
-
calcul de la tuile à afficher à partir de
self.stet det
Le code est compris ? Alors il est temps de créer un jeu en partant de ce code et en le modifiant selon vos besoins et vos envies !
En adaptant les paramètres grav et jumpvel à vos goûts et en modifiant les tuiles/sprites, créer un jeu de plateformes à plusieurs niveaux. Dans chaque niveau, le joueur doit collectionner des objets pour gagner des points et récupérer une clé pour ouvrir la porte permettant d’accéder au niveau suivant.
Prendre le temps de faire quelque chose de satisfaisant !
Une fois votre jeu terminé, il est temps de faire une pause ludique ! Télécharger la cartouche .tic d’un jeu TIC-80 parmi ceux présentés en fin de fiche. Ajouter cette cartouche à votre TIC-80 et jouer une partie du jeu ! Comparer le comportement du joueur dans votre jeu et dans celui choisi : que manque-t-il encore à votre jeu ?
2 Améliorations, trucs et astuces
Cette section est une collections de petites améliorations et autres astuces. Le jeu GameOn/TD3/runner.tic met en œuvre une grande partie de ces améliorations.
2.1 Améliorer le saut
Les joueurs modernes préférent des jeux de plateformes plus arrangeants.
2.1.1 Prise en compte anticipé du saut
Dans les jeux moderne, si le joueur presse le bouton de saut quelques frames avant d’avoir touché le sol, le personnage sautera dès qu’il atteindra le sol.
Pour mettre en œuvre cet effet, il suffit de mémoriser la date à laquelle le dernier appui sur une touche de saut a eu lieu et de sauter si l’intervalle est suffisamment petit, dès que les conditions de saut sont satisfaites.
Ajouter cette fonctionnalité à votre code de jeu de plateformes ! Pour cela, ajouter un attribut jumpt au joueur qui enregistre la valeur de t lors du dernier appui sur la touche saut. Modifier la fonction update en conséquence. Tester.
2.1.2 Temps du Coyote
Tel le coyote dans le dessin animé Bip Bip et Coyote, il s’agit d’autoriser un saut après que le joueur ait quitté la plateforme depuis quelques frames.
Pour mettre en œuvre cet effet, il faut mémoriser la date à laquelle le joueur a quitté la plateforme et autoriser le saut si l’intervalle est suffisament petit.
Ajouter cette fonctionnalité à votre code de jeu de plateformes ! L’idée est la même que pour la fonctionnalité précédente. Tester.
2.2 Sauts spéciaux
Les jeux de plateformes modernes proposent des sauts spéciaux (soit dès le début du jeu soit après les avoir débloqués comme dans tout bon metroidvania) :
- super saut : sauter plus haut que la normale (par exemple par un appui prolongé) ;
- double saut : sauter une seconde fois en l’air pour atteindre une plateforme plus lointaine ;
- saut mural : sauter en appui sur une paroi verticale ;
- glissade murale : rester collé et glisser le long d’une paroi ;
- saut avec dash : déplacement rapide en plein vol ;
- saut avec grapin : agripper la paroi avec un grappin et se balancer ;
- saut avec élan : emmagasiner de l’énergie avant de sauter pour sauter plus loin ;
- saut sur trampoline : utiliser des éléments du décor pour sauter plus haut ;
- inversion de gravité : sauter inverse le sens de la gravité comme dans l’excellent VVVVVV.
Choisir un ou deux sauts spéciaux et les ajouter à votre moteur de jeux de plateformes. Tester.
2.3 Retour sur TIC-80
2.3.1 L’éditeur de sons
L’éditeur de sons (sfx editor) est le quatrième des cinq éditeurs spécialisés de TIC-80. On y accède depuis les éditeurs avec la touche F4 ou avec l’icône en forme de porte-voix .
Lire la documentation du Wiki TIC-80 et la documentation de la fonction d’API sfx. Regarder le code source de GameOn/TD3/runner.tic pour des exemples d’utilisation.
Faites du bruit ! Créer quelques sons pour le (dé)plaisir des oreilles de vos voisins :
- le son d’un personnage qui saute ;
- le son de la défaite ;
- le son de la victoire ;
- le son lorsqu’une clé est ramassée.
Invoquer ces sons avec la fonction d’API sfx dans vos jeux !
2.3.2 L’éditeur de musique
L’éditeur de musique (music editor) est le dernier des cinq éditeurs spécialisés de TIC-80. On y accède depuis les éditeurs avec la touche F5 ou avec l’icône en forme de notes de musique .
Lire la documentation du Wiki TIC-80.
Références
- Le site futurecoder pour débuter en Python en autonomie ;
- Le wiki officiel de TIC-80 ;
- Une sélection de jeux de plateformes TIC-80 pour nourrir votre inspiration :
- Avant d’être un succès planétaire, le jeu Celeste, dont le moteur de plateformes est repris dans le jeu TowerFall, a fait ses débuts sur la console fantaisiste Pico-8. La version classic issue de Pico-8 sera même adaptée pour Game Boy Advance. Les auteurs du jeu fournissent grâcieusement une partie du code source et ont décrit dans un chouette article la physique derrière Celeste & TowerFall.
Notes de bas de page
en réalité ce n’est pas vraiment le constructeur, c’est le rôle de
__new__, mais c’est une autre histoire…↩︎