植物 是玩家种植和浇水种子的参考经验,因此他们可以稍后收获和出售获得的植物。

项目专注于你在 Roblox 上开发体验时可能遇到的常见使用案例。如果适用,你会发现关于权衡、妥协和各种实现选择的理由的笔记,这样你就可以做出最适合自己经验的决定。
获取文件
- 导航到植物体验页面。
- 点击 ⋯ 按钮和 在工作室编辑 。
使用案例
植物 涵盖以下使用案例:
- 会话数据和玩家数据持续存在
- 用户界面视图管理
- 客户端-服务器网络
- 第一次用户体验(FTUE)
- 硬币和软币购买
此外,该项目解决了更窄的问题集,可应用于许多经验,包括:
- 与玩家相关的地方的区域定制
- 管理玩家角色的移动速度
- 创建一个跟随角色周围的对象
- 检测角色所在的世界部分
请注意,此体验中有几个使用案例太小、太窄或没有展示解决一个有趣的设计挑战的解决方案;这些情况未被涵盖。
项目结构
创建体验时的第一个决定是决定如何结构项目,其主要包括在哪里放置特定实例在数据模型中以及如何组织和结构客户端和服务器代验证码的入口点。
数据模型
下表描述了数据模型实例中哪些容器服务放置在哪里。
服务 | 实例类型 |
---|---|
Workspace | 包含静态模型,代表 3D 世界,特别是不属于任何玩家的世界部分。您不需要在执行时间时动态创创建 or 创作、修改或删除这些实例,因此可以将它们留在这里。: 还有一个空的 Folder,玩家的农场模型将在执行时间时添加到其中。 |
Lighting | 大气和照明效果。 |
ReplicatedFirst | 包含显示加载屏幕和初始化游戏所需的最小子集的实例。放置在 ReplicatedFirst 中的实例越多,等待它们在 ReplicatedFirst 中复制代码的时间就越奔跑。:
|
ReplicatedStorage | 作为客户端和服务器上都需要访问的所有实例的存储容器。: 在依赖文件夹中存在一些由项目使用的第三方库。: 在 实例文件夹中存在各种各样的预制实例,由游戏中的各种类别使用。: 在源文件夹中存在所有代码,不需要从客户端和服务器上访问的加载过程。 8> 7> |
ServerScriptService | 包含一个 Script 为项目中所有服务器端代码提供入口的点。 |
ServerStorage | 作为不需要复制到客户端的所有实例的存储容器。: 在实例文件夹中存在一个模板农场模型。将此副本放置在 当玩家加入游戏时,将复制到所有玩家。: 在源文件夹中存在所有专属于服务器的代码。 |
SoundService | 包含游戏中用于音效的 Sound 对象。在 SoundService 下,这些 Sound 对象没有位置,在 3D 空间中不被模拟。 |
入口点
大多数项目将代码组织在可重复使用的 ModuleScripts 中,可以导入整个代码库。ModuleScripts 是可重复使用的,但它们不会独拥有执行;它们需要由 Script 或 LocalScript 导入。许多 Roblox 项目将包含大量 Script 和 LocalScript 对象,每个对应游戏中的一个行为或特定系统,创建多个入口点。
对于植物微游戏,通过单个入口实现了不同的方法,单个入口用于所有客户端验证码,单个入口用于所有服务器验证码。您项目的正确方法取决于您的需求,但单个入口提供更多控制系统在顺序中执行的方式。
以下列表描述了两种方法的权衡:
- 单个 Script 和单个 LocalScript 分别覆盖服务器和客户端代码。
- 由于所有代码都从单个脚本中初始化,因此对不同系统的启动顺序有更大控制。
- 可以通过引用在系统之间传递对象。
高级系统架构
项目中的顶层系统在下面详细列出。这些系统中的一些比其他系统复杂得多,在许多情况下,其功能被抽象到其他类别的层次结构中。

