La plante est une expérience de référence où les joueurs plantent et arrosent des graines, afin qu'ils puissent récolter et vendre les plantes résultantes plus tard.

Le projet se concentre sur des cas d'utilisation communs 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 concessions et la raison des différents choix d'implémentation, afin que vous puissiez prendre la meilleure décision pour vos propres expériences.
Obtenir le fichier
- Accédez à la page d'expérience Plante.
- Cliquez sur le bouton ⋯ et Éditer dans Studio .
Cas d'utilisation
La plante couvre les cas d'utilisation suivants :
- Données de session et persistance des données du joueur
- Gestion des vues d'interface utilisateur
- Réseau client-serveur
- Expérience utilisateur pour la première fois (FTUE)
- Achats de devises dures et souples
En outre, ce projet résout des ensembles de problèmes plus étroits qui sont applicables à de nombreuses expériences, notamment :
- Personnalisation d'une zone dans l'endroit associé à un joueur
- Gérer la vitesse de déplacement du personnage joueur
- Créer un objet qui suit les caractères autour
- Détecter dans quelle partie du monde se trouve un personnage
Notez qu'il y a plusieurs cas d'utilisation dans cette expérience qui sont trop petits, trop nichés ou ne démontrent pas de 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 est de 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 client et le serveur.
Modèle de données
La table suivante décrit les services de conteneur dans les instances de modèle de données dans lesquelles ils sont placés.
Service | Les types d'instance |
---|---|
Workspace | Contient des modèles statiques représentant le monde 3D, plus précisément des parties du monde qui ne appartiennent à aucun joueur.Vous n'avez pas besoin de créer, de modifier ou de supprimer dynamiquement ces instances au moment de l'exécution, donc il est acceptable de les laisser ici.: Il y a aussi un modèle vide Folder , auquel les modèles de ferme des joueurs seront ajoutés au moment de l'exécution. |
Lighting | Effets atmosphériques et de lumière. |
ReplicatedFirst | Contient le plus petit sous-ensemble possible d'instances nécessaires pour afficher l'écran de chargement et initialiser le jeu.Plus il y a d'instances placées dans ReplicatedFirst, plus longtemps il faut attendre qu'elles se reproduisent avant que le code dans ReplicatedFirst puisse s'exécuter.:
|
ReplicatedStorage | Sert de conteneur de stockage pour toutes les instances pour lesquelles l'accès est requis à la fois sur le client et sur le serveur.:
|
ServerScriptService | Contient une Script servant de point d'entrée pour tout le code côté serveur dans le projet. |
ServerStorage | Sert de conteneur de stockage pour toutes les instances qui ne doivent pas être répliquées au client.:
|
SoundService | Contient les objets Sound utilisés pour les effets sonores dans le jeu.En dessous de 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 d'un réutilisable ModuleScripts qui peut être importé dans l'ensemble de la base de code.ModuleScripts sont réutilisables mais ne s'exécutent pas par eux-mêmes ; ils doivent être importés par un Script ou LocalScript .De nombreux projets Roblox auront un grand nombre d'objets Script et LocalScript, chacun appartenant à un comportement ou à un système spécifique dans le jeu, créant plusieurs points d'entrée.
Pour le microjeu Plante, une approche différente est mise en œuvre via une seule LocalScript qui est le point d'entrée pour tout le code client, et une seule Script qui est le point d'entrée pour tout le code serveur.La bonne approche pour votre projet dépend de vos exigences, mais un seul point d'entrée fournit un meilleur contrôle sur l'ordre dans lequel les systèmes sont exécutés.
Les listes suivantes décrivent les compromis de chacune des approches :
- A single Script and a single LocalScript cover server and client code respectively.
- Greater control over the order in which different systems are started because all code is initialized from a single script.
- Can pass objects by reference between systems.
Architecture de système de haut niveau
Les systèmes de premier niveau dans le projet sont détaillés ci-dessous.Certains de ces systèmes sont beaucoup 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 instantanée initialisée par le script client ou serveur pertinent start.Vous pouvez en savoir plus sur le modèle singleton plus tard dans ce guide.
Serveur
Les systems suivants sont associés au serveur.
Système | Avertissement |
---|---|
Réseau |
|
Serveur de données du joueur |
|
Marché |
|
Gestionnaire de groupe de collision | Attribue les modèles de personnage de joueur aux groupes de collision .:
|
Serveur de gestionnaire de ferme |
|
Conteneur d'objets de joueur |
|
Joueurs de tag |
|
Serveur FtueManager |
|
Générateur de caractères |
|
Client
Les systèmes suivants sont associés au client.
Système | Avertissement |
---|---|
Réseau |
|
Client de données joueur |
|
Client du marché |
|
Gestionnaire de saut de marche locale |
|
Clienteur de ferme |
|
Installation de l'interface utilisateur |
|
Clienteur FtueManager |
|
Course de caractères |
|
Communication entre client et serveur
La plupart des expériences Roblox impliquent une certaine forme de communication entre le client et le serveur.Cela peut inclure la demande du client au serveur d'effectuer une certaine action et la réplication des mises à jour du serveur 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 à suivreCe projet utilise les méthodes suivantes, par ordre de préférence :
- Réplication via le système de données du joueur.
- Réplication via attributs.
- Messagerie directement via le module Réseau .
Réplication via le système de données du joueur
Le système de données joueur permet d'associer des données au joueur qui persiste 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 aux modifications, ce qui le rend idéal pour répliquer les modifications de l'état du joueur du serveur au client.
Par exemple, plutôt que de tirer un bespoke UpdateCoins``Class.RemoteEvent pour dire au client combien de pièces il a, vous pouvez appeler le suivant et laisser le client s'abonner à celui-ci via l'événement PlayerDataClient.updated.
PlayerDataServer:setValue(player, "coins", 5)
Bien sûr, cela n'est utile que pour la réplication serveur-client et pour les valeurs que vous souhaitez persister entre les sessions, mais cela s'applique à un nombre surprenant de cas dans le projet, y compris:
- L'étape FTUE actuelle
- L'inventory du joueur
- Le nombre de pièces que le joueur a
- L'état de la ferme du joueur
Réplication via des attributs
Dans des situations où le serveur doit répliquer une valeur personnalisée au client qui est spécifique à un donné Instance , vous pouvez utiliser les attributs.Roblox réplique automatiquement les valeurs d'attribut, vous n'avez donc 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.
Cela est particulièrement utile pour les instances créées au moment de 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 reproduiront atomiquement avec l'instance elle-même.Cela évite le 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, du client ou du serveur, avec la méthode GetAttribute() et vous abonner aux modifications avec la méthode GetAttributeChangedSignal().Dans le projet Plante, cette approche est utilisée, entre autres, pour répliquer l'état actuel des plantes aux clients.
Réplication via des balises
CollectionService vous permet d'appliquer une balise de chaîne à un Instance. Ceci est utile pour catégoriser les instances et de répliquer cette catégorisation au client.
Par exemple, la balise CanPlant est appliquée sur le serveur pour indiquer au client qu'un pot donné est capable de recevoir une plante.
Message 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 du projet qui permet la communication client-serveur et est donc la plus utile pour transmettre les demandes du client et recevoir une réponse du serveur.
Plante utilise des appels réseau directs pour une variété de demandes client, y compris :
- Arroser une plante
- Semer une graine
- Acheter un article
L'inconvénient de cette approche est que chaque message individuel nécessite une configuration personnalisée qui peut augmenter la complexité du projet, bien que cela ait été évité partout où cela a été possible, notamment pour la communication serveur-client.
Classements et singletons
Les classes dans le projet Plante peuvent être créées et détruites, comme des instances sur Roblox.Sa syntaxe de classe est inspirée de l'approche idiomatique de Lua à la programmation orientée objet avec un certain nombre de modifications pour activer le support de vérification de type strict.
Instantiation
De nombreuses classes du projet sont associées à une ou plusieurs Instances.Les objets d'une classe donnée sont créés en utilisant une méthode new() , conforme à la façon dont les instances sont créées dans Roblox en utilisant Instance.new() .
Ce modèle est généralement utilisé pour les 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 donnés Attachment et garde ces annexes orientées afin que le rayon soit toujours dirigé vers le haut.Ces instances pourraient être clonées à partir d'une version préfabriquée dans ReplicatedStorage ou transféré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 de modèle de données, une instance qui correspond à la classe et qui est manipulée par elle.
Plutôt que de créer ces instances lorsqu'un objet de classe est instancé, le code choisit généralement de Clone() une version préfabriquée du Instance stockée sous ReplicatedStorage ou ServerStorage .Bien qu'il soit possible de sérialiser les propriétés de ces instances et de les créer à partir de zéro dans les fonctions de classe new(), le faire rendrait l'édition des objets très laborieuse et les rendrait plus difficiles à analyser pour un lecteur.De plus, le clonage d'une instance est généralement une opération plus rapide que la création d'une nouvelle instance et la personnalisation de ses propriétés en temps d'exécution.
composition
Bien que l'héritage soit possible dans Luau en utilisant métatables, le projet choisit plutôt d'autoriser les classes à s'étendre les unes aux autres par le biais de composition .Lors de la combinaison de classes par composition, l'objet « enfant » est instancié dans la méthode new() de la classe et est inclus en tant que membre sous self .
Pour un exemple de cela en action, voir la classe CloseButton qui enveloppe la classe Button.
Supprimer
De la même manière qu'un Instance peut être détruit avec la méthode Destroy(), les classes qui peuvent être instanciées peuvent également être détruites.La méthode destructeur pour les classes de projet est avec une minuscule pour la cohérence entre les méthodes de l'ensemble du code source, 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 couper toutes les connexions, et d'appeler destroy() sur tous les objets enfants.C'est particulièrement important pour les connexions car les instances avec des connexions actives ne sont pas nettoyées par le collecteur de déchets Luau, même si aucune référence à l'instance ou aucune connexion à l'instance ne reste.
Singletons
Les singletons, comme le nom l'indique, sont des classes pour lesquelles un seul objet peut exister à jamais.Ils sont l'équivalent des services de Roblox pour le projet Services.Plutôt que de stocker une référence à l'objet singleton et de le transmettre dans le code Luau, Plante profite du fait que la nécessité d'un ModuleScript cache sa valeur retournée.Cela signifie que la nécessité du même singleton ModuleScript à partir de différents endroits fournit de manière cohérente 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 sont distingués des classes instantanées par le fait qu'ils n'ont pas de méthode new().Plutôt, l'objet avec ses méthodes et son état est retourné directement via le ModuleScript .Comme les singletons ne sont pas instantanés, la syntaxe self n'est pas utilisée et les méthodes sont plutôt appelées avec un point ( . ) plutôt qu'une virgule ( : ).
Inference de type strict
Luau prend en charge le saisissement progressif ce qui signifie que vous êtes libre d'ajouter des définitions de type optionnelles à certaines ou à toutes vos modifications de code.Dans ce projet, strict le contrôle de type est utilisé pour chaque script.C'est l'option la moins restrictive pour l'outil d'analyse des scripts de Roblox et donc la plus susceptible de détecter les erreurs de type avant l'exécution.
Syntaxe de classe typée
La démarche établie pour créer des classes en Lua est bien documentée, cependant, elle n'est pas bien adaptée au fort typage Luau.Dans 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 ce n'est pas très utile lorsque votre classe est initialisée avec des valeurs qui n'existent qu'au moment de l'exécution, par exemple des objets Player .En outre, l'hypothèse faite dans la syntaxe de classe Lua idiomatique 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 prendre en charge l'inférence de type strict, le projet Plante utilise une solution qui diffère de la syntaxe de classe Lua idiomatique d'un certain nombre de 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 une charge de maintenance, mais des avertissements seront affichés si les deux définitions tombent en désaccord les unes avec les autres.
- Les méthodes de classe sont déclarées avec un point, donc self peut être explicitement déclaré de type ClassType.Les méthodes peuvent toujours être appelées avec une virgule comme prévu.
--!严格ement
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
Lancez des types après des gardes logiques
Au moment de l'écriture, le type d'une valeur n'est pas restreint après une déclaration conditionnelle de garde.Par exemple, en suivant la garde ci-dessous, le type de optionalParameter n'est pas restreint à number .
--!严格ement
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 exprimé.
--!严格ement
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 parcourir la hiérarchie du modèle de données d'un arbre d'objets créés au moment de l'exécution.Cela présente un défi intéressant pour le contrôle de type.Au moment de l'écriture, il n'est pas possible de définir une hiérarchie de modèle de données générique comme un type.Par conséquent, il existe des cas où les seules informations de type disponibles pour une structure de modèle de données sont le type de l'instance de niveau supérieur.
Une approche de ce défi consiste à lancer à any et à affiner ensuite. Par exemple :
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
end
Le problème avec cette approche est qu'elle a un impact sur la lisibilité.Au lieu de cela, le projet utilise un module générique appelé getInstance pour parcourir les hiérarchies de modèle de données qui se traduisent en interne en any.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
À mesure que l'évolution de la compréhension du modèle de données par le moteur de type évolue, il est possible que des modèles comme celui-ci ne soient plus nécessaires
Interface utilisateur
Plante comprend une variété d'interfaces utilisateur 2D complexes et simples.Ceux-ci incluent des éléments de notification non interactifs (HUD) comme le compteur de pièces et des menus interactifs complexes comme la boutique.
Approche UI
Vous pouvez comparer de manière imprécise l'interface utilisateur Roblox à la DOM HTML, car c'est une hiérarchie d'objets qui décrivent 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'interface utilisateur est traitée comme toute autre hiérarchie d'instance sur Roblox.La structure de l'interface utilisateur est créée avant l'exécution dans Studio et ajoutée au modèle de données, généralement directement dans StarterGui .Ensuite, au moment de l'exécution, le code manipule des parties spécifiques de l'interface utilisateur pour refléter l'état que le créateur requiert.: Cette approche comporte certains avantages.Vous pouvez créer l'interface utilisateur à partir de zéro dans 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'interface utilisateur.Parce que le code d'interface utilisateur impératif ne se soucie que de ce qui doit être modifié, il facilite également l'implémentation de modifications d'interface utilisateur simples.: Un inconvénient notable est que, puisque les approches d'interface utilisateur impératives nécessitent que l'état soit implémenté manuellement sous forme de transformations, les 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 d'interface utilisateur impératif, notamment lorsque l'état et l'interface utilisateur deviennent désynchronisés en raison de plusieurs mises à jour interagissant dans un ordre inattendu.: Un autre défi avec des approches imposées est que c'est plus difficile de décomposer l'interface utilisateur en composants significatifs qui peuvent être déclarés une fois et réutilisés.Puisque l'ensemble de l'arbre d'interface utilisateur est déclaré au moment de l'édition, des modèles communs peuvent se répéter dans plusieurs parties du modèle de données. |
Déclaratif | Dans l'approche déclarative, l'état souhaité des instances d'interface est déclaré explicitement, et la mise en œuvre efficace de cet état est abstraite par des bibliothèques telles que Roact ou Fusion.: L'avantage de cette approche est que la mise en œuvre de l'état devient trivial et que vous n'avez besoin de décrire que ce que vous voulez que votre interface utilisateur ressemble.Cela rend l'identification et la résolution des bugs beaucoup plus faciles.: Le principal inconvénient est de devoir déclarer l'ensemble de l'arbre de l'interface utilisateur en code.Les bibliothèques comme Roact et Fusion ont une syntaxe pour rendre cela plus facile, mais c'est toujours un processus fastidieux et une expérience d'édition moins intuitive lors de la composition de l'interface utilisateur. |
L'usine utilise une approche impérative sous la notion que montrer les transformations directement donne une vision plus efficace de la façon dont l'interface utilisateur est créée et manipulée sur RobloxCela ne serait pas possible avec une approche déclarative.Certaines structures et logiques d'interface répétées sont également abstraites en composants réutilisables pour éviter un piège commun dans le design d'interface impératif.
Architecture de haut niveau

