植物是一个参考体验,在这里玩家种植并水浇种子,以便稍后收获并出售结果。
该项目专注于您在 Roblox 开发体验时可能遇到的常见用例。 在适用的情况下,您将找到关于交易、补充协议和各种实现选择的理由,以便您做出最佳的决策。
获取文件
- 导航到植物体验页面。
- 点击 ⋯ 按钮和 在工作室编辑 。
使用例子
植物 覆盖以下使用场景:
- 会话数据和玩家数据持久性
- UI 视图管理
- 客户端-服务器网络
- 第一次用户体验 (FTUE)
- 购买硬币和软币
此外,该项目解决了许多体验中适用的更窄的问题集,包括:
- 在与玩家关联的地方自定义区域
- 管理玩家角色的移动速度
- 创建跟随角色的对象
- 检测角色所在的世界部分
注意,在这个体验中有几个使用场景太小、太狭、或不能解决复杂的设计挑战;这些都不会被覆盖。
项目结构
创建体验时的第一个决定是决定如何结构项目,这主要包括在数据模型中放置特定实例的位置以及客户端和服务器代验证码的组织和结构入口。
数据模型
下表描述数据模型实例中哪些容器服务。
服务 | 实例类型 |
---|---|
Workspace | 包含静态模型表示 3D 世界的特定部分,不属于任何玩家。您不需要在执行时间时创创建 or 创作、修改或销毁这些实例,因此它是可以在此留下的。 还有一个空的 Folder ,其中玩家的农场模型将在执行时间时添加。 |
Lighting | 大气和照明效果。 |
ReplicatedFirst | 包含需要显示加载屏幕和初始化游戏的最小子集。 越多放置在 ReplicatedFirst 中的实例,就越长,在 ReplicatedFirst 中的代码在兑换码之前的奔跑成时间就越长。 在实例文件夹中存在加载屏幕 GUI。 在源文件夹中存在加载屏幕代码和需要等待游戏加载剩余部分的代码。0>start0>
|
ReplicatedStorage | 作为存储容器,为所有需要访问的客户端和服务器端的实例提供服务。
|
ServerScriptService | 包含一个 Script 作为项目中所有服务器端代码的入口点。 |
ServerStorage | 作为存储容器,用于所有不需要复制到客户端的实例。 在 实例文件夹中,存在一个模板农场模型。一个副本放置在0> Class.Workspace0>,当玩家加入游戏时,该模型将被复制到所有玩家。0>
|
SoundService | 包含用于游戏中声音效果的 Sound 对象。在 SoundService 下,这些 Sound 对象没有位置,不在 3D 空间中模拟。 |
入口点
大多数项目在导入代码时将代码组织成 ModuleScripts 可以在整个代码库中导入的可重用的 Class.ModuleScript|ModuleScripts
对于 Plant 微游戏,通过一个单一的 LocalScript 实现不同的方法,其中所有客户端代验证码的入口点,并且单个 Script 作为服务器代验证码的入口点。您的项目的正确方法取决于您的需求,但一个单一的入口提供更大的控制过程系统在运行的顺序。
下列列表描述了两种方法之间的交易关系:
- 一个单独的 Script 和一个单独的 LocalScript 覆盖服务器和客户端代码。
- 更高地控制不同系统的启动顺序,因为所有代码都从单个脚本初始化。
- 可以在系统之间通过参考来传递对象。
高级系统架构
项目中的顶级系统在下面详细介绍。一些系统与其他系统的复杂度不同,在许多情况下它们的功能被抽象到其他类别型的层次结构中。
这些系统中的每一个都是“单例”,因为它是由相关的客户或服务器 start 脚本初始化的非即时化类。您可以在本指南中阅读更多关于 单例模式 的后面。
服务器
以下系统与服务器关联。
系统 | 描述 |
---|---|
网络 | 创建所有 Class.RemoteEvent 和 Class.RemoteFunction 实例。 > 0> 显示方法向发件人发送和接收消息的方法。0> >0>
|
PlayerDataServer |
|
市场 |
|
碰撞群组管理器 |
|
农场经理服务器 |
|
玩家对象容器 |
|
标签玩家 |
|
FtueManagerServer |
|
角色生成器 |
|
客户
以下系统与客户端关联。
系统 | 描述 |
---|---|
网络 |
|
PlayerDataClient | 在内存中存储本地玩家的数据。 用于查询和订阅玩家数据的方法和信号。 |
市场客户 |
|
本地跳跃管理器 |
|
农场经理客户端 | 列听特定 Class.CollectionService 标签被应用到实例,并创建“组件” appending 的行为添加到这些实例。一个“组件” 指 到一个 创建 在 一个 Class.CollectionService 标签 添加 到 一个 实例 并 被 移除 时 会 被 使用 来 提示 CTA |
UI设置 |
|
FtueManagerClient |
|
角色冲刺 |
|
客户端-服务器通信
大多数 Roblox 体验都涉及客户端和服务器之间的一些通信元素。这可以包括客户端要求服务器执行特定操作和服务器复制更新到客户端。
在此项目中,客户端-服务器通信尽可能通用,限制使用 RemoteEvent 和 RemoteFunction 对象来减少追踪的特殊规则数量,因此项目使用以下方法,按照优先级顺序:
复制通过玩家数据系统
玩家数据系统 允许数据与玩家在保存会话之间关联。 此系统提供从客户端到服务器的复制,并提供一组 API 可以用于查询数据并订阅更改,使其 ideal 用于从服务器到客户端复制玩家状态的重复。
例如,而不是发射一个 bespoke UpdateCoins``Class.RemoteEvent 告诉客户有多少枚硬币,您可以调用以下并让客户通过 PlayerDataClient.updated 事件订阅它。
PlayerDataServer:setValue(player, "coins", 5)
当然,这仅对服务器端到客户端复制有用,并且仅适用于您想要在会话之间保持的值,但这在项目中的许多情况中都适用:
- 当前 FTUE 阶段
- 玩家的物品栏
- 玩家拥有的金币数量
- 玩家农场的状态
通过特性复制
在需要将服务器的重复值复制到客户端,该服务器的实例具体指定为 Instance 的情况下,您可以使用 属性。 Roblox 会自动复制属性值,因此您无需维护任何与服务对象状态相关的代码路径。另一个优点是,这种复制与实例本身发生在同一时间。
这对于在执行时间时创建的实例非常有用,因为在新实例上设置的属性将在其父实例中重复使用。这使得无需写代码来“等待”额外数据通过 RemoteEvent 或 StringValue 复制。
您还可以直接从数据模型中,从客户端或服务器,使用 GetAttribute() 方法,以及订阅更改使用 GetAttributeChangedSignal() 方法。在 植物 项目中,此方法用于,例如,复制当前植物的状态到客户端。
使用标签复制
CollectionService 允许您将字符串标签应用到 Instance 。这对于类别化实例和复制该类别到客户端有很大的用途。
例如,CanPlant标签在服务器上应用,表示服务器可以接收指定的花盆。
使用网络模块直接发送消息
在没有上述选项适用的情况下,您可以通过 网络 模块使用自定义网络调用。这是项目中唯一一个允许客户端与服务器通信的选项,因此它是最有用的选择用于传输客户端请求并接收服务器回应。
植物 使用直接网络调用来满足多种客户端请求,包括:
- 给植物浇水
- 种植种子
- 购买物品
这种方法的弊端是,每个个人消息都需要一些自定义配置,这可能会增加项目的复杂性,尽管这在可能的情况下已被避免,特别是对于服务器到客户端通信。
类和单击钮
在 Plant 项目中,像 Roblox 上的实例一样,可以创建和摧毁 Class。 它的类型语法灵感于 idiomatic Lua 对 对象向编程 的多个更改,以启用 1>严格的类型检查1> 协助。
即时化
项目中的许多类都与一个或多个 Instances 关联。 对于指定的类的对象使用 new() 方法创建,与 Roblox 使用 Instance.new() 类似。
此模式通常用于在数据模型中有物理表示的对象,并且类似于 BeamBetween ,它创建一个 Beam 对象在两个给定的
相应的实例
如上所述,该项目中的许多类别都有数据模型表示,与类别对应的实例,通过它操作。
在 Class 对象实例创建时创建这些实例,而不是选择 Clone() 一个预制版本的 Instance
组成
虽然在 Lua 使用 metatables 中可能使用继承,但项目选择允许通过 组合 允许类别相互扩展。当通过组合来扩展类别时,“子”对象是在 class 的 new() 方法中初始化,并且作为成员包含在 1> self1> 下。
有关此示例的实动作,请参阅CloseButton类,该类包括Button类。
清理
与 Instance 使用 Destroy() 方法类似,可以实现的类也可以被摧毁。项目类的破坏器方法为 destroy() ,其中 2> d2> 为项目类的 5> cam
destroy() 方法的角色是摧毁对象创建的任何实例,连接任何连接,并在任何子对象上调用 destroy() 。这对于连接非常重要,因为有关连接的实例不会被 Lua 垃圾收集器清理,即使没有对该实例或连接的参考。
单击钮
单击钮,如其名称所示,是一个只有一个对象可以存在的类。它们是项目的对应版本,Roblox的 服务 。 而不是存储一个引用于 singleton
单击钮与即时类别不同,因为它们没有 new() 方法。 相反,对象与其方法和状态通过 ModuleScript 直接返回。 作为单击钮不是实例化的,因此 self 语法不是使用,方法通常通过 dot ( 2>) 而不是
严格类型推导
Luau 支持渐变输入,这意味着您可以为一些或所有代验证码添加任意类型的定义。在此项目中, strict 类型检查为每个脚本使用。 此是 Roblox 的脚本分析工具的最小权限选项,因此在执行时间时最有可能检查到类型错误。
输入的类型
在 Lua 中创建类型的最佳方法是 很好文档化 ,但它不适合强 Luau 输入。在 Lua 中,最简单的方法为获取类型的类型是 typeof() 方法:
type ClassType = typeof(Class.new())
这种情况下,当您的类型初始化为只在执行时间时存在的值,例如 Player 对象时,它不是很有用。此外,在 idiomatic Lua 类语言中,声称将在运行时声明一个方法在 self 上的假设是,总是会是 Class.Player 上的实例。这不是类型在ference 语言中可以做的假设。
为了支持 strict 类型 infer,植物项目使用了一个解决方案,其中一些可能感觉非常非直观:
- self 的定义在类型声明和构建器中都有重复。这会导致一个可维护的负载,但如果两个定义因错误而失去同步,警告将被标记。
- 类方法用一个点声明,因此 self 可以明示为类型 ClassType 。方法仍然可以使用 colon 作为期望的调用方式。
--! 严格
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 来 traversing 数据模型层级,其中 any 内部 cast.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
随着类型引擎的数据模型理解进展,它可能不再需要这样的模式。
用户界面
植物 包括多种复杂和简单的 2D 用户界面。这些包括不交互的头部显示(HUD)项目,例如硬币计数器和复杂的交互菜单,例如商购物。
用户界面方法
您可以将 Roblox UI 稍微比较到 HTML DOM,因为它是一个描述用户应该看到的内容的对象的层次结构。 创建和更新 Roblox UI 的方法是由 imperative 和 declarative 实践分为两大类。
Plant 使用了一个 imperative 的方法,在认为显示变形直接提供更有效的概览的 Roblox 下。 这不是可以使用声明的方法。一些重复的 UI 结构和逻辑也被抽象成 可重用的组件 以避免在导致性 UI 设计中的常见陷阱。
高级架构
层和组件
在植物中,所有的用户界面结构都是一个Layer或一个Component。
- Layer 定义为顶级组合单元,它将预制的 UI 结构包装在 ReplicatedStorage 中。层可能包含多个组件,或者它可能完全封装自己的逻辑。层的例子包括库存菜单或头部显示的金币指示器。
- Component 是一个可重用的 UI 元素。当新的组件对象实例时,它会从 ReplicatedStorage 中克隆一个预制模板。组件可以在其中包含其他组件。例子组件的示例是一个通用按钮类或列表项目的概念。
查看处理
一个常见的UI管理问题是视图处理。 此项目有多个菜单和HUD项目,其中一些听取用户输入,需要仔细管理,当它们显示或启用时。
Plant 解决了这个问题的 UI 界面处理器系统,其管理当一个 UI 层可见或不可见。游戏中的所有 UI 层都归为 HUD 或 HUD ,并由以下规则来管理其可见度:
- HUD层可以切换为启用状态。
- 启用 HUD 层只会显示,如果没有 Menu 层。
- 启用 Menu 层存储在堆中,并且只有一个 Menu 层在一次显示。 启用 Menu 层时,它将插入到Stack的前端,并且显示。 禁用 1> Menu1> 层时,它将移除到Stack的前端,并且显示。
这种方法直观,因为它允许菜单以历史记录的方式导航。如果从另一个菜单打开一个菜单,关闭新菜单会显示旧菜单。
UI层单击钮注册为 UIHandler ,并且提供一个信号来显示它们当其可见度变更时触发。
进一步阅读
从这个详细的 Plant 项目概览,您可能会想要探索这些指南,这些指南涵盖了相关概念和主题的更多深度信息。