Les classes avec LÖVE2D

Les classes sont comme les plans d’une maison. Vous pouvez construire plusieurs maisons à partir d’un même plan. De la même manière, nous pouvons créer plusieurs objets à partir d’une classe.

Représentation visuelle d’une classe
Représentation visuelle d’une classe

Pour utiliser les classes, nous allons utiliser la bibliothèque classic.

Cliquez sur classic.lua puis sur Raw et copiez le code

Allez dans votre éditeur de texte, créez un nouveau fichier classic.lua et collez-y le code.

Maintenant, nous devons charger cette bibliothèque :

1
2
3
function love.load()
  Object = require("classic")
end

Nous sommes prêts à créer des classes. Créez un nouveau fichier rectangle.lua et mettez-y le code suivant :

1
2
3
4
5
6
7
8
--! file: rectangle.lua
-- Notez l'utilisation de : au lieu de .

Rectangle = Object:extend()

function Rectangle:new()
  self.test = math.random(1, 1000)
end

Tout sera expliqué ultérieurement. Mettez ce code dans votre main.lua :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--! file: main.lua

function love.load()
  Object = require("classic")
  -- N'oublions pas de charger le fichier
  require("rectangle")

  r1 = Rectangle()
  r2 = Rectangle()
  print(r1.test, r2.test)
end

Quand vous lancez le jeu, vous devriez voir 2 nombres aléatoires s’afficher.

Examinons ce code étape par étape :

  1. Nous créons une nouvelle classe avec Rectangle = Object:extend(). Rectangle devient une classe. Ce sera notre plan/modèle. Contrairement aux propriétés, les classes sont généralement nommées avec une majuscule initiale.
  2. Dans main.lua, nous écrivons r1 = Rectangle(). Même si Rectangle est une table, nous pouvons l’appeler comme si c’était une fonction. Son fonctionnement interne sera expliqué dans un autre chapitre, mais en appelant Rectangle(), cela crée une nouvelle instance. Cela signifie que notre plan est utilisé pour créer un nouvel objet avec toutes ses propriétés. Chaque nouvelle instance est unique.
  3. Pour prouver que r1 est unique, nous avons créé une autre instance appelée r2. Toutes les deux ont la propriété test mais avec des valeurs différentes.
  4. Quand nous appelons Rectangle(), cela exécute Rectangle:new(). C’est ce que l’on appelle un constructeur.
  5. Le paramètre self représente l’instance que nous sommes en train de créer. Si nous avions écrit Rectangle.test = math.random(1, 1000), nous aurions donné la propriété au plan lui-même et non à une instance créée à partir de ce plan.

Faisons quelques modifications à notre classe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
--! file: rectangle.lua

function Rectangle:new()
  self.x = 100
  self.y = 100
  self.width = 200
  self.height = 150
  self.speed = 100
end

function Rectangle:update(dt)
  self.x = self.x + self.speed * dt
end

function Rectangle:draw()
  love.graphics.rectangle("line", self.x, self.y, self.width, self.height)
end

C’est similaire à l’objet rectangle en mouvement que nous avons créé au chapitre 9, sauf que cette fois-ci nous mettons le code du mouvement et du dessin directement dans l’objet. Maintenant, il suffit d’appeler les méthodes update et draw dans main.lua :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
--! file: main.lua

function love.load()
  Object = require("classic")
  require("rectangle")
  r1 = Rectangle()
  r2 = Rectangle()
end

function love.update(dt)
  r1.update(r1, dt)
end

function love.draw()
  r1.draw(r1)
end

Quand vous lancez le jeu, vous devriez voir un rectangle se déplacer !

En fait, nous avons créé une classe appelée Rectangle. Nous avons créé une instance de cette classe appelée r1. Maintenant r1 possède une fonction qui le met à jour (update) et une autre qui le dessine (draw). Nous appelons ces fonctions et comme premier argument, nous passons l’instance r1. C’est ce que devient le paramètre self dans les fonctions.

Cependant, il est un peu fastidieux de passer r1 à chaque appel de fonction. Heureusement, Lua offre un raccourci pour cela. Quand nous utilisons deux points (:), l’appel de fonction passera automatiquement l’objet à gauche des “:” comme premier argument :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--! file: main.lua

function love.update(dt)
  -- Lua convertit ceci en r1.update(r1, dt)
  r1:update(dt)
