Comment créer ses propres jeux ?

Maintenant que vous avez vu des exemples de ce qu'on peut réaliser avec notre nouvelle console, nous allons détailler un exemple concret qui est la balle rebondissante présentée à la page précédente. L'exemple est basique mais illustre les principaux concepts à comprendre.

Le fichier principal

Sur circuitpython, le programme principal se nomme main.py. Ce fichier est exécuté au démarrage de la carte. C'est donc ce fichier que nous allons créer. Pour développer vos jeux, vous pouvez bien sûr utiliser mu-editor car circuitPython est pris en charge par mu.

Le programme le plus basique contient ces lignes

1
import ugame
2
import stage

ce programme ne fait rien d'autre que d'importer les deux librairies dont nous aurons besoin. Rien ne s'affiche donc à l'écran car il nous faut préparer notre scène. Ces deux librairies n'ont pas besoin d'être copiées sur la carte car elles sont intégrées à la version personnalisée de circuitpython que nous avons installé sur la carte.

  • ugame prend en charge le matériel. C'est dans cette librairie que sont initialisés l'écran, les boutons, le son.

  • stage est une librairie de jeu basique mais efficace permettant de dessiner des tuiles et des sprites à 'écran.

Les banques

Tous les graphiques sont organisés dans ce qu'on appelle des banques. Ce sont des ensembles d'images de 16x16 pixels avec une palette de 16 couleurs. Pour notre exemple de balle rebondissante, nous aurons besoin de seulement 5 images : la trame de fond et 4 images simulant la balle qui roule. Notre banque ressemble à ceci. Copiez le fichier ball.bmp sur la carte (il se trouve dans l'archive de l'exemple précédent [zip]).

Attention

Le format des banques doit être très précis car le système est très basique : les fichiers doivent avoir une taille de 16 pixels de large et 256 pixels de haut. Le format d'image est un BMP à 16 couleurs. La couleur magenta que vous apercevez dans l'image ci-dessus correspond à la couleur de transparence. Elle se compose de 100% de rouge, 100% de bleu et 0% de vert. La transparence nous permet d'avoir une balle ronde et non carrée ! !

Complétons notre code pour charger a banque en mémoire :

1
import ugame
2
import stage
3
4
bank = stage.Bank.from_bmp16("ball.bmp")

Il ne s'affiche toujours rien à l'écran mais la banque est prête à l'emploi.

Les grilles

Les grilles sont une manière d'afficher un motif répétitif sur l'écran. Chaque motif est une tuile carrée de 16 pixels par 16 pixels. Le code suivant définit l'arrière plan de notre scène. La grille remplit l'espace disponible à l'écran avec la première tuile de la banque. Les paramètres 10 et 8 indiquent que nous voulons 10 tuiles horizontalement (\(10\times 8=160\)) et 8 tuiles verticalement (\(8 times 16 = 128\)). Notre écran possède une résolution de \(160\times 128\), le compte est bon.

1
import ugame
2
import stage
3
4
bank = stage.Bank.from_bmp16("ball.bmp")
5
background = stage.Grid(bank, 10, 8)

Ce n'est pas encore complet. Rien ne s'affiche encore, mais patience, cela vient !

La scène (stage)

Pour faire apparaître quelque chose à l'écran, on va avoir besoin d'une scène (objet stage) qui représente l'écran entier de notre jeu, avec tout ce qui doit être affiché. Les objets sont organisés en couches - démarrant de celle la plus proche de vous en allant au plus lointain. L'ordre des couches est important car elles se superposent en masquant éventuellement les objets des couches inférieures.

Pour le moment nous n'avons que notre arrière plan, donc nous n'avons besoin que d'une seule couche.

1
import ugame
2
import stage
3
bank = stage.Bank.from_bmp16("ball.bmp")
4
background = stage.Grid(bank, 10, 8)
5
6
7
game = stage.Stage(ugame.display, 12)
8
game.layers = [background]
9
game.render_block()
10
11
while True:
12
    pass

Tadaaaa ! C'est pas encore le jeu du siècle mais il apparaît quelque chose enfin à l'écran. On utilise une boucle vide pour éviter que notre scène si durement acquise ne s'envole...

Les trois lignes commençant par game sont celles qui décrivent notre scène :

  • la première ligne crée la scène dans l'objet nommé game

  • la seconde définit les couches à afficher sous forme d'une liste (du plus près au plus lointain)

  • la dernière ligne fait le rendu de la scène donc l'affiche concrètement à l'écran.

game.render_block() doit être appelé au moins une fois dans le programme. Sans paramètre sspécifique, cette commande va redessiner l'écran tout entier. On peut bien sûr ne rafraichir que certaines zones de l'écran ce qui est plus rapide, mais laissons cela pour le moment.

