Plant è un'esperienza di riferimento in cui i giocatori piantano e annaffiano semi, in modo da poter successivamente raccogliere e vendere le piante risultanti.

Il progetto si concentra su casi d'uso comuni che potresti incontrare nello sviluppo di un'esperienza su Roblox. Dove applicabile, troverai note sui compromessi, le scelte e le giustificazioni delle varie implementazioni, in modo da poter prendere la decisione migliore per le tue esperienze.
Ottieni il file
- Naviga alla pagina dell'esperienza Plant.
- Fai clic sul pulsante ⋯ e Modifica in Studio.
Casi d'uso
Plant copre i seguenti casi d'uso:
- Persistenza dei dati di sessione e dei dati del giocatore
- Gestione della visualizzazione dell'interfaccia utente
- Rete client-server
- Esperienza per il primo utente (FTUE)
- Acquisti di valuta hard e soft
In aggiunta, questo progetto risolve insiemi più ristretti di problemi applicabili a molte esperienze, tra cui:
- Personalizzazione di un'area nel luogo associata a un giocatore
- Gestione della velocità di movimento del personaggio del giocatore
- Creazione di un oggetto che segue i personaggi
- Rilevamento di qual parte del mondo si trova un personaggio
Nota che ci sono diversi casi d'uso in questa esperienza che sono troppo piccoli, troppo di nicchia o non dimostrano una soluzione a una sfida di design interessante; questi non sono coperti.
Struttura del progetto
La prima decisione nella creazione di un'esperienza è decidere come strutturare il progetto, che include principalmente dove posizionare istanze specifiche nel modello dei dati e come organizzare e strutturare i punti di ingresso sia per il codice client che per il server.
Modello dati
La seguente tabella descrive quali servizi contenitore nel modello dei dati le istanze sono posizionate.
| Servizio | Tipi di istanze |
|---|---|
| Workspace | Contiene modelli statici che rappresentano il mondo 3D, nello specifico parti del mondo che non appartengono a nessun giocatore. Non è necessario creare, modificare o distruggere dinamicamente queste istanze durante il runtime, quindi è accettabile lasciarle qui. C'è anche un Folder vuoto, a cui verranno aggiunti i modelli delle fattorie dei giocatori durante il runtime. |
| Lighting | Effetti atmosferici e di illuminazione. |
| ReplicatedFirst | Contiene il più piccolo sottoinsieme possibile di istanze necessarie per visualizzare la schermata di caricamento e inizializzare l'esperienza. Maggiore è il numero di istanze posizionate in ReplicatedFirst, più lungo sarà l'attesa affinché si replicano prima che il codice in ReplicatedFirst possa essere eseguito.
|
| ReplicatedStorage | Funziona come contenitore di archiviazione per tutte le istanze a cui è necessario accedere sia dal client che dal server.
|
| ServerScriptService | Contiene uno Script che funge da punto di ingresso per tutto il codice lato server nel progetto. |
| ServerStorage | Funziona come contenitore di archiviazione per tutte le istanze che non devono essere replicate sul client.
|
| SoundService | Contiene gli oggetti Sound utilizzati per gli effetti sonori nell'esperienza. Sotto SoundService, questi oggetti Sound non hanno posizione e non sono simulati nello spazio 3D. |
Punti di ingresso
La maggior parte dei progetti organizza il codice all'interno di ModuleScripts riutilizzabili che possono essere importati in tutto il codice. ModuleScripts sono riutilizzabili, ma non vengono eseguiti da soli; devono essere importati da uno Script o uno LocalScript. Molti progetti Roblox avranno un gran numero di oggetti Script e LocalScript, ciascuno relativo a un comportamento o un sistema particolare nell'esperienza, creando più punti di ingresso.
Per il microgioco Plant, viene implementato un approccio diverso attraverso un singolo LocalScript che funge da punto di ingresso per tutto il codice client, e un singolo Script che funge da punto di ingresso per tutto il codice server. L'approccio corretto per il tuo progetto dipende dai tuoi requisiti, ma un singolo punto di ingresso fornisce un maggiore controllo sull'ordine in cui i sistemi vengono eseguiti.
Le seguenti liste descrivono i compromessi di entrambi gli approcci:
- Un singolo Script e un singolo LocalScript coprono rispettivamente il codice server e client.
- Maggiore controllo sull'ordine in cui vengono avviati i diversi sistemi perché tutto il codice è inizializzato da un unico script.
- La possibilità di passare oggetti per riferimento tra i sistemi.
Architettura di alto livello dei sistemi
I sistemi di livello superiore nel progetto sono dettagliati di seguito. Alcuni di questi sistemi sono sostanzialmente più complessi di altri e, in molti casi, la loro funzionalità è astratta attraverso una gerarchia di altre classi.

