Plantacja to doświadczenie referencyjne, w którym gracze sadzą i wodzą nasiona, aby później mogli zebrać i sprzedać wynikające z nich rośliny.
Projekt koncentruje się na wspólnych przypadkach użycia, które możesz napotkać podczas rozwijania doświadczenia na Roblox. Gdzie to mające zastosowanie, znajdziesz notatki na temat tradeoffów, kompromisów i uzasadnienia różnych wyborów implementacji, abyś mógł podjąć najlepszą decyzję dla własnych doświadczeń.
Zdobądź plik
- Przejdź do strony doświadczenia Roślinka.
- Kliknij przycisk ⋯ i Edytuj w Studio .
Użyj przypadków
Roślinka pokrywa następujące przypadki użycia:
- Trwałość sesji danych i gracza
- Zarządzanie widokiem interfejsu
- Sieci klient-serwer
- Pierwszy raz użytkownika doświadczenie (FTUE)
- Kupowanie walut twardych i miękkich
Dodatkowo, ten projekt rozwiązuje węższe zestawy problemów, które mają zastosowanie do wielu doświadczeń, w tym:
- Personalizacja obszaru w miejscu, który jest związany z graczem
- Zarządzanie prędkością poruszania się gracza
- Tworzenie obiektu, który podąża za znakami
- Wykrywanie, w której części świata znajduje się postać
Uwaga, że istnieje kilka przypadków użycia w tym doświadczeniu, które są zbyt małe, zbyt niszowe lub nie pokazują rozwiązania na ciekawy problem projektowy; nie są one pokryte.
結構프로젝트
Pierwszą decyzją przy tworzeniu doświadczenia jest zdecydowanie, jak strukturować projekt, który w dużej mierze obejmuje to, gdzie umieścić konkretne instancje w modelu danych i jak zorganizować i zstrukturyzować punkty dostępu dla obu kodu klienta i serwera.
Model danych
Poniższy tabela opisuje które usługi kontenerowe w instancjach modelu danych są umieszczone.
Usługa | Typy instancji |
---|---|
Workspace | Zawiera statyczne modele przedstawiające świat 3D, w szczególności części świata, które nie należą do żadnego gracza. Nie musisz dynamycznie tworzyć, modyfikować lub niszczyć tych instancji podczas uruchomienia, więc można je tutaj zostawić. Istnieje również pusty Class.Folder , do którego zostaną dodane modele farm |
Lighting | Efekty atmosferyczne i oświetleniowe. |
ReplicatedFirst | Zawiera najmniejszy podstawowy podzestaw instancji wymaganych do wyświetlenia ekranu ładowania i inicjatyzacji gra. Im więcej instancji zostanie umieszczonych w ReplicatedFirst, tym dłużej będzie czekać na ich replikację przed kodem w ReplicatedFirst.
|
ReplicatedStorage | Służy jako pojemnik do przechowywania dla wszystkich instancji, dla których wymagany jest dostęp zarówno na klienta, jak i na serwer.
|
ServerScriptService | Zawiera Script służący jako punkt wejścia dla wszystkiego kodu stron serwera w projekcie. |
ServerStorage | Służy jako pojemnik do przechowywania wszystkich instancji, które nie muszą być replikowane na klienta. W katalogu Instancje znajduje się model Gospodarstwo. Kopia tego jest umieszczona w 0> Class.Workspace0>, gdy gracz dołącza do gry, gdzie zostanie ona skopiowana do wszystkich graczy. > |
SoundService | Zawiera obiekty Sound używane do efektów dźwiękowych w gra. Pod SoundService , te obiekty Sound nie mają pozycji i nie są simulowane w przestrzeni 3D. |
Punkty dostępu
Większość projektów organizuje kod w ModuleScripts , który może być importowany w całej bazie kodu. ModuleScripts
Dla mikrogry Plant zaimplementowano inny podejście poprzez pojedynczy LocalScript , który jest punktem wejścia dla wszystkiego kodu klienta, i pojedynczy Script , który jest punktem wejścia dla wszystkiego kodu serwera. Poprawny podejście dla Twojego projektu zależy od Twoich wym
Poniższe listy opisują tradeoffy obu podejść:
- Jeden Script i jeden LocalScript kryształ serwera i kod klienta.
- Większa kontrola nad kolejnością uruchamiania różnych systemów, ponieważ wszystkie kody są inicjalizowane z jednego skryptu.
- Można przesyłać obiekty poprzez odniesienie między systemami.
Architektura systemów wysokiego poziomu
Najwyższy poziomowe systemy w projekcie są szczegółowo opisane poniżej. Niektóre z tych systemów są znacznie bardziej złożone niż inne, a w wielu przypadkach ich funkcjonalność jest abstrakcyjna w hierarchii innych klas.
Każdy z tych systemów jest "singletonem", ponieważ jest to klasa nieinstytantialna, która zostaje zainicjatyzowana przez odpowiedni skrypt start. Możesz przeczytać więcej o wzorcu singtona później w tej przewodniku.
Serwer
Poniższe systemy są powiązane z serwerem.
System | Opis |
---|---|
Sieć | Wszystkie instancje Class.RemoteEvent i Class.RemoteFunction. 0> 1> Wyświetla metody wysyłania i słuchania wiadomości z klienta. 1>
|
PlayerDataServer | Zapisywanie i ładowanie trwałych danych gracza przy użyciu Class.DataStoreService . >
|
Rynek |
|
KolisionGroupManager |
|
FarmManagerServer |
|
ContainerPlayerObjektów gracza |
|
TagPlayers |
|
FtueManagerServer ] |
|
Generator postaci |
|
Klient
Poniższe systemy są powiązane z klientem.
System | Opis |
---|---|
Sieć |
|
ClientPlayerData |
|
Klient Marketowy |
|
Lokalny Menedżer Skoku |
|
Kliент FarmManager | Słucha szczególnych tagów Class.CollectionService , które są stosowane do instancji, i tworzy "komponenty" zachowań przy zastosowaniu tych instancji. Komponent odnosi się do klasy, która jest tworzona, gdy tag Class.CollectionService jest dodany do instancji i zostanie usunięty; te są uży |
Ustawienia interfejsu |
|
FtueManagerClient |
|
Bieg znaków |
|
Komunikacja klient-serwer
Większość doświadczeń Roblox wiąże się z pewnym elementem komunikacji między klientem a serwerem. To może obejmować prośbę klienta o wykonanie pewnej akcji i serwer replikując aktualizacje do klienta.
W tym projekcie komunikacja klient-serwer jest możliwa w takim stopniu, jak to możliwe, ograniczając użycie RemoteEvent i RemoteFunction obiektów, aby zmniejszyć ilość specjalnych zasad do śledzenia. Ten projekt używa następujący metody, w kolejności preferencji:
- Replikacja za pośrednictwem zespółu danych gracza.
- Replikacja za pośrednictwem cech .
- Replikacja za pośrednictwem tagów .
- Wiadomości pośrednio poprzez moduł Sieć .
Replikacja za pośrednictwem systemu danych gracza
system danych gracza umożliwia łączenie danych z graczem, który trwa między sesjami zapisu. Ten system zapewnia replikację od klienta do serwera i zestaw API, które można użyć do zapisu danych i subskrypcji zmian, co czyni go idealnym do replikacji zmian w stanie gracza z serwera na klienta.
Na przykład, zamiast wystrzelić niestandardowy UpdateCoins``Class.RemoteEvent aby powiedzieć klientowi, ile monet ma, możesz zadzwonić następujące i pozwolić klientowi subskrybować go poprzez wydarzenie PlayerDataClient.updated .
PlayerDataServer:setValue(player, "coins", 5)
Oczywiście, jest to tylko użyteczne dla replikacji serwer-do-klienta i wartości, które chcesz utrzymać między sesjami, ale dotyczy to zadziwiającej liczby przypadków w projekcie, w tym:
- Obecny etap FTUE
- wyposażeniegracza
- Ilość monet, które gracz ma
- Stan farmy gracza
Replikacja za pośrednictwem cech
W sytuacjach, w których serwer musi replikować niestandardową wartość dla 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 związanych ze stanem związanym z danym obiektu. Innym plusem
Jest to szczególnie przydatne dla instancji utworzonych podczas uruchomienia, ponieważ zmienne ustawione na nowej instancji przed jej zaimportowaniem do modelu danych będą replikowane atomowo z instancji samą w sobie. To unika wszelkiej potrzeby pisania kodu "czekaj" na dodatkowe dane do zaimportowania poprzez RemoteEvent lub StringValue .
Możesz również bezpośrednio czytać właściwości z modelu danych, zarówno z klienta, jak i serwera, za pomocą metody GetAttribute() i subskrybować zmiany za pomocą metody GetAttributeChangedSignal(). W projekcie Plant metoda ta jest używ
Replikacja za pomocą Tagów
CollectionService pozwala na zastosowanie etykiety stron do Instance . To jest użyteczne do kategorystyki instancji i replikacji tej kategorii na klienta.
Na przykład tag CanPlant jest zastosowany na serwerze, aby określić dla klienta, że dany gąbka może otrzymać roślinę.
Wysyłanie bezpośrednio za pośrednictwem modułu sieci
Dla sytuacji, w których żadne z wcześniejszych opcji nie ma zastosowania, możesz używać niestandardowych wezwów sieciowych poprzez moduł Sieć . Jest to jedyna opcja w projekcie, która umożliwia komunikację klient-serwer i jest więc najbardziej przydatna do odbierania żądań klientów i otrzymywania odpowiedzi serwera.
Roślinka używa bezpośrednich wezwów sieciowych dla różnych wniosków klienta, w tym:
- Podlewanie rośliny
- Zasadzanie nasiona
- Kupowanie przedmiotu
Wadą tego podejścia jest to, że każda pojedyncza wiadomość wymaga nieco niestandardowej konfiguracji, co może zwiększyć złożoność projektu, choćby to zostało uniknięte, szczególnie dla komunikacji serwer-klient.
Klasy i Singletons
Klasy w projekcie Plant, podobnie jak instancje na Roblox, można tworzyć i usuwać. Jego słownik klas jest zainspirowany przez idiomatyczny podejście Lua do Programowanie obiektowe z kilkoma zmianami włączającymi wsparcie dla ściślej sprawdzenia.
Instalacja
Wiele klas w projekcie jest powiązanych z jedną lub większą liczbą Instances. Objetoce jednej danej klasy są tworzone przy użyciu metody new(), zgodnie z tym, jak instancje są tworzone w Roblox przy użyciu Instance.new().
Ten wzór jest ogólnie używany dla obiektów, w których klasa ma fizyczną reprezentację w modelu danych i klasa rozszerza swoją funkcjonalność. Dobrym przykładem jest Beam
Zgadzające się instancje
Jak wspomniano powyżej, wiele klas w tym projekcie ma przedstawienie modelu danych, instancję, która odpowiada na klasę i jest przez nią manipulowana.
Zamiast tworzyć te instancje, gdy obiekt klasy zostanie zainstalowany, kod generalnie wybiera Clone() w wersji zapakiet
Skład
Chociaż dziedzictwo jest możliwe w Lua używając metatabeli, projekt zdecydował się zamiast tego pozwolić klasom rozciągnąć się nawzajem poprzez kompozycję . Gdy łączymy klasy poprzez kompozycję, obiekt "dziecka" jest instancyjowany w metodzie new() klasy i jest włączony jako członek pod
Aby uzyskać przykład tego w akcji, zobacz klasę CloseButton, która otacza klasę Button.
Czyszczenie
Podobnie jak można zniszczyć Class.Instance za pomocą metody Class.Instance:Destroy() , klasy, które można zainstalować, również mogą być zniszczone. Metoda dezstruktora dla klas projektowych jest destruct() z niższą dużą Destroy() dla camelCase
Rolem metody destroy() jest zniszczenie dowolnych instancji stworzonych przez obiekt, odłączenie dowolnych połączeń i wezwanie destroy() na dowolnym dziecięcym obiektach. Jest to szczególnie ważne dla połączeń, ponieważ instancje z aktywnymi połączeniami nie są czyszczone przez zbieracz kosza Lua, nawet jeśli nie pozostają żadne odniesienia do
Jedynaki
Singletons, jak nazwa sugeruje, są klasami dla których tylko jeden obiekt może kiedykolwiek istnieć. Są one równivalentem Roblox's Usługi . Zami
Singletons są odróżnione od natychmiastowych klas przez to, że nie mają new() metody. Zamiast tego obiekt wraz ze swoimi metodami i stanem jest bezpośrednio zwracany poprzez ModuleScript . Nie jest używana więc Syntax self , a metody są zamiast
Ścisłe wiązanie typu
Luau wspiera stopniowe wtyczanie, co oznacza, że możesz dodać opcjonalne definicje typu do niektórych lub wszystkich swoich kodów. W tym projekcie używany jest strict typcheckingu dla każdego skryptu. Jest to najmniej uprawnione opcja dla narzędzia do analizy kodu Roblox i więc najbardziej prawdopodobne, aby złapać błędy typ
Wykazany Syntetyczny Klasy
Najlepiej ustanowiony podejście do tworzenia klas w Lua jest dobrze opisowany, jednak nie nadaje się do silnego wpisywania Luau. W Lua najprostszym podejściem do uzyskania typu klasy jest metoda typeof():
type ClassType = typeof(Class.new())
To działa, ale nie jest to bardzo użyteczne, gdy twój klas jest inicjowany z wartościami, które istnieją tylko podczas uruchomienia, na przykład Player obiektów. Ponadto założenie stworzone w idiomaticznej klasy Lua jest, że deklarując metodę na klasie self zawsze będzie to instancja tego klasu; to nie jest założenie,
Aby popierać rygorystyczną infrastrukturę typu, projekt Plant używa rozwiązania, które różnią się od idiomaticznej klasy Lua w kilku sposób, niektóre z których mogą wydawać się nieintuicyjne:
- Definicja self jest duplikowana zarówno w deklaracji typu, jak i w konstruktorze. To wprowadza obciążenie utrzymywalności, ale ostrzeżenia będą wyświetlać się, jeśli dwa definicje spadną z synchro z jedną z nich.
- Modyfikatory klasy są deklarowane za pomocą kropki, więc self można wyraźnie deklarować, że jest typu ClassType . Modyfikatory nadal można wzywać za pomocą kolonki, jak oczekiwano.
--!surowy
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
Wykorzystywanie typów po logicznych filtrach
Podczas pisania typu wartości nie jest ograniczany po stwierdzeniu warunku. Na przykład, po następującym użyciu optionalParameter typu number nie jest ograniczony do number.
--!surowy
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
print(optionalParameter + 1)
end
Aby to wyeliminować, nowe zmienne są tworzone po tych ochronach z ich wyraźnie określonym typem.
--!surowy
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
Przeglądanie Hierarchii Modelu Danych
W niektórych przypadkach baza kodowa musi przejść przez hierarchię modelu danych drzewa obiektów, które są tworzone podczas uruchomienia. To przedstawia zainteresowujące wyzwanie dla typcheckingu. W momencie pisania nie jest możliwe określenie genérica modelu danych jako wpisywać. W wynikistnieją przypadki, w których jedyną dostępną informacją dla modelu danych jest typ instancjana poziomie góry
Jednym podejściem do tego wyzwania jest rzucanie do any i następnie refinowanie. Na przykład:
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
end
Problem z tym podejściem jest to, że wpływa na czytelność. Zamiast tego projekt używa generycznego modułu nazywanego getInstance do przechwytywania hierarchii modelu danych, które zwracają się do any wewnętrznie.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
Podczas gdy rozwija się zrozumienie silnika typu danych, może się okazać, że wzory takie nie będą już konieczne.
Interfejs użytkownika
Roślinka Zawiera różnorodne interfejsy użytkownika 2D. W tym znajdują się nieinteraktywne głowy uproszczonej animacji (HUD), takie jak koszyk koinowy i złożone interaktywne menu, takie jak robić zakupy.
Podejście UI
Możesz swobodnie porównać Roblox interfejs użytkownika do DOMu HTML, ponieważ jest to hierarchia obiektów, która opisuje to, co użytkownik powinien widzieć. Zakresy do tworzenia i aktualizacji interfejsu użytkownika są podzielone szeroko na praktyki zakazowe i odeklaracyjne .
Podejście | Za i przeciw |
---|---|
Imperatyw | W podejściu imperatywnym UI jest traktowana jak każda inna hier |
Oświadczenie | W podejściu deklaratywnym wymierzone stany instancji UI są deklarowane wyraźnie, a efektywne implementowanie tego stanu jest abstrakcją przez biblioteki, takie jak |
Roślinka używa podejścia imperatywnego pod koncepcją, że pokazanie transformacji bezpośrednio daje bardziej efektywny wgląd na to, jak UI jest tworzona i manipulowana na Roblox. Nie byłoby to możliwe przy podejściu deklaratywnym. Niektóre powtarzające się struktury i zasady logiczne są również abstrakcyjne w komponentach
Wysokozwровadzona Architektura
Warstwa i komponenty
W Plant wszystkie struktury UI są albo Layer lub Component.
- Layer jest zdefiniowany jako poziomowy grupowy singiel, który zapakowuje w ReplicatedStorage prefabrykatyzowane struktury UI. Plikier może zawierać wiele komponentów lub może całkowicie zapsłonować własną logikę. Przykładami plików są menu ekwipunku lub licznik monet w głównym okienku wyświetlania.
- Component jest elementem interfejsu użytkownika ponownie używalnym. Gdy nowy element komponentu zostanie zainstalowany, kopiuje gotowy szablon z ReplicatedStorage. Komponenty mogą samodzielnie zawierać inne komponenty. Przykładami komponentów są genérica klasy przycisku lub koncepcja listy przedmiotów.
Zobacz sterowanie
Zwykłym problemem zarządzania interfejsem jest ustawienie widoku. Ten projekt ma zestaw menu i pozycji HUD, niektóre z których słuchają wejścia użytkownika, a właściwe zarządzanie tymi widokami lub włączaniem ich wymaga.
Roślinka podejmuje ten problem z jego systemem UIHandler , który zarządza, kiedy warstwa UI powinna lub nie powinna być widoczna. Wszystkie warstwy UI w grze są kategoryzowane jako HUD lub 0>Menu0> i ich widoczność jest zarządzana przez następujące zasady:
- Włączone stanowiska Menu i HUD można przełączać.
- Włączone HUD warstwy są pokazane tylko wtedy, gdy nie włączone są Menu warstwy.
- Włączone Menu warstwy są zapisane w pila, a tylko jedna Menu warstwa jest widoczna na raz. Gdy włączone jest Menu warstwa, jest wstawiona na przód pila i wyświetlana. Gdy włączone jest 1> Menu1> warstwa, jest usunię
Ten podejście jest intuicyjny, ponieważ umożliwia menu przeglądane z historią. Jeśli jedno menu zostanie otworzone z innego menu, zamknięcie nowego menu pokaże stary menu ponownie.
Layery interfejsu rejestrują się z UIHandler i otrzymują sygnał, który uruchamia się, gdy jego widoczność powinna się zmienić.
Dalsze czytanie
Z tego kompleksowego przegladu projektu Planta, mozna chciec zbada? nast?puj?ce przewodniki, kt?re id? dalej w g?b?d?ce zwi?zane z koncepcjami i tematami.
- Model klient-serwer — Przeglad modelu klient-serwer w Roblox.
- Zd zdalnych zdarzeń i wezwyków — Wszystko o zdalnych wydarzeniach sieciowych i wezwykach dla komunikacji pomiędzy klientem a serwerem.
- Interfejs użytkownika — szczegóły na temat obiektów interfejsu użytkownika i projektu na Roblox.