Rośliny to doświadczenie referencyjne, w którym gracze sadzą i podlewają nasiona, aby później móc zbierać i sprzedawać powstałe rośliny.

Projekt koncentruje się na typowych przypadkach użycia, które możesz napotkać podczas tworzenia doświadczenia na Robloxie. Gdzie to możliwe, znajdziesz uwagi dotyczące kompromisów, wyrzeczeń i uzasadnienia różnych wyborów implementacyjnych, abyś mógł podjąć najlepszą decyzję dla swoich własnych doświadczeń.
Pobierz plik
- Przejdź do strony doświadczenia Rośliny.
- Kliknij przycisk ⋯ i Edytuj w Studio.
Przypadki użycia
Rośliny obejmują następujące przypadki użycia:
- Utrzymywanie danych sesji i danych gracza
- Zarządzanie widokiem UI
- Sieciowanie klient-serwer
- Doświadczenie dla nowych użytkowników (FTUE)
- Zakupy za pomocą waluty twardej i miękkiej
Ponadto projekt ten rozwiązuje węższe zestawy problemów, które mają zastosowanie w wielu doświadczeniach, w tym:
- Personalizacja obszaru w grze związanego z graczem
- Zarządzanie prędkością ruchu postaci gracza
- Tworzenie obiektu, który podąża za postaciami
- Wykrywanie, w której części świata znajduje się postać
Zwróć uwagę, że w tym doświadczeniu jest kilka przypadków użycia, które są zbyt małe, zbyt niszowe lub nie demonstrują rozwiązania interesującego wyzwania projektowego; te nie są omawiane.
Struktura projektu
Pierwszą decyzją przy tworzeniu doświadczenia jest ustalenie, jak zorganizować projekt, co głównie obejmuje, gdzie umieścić konkretne instancje w modelu danych i jak zorganizować i uporządkować punkty wejścia zarówno dla kodu klienta, jak i serwera.
Model danych
Poniższa tabela opisuje, w których usługach modelu danych umieszczane są instancje.
| Usługa | Rodzaje instancji |
|---|---|
| Workspace | Zawiera statyczne modele reprezentujące świat 3D, konkretnie części świata, które nie należą do żadnego gracza. Nie musisz dynamicznie tworzyć, modyfikować ani niszczyć tych instancji w czasie wykonywania, więc akceptowalne jest pozostawienie ich tutaj. Istnieje również pusta Folder, do której modele farm graczy będą dodawane w czasie wykonywania. |
| Lighting | Efekty atmosferyczne i oświetleniowe. |
| ReplicatedFirst | Zawiera najmniejszy możliwy podzbiór instancji potrzebnych do wyświetlenia ekranu ładowania i zainicjowania doświadczenia. Im więcej instancji umieszczonych jest w ReplicatedFirst, tym dłuższe jest oczekiwanie na ich replikację przed uruchomieniem kodu w ReplicatedFirst.
|
| ReplicatedStorage | Służy jako kontener do przechowywania wszystkich instancji, do których dostęp jest wymagany zarówno po stronie klienta, jak i serwera.
|
| ServerScriptService | Zawiera Script, służący jako punkt wejścia dla całego kodu po stronie serwera w projekcie. |
| ServerStorage | Służy jako kontener do przechowywania wszystkich instancji, które nie muszą być replikowane do klienta.
|
| SoundService | Zawiera obiekty Sound używane do efektów dźwiękowych w doświadczeniu. Pod SoundService te obiekty Sound nie mają pozycji i nie są symulowane w przestrzeni 3D. |
Punkty wejścia
Większość projektów organizuje kod w wewnętrznych ModuleScripts które można importować w całej bazie kodowej. ModuleScripts są wielokrotnego użytku, ale nie wykonują się same; muszą być importowane przez Script lub LocalScript. Wiele projektów Roblox będzie miało dużą liczbę obiektów Script i LocalScript, z których każdy dotyczy jakiegoś zachowania lub konkretnego systemu w doświadczeniu, tworząc wiele punktów wejścia.
W mikrogrze Rośliny zastosowano inne podejście poprzez jeden LocalScript, który jest punktem wejścia dla całego kodu klienta i jeden Script, który jest punktem wejścia dla całego kodu serwera. Poprawne podejście dla twojego projektu zależy od twoich wymagań, ale jeden punkt wejścia zapewnia lepszą kontrolę nad kolejnością, w jakiej uruchamiane są systemy.
Poniższe listy opisują kompromisy obu podejść:
- Jeden Script i jeden LocalScript odpowiadają za kod serwera i klienta.
- Większa kontrola nad kolejnością uruchamiania różnych systemów, ponieważ cały kod jest inicjowany z jednego skryptu.
- Możliwość przekazywania obiektów poprzez odniesienie między systemami.
Architektura systemów na wysokim poziomie
Główne systemy w projekcie są szczegółowo opisane poniżej. Niektóre z tych systemów są zasadniczo bardziej złożone niż inne, a w wielu przypadkach ich funkcjonalność jest zastrzeżona w hierarchii innych klas.