Ognuno di questi sistemi è un "singleton", in quanto è una classe non istanziabile che viene invece inizializzata dallo script di avvio client o server pertinente. Puoi leggere di più sul modello singleton più avanti in questa guida.
Server
I seguenti sistemi sono associati al server.
| Sistema | Descrizione |
|---|---|
| Network |
|
| PlayerDataServer |
|
| Market |
|
| CollisionGroupManager |
|
| FarmManagerServer |
|
| PlayerObjectsContainer |
|
| TagPlayers |
|
| FtueManagerServer |
|
| CharacterSpawner |
|
Client
I seguenti sistemi sono associati al client.
| Sistema | Descrizione |
|---|---|
| Network |
|
| PlayerDataClient |
|
| MarketClient |
|
| LocalWalkJumpManager |
|
| FarmManagerClient |
|
| UISetup |
|
| FtueManagerClient |
|
| CharacterSprint |
|
Comunicazione client-server
La maggior parte delle esperienze Roblox comporta un elemento di comunicazione tra client e server. Ciò può includere la richiesta del client al server di eseguire una determinata azione e il server che replica aggiornamenti al client.
In questo progetto, la comunicazione client-server viene mantenuta il più generica possibile limitando l'uso degli oggetti RemoteEvent e RemoteFunction in modo da ridurre la quantità di regole speciali da tenere traccia. Questo progetto utilizza i seguenti metodi, in ordine di preferenza:
- Replica tramite il sistema di dati dei giocatori.
- Replica tramite attributi.
- Replica tramite tag.
- Messagistica direttamente tramite il modulo Network.
Replica tramite il sistema di dati dei giocatori
Il sistema di dati dei giocatori consente di associare dati al giocatore che persistono tra le sessioni di salvataggio. Questo sistema fornisce replica dal client al server e un insieme di API che possono essere utilizzate per interrogare i dati e iscriversi a modifiche, rendendolo ideale per replicare modifiche allo stato del giocatore dal server al client.
Ad esempio, piuttosto che attivare un UpdateCoins RemoteEvent su misura per informare il client di quanti soldi ha, puoi chiamare il seguente e lasciare che il client si iscriva tramite l'evento PlayerDataClient.updated.
PlayerDataServer.setValue(player, "coins", 5)
Certo, questo è utile solo per la replica dal server al client e per i valori che desideri mantenere tra le sessioni, ma questo si applica a un sorprendente numero di casi nel progetto, tra cui:
- L'attuale fase FTUE
- L'inventario del giocatore
- L'ammontare di monete che il giocatore ha
- Lo stato della fattoria del giocatore
Replica tramite attributi
In situazioni in cui il server deve replicare un valore personalizzato al client specifico per un determinato Instance, puoi utilizzare attributi. Roblox replica automaticamente i valori degli attributi, quindi non hai bisogno di mantenere percorsi di codice per replicare lo stato associato a un oggetto. Un altro vantaggio è che questa replica avviene insieme all'istanza stessa.
Questo è particolarmente utile per le istanze create durante il runtime, poiché gli attributi impostati su una nuova istanza prima che venga parentata al modello dei dati si replicano in modo atomico con l'istanza stessa. Questo evita la necessità di scrivere codice per "attendere" che dati aggiuntivi vengano replicati tramite un RemoteEvent o un StringValue.
Puoi anche leggere direttamente gli attributi dal modello dei dati, sia dal client che dal server, con il metodo GetAttribute() e iscriversi a modifiche con il metodo GetAttributeChangedSignal(). Nel progetto Plant, questo approccio viene utilizzato per, tra le altre cose, replicare l'attuale stato delle piante ai client.
Replica tramite tag
CollectionService ti permette di applicare un tag di stringa a un Instance. Questo è utile per categorizzare le istanze e replicare quella categorizzazione al client.
Ad esempio, il tag CanPlant viene applicato sul server per segnalare al client che un determinato vaso è in grado di ricevere una pianta.
Messaggistica direttamente tramite il modulo di rete
Per situazioni in cui nessuna delle opzioni precedenti si applica, puoi utilizzare chiamate di rete personalizzate attraverso il modulo Network. Questa è l'unica opzione nel progetto che consente la comunicazione client-server e quindi è utile per trasmettere richieste del client e ricevere una risposta dal server.
Plant utilizza chiamate di rete dirette per una varietà di richieste client, inclusi:
- Annaffiare una pianta
- Piantare un seme
- Acquistare un oggetto
Lo svantaggio di questo approccio è che ciascun messaggio individuale richiede una configurazione speciale che può aumentare la complessità del progetto, anche se questo è stato evitato ovunque possibile, in particolare per quanto riguarda la comunicazione dal server al client.
Classi e singleton
Le classi nel progetto Plant, come le istanze su Roblox, possono essere create e distrutte. La sua sintassi di classe è ispirata all'approccio idiomatico di Lua per la programmazione orientata agli oggetti con una serie di modifiche per consentire il supporto della verifica dei tipi rigorosi.
Istanze
Molte classi nel progetto sono associate a una o più Instances. Gli oggetti di una data classe vengono creati utilizzando un metodo new(), coerente con il modo in cui le istanze sono create in Roblox utilizzando Instance.new().
Questo modello viene generalmente utilizzato per oggetti la cui classe ha una rappresentazione fisica nel modello dei dati e la classe estende la sua funzionalità. Un buon esempio è BeamBetween che crea un oggetto Beam tra due dati Attachment e mantiene quegli attachment orientati in modo che il raggio sia sempre rivolto verso l'alto. Queste istanze potrebbero essere clonate da una versione prefabbricata in ReplicatedStorage o passate a new() come argomento e memorizzate all'interno dell'oggetto sotto self.
Istanze corrispondenti
Come notato sopra, molte classi in questo progetto hanno una rappresentanza nel modello dei dati, un'istanza che corrisponde alla classe e che viene manipolata da essa.
Piuttosto che creare queste istanze quando un oggetto di classe viene istanziato, il codice generalmente sceglie di Clone() una versione prefabbricata dell'istanza Instance memorizzata sotto ReplicatedStorage o ServerStorage. Anche se sarebbe possibile serializzare le proprietà di queste istanze e crearle da zero nelle funzioni new() della classe, farlo renderebbe molto scomoda la modifica degli oggetti e renderebbe più difficile per un lettore interpretarli. Inoltre, clonare un'istanza è generalmente un'operazione più veloce rispetto a creare una nuova istanza e personalizzarne le proprietà durante il runtime.
Composizione
Sebbene l'ereditarietà sia possibile in Luau utilizzando metatabelle, il progetto opta invece di consentire alle classi di estendersi a vicenda tramite composizione. Quando si combinano classi tramite composizione, l'oggetto "figlio" viene istanziato nel metodo new() della classe ed è incluso come membro sotto self.
Per un esempio di questo in azione, vedere la classe CloseButton che avvolge la classe Button.
Cleanup
Allo stesso modo in cui un Instance può essere distrutto con il metodo Destroy(), le classi che possono essere istanziate possono anche essere distrutte. Il metodo distruttore per le classi del progetto è destroy() con una d minuscola per coerenza con camelCase attraverso i metodi del codice, oltre a distinguere tra le classi del progetto e le istanze di Roblox.
Il ruolo del metodo destroy() è quello di distruggere qualsiasi istanza creata dall'oggetto, disconnettere eventuali collegamenti e chiamare destroy() su eventuali oggetti figli. Questo è particolarmente importante per le connessioni perché le istanze con connessioni attive non vengono pulite dal garbage collector Luau, anche se non rimangono riferimenti all'istanza o connessioni all'istanza.
Singleton
I singleton, come suggerisce il nome, sono classi per le quali può esistere solo un oggetto. Sono l'equivalente del progetto dei Servizi di Roblox. Piuttosto che memorizzare un riferimento all'oggetto singleton e passarne un'istanza nel codice Luau, Plant sfrutta il fatto che richiedere un ModuleScript memorizza il suo valore restituito. Questo significa che richiedere lo stesso ModuleScript singleton da luoghi diversi fornisce costantemente lo stesso oggetto restituito. L'unica eccezione a questa regola sarebbe se ambienti diversi (client o server) accedessero allo ModuleScript.
I singleton si distinguono dalle classi istanziabili per il fatto che non hanno un metodo new(). Piuttosto, l'oggetto insieme ai suoi metodi e stato vengono restituiti direttamente tramite lo ModuleScript. Poiché i singleton non vengono istanziati, la sintassi self non viene utilizzata e i metodi vengono invece chiamati con un punto (.) piuttosto che con due punti (:).
Verifica di tipo rigorosa
Luau supporta la tipizzazione graduale, il che significa che sei libero di aggiungere definizioni di tipo opzionali a parte o tutto il tuo codice. In questo progetto, la verifica di tipo strict è utilizzata per ogni script. Questa è l'opzione meno permissiva per lo strumento Analisi degli script di Roblox e quindi la più probabile per catturare errori di tipo prima del runtime.
Sintassi di classe tipizzata
L'approccio consolidato per creare classi in Lua è ben documentato, tuttavia non è ben adattato alla tipizzazione rigorosa di Luau. In Luau, l'approccio più semplice per ottenere il tipo di una classe è il metodo typeof():
type ClassType = typeof(Class.new())
Questo funziona ma non è molto utile quando la tua classe viene inizializzata con valori che esistono solo durante il runtime, ad esempio gli oggetti Player. Inoltre, l'assunzione fatta nella sintassi idiomatica delle classi Lua è che dichiarare un metodo su una classe self sarà sempre un'istanza di quella classe; questa non è un'assunzione che il motore di inferenza dei tipi può fare.
Per supportare l'inferenza di tipo rigorosa, il progetto Plant utilizza una soluzione che differisce dalla sintassi idiomatica delle classi di Lua in diversi modi, alcuni dei quali potrebbero sembrare non intuitivi:
- La definizione di self è duplicata, sia nella dichiarazione del tipo che nel costruttore. Questo introduce un onere di manutenibilità, ma verranno segnalati avvisi se le due definizioni non rimangono sincronizzate.
- I metodi delle classi vengono dichiarati con un punto, in modo che self possa essere esplicitamente dichiarato come tipo ClassType. I metodi possono comunque essere chiamati con due punti come previsto.
--!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
Tipi di cast dopo le guardie logiche
Al momento della scrittura, il tipo di un valore non viene ristretto dopo una dichiarazione condizionale di guardia. Ad esempio, seguendo la guardia di seguito, il tipo di optionalParameter non viene ristretto a number.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
print(optionalParameter + 1)
end
Per mitigare questo, vengono creati nuovi variabili dopo queste guardie con il tipo esplicitamente castato.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
Traversare le gerarchie del DataModel
In alcuni casi, il codice deve attraversare la gerarchia del modello dei dati di un albero di oggetti creati durante il runtime. Questo presenta una sfida interessante per la verifica dei tipi. Al momento della scrittura, non è possibile definire una gerarchia generica del modello di dati come tipo. Di conseguenza, ci sono casi in cui l'unica informazione di tipo disponibile per una struttura del modello di dati è il tipo dell'istanza di alto livello.
Un approccio a questa sfida è effettuare un cast a any e poi raffinare. Ad esempio:
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
end
Il problema con questo approccio è che influisce sulla leggibilità. Invece, il progetto utilizza un modulo generico chiamato getInstance per attraversare gerarchie del modello dei dati che effettua internamente un cast a any.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
Man mano che la comprensione del motore di tipo del modello dei dati evolve, è possibile che modelli come questo non siano più necessari.
Interfaccia utente
Plant include una varietà di interfacce utente 2D complesse e semplici. Queste includono elementi non interattivi dell'interfaccia visiva (HUD) come il contatore di monete e menu interattivi complessi come il negozio.
Approccio UI
Puoi confrontare liberamente le UI di Roblox con il DOM HTML, poiché è una gerarchia di oggetti che descrivono cosa dovrebbe vedere l'utente. Gli approcci per creare e aggiornare una UI di Roblox sono ampiamente divisi in pratiche imperative e declarative.
| Approccio | Vantaggi e svantaggi |
|---|---|
| Imperativo | Nel approccio imperativo, l'interfaccia utente è trattata come qualsiasi altra gerarchia di istanze su Roblox. La struttura dell'interfaccia utente viene creata prima del runtime in Studio e aggiunta al modello dati, tipicamente direttamente in StarterGui. Poi, durante il runtime, il codice manipola pezzi specifici dell'interfaccia utente per riflettere lo stato richiesto dal creatore. Questo approccio ha alcuni vantaggi. Puoi creare l'interfaccia da zero in Studio e memorizzarla nel modello di dati. Questa è un'esperienza di modifica semplice e visiva che può accelerare la creazione dell'interfaccia utente. Poiché il codice dell'interfaccia utente imperativa si preoccupa solo di cosa deve essere cambiato, rende anche facili da implementare piccole modifiche all'interfaccia utente. Un notevole svantaggio è che, poiché gli approcci UI imperativi richiedono che lo stato sia implementato manualmente sotto forma di trasformazioni, rappresentazioni complesse dello stato possono diventare molto difficili da trovare e debug. È comune che emergano errori durante lo sviluppo di codice UI imperativo, specialmente quando lo stato e l'interfaccia utente diventano desincronizzati a causa di aggiornamenti multipli che interagiscono in un ordine inaspettato. Un'altra sfida con gli approcci imperativi è che è più difficile suddividere l'interfaccia utente in componenti significativi che possono essere dichiarati una sola volta e riutilizzati. Poiché l'intero albero dell'interfaccia utente viene dichiarato nella fase di modifica, modelli comuni possono essere ripetuti in più parti del modello di dati. |
| Declarativo | Nell'approccio dichiarativo, lo stato desiderato delle istanze UI viene dichiarato esplicitamente, e l'implementazione efficiente di questo stato è astratta da librerie come Roact o Fusion. Il vantaggio di questo approccio è che l'implementazione dello stato diventa banale e devi solo descrivere come vuoi che appaia la tua UI. Questo rende molto più facile identificare e risolvere i bug. Lo svantaggio principale è dover dichiarare l'intero albero UI nel codice. Librerie come Roact e Fusion hanno sintassi per semplificare questo, ma è comunque un processo che richiede tempo e un'esperienza di modifica meno intuitiva quando si compone l'interfaccia utente. |
Plant utilizza un approccio imperativo con l'idea che mostrare le trasformazioni direttamente fornisce una visione più efficace di come l'interfaccia utente venga creata e manipolata su Roblox. Questo non sarebbe possibile con un approccio dichiarativo. Alcune strutture e logiche UI ripetute sono anche astratte in componenti riutilizzabili per evitare un difetto comune nel design UI imperativo.
Architettura di alto livello

