L’objet Bordure

Dans cet article nous allons décrire comment on s’émancipe du code chatGPT pour définir la bordure et quels différents types de bordure celà nous permet d’initialiser. Le point commun entre tous ses objets est qu’ils sont définis comme des StaticBody, ils ne peuvent donc pas se déplacer est il est possible d’initialiser leur fonctionnement et leur apparence directement dans le constructeur de l’objet. 

  1. Redéfinir l’objet Bordure

https://github.com/Thibautomaton/BoxBoundaries/tree/main

Dans cet exemple on redéfinit l’objet bordure pour le rendre plus compréhensible et non plus comme un morceau de code recraché par chatGPT, mais véritablement une partie du code que l’on maîtrise et à laquelle on peut faire faire ce que l’on veut. 

boundaries = []

bo1 = Boundary(0, HEIGHT-15, WIDTH/2, 15, world)

bo2 = Boundary(WIDTH/2, HEIGHT-100, WIDTH/2, 15, world)

boundaries.append(bo1)

boundaries.append(bo2)

En dessous de nos initialisations, on crée deux objets Boundary qui prennent en arguments d’abord les coordonnées x et y de où on veut les voir apparaître, et les dimensions de cet objet. Plus l’objet world dont on a vu qu’on en avait besoin pour créer le body de l’objet à l’intérieur du constructeur.

On remarque que dans notre code on a encore le fichier settings.py qui contient les fonctions de conversions entre l’environnement simulé pybox2d et les pixels qui s’affichent à l’écran. On retrouve : 

def coordPixelsToWorld(P):

    world_x = P[0]/PPM

    world_y = (HEIGHT-P[1])/PPM

    return world_x, world_y

def coordWorldToPixel(P):

    screen_x = P[0]*PPM

    screen_y = -P[1]*PPM + HEIGHT

    return screen_x, screen_y

def scalarWorldToPixel(val):

    return val*PPM

def scalarPixelToWorld(val):

    return val/PPM

Dans l’initialiseur de Boundary, on crée un body avec sa position qui est celle qui lui a été passée en argument. 

self.body= world.CreateStaticBody(

            position=(coordPixelsToWorld((self.x, self.y)))

        )

On crée ensuite une shape à noter que cette fonction prend la moitié de la hauteur et largeur définis en coordonnées du monde pybox2d. 

        box2dW = scalarPixelToWorld(self.w/2)

        box2dH = scalarPixelToWorld(self.h/2)

        ps = polygonShape(

            box=(box2dW, box2dH)

        )

On appelle create fixture en passant ps en argument :

        self.body.CreateFixture(

            shape = ps,

            restitution=0.2

        )

 En clair on définit notre bordure comme une boîte que l’on instancie à la différence près qu’on crée un staticBody plutôt qu’un DynamicBody.

De même dans la fonction display on reprend exactement le même code :

        color = (0, 0, 255)

        rect_surface = pygame.Surface((self.w, self.h), pygame.SRCALPHA)

        pygame.draw.rect(rect_surface, color, (0, 0, self.w, self.h))

        angle_degree = -math.degrees(self.body.transform.angle)

        rotated_surface = pygame.transform.rotate(rect_surface, angle_degree)

        rotated_rect = rotated_surface.get_rect(center= coordWorldToPixel(self.body.transform.position))

        self.display_surface.blit(rotated_surface, rotated_rect)

On aurait pu donner l’angle est la localisation en coordonnées absolues en fonction de ce qui a été passé en paramètre au constructeur, vu que ces données ne changent pas , mais self.body.transform nous donne aussi ces valeurs qui ont été définies dans CreateBody et CreateFixture.

La classe Box est donc très similaire à notre bordure. On retrouve les fonctions CreateDynamicBody et CreateFixture dans le constructeur (on a un peu plus d’arguments dans CreateFixture que simplement la restitution pour un StaticBody). Et display est une copie conforme de la bordure, cette fois self.body.transform traque les évolutions de l’objet. 

On a cependant une fonction killBody() et cela nous permet de détruire un body, et donc de ne plus traquer la physique lorsqu’on décide d’appeler cette fonction pour un objet. 

Effectivement dans main on voit tout de suite l’utilité de cette fonction qui est appelée lorsque l’objet n’apparaît plus dans la fenêtre : 

    for p in boxes:

        nbActivBoxes+=1

        p.display()

        if not 0<p.body.transform.position[0]<WIDTH or not 0<p.body.transform.position[1]<HEIGHT:

            boxesToDestroy.append(p)

            boxes.remove(p)

