TD6 : Lemmings

L’objectif de ce TD est d’étudier la gestion des IA des personnages d’un jeu de tactique/gestion. Nous allons présenter la mise en place d’une machine à états permetant à un personnage d’effectuer successivement différentes actions afin de suivre un scénario.

Introduction

Présentation

Phénomène vidéoludique de l’année 1991, Lemmings est plébiscité pour son concept singulier, à la fois simple et ingénieux mélangeant jeu de gestion et jeu de casse-tête. Le joueur doit guider vers la sortie des dizaines de lemmings ceci dans des niveaux truffés de dangers mortels ceci afin d’en sauver un maximum ! Ce ne sera pas facile, et beaucoup n’y arriveront pas : chute, explosion, lave… La vie d’un lemmming est particulièrement aléatoire.

Heureusement des aptitudes sont disponibles, elles sont représentées par des icônes au bas de la fenêtre de jeu. Le joueur peut cliquer sur un icône et ensuite attribuer l’aptitude en question à un lemming en cliquant dessus. Voici les différents aptitudes présentes dans le jeu :

  • Climber – capable d’escalader les parois verticales

  • Floater – chute avec un parasol lui évitant de s’écraser au sol !

  • Blocker – s’immobilise et empêche les autres lemmings de passer

  • Bomber – explose après un compte à rebours de cinq secondes en détruisant une partie des murs

  • Builder – échafaude un escalier de 12 marches qui sera emprunté par ses collègues

  • Basher – creuse à l’horizontale

  • Digger – creuse sous ses pieds

  • Miner – creuse en diagonale

Le jeu comprend 30 niveaux de plus en plus complexes. A chaque niveau, le joueur dispose de certaines aptitudes en quantité limitée afin de contourner les différents pièges. Ainsi, le jeu se tranforme en énigme où il faut trouver quelle aptitude utiliser, sur quel lemming et à quel moment/endroit. Pour réussir un niveau, il faudra sauver un minimum de lemmings en utilisant les aptitudes disponibles.

Pour mieux visualiser le gameplay voici un extrait des niveaux introductifs présentant les différentes aptitudes :

Digger

Blocker

Voici les vidéos pour les autres aptitudes :

Test

Vous pouvez tester le jeu en réel sur cette page

  • Choisissez : 4 - VGA

  • Choisissez : 2 - High Performance PC

  • Choisissez : 1 player

  • ESC : pour récupérer le curseur de la souris

Les lemmings

Parenthèse animalière, le nom du jeu et le look des personnages sont inspirés des véritables lemmings minuscules rongeurs que l’on trouve dans les régions arctiques. Mi-souris, mi-marmottes, ils ont la force de survivre dans un milieu où le sol est en permanence gelé. Ils passionnent les habitants et les chercheurs car leur population suit un cycle de 4 ans, alternant une phase de surpopulation où l’on peut voir des groupes de lemmings traverser des lacs gelés à la course avec une phase de quasi-instinction de l’espèce. Ce phénomène mystérieux reste inexpliqué mais il continue à animer de nombreuses légendes. Si vous voulez en savoir plus sur les lemmings, rien de mieux que de consulter leur page sur l’office de la faune du Canada.

../_images/lemming.png

Mise en place

Téléchargez le projet Lemmings.zip et dézippez-le.

Au lancement du programme, vous obtenez ceci :

../_images/demo3.gif

Le projet est incomplet. Quelques remarques :

  • Le décor semble complet

  • Les lemmings apparaissent et chutent dans le vide

  • Aucune gestion de collision ne semble active

  • Une fois les 10 lemmings disparus, plus rien ne se passe

Notions clefs

Gérer une animation

Nos lemmings effectuent une multitude d’actions. Ils creusent, ils marchent, ils grimpent… Pour produire un rendu graphique sympathique, les graphistes dessinent une animation sous forme d’une suite de sprites. Ci-dessous, nous vous présentons la série associée à l’action « creuser le sol », elle se lit de gauche à droite.

../_images/creuse.png

Il y a plusieurs actions et chacune comporte plusieurs sprites. Ainsi pour les lemmings, on dispose d’environ 250 sprites d’animation. Plutôt que de stocker chaque sprite dans un fichier, on préfère stocker l’ensemble dans une grande image appelée « sprite sheet », comme par exemple pour le personnage de Sonic ci-dessous :

../_images/sonic.png

Tester une animation dans Piskel

