Plante est une expérience de référence où les joueurs plantent et arrosent des graines, afin de pouvoir ensuite récolter et vendre les plantes résultantes.

Le projet se concentre sur des cas d'utilisation courants que vous pourriez rencontrer lors du développement d'une expérience sur Roblox. Lorsque cela est applicable, vous trouverez des notes sur les compromis, les choix de conception et la motivation derrière les différentes choix d'implémentation, afin que vous puissiez prendre la meilleure décision pour vos propres expériences.
Obtenez le fichier
- Accédez à la page de l'expérience Plante.
- Cliquez sur le bouton ⋯ et Modifier dans Studio.
Cas d'utilisation
Plante couvre les cas d'utilisation suivants :
- Persistance des données de session et des données des joueurs
- Gestion de la vue de l'interface utilisateur
- Réseau client-serveur
- Expérience utilisateur pour la première fois (FTUE)
- Achats de devises dures et douces
De plus, ce projet résout des ensembles de problèmes plus étroits qui sont applicables à de nombreuses expériences, notamment :
- Personnalisation d'une zone dans le lieu associée à un joueur
- Gestion de la vitesse de mouvement du personnage du joueur
- Création d'un objet qui suit les personnages
- Détection de la partie du monde dans laquelle un personnage se trouve
Notez qu'il existe plusieurs cas d'utilisation dans cette expérience qui sont trop petits, trop spécifiques ou ne montrent pas une solution à un défi de conception intéressant ; ceux-ci ne sont pas couverts.
Structure du projet
La première décision lors de la création d'une expérience consiste à décider comment structurer le projet, qui comprend principalement où placer des instances spécifiques dans le modèle de données et comment organiser et structurer les points d'entrée pour le code client et serveur.
Modèle de données
Le tableau suivant décrit quels services conteneurs dans le modèle de données contiennent les instances.
| Service | Types d'instances |
|---|---|
| Workspace | Contient des modèles statiques représentant le monde 3D, en particulier des parties du monde qui n'appartiennent à aucun joueur. Vous n'avez pas besoin de créer, modifier ou détruire ces instances dynamiquement à l'exécution, il est donc acceptable de les laisser ici. Il y a également un Folder vide, auquel les modèles de ferme des joueurs seront ajoutés à l'exécution. |
| Lighting | Effets atmosphériques et d'éclairage. |
| ReplicatedFirst | Contient le plus petit sous-ensemble possible d'instances nécessaires pour afficher l'écran de chargement et initialiser l'expérience. Plus il y a d'instances dans ReplicatedFirst, plus l'attente pour qu'elles se répliquent avant que le code dans ReplicatedFirst puisse s'exécuter sera longue.
|
| ReplicatedStorage | Agit comme un conteneur de stockage pour toutes les instances dont l'accès est requis à la fois par le client et le serveur.
|
| ServerScriptService | Contient un Script servant de point d'entrée pour tout le code côté serveur dans le projet. |
| ServerStorage | Agit comme un conteneur de stockage pour toutes les instances qui n'ont pas besoin d'être répliquées au client.
|
| SoundService | Contient les objets Sound utilisés pour les effets sonores dans l'expérience. Sous SoundService, ces objets Sound n'ont pas de position et ne sont pas simulés dans l'espace 3D. |
Points d'entrée
La plupart des projets organisent le code à l'intérieur de ModuleScripts réutilisables qui peuvent être importés dans l'ensemble du code. Les ModuleScripts sont réutilisables mais ils ne s'exécutent pas d'eux-mêmes ; ils doivent être importés par un Script ou un LocalScript. De nombreux projets Roblox auront un grand nombre d'objets Script et LocalScript, chacun se rapportant à un comportement ou à un système particulier dans l'expérience, créant plusieurs points d'entrée.
Pour le microjeu Plante, une approche différente est mise en œuvre via un seul LocalScript qui est le point d'entrée pour tout le code client, et un unique Script qui est le point d'entrée pour tout le code serveur. L'approche correcte pour votre projet dépend de vos besoins, mais un seul point d'entrée offre un meilleur contrôle sur l'ordre dans lequel les systèmes sont exécutés.
Les listes suivantes décrivent les compromis des deux approches :
- Un seul Script et un seul LocalScript couvrent respectivement le code serveur et client.
- Un meilleur contrôle sur l'ordre dans lequel différents systèmes sont démarrés, car tout le code est initialisé à partir d'un seul script.
- Peut passer des objets par référence entre les systèmes.
Architecture des systèmes de haut niveau
Les systèmes de niveau supérieur dans le projet sont détaillés ci-dessous. Certains de ces systèmes sont substantiellement plus complexes que d'autres et, dans de nombreux cas, leur fonctionnalité est abstraite à travers une hiérarchie d'autres classes.

