工廠參考項目

*此內容是使用 AI(Beta 測試版)翻譯,可能含有錯誤。若要以英文檢視此頁面,請按一下這裡

植物是玩家種植和灌溉種子的參考經驗,因此他們可以稍後收穫和出售結果的植物。

Plant project banner

項目專注於您在 Roblox 上開發體驗時可能遇到的常見使用案例。在適用的情況下,你會找到關於權衡、妥協和各種實裝選擇原因的說明,這樣你就可以做出最適合自己經驗的決定。

取得檔案

  1. 導航到植物體驗頁面。
  2. 點擊 按鈕,然後 在 Studio 編輯

使用案例

植物 涵蓋以下使用案例:

  • 會話資料和玩家資料持續存在
  • 介面視圖管理
  • 客戶端與服務器網路
  • 第一次使用者體驗(FTUE)
  • 購買硬幣和軟幣

此外,這個項目解決了更窄的問題集,可應用於許多經驗,包括:

  • 與玩家相關的區域的自定義
  • 管理玩家角色的移動速度
  • 創建跟隨字元的對象
  • 偵測角色身處世界的哪部分

請注意,此體驗中有許多使用案例過小、過於狹窄,或未能展示有趣的設計挑戰的解決方案;這些未被涵蓋。

項目結構

創建體驗時的第一個決定是決定如何結構 項目 ,主要包括在哪裡放置特定實例在 資料模型 中以及如何組織和結構客戶端和服務器代碼的入口點。

数据模型

下表描述了數據模型實例中哪些容器服務放置在哪裡。

服务實例類型
Workspace

包含代表 3D 世界的靜態模型,特別是不屬於任何玩家的世界部分。您不需要在運行時動態創建、修改或刪除這些實例,因此可以將它們留在這裡。:

還有一個空的 Folder,玩家的農場模型將在運行時添加到其中。

Lighting

大氣和燈光效果。

ReplicatedFirst

包含需要顯示載入屏幕和初始化遊戲的最小子集的實例。放置在 ReplicatedFirst 中的實例越多,在 ReplicatedFirst 中執行代碼前的等待時間就越長。:

  • 實例 文件夾中存在載入屏幕GUI。:
  • 來源 文件夾中存在載入屏幕代碼和需要等待遊戲剩下部分載入的代碼。start``Class.LocalScript 是項目中所有客戶端代碼的入口。
ReplicatedStorage

作為客戶端和伺服器上都需要存取的所有實例的儲存容器。:

  • 依存性 文件夾中存在一些由項目使用的第三方庫。:
  • 實例 文件夾中存在各種各樣的預製實例,由遊戲中的各種類別使用。:
  • 來源 文件夾中存在所有代碼, 不需要 從客戶端和伺服器上都可以訪問的載入過程。
ServerScriptService

包含一個 Script 作為項目的入口點,用於項目中所有服務器側代碼。

ServerStorage

為所有不需要複製到客戶端的實例提供儲存容器。:

  • 實例 文件夾中存在一個樣板模型 農場 模型。將此副本放置在 Workspace 當玩家加入遊戲時,將被重複給所有玩家。:
  • 來源 文件夾中存在所有專屬於服務器的代碼。
SoundService

包含遊戲中使用的音效對象 Sound 。在 SoundService 下,這些 Sound 對象沒有位置,並且在 3D 空間中未被模擬。

進入點

大多數項目會將代碼組織在可以在整個代碼庫中匯入的可重複使用 ModuleScripts 中。ModuleScripts 可重複使用,但不會獨立執行;它們需要由 ScriptLocalScript 匯入。許多 Roblox 項目會有大量的 ScriptLocalScript 對象,每個對應遊戲中的一種行為或特定系統,創建多個入口點。

對於 植物 微遊戲,通過單個 LocalScript 實現了不同的方法,該入口是所有客戶端代碼的入口,單個 Script 是所有服務器代碼的入口。您項目的正確方法取決於您的需求,但單一入口提供更多控制執行系統的順序。

下列列表描述了兩種方法的權衡:

  • A single Script and a single LocalScript cover server and client code respectively.
  • Greater control over the order in which different systems are started because all code is initialized from a single script.
  • Can pass objects by reference between systems.

高層系統架構

項目中的高級系統詳細說明如下。這些系統中的一些比其他系統複雜得多,在許多情況下,它們的功能會被抽象到其他類別的階層中。