Vous pouvez visualiser et éventuellement corriger cette animation grâce à l’éditeur en ligne Piskel.

  • Téléchargez l’animation creuse.png

  • Dans Piskel, cliquez sur l’icône Import sur la droite :

../_images/piskel1.png
  • Choisissez : Import From Picture

../_images/piskel2.png
  • Dans la boite de dialogue, choisissez l’option Sprite Sheet et entrez les valeurs 30x28 pour indiquer la taille de chaque sprite. Remarquez que le logiciel découpe alors chaque sprite par un rectangle orange :

../_images/piskel3.png

Nous revenons à l’interface principale, vous remarquez les différentes images de l’animation numérotées dans la colonne de gauche. Au centre se trouve l’éditeur de l’image en cours, on peut modifier les pixels ou la recaler s’il y a un problème de centrage.

../_images/piskel4.png

Dans la zone de test d’affichage en haut à droite, nous voyons l’animation qui se déroule :

../_images/anim1.gif

Nous vous laissons regarder les tutoriaux en ligne pour prendre en main Piskel si vous êtes intéressés.

Utiliser une Sprite Sheet

Nous vous fournissons une planche simplifiée de sprites pour le jeu Lemmings. Chaque sprite est stocké dans un carré 30x32 et chaque animation occupe une ligne. Lorsqu’il n’y a plus de sprites à charger, une case rouge est présente :

../_images/planche.png

Voici la fonction qui charge la ligne d’animation numéro id et qui retourne une liste de sprites :

../_images/code2.png

Pour gérer une animation, nous utilisons la ligne suivante :

compteur = int( pygame.time.get_ticks() / 100 )

Ainsi, toutes les 0.1 secondes, la valeur de compteur augmente de 1. Cette valeur nous indique le sprite à afficher parmi les sprites de l’animation. Ainsi, nous allons afficher le sprite donné par :

ListeSprites[compteur%len(creuse)]

En utilisant cette ligne, lorsque la variable compteur progresse de 3 à 10, avec len(creuse)=4 par exemple, l’indice va varier ainsi : 3, 0, 1, 2, 3, 0, 1, 2, 3… De cette façon, une animation avec 15 sprites dure 1.5 secondes.

Gérer des personnages

Fini la tranquillité de l’aquarium où il n’y avait que quelques poissons à gérer, avec le jeu Lemmings, on passe à la vitesse supérieure. En effet, on doit pouvoir gérer une centaine d’individus. On va donc utiliser une liste Python dans laquelle nous allons stocker nos lemmings. Comment représenter un personnage ? Nous pouvons utiliser un concept très plébiscité dans le monde du jeu vidéo : les dictionnaires. Ils fournissent les mêmes facilités que l’objet avec l’aspect dynamique en plus. Ainsi, un lemming est créé de la manière suivante :

lemming = { }
lemming['x'] = 250    # position
lemming['y'] = 100
lemming['vx'] = -1    # vitesse
lemming['vy'] = 0
lemming['etat'] = ETAT.Marche  # action courante

On stocke ensuite ce personnage dans les listes des lemmings au moyen de la commande « append » :

lemmingsLIST  = []
lemmingsLIST.append(lemming)

Ainsi dans la fonction d’affichage de notre jeu, nous allons trouver :

AfficheEcranJeu :
        Afficher le fond
        Pour chaque lemming dans la liste des lemmings
                lemming courant => fais ton affichage
        Afficher le score
        ...

Python nous fournit une syntaxe tout à fait simple pour parcourir une liste :

for onelemming in lemmingsLIST:
        xx = onelemming['x']
        yy = onelemming['y']
        screen.blit(…,(xx,yy))

A ce niveau, il faut bien saisir que onelemming est un lemming de la liste. Lorsque l’on rentre dans la boucle, onelemming correspond au premier lemming de la liste. Ensuite, une fois l’opération blit effectuée, onelemming est mis à jour par l’instruction for, il correspond alors au 2ème lemming dans la liste et ainsi de suite jusqu’au dernier. La variable onelemming va désigner à tour de rôle chaque lemming de la liste.

Gestion des transitions et des états

Un lemming a une intelligence et doit se comporter différemment en fonction de ce qu’il se passe dans le jeu. Pour gérer cela, nous utilisons une machine à états, ce qui rend la gestion du jeu un peu plus complexe. Par exemple, lorsqu’il marche et qu’il se retrouve au-dessus du vide, le lemming passe en mode « chute ». Lorsqu’il chute, on incrémente un compteur pour estimer la hauteur de sa chute et lorsqu’il touche le sol deux options sont possibles : soit le compteur est trop important et il s’écrase au sol et passe en état « dead » sinon il survit et repasse à l’état « marche ».

