植物是一個參考體驗,在這裡玩家可以種植和澆水種子,以便之後收穫並出售隨之而來的植物。

該專案專注於您在開發Roblox體驗時可能遇到的常見用例。在適用的地方,您會看到有關權衡、妥協和各種實現選擇的合理性說明,以便您為自己的體驗做出最佳決策。
獲取文件
- 前往植物體驗頁面。
- 點擊 ⋯ 按鈕並 在Studio中編輯。
使用案例
植物涵蓋以下使用案例:
- 設定資料和玩家資料持久性
- UI視圖管理
- 客戶端-伺服器網路
- 初次使用者體驗(FTUE)
- 硬幣和軟幣購買
此外,該專案還解決了一些適用於許多體驗的狹義問題,包括:
- 自訂與玩家相關的地區
- 管理玩家角色的移動速度
- 創建跟隨角色移動的物體
- 偵測角色所在的世界部分
請注意,這個體驗中有幾個使用案例過於簡小、利基,或未展示有趣設計挑戰的解決方案;這些不包含在內。
專案結構
創建體驗時的第一個決策是如何構建專案,這主要包括在數據模型中放置特定實例的位置以及如何組織和結構用於客戶端和伺服器代碼的進入點。
數據模型
下表描述了數據模型中實例所放置的容器服務。
| 服務 | 實例類型 |
|---|---|
| Workspace | 包含表示三維世界的靜態模型,特別是那些不屬於任何玩家的世界部分。您不需要在運行時動態地創建、修改或銷毀這些實例,因此將它們留在這裡是可以接受的。 這裡還有一個空的 Folder,玩家的農場模型將在運行時添加到此。 |
| Lighting | 氣氛和光照效果。 |
| ReplicatedFirst | 包含顯示加載畫面和初始化體驗所需的最小實例子集。放置在 ReplicatedFirst 中的實例越多,代碼在 ReplicatedFirst 中運行之前的重複等待時間就越長。
|
| ReplicatedStorage | 作為需要在客戶端和伺服器之間訪問的所有實例的存儲容器。
|
| ServerScriptService | 包含作為專案所有伺服器端代碼進入點的 Script。 |
| ServerStorage | 作為所有不需要複製到客戶端的實例的存儲容器。
|
| SoundService | 包含在體驗中的音效使用的 Sound 對象。在 SoundService 下,這些 Sound 對象沒有位置,並且不在三維空間中模擬。 |
進入點
大多數專案按可重用的 ModuleScripts 組織代碼,這些可在整個代碼庫中導入。ModuleScripts 是可重用的,但不能自行執行;需要通過 Script 或 LocalScript 導入。許多Roblox專案將擁有大量的 Script 和 LocalScript 對象,每個對象與體驗中的行為或特定系統相關,從而創建多個進入點。
對於植物微遊戲,實施了一種不同的方式,通過單個 LocalScript 作為所有客戶端代碼的進入點,以及單個 Script 作為所有伺服器代碼的進入點。適合您專案的正確方法取決於您的需求,但單一進入點提供了對系統執行順序的更大控制。
以下列表描述了兩種方法的權衡:
- 一個 Script 和一個 LocalScript 分別覆蓋伺服器和客戶端代碼。
- 對不同系統啟動順序的更大控制,因為所有代碼從單個腳本初始化。
- 可以在系統之間通過引用傳遞對象。
高級系統架構
專案中的頂層系統詳述如下。其中某些系統比其他系統複雜得多,在許多情況下,它們的功能被抽象跨越其他類的層次結構。

