Milo Jacobs - Sleeping with the Roaches Portfolio

Sleeping with the Roaches is a third-person two-player puzzle platformer where the players, tied together with a spider web, must work together to escape the spider mafia. Players are stuck in a cluttered basement and must use the spider silk that connects them and their own unique abilities to get out.

Sleeping with the Roaches was made over a 7-week period within WPI's Interactive Media and Game Development Program on a team of 6, with 4 tech students and 2 artist students. Most students on the team, including me, were completely new to Unreal Engine at the start of the term. I was the producer, rope coordinator, interactable object maker, export debugger, and one of the level designers. I fell into the role of producer by coordinating work for others during meetings and coordinating who would write which parts, but my technical work is much more interesting; it's listed below.

The Rope

I was in charge of the main physics mechanic of the game: the web connecting the players. The web was designed to be both a handicap to the players and a vital tool for overcoming platforming challenges. To implement rope-like physics, I used a skeletal mesh with 30 bones in a straight line. The skeletal mesh had constraints adhering each bone together and separate constraints connecting each end to the two main characters. These constraints combined with the simulated physics were finnicky, but they worked well enough for our purposes.

The web also had another important mechanic: launching. This mechanic is what made it a vital platforming tool. If the players stretched the web far enough, it would snap back like a rubber band, launching them in opposite directions.

Our two player characters, Giovani and Marcelo, are a spider and a moth, respectively. Giovani can make webs that the players can get stuck in, and Marcelo can fly. They can use the differences in their character controllers to complete platforming challenges, where Giovani shoots a web high up, Marcelo gets stuck in it, and then they use the launching mechanic to get Giovani up to a new location. At first, the web hinders them because Marcelo cannot freely explore the area without Giovani, but then it helps them by allowing Giovani to get there in the first place.

Web launching occurs in 3 stages. By default, the web is in stage 1. When the characters get far enough a part, the rope changes color and lengthens, bringing it to stage 2. This happens again for the third stage when the players get farther apart, and when the length reaches a final threshold, a large impulse is applied to each character. To detect when the rope should stretch, we used several approaches, but settled on a simple calculation of the length of the two lines between each character and the center of the rope. Doing a full calculation of the distance between each individual bone was possible, but it led to some unintuitive results and discrepancies between different systems on the exported version, which I will describe in a later section.

Below is a control flow diagram detailing how the rope works.

Interactable Objects

I was in charge of making the interactable objects throughout our game. One object was a pair of skis that the players could knock down to unlock a new area. Another was a baseball that the players could use the web to launch, which broke a window. Lastly, I implemented a coat hanger that the players could ride to victory at the end of the level.

Each of these objects' behaviors was specially coded for the task at hand. In a game with very few interactables, all of which do something different, this was the quickest approach. The skis have a trigger near the top that cause their angle to be linearly interpolated, simulating being knocked over.

The baseball has no physics until the players launch the rope within a specific trigger behind it. The baseball then receives physics simulation and a specifically chosen linear velocity that causes it to be launched up a ramp to the window. When it collides with the window, the window is deleted.

The coat hanger waits until both players have entered a trigger, removes them from any webs they may be inside, connects two physics constraints to prevent the players from moving relative to the coat hanger, then flies down a spline to the end of the level outside the window.

Level Design

I helped the team in brainstorming a lot of the initial level design challenges, including some of the web-swinging-based platforming challenges, but the main section of the level I made was the final segment of the level leading up to the coat hanger.

I wanted this section to be a sort of "apply what you've learned" area. There weren't any new mechanics, but instead a few of my personal favorite uses of the web mechanic from previous points in the level in a more challenging form. Therefore, I started with a tall vertical wall that the players needed to scale. This can be solved similarly to the very first segment in the game, by getting Marcelo stuck in a web and letting Giovani catapult up, but the height needed is much more precise.

The next part was a reprise of the "swing over the gap" challenge. Previously, the players had to swing over a gap leading to the pegboard. At the end of the level, the players need to overcome a similar challenge. The main difference, however, is that at the end of the level there is no height gap between the start and end of the jump, which means they must be more precise than before to make it over the gap.

The last part of this section was climbing the stairs. I opted not to add much to the model and hitbox of the stairs already in the game, because I found it to be an interesting free-form platforming challenge that could be overcome in a variety of fun ways. I added some webs to give players various ideas of how they could get over each step, but I ultimately left that decision up to the players.

Checkpoints

I was in charge of implementing checkpoints in the game as well. Usually, the task would be very straightforward; I would simply keep track of the last checkpoint the players visited and move them back when they press a button. But due to how skeletal mesh physics constraints work in Unreal, the moving became quite challenging. The main issue was that there is no direct way to reposition the individual bones of a skeletal mesh. And if the players get too far apart, the mesh constraints can become so far from being met that the engine applies an extreme amount of force to the web and its attached bodies. This sometimes leads to the engine simply deleting the web or the spider entirely.

I tried many approaches to fix this. One was simply moving Giovani to wherever the end of the skeletal mesh ended up after moving Marcelo. This worked well enough, except if Marcelo was far above Giovani when moving back to the checkpoint, Giovani would spawn in the floor. I made a very complex system to try to keep the characters' relative locations before the move as similar as possible to their relative locations after the move, but ultimately nothing I did created a reliable enough checkpoint system. Either the character positions would be unreliable and could end up out of bounds, or the rope could break upon returning to a checkpoint (or sometimes both!).

