Cet article a été initialement écrit par EgoMoose sur Scripting Helpers. Le tutoriel original peut être trouvé ici.
L'une des demandes les plus courantes que je reçois pour les sujets d'articles de blog concerne un système de placement de décoration qui permet d'économiser. Je me suis éloigné de ce sujet dans le passé car il n'y a pas de façon singulière «correcte» de le faire. Cela étant dit, je pense que c'est un bon exercice pour tous les développeurs de jeux et qu'il jouera potentiellement un rôle dans un futur article de blog que je prévois d'écrire.
En plus de changer les choses, je prendrai également le temps d'expliquer et d'utiliser la programmation orientée objet (POO) car ce n'est pas seulement mon propre style préféré, mais aussi quelque chose que Roblox utilise pour ses scripts de base.
Bon, allons-y !
Programmation orientée objet
La POO consiste à écrire des classes, ce qui dans ce contexte est un synonyme très sophistiqué du mot « plan directeur ». Nous pouvons ensuite utiliser ces plans pour créer des objets qui ont certaines propriétés et méthodes.
Si certains de ces mots vous semblent familiers, eh bien, c'est parce qu'ils le sont. Les objets avec lesquels vous interagissez tels que Les pièces, Humanoïdes, Les Motor6D, et ainsi de suite, sont de par leur conception le paradigme de la POO. Ces objets ont des propriétés et des méthodes qui, lorsqu'ils sont combinés, définissent comment ils interagissent avec notre jeu.
Les propriétés sont utilisées pour définir les « caractéristiques » d'un objet. Par exemple, une pièce a une propriété appelée Taille qui, comme son nom l'indique, définit la taille ou la taille de l'objet. A leur tour, ces propriétés jouent souvent un rôle dans le comportement et les actions associées audit objet qui est défini par des méthodes. Par exemple, la méthode :GetMasse() renvoie la masse de la pièce qui varie entre autres avec la taille. Ainsi, nous pouvons voir ici un exemple de lien clair entre méthodes et propriétés.
Maintenant que nous avons une partie de la terminologie et un exemple, j'aimerais discuter davantage de la distinction entre les classes et les objets. Une classe définit les propriétés et les méthodes d'un objet. Par exemple, nous savons que toutes les pièces vont avoir une propriété de position, nous la définissons donc dans notre classe. La valeur réelle de la propriété de position variera entre les objets individuels, mais le concept global d'une pièce que nous connaissons aura une propriété appelée Position avec un Vector3 comme sa valeur. Dans un sens similaire, lors de l'écriture des méthodes de notre classe, nous ne connaissons peut-être pas les valeurs littérales de chaque propriété, mais puisque nous savons que ces valeurs existeront, nous pouvons les traiter presque comme des paramètres de fonction.
La différence entre la POO et une approche plus fonctionnelle pour la même tâche est visible dans l'exemple de code ci-dessous.
local part = Instance.new("Part") part.Size = Vector3.new(1, 2, 10) part.Material = Enum.Material.Wood print(part:GetMass()) -- 6.9999998807907 -- vs: local fonction getMass(taille, matériau) -- masse = volume * densité volume local = taille.x * taille.y * taille.z densité locale = PhysicalProperties.new(material).Density return volume * densité end print(getMass(part. Taille, pièce.Matériel)) -- 6.9999998807907
Ignorant le fait que la méthode est intégrée et n'avait donc pas besoin d'être définie, la seule différence était que dans l'approche fonctionnelle, nous devions insérer manuellement les propriétés de la pièce en tant qu'arguments. Dans le cas de la méthode, nous n'avons pas eu à insérer d'arguments car Lua savait que nous appelions une méthode sur un objet spécifique et que nous pouvions donc récupérer les propriétés nécessaires directement à partir de celui-ci. Une méthode est simplement le nom que nous donnons aux fonctions qui sont appliquées à un objet spécifique. Voici un exemple de ce à quoi cela pourrait ressembler :
function Part:GetMass() volume local = self.Size.x * self.Size.y * self.Size.z densité locale = PhysicalProperties.new(self.Material).Density return volume * Density end
Vous remarquerez peut-être que le code ci-dessus fait référence à quelque chose appelé soi et naturellement, cela semble sortir de nulle part. En Lua, lorsque vous appelez une méthode, le premier argument transmis est TOUJOURS l'objet sur lequel la méthode a été appelée. Lors de la définition d'une méthode avec la fonction de forme syntaxique Class:MyMethod(param1, param2, …), le paramètre qui représentera l'objet reçoit de force le nom self et ne doit pas être défini entre parenthèses comme tout autre paramètre supplémentaire. Donc, si j'ai exécuté somePart:GetMass() alors l'argument qui remplacerait self serait somePart.
En remarque, soit en raison de préférences personnelles, soit d'une familiarité avec d'autres langues qui utilisent un autre mot-clé autre que soi tel que précise, certains programmeurs peuvent se demander s'il existe un moyen d'utiliser un nom de paramètre différent. C'est possible, et le code équivalent serait le suivant :
-- l'argument self est toujours passé, mais son nom de paramètre n'est plus masqué et peut être modifié fonction Part.GetMass(self) volume local = self.Size.x * self.Size.y * self.Size.z densité locale = PhysicalProperties.new(self.Material).Density return volume * Density end -- toujours appelé comme une méthode normale print(somePart:GetMass())
Ce serait ma recommandation personnelle cependant que vous ne le fassiez pas car cela peut être déroutant pour les autres du point de vue de la lisibilité.
D'accord, alors comment écrivons-nous une classe ? La dernière pièce du puzzle est ce qu'on appelle un constructeur. Un constructeur crée un objet à partir de la classe et nous le renvoie avec un ensemble de propriétés remplies. Un constructeur très commun que je pense (?) Toutes les classes intégrées ont est .Nouveau() mais d'autres exemples pourraient être Vector3.FromNormalId or CFrame.Angles. Une classe peut avoir plusieurs constructeurs et ils peuvent être nommés à peu près n'importe quoi. Parfois, lorsque nous écrivons ces constructeurs, ils ont des paramètres qui nous aident à remplir les propriétés et d'autres fois non. Cela dépend entièrement de vous en tant que programmeur et dépend de ce que la classe veut faire.
Regardons comment le personnel de Roblox pourrait écrire un constructeur en Lua et nous allons décomposer les parties à partir de là. Voici un exemple de la façon dont on peut copier le Vector3 constructeur de classe.
local Vector3 = {} Vector3.__index = Vector3 -- fonction constructeur Vector3.new(x, y, z) local self = setmetatable({}, Vector3) self.x = x ou 0 self.y = y ou 0 self. z = z ou 0 retour self fin
Pour certains d'entre vous, cela peut déjà avoir un sens parfait et pour d'autres non. La principale différence entre ceux qui comprennent et ceux qui ne comprennent pas devrait être la familiarité avec les métatables. C'est un gros sujet en soi, mais heureusement, nous n'avons vraiment besoin de comprendre qu'un aspect de la méta-méthode __index pour comprendre ce code.
La meilleure explication "abêtie" que j'ai entendue des méta-tables est "les événements, mais pour les tables" et cela s'applique particulièrement à la méta-méthode __index. La métaméthode __index est « déclenchée » lorsqu'une clé inexistante dans une table est indexée, ce qui signifie qu'elle est lue et non écrite.
local t = { chats = 10; chiens = 32 ; } chats locaux = t.cats -- non tiré b/c la valeur existe pour la clé local dogs = t.dogs -- non tiré b/c la valeur existe pour la clé t.turtles = 60 -- non tiré b/c nous écrivons print(t.hamsters) -- la valeur b/c déclenchée n'existe pas pour la clé
Désormais, les méta-méthodes "déclenchent" généralement une fonction et la méta-méthode __index peut également fonctionner de cette manière. Cependant, si au lieu de définir une fonction sur la métaméthode __index vous définissez une autre table, alors lorsque la métaméthode __index est « déclenchée », elle traite le processus comme tel :
- La table a été indexée avec la clé => La clé correspond-elle à une valeur nulle dans la table ?
- Oui => La clé correspond-elle à une valeur non nulle dans la table de la métaméthode __index ?
- Oui => Renvoyer cette valeur
- Non => Retour nul
- Oui => La clé correspond-elle à une valeur non nulle dans la table de la métaméthode __index ?
Cela nous est très utile car cela nous permet de définir des valeurs par défaut pour les clés et de ne pas avoir à constamment redéfinir et nous répéter lors de la copie. Dans le cas du code ci-dessus, nous l'utilisons de telle sorte que self, la table que nous renvoyons de notre constructeur, aura accès à tous les constructeurs et méthodes que nous attachons à la table Vector3.
Disons que nous ajoutons la méthode suivante au code ci-dessus, puis créons un objet et exécutons la méthode dessus :
fonction Vector3:Magnitude() local x, y, z = self.x, self.y, self.z return math.sqrt(x*x + y*y + z*z) end local v = Vector3.new(1 , 2, 3) print(v:Magnitude())
Le processus est traité comme tel :
- v a été indexé avec la clé Magnitude => La clé correspond-elle à une valeur nulle dans v ?
- Oui => La clé correspond-elle à une valeur non nulle dans Vector3 ?
- Oui => Renvoie cette valeur (la méthode magnitude)
- Oui => La clé correspond-elle à une valeur non nulle dans Vector3 ?
Ainsi, la méthode :Magnitude() est appelée sur v qui a des valeurs réelles pour les propriétés x, y et z et en tant que tel, nous obtenons le résultat correspondant.
Il y a beaucoup plus à dire sur la POO et ce que j'ai expliqué a à peine effleuré la surface. Certains autres langages vous obligent à utiliser la POO et disposent d'un ensemble de fonctionnalités beaucoup plus riche que Lua. Si vous souhaitez explorer davantage la POO en Lua, je vous recommande de lire le post suivant sur les devforums.
Cela étant dit, la question à laquelle je n'ai toujours pas répondu est « Pourquoi utiliser la POO ? ». Ma réponse : personnellement, je l'apprécie car cela m'oblige à organiser mon code de manière modulaire et réutilisable qui peut être combiné pour une variété de tâches complexes. Cela étant dit, il y a des points positifs et négatifs pour tout, alors utilisez ce qui fonctionne pour vous.