每個這樣的系統都是一個“單例”,因為它是一個不可實例化的類,而是由相關的客戶端或伺服器 start 腳本初始化。您可以在本指南稍後的部分了解更多有關單例模式的內容。
伺服器
以下系統與伺服器相關聯。
| 系統 | 描述 |
|---|---|
| 網絡 |
|
| PlayerDataServer |
|
| 市場 |
|
| CollisionGroupManager |
|
| FarmManagerServer |
|
| PlayerObjectsContainer |
|
| TagPlayers |
|
| FtueManagerServer |
|
| CharacterSpawner |
|
客戶端
以下系統與客戶端相關聯。
| 系統 | 描述 |
|---|---|
| 網絡 |
|
| PlayerDataClient |
|
| MarketClient |
|
| LocalWalkJumpManager |
|
| FarmManagerClient |
|
| UISetup |
|
| FtueManagerClient |
|
| CharacterSprint |
|
客戶端-伺服器通訊
大多數Roblox體驗涉及某種形式的客戶端和伺服器之間的通訊。這可能包括客戶端請求伺服器執行某些動作,以及伺服器將更新複製到客戶端。
在該專案中,通過限制RemoteEvent和RemoteFunction對象的使用來保持客戶端-伺服器的通訊儘可能通用,以減少需要跟蹤的特殊規則數量。該專案按偏好順序使用以下方法:
通過玩家數據系統複製
玩家數據系統 允許將數據與玩家關聯,並在保存會話之間持久化。該系統提供從客戶端到伺服器的複製以及一組 API 可用於查詢數據和訂閱更改,使其成為從伺服器到客戶端複製玩家狀態變更的理想選擇。
例如,不是發送一個專用的 UpdateCoins RemoteEvent 來告訴客戶端它有多少硬幣,您可以調用以下代碼,讓客戶端通過 PlayerDataClient.updated 事件訂閱它。
PlayerDataServer.setValue(player, "coins", 5)
當然,這僅對伺服器到客戶端的複製以及希望在會話之間持久化的值有用,但這適用於該專案中令人驚訝的多種情況,包括:
- 當前FTUE階段
- 玩家的庫存
- 玩家擁有的硬幣數量
- 玩家農場的狀態
通過屬性複製
在伺服器需要將特定於給定 Instance 的自定義值複製到客戶端的情況下,您可以使用屬性。Roblox會自動複製屬性值,因此您不需要維護任何代碼路徑來複製與對象相關的狀態。另一個優勢是這種複製與實例本身一起進行。
這對於在運行時創建的實例特別有用,因為在將新實例加載到數據模型之前設置的屬性將與實例本身原子性地複製。這避免了編寫代碼以“等待”通過 RemoteEvent 或 StringValue 復制額外數據的需要。
您還可以直接從數據模型中讀取屬性,無論是從客戶端還是伺服器,使用 GetAttribute() 方法,並使用 GetAttributeChangedSignal() 方法訂閱更改。在植物專案中,此方法用於,除了其他事情,還複製當前植物的狀態到客戶端。
通過標籤複製
CollectionService 允許您對 Instance 應用字符串標籤。這對於對實例進行分類和將該分類複製到客戶端非常有用。
例如,CanPlant 標籤在伺服器上應用,以向客戶端顯示給定的花盆可以接收植物。
通過網絡模塊直接發送消息
對於之前選項都不適用的情況,您可以通過 網絡 模塊使用自定義網絡調用。這是該專案中唯一允許客戶端與伺服器之間通訊的選擇,因此對於傳輸客戶端請求和接收伺服器響應非常有用。
植物 利用直接網絡調用來處理各種客戶端請求,包括:
- 澆水植物
- 種植種子
- 購買物品
這種方法的缺點是每個個別消息都需要一些專用配置,這可能會增加專案的複雜性,但在可能的情況下已被避免,特別是在伺服器與客戶端通訊方面。
類和單例
植物專案中的類,就像Roblox上的實例一樣,可以被創建和銷毀。其類語法受到 Lua 語言對面向對象編程的慣用程序啟發,並進行了多項更改以支持嚴格類型檢查。
實例化
項目中許多類與一個或多個 Instances 相關聯。給定類的對象使用 new() 方法創建,這與在Roblox中使用 Instance.new() 創建實例相一致。
該模式通常用於類在數據模型中具有物理表示的對象,並且該類擴展了其功能。一個很好的例子是 BeamBetween,它在兩個給定的 Attachment 對象之間創建 Beam 對象,並保持這些附加物的方向,以便光束始終朝上。這些實例可以從 ReplicatedStorage 中的預製版本克隆,或者作為參數傳遞給 new(),並存儲在 self 對象的屬性下。
相應實例
如上所述,該專案中的許多類都有數據模型表示,與類相對應的實例,並且由它來操縱。
代碼通常選擇 Clone() 一個存儲在 ReplicatedStorage 或 ServerStorage 的預製版本,而不是在創建類對象時創建這些實例。雖然可以序列化這些實例的屬性並在類的 new() 函數中從頭創建它們,但這會使編輯對象變得非常繁瑣,並使閱讀者更難解析。此外,克隆實例通常是一個比在運行時創建新實例並自定義其屬性更快的操作。
組合
雖然在 Luau 中使用元表可以進行繼承,但該專案選擇通過組合 來允許類擴展彼此。通過組合將類結合時,“子”對象在類的 new() 方法中實例化,並以成員的身份加入 self 中。
例如,請參見 CloseButton 類,它包裝了 Button 類。
清理
類似於 Instance 可以通過 Destroy() 方法銷毀,能夠實例化的類也可以被銷毀。專案類的析構函數是 destroy(),以小寫字母 d 使代碼庫的方法之間保持 camelCase 一致性,並區分專案的類和Roblox實例。
destroy() 方法的作用是銷毀由對象創建的任何實例,斷開任何連接,並在任何子對象上調用 destroy()。這對於連接特別重要,因為具有活動連接的實例不會被Luau垃圾收集器清理,即使沒有對該實例或連接到該實例的引用。
單例
單例,如其名所示,是指只能存在一個對象的類。它們是專案中相當於Roblox的服務。不是在Luau代碼中存儲對單例對象的引用並四處傳遞,植物 利用加載 ModuleScript 時的返回值會被緩存。這意味著,從不同位置加載同一個單例 ModuleScript 時,始終提供相同的返回對象。唯一例外是如果不同環境(客戶端或伺服器)訪問 ModuleScript。
單例與可實例化類的區別在於它們沒有 new() 方法。相反,對象及其方法和狀態是通過 ModuleScript 直接返回的。由於單例不被實例化,因此不使用 self 語法,而是使用點(.)而不是冒號(:)來調用方法。
嚴格類型推斷
Luau支持漸進類型,這意味著您可以選擇性地為部分或全部代碼添加類型定義。在該專案中,對每個腳本使用 strict 類型檢查。這是Roblox的腳本分析工具的最低許可選項,因此最有可能在運行時之前捕獲類型錯誤。
類型化類語法
在Lua中創建類的既定方法是文檔良好的,然而它並不太適合強Luau類型。在Luau中,獲取類類型的最簡單的方法是使用 typeof() 方法:
type ClassType = typeof(Class.new())
這是有效的,但當您的類使用在運行時僅存在的值來初始化時,例如 Player 對象,則不是很有用。此外,在習慣性 Lua 類語法中假定在類 self 上聲明方法將始終是該類的實例;這並不是類型推斷引擎可以做的假設。
為了支持嚴格類型推斷,植物 專案使用了一個在多個方面與慣用Lua類語法不同的解決方案,其中一些可能感覺不直觀:
- self 的定義被重複,在類型聲明和構造函數中都有。這引入了可維護性的負擔,但如果兩個定義不一致,將會標記警告。
- 類方法使用點來聲明,因此可以明確地將 self 聲明為類型 ClassType。方法仍然可以像預期中那樣用冒號調用。
--!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
在邏輯保護後強制類型轉換
在撰寫時,在保護條件語句後,值的類型不會被縮小。例如,在以下保護條件後,optionalParameter 的類型不會縮小為 number。
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
print(optionalParameter + 1)
end
為了緩解此情況,在這些保護條件之後創建的新變數會明確強制轉換其類型。
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
瀏覽數據模型層次結構
在某些情況下,代碼庫需要遍歷在運行時創建的物件樹的數據模型層次結構,這對類型檢查提出了有趣的挑戰。撰寫時,不可能將通用數據模型層次結構定義為類型。因此,在某些情況下,可用於數據模型結構的唯一類型信息是頂層實例的類型。
解決這一挑戰的一種方法是在強制類型轉換為 any 並然後進行細化。例如:
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
end
這種方法的問題在於它影響可讀性。相反,該專案使用了一個名為 getInstance 的通用模塊,該模塊用於遍歷數據模型層次結構,並在內部進行強制類型轉換為 any。
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
隨著類型引擎對數據模型的理解演變,可能會出現不再需要這樣的模式的情況。
用戶界面
植物 包含各種複雜和簡單的 2D 用戶界面。這些包括非互動式的 Heads Up Display (HUD) 項目,如硬幣計數器,以及複雜的互動菜單,如商店。
UI 方法
您可以粗略地將Roblox的UI 與HTML DOM 進行比較,因為它是一個描述用戶應該看到的對象層級。創建和更新Roblox UI 的方法大致分為 命令式 和 聲明式 兩種實踐。
| 方法 | 優勢和缺點 |
|---|---|
| 命令式 | 在命令式方法中,UI 被視為Roblox上的任何其他實例層級。UI 結構在運行前於Studio中創建並添加到數據模型中,通常直接在 StarterGui 中。然後,在運行時,代碼操縱特定的UI 部分以反映創建者所需的狀態。 此方法具有一些優勢。您可以在Studio中從頭創建UI並將其存儲在數據模型中。這是一種簡單且直觀的編輯體驗,可以加快UI的創建。由於命令式UI代碼僅關注需要更改的內容,簡單的UI更改也易於實施。 一個顯著的缺點是,由於命令式UI方法要求以轉換的形式手動實現狀態,因此複雜的狀態表示可能非常難以查找和調試。在開發命令式UI代碼時,尤其是當狀態和UI因多次更新以意外順序互動而變得不同步時,錯誤確實是常見的。 命令式方法的另一個挑戰是,將UI拆分為可以一次聲明並重用的有意義的元件更難。由於整個UI樹是在編輯時聲明的,常見模式可能會在數據模型的多個部分中重複。 |
| 聲明式 | 在聲明式方法中,UI 實例所需的狀態被明確聲明,而這種狀態的高效實現則由如 Roact 或 Fusion 的庫抽象。有該方法的優勢是狀態的實現變得微不足道,您只需要描述您希望UI呈現的樣子。這使得識別和解決錯誤變得顯著更容易。 主要的缺點是必須在代碼中聲明整個UI樹。像Roact和Fusion這樣的庫有語法可以簡化這一過程,但這仍然是一個耗時過程,並且在組合UI時,編輯體驗不如直觀。 |
植物 使用 命令式 方法,認為直接展示轉換會更有效地概述如何在Roblox中創建和操縱UI。以聲明式方法是無法實現的方式。某些重複的UI結構和邏輯也被抽象為可重用組件,以避免命令式UI設計中常見的陷阱。
高層架構

