update rules

This commit is contained in:
Joe 2026-02-02 14:05:11 -06:00
parent 4c390fdd37
commit d5522b2fef
8549 changed files with 11783 additions and 1888 deletions

View file

@ -0,0 +1,325 @@
---
title: Construct the house
comments:
next: /resources/the-mystery-of-duvall-drive/materialize-the-world
prev: /resources/the-mystery-of-duvall-drive
description: Explains the design process for house in The Mystery of Duvall Drive.
---
It took many iterations and rapid testing at the beginning of the process to make the house both fun and creepy, easy to explore, and something you want to get lost in. We'll show you how we thought through the space and constructed it using **packages** to make updating individual chairs and entire rooms faster toward the end.
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/overview.png" width="100%" />
## Begin with a 2D layout
Since the house is both the majority of the playable area and an entire character unto itself, we wanted to give a lot of thought about how to make sure it remained both real and fun. We started with the idea that the house should contain several visual and audio gags, lore, and corrupt puzzle rooms, and we wanted players to initially approach the house with little knowledge of what was going on, learning what happened to the house and the family that lived there as they were playing.
To balance all of these important elements, we started imagining the 3D environment with a 2D layout! Starting with figuring out the general **story beats**, or key moments, we wanted the player to experience, we drew a layout of the space, and thought about how we would guide the player where we wanted them while maintaining an environment that felt larger than life.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/early-draft-playable-area.png" width="80%" />
<figcaption>An early draft of what we wanted the playable area and surrounding environment to look like.</figcaption>
</figure>
Once we had our early draft of the overall layout, we used [terrain](../../parts/terrain.md) to quickly lay out the surrounding hills, lake, road, and the driveway up to the house. We didn't bother with the visual quality of the demo at this point, but instead focused on how long it took to get to the house and what it would take to give the player glimpses of the house and the event in the sky.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/early-concept-approach-house.png" width="60%" />
<figcaption>Early concept of how we wanted the player to approach the house. To drive players to want to get a closer look at the house, we used trees to obscure their view while still giving glimpses of the house and the event in the sky.</figcaption>
</figure>
Using both simple [parts](../../parts/index.md) along with [Creator Store](../../production/creator-store.md) assets, we were able to quickly add in content that would help us compose the environment, including:
- Making the road bend so players couldn't see all the way down the length of it.
- Adding trees with denser areas where we wanted to make it clear the player couldn't access, along with thinner areas where we wanted to encourage them to explore.
- Raising the house's driveway so that when players approach it, they would have to climb. This gave the house a more intimidating presence.
- Positioning the giant event behind the storm in the sky so players could see parts, but not the whole thing, until they approach the house itself.
- Placing and figuring out the general shape of the house we ultimately settled with.
No amount of experience guarantees your first ideas are the best ones. This is why we got assets into Studio quickly and used simple parts and Creator Store assets to fill in the environment with temporary assets. This allowed us to not have to make everything up front, and we could start playtesting within the first few days.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/earliest-version-toolbox-house.png" width="60%" />
<figcaption>Earliest version using simple parts and the Creator Store.</figcaption>
</figure>
While we wanted to eventually replace the temporary assets with our own, the only thing we cared about early on was laying out the basics and playing the experience to make sure the environment was fun to move around and wasn't too large or too much of a "straight shot" so that players were encouraged to explore.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/early-shot-driveway-approach.png" width="60%" />
<figcaption>Early shot of the driveway approach.</figcaption>
</figure>
## Plan each room
The exterior was only meant to be a taste of the overall story to immerse the player and get them familiar with the world they were playing in. The main character was the house itself, and we wanted players to spend most of their gameplay inside it plumbing for secrets and finding surprises in every room. To do this, we needed to figure out what rooms the house should include, when we wanted players to run into them, and where they should exist within the house. We drew a 2D layout of the house, and the team bounced around ideas for cool things that could happen. No ideas were ignored at this point.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/blueprint-whole-house.png" width="80%" />
<figcaption>Early 2D "blueprint" with descriptions of what creepy events we wanted to happen in each room.</figcaption>
</figure>
We used 2D boxes of each room to figure out how we wanted to arrange them with each other. Each room should be an opportunity to do something, and we threw around a lot of ideas, including the main uses of each room, what creepy gags we could use to surprise the player, and potential puzzles they could face.
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/blueprint-foyer.png" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/blueprint-dining.png" />
</GridContainer>
Each room had to work together, and we wanted to tell the story slowly and progressively. We knew we couldn't control where someone went first, so we planned for some of the harder puzzles to be further from the front door and harder to stumble upon. This meant adding a few floors to the house, including an attic and a basement for both gameplay and thematic reasons. Final puzzle in a basement that looks evil and scary? Sign us up!
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/blueprint-second-floor.png" width="80%" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/blueprint-attic.png" width="60%" />
## Build a 3D layout
All the early planning and ideas in the world don't make a fun experience, so it was time to start making the idea 3D! The exterior layout was blocked out and we made space for the house to be expanded or contracted. Earliest iterations were done in another 3D application where we could make simple boxes and shapes to start turning that 2D image into a playable space. You may find that building the early versions in Studio and using simple parts is your favorite method. There's no wrong way, so long as you get your ideas into 3D and playable quickly!
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/gray-mesh-top-down.png" />
<figcaption>Top-down view of the layout. You'll notice it's flipped from the concept.</figcaption>
</figure>
<figure>
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/gray-mesh-front.png" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/gray-mesh-side.png" />
</GridContainer>
<figcaption>Gray meshes are simple meshes you can use to start making the space in 3D.</figcaption>
</figure>
Nothing gets easier as the work gets more complex, so it was important to us to experiment often and figure out the environment structure we wanted to create. The earliest iterations of the house were as simple as the images you see above, and the simple shapes allowed us to be as flexible as possible and quickly make changes to the sizes of the rooms as we played through the demo. Scale was also important early on, so we always built with an avatar both in our 3D application and inside Studio so we could know how big a door or how tall the steps should be.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/scale-outside.png" width="94%" />
<figcaption>We wanted both realistic and block characters to feel natural in scale to the exterior of the house.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/scale-inside.png" width="100%" />
<figcaption>When they were inside the house, we wanted everything to feel spacious, but not absurdly large!</figcaption>
</figure>
</GridContainer>
We went through many small and large iterations to settle on what we have in the final demo. Compare the early drafts above with a much later version of the foyer, and you'll see many of the original decisions we made after playing through the house were able to stay!
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/scale-final.png" width="80%" />
<figcaption>Later iteration of the house layout. Most of the layout stayed the same as the early greybox!</figcaption>
</figure>
## Furniture and props
We needed a lot of furniture and props to fill a house of this size, so before we started to gray box any of its content, we made a document to track everything we could use that would compliment what type of room it was and the puzzles we were in the process of creating. By getting all of our thoughts down into one spot, we could organize, prioritize, and see where we could reuse content in different rooms. While this was a fair amount of up-front planning, it saved us from either making not enough content or too many unnecessary items.
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/furniture-spreadsheet.jpeg" width="80%" />
This list gave us a good understanding of what needed to take priority when gray boxing out the content and populating it throughout the house. By adding high-priority content early, we could easily see when assets were repetitious, had incorrect proportions, or when they would need a second variation or a texture swap when they were in certain rooms. Some content we blocked out early on ended up not being important to the experience and removed, but we didn't lose a lot of work in the process since they were only quick blockouts. For all the furniture and props we kept, we made them into [packages](#utilize-packages) in their blockout phase, and this made it easier for us to replace everything throughout the house with their final versions.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/gray-boxed-versions.jpeg" width="80%" />
<figcaption>Early gray boxed versions.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/final-content-versions.jpeg" width="70%" />
<figcaption>Final content versions.</figcaption>
</figure>
</GridContainer>
Because of the sheer quantity of assets needed for this demo, reusing textures as much as we could was extremely important for keeping within our memory budget. A lot of the furniture shares the same trim or tileable sheets for wood, metal, stone, glass, and fabric, and we utilized transparent textures wherever we could. For example, we used a transparent texture for fabrics so we could have color variations without having to create a second Albedo texture. For more information on how to reuse textures, see [Planning, Reuse, and Budgets](../../resources/the-mystery-of-duvall-drive/materialize-the-world.md#plan-reuse-and-budgets).
<figure>
<GridContainer numColumns="4">
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/texture-1.png" width="80%" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/texture-2.png" width="80%" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/texture-3.png" width="80%" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/texture-4.png" width="80%" />
</GridContainer>
<figcaption>Four textures we reused in several rooms.</figcaption>
</figure>
## Photogrammetry
We knew we required a lot more content than we had time to build ourselves, so we decided to speed up the process of creating high-quality content on a 1:1 texture sheet by utilizing photogrammetry sourced from the **[Creative Commons](https://en.wikipedia.org/wiki/Creative_Commons) (CC0)**. **[Photogrammetry](https://en.wikipedia.org/wiki/Photogrammetry)** is the process of capturing hundreds of images of objects from every angle in order to create high-quality models with accurate proportions, and Creative Commons models are mostly free models that are typically educational in nature that you can use and/or share legally. These models can range from hand-modeled assets to assets made from photogrammetry, and working with them in this process made it so we could skip the time-consuming step of creating a detailed high-resolution model.
The backstory we created for the family describes them as "eccentric collectors'' dabbling in the occult. They've gathered artifacts from all over the world, and we wanted the house filled with things to reflect that story. We were able to source the scan data from [CC0 museum content](https://sketchfab.com/nebulousflynn/collections/cc0-9e9b8c5442ab4b59ba16b6fa5e43b8da) for models like [antiques](https://sketchfab.com/3d-models/philips-7-39-radio-a0f95588f0ed472b8e135474c7b2516d), [statues](https://sketchfab.com/3d-models/asklepios-879e7f628ecb4683b23e803242abea62), and [taxidermy](https://sketchfab.com/3d-models/barn-owl-0e65de5934db4f0eae02835ba7227dcc), then begin the UV mapping, retopologizing, and texture work required to optimize them for Studio.
<Alert severity="info">
Content from museums and other similar avenues are great free-to-use artist resources for historically accurate assets, and we want to thank the [museums and all involved](https://sketchfab.com/blogs/community/sketchfab-launches-public-domain-dedication-for-3d-cultural-heritage/) in this project for their important contributions and services.
</Alert>
<figure>
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/original-scan-crow.png" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/original-scan-candle.png" />
</GridContainer>
<figcaption>Original scan data meshes with a Diffuse texture.</figcaption>
</figure>
**[UV mapping](https://en.wikipedia.org/wiki/UV_mapping)** is the 3D modeling process of unwrapping a 3D model's mesh and projecting a 2D image onto it. "U" and "V" represent the axes of the 2D texture because "X", "Y", and "Z" represent the axes of the 3D model. UVs tend to be automated at the time of the scan, and they tend to be poorly done for the purposes of game development. For example, the UVs for certain photogrammetry models can look like a bunch of individual planes, but this isn't the most performant for a mobile experience when creators are limited to 10,000 vertices. Sometimes we would get lucky with the scan data, but most of the time we needed to either update or completely redo these two-dimensional texture coordinates for the purposes of the experience.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/original-scan-automated-uvs.jpeg" width="80%" />
<figcaption>Original scan data with automated UVs.</figcaption>
</figure>
In addition, most meshes from the models would only include the Diffuse texture with lighting information baked into the texture when the model was initially scanned. While this could be helpful to use as a base for the mesh's Albedo texture, it'd still require manual cleanup where we needed to paint out the light and shadow. If a mesh had strong directional lighting, it might look odd where the world lighting doesn't match. For this reason, we had to recreate most PBR textures for the models we used from the CC0 museum content.
<figure>
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/scan-data-texture-1.jpeg" width="80%" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/scan-data-texture-2.png" width="80%" />
</GridContainer>
<figcaption>Original scan data textures with baked in lighting in the diffuse.</figcaption>
</figure>
**[Physically-based rendering](../../art/modeling/surface-appearance.md) (PBR)** is the notion of using realistic shading and lighting by more accurately simulating and representing materials and lighting in the real world. It was important to us when creating PBR textures to either not have any or very little lighting information baked into the [Albedo](../../art/modeling/surface-appearance.md#colormap) texture because Studio's engine calculates this from the Image-based Lighting (IBL), in addition to using the texture's [Normal](../../art/modeling/surface-appearance.md#normalmap), [Roughness](../../art/modeling/surface-appearance.md#roughnessmap), and [Metalness](../../art/modeling/surface-appearance.md#metalnessmap) surface values to correctly represent real world materials. In comparison, a Diffuse texture may have either some or all of the detail from these individual maps baked into one texture, which is helpful when you may need the Diffuse texture to carry that detail on its own without the help of IBL or other individual PBR textures. When you're deciding whether to keep a Diffuse texture or create your own PBR textures, consider your own design or memory budget limits to what makes sense for your own experience.
The meshes of the scan data that we used were usually fairly good, but some meshes needed to be cleaned up. For example, some meshes had problematic holes that needed to be filled, jagged edges that needed to be smoothed out, or thin edges that needed to be thickened up. For this process, we used **[Zbrush](https://www.maxon.net/en/zbrush)** to edit the scan data.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/crow-notes.png" width="80%" />
<figcaption>Notes from left to right: Too thin and Jagged | Whole model needs a bit of polish | Also too thin and jagged. Fill and smooth out. | Fill Holes.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/crow-cleaned-up.png" width="80%" />
<figcaption>The same mesh after we cleaned up the scan data.</figcaption>
</figure>
This process was by no means perfect, but by clearing up the high-resolution model, it was significantly easier to either automatically or manually retopologize the model's mesh. **Retopologizing** is the process of modifying a 3D model's edge distribution and structure, and for our purposes, retopologizing allows for a better texture bake when creating a lower resolution version of a high-resolution model. Lower resolution models are significantly easier on performance, especially on mobile devices, and it still keeps the high-resolution detail from the mesh and textures.
For manually or automatically retopologizing, you can use any 3D application of your choice. We chose to use [3DS Max](https://www.autodesk.com/products/3ds-max/overview) to manually retopologize, and Zbrush to automatically retopologize using the Zremesher or Decimation Tool. The **[Zremesher](https://docs.pixologic.com/user-guide/3d-modeling/topology/zremesher/)** tool allowed us to auto-retopologize a mesh to maintain quads which requires a higher vert count to maintain the shape, while the **[Decimation](https://docs.pixologic.com/user-guide/zbrush-plugins/decimation-master/)** tool allowed us to auto-retopologize using triangles with a lower vert count and was still able to maintain the shape.
<figure>
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/zremesher-crow.png" width="80%" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/zremesher-settings.png" width="60%" />
</GridContainer>
<figcaption>The Zremesher Tool in Zbrush.</figcaption>
</figure>
<figure>
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/decimation-crow.png" width="80%" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/decimation-settings.png" width="60%" />
</GridContainer>
<figcaption>The Decimation Tool in Zbrush.</figcaption>
</figure>
Regardless of the automatic route we could choose for our models/meshes, we likely needed to do some cleanup since the auto-retopologizing greatly relies on the high-resolution mesh, how it was constructed, and its algorithm. We tended to choose the decimation route with a triangulated mesh because it was more important to us to have a lower vertex count while maintaining the original shape.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/overlapping-crow.png" width="80%" />
<figcaption>Overlapping and bad topology causes surface normal errors that can occur when auto-retopologizing. This needed to be fixed, otherwise we'd have mesh and lighting issues on the model.</figcaption>
</figure>
<figure>
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/hand-draw-crow.png" width="60%" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/freeform-settings.png" width="80%" />
</GridContainer>
<figcaption>The process of hand drawing out the faces adhering to the high-resolution's surface normals.</figcaption>
</figure>
After we completed the process of retopologizing the mesh with new UVs, we needed to then bake and transfer the high-resolution mesh detail and textures. **[Marmoset](https://marmoset.co/)** made it easy to do this process when baking mesh maps and transferring any original textures from the high-resolution model to low-resolution, but you may find other workflows and methods in other 3D applications that work better for you. Regardless of the tool, ensure any original textures on the high-res model is assigned to the model when baking.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/assigning-textures-crow.png" width="80%" />
<figcaption>Assigning original textures on the high-res model to the crow model when baking.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/pbr-textures-1.png" width="80%" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/pbr-textures-2.png" width="80%" />
<figcaption>New UVs and 512 sized PBR textures with painted out lighting.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/retopologized-crow.png" width="80%" />
<figcaption>Retopologized by hand with transferred textures. Doing this by hand meant we could control the way the geometry flowed and where we spent vertices. A lot of times it's a better choice, especially when a high-res mesh is made up of a lot of separate objects or has problematic shapes.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/retopologized-candle.png" width="80%" />
<figcaption>Retopologizing automatically in a 3D application. Doing this automatically saved the time of creating the low-resolution mesh by hand, but the trade off was a mesh with higher vertex count to maintain the silhouette and a more challenging model to work with when creating UVs</figcaption>
</figure>
After we finalized our Albedo, Normal, Metalness, and Roughness PBR textures in **[Substance Painter](https://www.substance3d.com/)**, our new assets were ready to be imported into Studio! There were some quality differences between the scan data assets vs. the retopologized assets, but it was ultimately a worthy trade-off when the asset was only 1,000 vertices instead of 1,000,00. It also kept our memory budget intact and allowed the experience to run more smoothly.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/scan-data-crow-final.png" width="80%" />
<figcaption>Scan data raven.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/final-crow-asset.png" width="70%" />
<figcaption>Manually retopologized in-game asset.</figcaption>
</figure>
</GridContainer>
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/scan-data-candle-final.png" width="80%" />
<figcaption>Scan data candelabra.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/final-candle-asset.png" width="70%" />
<figcaption>Automatically retopologized in-game asset.</figcaption>
</figure>
</GridContainer>
## Utilize packages
**[Packages](../../projects/assets/packages.md)** are a system that allows you to store and use a version of either a single object or a group of objects, and when you update that version, it updates all instances of that object or group at once. For example, if you make a tree object and place hundreds of that object within your experience, you only need to update the package in order for the hundreds of trees to update to your new version at once. We knew early on we would have **two states** of most rooms in the house, their normal state and their corrupted state, and we needed to maintain both copies of the house and their contents throughout the entire iteration process. For this reason, we decided on what objects needed to be shared in each house state and converted them into packages.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/package-entire-bedroom.png" width="80%" />
<figcaption>Example of a package for an entire bedroom. Anything that was unique to its normal or corrupt state lived outside of this package.</figcaption>
</figure>
Packages allowed us to modify everything from the wood trim on wall panels to entire rooms, then both states of the room would update to our changes. We were also able to convert `Class.SurfaceAppearance` objects to packages so we could update a material's appearance everywhere throughout the experience at once. Reusing materials was instrumental to make the demo as performant as possible for devices despite its clear complexity.
For example, the following images display an armillary model package with a complex group of child assets, including a metal [surface appearance](../../art/modeling/surface-appearance.md) package that we use in several places within the demo. Wherever we updated the metal surface appearance package, we could also perform a [mass update](../../projects/assets/packages.md#mass-updates) that would force the armillary model package to update the child surface appearance package too.
<GridContainer numColumns="3">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/armillary-asset.png" />
<figcaption>Armillary Asset</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/armillary-model-package.png" />
<figcaption>Armillary Model Package</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/metal-surface-appearance-package.png" />
<figcaption>Metal Surface Appearance Package</figcaption>
</figure>
</GridContainer>
For more information on how to turn an individual object or a group of objects like `Class.Model|Models` or `Class.Folder|Folders` into a package, or for how to choose a creator of the package for edit access, see [Packages](../../projects/assets/packages.md). We chose to designate the Vistech Demo Group as the creator of all of our packages because it let everyone in our group access and edit the packages whenever they needed to. It also allowed us to share these packages between every experience the group owned!
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/convert-to-package-dialog.png" width="60%" />
We set parent packages, such as the armillary above or an entire room, to [automatically update](../../projects/assets/packages.md#automatic-updates) so that the changes to the model or folder automatically shared across all instances of that package without us having to manually check. This process meant we could smoothly create changes for each room's normal and corrupt states. For example, the following two images show the study's normal and corrupt state that were created through the **same** package. Only a few items outside of their packages differ between them, even considering that the corrupt state is sideways!
<figure>
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/package-normal-state.png" width="93%" />
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/package-corrupt-state.png" />
</GridContainer>
<figcaption>Using packages allowed us to make two versions of a room, while only dealing with the content of one.</figcaption>
</figure>
Instead of working entirely in the same place file as the experience, we built an asset storage place with all our components with package links. Whenever we made a change to a package in this storage place, it would update automatically and immediately across **every place** that uses that package. We could modify objects, add functions, and work on details without having to navigate the main experience's mood and lighting that could make it hard to see what we were doing.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/asset-storage-place.png" width="80%" />
<figcaption>The asset storage place with every component within the demo.</figcaption>
</figure>
Packages allowed us more freedom to explore different ideas in 3D without having to do a lot of extra work. We could plan, test, or even [revert to any previous version](../../projects/assets/packages.md#revert-changes) of an asset if an experiment didn't pan out in practice or broke the experience. There were many times where hours worth of work were restored by going back a version in the packages. In the end, we couldn't have achieved the final level of polish in this demo without packages.
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/final-foyer.png" width="60%" />

View file

@ -0,0 +1,115 @@
---
title: Design dark soundscapes
comments:
prev: /resources/the-mystery-of-duvall-drive/develop-a-moving-world
description: Explains the design concepts for gloomy soundscapes in The Mystery of Duvall Drive.
---
In order for The Mystery of Duvall Drive to feel authentic and plausible, we made sure that ambience was king at every stage in development. We included scripted audio events, music, character sounds, and point-source audio to support our "grass is always greener" approach, meaning that we always wanted there to be a more intriguing sound in the distance no matter where the player is located in the house so that they would want to further investigate each room. These sounds ranged anywhere from a radio playing music in a bedroom, the kitchen sink dripping, rain hitting the attic roof, or the oscillating ritual orb in the center of the house.
Sound design for an experience with horror themes is a balancing act between embedding both realistic and abstract audio. While realistic audio provides a sense of immersion and authenticity to the world, abstract audio drives the player's attention to the otherworldly elements that pull the story forward. Both audio types have their strengths separately, but together they can create a dark soundscape that feels authentic, plausible, and horrifying. In this section, we'll show you how and why we used volumetric audio and nested `Class.SoundGroup` mixing to balance our dark soundscape and bring life to our 3D space.
<img src="../../assets/resources/mystery-of-duvall-drive/designing-dark-soundscapes/overview.png" width="100%" />
## Volumetric audio
**[Volumetric audio](../../sound/objects.md#volumetric)** is the most realistic audio option in Studio to how people perceive sound depending on its distance to their ears. This type of audio allows you to place a `Class.Sound` object on a `Class.BasePart`, then use that `Class.BasePart` to cover an entire area of where you want that audio to play. It's highly useful for large objects that make sound, such as waterfalls, vehicles, and large cities. When a player is within a `Class.BasePart` that has a child `Class.Sound` object with volumetric audio enabled, the audio plays all around the player, similar to music in headphones playing at the same volume in each speaker. When the user exits the object, audio gradually decreases in volume and becomes more directional per speaker, moving around the user's head when their listener rotates. This means that if the sound is emitting to the left of your listener, the sound plays louder in your left speaker than your right speaker.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/designing-dark-soundscapes/audio-volume.png" width="60%" />
<figcaption>Example of an audio volume that plays intermittent creepy human whispers. This reinforces a dark soundscape, setting the stage for the mood inside the house.</figcaption>
</figure>
A player's experience with audio is not only dependent on what sounds they can hear, but also **which direction** those sounds come from. In the real world, somebody yelling your name from across a field sounds a lot more directional than something like the soft hum of the air conditioning in a room or the reverb in a cave. This is because sound waves are bouncy, and when they hit solid objects like walls, pavement, and trees, they scatter and reflect back to your ears from different directions. In fact, sound waves can even bend around objects like your own head!
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/designing-dark-soundscapes/volumetric-audio-immersive.png" width="60%" />
<figcaption>In this section of the house, volumetric audio allows you to immerse yourself in the sounds of rain hitting nearby windows, the crackling of a fireplace, and a soft tune being played through an old radio nearby.</figcaption>
</figure>
Before volumetric audio, you were limited to having your audio emit from a [single point in space](../../sound/objects.md#point-source), or having it [play back in both headphones like music](../../sound/objects.md#background-audio). Because we wanted our audio to be as realistic as possible while representing a wide range of sound types, neither of these previous implementations would have been sufficient for creating the immersive world that we wanted to capture in this experience. As a result, we used volumetric audio emitters for the vast majority of our sound effects, including the outdoor rain, interior room tones, corrupted zone ambiences, television static, and rain hitting various surfaces. If we were to disable volumetric audio on the project, all of these sounds would collapse down to a single point of emission at their center of mass, and the entire soundscape would shift between the player's ears whenever they rotate their camera. This would make the entire space feel unrealistic and digital, and the sound's movement would be quite jarring and undesirable for the player.
<figure>
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/designing-dark-soundscapes/exterior-audio-hierarchy.png" width="80%" />
<img src="../../assets/resources/mystery-of-duvall-drive/designing-dark-soundscapes/sound-emitters-hierarchy.png" width="80%" />
</GridContainer>
<figcaption>Just a few examples of how we used volumetric audio to create our exterior ambience, as well as rain colliding with various materials in the backyard.</figcaption>
</figure>
Volumetric audio emitters allowed us to control the directionality of our soundscape to give a real sense of movement through the space. Because their influence is spread over a larger area and the audio spreads naturally to both ears, they give the experience's overall sound bed, or constant hum of audio activity, a far more predictable and stable volume level. This prevents the experience from being silent when the player's character isn't doing anything, and it makes the experience feel realistic as there are very few spaces in real life completely void of noise!
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/designing-dark-soundscapes/volumetric-audio-emitter-greenhouse.png" width="50%" />
<figcaption>We placed a volumetric audio emitter on the greenhouse to add immersive ambient sounds of the raindrops hitting the glass roof and echoing throughout the room.</figcaption>
</figure>
When adding detail to our outdoor soundscape, it was important to make the rain feel alive instead of a simple looping rain sound effect. One of the ways we achieved this was by adding rain surface sounds to various objects so that you could hear the storm's impact on your immediate surroundings. In this project, there are generic outdoor rain sounds, but also volumetric audio emitters for rain hitting objects like windows, plastic, and rooftops. For example, we added a volumetric audio emitter to a car through the following process to give players the impression that raindrops were impacting the entire surface of the car instead of just a single point on the car's hood:
1. In the **Explorer** window, we selected **SoundService**.
1. In the **Properties** window, we navigated to the **VolumetricAudio** property, then set it to **Enabled**. This turned on the volumetric sounds behavior for the entire project.
1. In the viewport, we created a new `Class.Part` that matched the size of the car. This defined the entire region we wanted to sound to emit from.
1. In the **Properties** window, we set the following properties for the part to ensure that our part didn't fall through the ground, collide with the car model, or obscure the visibility of the car:
- `Class.Part.Anchored` = **Enabled**
- `Class.Part.CanCollide` = **Disabled**
- `Class.Part.Transparency` = **`1`**
1. We then added a `Class.Sound` object to the invisible part and set the following properties for the `Class.Sound` object so the raindrop sound started playing when players would load into the experience, looped continuously, and became audible when a player was 60 studs away/reached its maximum volume at 15 studs away:
- `Class.Sound.SoundId` = **`9178663282`**
- `Class.Sound.Playing` = **Enabled**
- `Class.Sound.Looped` = **Enabled**
- `Class.Sound.RollOffMaxDistance` = **`60`**
- `Class.Sound.RollOffMinDistance` = **`15`**
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/designing-dark-soundscapes/car-part-volume.png" width="60%" />
<figcaption>The edges of our part can be seen in white outlining the car.</figcaption>
</figure>
This process allowed the sound to emit from the entire surface of the invisible part. If the player was inside of the part, the raindrop audio would play all around the player's head. The larger you make your own volumetric audio emitters, the more spread and area the volumetric sound will cover, so experiment with different sizes and shapes for your emitter to get the soundscape you want for your own experiences!
## Nested SoundGroup mixing
As human beings, we like to think we are great multitaskers, but in reality it's quite difficult for us to focus on more than 2 things at once, especially in regards to listening to multiple audio sources! In The Mystery Of Duvall Drive, there could be a dozen sounds playing at any given time, such as ambience, music, footsteps, rain, or UI notifications, but we needed a way to make sure that it didn't feel as though they were all competing for your attention. For this reason, we decided to nest `Class.SoundGroup|SoundGroups` in child/parent relationships so we could create an audio mix to ensure both the organization and consistency of the project's sound design.
**[Nesting SoundGroups](../../sound/groups.md#nest-soundgroups)** is a new feature that allows you to organize `Class.SoundGroup|SoundGroups` together into meaningful categories under a mix tree for further audio functionality, such as being able to fine-tune the volume of many audio sources at once, or apply common audio effects at a lower CPU cost. As we took a very progression-oriented approach to the mix in this project, we wanted sounds that directly contribute to player progression in the puzzles (puzzle and key object interaction audio) to be given priority over sounds that don't contribute to their progression (wind, rain, footsteps, atmosphere audio). By nesting child `Class.SoundGroup|SoundGroups` into parent `Class.SoundGroup|SoundGroups` of different priority levels, we could then add a `Class.CompressorSoundEffect` that would lower the volume of low-priority `Class.SoundGroup|SoundGroups` in real time whenever audio from high-priority `Class.SoundGroup|SoundGroups` started playing. This process is called [ducking](../../sound/groups.md#ducking).
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/designing-dark-soundscapes/main-audio-mixer.png" width="80%" />
<figcaption>A collapsed view of our main audio mixer for this experience. </figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/designing-dark-soundscapes/compressorsoundeffect-properties.png" width="80%" />
<figcaption>The Properties window for the CompressorSoundEffect that we added to the low-priority SoundGroup.</figcaption>
</figure>
</GridContainer>
The `Class.CompressorSoundEffect` has five main properties we had to set in order to lower the volume of low-priority audio whenever high-priority audio played:
<table>
<thead>
<tr>
<th>Property</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>`Class.CompressorSoundEffect.Threshold`</td>
<td>How sensitive the compressor is to the incoming signal to lower the volume from a `Class.SoundGroup`. The lower the number, the less loud the high-priority sound has to be to start taking effect. </td>
</tr>
<tr>
<td>`Class.CompressorSoundEffect.Ratio`</td>
<td>How much you want to lower low-priority sounds. A higher number basically means more volume ducking!</td>
</tr>
<tr>
<td>`Class.CompressorSoundEffect.Attack` and `Class.CompressorSoundEffect.Release`</td>
<td>How quickly the volume reduction fades in and out when activated. There are no hard and fast rules here, tune them to fit your soundscape using your own judgment and ears! </td>
</tr>
<tr>
<td>`Class.CompressorSoundEffect.Sidechain`</td>
<td>Which `Class.SoundGroup` we want to duck the low-priority sounds, as we have placed the compressor on the **Low** `Class.SoundGroup`. This is what makes the magic happen! </td>
</tr>
</tbody>
</table>
For more information on these properties and the whole process of prioritizing audio from different `Class.SoundGroup|SoundGroups`, see [Nesting SoundGroups](../../sound/groups.md#nest-soundgroups) and [Ducking](../../sound/groups.md#ducking).

View file

@ -0,0 +1,582 @@
---
title: Develop a moving world
comments:
next: /resources/the-mystery-of-duvall-drive/design-dark-soundscapes
prev: /resources/the-mystery-of-duvall-drive/stream-in-immersion
description: Explains the environmental design concepts used in The Mystery of Duvall Drive.
---
Creating movement in any environment within an experience helps it to instantly feel more immersive and realistic to our world, whether that's from ambient tree movement, reactive doors from player interaction, or even boxes that move when they bump into them. Studio has many unique methods to create motion in order to help worlds feel more alive, including a physics system, `Class.TweenService`, and animations, and analyzing your experience's specific needs can help you determine which one to use. In this section, we'll demonstrate how we determined what type of movement we wanted to create in Studio, and what tools we used to accomplish these distinct goals.
<img src="../../assets/resources/mystery-of-duvall-drive/streaming-in-immersion/perspective-house.png" width="100%" />
## Create the storm
The storm went through many iterations before we settled on what is live in The Mystery of Duvall Drive. Early on, we thought about the storm as a giant obsidian pillar, and in later iterations we considered it to be a giant portal to the corrupt space. After experimenting with many different storms with unique looks and feels to them, we settled on a storm with a smaller central "eye" because:
- The storm should give players a sense of the **impact of this event on the world**, including trees blowing and debris flying around.
- The spinning vortex of the cloud themselves should give players a peek at the central portal **without revealing everything**. This would encourage players to investigate closer to see what's going on.
- The tighter point of light would allow us to **focus on the composition of the house**, which is both the main character and where most of the gameplay is located.
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/storm-iteration-1.png" />
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/storm-iteration-2.png" />
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/storm-iteration-3.png" />
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/storm-iteration-4.png" />
</GridContainer>
To make the storm feel dynamic, aggressive, and ever-changing within its environment, we used the following systems and features:
1. **`Class.TweenService`** - For cloud movement.
1. **Lighting changes** - For creating the cloud to cloud lightning.
1. **[Beams](../../effects/beams.md)** - For the "volumetric lighting" and the lightning bolts.
1. **[Particle Emitters](../../effects/particle-emitters.md)** - For debris flying up to the portal and flying around due to the wind blowing.
1. **[Animations](../../animation/index.md)** - For the trees that were blowing in the wind.
### Add clouds with textures
While [dynamic clouds](../../environment/clouds.md) are great for normal, high altitude realistic clouds, we needed something that felt dramatic and that we could more heavily direct and customize. To do this, we applied [surface appearance](../../art/modeling/surface-appearance.md) objects with semi-transparency to a series of heavily stacked and layered cloud meshes in order to fake cloud cover. Why did we stack them and layer them so heavily? Because when each cloud mesh moves at different speeds, they intersect and create cloud forms that go inside and out of each other. This process made the clouds feel a bit more dynamic and natural, despite just being spinning discs. It was also important that the clouds were [semi-transparent](../../resources/beyond-the-dark/layered-clothing.md#import-to-studio-1), because we wanted players to be able to peek through them to see something bright in the center prior to arriving at the house!
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/single-cloud-mesh.png" width="80%" />
<figcaption>A single cloud mesh.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/layered-cloud-meshes.png" />
<figcaption>Layered cloud meshes without their textures!</figcaption>
</figure>
</GridContainer>
Since each cloud mesh needed to be massive to fully surround the house and convey how enormous the storm was, we knew we needed to tile the texture we wanted to use on the individual cloud meshes so that it would heavily repeat throughout the surface of the mesh. We tested the materials we made for the cloud on these simple parts, then applied them to the vortex!
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/cloud-texture-on-parts.png" width="60%" />
Unlike particle emitters or beams, meshes allowed us to be able to bounce light off of each mesh, which was important when we wanted to implement cloud-to-cloud lightning. We also modeled in the twisting so that lighting bouncing off of it would look like it had depth! This was important especially in situations where the performance demands of the experience dropped the quality levels of our surface appearance objects.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/cloud-shading.png" width="60%" />
<figcaption>Once we started adding lighting to it, we needed to add details to the meshes to make them react better to lighting!</figcaption>
</figure>
### Rotate cloud meshes
After we were satisfied with the overall visual appearance of the clouds, we needed to get it moving! We had the general shapes of each cloud layer in place, but it took some trial and error to make sure the spinning effect looked good in practice. We initially tried using [constraints](../../physics/mechanical-constraints.md) to introduce velocity that would physically drive the clouds to move. This was more difficult than we wanted it to be to iterate later, and the player would never interact with it, so we didn't need it to be as accurate in its movement.
We wanted an easy-to-use method to rotate instances that were either too far to be interactable, such as clouds, or too small or decorative to be important for gameplay/physics, such as indoor furniture like small lamps. We decided to use a `Class.LocalScript` to reduce client-server bandwidth, allow for a smoother movement, and have each cloud mesh be able to have a different rate of rotation and delay. To make it more generic, we also made it possible to specify the axis of rotation. It is possible to use 3 attributes, but for our case we used 3 values: `Axis`, `Delay`, and `Speed`.
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/main-vortex-settings.png" width="30%" />
As in many cases in the demo, we used a `LocalSpaceRotation` tag so that we were able to manage affected instances in Studio using an instance tagging plugin. We utilized only a single `Class.LocalScript` that handled all tagged instances using the `Class.CollectionService` so that we didn't have a ton of scripts to maintain throughout the development process.
In our demo, portions of the world are cloned from the `Class.ServerStorage` into the workspace as needed, so we needed to handle cases where tagged objects were created and destroyed. With `Class.LocalScript|LocalScripts`, we also have to be aware of the streaming, where meshes and their child values might be streamed in and out. We processed initially placed objects in the `Init()` function, and connected to `Class.CollectionService.GetInstanceAddedSignal` and `Class.CollectionService.GetInstanceRemovedSignal` for tagged objects to handle newly created/destroyed objects. The same `SetupObj` function was used to initialize new objects in `Init()` and in `Class.CollectionService.GetInstanceAddedSignal`.
```lua
local function Init()
for _, obj in CollectionService:GetTagged("LocalSpaceRotation") do
if obj:IsDescendantOf(workspace) then
SetupObj(obj)
end
end
end
CollectionService:GetInstanceAddedSignal("LocalSpaceRotation"):Connect(function(obj)
objInfoQueue[obj] = true
end)
CollectionService:GetInstanceRemovedSignal("LocalSpaceRotation"):Connect(function(obj)
if objInfo[obj] then
objInfo[obj] = nil
if objInfoQueue[obj] then
objInfoQueue[obj] = nil
end
end
end)
Init()
```
`objInfo` is a map that has information for all relevant objects, such as their rotation speed and axis. Note that we do not call `SetupObj` from `Class.CollectionService.GetInstanceAddedSignal` immediately, but we added an object into `objInfoQueue`. With streaming and cloning objects on the server, when `Class.CollectionService.GetInstanceAddedSignal` is called, we might not yet have had our `Axis`, `Delay`, and `Speed` values, so we add the object into a queue, and called `SetupObj` on subsequent frames from the `Update` function until the values were there and we could read them into per object "info" structure.
We rotated the instances in the `Update` function connected to heartbeat. We got the parent transform (parentTransform), accumulated a new rotation angle (`curObjInfo.curAngle`) based on rotational speed of this object, calculated the local transform (`rotatedLocalCFrame)`, and finally set it to the `Datatype.CFrame`. Note that both parent and the object can be a `Class.Model` or `Class.MeshPart`, so we had to check IsA("Model") and use either a `PrimaryPart.CFrame` or `Datatype.CFrame`.
```lua
local parentTransform
if parentObj:IsA("Model") then
if not parentObj.PrimaryPart then
-- primary part might might not be streamed in yet
continue -- wait for primary part to replicate
end
parentTransform = parentObj.PrimaryPart.CFrame
else
parentTransform = parentObj.CFrame
end
curObjInfo.curAngle += dT * curObjInfo.timeToAngle
local rotatedLocalCFrame = curObjInfo.origLocalCFrame * CFrame.Angles( curObjInfo.axisMask.X * curObjInfo.curAngle, curObjInfo.axisMask.Y * curObjInfo.curAngle, curObjInfo.axisMask.Z * curObjInfo.curAngle )
if obj:IsA("Model") then
obj.PrimaryPart.CFrame = parentTransform * rotatedLocalCFrame
else
obj.CFrame = parentTransform * rotatedLocalCFrame
end
```
We checked for a valid `Class.Model.PrimaryPart` to be set to handle streaming. If an Update was called on our object while a `Class.Model.PrimaryPart` (that can point to a child mesh) wasn't streamed yet, we would just skip the update. The current system is a second iteration of object rotation, and the previous system worked differently: the values were 12 times different! To keep the same data, we converted it in our script, like "12 \* obj.Speed.Value".
### Design lightning strikes
Because Studio doesn't offer an out of the box lightning generator, and the particle system had some limitations that wouldn't work for the hero lightning strikes, we had to get creative with a solution for the hero lightning strikes. We decided on two main systems to make up the lightning: textured beams for the hero lightning strikes coming from the eye of the storm are scripted textured beams that reveal and sync with audio and post process effects, and a simple particle effect for the distant cloud-to-cloud lightning.
#### Texture beams
We'd typically either use a sequencer or timeline tool to drive the timing of a lighting bolt strike effect like this, but since Studio doesn't offer this functionality yet, we decided to write scripts that would control lighting bolt timing. The scripting of this effect is fairly simple, but it accomplishes the following important goals:
1. Elements of the lightning bolt strikes, such as their textures, brightness, and delays, are randomized with every strike.
1. Audio and post FX changes are in sync with strike FX.
1. Players that are either indoors or in the corrupt area would not be able to see or hear them.
We have a server-side `Class.Script` that calculates various parameters and timings, sends them to all clients, and waits for a random amount of time:
```lua
local function LightningUpdate()
while true do
task.wait(rand:NextNumber(3.0, 10.0))
local info = CreateFXData()
lightningEvent:FireAllClients(info)
end
end
```
Inside `CreateFXData`, we fill in the info structure, so that all clients get the same parameters.
On the client side (**LightningVFXClient**), we check if this client should run the FX:
```lua
local function LightningFunc(info)
…
-- no FX when indoors
if inVolumesCheckerFunc:Invoke() then
return
end
-- no FX when not in the "normal" world
if not gameStateInfoFunc:Invoke("IsInNormal") then
return
end
…
```
In addition, we run the sequence to set textures, positions, and brightness, run tweens, and use `Library.task.wait()|task.wait(number)`. Randomized parameters are from the info structure that we received from the server, and some numbers are fixed.
```lua
beam.Texture = textures[info.textIdx]
beamPart.Position = Vector3.new(info.center.X + og_center.X, og_center.Y, info.center.Y + og_center.Z)
-- Wipe
beam.Brightness = 10
ppCC.Brightness = maxPPBrightness
ppBloom.Intensity = 1.1
bottom.Position = top.Position
tweenBrightness:Play()
tweenPPBrightness:Play()
tweenPPBrightness:Play()
tweenBottomPos:Play()
tweenBrightness.Completed:Wait()
-- audio
if audioFolder and audioPart then
if audioFolder.Value and audioPart.Value then
audioUtils.PlayOneShot(audioObj, audioFolder.Value, audioPart.Value)
end
end
task.wait(info.waitTillFlashes)
-- and so on
```
To check if a player is indoors we use a helper `inVolumesCheckerFunc` function, which goes over pre-placed volumes approximating indoor areas, and checks if player position is inside any of them (PointInABox). We could have used touch-based detection, but we found out that when a player takes a seat inside the volume, they are no longer "touching" the volume. Testing a point in a few boxes is simpler, and we do it only when a player moves far enough from the previously tested position.
To check if a player is in corrupt areas, we invoke a helper `gameStateInfoFunc` function, which checks the current game state. To play a random sound from a folder, we also used a helper `PlayOneShot` function. For the lightning bolts themselves, these were super easy to create in Photoshop; we drew a squiggly line, then added an "Outer Glow" layer effect.
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/lightning-bolts.png" width="40%" />
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/lightning-bolt-photoshop-settings.png" width="60%" />
</GridContainer>
#### Utilize particle emitter systems
The hero lightning strikes are supported by a particle system that suggests distant lightning by creating the impression of a layer of clouds in the background catching light from distant strikes, or cloud-to-cloud lighting. We achieved this effect through a very simple particle system which flashes a cloud billboard on the periphery of the main storm cloud. The system emits a cloud particle periodically with a randomized transparency curve:
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/cloud-billboard.png" width="40%" />
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/particle-emitter-settings.png" width="100%" />
### Make trees blow in the wind
After we had the clouds and the lightning working the way we wanted it to, we then needed to add two other major components of a storm: the wind and the rain! These elements presented a few challenges, including needing to work within Studio's current limitations of our physics and special effects systems. For example, making trees move with actual wind isn't possible in today's engine, so we utilized [particle emitter](../../effects/particle-emitters.md) effects and [custom character animations](../../animation/editor.md#create-an-animation) for the trees.
We knew to really sell the effect of the wind and rain, we needed the trees themselves to move. There are a few ways you can do this within the engine, including moving parts using [plugins](../../studio/plugins.md) that are publicly available, using `Class.TweenService`, or animating models directly. For our purposes, animations gave us the ability to control the motion we wanted out of our trees, and it allowed us to use a single animation we could share among all trees within the experience.
<Alert severity="warning">
There are pros and cons with any of these systems, and one of the cons for animations specifically skinned meshes are not currently instanced. This means that the more skinned meshes you use in your experience, the greater the impact on performance because of the memory cost, so we used them sparingly for the effect we wanted. At some point this will be resolved, so be sure to check the [rigging and skinning documentation](../../art/modeling/rigging.md) for the most up-to-date information.
</Alert>
We started by skinning several trees from the [Endorse Model Pack - Forest Assets](https://www.roblox.com/library/6432306802/Forest-Pack). Since these trees already existed, and our experience took place in the Pacific Northwest, it saved us some time early on from having to create each tree model.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/forest-pack.png" width="60%" />
<figcaption>The Forest pack contains several tree types, which can save you time in your own experiences.</figcaption>
</figure>
After we picked our trees, we knew we needed to skin them. [Skinning a mesh](../../art/modeling/rigging.md) is the act of adding joints (or bones) to a mesh in another 3D modeling application, such as [Blender](https://www.blender.org) or [Maya](https://www.autodesk.com/products/maya/overview), then applying influence to those joints/bones to move the mesh. This is most commonly used in [humanoid characters](../../art/modeling/skin-a-humanoid-model.md), but with [custom characters](../../art/modeling/skin-a-simple-mesh.md), you can skin pretty much anything.
We knew we wanted to save time and reuse the same animation, so we built our first tree rig and made sure the joint names were generic because we wanted to use these same names in the rigs for the other trees. We also knew we needed to include primary, secondary, and tertiary joints/bones for the trunks to bend with the wind, the branches to swing, and the leaves to seem like they were shaking in response. For this process, we needed to create a **secondary motion**, which is an animation concept where any action causes other parts of the object to react to that action and appear to catch up with the initial movement.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/tree-joints.png" width="80%" />
<figcaption>The trees have primary, secondary, and tertiary joints so that we could have believable movement from being blown around by the wind.</figcaption>
</figure>
Once we had created our joints/bones, it was time to create a test animation to move around all the joints and bones in Studio to see if it moved the way we wanted it to. To do this, we had to [import the tree into Studio](../../resources/beyond-the-dark/custom-characters.md#import-the-mesh-to-studio) through the **Custom Rig** setting in the **3D Importer**, then move/animate the mesh using the [Animation Editor](../../resources/beyond-the-dark/custom-characters.md#import-the-mesh-to-studio). We set up the materials and textures after these tests, but you can see the result below.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/tree-hierarchy.png" width="80%" />
<figcaption>The same hierarchy within Studio.</figcaption>
</figure>
After we were happy with the results on that tree, it was time to test the same animation on a different tree! We already knew it was going to be the same animation between the different rigs for each tree type, so we just made sure our animation looked like it was general enough to work between a tall Redwood and stout Beechwood tree!
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/tree-animation-import.png" width="80%" />
<figcaption>The animation we imported on the Redwood tree.</figcaption>
</figure>
To do this, we took the Beechwood tree from that Forest Pack and built a similar rig, using the same exact naming for the joints. This was so the animation we had previously imported could be applied to this tree too. Since the animations were all based on rotating joints, it didn't matter how big, small, tall or wide the tree was!
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/beechwood-joint-animation.png" width="80%" />
<figcaption>The Beechwood tree has the same exact naming for its joints, just not the same amount. This is fine since the animation system will only apply animation to those specific joints that match the name in it! For this reason, we could apply the same animations to anything that matched the joint names!</figcaption>
</figure>
After we [rig and skin](../../art/modeling/rigging.md) the Beechwood tree, we could then import it and apply the exact same animation. This meant iterating and editing only needed to be done on one file, and it also saved on performance with fewer animations when running the experience.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/redwood-tree-animation.png" width="80%" />
<figcaption>Using the **Animation Editor**, we could apply the same Redwood tree animation to the Beechwood tree!</figcaption>
</figure>
Once we had all the tree types we wanted animated, we made each into [packages](../../projects/assets/packages.md) so we could continue to edit and update while playing several of the animations around the main area of the experience. Since we knew they had a performance cost, we used them sparingly around the house where the effect was most valuable! In the future as this becomes more performant, you'll be able to add more and more skinned mesh instances!
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/animated-trees.png" width="80%" />
<figcaption>We used animated trees immediately around the house where the vortex was strongest and the visual effect would be the most impactful for players.</figcaption>
</figure>
### Make storm debris
We wanted the rain to appear heavy, and for the fog and debris to blow through the trees. To do this, we set up a few invisible parts to act as particle volumes with child [particle emitters](../../effects/particle-emitters.md) immediately below the large storm clouds. Because of the particle count limit in Studio, we couldn't use one particle emitter for the entire space. Instead we added several that were the same size as each other in a grid pattern over the playable area space, because the presence of the trees means the players wouldn't be able to see very far.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/part-volumes.png" width="100%" />
<figcaption>We used several volumes to both get the amount of rain, and the specific coverage of rain that we wanted.</figcaption>
</figure>
The rain particles leveraged a new particle emitter property `Class.ParticleEmitter.Squash` that allows you to make a particle longer, or squatter. It is particularly useful for rain because it meant we didn't need a large rain texture, just stretch the one that was there. Just know that if you increase the value of `Class.ParticleEmitter.Squash`, you may need to increase the overall `Class.ParticleEmitter.Size` property too so that it's not too skinny! Overall, it was just a matter of playing around with the values until we got the rain heavy enough, but not so much that it blocked the visibility of the experience!
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/squash-3.png" />
<figcaption>A Squash value of 3 starts to stretch the texture longer.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/squash-20.png" />
<figcaption>A Squash value of 20 stretches the particles much longer, but we also needed to increase the Size value too.</figcaption>
</figure>
</GridContainer>
For the mist, fog, and leaves blowing through, it was much simpler to add a single larger part volume covering fewer areas because we didn't need a ton of particles running at one time. We started by setting up a volume and got the frequency of the particles where wanted them.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/debris-volumes.png" width="98%" />
<figcaption>There ended up being a few particle part volumes so we didn't have particles entering the house, and because we didn't feel they needed to move through the trees like the fog did.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/fog-particle-volume.png" />
<figcaption>The fog particle part volume was much larger since the particles were large, and we didn't need to be as precise with location.</figcaption>
</figure>
</GridContainer>
After that, we made our leaf blowing and wind textures, and set the particles to all rotate/move at different rates and start at different speeds. This meant that the larger fog particles would interact more naturally and not look so much like a repeating texture, especially given their size.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/fog-particle.png" width="40%" />
<figcaption>Fog particle</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/leaf-particle.png" width="40%" />
<figcaption>Leaf particle</figcaption>
</figure>
</GridContainer>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/particle-emitter-properties.png" width="30%" />
The result was some great action between the trees moving, the window blowing, and the lightning to create the effect of the storm surrounding the central eye of the storm.
<video
controls
src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/movement-example.mp4"
width="100%"></video>
### Set up the eye of the storm
The fractured stone eye with a glowing core is meant to give players the first hint that there is something sinister and arcane occurring at the house that they should explore further. Since our scene is dark and the eye is far up in the sky, it was important to create a believable fractured stone silhouette, but it wasn't as important to create believable stone surface details because players wouldn't be able to see that. Knowing what is realistic for your players to see within your scene's lighting before putting in a ton of time into unnecessary details can save you many resources in the development process.
<figure>
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/eye-of-the-storm-1.png" width="99%" />
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/eye-of-the-storm-2.png" />
</GridContainer>
<figcaption>Setting up the final lighting in your scene early on can save you a lot of unnecessary work. You wouldn't be able to see surface details on the rings with our scene's final lighting so there was no need to spend the time putting them there!</figcaption>
</figure>
The distance from the player also meant that we could rely entirely on a normal map for the surface details of the eye so the mesh is just a plain sphere! We sculpted the details into a high poly mesh and baked its normal map onto a much lower poly sphere so that we could get all that beautiful detail without the massive performance cost.
<GridContainer numColumns="3">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/high-poly-sculpt.png" />
<figcaption>High poly sculpt</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/low-poly-mesh.png" />
<figcaption>Low poly mesh</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/low-poly-mesh-with-normal-info.png" />
<figcaption>The low poly mesh with the normal information from the high poly sculpt baked in</figcaption>
</figure>
</GridContainer>
In order to add a supernatural feeling to the eye and to emphasize its presence, we decided to create a glowing, neon magma that would seep through its cracks. While there's no emissive channel for surface appearance, we overcome this hurdle by creating the eye out of 2 spheres: one for the rocky outside surface and a second, slightly smaller one for the glowing magma. In [Substance Painter](https://www.substance3d.com/), we created a base color texture for the outer sphere with transparency in the areas where we wanted the inner core to come through. In [Blender](https://www.blender.org), we "vertex painted" the inner sphere for a cheap and easy way to get some color variation on it.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/vertex-painting.png" width="40%" />
<figcaption>The vertex painting on the inner sphere. We created a gradient that was lightest around the eye in order to give a greater sense of depth and visual interest.</figcaption>
</figure>
Another challenge we ran into when creating the eye was imposed by our use of [streaming](../../workspace/streaming.md) combined with the eye's distance from the player. Given the centrality of this structure, we wanted it to always be visible despite its distance but, without any hacks to its mesh, players were not able to see the eye unless they were in the solarium. We were able to force the eye's constant presence in the scene by adding some geometry to the eye and its rings. This geometry sits right below the terrain's surface, and this is enough to trick the engine into thinking the sphere is closer to the player than it is and always streaming it in. This should be done pretty sparingly though as forcing too many large objects to be streamed in could negate the benefits of streaming enabled and negatively impact performance.
We were able to add movement to the eye and its rings thanks to the same script we used to [rotate the cloud meshes](#rotate-cloud-meshes). For a final touch, we decided to add a hint to the presence of another world beyond the clouds, but we had to take a creative approach in order to avoid adding more geometry to the scene and additionally having to deal with the previously mentioned hurdles posed by streaming enabled. We created a scene that had a lot of depth due to the relative size and distance of objects, rendered an image of this scene, then used said image as a [decal](../../parts/textures-decals.md) on a part placed just behind the eye of the storm. We used the same method for rotating this part as we used for the eye and its rings.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/illusion-image.png" width="100%" />
<figcaption>The image we used to create an illusion of a world beyond the clouds. When players are far away from something, a simple image might be enough to create the illusion of more depth and complexity in your scene!</figcaption>
</figure>
## Make the expanding pantry
One of the most fun things to produce were the corrupt spaces, where we could subvert players expectations of reality by literally changing it around them. For example, in the father's puzzle we wanted to emulate a moment similar to a nightmare where no matter how fast you run, the room feels like it keeps getting longer. We decided to make an expanding pantry that would run away from players as they were looking for ingredients to turn the room back into its normal state.
<video
controls
src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/pantry-movement.mp4"
width="100%"></video>
We set this up with a simple movement of the walls, and a clever layout of our rooms that would appear on either side of the pantry. In the room's normal state, the pantry was a simple hallway, but in the corrupt space, it was actually much longer with several wings and a false wall!
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/pantry-corrupt-state.png" />
<figcaption>The corrupt state of the kitchen pantry.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/false-wall.png" />
<figcaption>The false wall moving away from players.</figcaption>
</figure>
</GridContainer>
The false wall was a model group that we would move back the moment players entered a trigger volume, which was a transparent part earlier in the pantry that they would walk through. That trigger was also used in a script similar to ones used on all our doors, which called the `Class.TweenService` to move from one goal to another. We used part volumes to tell the tweening operation where the start and end positions were for the wall.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/yellow-part-volume.png" width="98%" />
<figcaption>The part volume triggers a false wall behind it to move to its end point. It is made visible in this image with a yellow tint.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/target-closed.png" />
<figcaption>Target_Closed was a generic goal part we used on all our doors for where they should rotate. Here it was repurposed to tell the hallway wall where to go.</figcaption>
</figure>
</GridContainer>
Because `Class.TweenService` is such a general system, all our wall data model had to contain was the same components. For example, a general "Door_Script" script plays a sound defined by a "value" below the "Grow_Wall" model. That same script, with some modifications in the following code sample, also triggered audio for the pantry moving. This added a lot to the movement!
```lua
local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local model = script.Parent
local sound = model.Sound.Value
local trigger = model.Trigger
local left = model.TargetL_Closed
local right = model.TargetR_Closed
local tweenInfo = TweenInfo.new(
model.Speed.Value, --Time/Speed of Door Tween
Enum.EasingStyle.Quart, --Easing Style
Enum.EasingDirection.InOut, --EasingDirection
0, --Repeat Count
false, --Reverse true
0 --Delay
)
local DoorState = {
["Closed"] = 1,
["Opening"] = 2,
["Open"] = 3,
["Closing"] = 4,
}
local doorState = DoorState.Closed
local playersNear = {}
local tweenL = TweenService:Create(left, tweenInfo, {CFrame = model.TargetL_Open.CFrame})
local tweenR = TweenService:Create(right, tweenInfo, {CFrame = model.TargetR_Open.CFrame})
local tweenLClose = TweenService:Create(left, tweenInfo, {CFrame = model.TargetL_Closed.CFrame})
local tweenRClose = TweenService:Create(right, tweenInfo, {CFrame = model.TargetR_Closed.CFrame})
local function StartOpening()
doorState = DoorState.Opening
sound:Play()
tweenL:Play()
tweenR:Play()
end
local function StartClosing()
doorState = DoorState.Closing
--model["Door"]:Play()
tweenLClose:Play()
tweenRClose:Play()
end
local function tweenOpenCompleted(playbackState)
if next(playersNear) == nil then
StartClosing()
else
doorState = DoorState.Open
end
end
local function tweenCloseCompleted(playbackState)
if next(playersNear) ~= nil then
StartOpening()
else
doorState = DoorState.Closed
end
end
tweenL.Completed:Connect(tweenOpenCompleted)
tweenLClose.Completed:Connect(tweenCloseCompleted)
local function touched(otherPart)
if otherPart.Name == "HumanoidRootPart" then
local player = Players:GetPlayerFromCharacter(otherPart.Parent)
if player then
--print("touch")
playersNear[player] = 1
if doorState == DoorState.Closed then
StartOpening()
end
end
end
end
```
Once we had the false wall moving to the back of the room, we needed the rest of the content to move with it. To do that, we needed all loose items on the pantry to be welded to the wall as it moved. Using [Weld Constraints](../../physics/mechanical-constraints.md#weldconstraint), we were quickly able to weld all objects to the pantry wall to move as a single object. Doing this meant we had the option to unweld these items so players could bump into them and knock them around!
## Make the corrupted treehouse
Studio is a fantastic physically-based engine that you can use to create everything from a swinging gate to a spinning platform. With our demo, we wanted to use physics to create a sense of realism in an otherwise unrealistic set of environments. Using just a few **constraints**, you can create some fun and challenging obstacle courses within your own experiences!
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/obstacle-course.png" width="80%" />
**[Constraints](../../physics/mechanical-constraints.md)** are a group of physically-based motors that align objects and constrain behaviors. For example, you can use a [rod constraint](../../physics/mechanical-constraints.md#rodconstraint) to connect to objects in order to keep them a fixed distance from each other, or the [rope constraint](../../physics/mechanical-constraints.md#ropeconstraint) to have a lamp hanging from the end of the line. For the son's puzzle in which players are transported to the corrupt state of the study, we wanted to literally flip the world on its side. Doing so would subvert players' expectations of reality and the rules there, while still using the physics system as it was intended!
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/son-puzzle.png" width="80%" />
<figcaption>The son's puzzle started with the players in the same room, but everything was sideways.</figcaption>
</figure>
Once players worked down to the main area of the puzzle, they were greeted with a familiar sight on Roblox: an obstacle course. This particular obstacle course consisted of several spinning platforms and rotating walls, along with "safe areas" that progressed the story. We'll focus on the rotating/spinning elements.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/spinning-elements.png" width="80%" />
<figcaption>The mind-bending appearance hid the fact that the gameplay here was very simple.</figcaption>
</figure>
Why did we use constraints here? Because `Class.TweenService` or other methods wouldn't move the player while they stood on them. Without the object moving the player, someone could jump on a platform and it would spin out from under them. Instead, we wanted players to navigate through a spinning platform while trying to make their jump onto the next one. Because of this approach, players felt rooted where they stood while making a decision with how to proceed through the course, and we didn't need to do anything special to ensure they moved with a rotating surface!
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/friends-spinning.png" width="80%" />
<figcaption>You could watch your connections spinning around while trying to navigate the obstacle course too.</figcaption>
</figure>
To do this, we needed to first use assets from our current kit and add any new content for a visual effect. We made a few incomplete walls and platforms with holes in them to tell the story of the grandmother building the treehouse. Because we didn't want to create a bunch of unique platforms, we made 4 different base pieces and railing pieces separately. This allowed us to mix and match individual base and railing pieces to have plenty of variety.
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/base-piece.png" width="100%" />
We knew that since we were using constraints, we wouldn't be able to anchor these meshes because they wouldn't move even with the presence of a constraint/motor driving them. The constraint needed to be a child of something that was anchored in order for the platform to not just fall out of the world. We solved this through a part we named **Motor_Anchor** that had a [hinge constraint](../../physics/mechanical-constraints.md#hingeconstraint) to drive the overall movement of the platform. After that, we needed the two meshes to move as one, so we created a part we named **Motor_Turn**, then we welded the two meshes to it. This way the constraint would be able to work on a single part, as opposed to multiple hinges working with multiple parts.
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/unanchored-platform.png" width="100%" />
It was now time to set up the actual behavior of the hinge constraint itself, and add the attachments that would act as the orientation of the part and the constraint together. We placed the turning attachment on the Motor_Turn, which the walkway pieces were welded to, and another attachment for the anchor behavior on the Motor_Anchor itself, beside the hinge constraint. Since this needed to rotate on its own, as opposed to being influenced by the player (like a door hinge), we set the `Class.HingeConstraint.ActuatorType` to **Motor**, treating the constraint like a self-moving motor.
To keep the platforms spinning at a constant speed, we then set up the `Class.HingeConstraint.AngularVelocity`, `Class.HingeConstraint.MotorMaxAcceleration`, and `Class.HingeConstraint.MotorMaxTorque` properties to values that would allow movement and prevent interruption if a player jumped on it.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/hingeconstraint-properties.png" width="40%" />
<figcaption>Attachment0 was essentially the anchor for the hinge and Attachment1 represented the hinge itself. We had the hinge constantly rotating, but you can also use a hinge constraint for doors.</figcaption>
</figure>
Now we needed to make the rotating walls. The walls needed to rotate on their apparent center, and we knew we wanted them to be able to handle any orientation relative to the rest of the level. Like the platforms, we constructed these so that all the walls were unanchored and welded to the Motor_Turn.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/treehouse-mesh.png" width="100%" />
<figcaption>We wanted to reuse as much of the actual treehouse meshes to save on performance, so we followed a similar path as the platforms. Several wall types were made that could stick together in different combinations for some variation.</figcaption>
</figure>
We used `Class.Texture` objects on top of `Class.SurfaceAppearance` objects to add some variation to our base materials. [Textures](../../parts/textures-decals.md), similar to Decals, allow you to place an image on a plane of a mesh. This can be useful if you want to add dirt to a brick wall, or make wood look aged while using the same base wood material. `Class.Texture` objects have a slightly different behavior than `Class.Decal` objects in that you can tile and offset the image however you want, which is very useful if you want to be able to scale your overlay texture and don't mind it repeating!
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/treehouse-texture.png" width="100%" />
<figcaption>You can see both the similar behavior and set up for the hinge constraint, and also how we used `Class.Texture` objects.</figcaption>
</figure>
Once we'd tested a few platforms and rotating walls, we made several variations and played with their placement to make sure the obstacle course was challenging, mind-bending, and also clear where the player needed to go! It took some tuning to both their values and positions to get them to run well. We had several points where the platforms and walls were hitting each other or the surroundings, but with some moving around and frequent testing, we were able to land on the settings we have in the demo!
If you're not sure what your physical objects are hitting, you can toggle on **Collision&nbsp;fidelity** from the [Visualization&nbsp;Options](../../studio/ui-overview.md#visualization-options) widget in the upper‑right corner of the 3D viewport.
<img src="../../assets/studio/general/Visualization-Options.png" width="780" alt="A close up view of the 3D viewport with the Visualization Options button indicated in the upper-right corner." />
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/show-decomposition-geometry-disabled.png" />
<figcaption>When collision visualization is disabled, you can see the normal geometry representation that displays in-game.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/show-decomposition-geometry-enabled.png" />
<figcaption>When collision visualization is enabled, you can see the tree leaves don't have collisions, so they won't interfere with the spinning platforms or walls.</figcaption>
</figure>
</GridContainer>
As you can see below the doorway/window holes are visible, but the smaller details like the sub-paneling are not. This is because the `CollisionFidelity` property for the walls was set to **Box**. We didn't need the precision for these panels, so to save on performance cost, this was detailed enough for players to jump onto. With the platforms and rotating walls done, we only needed to add detailing assets like boxes and lamps, then it was ready to be played!
<img src="../../assets/resources/mystery-of-duvall-drive/developing-a-moving-world/collision-fidelity-treehouse.png" width="80%" />

View file

@ -0,0 +1,247 @@
---
title: Foundational gameplay systems
comments:
next: /resources/the-mystery-of-duvall-drive/supporting-systems
prev: /resources/the-mystery-of-duvall-drive/main-design-requirements
description: Explains the gameplay design used in The Mystery of Duvall Drive.
---
The following systems were the foundation in establishing the gameplay we wanted for [The Mystery of Duvall Drive](https://www.roblox.com/games/7902470429/The-Mystery-of-Duvall-Drive).
## GameStateManager
The **GameStateManager**/**GameStateClient** is probably the most complicated system in the experience, as it deals with:
- Starting players within the lobby, starting the countdown to teleporting the group to the main gameplay area, and teleporting players to reserved servers.
- Cloning corrupt rooms, async streaming them, and teleporting players to and from a specific assigned `Datatype.CFrame` coordinate.
- Grabbing and placing seals mechanics.
- Locking and unlocking doors.
- Initializing the final teleport to the foyer and playing the finishing cutscene.
We implemented it as a simple state machine (Update function), and the states are in **DemoConfig** (GameStates enum). Some states deal with initial lobby/reserved server teleport, while others deal with finding a mission, triggering the puzzle, and solving the mission. Note that apart from seals, we tried to not have mission-specific code in the **GameStateManager**.
GameStates is mostly server-side, but when the client needs to do something, such as show countdown, lore, or disable streaming pause UI, server and client (GameStatesClient) communicate via a remote event called `GameStateEvent`. As with most cases, the event payload has event "type" (Config.GameEvents) as the first parameter, and event specific data after that.
### Teleportation states
There is a group of 3 states that run three unique cutscenes that hide the teleportation to the corrupt room: Warmup, InFlight, and Cooldown. **Warmup** runs for the whole duration and ends with an almost black screen in which the 3D world is no longer visible. During this time, we clone the room, get desired player positions in the corrupt room for each player, call `Class.Player.RequestStreamAroundAsync`, and transport players to a specific assigned `Datatype.CFrame` coordinate within the corrupt room. This type of teleportation might trigger a streaming pause. When a streaming pause occurs, the client displays a warning message. We disabled this default UI to keep the experience immersive.
<Alert severity="info">
Players cannot transport to a specific assigned `Datatype.CFrame` while they are sitting, so the server sends a message to the client to "unseat" players before the teleport, as well as disable the seating state while the teleportation is in progress.
</Alert>
While streaming is being handled, **InFlight** runs, keeping a slightly pulsing dark screen. When both `Class.Player.RequestStreamAroundAsync` returns and clients inform the server that streaming pause is off and each player is close enough to the desired location, we cancel the InFlight cutscene, and start the Cooldown cutscene. The goal of **Cooldown** is to make the 3D world visible again by smoothly removing the dark screen. If `Class.Player.RequestStreamAroundAsync` takes too long to return, or the client doesn't report that the streaming pause is off, we still proceed to the Cooldown cutscene after a timeout of several seconds.
A similar set of Warmup, InFlight, and Cooldown cutscenes occur when we teleport the player back to the normal state of the room, **TeleportWarmupBack**, **TeleportInFlightBack**, and **TeleportCooldownBack**, and at the end of the experience, we also run **TeleportWarmupFinal**, **TeleportInFlightFinal**, and **TeleportCooldownFinal** to teleport players into the foyer for the finishing cutscene.
### Lighting and atmosphere states
We knew that we wanted each room's normal and corrupt state to have a different visual appearance so it could give players clear visual feedback that they were in a completely different location. Game states allowed us to change the lighting and atmosphere properties for normal and corrupt rooms, in which the GameStateManager selected which instances to use based on if players are teleporting from the normal to the corrupt state of the room (`TeleportWarmup`), or vice versa (`TeleportWarmupBack`). Events playing during the teleport make the whole screen either dark or white, so we decided to change the `Class.Lighting` and `Class.Atmosphere` instances at those moments to hide the process from players. To make it simple to change, **DemoConfig** includes maps that define what instances under these services need to change.
### Locking doors game states
We wanted to be able to keep players in certain rooms while they finished missions, so we created game states to lock doors: `InMission` and `CanGetSeal`. `InMission` locks players in their active mission room, and `CanGetSeal` keeps the mission room's door locked until they pick up the "restored" seal. We mostly used this to have the doors lock when players return from a mission so that they have an incentive to pick up the seal. After they pick up the seal, doors unlock so they can place it within the seal's location in the foyer. The last mission is unique to this typical process, as the door to the room with its seal is locked until players solve every other puzzle (`EnableRegularMissionDoors`, `EnableOneMissionDoors` functions).
## EventManager
**EventManager** allowed us to run "actions" over time by using keyframes, such as:
- Interpolating instance properties and attributes.
- Running scripts.
- Playing audio.
- Running camera shakes.
We'd ideally use a tool with track-based UI, but for this demo, we typed the keys and property names manually. The **EventManager** system consists of several scripts and an event/function, including:
- `EventManager` - Overall logic for creating and stopping events, as well as server-side actions.
- `EventManagerClient` - Client-side actions.
- `EventManagerModule` - Common code for both server and client-side actions.
- `EventManagerConfig` - Small file with a few command declarations.
- `EventManagerDemo` - Where all actual events for this demo are defined in game-specific script.
- `EventManagerEvent`, `EventManagerFunc` - Remote event and bindable function to run and stop events from the client or server. This is how other systems can set up, run, and stop events.
Each event has a name, a section with optional information about cooldown, function to run on start or end, event parameters, and sections with interpolants (interpolating any number of properties or attributes over time), scripts (running registered scripts at keyframes), camera shakes, and playing audio.
### Interpolation
Interpolation lets object properties and attributes seamlessly change from one value to another instead of disjointedly jumping between keyframes. We defined interpolants to change a variety of visual effects; for example, the following code snippet shows how we interpolated the `Class.TextLabel.TextTransparency` property on an object defined by `Class.TextLabel` parameter from a value of `1` at the start to `0`, then later back to `1` again:
```lua
interpolants = {
objectParam = "TextLabel",
property = "TextTransparency",
keys = {
{value = 1},
{time = .5, value = 0},
{time = 2.25, value = 0},
{time = 3, value = 1}
}
}
```
<video src="../../assets/resources/mystery-of-duvall-drive/technical-overview/interpolation.mp4" controls width="100%"></video>
While we could define which object property or attribute belongs to what like in the following code sample, we wanted to be able to re-use the same events on different "groups of objects" to allow it to work with streaming on the client, and with objects created at run time.
```lua
object = workspace.SomeFolder.SomeModel
```
To accomplish this, we allowed referencing by object name and passing named parameters on event start. To find named objects, we allowed specifying a "root" for the event, which let objects to be found by name under this root when the event started. For example, in the following code snippet, the **EventManager** tries to find an object named "Wander" somewhere under `Workspace.Content.Interior.Foyer["Ritual-DemoVersion"]`.
```lua
params = {
["RootObject"] = workspace.Content.Interior.Foyer["Ritual-DemoVersion"],
},
interpolants = {
objectName = "Wander",
attribute = "TimeScale",
keys = {
{value = 0.2}
}
}
```
We allowed passing parameters into events in the params section, and the scripts running on event start could either change existing parameters, or add more parameters into the "param" table. In the following example, we have `isEnabled` parameter with a default value of false, and the "Enabled" property on an object with the name **FocuserGlow** will be set to the value of `isEnabled`. A script running on event start or a script invoking the event can set isEnabled, so we could use the same event description for both enabling and disabling FocuserGlow.
```lua
params = {
isEnabled = false
},
interpolants = {
{
objectName = "FocuserGlow",
property = "Enabled",
keys = {
{valueParam = "isEnabled"}
}
}
```
Parameters allowed us to refer to objects that do not even exist in the beginning of the experience. For example, in the following code sample a function running on event start will create an object, and set BlackScreenObject entry in the params to point to the created object.
```lua
{objectParam = "BlackScreenObject",
property = "BackgroundTransparency",
keys = {
{value = 0},
{time = 19, value = 0},
{value = 1},
}}
```
### Run events, event instances, and connect to triggers
To run an event, we would either use a remote event from clients, or a function from the server. In the following example, we passed a couple of parameters to the `RootObject` and `isEnabled` events. Internally, an instance of the event description was created, params resolved to actual objects, and the function returned an ID for the event instance.
```lua
local params = {
RootObject = workspace.Content.Interior.Foyer["Ritual-DemoVersion"]["SealDropoff_" .. missionName],
isEnabled = enabled
}
local eventId = eventManagerFunc:Invoke("Run", {eventName = "Ritual_Init_Dropoff", eventParams = params} )
```
We could stop running an event by calling function with "Stop":
```lua
eventManagerFunc:Invoke("Stop", {eventInstId = cooldownId} )
```
Interpolants or other actions that are "cosmetic" (do not change simulation for all players) could be run on clients, which could result in smoother interpolation. In the event description, we could provide a default value for all actions as onServer = true (without it, default is client). Each action can overwrite it by setting its own onServer.
To easily connect running an event to a trigger, we used helper functions `ConnectTriggerToEvent` or `ConnectSpawnedTriggerToEvent`, the latter of which finds the trigger by name. To allow the same event to be triggered using different triggers, we could call `eventManagerFunc` with a "Setup" key and a set of trigger volumes. For an example of a trigger volume in action, see [Making the Expanding Pantry](../../resources/the-mystery-of-duvall-drive/develop-a-moving-world.md#making-the-expanding-pantry).
#### Event parameters
In addition to custom event parameters passed from scripts, other data that can be optionally passed when creating an event includes player, callback (to be called when event ends), and callback parameters. Some events should run only for one player (events with actions running on client), while others should run for all. To make it run for only one player, we used `onlyTriggeredPlayer = true` in the params.
Events can have cooldowns defined by `minCooldownTime` and `maxCooldownTime`. The min and max provide a range for scaling based on player count, but we didn't use it in this demo. If we were to have needed cooldown needs to be per player, we had the capability to use `perPlayerCooldown = true`. Each Event has a duration in seconds, and cooldown timings and callbacks are based on it. To inform about finishing an event, invoking code could pass a callback and parameters it will get.
#### Call scripts
We could call `Class.Script|Scripts` at specific keyframes in the **Scripts** section. For example:
```lua
scripts = {
{startTime = 2, scriptName = "EnablePlayerControls", params = {true}, onServer = false }
}
```
In the previous example, the **EnablePlayerControls** `Class.Script` would need to be registered with the event manager module, like so:
```lua
emModule.RegisterFunction("EnablePlayerControls", EnablePlayerControls)
```
RegisterFunction must be called in the client script for functions called on the client, and in the server script for `onServer = true`. The function itself will get eventInstance and parameters passed, but in this case, only one parameter is passed with a true value.
```lua
local function EnablePlayerControls(eventInst, params)
```
#### Play audio
We have limited support for playing [non-positional audio](../../sound/objects.md#volumetric) at keyframes in the **Sounds** section, for example:
```lua
sounds = {
{startTime = 2, name = "VisTech_ethereal_voices-001"},
}
```
Note that the event finishing callbacks fire when event duration expires, but audio actions might be still playing after.
#### Run camera shakes
We could define camera shakes in the **cameraShakes** section, like so:
```lua
cameraShakes = {
{startTime = 15, shake = "small", sustainDuration = 7, targets = emConfig.ShakeTargets.allPlayers, onServer = true},
}
```
"targets" can be initiated only for the player who triggered the event, allPlayer, or playersInRadius to the triggering player. We used a 3rd party script for camera shakes, and the shakes were pre-defined: `eventManagerDemo.bigShake` and `eventManagerDemo.smallShake`. `sustainDuration` could also be passed.
## Missions logic
There are 7 missions total, and only 6 of them use seals. Most missions have common parameters, though some are only for missions with seals and teleporting to corrupt rooms. Each mission has an entry in the **DemoConfig** script with a set of parameters in the **Config.Missions** map:
- **MissionRoot**: A folder of all non-corrupt versions of objects.
- **Doors**: Doors to lock until a player picks up a seal.
- **SealName**/**SolvedSealName**: Non-corrupt seal and corrupted seal names.
- **SealPlaceName**: Places to put the seal.
- **PlacedSealPlaceholderName**: Placeholder object at the place to put the seal.
- **TeleportPositionsName**: Name of a folder with placeholder meshes to define player teleport positions and rotations when moving to the corrupt room, and back to the normal area. The same name is used in both cases.
- **CorruptRoomName**: Names of the root folders (relative to ServerStorage) for the corrupt rooms. Corrupt rooms clone under TempStorage.Cloned when the mission starts, and they are destroyed when the mission is finished.
- **MissionCompleteButtonName**: A cheat button in the corrupt rooms to finish the mission immediately. This is for [debugging purposes](../../resources/the-mystery-of-duvall-drive/main-design-requirements.md#debug-version).
- **CheatKey**: The same cheat as a number or <kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>[Number]</kbd>.
Some of the mission logic is in the **[GameStateManager](#gamestatemanager)** scripts, as seals and doors provide the main game flow for most missions, but most of the mission-specific logic is in `MissionsLogic` and `MissionsLogicClient` scripts that define several "types" of missions. The type is defined just by the presence of specifically named members in the mission description. There are a few types of missions:
- **Use a key on a lock** - The first mission to open a door. This type is defined by `LockName`, `KeyName`.
- **Matching items** - 4 missions match items. This type is defined by `MatchItems`.
- **Dressing up a mannequin using layered cloth** - 1 mission in the attic has players collect three items. This type is defined by `DressItemsTagList`.
- **Click on item to finish** - 1 mission has this type, which is defined by `ClickTargetName`.
Each mission type has its own `StartMissionFunc` and `CompleteMissionFunc`. Starting function usually reads parameters from the **MatchItem** map, resolves names to objects, and sets up any click detectors or UI elements. Almost all the logic is on a server, but MissionsLogicClient provides a UI for showing items counter, used in many missions. `MissionLogicEvent` remote event is used for server - client communications, with a small MissionEvents defining types of commands passed. The `MiscGameLogic` script binds some triggers to events and removes debug objects in Release version.
Matching items logic allows to "use" (click while holding) items marked with **PuzzlePieceXX** tags over items with **PuzzleSlotYY** tag. There are a few options available as parameters in **MatchItems** map (if pieces need to be applied in order, if only one of each is required). We could specify names for simple audio and visual FX. When pieces need to be placed at specific locations, an extra "Placement" map provides mapping from pieces tags to names of placeholder parts that define transforms.
## Grabbing
We developed a simple grabbing system for holding an object by attaching the object to the character's right arm. Grabbing is implemented in **GrabServer2** and **GrabClient** scripts. It starts in `ProcessClick`, which fires a ray through the clicked/touched point. It then checks if we hit a mesh that can be grabbed, and the hit is within the `maxMovingDist` where we can start grabbing interaction. If the model clicked on has `Class.Attachment|Attachments` called **GrabHint**, we pick the closest to the clicked spot. We remember the grabbed part, the model it belongs to, and either the closest **GrabHint** or the clicked position in the `grabAttempt` structure. If the distance is more than maxGrabDist, the player first needs to walk close enough to the attempted grab spot, so we call `Class.Humanoid.MoveTo`.
On each frame, we check if a grab attempt is in progress. If the player is within **reachDist**, we start playing **ToolHoldAnim**. When a player is within **maxGrabDist**, the client fires a request to the server to actually grab a model (`performGrab` function).
Server side script has 2 main functions:
- **Grab** - Handles a client's request to grab a model.
- **Release** - Handles the request to release a grabbed model.
Information about what each players holds is kept in **playerInfos** map. In the grab function, we check if this model is already grabbed by another player. If so - an "EquipWorldFail" is sent to the client, and it cancels the grab attempt. Note that we needed to handle situations where players grab different parts of the same `Class.Model`, and cancel the grab in this case.
If grabbing is allowed, the script creates two `Class.Attachment|Attachments`, one on the right hand and another on the object using a passed grab spot from the client. It then creates a `Class.RigidConstraint` between the two `Class.Attachment|Attachments`. `Class.Constraint|Constraints` and `Class.Attachment|Attachments` are stored under the **CurrentGrips** folder under the player's character. Grabbing also plays a sound, disables collisions on the grabbed object, and deals with Restorables, if needed.
For releasing a grabbed model, the client script connects to the **GrabReleaseButton** button in HUD ScreenGui. A `Connected` function fires an event to the server. On the server, release deletes the `Class.Attachment|Attachments` and `Class.Constraint|Constraints`, restores collision, deals with any applicable Restorables, and clears grab data for this client in **playerInfos**.

View file

@ -0,0 +1,142 @@
---
title: Immersive narrative
comments:
next: /resources/the-mystery-of-duvall-drive/stream-in-immersion
prev: /resources/the-mystery-of-duvall-drive/materialize-the-world
description: Explains the narrative concepts used in The Mystery of Duvall Drive.
---
Like the [Beyond the Dark](../../resources/beyond-the-dark/index.md) demo, one of our earliest decisions was to have as much of the gameplay and story told diegetically within the world itself. This can be a difficult process when you want to balance both communicating progress to the player through the user interface while also making them feel as though they really are within the world you've built. To meet these goals for our experience, we broke the way we communicate information to players down into four techniques:
1. **Visual cues** - Objects, images, or arrangement of assets that passively reinforce story or give hints to the player when it makes sense for the world.
2. **Lore** - When a player clicks/taps on specific items in the house, important story information displays **on the entire screen**. This narrative is only visible to the player that clicked/tapped the object. The tone is always informational in tone and phrasing.
3. **Thought bubbles** - When a player clicks/taps on specific items in the house, a text dialogue "reaction" displays **near the object**. This narrative is only visible to the player that clicked/tapped the object. The tone is always first person and observational.
4. **Announcements** - When a player clicks/taps on a corrupted item in the house, third-person dialogue displays on **every player's screen at once**. This narrative sometimes transitions all players to the corrupted state of the room they are in.
In this section, we will demonstrate how we used these techniques and specific features to immerse players into our narrative while still encouraging exploration, communicating progress, and keeping gameplay both intimate to the individual player and fair in a multiplayer experience.
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/overview-visual-cues.png" width="80%" />
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/overview-announcement.png" />
<figcaption>An announcement on-screen overlay of the character's internal thoughts.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/overview-thought-bubble.png" width="99%" />
<figcaption>A thought bubble, or object-specific dialogue, that displays when you select certain objects.</figcaption>
</figure>
</GridContainer>
## Visual cues
A visual cue is an important tool to both reinforce narrative and tell players what they need to know about their objective **without using words**. Using an arrangement of assets, such as chalkboards, statues, and wallpaper, we were able to passively provide a depth of information about the main characters of Duvall Drive, as well as guidance on how to complete puzzles without having to include invasive UI elements. We began this process by first figuring out what we wanted to communicate to the player, then how it would make sense to communicate that within the world of our experience. For example, we wanted players to understand how obsessed the grandfather was in his mission to defy death, so we left his room in disarray with a lot of his research and several walls with his scribbled thoughts.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/grandfather-room.png" width="92%" />
<figcaption>Scribbles throughout the grandfather's room communicate the grandfather's compulsive nature.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/dog-statue.png" />
<figcaption>An abundance of sculptures and reference photos demonstrate how the daughter is an artist.</figcaption>
</figure>
</GridContainer>
Our first rule was that if we wanted to use a visual cue to teach players something, it must have a reason to exist even if players aren't present. If we put a giant sign with clear instructions of what to do for a puzzle, this would seem out of place within the house. However, if we established a grandfather that is consumed with drawing on everything, including the walls of his bedroom, it is much more believable to have a chalkboard with his plan scribbled out for the player to follow.
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/chalkboard.png" width="60%" />
Our second rule was that we don't always need to tell players what to do or how to solve puzzles because they are insightful to their environment. For example, sometimes a fallen power line in a pool of water is enough of a visual cue to communicate that players are in danger of being electrified if they continue moving in that direction.
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/power-line.png" width="60%" />
## Lore
In addition to using visual cues to passively guide players, we also chose to display important story information about the family **on the entire screen** as a precursor to start the room's puzzle whenever a player clicked or tapped on specific items in the house. To guide players to the objects they needed to select in order for lore to display, we used the new highlight visual effect to make specific objects stand out from their surroundings. This effect allows you to either outline and/or overlay objects with a specific color that fits to the shape of the object itself, and choose whether other objects can hide the highlight if they are closer to the camera.
<GridContainer numColumns="4">
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/lore-1.png" width="80%" />
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/lore-2.png" width="80%" />
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/lore-3.png" width="92%" />
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/lore-4.png" width="92%" />
</GridContainer>
We initially used `Class.Highlight` for every object a player could interact with, but the visual noise was overwhelming and players wouldn't clearly know what to interact with to read the lore. We decided to only use it to highlight items that were either corrupted or dangerous, and this visual feedback allowed players to easily drive the story forward.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/dog-statue-lore.png" width="60%" />
<figcaption>Fun fact: we originally had lore on everything you could click on! This wasn't helpful because players weren't sure what information was important and what was just for flavor. Eventually we removed much of the lore and used visual cues to leave the rest of the narrative to the player's imagination.</figcaption>
</figure>
After the player clicked on the object with the lore, we needed a way to display the information on top of the screen. We decided to create an overlay by using a `Class.ScreenGui` as our on-screen UI container with a child `Class.Frame` to control the sizing and rescaling of its children `Class.TextLabel|TextLabels` and `Class.ImageLabel|ImageLabels`. We set the positions and scale of the `Class.TextLabel|TextLabels` and `Class.ImageLabel|ImageLabels` properties so they fit visually together and would scale for different devices players use to access the experience.
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/jacob-photo-album.png" width="60%" />
`Class.TextLabel|TextLabels` and `Class.ImageLabel|ImageLabels` affect visual elements of their respective text and images, including their color, transparency, size, and arrangement. For `Class.TextLabel|TextLabels` specifically, you can adjust their `Class.TextLabel.TextScaled` and `Class.TextLabel.RichText` properties to control the layout and visual styling of text. `Class.TextLabel.TextScaled` allows you to scale text to fill the entire parent UI element's space, and `Class.TextLabel.RichText` allows you to use simple markup tags to style sections of text in bold, italics, color, or even different fonts.
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/lore-template.png" width="100%" />
We knew that players would be accessing this experience from a variety of devices, including PC, console, and mobile devices, and they would need to be able to either click or tap the screen to interact with the experience. We didn't want a bunch of buttons on the screen to distract from the narrative, so we made an invisible back button the size of the entire screen so that players could either click or tap anywhere on the lore overlay to close the content and get back to gameplay.
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/overlay-back-button.png" width="30%" />
## Thought bubbles
We needed a way for the player's character to communicate ideas to the player, such as providing additional narrative flavor or reinforcing what they needed to do. Our solution was to display thought bubbles, or **text reactions from the player's character**, near non-lore objects whenever the player selected them. This narrative only displays to the player that clicked or tapped the object, and the tone is always first person and observational.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/barn-owl-thought-bubbles.png" width="70%" />
<figcaption>A relatively simple yet powerful system to have the world react to the player's interactions!</figcaption>
</figure>
To create thought bubbles, we parented a `Class.BillboardGui` with a child `Class.TextLabel` to non-lore objects. This allowed us to display dialogue in the 3D space near the object without the text taking up the entirety of the screen. Similar to our process in creating lore, we used the `Class.TextLabel.RichText` property to style the text and add visual effects like another otherworldly being interrupting the player's own thoughts. The following is an example of RichText HTML used to make the image below.
`"What is…this…<br/><stroke color="#dcc7ff" joins="miter" thickness="2" transparency="0.5"><font size="60"><font color="rgb(10,8,11)"><i>....hunger….void….pull…</i></font></font></stroke>`
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/richtext-thought-bubble.png" width="80%" />
By using `Class.TextLabel|TextLabels` instead of images, we kept the text readable by Roblox, and we were able to auto-localize this content and make it accessible to many more players without having to do anything! For more information on this process, see [automatic translations](../../production/localization/automatic-translations.md).
## Announcements
While we wanted the player's character to have reactions to the world, we also wanted the world itself to react to their presence. To meet this goal, we included announcements, or third-person dialogue, that displays **on every player's screen at once**. Similar to lore, we created announcements by using a `Class.ScreenGui` as our on-screen UI container with a child `Class.TextLabel`. We also used the `Class.TextLabel.RichText` property to change the dialogue's color, font, and size to help convey different "entities" talking to the players in the house.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/announcements-white.png" width="80%" />
<figcaption>Announcements with plain white text were the player's character reaction that was general to an area, not an object like the lore narrative technique.</figcaption>
</figure>
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/announcements-purple.png" width="95%" />
<figcaption>Announcements with purple text were the reaction from a corrupt entity.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/announcements-yellow.png" />
<figcaption>Announcements with yellow text were the reaction from a restored entity.</figcaption>
</figure>
</GridContainer>
By using the `Class.TextLabel.RichText` property and providing some HTML commands in it to drive changes, we were also allowed to make changes mid-sentence! For example, the following rich text markup allowed us to interrupt the player character's reaction by the corrupt entity's thoughts:
`What is…this…<br/><stroke color="#dcc7ff" joins="miter" thickness="2" transparency="0.5"><font size="60"><font color="rgb(10,8,11)"><i>....hunger….void….pull…</i></font></font></stroke>`
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/announcements-interrupt.png" width="70%" />
We had several of these types of announcements, either triggered by invisible part volumes that players walked into, or by completing certain goals. To avoid dozens of individual `Class.ScreenGui|ScreenGuis`, we used a separate script in the demo that allowed us to provide the text that the script would then add to a single object we named **StoryNote** in our `Class.PlayerGui` folder. This object was a single `Class.ScreenGui` with a child `Class.TextLabel`. Using scripting, we could inject whatever phrases we wanted directly into the single `Class.TextLabel`, which made it a lot easier to make changes as we figured out how and what we wanted to communicate to the player!
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/storynote-object.png" width="100%" />
<figcaption>StoryNote was an object we called for any instances that triggered it via script. Those same scripts provided the text and formatting we wanted for each event.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/example-script.png" width="100%" />
<figcaption>Example of a script calling that object and defining the text, per event.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/immersive-narrative/blank-textlabel.png" width="100%" />
<figcaption>The TextLabel was actually blank, and using scripts we injected the RichText as needed to the same object.</figcaption>
</figure>

View file

@ -0,0 +1,29 @@
---
title: The Mystery of Duvall Drive
comments:
next: /resources/the-mystery-of-duvall-drive/construct-the-house
description: The Mystery of Duvall Drive is a showcase demo with accompanying documentation on the concepts and processes used during development.
---
<img
alt="The Mystery of Duvall Drive Banner"
src="../../assets/resources/mystery-of-duvall-drive/overview/overview.png"
width="100%" />
[The Mystery of Duvall Drive](https://www.roblox.com/games/7902470429/The-Mystery-of-Duvall-Drive) is an official Roblox experience used to showcase Studio's various features and tools to create an immersive environment. Returning from the [Kerr-Newman DSR-14 space station](https://www.roblox.com/games/7208091524/Beyond-the-Dark-Vistech-Showcase), we now invite you to explore the dark abandoned house and its many mysteries in this official demo, which is both uncopylocked and editable in Studio, as well as a great palette of assets and examples that you can model and use similarly in your own experiences!
Similar to the previous [Beyond the Dark](../../resources/beyond-the-dark/index.md) showcase, this document demonstrates the process of going from simple sketches to a rich 3D environment with mind-bending visuals. Our goal is to provide information on both **how** we used Studio tools and **why** we used the features we did to achieve our results, including:
- **`Class.MaterialService`** and custom materials
- **Packages** and their ability to streamline processes
- **Volumetric audio** and `Class.SoundService`
- **Streaming enabled** for complex experiences
You can follow the articles sequentially or jump between the following sections:
- **[Construct the house](../../resources/the-mystery-of-duvall-drive/construct-the-house.md)** explores the transformation from a simple blueprint to the final environment, including the process to ensure the environment was fun from start to finish.
- **[Materialize the world](../../resources/the-mystery-of-duvall-drive/materialize-the-world.md)** details how both `Class.MaterialService` and `Class.SurfaceAppearance` create the look and feel of each room's normal and corrupted state.
- **[Immersive narrative](../../resources/the-mystery-of-duvall-drive/immersive-narrative.md)** demonstrates how to utilize UI features to build lore throughout the playable area.
- **[Stream in immersion](../../resources/the-mystery-of-duvall-drive/stream-in-immersion.md)** explains how we both code and constructed the experience to take advantage of Streaming Enabled capabilities.
- **[Develop a moving world](../../resources/the-mystery-of-duvall-drive/develop-a-moving-world.md)** describes how and why movement can give the world a sense of life and reactiveness.
- **[Design dark soundscapes](../../resources/the-mystery-of-duvall-drive/design-dark-soundscapes.md)** details how `Class.SoundService` and [volumetric audio](../../sound/objects.md#volumetric) craft the distinct soundscapes of the normal and corrupted worlds.

View file

@ -0,0 +1,101 @@
---
title: Main design requirements
comments:
next: /resources/the-mystery-of-duvall-drive/foundational-gameplay-systems
prev: /resources/the-mystery-of-duvall-drive/technical-overview
description: Explains the fundamental design concepts used in The Mystery of Duvall Drive.
---
The following list provides an overview of the main technical design requirements we thought about as we worked on [The Mystery of Duvall Drive](https://www.roblox.com/games/7902470429/The-Mystery-of-Duvall-Drive). For more information on the visual design of these elements, see the [Mystery of Duvall Drive Showcase](../../resources/the-mystery-of-duvall-drive/index.md), which highlights Studio's various features and tools we used to create the immersive environment.
## Missions
There are several types of missions players must solve in order to progress through the experience, including navigating a [series of spinning platforms](../../resources/the-mystery-of-duvall-drive/develop-a-moving-world.md#make-the-corrupted-treehouse) until players are able to retrieve a special item, or finding different ingredients in an [expanding pantry](../../resources/the-mystery-of-duvall-drive/develop-a-moving-world.md#make-the-expanding-pantry) and placing them into a boiling pot. To organize the scripting process of creating and debugging missions, we created a mission framework and a debug version.
### Mission framework
To ensure that we unified each step of the mission throughout the experience, we designed a **simple puzzle mission framework** for each mission which consisted of hooks for the start and finish of the puzzle, a spot to read configuration data, and a button to complete the puzzle for [debugging purposes](#debug-version). For more detailed information on this process and its parameters, see [Missions Logic](../../resources/the-mystery-of-duvall-drive/foundational-gameplay-systems.md#missions-logic).
#### Seals and game states
When players entered specific rooms, we wanted them to interact with special small objects known as **seals** to trigger missions. After they solved the mission, we wanted them to grab the seal and place it in a predefined location within the foyer in order to progress through the overall gameplay. After they placed the seal in the correct location, they could then continue to another room within the house and start the next mission by clicking on that room's special object.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/technical-overview/key-grabbing-system.jpg" width="100%" />
<figcaption>We developed a simple grabbing system for holding an object by attaching the object to the character's right arm.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/final-foyer.png" width="88%" />
<figcaption>The foyer where players need to place each seal to progress through the experience.</figcaption>
</figure>
</GridContainer>
To implement this, we created **game states**, which would specify the period of time players could begin process like:
- Looking for a "corrupted" seal in a room to trigger the mission.
- Pick up the "restored" seal when they solve the mission.
- Place the seal into the foyer circle.
Game states largely control the flow of the experience and how players interact with the experience's [narrative](../../resources/the-mystery-of-duvall-drive/immersive-narrative.md). For more information, see [GameStateManager](../../resources/the-mystery-of-duvall-drive/foundational-gameplay-systems.md#gamestatemanager).
#### Normal and corrupt rooms
We wanted to have two states of six rooms in the house: a normal state and a corrupted state. When a player would touch a corrupted seal in a room to trigger the room's corrupted state, the environment would change to a darker atmosphere with modified lighting, environmental objects, and special effects. Players would then have to solve the mission in order to return to the room's normal state.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/package-normal-state.png" width="93%" />
<figcaption>The study's normal state</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/constructing-the-house/package-corrupt-state.png" />
<figcaption>The study's corrupt state</figcaption>
</figure>
</GridContainer>
To implement this, we prepared a special space in the experience that was very far from the house, about 6,500 studs away in the **X** direction that could accommodate the corrupt state of a room. When a player triggers any corrupted state, the corrupt state of that specific room clones into this area from `Class.ServerStorage` to the **TempStorage/Cloned** folder, then players [teleport](#teleportation) into that room. Small `Class.Part|Parts` exist within each normal and corrupt room that serve as spawn points for players when entering or leaving rooms. After they solve the mission, we simultaneously teleport players back into the normal state of the room, and destroy all objects in the corrupt state of the room. For more information on the game states that control this teleportation process, see [GameStateManager](../../resources/the-mystery-of-duvall-drive/foundational-gameplay-systems.md#gamestatemanager).
### Debug version
To assist us in periodically debugging missions, we created a version of the experience where we wouldn't have to wait in the lobby or for cutscenes. This version had keyboard-based cheats and buttons that would allow us to automatically complete missions so we could quickly test aspects of the experience. We would periodically copy and publish this version into the version we planned to release that had the proper gameplay flow. To distinguish these two versions, we made a **DemoGlobalSettings** script to check the placeID, see if it's running in Studio, and enable/disable various gameplay cheats and buttons. For more information, see [Missions Logic](../../resources/the-mystery-of-duvall-drive/foundational-gameplay-systems.md#missions-logic) and [Configuration Scripts](../../resources/the-mystery-of-duvall-drive/supporting-systems.md#configuration-scripts).
## Teleportation
There are three types of teleportation that occur within the experience:
1. Teleporting players from the simple lobby to the main gameplay area in a [reserved server](#reserved-servers).
1. Teleporting players from a room's normal state to the corrupt state, then back again while showing a [cutscene](#cutscenes).
1. Teleporting players within some puzzles, or when they [respawn](#respawn-players) after falling off the gameplay area.
### Reserved servers
We decided to group players into groups of five in a **simple lobby** before teleporting them over to a **reserved server** for the main gameplay area of the house. The lobby provided time for additional players to join and play together, and reserved servers prevented additional players from missing aspects of the gameplay and narrative from joining the experience late. This teleportation only happens once.
<img src="../../assets/resources/mystery-of-duvall-drive/technical-overview/simple-lobby.jpg" width="50%" />
### Cutscenes
We wanted to be able to transport players throughout the game whenever they accomplish certain tasks, such as touching a seal or completing a mission. To implement this, we developed a simple version of a script-based tool called **EventManager** that controlled various properties and attributes, ran scripts and audio, and performed camera shakes over a duration of time. This allowed us to both move players and stream in and out different rooms while hiding this process from the player using special effects. For more information, see [EventManager](../../resources/the-mystery-of-duvall-drive/foundational-gameplay-systems.md#EventManager).
<video src="../../assets/resources/mystery-of-duvall-drive/technical-overview/cutscene.mp4" controls width="100%"></video>
### Respawn players
The third type of teleportation we wanted to implement was a short teleport with only a player `Datatype.CFrame` coordinate change within some puzzles and when players fall and respawn. Unlike the previous two types of teleportation, this type doesn't explicitly request async streaming.
<video src="../../assets/resources/mystery-of-duvall-drive/technical-overview/respawning-players.mp4" controls width="100%"></video>
## Gameplay scripting
Scripting allowed us to execute on specific gameplay elements, such as fading UI elements, creating trigger volumes for key events, and highlighting objects when they were in focus with the player's cursor. Many of these systems relied on tagging objects to perform custom behavior, then using a variety of client or server scripts to contain specific workflows depending on what action needed to happen at set points in the experience. For more information on how we implemented these systems, see [Foundational gameplay systems](../../resources/the-mystery-of-duvall-drive/foundational-gameplay-systems.md) and [Supporting Systems](../../resources/the-mystery-of-duvall-drive/supporting-systems.md).
### Custom behavior with tags
We wanted a way to add custom behavior for objects, such as locking doors so players would have to stay within the room until they completed the active mission. To implement this, we decided to use tags for specific objects, then we used `Class.CollectionService` to find these objects and connect any corresponding `Class.Script|Scripts` to add custom behavior. We had one `Class.Script` for each tag category that acted as a manager to handle all objects tagged under that category so we could keep the `Class.Script` in a single location rather than being copied many times in `Class.DataModel`. For more information, see [DoorManager](../../resources/the-mystery-of-duvall-drive/supporting-systems.md#doormanager), [MasterAnimator](../../resources/the-mystery-of-duvall-drive/supporting-systems.md#masteranimator), [DrawerManager](../../resources/the-mystery-of-duvall-drive/supporting-systems.md#drawermanager), [KillVolumes](../../resources/the-mystery-of-duvall-drive/supporting-systems.md#killvolumes), [PlayerMissionRespawn](../../resources/the-mystery-of-duvall-drive/supporting-systems.md#playermissionrespawn), and [PianoManager](../../resources/the-mystery-of-duvall-drive/supporting-systems.md#pianomanager).
The "manager" `Class.Script` uses an `Init` function to find any objects tagged when the experience starts, and connect a custom behavior to them. For example, DoorManager finds any object tagged with **Door**, then it attaches the correct behavior to the door objects (moves the door open when clicked, plays a door swing sound effect, etc.). However, any objects that are added or removed during runtime, such as any objects that are added to a corrupted room after a player interacts with a corrupted seal, miss this initial call and never get the expected behavior. To solve this, we use `Class.CollectionService.GetInstanceAddedSignal` and `Class.CollectionService.GetInstanceRemovedSignal` to grant the same behavior to new objects that are tagged and untagged, respectively.
### Client and server scripts
We wanted to reduce bandwidth for different aspects of gameplay that would be expensive on performance. We decided that when object functionality can affect simulation for other players, such as moving object through collision, we would run this on the **server**, but if something is more related to environment design, such as lights, environmental animations that don't affect gameplay, special effects, and audio, we would run them on **clients**. This would reduce bandwidth, and keep both movement and environment changes smoother on user devices.

View file

@ -0,0 +1,250 @@
---
title: Materialize the world
comments:
next: /resources/the-mystery-of-duvall-drive/immersive-narrative
prev: /resources/the-mystery-of-duvall-drive/construct-the-house
description: Explains the use of materials and textures in The Mystery of Duvall Drive.
---
We use [materials](../../parts/materials.md) and [surface appearance](../../art/modeling/surface-appearance.md) objects to create an immersive and realistic indoor and outdoor environment. Without materials, this scene could still have some interesting depth, silhouettes, and lighting, but with our materials and texture systems, we can really bring the room to life and add a dimension of realism to the world.
These [physically-based rendering](../../art/modeling/surface-appearance.md) (PBR) materials are what makes surfaces like wood look, react, and reflect the way wood does in the real world. In this section, we will go over some of the decisions and processes we went through to create the palette of materials used in the demo. As in the [Beyond the Dark](../../resources/beyond-the-dark/index.md) demo, we wanted a realistic look and feel for our materials and we achieved it through a few different approaches.
You can break down the process of building all our textures and materials into three areas:
- Surface appearance objects for 1:1 or unique materials for meshes, such as a marble statue.
- Surface appearance objects for shared trim maps for `Class.MeshPart|MeshParts`, such as repeatable wood trim details on furniture.
- [Material variants](../../parts/materials.md#custom-materials) for shared materials on Parts or terrain, such as flagstone replacing cobblestone for both fireplaces and ground.
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/overview.png" width="100%" />
## Plan, reuse, and budgets
When starting any project, give some thought about material art direction for consistency and reuse. Every platform and device has a limit on the amount of memory you can use for textures so some upfront planning goes a long way to help keep a handle on the number of textures in your experience.
When considering the look and feel of the experience, we deliberately chose to go with a craftsman style house to ensure we could reuse a handful of materials over a wide array of assets, from small picture frames and furniture to large architectural elements.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/craftsman-reference-images.png" width="60%" />
<figcaption>Here are some of the reference images we collected for the Craftsman Style.</figcaption>
</figure>
Distilled down to its basics, [American Craftsman](https://en.wikipedia.org/wiki/American_Craftsman) characteristics include simple, clean lines and natural wood tones, letting the wood and artistry of the carpenter take center stage. These characteristics allow us to use one set of wood trim texture maps for furniture and architectural elements in the house and also allow us to keep our models fairly low poly. With this in mind, we set out to create the trim map texture set you see below.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/wood-trim-texture-set.png" width="40%" />
<figcaption>Wood Trim Texture Set</figcaption>
</figure>
## Surface appearance and trim maps
Trim maps are simple repeatable textures that can be applied to a wide variety of assets. Trim maps do a lot of heavy lifting in experiences and fill the gap between completely tiling textures and creating a 1:1 texture set that you would get from painting an object in a program like [Substance Painter](https://www.adobe.com/products/substance3d-painter) or using [photogrammetry](../../resources/the-mystery-of-duvall-drive/construct-the-house.md#photogrammetry). The wood material shown previously was set up as a trim map which means that it could be reused on many different meshes, each mesh taking advantage of the same trim textures just by laying out its UVs creatively.
<GridContainer numColumns="3">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/tiling-texture.png" />
<figcaption>Tiling Texture</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/trim-texture.png" />
<figcaption>Trim Texture</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/1-1-texture.png" />
<figcaption>1-1 Texture</figcaption>
</figure>
</GridContainer>
When using trim map texture sets for use across a large variety of scales and objects, try to keep your textures fairly clean and free of details that would be easily recognizable as repeating.
<GridContainer numColumns="3">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/too-much-detail-texture.png" />
<figcaption>Example of too much detail</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/cleaner-texture.png" />
<figcaption>Cleaner textures work best</figcaption>
</figure>
</GridContainer>
For an example of over-detail, say we wanted to add some grime to our BaseColor map. Look how quickly you start to notice the unrealistic direction of textures and stretched UVs.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/furniture-set-stain.png" width="60%" />
<figcaption>Notice how the extra detail of stain on the furniture on the left stands out for repetition and incorrect direction compared to the cleaner set on the right.</figcaption>
</figure>
If an asset really needs heavy staining, wear, and/or dirt buildup it would be better to use a 1:1 map unique for that specific mesh. For additional information on working with trim maps, see the [Create trim sheets](../../resources/beyond-the-dark/building-architecture.md#create-trim-sheets) section in the Beyond the Dark documentation.
You can package a single Surface Appearance node with these trim maps and apply the trim to all of the assets shown below.
<figure>
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/wood-trim-asset-place.png" width="80%" />
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/wood-trim-assets-in-place.png" />
</GridContainer>
<figcaption>Just some of the many assets that used the same Wood Trim Surface Appearance Node.</figcaption>
</figure>
Tinting is another way to make your Surface Appearance nodes and Material Variants go further. By adding an alpha mask to your `Class.SurfaceAppearance.ColorMap` and setting the `Class.SurfaceAppearance.AlphaMode` to **Overlay**, you can use `Datatype.BrickColor` or `Datatype.Color3` to tint your objects for visual variation.
<figure>
<GridContainer numColumns="2">
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/couch-surface-appearance-set.png" />
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/leather-couch-settings.png" />
</GridContainer>
<figcaption>These 4 color variations are all utilizing the same Packaged Surface Appearance node.</figcaption>
</figure>
Alpha masks, regardless of how they are created, are merged in your `Class.SurfaceAppearance.ColorMap`, changing it from a standard RGB texture to an RGBA. You can play around with values and levels in your alpha mask to get the desired level of tinting, but with a little finesse you can achieve some great results.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/basecolor-rgb.png" width="40%" />
<figcaption>BaseColor RGB</figcaption>
</figure>
In this leather example, we just blended the height map with some scratches and wear marks to create our alpha mask.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/height-map.png" width="40%" />
<figcaption>Height Map</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/scratches-and-wear-mask.png" width="40%" />
<figcaption>Scratches and wear marks</figcaption>
</figure>
</GridContainer>
These two maps were then sent to an Alpha Merge Node to create the final RGBA used in the Overlay Surface Appearance Node.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/final-alpha.png" width="40%" />
<figcaption>Final Alpha texture. White pixels don't apply tint and only show the Base Color RGB map. Black pixels tint and only show the flat BrickColor/Color with a gradient in between the two. </figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/alpha-merge-substance-designer.png" width="80%" />
<figcaption>Example of the BaseColor and Alpha being merged via the Alpha Merge Node in Substance Designer.</figcaption>
</figure>
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/rgba-in-combination.png" width="69%" />
<figcaption>Examples of the RGBA in combination with Mesh Color on white, brown, red, and blue meshes.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/only-colormap-and-mesh-color.png" width="70%" />
<figcaption>Same example only showing the ColorMap and Mesh Color.</figcaption>
</figure>
</GridContainer>
## Texture sizes
Along with heavily reusing textures, you can apply other techniques to keep your texture memory usage down. You can reduce texture sizes based on usage. For example, you can often drop the resolution of your Surface Appearance object's `Class.SurfaceAppearance.ColorMap`, `Class.SurfaceAppearance.RoughnessMap`, and `Class.SurfaceAppearance.MetalnessMap` down by 1 or 2 times without losing any visual fidelity. Many times the `Class.SurfaceAppearance.MetalnessMap` can be left blank, which results in a pure black/non-metallic surface.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/leather-trim-texture-set.png" width="40%" />
<figcaption>This leather trim texture set consists of a normal map at 1024, a base color map at 512, a roughness map at 256, and no metalness map. </figcaption>
</figure>
In [Substance Designer](https://www.adobe.com/products/substance3d-designer), the easiest way to resize outputs is to add a [Transformation 2D node](https://substance3d.adobe.com/documentation/sddoc/transformation-2d-172825332.html) just before your final output node and adjust the **Output Size** within the Base Parameters section for that node.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/transform-2d-node.png" width="80%" />
<figcaption>Transform 2D Node with adjusted Output Size.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/final-surface-on-couch.png" width="80%" />
<figcaption>Final Surface on couch meshes.</figcaption>
</figure>
</GridContainer>
It generally takes several iterations of resizing the various maps in your texture set and visually inspecting the asset in Studio to get them to a spot where they hold up well enough across the variously sized assets. Using this process, you can settle on the smallest texture sizes you feel you can get away with without losing the visual quality needed for the art direction of your experience.
With these few techniques you can keep your texture budget under control and still have a beautiful experience.
<Alert severity="warning">
When importing custom meshes into Studio, base textures included with your model may be imported with your mesh and automatically applied as a textureID. This can become a problem when importing multiple meshes with the same base texture applied. Each import can create a new version of the same texture map and bloat your texture memory.
When a packaged Surface Appearance is applied for reuse, make sure to delete the textureID within the properties of the mesh to prevent this issue. This is a known behavior and Roblox is looking into improving this flow.
</Alert>
## Base materials
Since we used [parts](../../parts/index.md), [terrain](../../parts/terrain.md), and [meshes](../../parts/meshes.md) side-by-side in this demo we chose to utilize the default [built-in materials](../../parts/materials.md#asset-ids-for-built-in-materials), [Material Variants](../../parts/materials.md#custom-materials), and the newly released [Material Manager](../../parts/materials.md#material-manager) tool to ensure our various parts and terrain surfaces match the look and feel of our PBR-textured meshes.
Base materials are available for use as-is or to modify as a new variant. In many cases, the wood base materials serve us very well and only require adjusting the color to match our textured meshes.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/mesh-in-foreground.png" width="50%" />
<figcaption>Example of a mesh in the foreground using a Surface Appearance node and CSG (constructive solid geometry) objects built in Studio with Parts and using the standard Wood material colored to match.</figcaption>
</figure>
## Material variants
Since the experience includes an outdoor environment during a downpour, we knew we wanted our ground to feel wet and muddy to match our stormy pacific northwest art direction. We accomplished this by creating a handful of new [Custom Materials](../../parts/materials.md#custom-materials) using [Material Variants](../../parts/materials.md#create-a-custom-material-in-the-explorer) that we could override the base materials with. We use them to paint down things like mulch, large and small gravel, mud, as well as a shallow puddled version of each.
Material Variants allow the user to redefine textures that are used for any given default material, adding a lot of opportunities to customize materials used in terrain and parts.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/backyard-wet-material-variants.png" width="60%" />
<figcaption>Image of the backyard and use of wet Material Variants.</figcaption>
</figure>
Material Variants are also used to create wet and dry versions of our materials. This is helpful for surfaces such as concrete and our flag stone hardscape that would be dry under cover of the porch roof and wet when exposed to the rain out in the yard.
Creating a Material Variant can be done inside the [Material Manager](../../parts/materials.md#material-manager). As the name suggests, the Material Manager also allows you to manage both default and material variants in one place.
Once the Material Manager is open you will click the Circle Plus button in the upper left of the window to edit and add a new Variant. When adding a name, it's a good idea to add a suffix of \_MV after your name so you can easily pick out the new material variant from the default materials.
Choose a **Base Material** that most closely relates to your new material. You can think of the Base Material field as the **Physical Properties** of the material, which affects things like friction, density, weight, hit effects, and footstep sounds.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/edit-variant-dialog.png" width="60%" />
<figcaption>Notice that the Metalness map was left blank since it was not needed.</figcaption>
</figure>
Why create a Material Variant? Material Variants that override a Base Material inherit all properties of that material, except for the visuals. This means your material variant will inherit the physical behaviors of the default material, such as density. It also means future additions, like adding unique acoustics to concrete, will automatically be inherited. In this case, we want to create a new wet concrete variant that inherits nearly all the properties of the concrete material.
If the Base Material relates to the **Physical Properties** then the Texture Maps are our **Visual Properties**. **Texture Maps** tell the renderer how light should interact with the material through our PBR rendering pipeline. The Base Material field also defines what default material is overwritten with the new visual properties. In this case, we are proceeding to overwrite the **Concrete** base material.
Now that we have created our new "Concrete_Wet_MV" we need to override the default Concrete material with it so that we can use it in our scene. To do that just click on your new MV and click on the **Set Override** toggle.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/set-override.png" width="60%" />
<figcaption>Notice that the default Concrete material changes to the new Concrete_Wet_MV when the Set Override is toggled.</figcaption>
</figure>
<Alert severity="warning">
Material Variants can be applied on a per-part/meshpart basis, so you can make an unlimited amount of custom materials simultaneously used on parts. Terrain requires using overrides for customization, and you're limited to overriding the 22 built-in materials.
</Alert>
Now everything that uses Concrete in your scene will now be using the new override, including any concrete that you might have painted down on your terrain. The only exception to this is parts that have a specifically listed "material variant" on that object.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/material-variant-overrides.png" width="50%" />
<figcaption>Example of Material Variant Overrides being used on terrain and parts.</figcaption>
</figure>
To allow our terrain to have a variety of surfaces, including both wet and with puddles, we ended up overriding about 10 of the default materials. This was necessary for the rain soaked look we were going for on our final terrain.
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/before-override.png" width="80%" />
<figcaption>Before Material Variant Override.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/after-override.png" width="80%" />
<figcaption>After Material Variant Override.</figcaption>
</figure>
</GridContainer>
Using all the material options and tools at hand, the possibilities of what you can create are limitless. With a little forethought and planning upfront, you can create awe inspiring materials all while keeping your experience textures under budget.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/materializing-the-world/backyard-terrain-materials.png" width="80%" />
<figcaption>Shot of the backyard showing the variation in the terrain materials.</figcaption>
</figure>

View file

@ -0,0 +1,59 @@
---
title: Stream in immersion
comments:
next: /resources/the-mystery-of-duvall-drive/develop-a-moving-world
prev: /resources/the-mystery-of-duvall-drive/immersive-narrative
description: Explains the use of instance streaming The Mystery of Duvall Drive.
---
Roblox Studio is a powerful engine that can create experiences on a wide variety of platforms, but even some high-end phones may struggle to load all of our content at once. To address this, we used [streaming](../../workspace/streaming.md) to achieve the highest level of quality possible on a wide spectrum of devices. Streaming allowed us to author all our content in one place and dynamically load smaller portions of the content visible to the user.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/streaming-in-immersion/content-streaming.png" width="100%" />
<figcaption>Streaming enabled on the workspace itself, with default settings</figcaption>
</figure>
## Technical considerations
When enabling streaming on your experience, there are a few things to consider to ensure that your experience runs well and that you maximize the benefits of the streaming feature:
1. **Are there scripts or elements that are reliant on another piece of content?** If so, one or more of your dependencies may not be loaded at certain times.
2. **Are there scripts that scan the workspace when the experience starts?** Client scripts should not rely on finding all instances they require at the time an experience starts and this is not a recommended practice.
3. **Does your experience suffer visually if some content doesn't display at the correct time?** There are various techniques and tricks to create a [streaming-friendly environment](#streaming-friendly-level-design) and ways to use [perspective](#play-with-perspective) to your advantage.
When creating The Mystery of Duvall Drive, we encountered all three of these problems and were able to work around them by using a combination of careful script design and cleverly applied set dressing and world layout. When working with streaming on your own experience, remember you can tune your streaming min/max distance to what suits your experience and adjust a variety of [highly customizable options](../../workspace/streaming.md#streaming-properties).
<GridContainer numColumns="2">
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/streaming-in-immersion/64-max-distance.png" />
<figcaption>Streaming with a max distance of 64 stud units.</figcaption>
</figure>
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/streaming-in-immersion/1024-max-distance.png" />
<figcaption>Streaming with a max distance of 1024 stud units.</figcaption>
</figure>
</GridContainer>
## Streaming-friendly level design
To create an immersive environment, players have to be consistently surrounded by the world we built for them. When using instance streaming, we had to make sure that players wouldn't see unexpected content, such as seeing the "end of the world" if there weren't any visual assets loaded in their instance streaming range.
Since distant trees behind the house and other blocking geometry like hills and mountains would be out of range, we had to find a solution to keep players from seeing the horizon and breaking their immersion. To address this, we added nearby blocking geometry along all areas of the player's path and designed the path that would wind to effectively hide the lack of content in the distance. We placed blocking geometry in a way that ensures the player would be surrounded by streamable visual assets at any point on the path. This aligned well with our original design to create a windy driveway and is a prime example of thoughtful level design that incorporates aesthetic and technical applications.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/streaming-in-immersion/winding-path.png" width="80%" />
<figcaption>The player starts surrounded by nearby trees and bushes to help hide the fact distant content hasn't loaded yet. The winding path keeps much of the content hidden until players are close enough to load the asset.</figcaption>
</figure>
## Play with perspective
There are some cases where content that a player would expect to see from far away is too far from the camera, such as our [storm in the sky](../../resources/the-mystery-of-duvall-drive/develop-a-moving-world.md#create-the-storm). We wanted players to see this large phenomenon, but it would break the immersion if the sky storm didn't exist when the player was too far away to load the asset. It would also be jarring to see a large asset suddenly load in when the player got close enough to an object that they should've seen further away. We tried to minimize this by playing with perspective and making sure the models in the sky were large enough to be within the bounds of the player's streaming distance.
<img src="../../assets/resources/mystery-of-duvall-drive/streaming-in-immersion/perspective-house.png" width="80%" />
Another solution was to increase the vertical height of the trees surrounding the player when they weren't close enough to the storm. This solved the situation where the player is too far away to load the asset but would still expect to see it in their line of vision. Big trees are common in our Pacific Northwest setting, and this adjustment helped solve the problem with minimal concessions to our aesthetic and design.
<figure>
<img src="../../assets/resources/mystery-of-duvall-drive/streaming-in-immersion/perspective-treelines.png" width="80%" />
<figcaption>Treelines help to obscure some of the content not loading in yet.</figcaption>
</figure>

View file

@ -0,0 +1,128 @@
---
title: Supporting systems
comments:
prev: /resources/the-mystery-of-duvall-drive/foundational-gameplay-systems
description: Explains the supporting systems and functions in The Mystery of Duvall Drive.
---
We used the following systems to support both the [foundational gameplay systems](../../resources/the-mystery-of-duvall-drive/foundational-gameplay-systems.md), as well as any goals of the [main design requirements](../../resources/the-mystery-of-duvall-drive/main-design-requirements.md).
## 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 `Class.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 `Class.LocalScript` utilizes a new engine feature `Class.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](../../effects/highlighting.md). `Class.Highlight|Highlights` and the mouse cursor **[OnItemIndicator](#onitemindicator)** systems work together, so `Class.Highlight|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](../../resources/the-mystery-of-duvall-drive/foundational-gameplay-systems.md#eventmanager) uses it with an **Enable** command to enable or disable a `Class.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 `Class.CollectionService.GetInstanceRemovedSignal`.
## Lore and ThoughtBubbles
**Lore** and **ThoughtBubbles** are 2 similar systems. Lore uses a `Class.ScreenGui` as an on-screen UI container with a child `Class.Frame` to control the sizing and rescaling of its children `Class.TextLabel|TextLabels` and `Class.ImageLabel|ImageLabels`, and lore waits for the player to click anywhere on the screen to remove it. Similarly, thought bubbles use a `Class.BillboardGui` with a child `Class.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](../../resources/the-mystery-of-duvall-drive/immersive-narrative.md#lore) and [Thought Bubbles](../../resources/the-mystery-of-duvall-drive/immersive-narrative.md#thought-bubbles).
Lore is implemented in the **LoreManger** `Class.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 `Class.Camera.ViewportPointToRay` or `Class.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`](../../resources/the-mystery-of-duvall-drive/foundational-gameplay-systems.md#gamestatemanager) 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 `Class.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`](#highlights) that determines the highlight status, the types of items, and which icon to use. For example:
```lua
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`](#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** `Class.LocalScript` uses a **Door** tag and `Class.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** `Class.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 `Class.SurfaceGui` would have an Animator `Class.ModuleScript`, with the main purpose to provide an `Animator.GetParams` function that returns all the parameters. This helps the MasterAnimator `Class.LocalScript` that uses the **ImageAnimation** tag and `Class.CollectionService` to gather such objects, and find the Animator `Class.ModuleScript` under them. It then uses pcall to require Animator `Class.ModuleScript` and call `GetParams` on it.
## LocalSpaceAnimations
The **LocalSpaceAnimations** `Class.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](../../resources/the-mystery-of-duvall-drive/develop-a-moving-world.md#rotating-cloud-meshes).
## HeadlampManager
The **HeadlampManager** `Class.LocalScript` handles when users select the on-screen `Class.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 `Class.ClickDetector|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 `Datatype.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 `Class.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 `Class.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 `Class.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 `Class.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 `Class.CollectionService` to add `Class.ClickDetector|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** `Class.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](../../resources/the-mystery-of-duvall-drive/main-design-requirements.md#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:
```lua
P1.Touched:Connect(function(otherPart) utils.ProcessPortal(otherPart, P2) end)
```
**ProcessPortal** handles checking that otherPart is a human, teleporting the player through a `Datatype.CFrame` coordinate change, and invoking a small cutscene to hide the transition using a **Teleport_Jump** event in [EventManager](../../resources/the-mystery-of-duvall-drive/foundational-gameplay-systems.md#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](../../resources/the-mystery-of-duvall-drive/foundational-gameplay-systems.md#eventmanager).
**AudioUtils**. A couple of functions to play weighted random sounds from a set.
**GrabUtil**. Helper functions for grabbing.

View file

@ -0,0 +1,14 @@
---
title: Technical overview
comments:
next: /resources/the-mystery-of-duvall-drive/main-design-requirements
description: Explains the fundamental design and gameplay systems in The Mystery of Duvall Drive.
---
The following document is a technical overview of the main design requirements and systems we created for [The Mystery of Duvall Drive](https://www.roblox.com/games/7902470429/The-Mystery-of-Duvall-Drive) experience. This high-level information is meant as an aid for technical designers and engineers as they dig deeper into the experience's source code.
You can follow the reference articles sequentially or jump between the following sections:
- **[Main design requirements](../../resources/the-mystery-of-duvall-drive/main-design-requirements.md)** covers a high-level overview of the essential systems we created, including a mission framework, three types of teleportation, and gameplay scripting.
- **[Foundational gameplay systems](../../resources/the-mystery-of-duvall-drive/foundational-gameplay-systems.md)** digs more into the implementation of the main gameplay systems that meet the main design requirements.
- **[Supporting systems](../../resources/the-mystery-of-duvall-drive/supporting-systems.md)** explores any smaller systems made to assist the foundational systems.