Ainsi, dans la fonction gérant la logique du jeu, pour un lemming donné, nous devons effectuer deux traitements pour gérer sa machine à états :

  1. Déterminez s’il y a une transition : analysez l’état courant et l’environnement

  2. Appliquez l’action associée à l’état courant

Prenons un exemple avec trois états et leurs transitions : Dead, Chute, Marche et une variable fallcount indiquant le nombre de pixels parcourus depuis le début de la chute du lemming. En représentant les états par des ronds et les transitions par des flèches, nous obtenons le schéma des transitions suivant :

../_images/etats.png

Voici les actions effectuées dans chaque état :

  • Dead : pas grand-chose à faire ! Éventuellement un compteur pour faire disparaître le sprite 10 secondes plus tard.

  • Chute : le lemming tombe y = y - 1 et on incémrente la variable de comptage : fallcount = fallcount + 1

  • Marche : le lemming avance : x = x + vx

Afin de gérer les états, chaque lemming embarque son paramètre [“etat”]. Cependant, il est dangereux de représenter les états par des chiffres, en effet, pour des raisons de lisibilité, vous allez rapidement vous mélanger. Un programmeur averti va préférer utiliser des noms de variable :

# Liste des états
EtatMarche = 1
EtatChute  = 2
EtatStop   = 3
EtatDead   = 4

Pour faciliter le Débogage, on pourra encore améliorer la lisibilité en écrivant :

# Liste des états
EtatMarche = 'EtatMarche'
EtatChute  = 'EtatChute'
EtatStop   = 'EtatStop'
EtatDead   = 'EtatDead'

Ainsi, au lieu d’écrire lemming[“Etat”] = 200 ce qui est peu lisible, on écrira : lemming[“etat”] = EtatChute ce qui est bien plus clair. Pour gérer les transitions comme pour gérer les actions, nous utilisons le mot clef match :

match lemming.Etat:
   case EtatMarche:
      actionMarche(lemming)
   case EtatChute:
      actionChute(lemming)
   case EtatStop:
      actionStop(lemming)

Vous pouvez encore alléger le code en utilisant une version plus élégante basée sur les dictionnaires :

def actionMarche(lemming):
   ...
def actionChute(lemming):
   ...

ActionToPerform = { EtatMarche:actionMarche, EtatChute:actionChute }

# et plus tard dans le code, on trouvera :
lemming = { 'Etat':EtatMarche }
...
ActionToPerform[lemming['Etat']](lemming)

Note

Évidemment, lors de la gestion de l’affichage, l’état courant va nous permettre de choisir quelle animation afficher.

Gestion des scénarios

On entend par scénario un regroupement d’états dans notre diagramme. Ainsi, pour le scénario Bomber, on construit un enchaînement d’états/actions :

  • Etat Attendre

  • Etat Explose

  • Etat Dead

Il n’est pas évident d’identifier quels sont les états d’un scénario. Mais dans ce jeu, c’est assez simple, car ils sont chacun associés à une animation. Ainsi dans l’état Attendre, le lemming reste figé en tapant du pied, ensuite dans l’état Explose, une animation avec une petite onde de choc se produit et pour finir dans l’état Dead, il reste au sol éventuellement.

Les scénarios sont très communs dans le monde des jeux de gestion/simulation, par exemple, on peut envoyer un bucheron ramasser du bois :

  • Se déplacer jusqu’à l’arbre le plus proche

  • Couper l’arbre

  • Retourner au chateau

  • Déposer le bois
    • Transition : recommencer le cycle s’il reste des arbres dans la forêt

Ajouter des états

En ajoutant de nouveaux scénarios (creuser, bloquer…), nous allons créer de nouveaux états et de nouvelles transitions. Afin de faciliter la conception du jeu, nous faisons en sorte que chaque scénario ne puisse être activé que par une transition depuis l’état Marche. Nous obtenons donc un schéma en étoile comme celui-ci :

../_images/etoile.png

Note

Ce choix peut paraître étrange ou maladroit. En effet, pour l’état Creuser par exemple, une fois terminé, le lemming devrait passer en état Chute fort probablement. Mais avec notre convention, il va d’abord passer en état Marche et juste après en état Chute. Cette petite erreur va durer 1/30eme de seconde et personne ne s’en apercevra et notre astuce nous fera économiser beaucoup de lignes de code.

