Apprendre à coder avec Pyxel : gérer la souris dans son jeu

Voici un nouveau chapitre pour apprendre le framework de création de jeux Pyxel. Oui, on dit cadriciel en français, mais cette traduction est tellement laide.

Cette nouvelle démo va être un poil plus compliqué que la soucoupe volante gérée au clavier. Pas d’inquiétudes, la marche entre les deux projets n’est pas très haute.

Bouquins pour apprendre Python

Learning Python Python pour les kids

De toute façon, vous avez trouvé un bon bouquin pour apprendre Python et vous avez commencé à le lire, n’est-ce pas ?

Pour les grands, en anglais, il y a le Oreilly : Learning Python.

Il existe une traduction faite par une IA que je n’aurai absoluement envie de rire.

Pour les jeunes, il y a le No Starch, traduit à la main par Eyrolles : Python pour les kids.

Démo

Bat Cette fois-ci, avec sa souris, le joueur va faire voler une charmante chauvesouris dans l’obscurité de sa grotte.

Pour ne pas s’embrouiller entre chauvesouris (le sprite) et souris (le pointeur), on va déclarer que c’est une pipistrelle.

Assets

Les assets contiennent deux sprites de la chauvesouris, deux images de 16 pixels posées côte à côte. La première avec les ailes en haut, la seconde avec les ailes en bas.

Éditeur de chauvesouris

Si vous avez la flemme de dessier, le plus simple est de télécharger le fichier bat.pyxres.

Code

Le code étant un poil plus compliqué que l’initiation, la chauvesouris a sa propre class : Bat.

Comme tout bon code orienté objet, Bat va gérer tout ce qui le concerne.

Une chauvesouris qui bat des ailes

L’attribut frame indique l’image courante, 0 pour les ailes en haut, 1 pour en bas.

La fonction swap_wings va inverser la position des ailes.

La fonction draw va commencer par inverser la position des ailes toutes les 10 frames, avant de dessiner son sprite. Une fois sur 10 permet d’avoir un battement lent, pas celui d’un colibri.

App est minimaliste, il prépare le jeu puis démarre la boucle, seule nouveauté, il instancie une Bat et lui envoie les évènements.

Dans la première itération du code, il n’y a pas d’interactions, App se contente de demander à l’instance de Bat de draw.

import pyxel


class Bat:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.images = [ # The bat uses two images : wings up, wings down
            (0, 0, 16, 16, 0),  # x, y, width, height, transparent color
            (16, 0, 16, 16, 0),
        ]
        self.frame = 0 # 0 : bat wings are up, 1 : down

    def swap_wings(self):
        "The wings switch from up to dwon"
        if self.frame == 0:
            self.frame = 1
        else:
            self.frame = 0

    def draw(self):
        "Draw the sprite"
        if pyxel.frame_count % 10 == 0: # Every 10 frames, to wings flip
            self.swap_wings()
        pyxel.blt(self.x, self.y, 0, *(self.images[self.frame]))


class App:
    def __init__(self):
        pyxel.init(160, 120, title="Flying bat")  # width, height, title
        pyxel.load("bat.pyxres") # Load the assets

        self.bat = Bat(72, 72) # Spawn a new bat 🦇

        pyxel.run(self.update, self.draw) # Starts Pyxel loop

    def update(self):
        if pyxel.btnp(pyxel.KEY_Q): # Hit Q to quit
            pyxel.quit()

    def draw(self):
        pyxel.cls(0) # Clear screen
        self.bat.draw() # Draw the bat at its current position


App()

Le jeu se contente d’afficher une chauvesouris qui bat des ailes. La seule interaction possible est de quitter le jeu, en taper sur la touche Q, oui, c’est frustrant.

Une chauvesouris qui suit la souris quand on clique

On commence par afficher le curseur de la souris avec pyxel.mouse(True). Un clique sur le bouton de gauche (pyxel.MOUSE_BUTTON_LEFT) déplace la pipistrelle à la position du curseur.

En maintenant le bouton, il est possible de lier la position de la pipistrelle au curseur de la souris, et de la faire gigoter en secouant la souris.

import pyxel


class Bat:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.images = [  # The bat uses two images : wings up, wings down
            (0, 0, 16, 16, 0),  # x, y, width, height, transparent color
            (16, 0, 16, 16, 0),
        ]
        self.frame = 0  # 0 : bat wings are up, 1 : down

    def swap_wings(self):
        "The wings switch from up to dwon"
        if self.frame == 0:
            self.frame = 1
        else:
            self.frame = 0

    def draw(self):
        "Draw the sprite"
        if pyxel.frame_count % 10 == 0:  # Every 10 frames, to wings flip
            self.swap_wings()
        pyxel.blt(self.x, self.y, 0, *(self.images[self.frame]))


class App:
    def __init__(self):
        pyxel.init(160, 120, title="Flying bat")  # width, height, title
        pyxel.load("bat.pyxres")  # Load the assets

        self.bat = Bat(72, 72)  # Spawn a new bat 🦇

        pyxel.mouse(True)  # Show the mouse

        pyxel.run(self.update, self.draw)  # Starts Pyxel loop

    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):  # Hit Q to quit
            pyxel.quit()

        if pyxel.btn(pyxel.MOUSE_BUTTON_LEFT):
            self.bat.x = pyxel.mouse_x
            self.bat.y = pyxel.mouse_y
            pyxel.mouse(False)
        else:
            pyxel.mouse(True)

    def draw(self):
        pyxel.cls(0)  # Clear screen
        self.bat.draw()  # Draw the bat at its current position