Każdy z tych systemów jest "singletonem", co oznacza, że nie jest klasą instanciowalną, która jest inicjowana przez odpowiedni skrypt start na kliencie lub serwerze. Możesz przeczytać więcej o wzorcu singletona później w tym przewodniku.
Serwer
Poniższe systemy są związane z serwerem.
| System | Opis |
|---|---|
| Sieć |
|
| PlayerDataServer |
|
| Rynek |
|
| CollisionGroupManager |
|
| FarmManagerServer |
|
| ContainerObiektówGracza |
|
| TagGraczy |
|
| FtueManagerServer |
|
| CharacterSpawner |
|
Klient
Poniższe systemy są związane z klientem.
| System | Opis |
|---|---|
| Sieć |
|
| PlayerDataClient |
|
| MarketClient |
|
| LocalWalkJumpManager |
|
| FarmManagerClient |
|
| UISetup |
|
| FtueManagerClient |
|
| CharacterSprint |
|
Komunikacja klient-serwer
Większość doświadczeń w Robloxie wiąże się z pewnym elementem komunikacji między klientem a serwerem. Może to obejmować wymaganie przez klienta od serwera wykonania określonej akcji i serwer replikujący aktualizacje do klienta.
W tym projekcie komunikacja klient-serwer jest zachowywana tak ogólnie, jak to możliwe, ograniczając użycie obiektów RemoteEvent i RemoteFunction w celu zmniejszenia liczby specjalnych zasad do śledzenia. Ten projekt używa następujących metod, w kolejności preferencji:
- Replikacja przez system danych graczy.
- Replikacja przez atrybuty.
- Replikacja przez tagi.
- Komunikacja bezpośrednia za pomocą modułu Sieć.
Replikacja przez system danych graczy
system danych graczy pozwala na powiązanie danych z graczem, które utrzymują się pomiędzy sesjami zapisu. System ten zapewnia replikację z klienta do serwera i zestaw API, które można wykorzystać do zapytywania o dane i subskrybowania zmian, co czyni go idealnym do replikacji zmian w stanie gracza z serwera do klienta.
Na przykład, zamiast wyzwalać przypisany UpdateCoins RemoteEvent aby poinformować klienta, ile monet ma, możesz wywołać następujące i pozwolić klientowi subskrybować za pomocą zdarzenia PlayerDataClient.updated.
PlayerDataServer.setValue(player, "coins", 5)
Oczywiście to jest użyteczne tylko dla replikacji serwera do klienta oraz dla wartości, które chcesz, aby się utrzymywały pomiędzy sesjami, ale to dotyczy zaskakującej liczby przypadków w projekcie, w tym:
- Aktualny etap FTUE
- Ekwipunek gracza
- Ilość monet, które ma gracz
- Stan farmy gracza
Replikacja przez atrybuty
W sytuacjach, gdy serwer musi zreplikować niestandardową wartość do klienta, która jest specyficzna dla danej Instance, możesz użyć atrybutów. Roblox automatycznie replikuje wartości atrybutów, więc nie musisz utrzymywać żadnych ścieżek kodu do replikacji stanu związanego z obiektem. Inną zaletą jest to, że ta replikacja zachodzi równocześnie z instancją.
To jest szczególnie przydatne dla instancji tworzonych w czasie wykonywania, ponieważ atrybuty ustawione na nowej instancji przed jej rodzicem do modelu danych zreplikują się atomowo razem z instancją. To omija jakąkolwiek potrzebę pisania kodu do "czekiwania" na dodatkowe dane, które mają zostać zreplikowane za pomocą RemoteEvent lub StringValue.
Możesz również bezpośrednio odczytać atrybuty z modelu danych, zarówno z klienta, jak i serwera, za pomocą metody GetAttribute() i subskrybować zmiany za pomocą metody GetAttributeChangedSignal(). W projekcie Rośliny to podejście jest używane do replikacji bieżącego statusu roślin do klientów.
Replikacja przez tagi
CollectionService pozwala na zastosowanie tagu tekstowego do Instance. To jest przydatne do kategoryzowania instancji i replikacji tej kategoryzacji do klienta.
Na przykład, tag CanPlant jest stosowany po stronie serwera, aby oznaczyć klientowi, że dany doniczka może przyjąć roślinę.
Komunikacja bezpośrednia za pomocą modułu sieciowego
W sytuacjach, w których żadna z wcześniejszych opcji nie ma zastosowania, możesz użyć niestandardowych wywołań sieciowych przez moduł Sieć. To jest jedyna opcja w projekcie, która pozwala na komunikację klient-serwer i jest zatem najbardziej użyteczna do przesyłania żądań klienta i otrzymywania odpowiedzi serwera.
Rośliny używają bezpośrednich wywołań sieciowych do różnych żądań klienta, w tym:
- Podlewanie rośliny
- Sianie nasiona
- Zakup przedmiotu
Wadą tego podejścia jest to, że każda indywidualna wiadomość wymaga pewnej niestandardowej konfiguracji, co może zwiększyć złożoność projektu, chociaż to zostało uniknięte, gdzie to możliwe, szczególnie w przypadku komunikacji serwera z klientem.
Klasy i singletony
Klasy w projekcie Rośliny, podobnie jak instancje w Robloxie, mogą być tworzone i niszczone. Ich składnia klas jest inspirowana idiomatycznym podejściem Lua do programowania obiektowego z szeregiem zmian umożliwiających wsparcie dla ściślejszej walidacji typów.
Instancjonowanie
Wiele klas w projekcie jest związanych z jedną lub więcej Instances. Obiekty danej klasy są tworzone przy użyciu metody new() zgodnie z tym, jak instancje są tworzone w Robloxie przy użyciu Instance.new().
Wzorzec ten jest ogólnie stosowany dla obiektów, w których klasa ma fizyczną reprezentację w modelu danych, a klasa rozszerza swoją funkcjonalność. Dobrym przykładem jest BeamBetween, które tworzy obiekt Beam pomiędzy dwoma danymi obiektami Attachment i utrzymuje te załączniki w odpowiedniej orientacji, aby wiązka zawsze była skierowana do góry. Te instancje mogą być klonowane z prefabrykowanej wersji w ReplicatedStorage lub przekazywane do new() jako argument i przechowywane wewnątrz obiektu pod self.
Odpowiadające instancje
Jak wspomniano powyżej, wiele klas w tym projekcie ma reprezentację modelu danych, instancję, która odpowiada klasie i jest przez nią manipulowana.
Zamiast tworzyć te instancje, gdy obiekt klasy jest instancjonowany, kod ogólnie decyduje się na Clone() prefabrykowaną wersję Instance przechowywaną w ReplicatedStorage lub ServerStorage. Chociaż możliwe byłoby serializowanie właściwości tych instancji i tworzenie ich od podstaw w funkcjach new() klasy, zrobienie tego sprawiłoby, że edytowanie obiektów byłoby bardzo uciążliwe i trudniejsze do zrozumienia dla czytelnika. Dodatkowo, klonowanie instancji zazwyczaj jest szybszą operacją niż tworzenie nowej instancji i dostosowywanie jej właściwości w czasie wykonywania.
Kompozycja
Chociaż dziedziczenie jest możliwe w Luau przy użyciu metatable, projekt decyduje się na pozwolenie klasom na rozbudowę poprzez kompozycję. Łącząc klasy poprzez kompozycję, "obiekt dziecka" jest instancjonowany w metodzie new() klasy i jest włączony jako członek pod self.
Aby zobaczyć przykład tego w akcji, zobacz klasę CloseButton, która oplata klasę Button.
Czyszczenie
Podobnie jak instancja Instance może być zniszczona za pomocą metody Destroy(), klasy, które mogą być instancjonowane, również mogą być niszczone. Metoda destrukcyjna dla klas projektowych to destroy() z małą literą d dla spójności camelCase w metodach bazy kodu, a także aby rozróżnić klasy projektu od instancji Roblox.
Rolą metody destroy() jest zniszczenie wszelkich instancji utworzonych przez obiekt, odłączenie wszelkich połączeń oraz wywołanie destroy() na wszelkich obiektach potomnych. Jest to szczególnie ważne dla połączeń, ponieważ instancje z aktywnymi połączeniami nie są czyszczone przez zbieracz śmieci Luau, nawet jeśli żadne odniesienia do instancji ani połączeń z instancją nie pozostają.
Singletony
Singletony, jak sama nazwa wskazuje, to klasy, dla których może istnieć tylko jeden obiekt. Są one odpowiednikiem Usług w Robloxie. Zamiast przechowywać odniesienie do obiektu singletona i przekazywać je w kodzie Luau, Rośliny korzystają z faktu, że wymaganie ModuleScript pamięta swoją zwróconą wartość. Oznacza to, że wymaganie tego samego singletonu ModuleScript z różnych miejsc konsekwentnie zwraca ten sam obiekt. Jedynym wyjątkiem od tej reguły jest to, że różne środowiska (klient lub serwer) uzyskują dostęp do ModuleScript.
Singletony różnią się od klas instanciowalnych tym, że nie mają metody new(). Zamiast tego, obiekt wraz z jego metodami i stanem jest zwracany bezpośrednio przez ModuleScript. Ponieważ singletony nie są instancjonowane, składnia self nie jest używana, a metody są wywoływane kropką (.) zamiast dwukropka (:).
Ścisła inferencja typów
Luau wspiera stopniowe typowanie, co oznacza, że możesz swobodnie dodawać opcjonalne definicje typów do niektórego lub całego kodu. W tym projekcie stosuje się ścisłe sprawdzanie typów dla każdego skryptu. To jest najbardziej restrykcyjna opcja dla narzędzia Analiza skryptów w Robloxie i tym samym najprawdopodobniej wykryje błędy typów przed czasem wykonywania.
Typowana składnia klas
Ugruntowane podejście do tworzenia klas w Lua jest dobrze udokumentowane, jednak nie jest dobrze przystosowane do silnego typowania Luau. W Luau, najszybszym podejściem do uzyskania typu klasy jest metoda typeof():
type ClassType = typeof(Class.new())
To działa, ale nie jest zbyt użyteczne, gdy twoja klasa jest inicjowana wartościami, które istnieją tylko w czasie wykonywania, na przykład obiektami Player. Dodatkowo, założenie przyjęte w idiomatycznej składni klas Lua mówi, że zadeklarowanie metody na klasie self zawsze będzie instancją tej klasy; nie jest to założenie, które silnik inferencji typów może przyjąć.
Aby wspierać ścisłą inferencję typów, projekt Rośliny używa rozwiązania, które różni się od idiomatycznej składni klas Lua w kilku sposobach, z których niektóre mogą wydawać się nieintuicyjne:
- Definicja self jest powielona, zarówno w deklaracji typu, jak i w konstruktorze. To wprowadza obciążenie związane z utrzymywaniem kodu, ale ostrzeżenia będą sygnalizowane, jeśli dwie definicje staną się niezgodne.
- Metody klas są deklarowane kropką, aby self można było jawnie zadeklarować jako typ ClassType. Metody nadal mogą być wywoływane za pomocą dwukropka, jak się spodziewano.
--!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
Rzuć typy po warunkach logicznych
W momencie pisania, typ wartości nie jest zredukowany po instrukcji warunkowej. Na przykład, po warunku poniżej, typ optionalParameter nie jest zredukowany do number.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
print(optionalParameter + 1)
end
Aby temu zaradzić, po tych warunkach tworzone są nowe zmienne z ich typem jawnie rzuconym.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
Przechodzenie przez hierarchie modelu danych
W niektórych przypadkach, baza kodu musi przejść przez hierarchię modelu danych drzewa obiektów, które są tworzone w czasie wykonywania. To stanowi interesujące wyzwanie dla sprawdzania typów. W momencie pisania, nie można zdefiniować ogólnej hierarchii modelu danych jako typu. W związku z tym istnieją przypadki, gdzie jedyną dostępną informacją o typie dla struktury modelu danych jest typ instancji najwyższego poziomu.
Jednym z podejść do tego wyzwania jest rzuć na any i następnie udoskonalić. Na przykład:
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
end
Problem z tym podejściem polega na tym, że wpływa na czytelność. Zamiast tego, projekt używa ogólnego modułu o nazwie getInstance do przeszukiwania hierarchii modelu danych, który wewnętrznie rzuca na any.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
W miarę jak rozumienie modelu danych przez silnik typów ewoluuje, możliwe jest, że wzorce takie jak ten nie będą już konieczne.
Interfejs użytkownika
Rośliny obejmują różnorodne złożone i proste interfejsy użytkownika 2D. Obejmują one nieinteraktywne przedmioty wyświetlacza (HUD), takie jak licznik monet, oraz skomplikowane interaktywne menu, takie jak sklep.
Podejście do UI
Można w luźny sposób porównać interfejs użytkownika Roblox UI do DOM HTML, ponieważ jest to hierarchia obiektów, która opisuje, co użytkownik powinien widzieć. Podejścia do tworzenia i aktualizacji interfejsu użytkownika w Robloxie są ogólnie podzielone na imperatywne i deklaratywne praktyki.
| Podejście | Zalety i wady |
|---|---|
| Imperatywne | W podejściu imperatywnym UI jest traktowane jak każda inna hierarchia instancji w Robloxie. Struktura UI jest tworzona przed czasem wykonywania w Studio i dodawana do modelu danych, zazwyczaj bezpośrednio w StarterGui. Następnie, w czasie wykonywania, kod manipuluje konkretnymi częścią UI, aby odzwierciedlić stan wymagany przez twórcę. To podejście ma kilka zalet. Możesz stworzyć UI od podstaw w Studio i przechowywać je w modelu danych. To jest proste i wizualne doświadczenie edytorskie, które może przyspieszyć tworzenie UI. Ponieważ imperatywny kod UI tylko dotyczy tego, co musi być zmienione, również sprawia, że proste zmiany w UI są łatwe do zaimplementowania. Znaczną wadą jest to, że ponieważ podejścia imperatywne wymagały wdrożenia stanu w formie transformacji, skomplikowane reprezentacje stanu mogą stać się bardzo trudne do znalezienia i debugowania. Typowe jest pojawienie się błędów podczas rozwijania kodu UI imperatywnego, szczególnie gdy stan i UI stają się niesynchronizowane z powodu interakcji wielu aktualizacji w nieoczekiwanej kolejności. Kolejnym wyzwaniem w podejściu imperatywnym jest to, że trudniej jest podzielić UI na sensowne komponenty, które mogą być zadeklarowane raz i używane wielokrotnie. Ponieważ całe drzewo UI jest zadeklarowane w czasie edycji, wspólne wzorce mogą być powtarzane w wielu częściach modelu danych. |
| Deklaratywne | W podejściu deklaratywnym pożądany stan instancji UI jest explicite zadeklarowany, a efektywna implementacja tego stanu jest abstrakcyjna przez biblioteki takie jak Roact czy Fusion. Zaletą tego podejścia jest to, że implementacja stanu staje się trywialna i wystarczy opisać, jak ma wyglądać twoje UI. To zdecydowanie ułatwia identyfikowanie i rozwiązywanie błędów. Kluczową wadą jest konieczność zadeklarowania całego drzewa UI w kodzie. Biblioteki, takie jak Roact i Fusion, mają składnię, która to ułatwia, ale nadal jest to czasochłonny proces i mniej intuicyjne doświadczenie edytorskie podczas tworzenia UI. |
Rośliny używa podejścia imperatywnego w przekonaniu, że bezpośrednie pokazywanie transformacji daje skuteczniejszy przegląd tego, jak UI jest tworzone i manipulowane w Robloxie. To nie byłoby możliwe w podejściu deklaratywnym. Niektóre powtarzane struktury i logika UI są również zastrzeżone w wielokrotnego użytku komponentach, aby uniknąć typowej pułapki w projektowaniu UI imperatywnego.
Architektura na wysokim poziomie

