Supporting Systems

We used the following systems to support both the foundational gameplay systems, as well as any goals of the main design requirements.

UseManager

UseManager provided a simple API to apply a grabbed object onto something, like a piece of layered clothing onto a mannequin. The main function for this API is UseManager.AddUse (tags, targetObjects, distance, onSuccess, onNothingEquipped, onWrongEquipped, extraData), which binds a set of tags to targetObjects. When a player has an object with one of the tags and clicks on a targetObject, the onSuccess callback function is called. Other call functions allowed us to show players extra visual info if either a click is made without a grabbed item or with the wrong type of item.

We could remove the "use" with UseManager.RemoveUse, which was usually helpful when a mission was finished or a specific item was "used". In addition, we could add or remove targets with AddUseTargets and RemoveUseTargets.

Highlights

When a player was near an item of interest, such as a seal, we wanted to have that item stand out from its surroundings. To implement this, we created a LocalScript called HighlightItems that uses a sphere centered around the player to detect touches with other meshes, connecting to Touched and TouchEnded events. The getHighlight function checks several tags on a touched mesh or its parents using a GetTaggedObjectUpHierarchy helper function. If any highlight isn't needed, we could forcibly remove it by using a NoHighlight tag. However, if it's needed but doesn't quite fit various other tags, we could force it using the Important tag.

This LocalScript utilizes a new engine feature Highlight that draws an outline of an object and/or fills the object's interior with a defined color; for more information on how to use this feature, see Highlighting Objects. Highlights and the mouse cursor OnItemIndicator systems work together, so Highlights not only determine if a mesh needs a highlight, but it also provides a type of mesh for OnItemIndicator.

HighlightItemsFunc is used to communicate with other client systems. For example, EventManager uses it with an Enable command to enable or disable a Highlight in certain cutscenes, and OnItemIndicator uses GetType to enquire about the type of object it is. To detect when an item is no longer present, such as when a corrupt room is destroyed, we connect to CollectionService.GetInstanceRemovedSignal.

Lore and ThoughtBubbles

Lore and ThoughtBubbles are 2 similar systems. Lore uses a ScreenGui as an on-screen UI container with a child Frame to control the sizing and rescaling of its children TextLabels and ImageLabels, and lore waits for the player to click anywhere on the screen to remove it. Similarly, thought bubbles use a BillboardGui with a child TextLabel to non-lore objects, and it displays dialogue in the 3D space near the object for a specified duration and cooldown period without the text taking up the entirety of the screen. For more information on the design behind these systems, see Lore and Thought Bubbles.

Lore is implemented in the LoreManger LocalScript. When clicked or touched, it fires a raycast using a helper function utils.RaycastAlongPointingDir, and it uses a NoPlayerCollision group. If a mesh under the click or one of the parents has a Lore or ThoughtBubble tag, we display the UI. The text, caption, and image are defined by the LoreText, LoreCaption, and LoreImage attributes on the object.

Note that we use Camera.ViewportPointToRay or Camera.ScreenPointToRay to construct the ray, depending upon if it was called from a non-touch or touch. The coordinates are in somewhat different coordinate systems. For the mouse, we get them from Class.UserInputService.InputEnded``:Connect for the MouseButton1, and for touch devices, we get them from Class.UserInputService.TouchTapInWorld``:Connect.

ThoughtBubbles are overall similar, using a raycast to check if a mesh or its parents have a ThoughtBubble tag. It also uses the ThoughtText attribute for text, and a ThoughtBubble tag to point to a placeholder object used for positioning the UI in the world. Thought bubbles that use the same positional object but have different text have different cooldowns.

Special Cases

Lore has a couple of special cases, one of which is the corrupted seals. When a player clicks a corrupted seal, it displays lore UI, and it waits for a click to start a mission, which affects the game flow. This is handled by the GameStateClient that uses a bindable LoreManagerFunc to request lore UI. A callback is provided to the Lore system by GameStateClient to know when lore is "closed" by the player. Another special case is when ThoughtBubbles and Lore tags are on the same object. In this case, to avoid an overlap of lore and thought bubble text, we run the thought bubble after the lore is closed.