App()

Le peu d’interaction est gratifiant, mais accrocher une bestiole aux mouvements de la souris reste simpliste.

Une chauvesouris qui volète vers un point désigné par un clic

Commencez par effacer le bout de code qui lie la souris à la pipistrelle.

Coordonnées Dans cette ambitieuse version, des maths (niveau 3°) vont être utilisées pour calculer le chemin :

  • distance calcule la distance entre deux points avec l’aide de notre ami Euclide.
  • angle calcule l’angle entre deux points, avec de la trigonométrie.

Techniquement, on passe de coordonnés cartésiens à coordonnées polaires.

Quand on clique, un chemin (un vecteur) est calculé entre la position courante de la chauvesouris et la position cible désignée par le clic.

À chaque frame, la chauvesouris avance d’un pas (sa vitesse), sur ce vecteur. Si la distance entre la chauvesouris et sa cible est inférieure au pas, elle s’arrête, ce qui permet de gérer simplement les problèmes de comptes qui ne tombent pas rond.

Pyxel utilise le coin haut gauche du sprite pour désigner sa position. Donc, sans correction, la chauvesouris stopperait quand son coin haut gauche atteint la position du clic, ce qui serait peu intuitif et même laid.

Pour les calculs de chemin et de distance, il faut utiliser la position du centre du sprite soit : x + width / 2 et y + height / 2.

import pyxel


class Bat:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.images = [  # The bat uses two images : wings up, wings down
            (0, 0, 16, 16, 0),  # x, y, width, height, transparent color
            (16, 0, 16, 16, 0),
        ]
        # self.x is the top left corner of the image,
        # computing the position of the center of the bat is more intuitive
        self.center_x = self.images[0][2] / 2
        self.center_y = self.images[0][3] / 2
        self.frame = 0  # 0 : bat wings are up, 1 : down
        self.angle = 0  # Where does the bat look ?
        self.target_x: int  # The bat goes to this target
        self.target_y: int
        self.speed = 0

    def move_to(self, x, y, speed):
        "The bat rotate and will move to the target"
        self.target_x = x
        self.target_y = y
        self.speed = speed
        self.rotate_to(x, y)

    def rotate_to(self, x, y):
        self.angle = angle(x, y, self.x + self.center_x, self.y + self.center_y)

    def one_step(self):
        "The bat moves to its target"
        if self.speed != 0:
            delta = distance(
                self.target_x,
                self.target_y,
                self.x + self.center_x,
                self.y + self.center_y,
            )
            if delta <= self.speed:
                self.speed = 0
            else:
                self.x += pyxel.sin(self.angle) * self.speed
                self.y += pyxel.cos(self.angle) * self.speed

    def swap_wings(self):
        "The wings switch from up to down"
        if self.frame == 0:
            self.frame = 1
        else:
            self.frame = 0

    def draw(self):
        "Draw the sprite"
        if pyxel.frame_count % 10 == 0:  # Every 10 frames, to wings swap
            self.swap_wings()
        pyxel.blt(self.x, self.y, 0, *(self.images[self.frame]))


def distance(x1, y1, x2, y2: float) -> float:
    "Euclidian distance"
    dx = x1 - x2
    dy = y1 - y2
    return pyxel.sqrt(dx**2 + dy**2)


def angle(x1, y1, x2, y2: int) -> float:
    "Get the angle between two points with trigonometry"
    dx = x1 - x2
    dy = y1 - y2
    return pyxel.atan2(dx, dy)


class App:
    def __init__(self):
        pyxel.init(160, 120, title="Flying bat")  # width, height, title
        pyxel.load("bat.pyxres")  # Load the assets

        self.bat = Bat(72, 72)  # Spawn a new bat 🦇

        pyxel.mouse(True)  # Show the mouse

        pyxel.run(self.update, self.draw)  # Starts Pyxel loop

    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):  # Hit Q to quit
            pyxel.quit()

        if pyxel.btn(pyxel.MOUSE_BUTTON_LEFT):  # Mouse click set the target
            self.bat.move_to(pyxel.mouse_x, pyxel.mouse_y, 2)  # x, y, speed

        self.bat.one_step()  # The bat fly, one step at time

    def draw(self):
        pyxel.cls(0)  # Clear screen
        self.bat.draw()  # Draw the bat at its current position


App()

Comme le calcul du chemin et relancé à chaque clic, il est possible de faire voleter la chauve souris d’un bout à l’autre de l’écran, sans qu’elle ait le temps de s’arrêter.

2 carrés

Dans les vrais jeux 8 bits, avec des sprites carrés, pour savoir si deux blocs sont au même endroit, on n’utilise pas Euclide, mais on vérifie simplement si le pointeur est plus bas que le haut de la pipistrelle, et plus bas que le haut, plus à gauche de sa droite, plus à droite que sa gauche.

Vous pouvez modifier le code pour rendre hommage à la Game Boy et son processeur Sharp z80 cadencé à 4,194304 MHz, et ses 8ko de RAM. Sur une machine récente, même un Raspberry Pi, personne ne verra la différence.

La suite

Prenez en main le code, pour rajouter des comportements qui vous semblent indispensables.

Un peu de random sur sa position pour rendre le vol un peu plus erratique ?

Un sprite de plus pour avoir une position statique, la tête en bas, pour qu’elle puisse se reposer ?

Un son de “flap flap” joué quand la bête vole ?

blogroll

social