end

function love.draw()
  -- Lua convertit ceci en r1.draw(r1)
  r1:draw()
end

Et nous pouvons faire de même avec toutes les fonctions de la classe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
--! file: rectangle.lua

-- Lua convertit ceci en Rectangle.new(self)
function Rectangle:new()
  self.x = 100
  self.y = 100
  self.width = 200
  self.height = 150
  self.speed = 100
end

-- Lua convertit ceci en Rectangle.update(self, dt)
function Rectangle:update(dt)
  self.x = self.x + self.speed * dt
end

-- Lua convertit ceci en Rectangle.draw(self)
function Rectangle:draw()
  love.graphics.rectangle("line", self.x, self.y, self.width, self.height)
end

Ajoutons quelques paramètres à Rectangle:new() :

1
2
3
4
5
6
7
8
9
--! file: rectangle.lua

function Rectangle:new(x, y, width, height)
  self.x = x
  self.y = y
  self.width = width
  self.height = height
  self.speed = 100
end

Avec cela, nous pouvons donner à r1 et r2 leur propre position et leur propre taille :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
--! file: main.lua

function love.load()
  Object = require("classic")
  require("rectangle")
  r1 = Rectangle(100, 100, 200, 50)
  r2 = Rectangle(180, 300, 25, 140)
end

function love.update(dt)
  r1:update(dt)
  r2:update(dt)
end

function love.draw()
  r1:draw()
  r2:draw()
end

Maintenant, nous avons 2 rectangles en mouvement. C’est ce qui rend les classes si utiles. r1 et r2 suivent le même modèle, mais sont des objets uniques avec leurs propres propriétés.

Exemple de classe avec 2 rectangles en mouvement
Exemple de classe avec 2 rectangles en mouvement

Une autre fonctionnalité puissante des classes est l’héritage.

L’héritage

Avec l’héritage, nous pouvons étendre nos classes. En d’autres termes, nous faisons une copie de notre plan et nous y ajoutons des fonctionnalités sans modifier l’original.

Illustration de l’héritage
Illustration de l’héritage

Imaginons un jeu avec différents types de monstres. Chaque monstre a sa propre attaque et se déplace différemment, mais tous peuvent subir des dommages et mourir. Les fonctionnalités communes devraient être placées dans une superclasse (ou classe de base). Cette superclasse fournit les fonctionnalités que tous les monstres partagent. Ensuite, la classe spécifique à chaque monstre peut étendre cette superclasse et y ajouter ses propres caractéristiques.

Créons une nouvelle forme mobile, le cercle. Qu’auront en commun notre rectangle et notre cercle ?
Eh bien, ils se déplaceront tous les deux. Créons donc une superclasse pour nos deux formes.

Créez un nouveau fichier appelé shape.lua et mettez-y le code suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
--! file: shape.lua
Shape = Object:extend()

function Shape:new(x, y)
  self.x = x
  self.y = y
  self.speed = 100
end

function Shape:update(dt)
  self.x = self.x + self.speed * dt
end

Notre superclasse Shape gère maintenant le mouvement. Je dois préciser que “superclasse” est juste un terme désignant la relation entre deux classes : “A est une superclasse de B”. Une superclasse est simplement une classe comme une autre, c’est juste la façon dont elle est utilisée qui diffère.

Maintenant que nous avons une superclasse qui gère les mouvements, nous pouvons faire de Rectangle une extension de Shape et supprimer sa méthode update :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
--! file: rectangle.lua

Rectangle = Shape:extend()

function Rectangle:new(x, y, width, height)
  Rectangle.super.new(self, x, y)
  self.width = width
  self.height = height
end

function Rectangle:draw()
  love.graphics.rectangle("line", self.x, self.y, self.width, self.height)
end

Avec Rectangle = Shape:extend(), nous avons créé Rectangle comme une extension de la superclasse Shape.

Shape possède sa propre fonction :new(). En créant Rectangle:new(), nous remplaçons la fonction d’origine. Cela signifie que lorsque nous appelons Rectangle(), cela n’exécutera pas Shape:new() mais plutôt Rectangle:new().

Cependant, Rectangle a accès à sa superclasse via la propriété super. Avec Rectangle.super, nous pouvons accéder aux fonctions de la superclasse, et nous l’utilisons ici pour appeler Shape:new().