Chacun de ces systèmes est un "singleton" en ce sens qu'il s'agit d'une classe non instanciable qui est plutôt initialisée par le script start client ou serveur pertinent. Vous pouvez en savoir plus sur le modèle singleton plus loin dans ce guide.
Serveur
Les systèmes suivants sont associés au serveur.
| Système | Description |
|---|---|
| Réseau |
|
| Serveur de données des joueurs |
|
| Marché |
|
| Gestionnaire de groupes de collisions |
|
| Gestionnaire de ferme serveur |
|
| Conteneur d'objets joueurs |
|
| Taguer les joueurs |
|
| Gestionnaire FTUE serveur |
|
| Spawneur de personnages |
|
Client
Les systèmes suivants sont associés au client.
| Système | Description |
|---|---|
| Réseau |
|
| Données des joueurs client |
|
| MarchéClient |
|
| Gestionnaire de marche et saut local |
|
| Gestionnaire de ferme client |
|
| Configuration de l'interface utilisateur |
|
| Gestionnaire FTUE client |
|
| Sprint de personnage |
|
Communication client-serveur
La plupart des expériences Roblox impliquent un élément de communication entre le client et le serveur. Cela peut inclure le client demandant au serveur d'effectuer une certaine action et le serveur répliquant des mises à jour au client.
Dans ce projet, la communication client-serveur est maintenue aussi générique que possible en limitant l'utilisation des objets RemoteEvent et RemoteFunction afin de réduire le nombre de règles spéciales à suivre. Ce projet utilise les méthodes suivantes, par ordre de préférence :
- Réplication via le système de données des joueurs.
- Réplication via attributs.
- Réplication via étiquettes.
- Messaging directement via le module Réseau.
Réplication via le système de données des joueurs
Le système de données des joueurs permet d'associer des données au joueur qui persistent entre les sessions de sauvegarde. Ce système fournit une réplication du client au serveur et un ensemble d'APIs qui peuvent être utilisées pour interroger des données et s'abonner à des changements, ce qui le rend idéal pour répliquer les changements d'état du joueur du serveur au client.
Par exemple, plutôt que d'envoyer un UpdateCoins RemoteEvent sur mesure pour dire au client combien de pièces il a, vous pouvez appeler ce qui suit et laisser le client s'y abonner via l'événement PlayerDataClient.updated.
PlayerDataServer.setValue(player, "coins", 5)
Bien sûr, cela n'est utile que pour la réplication du serveur au client et pour les valeurs que vous souhaitez persister entre les sessions, mais cela s'applique à un nombre surprenant de cas dans le projet, notamment :
- L'étape FTUE actuelle
- L'inventaire du joueur
- Le montant de pièces que le joueur a
- L'état de la ferme du joueur
Réplication via attributs
Dans les situations où le serveur doit réplica un valeur personnalisée au client qui est spécifique à une Instance donnée, vous pouvez utiliser attributs. Roblox réplique automatiquement les valeurs d'attributs, donc vous n'avez pas besoin de maintenir des chemins de code pour répliquer l'état associé à un objet. Un autre avantage est que cette réplication se produit en même temps que l'instance elle-même.
Ceci est particulièrement utile pour les instances créées à l'exécution, car les attributs définis sur une nouvelle instance avant qu'elle ne soit parentée au modèle de données se répliqueront de manière atomique avec l'instance elle-même. Cela contourne tout besoin d'écrire du code pour "attendre" que des données supplémentaires soient répliquées via un RemoteEvent ou StringValue.
Vous pouvez également lire directement les attributs du modèle de données, que ce soit depuis le client ou le serveur, avec la méthode GetAttribute() et vous abonner aux changements avec la méthode GetAttributeChangedSignal(). Dans le projet Plante, cette approche est utilisée pour, entre autres choses, répliquer l'état actuel des plantes aux clients.
Réplication via étiquettes
CollectionService vous permet d'appliquer une étiquette de chaîne à une Instance. Ceci est utile pour catégoriser les instances et répliquer cette catégorisation au client.
Par exemple, l'étiquette CanPlant est appliquée au serveur pour signifier au client qu'un pot donné est capable de recevoir une plante.
Messaging directement via le module réseau
Pour les situations où aucune des options précédentes ne s'applique, vous pouvez utiliser des appels réseau personnalisés via le module Réseau. C'est la seule option dans le projet qui permet la communication client-serveur et est donc la plus utile pour transmettre des requêtes du client et recevoir une réponse du serveur.
Plante utilise des appels réseau directs pour une variété de requêtes client, y compris :
- Arroser une plante
- Planter une graine
- Acheter un article
L'inconvénient de cette approche est que chaque message individuel nécessite une configuration sur mesure qui peut augmenter la complexité du projet, bien que cela ait été évité autant que possible, notamment pour la communication du serveur au client.
Classes et singletons
Les classes dans le projet Plante, comme les instances sur Roblox, peuvent être créées et détruites. Sa syntaxe de classe est inspirée de l'approche idiomatique de Lua pour la programmation orientée objet avec plusieurs modifications pour permettre la prise en charge de la vérification stricte des types.
Instanciation
De nombreuses classes dans le projet sont associées à une ou plusieurs Instances. Des objets d'une classe donnée sont créés à l'aide d'une méthode new(), conforme à comment les instances sont créées sur Roblox en utilisant Instance.new().
Ce modèle est généralement utilisé pour des objets où la classe a une représentation physique dans le modèle de données, et la classe étend sa fonctionnalité. Un bon exemple est BeamBetween, qui crée un objet Beam entre deux objets Attachment donnés et garde ces attachements orientés afin que le faisceau soit toujours dirigé vers le haut. Ces instances pourraient être clonées à partir d'une version préfabriquée dans ReplicatedStorage ou passées dans new() en tant qu'argument et stockées à l'intérieur de l'objet sous self.
Instances correspondantes
Comme noté ci-dessus, de nombreuses classes dans ce projet ont une représentation dans le modèle de données, une instance qui correspond à la classe et est manipulée par elle.
Plutôt que de créer ces instances lorsque l'objet de classe est instancié, le code opte généralement pour Clone() une version préfabriquée de la Instance stockée sous ReplicatedStorage ou ServerStorage. Bien qu'il serait possible de sérialiser les propriétés de ces instances et de les créer à partir de zéro dans les fonctions new() de la classe, cela rendrait l'édition des objets très lourde et les rendrait plus difficiles à comprendre pour un lecteur. De plus, cloner une instance est généralement une opération plus rapide que de créer une nouvelle instance et de personnaliser ses propriétés à l'exécution.
Composition
Bien que l'héritage soit possible en Luau en utilisant les métatables, le projet choisit plutôt de permettre aux classes de s'étendre les unes aux autres par le biais de composition. Lorsque vous combinez des classes par composition, l'objet "enfant" est instancié dans la méthode new() de la classe et est inclus comme membre sous self.
Pour un exemple de cela en action, voyez la classe CloseButton qui enveloppe la classe Button.
Nettoyage
Tout comme une Instance peut être détruite avec la méthode Destroy(), les classes qui peuvent être instanciées peuvent également être détruites. La méthode de destruction pour les classes du projet est destroy() avec un d minuscule pour maintenir la cohérence de camelCase à travers les méthodes de l'ensemble du code, ainsi que pour distinguer les classes du projet et les instances Roblox.
Le rôle de la méthode destroy() est de détruire toutes les instances créées par l'objet, de déconnecter toutes les connexions, et d'appeler destroy() sur tout objet enfant. Ceci est particulièrement important pour les connexions car les instances avec des connexions actives ne sont pas nettoyées par le ramasse-miettes Luau, même s'il ne reste aucune référence à l'instance ou connexions à l'instance.
Singletons
Les singletons, comme leur nom l'indique, sont des classes pour lesquelles un seul objet peut jamais exister. Ce sont l'équivalent du projet des Services de Roblox. Plutôt que de stocker une référence à l'objet singleton et de le passer autour dans le code Luau, Plante profite du fait que le fait de requérir un ModuleScript met en cache sa valeur retournée. Cela signifie que requérir le même ModuleScript singleton depuis différents endroits fournit systématiquement le même objet retourné. La seule exception à cette règle serait si différents environnements (client ou serveur) accédaient au ModuleScript.
Les singletons se distinguent des classes instanciables par le fait qu'ils n'ont pas de méthode new(). Au lieu de cela, l'objet avec ses méthodes et état est retourné directement via le ModuleScript. Comme les singletons ne sont pas instanciés, la syntaxe self n'est pas utilisée et les méthodes sont plutôt appelées avec un point (.) plutôt qu'un deux-points (:).
Vérification stricte des types
Luau prend en charge le typage progressif, ce qui signifie que vous êtes libre d'ajouter des définitions de types optionnelles à une partie ou à l'intégralité de votre code. Dans ce projet, une vérification de type strict est utilisée pour chaque script. C'est l'option la moins permissive pour l'outil Analyse de script de Roblox et donc la plus susceptible de détecter des erreurs de type avant l'exécution.
Syntaxe de classe typée
L'approche établie pour créer des classes en Lua est bien documentée, cependant elle n'est pas bien adaptée à un typage fort Luau. En Luau, l'approche la plus simple pour obtenir le type d'une classe est la méthode typeof() :
type ClassType = typeof(Class.new())
Cela fonctionne mais n'est pas très utile lorsque votre classe est initiée avec des valeurs qui n'existent qu'à l'exécution, par exemple des objets Player. De plus, l'hypothèse faite dans la syntaxe de classe idiomatique Lua est que la déclaration d'une méthode sur une classe self sera toujours une instance de cette classe ; ce n'est pas une hypothèse que le moteur d'inférence de type peut faire.
Afin de soutenir une inférence stricte des types, le projet Plante utilise une solution qui diffère de la syntaxe de classe idiomatique de Lua de plusieurs manières, dont certaines peuvent sembler non intuitives :
- La définition de self est dupliquée, à la fois dans la déclaration de type et dans le constructeur. Cela introduit un fardeau de maintien, mais des avertissements seront signalés si les deux définitions deviennent désynchronisées.
- Les méthodes de classe sont déclarées avec un point, de sorte que self peut être explicitement déclaré comme étant de type ClassType. Les méthodes peuvent toujours être appelées avec un deux-points comme prévu.
--!strict
local MyClass = {}
MyClass.__index = MyClass
export type ClassType = typeof(setmetatable(
{} :: {
property: number,
},
MyClass
))
function MyClass.new(property: number): ClassType
local self = {
property = property,
}
setmetatable(self, MyClass)
return self
end
function MyClass.addOne(self: ClassType)
self.property += 1
end
return MyClass
Typage après des gardes logiques
Au moment de la rédaction, le type d'une valeur n'est pas réduit après une instruction conditionnelle de garde. Par exemple, après la garde ci-dessous, le type de optionalParameter n'est pas réduit à number.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
print(optionalParameter + 1)
end
Pour atténuer cela, de nouvelles variables sont créées après ces gardes avec leur type explicitement casté.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
Traverser les hiérarchies du modèle de données
Dans certains cas, la base de code doit traverser la hiérarchie du modèle de données d'un arbre d'objets créés à l'exécution. Cela représente un défi intéressant pour la vérification des types. Au moment de la rédaction, il n'est pas possible de définir une hiérarchie générique du modèle de données en tant que type. En conséquence, il y a des cas où les informations de type disponibles pour une structure du modèle de données ne sont que le type de l'instance de niveau supérieur.
Une approche de ce défi consiste à caster à any et ensuite affiner. Par exemple :
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
end
Le problème avec cette approche est qu'elle impacte la lisibilité. Au lieu de cela, le projet utilise un module générique appelé getInstance pour traverser les hiérarchies du modèle de données qui cast à any en interne.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
Au fur et à mesure que la compréhension du moteur de types de la hiérarchie du modèle de données évolue, il est possible que des motifs comme celui-ci ne soient plus nécessaires.
Interface utilisateur
Plante comprend une variété d'interfaces utilisateurs 2D simples et complexes. Celles-ci incluent des éléments de l'affichage tête haute (HUD) non interactifs comme le compteur de pièces et des menus interactifs complexes comme la boutique.
Approche UI
Vous pouvez comparer de manière lâche les UI de Roblox au DOM HTML, car c'est une hiérarchie d'objets décrivant ce que l'utilisateur devrait voir. Les approches pour créer et mettre à jour une interface utilisateur Roblox sont largement divisées en pratiques impératives et déclaratives.
| Approche | Avantages et inconvénients |
|---|---|
| Impératif | Dans l'approche impérative, l'UI est traitée comme toute autre hiérarchie d'instances sur Roblox. La structure de l'UI est créée avant l'exécution dans le Studio et ajoutée au modèle de données, généralement directement dans StarterGui. Ensuite, à l'exécution, le code manipule des pièces spécifiques de l'UI pour refléter l'état requis par le créateur. Cette approche présente certains avantages. Vous pouvez créer l'UI depuis zéro dans le Studio et la stocker dans le modèle de données. C'est une expérience d'édition simple et visuelle qui peut accélérer la création d'UI. Comme le code de l'UI impératif ne se soucie que de ce qui doit être changé, il rend également les changements simples d'UI faciles à mettre en œuvre. Un inconvénient notable est que, puisque les approches UI impératives nécessitent que l'état soit mis en œuvre manuellement sous forme de transformations, des représentations complexes de l'état peuvent devenir très difficiles à trouver et à déboguer. Il est courant que des erreurs émergent lors du développement de code UI impératif, surtout lorsque l'état et l'UI deviennent désynchronisés en raison de plusieurs mises à jour interagissant dans un ordre inattendu. Un autre défi avec les approches impératives est qu'il est plus difficile de décomposer l'UI en composants significatifs qui peuvent être déclarés une fois et réutilisés. Comme l'ensemble de l'arbre UI est déclaré en temps d'édition, des motifs communs peuvent être répétés dans plusieurs parties du modèle de données. |
| Déclaratif | Dans l'approche déclarative, l'état désiré des instances UI est déclaré explicitement, et l'implémentation efficace de cet état est abstraite par des bibliothèques telles que Roact ou Fusion. L'avantage de cette approche est que l'implémentation de l'état devient triviale et il vous suffit de décrire à quoi vous voulez que votre UI ressemble. Cela rend l'identification et la résolution des bugs significativement plus faciles. L'inconvénient clé est d'avoir à déclarer l'ensemble de l'arbre UI dans le code. Des bibliothèques comme Roact et Fusion ont des syntaxes pour faciliter cela, mais c'est tout de même un processus long et une expérience d'édition moins intuitive lors de la composition de l'UI. |
Plante utilise une approche impérative sous l'idée que montrer les transformations directement donne une vue d'ensemble plus efficace de la façon dont l'UI est créée et manipulée sur Roblox. Cela ne serait pas possible avec une approche déclarative. Certaines structures et logiques UI répétées sont également abstraites en composants réutilisables pour éviter un écueil commun dans la conception UI impérative.
Architecture de haut niveau