Reste un mystère à éclaircir : d'où vient ce nombre magique 12 dans la création de notre scène game ? cela va servir au moment de l'animation. Cela représente le nombre d'images par secondes. Notre écran sera rafraîchi 12 fois par secondes. Cela peut paraître peu quand on est habitué aux jeux sur PC mais on travaille ici avec des moyens matériels très limités. 12 à 24 images secondes est amplement suffisant !

Les sprites (objets animés)

Mettons un peu d'animation à notre jeu ! nous allons introduire les sprites, ici la balle. Pour lui donner un effet d'animation, nous allons associer à notre balle une image différente à chaque fois que l'image sera rafraîchie. Tout de suite un exemple pour comprendre de quoi on parle :

1
import ugame
2
import stage
3
bank = stage.Bank.from_bmp16("ball.bmp")
4
background = stage.Grid(bank, 10, 8)
5
6
ball = stage.Sprite(bank, 1, 8, 8)
7
8
game = stage.Stage(ugame.display, 12)
9
game.layers = [ball, background]
10
game.render_block()
11
12
while True:
13
    ball.set_frame(ball.frame % 4 + 1)
14
    game.render_sprites([ball])
15
    game.tick()

Par rapport à l'exemple précédent, nous avons fait quelques changements :

tout d'abord la création de l'objet ball :

ball = stage.Sprite(bank, 1, 8, 8)

Le 1 représente le second objet de la banque (numérotés à partir de 0). Les 8 qui suivent sont les coordonnées x et y où la balle va s'afficher.

Ensuite, on remarque l'ajout de la balle à la liste des couches (layers). On fera attention de mettre a balle en premier car celle-ci est au dessus de l'image de fond.

Pour terminer la boucle principale :

  • ball.set_frame(ball.frame % 4 + 1) : permet l'alternance des images de la banque  (1, 2, 3, 4, 1, 2 etc... )

  • game.render_sprites([ball]) : permet de redessiner la balle car l'image de la banque a changé. On ne redessine pas tout l'écran ici, c'est bien plus efficace.

  • game.tick() pour finir permet de ralentir l'exécution de la boucle principale de manière à s'assurer que celle-ci ne s'exécute que 12 fois par secondes. Si vous commentez cette ligne, vous verrez la balle tournoyer à une vitesse folle !

Mettre le sprite en mouvement

Il s'agit maintenant de bouger notre balle. Il existe pour cela la méthode move() de l'objet sprite qui rend les choses très simples : ball.move(x,y) dessinera la balle aux coordonnées (x,y).

Il nous suffit à présent de modifier dans la boucle principale les coordonnées de notre sprite (ball.x et ball.y) d'une petite grandeur (vx et vy) et de gérer les rebonds sur les bords. Voyons cela en pratique :

MéthodeMise en pratique.

1
import ugame
2
import stage
3
bank = stage.Bank.from_bmp16("ball.bmp")
4
background = stage.Grid(bank, 10, 8)
5
6
ball = stage.Sprite(bank, 1,64, 64)
7
8
game = stage.Stage(ugame.display, 12)
9
game.layers = [ball, background]
10
game.render_block()
11
12
vx, vy = 5, 3
13
while True:
14
    ball.set_frame(ball.frame % 4 + 1)
15
    ball.move(ball.x+vx, ball.y+vy)
16
    if not (0<ball.x<144):
17
        vx = -vx
18
    if not (1<ball.y<112):
19
        vy = -vy
20
    game.render_sprites([ball])
21
    game.tick()
22

Si vous avez déjà programmé des balles rebondissantes sur ordinateur, ce code doit vous paraître familier. L'idée est d'inverser la vitesse lorsque l'on touche un bord. Si c'est un bord vertical, on change le signe de vx, si c'est un bord horizontal, on change le signe de vy. Chaque sprite stocke ses coordonnées dans ses attributs x et y, ce qui s'avère utile pour gérer notre déplacement.

Et avec plusieurs balles ? 

Si on veut plusieurs balles, il est possible de reprendre l'exemple précédent avec, mettons 3 variables ball1, ball2 et ball3, et en copiant-collant le code précédent. Cela peut vite devenir très lourd et apporte beaucoup de code redondant. Il est alors grand temps de penser objet ! Nous allons définir une classe Balle (les noms des classes commencent par convention par des majuscules) qui décrit le comportement d'une balle. Une balle étant un sprite, celle classe Balle héritera de la classe Sprite. Nous mettrons alors dans cette classe le code qui permet de gérer le rebond d'une balle.

Si les concepts d'objets en python vous sont totalement étrangers, je vous renvoie à mes autres cours Python, en particulier sur processing qui présente des concepts un peu similaires.