Nous devons passer self comme premier argument explicitement, sans utiliser la syntaxe avec deux points (:), car nous n’appelons pas la fonction comme une méthode d’instance.

Maintenant, créons une classe Circle. Créez un nouveau fichier appelé circle.lua et mettez-y le code suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
--! file: circle.lua

Circle = Shape:extend()

function Circle:new(x, y, radius)
  Circle.super.new(self, x, y)
  -- Un cercle n'a pas de largeur ou hauteur. Il a un rayon.
  self.radius = radius
end

function Circle:draw()
  love.graphics.circle("line", self.x, self.y, self.radius)
end

Nous faisons de Circle une extension de Shape. Nous passons x et y à la fonction new() de Shape avec Circle.super.new(self, x, y).

Nous donnons à notre classe Circle sa propre fonction draw. Les cercles n’ont pas besoin de largeur ou de hauteur, mais d’un rayon.

Maintenant, dans main.lua, chargeons shape.lua et circle.lua, puis transformons r2 en un cercle :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
--! file: main.lua

function love.load()
  Object = require("classic")
  -- Ne pas oublier de charger les fichiers dans le bon ordre
  require("shape")
  require("rectangle")
  require("circle")

  r1 = Rectangle(100, 100, 200, 50)
  -- r2 devient un cercle au lieu d'un rectangle
  r2 = Circle(180, 300, 50)
end

Quand vous lancez le jeu, vous verrez un rectangle et un cercle se déplacer.

Démonstration de l’héritage avec un rectangle et un cercle
Démonstration de l’héritage avec un rectangle et un cercle

Récapitulons

Revoyons ce code une dernière fois :

  1. Nous chargeons la bibliothèque classic avec require("classic"). Cette bibliothèque nous retourne une table que nous stockons dans la variable Object. Elle est essentielle pour simuler des classes en Lua, qui n’a pas de notion native de classe.

  2. Ensuite, nous chargeons shape.lua. Dans ce fichier, nous créons une classe Shape qui servira de superclasse pour Rectangle et Circle. Les éléments communs (position x et y, mouvement horizontal) sont définis dans cette classe.

  3. Nous créons ensuite la classe Rectangle comme extension de Shape. Dans son constructeur (:new()), nous appelons d’abord le constructeur de la superclasse avec Rectangle.super.new(self, x, y). Nous passons self comme premier argument pour que Shape modifie notre instance et non la classe elle-même. Nous ajoutons ensuite les propriétés spécifiques au rectangle (width et height) et sa méthode draw.

  4. De façon similaire, nous créons la classe Circle, avec un rayon au lieu de largeur et hauteur, et sa propre méthode de dessin adaptée.

  5. Dans main.lua, après avoir chargé toutes les classes, nous créons des instances : r1 comme Rectangle et r2 comme Circle. Chacune possède ses propres propriétés et se comporte selon sa classe, tout en héritant du comportement de déplacement de Shape.

Difficultés ?

Ce chapitre contient beaucoup d’informations nouvelles, et il est normal que vous puissiez avoir du mal à tout comprendre immédiatement.

Mon conseil : continuez à suivre le tutoriel. Si vous êtes nouveau en programmation, il vous faudra du temps pour assimiler ces concepts. Je continuerai à faire référence à ces notions dans les chapitres suivants, ce qui vous permettra de les comprendre progressivement.

Résumé

Les classes sont comme des plans à partir desquels nous pouvons créer plusieurs objets. Pour simuler des classes en Lua, nous utilisons la bibliothèque classic.

Une classe se crée avec ClassName = Object:extend(). Une instance de classe se crée avec instanceName = ClassName(), ce qui appelle automatiquement la méthode ClassName:new() (le constructeur).

Chaque méthode de classe devrait commencer par le paramètre self. Lorsque vous appelez une méthode d’instance, vous pouvez utiliser la syntaxe avec deux points (:) pour que Lua passe automatiquement l’instance comme premier argument : instanceName:methodName(args) au lieu de instanceName.methodName(instanceName, args).

L’héritage permet d’étendre une classe avec ChildClass = ParentClass:extend(). La classe enfant hérite de toutes les méthodes et propriétés de la classe parent, et peut les remplacer ou en ajouter de nouvelles. Pour appeler une méthode de la classe parent depuis la classe enfant, on utilise ChildClass.super.methodName(self, args).