植物是一個參考遊戲,玩家可以種植和澆水種子,然後收穫並出售所產生的植物。

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

這些系統中的每一個都是“單例”,因為它是一個不可實例化的類,並且由相關的客戶端或伺服器start腳本初始化。您可以在本指南的後面閱讀更多有關單例模式的內容。
伺服器
以下系統與伺服器相關。
| 系統 | 描述 |
|---|---|
| 網絡 |
|
| 玩家數據伺服器 |
|
| 市場 |
|
| 碰撞組管理器 |
|
| 農場管理伺服器 |
|
| 玩家對象容器 |
|
| 標記玩家 |
|
| Ftue管理伺服器 |
|
| 角色生成器 |
|
客戶端
以下系統與客戶端相關。
| 系統 | 描述 |
|---|---|
| 網絡 |
|
| 玩家數據客戶端 |
|
| 市場客戶端 |
|
| 本地行走跳躍管理器 |
|
| 農場管理客戶端 |
|
| UI設置 |
|
| Ftue管理客戶端 |
|
| 角色衝刺 |
|
客戶端-伺服器通信
大多數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下。
要查看這一點的示例,請參見包裝Button類的CloseButton類。
清理
類似於如何使用Destroy()方法銷毀Instance,可以實例化的類也可以被銷毀。專案類的析構方法是destroy(),以保持代碼庫方法的camelCase一致性,並區分專案的類和Roblox實例。
destroy()方法的作用是銷毀對象創建的任何實例,斷開任何連接,並對任何子對象調用destroy()。這對於連接特別重要,因為具有活動連接的實例不會被Luau垃圾收集器清理,即使對該實例或與該實例的連接沒有任何引用。
單例
單例,顧名思義,是只能存在一個對象的類。它們是專案中相當於Roblox的服務。植物利用要求ModuleScript會緩存其返回值的事實,而不是存儲對單例對象的引用並在Luau代碼中傳遞它。這意味著從不同地方要求相同的單例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用戶界面。這些包括非交互式的抬頭顯示(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註冊自己,並提供一個信號,當其可見性應該改變時觸發。
進一步閱讀
從這個對植物專案的全面概述中,您可能想要探索以下指南,這些指南更深入地探討相關概念和主題。