Pianta è un'esperienza di riferimento in cui i giocatori piantano e annaffiano i semi, in modo da poterli raccogliere e vendere in seguito le piante risultanti.

Il progetto si concentra su casi d'uso comuni che potresti incontrare quando sviluppi un'esperienza su Roblox.Dove Applicabile, troverai note su scambi, compromessi e la logica di diverse scelte di implementazione, in modo da poter prendere la migliore decisione per le tue esperienze.
Ottieni il file
- Vai alla pagina dell'esperienza Pianta.
- Fai clic sul pulsante ⋯ e Modifica in Studio .
Uso dei casi
Pianta copre i seguenti casi d'uso:
- Persistenza dei dati della sessione e dei dati del giocatore
- Gestione della vista UI
- Reti client-server
- Esperienza utente della prima volta (FTUE)
- Acquisti di valuta dura e soft
Inoltre, questo progetto risolve insiemi più stretti di problemi applicabili a molte esperienze, tra cui:
- Personalizzazione di un'area nel luogo che è associato a un Giocatore
- Gestire la velocità di movimento del personaggio del giocatore
- Creazione di un oggetto che segue i personaggi in giro
- Rilevare quale parte del mondo un personaggio è in
Si noti che ci sono diversi casi d'uso in questa esperienza che sono troppo piccoli, troppo nicchi o non dimostrano una soluzione a una sfida di design interessante; questi non sono coperti.
Structure del progetto
La prima decisione quando si crea un'esperienza è decidere come strutturare il progetto, che include principalmente dove posizionare istanze specifiche nel modello di dati e come organizzare e strutturare i punti di ingresso sia per il client che per il server codice.
modellodi dati
La seguente tabella descrive in quale contenitore sono posizionati i servizi container nell'istanza del modello di dati.
Servizio | Tipi di istanze |
---|---|
Workspace | Contiene modelli statici che rappresentano il Mondo3D, in particolare parti del mondo che non appartengono a nessun Giocatore.Non è necessario creare, modificare o distruggere dinamicamente queste istanze durante l'Tempo esecuzione, quindi è accettabile lasciarle qui.: Esiste anche un vuoto Folder , al quale saranno aggiunti i modelli di fattoria dei giocatori durante l'Tempo esecuzione. |
Lighting | Effetti atmosferici e di illuminazione. |
ReplicatedFirst | Contiene il sottinsieme più piccolo possibile di istanze necessarie per visualizzare la schermata di caricamento e inizializzare il Gioco.Più istanze vengono posizionate in ReplicatedFirst , più a lungo l'attesa prima che possano replicarsi prima che il codice in ReplicatedFirst possa essere Eseguire.:
|
ReplicatedStorage | Servisce come contenitore di storage per tutte le istanze per le quali è richiesto l'accesso sia sul client che sul Server.:
|
ServerScriptService | Contiene un Script che funge da punto di ingresso per tutto il codice lato server nel progetto. |
ServerStorage | Servisce come contenitore di storage per tutte le istanze che non devono essere replicate al client.:
|
SoundService | Contiene gli oggetti Sound utilizzati per gli effetti sonori nel Gioco.Sotto SoundService , questi oggetti Sound non hanno posizione e non vengono simulati nello Spazio3D. |
Punti di ingresso
La maggior parte dei progetti organizza il codice all'interno di riutilizzabili ModuleScripts che possono essere importati in tutta la base di codice.ModuleScripts sono riutilizzabili ma non vengono eseguiti da Proprio; devono essere importati da un Script o LocalScript .Molti progetti Roblox avranno un gran numero di Script e LocalScript oggetti, ciascuno pertinente a un comportamento o a un sistema particolare nel Gioco, creando più punti di ingresso.
Per il microgame Pianta, un approccio diverso viene implementato attraverso un singolo LocalScript che è il punto di ingresso per tutto il codice client e un singolo Script che è il punto di ingresso per tutto il codice server.L'approccio corretto per il tuo progetto dipende dai tuoi requisiti, ma un unico punto di ingresso fornisce un maggiore controllo sull'ordine in cui vengono eseguiti i sistemi.
Le seguenti liste descrivono i compromessi di entrambi gli approcci:
- Un singolo Script e un singolo LocalScript copre rispettivamente il codice del server e del client.
- Maggiore controllo sull'ordine in cui vengono avviati i diversi sistemi poiché tutto il codice viene inizializzato da un singolo script.
- Può passare oggetti per riferimento tra i sistemi.
Architettura di alto livello dei sistemi
I sistemi di alto livello nel progetto sono dettagliati qui sotto.Alcuni di questi sistemi sono sostanzialmente più complessi di altri e, in molti casi, la loro funzionalità viene astratta attraverso una gerarchia di altre Classi.