Plant project systems architecture diagram

這些系統每個都是「單例」,因為它們是一個非即時化的類別,而不是由相關的客戶端或伺服器 start 腳本初始化。您可以在此指南的後面閱讀更多關於 單體模式 的內容。

服务器

以下系統與伺服器有關。

系统說明
網路
  • 創建所有 RemoteEventRemoteFunction 實例。:
  • 暴露方法來從客戶端發送和收聽訊息。:
  • 在執行時對從客戶端收到的參數進行類型驗證。
玩家數據服務器

使用 保存和載入永久玩家資料。:

  • 在記憶體中儲存玩家資料並將變更複製給客戶端。:
  • 提供用於訂閱、查詢和更新玩家數據的信號和方法。
市場
  • 從客戶端處理軟幣交易。
  • 暴露一個方法來出售收集的植物。
衝突群管理器
  • 分配玩家角色模型到衝突群。:
  • 設定衝突群,以便玩家角色不能與植物車衝突。
農場管理員伺服器
  • 重新創建玩家的農場模型,當他們加入遊戲時,從玩家數據中創建農場模型。:
  • 當玩家離開時移除農場模型。:
  • 當玩家的農場變更時,更新玩家資料。:
  • 暴露一個方法來存取與給定玩家相關的 農場 類型。
播放器對象容器
  • 創建與玩家生命週期相關的各種對象,並提供一個方法來恢復這些對象。
標籤玩家
FtueManagerServer
  • 在FTUE期間,執行每個階段並等待完成。
角色生成器
  • 當角色死亡時重生角色。請注意,Players.CharacterAutoLoads已被禁用,因此生成將暫停,直到玩家的數據載入。

客戶端

以下系統與客戶端有關。

系统說明
網路
  • 等待服務器創建所有 RemoteEventRemoteFunction 實例。:
  • 提供方法來向服務器發送和接收消息。:
  • 強制執行運行時參數類型驗證。:
  • 執行 pcall() 在遠端函數上。
玩家數據客戶端
  • 在記憶體中儲存本地玩家的資料。:
  • 提供方法和信號來查詢和訂閱玩家數據的變更。
市場客戶
  • 曝光一個方法,要求服務器購買軟幣物品。
本地步行跳管理器
  • 曝光方法來修改角色的 WalkSpeedJumpHeight 通過乘數器來避免在從多個地點修改這些值時發生衝突。
農場管理員客戶端
  • 聆聽特定 CollectionService 標籤被應用於實例,並創建「組件」附加行為到這些實例。一個「組件」指的是當 CollectionService 標籤被添加到實例時創建的類別,在移除時被摧毀時使用;這些用於農場的 CTA 提示和各種類別向玩家傳送農場狀態。
UI設定
  • 初始化所有 UI 層。:
  • 配置某些層只在遊戲世界的物理部分可見。:
  • 為啟用選單時連接特殊攝影機效果。 >
FtueManager客戶端
  • 在客戶端配置 FTUE 階段。
字元衝刺
  • 使用 LocalWalkJumpManager 增加 WalkSpeed 當玩家角色在他們的農場之外時。

客戶端與伺服器通訊

大多數 Roblox 體驗都涉及客戶端和伺服器之間的通訊元素。這可能包括客戶請求服務器執行某個操作,並將更新複製到客戶端。

在這個項目中,客戶端與伺服器通訊盡量保持一般化,通過限制使用 RemoteEventRemoteFunction 對象來減少需要記錄的特殊規則數量。這個項目使用以下方法,依照優先級順序:

透過玩家資料系統進行複製

玩家資料系統 允許資料與持續在保存會話之間的玩家相關。這個系統提供從客戶端到服務器的複製,以及一組可用於查詢資料和訂閱變更的 API,使其成為從服務器到客戶端複製玩家狀態變更的理想選擇。

例如,您可以呼叫以下內容,讓客戶通過 UpdateCoins``Class.RemoteEvent 事件訂閱它,而不是發射特定的 PlayerDataClient.updated 來告訴客戶它有多少枚硬幣。


PlayerDataServer:setValue(player, "coins", 5)

當然,這只適用於服務器到客戶端複製和你想要在會話間保持的值,但這適用於項目中驚人數量的案件,包括:

  • 當前的FTUE階段
  • 玩家的物品庫
  • 玩家擁有的金幣數量
  • 玩家農場的狀態