Voici au final ce que cela donne :

MéthodeVersion objet, avec 3 balles

1
import ugame
2
import stage
3
from random import randint
4
5
class Balle(stage.Sprite):
6
    def __init__(self, x, y):
7
        super().__init__(bank, 1, x, y)
8
        self.vx = randint(1,5)
9
        self.vy =randint(1,5)
10
11
    def update(self):
12
        super().update()
13
        self.set_frame(self.frame % 4 + 1)
14
        self.move(self.x + self.vx, self.y + self.vy)
15
        if not 0 < self.x < 144:
16
            self.vx = -self.vx
17
        if not 0 < self.y < 112:
18
            self.vy = -self.vy
19
20
NB_BALLES = 3
21
22
bank = stage.Bank.from_bmp16("ball.bmp")
23
background = stage.Grid(bank,10,8)
24
25
balles = []
26
for i in range(NB_BALLES):
27
    balles.append(Balle(randint(1,143),randint(1,111)))
28
    
29
game = stage.Stage(ugame.display, 24)
30
game.layers = balles + [ background]
31
game.render_block()
32
33
while True:
34
    for b in balles:
35
        b.update()
36
    game.render_sprites(balles)
37
    game.tick()
38

Observons d'abord la méthode __init__() : Elle réalise 2 actions :

  • appeler la méthode __init__() de sa super classe à savoir Sprite afin d'initialiser le sprite

  • initialiser les propriétés vx et vy qui détermineront la vitesse de la balle

L'initialisation de la balle nécessite 2 arguments correspondant à la position de la balle. On a pas besoin de la banque, celle-ci est de toute façon communiquée à la méthode __init__() sur Sprite.

La méthode update() gère le mouvement de la balle. On commence par appeler update() sur la super classe puis on gère le rebond exactement de la même manière que précédemment avec une seule balle.

Dans la partie programme, on crée autant d'instances de Balle que l'on souhaite. Cela se fait dans une boucle. Il suffira de modifier simplement la constante NB_BALLES pour ajouter autant de balle qu'on souhaite. Essayez avec 10 balles !

Le fait d'avoir une liste balles qui contienne toutes les balles est trop de la balle ! on peut l'utiliser pour déterminer nos layers : c'est juste la liste des balles plus le fond. On peut aussi l'utiliser pour la mise a jour et le rendu des sprites dans une simple boucle.

On voit sur cet exemple la puissance du concept d'objet car une fois la classe Balle définie, on le code du reste du jeu se simplifie à l’extrême. Imaginez la lourdeur du code pour cet exemple avec 10 balles sans recours au concept de classe !

Afficher un texte

Il est fréquent d'avoir à afficher un texte, ne serait-ce que pour afficher des scores, ou un message type "Game Over". Cela peut se faire avec stage avec l'objet Text que l'on rajoute à notre scène en tant que couche. Trois lignes de code seulement sont nécessaires pour afficher le nombre de balles sur notre écran :

texte = stage.Text(20, 1) # 20 colonnes, 1 ligne

texte.move(16, 60) # position x=16 et y=60

texte.text("Il y a "+ str(NB_BALLES) + " balles")

et on pense bien sûr à ajouter notre texte aux layers, par exemples entre les balles et le fond de manière à ce que les balles roulent sur le texte.

game.layers = balles + [ texte, background]

MéthodeLe code final de cet exemple

1
import ugame
2
import stage
3
from random import randint
4
5
class Balle(stage.Sprite):
6
    def __init__(self, x, y):
7
        super().__init__(bank, 1, x, y)
8
        self.vx = randint(1,5)
9
        self.vy =randint(1,5)
10
11
    def update(self):
12
        super().update()
13
        self.set_frame(self.frame % 4 + 1)
14
        self.move(self.x + self.vx, self.y + self.vy)
15
        if not 0 < self.x < 144:
16
            self.vx = -self.vx
17
        if not 0 < self.y < 112:
18
            self.vy = -self.vy
19
20
NB_BALLES = 10
21
22
bank = stage.Bank.from_bmp16("ball.bmp")
23
background = stage.Grid(bank,10,8)
24
25
balles = []
26
for i in range(NB_BALLES):
27
    balles.append(Balle(randint(1,143),randint(1,111)))
28
29
texte = stage.Text(20, 1)
30
texte.move(16, 60)
31
texte.text("Il y a " + str(NB_BALLES) + " balles")
32
33
game = stage.Stage(ugame.display, 24)
34
game.layers = balles + [ texte, background]
35
game.render_block()
36
37
while True:
38
    for b in balles:
39
        b.update()
40
    game.render_sprites(balles)
41
    game.tick()
42