Another issue I wanted the checkpoints to mitigate was that, especially on lower-end systems, the rope could break and disappear entirely when it stretched too much. This was an inherent issue in using Unreal's skeletal mesh for the webs, but it could be mitigated by reinstating the rope when the players return to a checkpoint.

I solved both of the above problems by making an entirely different checkpoint system. Under this system, the entire level is reloaded upon moving to a checkpoint, and the players' positions are updated before the rope has a chance to move. This came with the added benefit of reinstating the rope automatically, but it meant I needed to reinstate other parts of the level that had been activated. For example, the skis need special functionality to spawn already knocked over if the players go back to a checkpoint after knocking them over. Similarly the window needs to start broken if the players have broken the window. Lastly, the dialogue triggers that have been activated need to remain activated when the player reloads.

To make this data persist, I added several variables to the Game Instance class, which does not get reloaded when the level loads. Restoring the state of the interactables was fairly straightforward. The skis would simply check if they were supposed to be knocked over at the start of the scene, knocking themselves over if necessary. The baseball and the window would simply destroy themselves at the start of the scene if the window has already been destroyed. Restoring the dialogue was a little bit more involved. When the player returns to a checkpoint, the checkpoint system loops through all existing dialogue triggers, checks if they've been activated, then sets up a corresponding array of booleans in the Game Instance. When the scene is restored, the checkpoint system will loop through this array and set the dialogue triggers' variables according to this array.

Version Control

We used plain old GitHub for version control, and it worked quite well! It's important on a big project like this with a lot of Binary files to work on different branches, though, and to coordinate with your team so that you're not working on the same things at the same time.

Since Unreal stores its data in binary files (often without human-readable names), handling merge conflicts is near impossible. That's why we found it important to work on different branches and merge them, so that both changes can be kept if there's a conflict, and so that we can manually resolve any conflicts that emerge without risking the integrity of the main branch.

Challenges and Lessons

Debugging the Export

I was the one in charge of exporting the game, which meant it was my job to track down the nastiest, most vile types of bug in game development: discrepancies between in-engine behavior and behavior of the exported game. Most of these export issues were, unsurprisingly given my experience in Unity, discrepancies between the physics tick rate in both versions. However, these issues weren't because we simply forgot to multiply by delta time. They were differences in behavior baked into Unreal Engine itself. For example, calling the "Add Force" function for a single frame caused majorly different behavior. Because Unreal multiplies by delta time for you, a single frame of added force will mean different amounts on different machines. Therefore, when applying instantaneous forces, we used the "Add Impulse" function instead, which didn't rely on the tick rate.

The more difficult issues related to Delta Time were baked into the connecting web itself. The physics constraints in a skeletal mesh are not always met; they simply apply force to the other bones and attached bodies to adjust to any violation of these constraints. This behavior is built into Unreal Engine. The issue with it is that the more physics frames you have, the more opportunities the engine will have to enforce the constraints, making them more likely to be met. At first, this caused a major discrepancy between the web launching mechanic on different machines. On higher-end machines, the length calculation always returned shorter values, making it almost impossible to trigger a launch, while on lower-end machines, the length calculation always returned longer values, making launches trigger way too often. We mitigated this issue by switching the rope length calculation to the two-line approach described above. This didn't fix all of the inconsistent behavior of the rope on lower-end machines, but it did cause more consistency between machines and at least made the rope launching detection consistent.

The last export issue was a simple one: calling the "Get Actor of Class" function on the "Begin Play" event works perfectly fine in the Unreal editor, but it returns null if called in the exported version. It was a pain to find out why this wasn't working, but it was an easy fix once I knew. If you simply wait until the first tick to call the function, it will work just fine.

Lessons from Debugging the Export

There are several lessons to be learned from these debug issues. The first is not to overengineer. In the length calculation, I should have started with the simpler, inaccurate solution that used the distance between players. Instead, I started with iterating over each bone to find the literal length of the rope. Though this approach seemed better intuitively, it was worse than the simple solution and took much more time too. In game development especially, you should always start with a simpler (if inaccurate) solution, because it might just work. And if it doesn't, you can always do the harder solution later.

The other export lesson is to never trust a game engine's default update function. It pulls little tricks on you, and if you're learning a new engine, start by learning what those tricks are and how to get around them. It will save you so much time in the long run.

Advice for Future Rope Programmers

If I had more time, I would have implemented the rope physics using a poseable mesh for rendering and something like Verlet integration to simulate the physics myself. This would have been much harder, but it would have given me a lot more access to the internal state of the rope and not forced me to rely on Unreal's skeletal mesh physics, which are often unreliable. In making a game entirely based around a rope, an approach like this would have been majorly beneficial. However, it isn't necessary in all games. If you're making a game that has a rope as one of many mechanics rather than as a core element, Unreal's skeletal meshes will probably work just fine for your purposes.

If you're working with simulated skeletal meshes, don't rely on the physics constraints being 100% met all the time, especially not if you have long chains of physics constraints. They will be violated quite often, especially on lower-end machines.