透過屬性複製

在需要將服務器重複給客戶的特定 Instance 值時,您可以使用 屬性 。Roblox 會自動複製屬性值,因此您不需要維護任何代碼路徑來複製與對象相關的狀態。另一個優點是,這種複製發生在實例本身旁边。

這對於在運行時創建的實例特別有用,因為在其被父聯到數據模型之前設置的屬性將與實例本身一起以原子方式複製。這樣就不需要寫代碼來「等待」額外資料透過 RemoteEventStringValue 複製。

您也可以直接從資料模型中讀取屬性,從客戶端或伺服器,使用 GetAttribute() 方法,並透過 GetAttributeChangedSignal() 方法訂閱變更。在 植物 項目中,此方法用於,例如,複製植物的當前狀態給客戶。

透過標籤複製資料

CollectionService 讓您對 Instance 應用字串標籤。這有助於分類實例並將該分類複製到客戶端。

例如,CanPlant標籤在服務器上應用,以向客戶表示指定的鍋可以接收植物。

直接透過網路模組傳送訊息

對於不適用以前選項的情況,您可以使用 網路 模組來進行自訂網路呼叫。這是項目中唯一允許客戶端與服務器通信的選項,因此最適合傳送客戶請求和接收服務器回應。

植物 使用直接網路呼叫來處理各種客戶請求,包括:

  • 灌溉植物
  • 種植一個種子
  • 购买物品

這種方法的缺點是,每個個別訊息需要一些特殊配置,可能會增加項目的複雜度,雖然在可能的情況下已避免這種情況,特別是在服務器到客戶端通訊中。

類別和單元體

植物 項目中的類別,像在 Roblox 上的實例,可以創建和摧毀。其類別語法受到 idiomatic Lua 對 對象導向編程 的某些變更啟發,以啟用 嚴格類型檢查 支持。

即時化

項目中的許多類別與一個或多個 Instances 相關。給定類別的對象使用 new() 方法創建,與 Roblox 中使用 Instance.new() 創建實例一致。

這個模式通常用於對象,其中類別具有數據模型中的物理表示,並擴展其功能。一個很好的例子是 BeamBetween ,它會在兩個給定的 Beam 物件之間創建一個 Attachment 對象,並保持附件的方向,使光束總是朝向上。這些實例可以從預製版本中複製到 ReplicatedStorage 或傳到 new() 作為參數,並存放在 self 下的對象內。

相應的實例

如上所述,本項目中的許多類別都有數據模型代表、與類別相對應的實例,並由它操縱。

當類別對象被實例化時,代碼通常選擇 Clone() 預製版本存儲在 InstanceReplicatedStorage 下,或者在 ServerStorage 下存儲已壓縮的版本。雖然可以將這些實例的屬性序列化並從零開始在類 new() 函數中創建它們,但這樣做會使編輯對象變得非常煩雜,並使它們更難被讀者解釋此外,複製實例通常比創建新實例並在運行時自訂其屬性快得多。

組成

雖然在 Luau 使用 元表 可以實現繼承,但項目選擇允許類別通過 組合 相互擴展。當通過組合使用類別來組合時,「子」對象在類別的 new() 方法中被實例化,並包含在 self 下作為成員。

要在行動中看到這種示例,請參閱包裝 CloseButton 類的 Button 類。

清理

與使用 Instance 方法摧毀 Destroy() 方法類似,可以被初始化的類也可以被摧毀。項目類別的毀滅方法是 使用小寫 來保證代碼庫的方法一致性,以及區分項目類別和 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 對象。此外,在 idiomatic Lua 類別語法中所假設的是,在類別上宣言方法 self 將永遠是該類別的實例;這不是類型推理引擎可以做出的假設

為了支持嚴格類型推論,植物項目使用一種解決方案,與常見的 Lua 類語法在多種方面不同,其中一些可能感覺不直觀:

  • self 的定義在類型宣言和生成器中都被複製。這會帶來維護負擔,但如果兩個定義與彼此失去同步,警告將被標記。
  • 類別方法使用點符來宣言,因此 self 可以明確宣稱為類型 ClassType 。方法仍然可以使用殖號來呼叫,如預期。

--!嚴格
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


--!嚴格
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
print(optionalParameter + 1)
end