Il faut bien sûr traquer les objets boxes et bordures dans des listes, ce qui permet d’appeler la fonction display  pour chacun de ces objets. Lorsque killBody est appelé, on peut retirer cet objet de la liste.

  1. Différentes Bordures

https://github.com/Thibautomaton/CurvyWaveNoiseBoundary

Ensuite on va utiliser l’objet chainShape qui définit une forme pour passer en argument à CreateFixture. Cela va nous permettre de créer une forme customisée pour l’objet bordure, un peu comme on pouvait définir une forme avec beginShape et endShape dans Processing, l’utilisation est très similaire à la création de vertexes.

  1. CurvyBoundary

boundaries = []

curvyboundary = CurvyBoundary(world)

boundaries.append(curvyboundary)

On définit l’objet CurvyBoundary un peu comme on l’avait fait pour l’objet Boundary, mais cette fois on ne lui passe pas de paramètres, tout sera défini dans l’initialisateur de CurvyBoundary. 

On crée surface, une liste qui va récupérer les coordonnées des vertex de la forme qu’on définit. 

       self.surface = []

        self.surface.append((0, HEIGHT/2))

        self.surface.append((WIDTH/2, 2*HEIGHT/3))

        self.surface.append((WIDTH, HEIGHT/2))

        self.surface.append((WIDTH, HEIGHT))

        self.surface.append((0, HEIGHT))

Dans CreateStaticBody on donne la position (0, 0), c’est important car comme display utilise l’objet surface pour afficher la bordure, elle ne serait plus aligné avec ce qui est affiché à l’écran. 

On crée vertices qui va récupérer les coordonnées mais dans les dimensions de Box2D. 

        vertices = []

        for surface in self.surface:

            vertices.append(pixelsToWorld(surface))

C’est cette liste qui est donné en argument à l’objet chainShape, celà permet de définir shape  dans CreateFixture. 

        chain = chainShape(vertices=vertices)

        self.bd.CreateFixture(

            shape = chain,

            restitution=0.5

        )

Dans la fonction display on utilise la fonction draw.polygon de pygame en passant en argument la liste des vertex. 

        pygame.draw.polygon(self.display_surface, (0, 255, 0), self.surface)

  1. WaveBoundary 

Pour les autres bordures, tout fonctionne de la même façon. Seule la liste surface sera différente, on lui donnera les vertexes qui correspondent à notre bordure. 

La création de l’objet WaveBoundary est très similaire : 

boundaries = []

waveboundary = WaveBoundary(world)

boundaries.append(waveboundary)

Dans Waveboundary.py voici comment on initialise surface : 

        self.amplitude = 250

        self.angle = 0

        self.angleVel=0.2

        self.surfaces = []

        for x in range(0, WIDTH, 24):

            y = self.amplitude*math.sin(self.angle) + HEIGHT/2

            self.surfaces.append((x, y))

            self.angle += self.angleVel

        self.surfaces.append((WIDTH, HEIGHT))

        self.surfaces.append((0, HEIGHT))

On utilise une sinusoïde pour avoir un effet de vague. On ajoute deux vertex pour fermer la forme, comme on l’avait fait avec endShape(CLOSE) dans processing.

  1. NoiseBoundary

Comme on a pas de fonction noise dans pygame, on importe un objet PerlinNoise, l’avantage est que l’on peut lui passer des paramètres comme l’amplitude, la fréquence, les octaves et l’interpolation.

On crée aussi une liste surface avec un x qui a un pas de 20. 

        self.noise = PerlinNoise(self.seed ,150, 10, 3, interp=Interp.LINEAR)

        for x in range(0, WIDTH, 20):

            y = self.noise.get(x) + HEIGHT/2

            self.surfaces.append((x, y))

        self.surfaces.append((WIDTH, HEIGHT))

        self.surfaces.append((0, HEIGHT))

        self.body = world.CreateStaticBody(

            position = (0,0)

        )

        vertices = []

        for surface in self.surfaces:

            vertices.append(pixelsToWorld(surface))

        chain = chainShape(vertices = vertices)

        print(chain)

        self.body.CreateFixture(

            shape = chain,

            restitution=0.5

        )

CONCLUSION

On s’est occupé dans cet article des objets fixes qui ont souvent le même schéma de fonctionnement et qu’il est assez facile de relier aux connaissances qu’on avait grâce à processing. Dans les prochains articles, nous verrons comment les objets mobiles peuvent avoir des interactions entre eux où l’arbitraire et la rigidité ne sont que rêveries.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Retour en haut