Ognuno di questi sistemi è un "singleton", in quanto è una classe non istantanea che viene invece inizializzata dal relativo client o server start script.Puoi leggere di più sul modello singleton singleton più tardi in questa guida.
Server
I seguenti sistemi sono associati al Server.
Sistema | Descrizione |
---|---|
Rete |
|
Server di dati del giocatore |
|
Mercato |
|
Gestore del gruppo di collisione |
|
Server di gestione della fattoria |
|
Contenitore di oggetti player |
|
Giocatori di etichetta |
|
FtueManagerServer |
|
Generatore di caratteri |
|
Cliente
I seguenti sistemi sono associati al client.
Sistema | Descrizione |
---|---|
Rete |
|
PlayerDataClient |
|
MercatoCliente |
|
LocalWalkJumpManager |
|
Client Manager di Fattoria |
|
Configurazione dell'interfaccia utente |
|
FtueManagerClient |
|
CharacterSprint |
|
Comunicazione client-server
La maggior parte delle esperienze Roblox coinvolge qualche elemento di comunicazione tra il client e il Server.Questo può includere la richiesta del client di eseguire una certa azione e il server replicare gli aggiornamenti al client.
In questo progetto, la comunicazione client-server è mantenuta il più generica possibile limitando l'uso di RemoteEvent e RemoteFunction oggetti al fine di ridurre la quantità di regole speciali da tenere traccia.Questo progetto utilizza i seguenti metodi, in ordine di preferenza:
- Replicazione attraverso il sistema di dati del giocatore.
- Replicazione attraverso attributi.
- Replicazione tramite tag .
- Messaggi direttamente attraverso il modulo Network .
Replicazione attraverso il sistema di dati del player
Il sistema di dati del giocatore consente ai dati di essere associati al giocatore che persiste tra le sessioni di salvataggio .Questo sistema fornisce la replicazione dal client al server e un insieme di API che possono essere utilizzate per interrogare i dati e sottoscrivere le modifiche, rendendolo ideale per replicare le modifiche allo stato del giocatore dal server al client.
Ad esempio, invece di sparare un personalizzato UpdateCoins``Class.RemoteEvent per dire al cliente quante monete ha, puoi chiamare il seguente e lasciare che il cliente si abboni a esso tramite l'evento PlayerDataClient.updated.
PlayerDataServer:setValue(player, "coins", 5)
Naturalmente, questo è utile solo per la replicazione server-client e per i valori che vuoi persistere tra le sessioni, ma questo si applica a un numero sorprendente di casi nel progetto, tra cui:
- La fase attuale di FTUE
- L'Inventario, reportoriodel Giocatore
- La quantità di monete che il giocatore ha
- Lo stato della fattoria del Giocatore
Replicazione tramite attributi
In situazioni in cui il server deve replicare un valore personalizzato al client che è specifico per un dato Instance , puoi usare attributi .Roblox replica automaticamente i valori degli attributi, quindi non è necessario mantenere alcun percorso di codice per replicare lo stato associato a un oggetto.Un altro vantaggio è che questa replicazione avvenga accanto all'istanza stessa.
Questo è particolarmente utile per le istanze create durante l'Tempo esecuzione, poiché gli attributi impostati su una nuova istanza prima di essere parentizzati al modello di dati si replicano atomically con l'istanza stessa.Questo evita la necessità di scrivere codice per "aspettare" che i dati extra vengano replicati tramite un RemoteEvent o StringValue .
Puoi anche leggere direttamente gli attributi dal modello di dati, dal client o dal Server, con il metodo GetAttribute(), e iscriverti alle modifiche con il metodo GetAttributeChangedSignal().Nel progetto Pianta, questo approccio viene utilizzato, tra le altre cose, per replicare lo stato attuale delle piante ai client.
Replicazione tramite tag
CollectionService ti consente 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 una data pentola è in grado di ricevere una pianta.
Messaggio direttamente tramite 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 è la più utile per trasmettere richieste client e ricevere una risposta del server.
Pianta utilizza chiamate di rete dirette per una varietà di richieste client, tra cui:
- Annaffiare una pianta
- Piantare un seme
- Acquisto di un Articolo
Il limite di questo approccio è che ogni singolo messaggio richiede una configurazione personalizzata che può aumentare la complessità del progetto, anche se questo è stato evitato ovunque sia stato possibile, in particolare per la comunicazione server-client.
Classi e singletons
Le classi nel progetto Pianta possono essere create e distrutte come istanze su Roblox.La sua sintassi di classe è ispirata all'approccio idiomatico di Lua alla programmazione oggettuale con una serie di modifiche per abilitare il Assistenzadi controllo rigoroso del tipo.
Istantaneizzazione
Molte classi nel progetto sono associate a uno o più Instances .Gli oggetti di una classe data vengono creati utilizzando un metodo new() , coerente con il modo in cui le istanze vengono create in Roblox usando Instance.new() .
Questo modello viene generalmente utilizzato per oggetti in cui la classe ha una rappresentazione fisica nel modello di dati e la classe estende le sue funzionalità.Un buon esempio è che crea un oggetto tra due oggetti dati e mantiene tali allegati 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 in new() come argomento e memorizzate all'interno dell'oggetto sotto self .
Istanze corrispondenti
Come notato sopra, molte classi in questo progetto hanno una rappresentazione del modello di dati, un'istanza che corrisponde alla classe e viene manipolata da essa.
Piuttosto che creare queste istanze quando un oggetto di classe viene istanziato, il codice generalmente opta per Clone() una versione prefabbricata del Instance memorizzata sotto ReplicatedStorage o ServerStorage .Sebbene sia possibile serializzare le proprietà di queste istanze e crearle da zero nelle funzioni della classe new(), farlo renderebbe l'editing degli oggetti molto laborioso e li renderà più difficili da analizzare per un lettore.Inoltre, clonare un'istanza è generalmente un'operazione più rapida rispetto alla creazione di una nuova istanza e alla personalizzazione delle sue proprietà in Tempo esecuzione.
Composizione
Sebbene l'ereditarietà sia possibile in Luau utilizzando tabelle metodiche, il progetto opta invece di consentire alle classi di estendersi a vicenda attraverso composizione .Quando si combinano classi attraverso la composizione, l'oggetto "figlio" viene istanziato nel metodo new() della classe e viene incluso come membro sotto self .
Per un esempio di questo in azione, vedi la classe CloseButton che avvolge la classe Button.
Pulizia
Simile a come 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 è con una minuscola per la coerenza tra i metodi del progetto, nonché per distinguere tra le classi del progetto e le istanze di Roblox.
Il ruolo del metodo destroy() è distruggere qualsiasi istanza creata dall'oggetto, disconnettere qualsiasi connessione e chiamare destroy() su qualsiasi oggetto figlio.Questo è particolarmente importante per le connessioni perché le istanze con connessioni attive non vengono pulite dal raccoglitore di spazzatura Luau, anche se non rimangono riferimenti all'istanza o connessioni all'istanza.
Singletons
Singletons, come il nome suggerisce, sono classi per le quali esista mai un solo oggetto.Sono l'equivalente del progetto dei Servizi di Roblox.Piuttosto che memorizzare un riferimento all'oggetto singleton e passarlo in giro nel codice Luau, Pianta approfitta del fatto che richiedere un ModuleScript cache il suo valore restituito.Questo significa che richiedere lo stesso singleton ModuleScript da luoghi diversi in modo coerente fornisce lo stesso oggetto restituito.L'unica eccezione a questa regola sarebbe se ambienti diversi (client o Server) accedessero al ModuleScript .
I singleton sono distinti dalle classi instabili dal fatto che non hanno un metodo new() .Piuttosto, l'oggetto insieme ai suoi metodi e stato viene restituito direttamente tramite il ModuleScript .Poiché i singletons non vengono istanziati, la sintassi self non viene utilizzata e i metodi vengono chiamati invece con un punto ( . ) piuttosto che con un colon ( : ).
Inferenza di tipo rigoroso
Luau supporta la digitazione graduale che significa che sei libero di aggiungere definizioni di tipo opzionale a alcuni o a tutti i tuoi codice.In questo progetto, strict il controllo del tipo viene utilizzato per ogni script.Questa è l'opzione meno permissiva per lo strumento di analisi degli script di Roblox e quindi la più probabile per catturare gli errori di tipo prima del Tempo esecuzione.
Sintassi di classe digitata
L'approccio stabilito per creare classi in Lua è ben documentato, tuttavia non è adatto a un forte tipaggio 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 l'esecuzione, ad esempio Player oggetti.Inoltre, l'assunzione fatta nella sintassi della classe Lua idiomatica è che dichiarare un metodo su una classe self sarà sempre un'istanza di quella classe; questa non è un'assunzione che il motore di inferenza di tipo può fare.
Al fine di supportare l'inferenza di tipo rigoroso, il progetto Pianta utilizza una soluzione che differisce dalla sintassi di classe Lua idiomatica in un certo numero di modi, alcuni dei quali possono sembrare non intuitivi:
- La definizione di self è duplicata, sia nella dichiarazione di tipo che nel costruttore.Questo introduce un onere di manutenzione, ma gli avvertimenti verranno contrassegnati se le due definizioni cadranno fuori sincronia l'una con l'altra.
- I metodi di classe sono dichiarati con un punto, quindi self può essere dichiarato esplicitamente di tipo ClassType .I metodi possono ancora essere chiamati con un colon come previsto.
--!stringente
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 restringuto dopo una dichiarazione condizionale di guard.Ad esempio, seguendo la guardia qui sotto, il tipo di optionalParameter non è restringente a number .
--!stringente
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
print(optionalParameter + 1)
end
Per mitigare questo, nuove variabili vengono create dopo queste guardie con il loro tipo esplicitamente cast.
--!stringente
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
Trasversale le gerarchie del modello di dati
In alcuni casi, il codebase deve percorrere la gerarchia del modello di dati di un albero di oggetti che vengono creati al momento dell'Tempo esecuzione.Questo presenta una sfida interessante per il controllo del tipo.Al momento della stesura, non è possibile definire una gerarchia di modello di dati generico come inserisci / scrivi.Di Risultato, ci sono casi in cui l'unica informazione di tipo disponibile per una struttura di modello di dati è il tipo dell'esempiodi alto livello.
Un approccio a questa sfida è quello di lanciare 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 le gerarchie del modello di dati che viene tradotto in any internamente.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
Mentre l'interpretazione del motore di tipo dell'数据模型 evolve, è possibile che modelli come questo non saranno più necessari.
Interfaccia utente
Pianta include una varietà di interfacce utente 2D complesse e semplici.Questi includono oggetti non interattivi di avviso (HUD) come il contapassi delle monete e menu interattivi complessi come il Negozio.
Approccio dell'UI
Puoi confrontare in modo informale Roblox UI con l'HTML DOM, perché è una gerarchia di oggetti che descrivono ciò che l'utente dovrebbe vedere.Gli approcci per creare e aggiornare un'interfaccia utente Roblox sono ampiamente divisi in imperativo e dichiarativo pratiche.
Avvicinamento | Vantaggi e svantaggi |
---|---|
Imperativo | Nell'approccio imperativo, l'interfaccia utente viene trattata come qualsiasi altra gerarchia di istanze su Roblox.La struttura dell'interfaccia utente viene creata prima del Tempo esecuzionein Studio e aggiunta al modello di dati, tipicamente direttamente in StarterGui .Quindi, durante l'esecuzione, il codice manipola parti specifiche dell'interfaccia utente per riflettere lo stato richiesto dal creatore.: Questo approccio viene con alcuni vantaggi.Puoi creare l'interfaccia utente da zero in Studio e conservarla nel modello di dati.Questa è una semplice e visiva esperienza di modifica che può accelerare la Creazionidell'interfaccia utente.Poiché il codice imperativo dell'interfaccia utente si preoccupa solo di ciò che deve essere cambiato, rende anche le modifiche semplici dell'interfaccia utente facili da implementare.: Un notevole svantaggio è che, poiché gli approcci imperativi all'interfaccia utente richiedono che lo stato venga implementato manualmente sotto forma di trasformazioni, le rappresentazioni complesse dello stato possono diventare molto difficili da trovare e Debug.È comune che si presentino errori durante lo sviluppo di codice imperativo UI, in particolare quando lo stato e l'interfaccia utente diventano desincronizzati a causa di più aggiornamenti che interagiscono in un ordine inaspettato.: Un'altra sfida con approcci imperativi è che è più difficile dividere l'interfaccia utente in componenti significativi che possono essere dichiarati una volta e riutilizzati.Poiché l'intero albero dell'interfaccia utente viene dichiarato al momento dell'edizione, i modelli comuni possono essere ripetuti in più parti del modello di dati. |
Dichiarativo | Nell'approccio dichiarativo, lo stato desiderato delle istanze UI viene dichiarato esplicitamente e l'implementazione efficiente di questo stato viene astratta da librerie come Roact o Fusion.: Il vantaggio di questo approccio è l'implementazione dello stato diventa trivial e devi solo descrivere ciò che vuoi che la tua interfaccia utente assomigli.Questo rende l'identificazione e la risoluzione degli errori significativamente più facile.: Il punto debole principale è quello di dover dichiarare l'intero albero dell'interfaccia utente in codice.Biblioteche come Roact e Fusion hanno una sintassi per rendere questo più facile, ma è ancora un processo che richiede tempo e un'esperienza di modifica meno intuitiva quando si compone l'interfaccia utente. |
Pianta utilizza un approccio imperativo sotto la nozione che mostrare le trasformazioni direttamente dà una panoramica più efficace di come l'interfaccia utente viene 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 una caduta comune nel design dell'interfaccia imperativa.
Architettura di alto livello