Couches et composants
Dans Plante, toutes les structures d'UI sont soit une Couche soit un Composant.
- Couche est définie comme un singleton de groupement de niveau supérieur qui encapsule les structures UI préfabriquées dans ReplicatedStorage. Une couche peut contenir un certain nombre de composants, ou elle peut encapsuler sa propre logique entièrement. Des exemples de couches sont le menu d'inventaire ou le compteur de pièces dans l'affichage tête haute.
- Composant est un élément UI réutilisable. Lorsqu'un nouvel objet composant est instancié, il clone un modèle préfabriqué de ReplicatedStorage. Les composants peuvent eux-mêmes contenir d'autres composants. Des exemples de composants sont une classe de bouton générique ou le concept d'une liste d'articles.
Gestion des vues
Un problème courant de gestion d'UI est la gestion des vues. Ce projet a une gamme de menus et d'éléments HUD, dont certains écoutent l'entrée utilisateur, et une gestion soigneuse de leur visibilité ou de leur activation est requise.
Plante aborde ce problème avec son système UIHandler qui gère quand une couche d'UI doit ou ne doit pas être visible. Toutes les couches d'UI de l'expérience sont catégorisées comme HUD ou Menu et leur visibilité est gérée selon les règles suivantes :
- L'état activé des couches Menu et HUD peut être basculé.
- Les couches HUD activées ne sont affichées que si aucune couche Menu n'est activée.
- Les couches Menu activées sont stockées dans une pile, et une seule couche Menu est visible à la fois. Lorsqu'une couche Menu est activée, elle est insérée à l'avant de la pile et affichée. Lorsqu'une couche Menu est désactivée, elle est retirée de la pile et la prochaine couche Menu activée en file d'attente est affichée.
Cette approche est intuitive car elle permet de naviguer dans les menus avec un historique. Si un menu est ouvert depuis un autre menu, fermer le nouveau menu affichera de nouveau l'ancien menu.
Les singletons de couche d'UI s'enregistrent auprès du UIHandler et reçoivent un signal qui se déclenche lorsque sa visibilité doit changer.
Lectures complémentaires
À partir de cet aperçu complet du projet Plante, vous voudrez peut-être explorer les guides suivants qui approfondissent encore plus des concepts et sujets connexes.
- Modèle Client-Serveur — Un aperçu du modèle client-serveur dans Roblox.
- Événements distants et rappels — Tout sur les événements réseau distants et les rappels pour la communication à travers la frontière client-serveur.
- UI — Détails sur les objets d'interface utilisateur et la conception sur Roblox.