Couches et composants
Dans Plante, toutes les structures d'interface utilisateur sont soit une Layer ou une Component.
- Layer est défini comme un groupe de niveau supérieur qui enveloppe les structures d'interface utilisateur préfabriquées dans ReplicatedStorage .Une couche peut contenir un certain nombre de composants, ou elle peut encapsuler toute sa propre logique.Des exemples de couches sont le menu d'inventaire ou l'indicateur du nombre de pièces dans l'affichage en aperçu
- Component est un élément d'interface utilisateur réutilisable.Lorsqu'un nouvel objet de composant est instancé, il clone un modèle préfabriqué à partir de ReplicatedStorage .Les composants peuvent eux-mêmes contenir d'autres composants.Les exemples de composants sont une classe de bouton générique ou la notion d'une liste d'éléments.
Afficher le traitement
Un problème commun de gestion de l'interface utilisateur est la manipulation des vues.Ce projet comporte une gamme de menus et d'éléments HUD, dont certains écoutent l'entrée de l'utilisateur et dont une gestion soigneuse de la visibilité ou de l'activation est requise.
L'entreprise aborde ce problème avec son système UIHandler qui gère quand une couche d'interface utilisateur doit ou ne doit pas être visible.Toutes les couches d'interface utilisateur dans le jeu sont catégorisées comme HUD ou Menu et leur visibilité est gérée par les règles suivantes :
- L'état activé des couches Menu et HUD peut être basculé.
- Les couches activées HUD ne sont affichées que si aucune couche Menu n'est activée.
- Les couches activées Menu sont stockées dans une pile, et une seule couche Menu est visible à la foisLorsqu'une couche Menu est activée, elle est insérée en avant de la pile et est affichée.Lorsqu'une couche Menu est désactivée, elle est supprimée de la pile et la couche suivante activée Menu de la file d'attente est affichée.
Cette approche est intuitive car elle permet de naviguer dans les menus avec l'histoire.Si un menu est ouvert à partir d'un autre menu, la fermeture du nouveau menu affichera à nouveau le menu ancien.
Les singletons de couche d'interface utilisateur s'enregistrent eux-mêmes auprès du gestionnaire d'interface utilisateur et sont fournis avec un signal qui se déclenche lorsque sa visibilité devrait changer
En savoir plus
À partir de cette vue d'ensemble complète du projet Plante, vous pouvez vouloir explorer les guides suivants qui vont plus en profondeur sur les concepts et les sujets liés.
- Modèle client-serveur — Un aperçu du modèle client-serveur dans Roblox.
- Événements et rappels à distance — Tout sur les événements et les rappels réseau à distance pour la communication à travers la frontière client-serveur.
- Interface utilisateur — Détails sur les objets d'interface utilisateur et le design sur Roblox