Sans cette astuce, la gestion des transitions deviendrait complexe comme dans le diagramme ci-dessous :

../_images/mega.png

To-do List

Attention, vous devez respecter les consignes :

  • Séparation de la logique et de l’affichage : il serait maladroit d’effectuer un affichage dans la partie gérant la logique

  • Gestion des transitions et des actions

Implémentez les fonctionnalités suivantes :

  • Démarrage du jeu :

    • Créez les états Dead & Chute

    • Faîtes en sorte que 15 lemmings soit créés et que leur état par défaut soit l’état Chute

    • Implémentez le compteur de chute

    • Testez la valeur du pixel sous le sprite du lemming, s’il n’est pas noir, activez la transition vers l’état Marche ou l’état Dead

  • Implémentez l’état marche

    • Implémentez l’état Marche

    • Gérez l’affichage de l’état Marche

    • Par défaut, faites les marcher vers la gauche

    • Les sprites utilisés pour la marche sont synchronisés : chaque lemming utilise le même ce qui produit un effet visuel désagréable. Ajoutez un paramètre « Decal » à chaque lemming et initialisez-le aléatoirement. Servez-vous de ce nombre pour désynchroniser l’animation de marche des lemmings.

  • La chute mortelle

    • Les lemmings doivent se déplacer jusqu’à la gauche, tomber de la falaise et s’écraser sur le sol

    • Gérez l’affichage de l’état Dead (dernière ligne en bas sur la planche de sprites)

    • L’état Dead est terminal, il n’y a pas de transition pour en sortir !

    • Gérez l’animation de l’état Dead. Attention, une fois l’animation jouée, elle ne recommence pas !

Nous allons programmer les premières modifications de comportement. Le joueur clique sur l’icône d’une aptitude dans l’interface : creuser, stopper… Ensuite, lorsqu’il clique sur un lemming, l’état de ce dernier est modifié pour correspondre à la tâche demandée.

  • Détectez le clic sur les icônes en bas de l’écran

  • Allumez la petite lampe au-dessus des icônes des aptitudes pour désigner l’aptitude active

    • Choisissez une couleur quelconque

    • Une seule lampe est allumée à la fois

  • Implémentez l’aptitude Blocker

    • C’est un état terminal, impossible d’en sortir

    • Si l’aptitude Blocker est activée et que le joueur clique sur un lemming, passez son état à STOP

    • Pour passer à l’état STOP, il faut que le lemming soit précédemment en état MARCHE

  • Interaction

    • Si en marchant un lemming rencontre un lemming en état STOP, il doit changer de direction

    • Programmez la détection de collision à gauche et à droite des lemmings

    • Un lemming doit aussi changer de direction lorsqu’il rencontre un mur

    • Pour détecter la collision avec un lemming, on peut calculer une distance comme dans le projet Labyrinthe

    • Pour détecter la collision avec un mur, on peut lire dans le sprite du décor la valeur du pixel se trouvant devant le lemming

    • Pour créer l’animation de marche vers la droite, utilisez la symétrie sur un sprite comme dans le projet Aquarium

    • Vous pouvez aussi appliquer un décalage sur le sprite symétrique pour que les animations marche gauche/marche droite soient centrées

  • Le scénario Creuser

    • Si l’icône Creuser est actif et que le joueur clique sur un lemming, passez son état à Creuser

    • Pour passer à l’état Creuser, le lemming doit être dans l’état Marche

    • Gérez l’animation de l’état Creuser

    • Dans l’état Creuser, le lemming fait disparaître 20 pixels du décor sous lui, ceci toutes les 2 secondes

    • Pour effacer une partie du décor, mettez les pixels à la couleur noire. A ce moment-là, vous pouvez aussi descendre le lemming d’un pixel

    • Transition : si les 20 pixels sous le lemming sont tous noirs => retour à l’état Marche, qui déclenchera probablement une transition vers l’état Chute

    • Relativement à l’histoire, les lemmings tombant de moins haut, ils doivent arriver vivant sur la ligne du bas, paramétrer la borne de fallcount en fonction

  • La sortie

    • Positionnez le sprite de la porte de sortie en bas à droite

    • Dessinez le sprite de la porte avant les sprites des lemmings afin qu’elle apparaisse en arrière-plan

    • Détectez lorsqu’un lemming arrive au milieu de la porte et retirez-le de la liste courante

  • Fin de partie

    • Lorsque tous les lemmings sont partis ou morts, affichez « WIN » en gros au centre de l’écran s’il y a eu plus de 10 survivants