Strato e componenti
In Plant, tutte le strutture UI sono o uno Layer o un Component.
- Layer è definito come un singleton di raggruppamento di alto livello che avvolge strutture UI prefabbricate in ReplicatedStorage. Un layer può contenere un certo numero di componenti, oppure può incapsulare completamente la propria logica. Esempi di layer sono il menu inventario o l'indicatore del numero di monete nell'interfaccia visiva.
- Component è un elemento UI riutilizzabile. Quando un nuovo oggetto componente viene istanziato, clona un template prefabbricato da ReplicatedStorage. I componenti possono a loro volta contenere altri componenti. Esempi di componenti sono una classe di pulsanti generici o il concetto di un elenco di oggetti.
Gestione della vista
Un comune problema nella gestione dell'interfaccia utente è la gestione delle viste. Questo progetto ha una serie di menu e elementi HUD, alcuni dei quali ascoltano l'input dell'utente, e una gestione attenta di quando sono visibili o abilitati è necessaria.
Plant affronta questo problema con il suo sistema UIHandler che gestisce quando un livello dell'interfaccia utente dovrebbe o non dovrebbe essere visibile. Tutti i livelli dell'interfaccia utente nell'esperienza sono categorizzati come HUD o Menu e la loro visibilità è gestita dalle seguenti regole:
- Lo stato abilitato dei livelli Menu e HUD può essere attivato.
- I livelli HUD abilitati vengono mostrati solo se nessun livello Menu è abilitato.
- I livelli Menu abilitati sono memorizzati in uno stack, e solo un livello Menu è visibile alla volta. Quando un livello Menu è abilitato, viene inserito in cima allo stack e mostrato. Quando un livello Menu è disabilitato, viene rimosso dallo stack e il prossimo livello Menu abilitato nella coda viene mostrato.
Questo approccio è intuitivo perché consente la navigazione nei menu con cronologia. Se un menu viene aprito da un altro menu, chiudere il nuovo menu mostrerà di nuovo il vecchio menu.
Ulteriori letture
Da questa panoramica approfondita del progetto Plant, potresti voler esplorare le seguenti guide che approfondiscono ulteriormente concetti e argomenti correlati.
- Modello Client-Server — Una panoramica del modello client-server in Roblox.
- Eventi e Callback Remoti — Tutto su eventi di rete remota e callback per la comunicazione attraverso il confine client-server.
- UI — Dettagli sugli oggetti e sul design dell'interfaccia utente su Roblox.