Strato e componenti
In Pianta , tutte le strutture dell'interfaccia utente sono o un Layer o un Component .
- Layer è definito come un singolo di gruppo di alto livello che avvolge le strutture di interfaccia prefabbricate in ReplicatedStorage .Una层 può contenere un certo numero di componenti, o può incapsulare la propria logica completamente.Esempi di strati sono il menu inventario o l'indicatore del numero di monete nella visualizzazione a schermo superiore.
- Component è un elemento UI riutilizzabile.Quando un oggetto componente nuovo viene istanziato, clona un modello prefabbricato da ReplicatedStorage .I componenti possono contenere in sé altri componenti.Esempi di componenti sono una classe di pulsanti generici o il concetto di una lista di oggetti.
Vedi gestione
Un problema comune di gestione dell'interfaccia utente è la gestione delle viste.Questo progetto ha una gamma di menu e oggetti HUD, alcuni dei quali ascoltano l'input dell'utente e è richiesta una gestione attenta quando sono visibili o abilitati.
Pianta affronta questo problema con il suo sistema UIHandler che gestisce quando uno strato UI debba o non debba essere visibile.Tutti gli strati dell'interfaccia utente nel gioco sono categorizzati come HUD o Menu e la loro visibilità è gestita dalle seguenti regole:
- Lo stato abilitato di Menu e HUD strati può essere attivato/disattivato.
- Gli strati abilitati HUD vengono mostrati solo se non sono abilitati strati Menu .
- Gli strati abilitati Menu vengono memorizzati in uno stack e solo uno strato Menu è visibile alla volta.Quando un livello Menu è abilitato, viene inserito alla fine della stack e mostrato.Quando un livello Menu viene disabilitato, viene rimosso dallo stack e viene mostrato il prossimo livello abilitato Menu nella coda.
Questo approccio è intuitivo perché consente ai menu di essere navigati con la storia.Se un menu viene aperto da un altro menu, chiudere il nuovo menu mostrerà di nuovo il vecchio menu.
I singletons dell'interfaccia utente si registrano con il UIHandler e vengono forniti con un segnale che si attiva quando la sua visibilità dovrebbe cambiare.
Ulteriori letture
Dalla dettagliata panoramica del progetto Pianta, potresti voler esplorare le seguenti guide che vanno più in profondità su concetti e argomenti correlati.
- Modello client-server — Una panoramica del modello client-server in Roblox.
- Eventi e richiami remoti — Tutto su eventi e richiami remoti della rete per la comunicazione attraverso il confine client-server.
- UI — Dettagli sugli oggetti dell'interfaccia utente e sul design su Roblox.