这些系统中的每一个都是“单例”,在那里它是由相关的客户端或服务器 start 脚本初始化的非可逆转的类。您可以在本指南中稍后阅读更多关于 单例模式 的内容。
服务器
以下系统与服务器相关。
系统 | 描述 |
---|---|
网络 |
|
玩家数据服务器 | 使用 保存和加载持久玩家数据。:
|
市场 |
|
碰撞组管理器 |
|
农场管理器服务器 |
|
玩家对象容器 |
|
标签玩家 |
|
FtueManagerServer |
|
角色生成器 |
|
客户
以下系统与客户端相关。
系统 | 描述 |
---|---|
网络 |
|
玩家数据客户端 |
|
市场客户 |
|
本地步行跳管理器 |
|
农场管理器客户端 | 监听特定标签被应用于实例,并创建添加这些实例的“组件”行为。一个“组件”指的是在实例上添加 CollectionService 标签时创建的类,在移除时被销毁;这些被用于农场的 CTA 提示和各种类型的农场状态传递给玩家。 |
UI设置 |
|
FtueManagerClient |
|
字符冲刺 |
|
客户端-服务器通信
大多数 Roblox 体验都涉及客户端和服务器之间的一些通信元素。这可能包括客户端要求服务器执行某个操作,并将服务器更新复制到客户端。
在这个项目中,客户端-服务器通信尽可能保持通用,通过限制使用 RemoteEvent 和 RemoteFunction 对象来减少需要跟踪的特殊规则数量。该项目使用以下方法,按优先级顺序:
通过玩家数据系统进行复制
玩家数据系统 允许数据与持续到保存会话之间的玩家关联。该系统提供从客户端到服务器的复制以及一组可用于查询数据和订阅更改的 API,使其成为从服务器到客户端复制玩家状态更改的理想选择。
例如,而不是发射特定的 UpdateCoins RemoteEvent 告诉客户它有多少枚硬币,您可以调用以下内容,让客户通过 PlayerDataClient.updated 事件订阅它。
PlayerDataServer:setValue(player, "coins", 5)
当然,这仅适用于服务器到客户端复制和你想要在会话之间保持的值,但这适用于项目中的惊人数量的案例,包括:
- 当前FTUE阶段
- 玩家的道具
- 玩家拥有的硬币数量
- 玩家农场的状态
通过特性复制数据库
在服务器需要向客户端复制特定 Instance 的自定义值的情况下,您可以使用 属性 。Roblox 自动复制属性值,因此您不需要维护任何代码路径以复制与对象相关的状态。另一个优势是,这种复制发生在实例本身旁边。
这对于在执行时间时创建的实例特别有用,因为在其被父辈到数据模型之前将属性设置到新实例上的新实例将与实例本身一起复制原子。这样可以避免需要写代码来“等待”额外数据通过 RemoteEvent 或 StringValue 复制。
您还可以直接从数据模型中阅读属性,从客户端或服务器,使用 GetAttribute() 方法,并订阅更改使用 GetAttributeChangedSignal() 方法。在 植物 项目中,这种方法被用于,例如,复制植物当前状态给客户。
通过标签进行复制
CollectionService 让你对 Instance 应用一个字符串标签。这有助于分类实例并将分类复制到客户端。
例如,CanPlant标签被应用到服务器上,向客户端表示一个给定的罐能够接收植物。
直接通过网络模块发送消息
对于不适用以前选项的情况,您可以通过 网络 模块使用自定义网络呼叫。这是项目中唯一允许客户端到服务器通信的选项,因此最适合用于发送客户端请求和接收服务器响应。
植物 使用直接网络呼叫为各种客户请求,包括:
- 浇灌植物
- 种植一颗种子
- 购买物品
这种方法的缺点是,每个单独消息需要一些特定配置,可能会增加项目的复杂性,尽管在可能的情况下已避免这一点,特别是在服务器到客户端通信中。
类和单例子
在 植物 项目中的类,像 Roblox 上的实例,可以创建和销毁。其类别语法受到 idiomatic Lua 对 对象导向编程 的某些更改启发,以启用 严格类型检查 协助。
即时化
项目中的许多类与一个或多个 Instances 相关。给定类别的对象使用 new() 方法创建,与 Roblox 中使用 Instance.new() 创建实例的方式一致。
该模式一般用于类在数据模型中具有物理表示的对象,并且类扩展其功能。一个很好的例子是 BeamBetween ,它创建了两个给定 Beam 对象之间的 Attachment 对象,并保持这些附件始终朝向上,以便光束总是朝向上。这些实例可以从预制版本中克隆到 ReplicatedStorage 或传到 new() 作为参数,并存储在 self 下的对象内。
相应的实例
如上所述,该项目中的许多类都有数据模型表示,这是与类相对应的实例,由它操纵。
而不是在类对象被实例化时创建这些实例,代码通常选择 Clone() 预制版本存储在 Instance 或 ReplicatedStorage 下,或 ServerStorage 下。虽然可以将这些实例的属性序列化并从零开始创建它们在类的 new() 函数中,这样做会使编辑对象变得很麻烦,并使它们更难被阅读器解析。此外,复制实例通常比创建新实例并在执行时间时自定义其属性快得多。
组成
虽然使用 metatables 在 Luau 中可以实现继承,但项目选择通过 组合 允许类别相互扩展。当通过组合使用类来组合类时,"子"对象在类的 new() 方法中被实例化,并作为 self 下的成员包含在内。
有关此在行动作中的示例,请参阅包装 CloseButton 类的 Button 类。
清理
与如何使用 Instance 方法摧毁一个 Destroy() 类相似,可以实例化的类也可以被摧毁。项目类的摧毁方法为 ,使用小写 来确保代码库的方法之间的一致性,以及区分项目类和 Roblox 实例。
destroy() 方法的角色是摧毁任何由对象创建的实例、断开任何连接,并在任何子对象上调用 destroy() 。这对连接特别重要,因为具有活跃连接的实例不会被 Luau 垃圾收集器清理,即使没有对实例或连接到实例的引用或连接也是如此。
单例
单例,如名称所示,是指只能存在一个对象的类。它们是项目与 Roblox 的 服务 相当的。而不是存储单例对象的引用并在 Luau 代验证码中传递,植物 利用了需要 ModuleScript 返回值的事实。这意味着需要从不同地点一致地要求相同的单例 ModuleScript 提供相同的返回对象。这个规则的唯一例外情况是,如果不同环境(客户端或服务器)访问了ModuleScript。
单例与不稳定类别的区别在于它们没有 new() 方法。相反,对象以及其方法和状态直接返回到 ModuleScript 。由于单例不被实例化,因此不使用 self 语法,而是使用 dot ( . ) 调用方法,而不是 colon ( : )。
严格类型推断
Luau 支持渐进输入,这意味着你可以自由地为一些或所有代验证码添加可选类型定义。在这个项目中,strict 类型检查用于每个脚本。这是 Roblox 的脚本分析工具最少限制选项,因此最有可能在执行时间时前检测到类型错误。
类型化的类语法
在 Lua 中创建类的确立的方法是 很好地文档,但它不适合强大的 Luau 类型。在 Luau 中,获取类型的最简单的方法是 typeof() 方法:
type ClassType = typeof(Class.new())
这有用,但当你的类以仅在执行时间时存在的值启动时,它并不很有用,例如 Player 对象。此外,在 idiomatic Lua 类语法中的假设是,在类 self 上宣言方法总是会导致该类的实例;这不是类型推理引擎可以做出的假设。
为了支持严格类型推断, 植物 项目使用一种解决方案,与 idiomatic 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)项目,例如硬币计数器和复杂的互动菜单,例如商购物。
用户界面方法
您可以松散地比较 Roblox UI 与 HTML DOM,因为它是描述用户应该看到的对象层次的。创建和更新 Roblox 用户界面的方法广泛分为 强制性 和 声明性 实践。
方法 | 优缺点 |
---|---|
强制性 | 在强制性方法中,用户界面被视为 Roblox 上的任何其他实例层次一样。在 Studio 中,运行时前创建 UI 结构,然后添加到数据模型,通常直接在 StarterGui 中。然后,在执行时间时,代码会对特定的 UI 部分进行操作,反映创建者需要的状态。: 这种方法带有一些优势。您可以在 Studio 中从零开始创建 UI 并将其存储在数据模型中。这是一个简单易视的编辑体验,可以加快 UI 作品建速度。因为强制性 UI 代码仅关心需要更改的内容,所以它也使简单的 UI 更改易于实现。: 一个显著的缺点是,由于必需的 UI 方法需要手动在变换形式中实现状态,因此状态的复杂表达可能很难找到和调试。开发强制性 UI 代验证码时出现错误是常见的,尤其是当状态和用户界面因多个更新在意外顺序中互相作用而失去同步时。: 另一个使用强制性方法的挑战是,更难将 UI 拆解为可以一次宣言并重复使用的有意义组件。因为整个用户界面树在编辑时被宣言,所以常见模式可能在数据模型的多个部分重复。 |
声明性 | 在说明性方法中,所需的 UI 实例状态被明确声明,有效实现此状态被库存储库抽象化,例如 Roact 或 Fusion 。: 这种方法的优势是实现状态变得简单,你只需要描述你希望你的用户界面看起来像什么。这使识别和解决错误变得更加容易。: 关键的缺点是必须在代验证码中声明整个 UI 树。像 Roact 和 Fusion 这样的库有语法来使这更容易,但它仍然是一个耗时的过程,当组合 UI 时编辑体验较少直观。 |
植物 使用在概念下的强制性方法来显示变形,这会提供更有效的方法来显示 Roblox 上的用户界面如何创建和操纵。这在宣言性方法无法实现。一些重复的用户界面结构和逻辑也被抽象为可重用的 组件 以避免在强制性用户界面设计中出现常见的问题
高级架构

