我们使用了以下系统来支持基础游戏系统以及主要设计要求的任何目标。
UseManager
UseManager 提供了一个简单的API,用于将抓取的对象应用于某个对象上,比如将一件分层衣物放在模型上。该API的主要函数是 UseManager.AddUse (tags, targetObjects, distance, onSuccess, onNothingEquipped, onWrongEquipped, extraData),它将一组标签绑定到targetObjects上。当玩家拥有一个带有其中一个标签的对象并点击一个targetObject时,onSuccess回调函数会被调用。其他调用函数允许我们在点击时如果没有抓取对象或抓取了错误类型的对象时向玩家展示额外的视觉信息。
我们可以使用 UseManager.RemoveUse 来移除“使用”,这通常在完成任务或特定项目被“使用”时很有帮助。此外,我们可以使用 AddUseTargets 和 RemoveUseTargets 来添加或移除目标。
高亮
当玩家靠近一个感兴趣的物品时,比如一个封印,我们希望这个物品能够从其周围环境中突出出来。为了实现这一点,我们创建了一个名为 HighlightItems 的 LocalScript,它使用一个围绕玩家的球来检测与其他网格的接触,连接到 Touched 和 TouchEnded 事件。getHighlight 函数使用 GetTaggedObjectUpHierarchy 辅助函数检查被触摸的网格或其父对象上的几个标签。如果不需要任何高亮,我们可以通过使用 NoHighlight 标签强制移除它。然而,如果需要高亮但不完全符合其他各种标签,我们可以使用 Important 标签强制它。
这个 LocalScript 利用一个新的引擎特性 Highlight,它在对象周围绘制轮廓和/或用定义的颜色填充对象的内部;有关如何使用此功能的更多信息,请参见高亮对象。Highlights 和鼠标光标 OnItemIndicator 系统一起工作,所以 Highlights 不仅决定一个网格是否需要高亮,还为 OnItemIndicator 提供了一种网格类型。
HighlightItemsFunc 用于与其他客户端系统通信。例如,EventManager 使用它的 Enable 命令在某些过场动画中启用或禁用 Highlight,而 OnItemIndicator 使用 GetType 来查询它是什么类型的对象。为了检测一个物品何时不再存在,比如当一个损坏的房间被摧毁时,我们连接到 CollectionService.GetInstanceRemovedSignal。
故事与思维气泡
Lore 和 ThoughtBubbles 是两个类似的系统。Lore 使用 ScreenGui 作为屏幕上的用户界面容器,配有子 Frame 用于控制其子对象 TextLabels 和 ImageLabels 的大小和缩放,Lore 等待玩家在屏幕上点击任意位置以将其移除。同样,思维气泡使用带有子 TextLabel 的 BillboardGui,在三维空间中展示对象附近的对话,持续一段指定的时间和冷却时间,而不会影响整个屏幕的文本展示。有关这些系统背后设计的更多信息,请参见Lore 和 思维气泡。
Lore在 LoreManger LocalScript 中实现。当被点击或触摸时,它使用辅助函数 utils.RaycastAlongPointingDir 发射一条射线,并使用 NoPlayerCollision 组。如果点击下的网格或其父对象具有 Lore 或 ThoughtBubble 标签,我们会展示UI。文本、标题和图片由该对象上的 LoreText、LoreCaption 和 LoreImage 属性定义。
请注意,我们使用 Camera.ViewportPointToRay 或 Camera.ScreenPointToRay 来构建射线,具体取决于它是从非触摸还是触摸调用。坐标在不同的坐标系统中有所不同。对于鼠标,我们通过 Class.UserInputService.InputEnded:Connect 获取,处理 MouseButton1,而对于触摸设备,则通过 Class.UserInputService.TouchTapInWorld:Connect 获取。
思维气泡整体上也类似,使用射线检测网格或其父对象是否具有 ThoughtBubble 标签。它还使用 ThoughtText 属性进行文本显示,并使用 ThoughtBubble 标签指向用于在世界中定位UI的占位对象。使用相同位置对象的思维气泡但文本不同则具有不同的冷却时间。
特殊情况
Lore 有几个特殊情况,其中一个是损坏的印章。当玩家点击损坏的印章时,它会显示 Lore UI,并等待一下点击以开始任务,这会影响游戏流程。这由GameStateClient 处理,该客户端使用可绑定的 LoreManagerFunc 请求 Lore UI。GameStateClient 通过提供一个回调来通知 Lore 系统何时由玩家关闭 Lore。另一个特殊情况是 ThoughtBubbles 和 Lore 标签在同一对象上。在这种情况下,为了避免 Lore 和思维气泡文本重叠,我们在 Lore 关闭后运行思维气泡。
LoreManager 还处理一个特殊情况,即在点击被禁用的门时显示一个小的过场动画,这些门在玩家拾取房间的印章之前是锁住的。
OnItemIndicator
我们希望在玩家查看特定感兴趣的物品时,在屏幕中心显示不同的图标。客户端脚本 OnItemindicator 沿着相机的 Class.CFrame.LookVector 进行射线检测并分析结果。根据结果,它在 OnItemIndicator2 ScreenGui 中设置一个图像。
当没有感兴趣的物品被命中时,默认图标为一个小点。我们可以通过向特定网格添加一个 OnItemIndicator 字符串属性,使用来自 onItemIndicatorImages 的名称,比如 Hand、Eye 或 DoorCurrentlyLocked,来设置任何图标。此属性仅在少数情况下需要,大多数时候其他现有标签或系统会提供图标的类型。有关详细信息,请参见 Update 函数。
类型检查以优先顺序进行。在 OnItemIndicator 重写后,我们通过 utils.CanGrabModel(model) 或 utils.GetTaggedObjectUpHierarchy("Drawer2", model) 检查它是否可抓取或是一个抽屉以获取“手”图标。之后,我们调用HighlightManager以确定高亮状态、物品类型以及使用的图标。例如:
highlightItemsFunc:Invoke({"GetType", curInst})
Lore 和 ThoughtBubble 标签稍后进行检查。对于门,我们有两个不同的图标:DoorCurrentlyClosed 和 DoorAlwaysLocked。DoorManager 为可以打开或关闭的门设置一个 true 或 false 的 DoorEnabled 属性,我们使用该属性的存在和数值。看起来像门但不可以打开的对象具有 DoorLocked 标签。
DoorManager
DoorManager LocalScript 使用 Door 标签和 CollectionService 管理门的开关。门有前后触发器,我们将其连接到触摸和 touchEnded 事件。我们创建了补间动画从前面和后面打开和关闭门。我们维护一个 playersNear 映射(触摸触发器的玩家,前后分别维护)。
每个门都有一个简单的状态系统 DoorState(关闭、开门、打开、关门),使用补间动画进行转换。我们可以通过调用 DoorManager.EnableDoor,设置 DoorEnabled 属性,来启用或禁用门的开闭能力。
MasterAnimator
MasterAnimator LocalScript 播放动画图像(纹理图集),我们用它来为电视屏幕动画化。要滚动图像,我们需要知道一组参数:行和列计数、总帧数、周期、图像尺寸和一组图像 ID。该系统允许我们在多张图像之间进行动画,每张图像可能分成行和列的子图像。我们可以通过属性或值提供这些数据,但在这个体验中,我们使用辅助脚本。UpdateImageAnimations(dT) 根据时间和参数计算需要展示的图像或子图像。如果需要更换为新图像,我们设置 Image。如果需要更换任何子图像,我们设置 ImageRectOffset。
具有动画的 SurfaceGui 对象将具有一个 Animator ModuleScript,其主要目的是提供一个 Animator.GetParams 函数,返回所有参数。这有助于 MasterAnimator LocalScript,它使用 ImageAnimation 标签和 CollectionService 来收集这样的对象,并在其下寻找 Animator ModuleScript。然后它使用 pcall 来要求 Animator ModuleScript 并调用 GetParams。
LocalSpaceAnimations
LocalSpaceAnimations LocalScript 使用 LocalSpaceRotation 标签以给定的旋转速度和延迟围绕 X、Y 或 Z 轴旋转大多数“美容”对象。我们使用此功能为玩家无法交互的远处对象或小型不会影响模拟的对象提供使用。参数通过 Speed、Delay 和 Axis 值定义。有关实现细节,请参见旋转云网格。
HeadlampManager
HeadlampManager LocalScript 处理用户选择屏幕上的 ImageButton 以打开或关闭头顶聚光灯时,发出注释到服务器,使用 HeadlampEvent,并播放开关的声音。当角色被添加或其 Head 被更改时,giveCharacterHeadlamp 函数克隆 templateHeadlamp 灯,并使用来自 FaceFrontAttachment 的一些偏移量和旋转定位灯。
SeatManager
我们不希望玩家在可以坐的物体附近自动坐下。相反,我们希望要求用户在座位附近点击以坐下。SeatManager 脚本基于 Seat 标签添加 ClickDetectors,并在被点击时调用 seat:Sit(humanoid)。在玩家在房间的正常状态和损坏状态之间传送时,我们不能让玩家坐下,因为 CFrame 坐标改变将无法正常工作,因此 SeatManager 具有在传送前后几秒内禁用或启用座位功能。
DrawerManager
DrawerManager 脚本使用 Drawer2 标签和 CollectionService 来处理点击抽屉以打开或关闭它们,并播放相应的音频。打开和关闭动作通过设置 PrismaticConstraint 的 TargetPosition 来完成。
杀死区域
在主游戏区域的几个地方,例如靠近通向房子的道路起点的电击火花和水中,当玩家进入带有 KillVolume 标签的区域时,可以将他们的 Humanoid.Health 设置为 0。KillVolumes 脚本使用 Touched:Connect 来确定玩家何时进入区域,然后将其生命值减少到 0。
PlayerMissionRespawn
PlayerMissionRespawn 脚本使用 RespawnVolume 标签和 CollectionService 来处理触摸时使玩家重新生成的区域。我们将这些区域放置在损坏的房间中,因为许多任务中有间隙或移动平台,玩家可能会掉落。触摸时,脚本播放一个小的 Teleport_Jump 过场动画并调用 GameStateFunc,使用 GameEvents.PlayerRespawn 命令。
在处理 GameEvents.PlayerRespawn 时,脚本可以使用 RespawnPositions,如果任务配置提供它。如果没有,它对特定任务使用 TeleportPositions。我们没有“检查点”系统,因此 CalcClosestTeleportPos 仅选择玩家击中 RespawnVolume 的最近 Respawn 或 Teleport 的点,使用仅水平的“2D”距离。
小辅助系统
PianoManager
PianoManager 脚本使用 Piano 标签和 CollectionService 添加 ClickDetectors,并在点击键盘时播放钢琴声音。
RitualSupport
玩家放置印章的前厅有一个复杂的装置,随着每个印章放入定义位置而发生变化。例如,根据放置的印章数量,特定事件播放以启用/禁用灯光和光束,改变某些对象的透明度等。RitualSupport ModuleScript 是对 EventManager:Invoke 调用的一个小封装,为这些事件提供参数,比如根据放置的具体印章,决定在什么“根对象”上播放它。
RestorableManager
一些可抓取的对象对游戏玩法很重要,比如印章,我们不希望它们在玩家随意将其放置时丢失。如果一个对象具有 Restorable 标签,则 RestorableManager 脚本在将其添加到可恢复系统时会记住其变换。当玩家放下这样的对象时,抓取系统调用 restorableManager.StartTracking。如果对象在五秒内没有被再次拾取,则 RestorableManger 脚本会将其放置在原始变换位置,并重置跟踪时间。
传送门
在某些任务中,我们在任务内部将玩家短距离传送,例如在重新生成玩家时,当他们从旋转平台掉落。为了简化建立这种类型的传送,我们在脚本中称之为“传送门”,使用 DemoUtils 中的辅助函数 ProcessPortal。例如,如果 P1 是定义初始触发的部分,P2 是定义目标玩家变换的部分,则以下代码片段可以定义这种传送功能:
P1.Touched:Connect(function(otherPart) utils.ProcessPortal(otherPart, P2) end)
ProcessPortal 处理检查 otherPart 是否是人,利用 CFrame 坐标变化传送玩家,并调用小过场动画以通过 EventManager 中的 Teleport_Jump 事件隐藏过渡。
配置脚本
我们有几个配置、数据定义和通用功能脚本:
DemoConfig。任务定义。游戏状态、事件的枚举,用于客户端与服务器的通信。
DemoGlobalSettings。我们在一个地方开发,但在其他地方发布(和游戏测试)。该脚本检查 placeID,并启用或禁用各种作弊和调试功能。
DemoUtils。各种实用功能。处理变换。设置可见性,固定或其他属性。检查点是否在框内。通过“点状”名称在层次结构中查找对象。管理 TempStorage(可用于临时移动模型“某个远方”并稍后返回)。点击检测器辅助功能。抓取支持。支持检查标签(特别是在层次结构中)。将触发器连接到 EventManager。
AudioUtils。一些功能从一组中播放加权随机音效。
GrabUtil。抓取的辅助功能。