LoreManager also handles a special case with showing a small cutscene when clicking on disabled doors that are locked until the player picks up the room's seal.

OnItemIndicator

We want to show different icons in the center of the screen when the player is looking at certain items of interest. The client script OnItemindicator does a raycast along the camera Class.CFrame.LookVector and analyzes the results. Based on the results, it sets an image in the OnItemIndicator2 ScreenGui.

When no items of interest are hit, the default icon is a small dot. We could set any icon by adding an OnItemIndicator string attribute to a specific mesh, using the names from onItemIndicatorImages, such as Hand, Eye, or DoorCurrentlyLocked. The attribute is only needed in rare cases, and most of the time other existing tags or systems provide the type of icon. For more details, see the Update function.

Type checks some in a priority order. After the OnItemIndicator override, we check if it is grabbable or a drawer for the "hand" icon through either utils.CanGrabModel(model) or utils.GetTaggedObjectUpHierarchy("Drawer2", model). After that, we call HighlightManager that determines the highlight status, the types of items, and which icon to use. For example:


highlightItemsFunc:Invoke({"GetType", curInst})

The Lore and ThoughtBubble tags are checked later by checking tags. For the doors, we have 2 different icons: DoorCurrentlyClosed and DoorAlwaysLocked. DoorManager sets a true or false DoorEnabled attribute for doors that can be open or closed, and we use presence and value of the attribute. Objects that look like doors but do not open have the DoorLocked tag.

DoorManager