层和组件
在 植物 中,所有的用户界面结构都是 Layer 或 Component 。
- Layer 被定义为顶层分组单元,用于将预制的 UI 结构包装在 ReplicatedStorage 中。层可能包含多个组件,或者可能完全封装自己的逻辑。层的例子是库存菜单或头部显示的硬币数量指示器。
- Component 是可重复使用的 UI 元素。当新组件对象被实例化时,它从 ReplicatedStorage 复制一个预制模板。组件可能自身包含其他组件。组件的例子是通用按钮类或一个列表的概念。
查看处理
一个常见的用户界面管理问题是视图处理。该项目包含多个菜单和 HUD 项目,其中一些会倾听用户输入,需要仔细管理它们何时可见或启用。
植物 以其 UIHandler 系统来解决这个问题,当 UI 层应该或不应该显示时管理它。游戏中的所有用户界面层都被分类为 HUD 或 Menu ,其可见度由以下规则管理:
- 可以切换Menu和HUD层的启用状态。
- 启用HUD仅显示无Menu启用时。
- 启用Menu将在堆中存储,只有一个Menu可以一次显示。当Menu层启用时,它将插入到堆的前端并显示。当Menu被禁用时,它将从堆中删除,下一个启用的Menu在队列中显示。
这种方法直观,因为它允许菜单以历史为导航。如果从另一个菜单打开一个菜单,关闭新菜单将再次显示旧菜单。
用户界面层单例注册自己与 UIHandler ,并提供一个信号,当其可见性应该更改时触发。
进一步阅读
对 植物 项目的全面了解之后,您可能想探索以下指南,它们进一步深入涉及相关概念和主题。