層和組件
在植物中,所有UI結構都是 Layer 或 Component。
- Layer 被定義為一個頂級分組單例,包裝在 ReplicatedStorage 中的預製UI結構。一個層可以包含許多組件,或完全封裝自己的邏輯。層的例子包括庫存菜單或硬幣計數指示器。
- Component 是可重用的UI元素。當實例化新的組件對象時,它會從 ReplicatedStorage 中克隆一個預製模板。組件本身也可以包含其他組件。組件的例子包括通用按鈕類或項目列表的概念。
視圖處理
一個常見的UI管理問題是視圖處理。該專案擁有多種菜單和HUD項目,其中一些監聽用戶輸入,因此需要仔細管理它們何時可見或啟用。
植物 使用其 UIHandler 系統來處理此問題,該系統管理UI層應該何時可見或不可見。體驗中的所有UI層被分類為 HUD 或 Menu,其可見性受到以下規則的管理:
- Menu 和 HUD 層的啟用狀態可以切換。
- 僅在沒有啟用的 Menu 層時才顯示啟用的 HUD 層。
- 啟用的 Menu 層存儲在堆棧中,任何時候僅顯示一個 Menu 層。當啟用 Menu 層時,它被插入到堆棧的最前面並顯示。當禁用 Menu 層時,它從堆棧中移除,而排隊中下一個啟用的 Menu 層將顯示。
這種方法是直觀的,因為它允許以歷史形式導航菜單。如果從另一個菜單打開一個菜單,關閉新菜單將再次顯示舊的菜單。
UI層單例向 UIHandler 註冊,並提供在其可見性應該發生變化時觸發的信號。
進一步閱讀
從這個全面的概述植物專案開始,您可能想要探索以下指南,深入了解相關概念和主題。