為了解決這個問題,在這些警衛後創建了新變量,並明確指定了他們的類型。


--!嚴格
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用戶介面。這些包括非互動的頭部顯示(HUD)項目,例如硬幣計數器和複雜的互動菜單,例如商店。

UI 方法

您可以粗略地將 Roblox UI 與 HTML DOM 進行比較,因為它是一個描述用戶應該看到的對象階層。創建和更新 Roblox 用戶介面的方法廣泛分為 強制性宣言性 實務。

方法優點和缺點
命令性

在強制性方法中,介面會被視為 Roblox 上的任何其他實例層次一樣。介面結構在 Studio 中在運行時創建,並添加到數據模型中,通常直接在 StarterGui 中。然後,在執行時,代碼會操作特定的 UI 部分來反映創建者所需的狀態。:

這種方法具有一些優點。您可以在 Studio 中從零開始創建介面,並將其存儲在資料模型中。這是一個簡單且視覺化的編輯體驗,可以加快 UI 創建速度因為強制性的使用者介面代碼只關心需要更改的內容,所以也使簡單的使用者介面變更容易實現。:

值得注意的缺點是,由於必須的使用者介面方法需要狀態手動實裝為變換形式,因此狀態的複雜表達可能很難找到和調試。開發強制性 UI 代碼時發生錯誤的情況很常見,特別是當狀態和使用者介面因多個更新互相作用而失去同步時。:

另一個使用強制性方法的挑戰是,更難將 UI 拆解為可以一次宣言並重複使用的有意義組件。因為整個使用者介面樹在編輯時被宣言,因此常見模式可能在數據模型的多個部分重複。

說明性

在說明性方法中,所需的 UI 實例狀態被明確宣言,並由像 RoactFusion 等圖形庫實現此狀態被抽象化。:

這種方法的優點是狀態的實現變得簡單,你只需要描述你希望你的用戶介面看起來像什麼。這使識別和解決錯誤變得相當容易。:

關鍵缺點是必須在代碼中宣言整個使用者介面樹。像 Roact 和 Fusion 這樣的圖形庫有助於使這更容易,但它仍然是一個消耗時間的過程,並且在組合用戶介面時的編輯體驗不那麼直觀。

植物 使用在「轉換」概念下的 強制性 方法來顯示轉換,這會提供更有效的方法來在 Roblox 上創建和操作 UI。這不會在宣言性方法中發生。一些重複的使用者介面結構和邏輯也會被抽取到可重複的 組件 中,以避免在強制性使用者介面設計中出現常見的問題。

高層架構

Plant project UI architecture diagram

層和組件

植物 中,所有的使用者介面結構都是 LayerComponent

  • Layer 被定義為一個頂層級集群單元,可將預製的 UI 結構包裝在 ReplicatedStorage 中。層可包含多個組件,或可完全封裝自己的邏輯。層級的範例是庫存選單或頭枕顯示中的硬幣數量指標。
  • Component 是可重複使用的 UI 元素。當新的組件對象被初始化時,它會從 ReplicatedStorage 複製一個預製模板。組件可能內含其他組件。組件的例子包括一個通用按鈕類或一個列表的概念。

查看處理方式

常見的使用者介面管理問題是視窗處理。這個項目包含多種菜單和 HUD 項目,其中一些會聆聽使用者輸入,並需要仔細管理它們何時會顯示或啟用。

植物 以其 UIHandler 系統來解決這個問題,當 UI 層應該或不應該顯示時。遊戲中的所有用戶介面層都被分類為 HUDMenu ,其可見度由以下規則管理:

  • 可以切換 MenuHUD 層的啟用狀態。
  • 啟用 HUD 層只會在沒有啟用 Menu 層時顯示。
  • 啟用Menu將在堆中儲存,只有一個Menu可以一次顯示。當 Menu 層被啟用時,它會被插入到堆的前端並顯示。當 Menu 層被禁用時,它將從堆中移除,下一個啟用的Menu層在隊列中顯示。

這種方法直觀,因為它允許菜單以歷史為基礎進行導航。如果從另一個選單開啟一個選單,關閉新選單會再次顯示舊選單。

使用者介面層單元註冊自己與 UIHandler 並提供在其可視性應該變更時發射的信號。

進一步閱讀

對於 植物 項目的這個全面瞭望,您可能想探索以下指南,進一步深入相關概念和主題。