Warstwa i komponenty
W Rośliny, wszystkie struktury UI to albo Layer, albo Component.
- Layer jest zdefiniowana jako singleton grupujący, który owrapowuje prefabrykowane struktury UI w ReplicatedStorage. Warstwa może zawierać szereg komponentów lub może całkowicie kapsułkować swoją własną logikę. Przykłady warstw to menu ekwipunku lub wskaźnik liczby monet w wyświetlaczu.
- Component jest wielokrotnego użytku elementem UI. Gdy nowy obiekt komponentu jest instancjonowany, klonuje prefabrykowany szablon z ReplicatedStorage. Komponenty mogą same w sobie zawierać inne komponenty. Przykłady komponentów to ogólna klasa przycisku lub koncepcja listy przedmiotów.
Zarządzanie widokiem
Typowym problemem zarządzania UI jest zarządzanie widokiem. Projekt ten ma szereg menu i elementów HUD, z których niektóre odpowiadają na interakcje użytkowników, a staranne zarządzanie, kiedy są widoczne lub aktywne, jest wymagane.
Rośliny podchodzą do tego problemu z systemem UIHandler, który zarządza tym, kiedy warstwa UI powinna być lub nie być widoczna. Wszystkie warstwy UI w doświadczeniu są klasyfikowane jako HUD lub Menu i ich widoczność jest zarządzana przez następujące zasady:
- Stan włączony warstw Menu i HUD może być przełączany.
- Aktywne warstwy HUD są pokazywane tylko wtedy, gdy żadna z warstw Menu nie jest aktywna.
- Aktywne warstwy Menu są przechowywane w stosie, a tylko jedna warstwa Menu jest widoczna w danym momencie. Gdy warstwa Menu jest aktywna, jest wstawiana na początku stosu i wyświetlana. Gdy warstwa Menu jest dezaktywowana, jest usuwana ze stosu, a następnie wyświetlana jest następna aktywna warstwa Menu w kolejce.
To podejście jest intuicyjne, ponieważ pozwala na nawigację po menu przy użyciu historii. Jeśli jedno menu jest otwierane z innego menu, zamykanie nowego menu ponownie pokaże stare menu.
Singletony warstw UI rejestrują się w UIHandler i otrzymują sygnał, który wyzwala się, gdy ich widoczność powinna się zmienić.
Dalsze czytanie
Z tego dokładnego przeglądu projektu Rośliny, możesz chcieć badać następujące przewodniki, które zgłębiają się w pokrewne koncepcje i tematy.
- Model klient-serwer — Przegląd modelu klient-serwer w Robloxie.
- Zdarzenia zdalne i wywołania zwrotne — Wszystko o zdalnych zdarzeniach sieciowych i wywołaniach zwrotnych do komunikacji na granicy klient-serwer.
- UI — Szczegóły dotyczące obiektów i projektowania interfejsu użytkownika w Robloxie.