The DoorManager LocalScript uses a Door tag and CollectionService to manage opening and closing doors. Door has front and back side triggers that we connect with touch and touchEnded events. We created tweens to open and close a door from the front and back sides. We maintain a playersNear map (of players touching the triggers, separately for the front and back sides.

Each door has a simple state system, DoorState (Closed, Opening, Open, Closing), with tweens used for transitions. We could enable or disable a door's ability to open or close from external systems by calling DoorManager.EnableDoor, which sets a DoorEnabled attribute.

MasterAnimator

The MasterAnimator LocalScript plays animated images (texture atlas), which we used to animate TV screens. To scroll through images, we needed to know a set of parameters: row and column count, total frames, perios, image dimensions, and a set of image IDs. The system allowed us to animate across multiple images, each possibly split into rows and columns of sub-images. We could provide this data through attributes or values, but in this experience, we used helper scripts. UpdateImageAnimations(dT) calculates what image or subimage we needed to show using time and parameters. If we needed to change to a new image, we set Image. If we need to change any subimage, we set ImageRectOffset.

An object with an animated SurfaceGui would have an Animator ModuleScript, with the main purpose to provide an Animator.GetParams function that returns all the parameters. This helps the MasterAnimator LocalScript that uses the ImageAnimation tag and CollectionService to gather such objects, and find the Animator ModuleScript under them. It then uses pcall to require Animator ModuleScript and call GetParams on it.

LocalSpaceAnimations

The LocalSpaceAnimations LocalScript uses a LocalSpaceRotation tag to rotate mostly "cosmetic" objects with a given rotational velocity and delay around either the X, Y, or Z axis. We used this either for distant objects that players wouldn't interact with, or for smaller objects that don't affect simulation much. Parameters defined through the Speed, Delay, and Axis values. For implementation details, see Rotating Cloud Meshes.

HeadlampManager

The HeadlampManager LocalScript handles when users select the on-screen ImageButton to toggle the spotlight on top of their head on or off, fires comments to the server using HeadlampEvent, and plates toggling on and off sounds. When a character is added, or their Head is changed, the giveCharacterHeadlamp function clones templateHeadlamp lamp, and positions the lamp using some offsets and rotations from the FaceFrontAttachment.

SeatManager

We do not want players to automatically seat when near an object they can sit on. Instead, we want to require users to click near a seat in order to sit. The SeatManager script adds ClickDetectors based on a Seat tag, and calls seat:Sit(humanoid) when clicked. When teleporting players between a room's normal and corrupt state, we can't have players sitting because CFrame coordinate change wouldn't be able to work, so the SeatManager has a functionality to disable or enable seating a few seconds before and after the teleport.

DrawerManager

The DrawerManager script uses a Drawer2 tag and CollectionService to handle clicking on drawers to open or close them, and play any corresponding audio. The opening and closing action is done by setting TargetPosition for a PrismaticConstraint.

KillVolumes

In a couple of areas of the main gameplay area, such as the electric sparks and water near the start of the road leading up to the house, a player can have their Humanoid.Health set to 0 when entering a volume with the KillVolume tag. The KillVolumes script uses Touched:Connect to determine when a player enters a volume, then reduces their health to 0.

PlayerMissionRespawn

The PlayerMissionRespawn script uses a RespawnVolume tag and CollectionService to deal with volumes that make players respawn when touched. We placed these volumes under corrupt rooms, as many missions have gaps or moving platforms in which the player could fall down. When touched, the script plays a small Teleport_Jump cutscene and invokes GameStateFunc with GameEvents.PlayerRespawn command.

When processing GameEvents.PlayerRespawn, the script can use RespawnPositions, if mission config provides it. If not, it uses TeleportPositions for the specific mission. We don't have a "checkpoint" system, so CalcClosestTeleportPos just selects closest Respawn or Teleport spots from where the player hit the RespawnVolume, using the only horizontal, "2D" distance.

Small Helper Systems

PianoManager

The PianoManager script uses a Piano tag and CollectionService to add ClickDetectors and play one of the piano sounds when clicked on the keyboard.

RitualSupport

The foyer where players place seals has a complicated contraption that undergoes changes as each seal is placed in its defined location. For example, depending on the number of seals placed, specific events play to enable/disable lights and beams, change the transparency of certain objects, etc. The RitualSupport ModuleScript is a small wrapper over EventManager:Invoke calls for those events, providing parameters to the event, such as what "root object" to play it on, depending upon what specific seal was placed.

RestorableManager

Some grabbable objects are important for gameplay, such as seals, and we didn't want them to get lost if a player dropped them somewhere. If an object has a Restorable tag, the RestorableManager script remembers its transform when it is added to the restorable system. When a player drops such an object, the grabbing system calls restorableManager.StartTracking. If the object isn't picked up again in five seconds, the RestorableManger script positions it at the original transform and resets the tracking time.

Portals

In a few missions, we teleport players a short distance within a mission, such as respawning players who fall off a spinning platform. To simplify setting up this type of teleportation, which we call "Portals" in script, a helper function ProcessPortal in DemoUtils is used. For example, if P1 is the part defining the initial trigger, and P2 is the part defining destination player transform, the following code snippet could define such portal functionality:


P1.Touched:Connect(function(otherPart) utils.ProcessPortal(otherPart, P2) end)

ProcessPortal handles checking that otherPart is a human, teleporting the player through a CFrame coordinate change, and invoking a small cutscene to hide the transition using a Teleport_Jump event in EventManager.

Configuration Scripts

We have several configuration, data definition, and common functionality scripts:

DemoConfig. Definitions of missions. Enumerations for game states, events for client-server communications.

DemoGlobalSettings. We develop in one place, but release (and playtest) in others. The script checks placeID and enables/disables various cheats and debugging functionality.

DemoUtils. Various utility functions. Dealing with transforms. Setting visibility, anchored or other properties. Checking for a point in a box. Finding objects in hierarchy by "dotted" name. Managing TempStorage (that can be used to temporarily move models "somewhere far" and bring back later). Click detector helpers. Grabbing support. Support for checking tags (especially along the hierarchy). Connecting triggers to EventManager.

AudioUtils. A couple of functions to play weighted random sounds from a set.

GrabUtil. Helper functions for grabbing.