Logan Pladl
Technical Game Designer
Parallel Universe Pizza Delivery
Platform
PC
Engine
Unity
Duration
May 2021 - November 2021
Team Size
Solo
Overview
A first-person puzzle platformer where you have a limited amount of time to deliver pizza to your customer's front door, but you can't do it on your own. You need to rewind time, shift into parallel universes, and work together with your parallel selves to get to the customer's door in time!
Accomplishments
-
Conceived and implemented a novel and technically complex core mechanic that received high praise from players.
-
Wrote tests to efficiently find and fix bugs in the game’s complex systems.
-
Analyzed playtest footage to identify points of confusion and unintended puzzle solutions.
-
Solved gameplay problems by implementing new mechanics and iterating level design.
Core Mechanics
Overview
For this game, which I originally made for the Toronto Game Jam, I implemented a complex core mechanic. The player must control multiple characters to traverse each level within some time limit; in the fiction of the game, they are switching between versions of themselves from parallel universes. Here is the end result:​
The player starts as the character in the blue universe. After pressing the Rewind button, the game rewinds to the start and the player chooses to switch to the character in the red universe. The blue character then repeats the exact actions that the player just performed while they were inhabiting the blue character.
The player must utilize the characters at their disposal to set up conditions that will allow them to reach the house and deliver their pizzas on time. For example:
The player cannot reach the house alone, but if they place one character on the lower platform then the other can land on their backpack and jump off to reach the goal.
To summarize:
-
Levels have multiple "universes," each with one character that the player can control.
-
Levels have a time limit, and the player must reach the house and knock on the door before then.​​
-
If the player fails to reach the goal on time or presses the Rewind button, then the game rewinds to the start.
-
The player is then presented with a choice to switch into a different universe and control a different character.
-
Characters that the player is not controlling repeat the exact actions that the player performed when they were last controlling that character.
-
Each character has a big backpack that acts as a platform that other characters can stand on and jump off of.
Rewind System
I'll first explain how I accomplished the rewind effect.
​
Objects are rewindable if given a component "RewindTarget." Characters all have this component, but there are other rewindable objects that do as well, so the modularity of a component was valuable. RewindTargets are responsible for recording their state over time, as well as rewinding backwards through that stored state. Another script, RewindController, communicates with all of the RewindTargets in the scene to inform them when it is time to start recording or rewinding.
​
When recording, RewindTargets create TimePoint data every physics tick and save that data in an array of TimePoints.
A time point with the above data is saved once every physics tick for every rewindable object in the scene.
The TimePoint data array contains all of the information needed to rewind the object back through time:
-
The object's position at that physics tick.
-
The object's rotation at that physics tick.
-
The camera's pitch at that physics tick if applicable (only for characters).
​
While rewinding, the RewindTarget iterates backwards through the array of stored TimePoints each physics tick, setting the object's current position and rotation to match the TimePoint at the current index. The end result is that all rewindable objects appear to be moving backwards through time.
The end result of the rewind behavior.
As seen in the above example, the game doesn't rewind at the same speed as normal gameplay. The rewind effect always lasts for 3 seconds, and so the RewindTargets don't iterate through every single TimePoint in their rewind data while rewinding; they instead pick an index depending on the current progress through the rewind effect (from 0 to 1).
​
Note that characters also add some additional functionality while rewinding:
-
The player cannot rotate the camera while rewinding (since it's being controlled by the rewind logic).
-
The player character is normally a physics object but is set to a kinematic object while rewinding (since again, the position is being set directly by the rewind logic instead of being affected by forces like normal).
-
The player character's animation is set to manual playback mode, leveraging Unity's built-in recording/playback functionality. I set the animation to record during gameplay, and manually set the playback time during rewind so that all of the animation that occurred during gameplay appears to happen in reverse.
The character class executes some additional functionality on top of the base RewindTarget functionality.
Input Storage and Replays
Separate from the rewind system, I also implemented an input-based replay system to drive the characters repeating the player's actions.
​
It may seem like I could've just used the rewind system's stored TimePoints to replay each character's actions, but this was not an option because characters are physically simulated and can affect each other's movement. Static state-based replays would have resulted in strange behavior; for instance, replay characters could've ended up floating in the air or jumping off of surfaces that are no longer there.
Physically simulated characters also support the comedic tone of the game and encourage extra caution; it's possible for the player to get in the way of their replay characters and cause them to diverge from their original movement, leading to replay characters potentially falling off ledges for instance.
The characters are physically simulated and can influence each other's movement, so the player needs to be careful not to get in the way of their replay characters.
I implemented this input-based replay system using the Command Pattern, in which inputs are reified as objects and in my case stored in an array similar to the TimePoints from earlier.
My Command Pattern implementation. I use an abstract Command base class and derive children for concrete commands such as Movement. Note the constructor storing the input axes and the Execute function performing the actual functionality associated with that input.
All relevant inputs have a derived Command class:
-
Movement (2d axis)
-
Look (2d axis)
-
Jump (button)
-
Interact (button)
​
Every physics tick, a script checks if any of those inputs are active. For all active inputs, a Command object of the correct type is created and added to a list of key/value pairs, with the key being the current physics tick ID (a simple integer that is incremented every physics tick) and the value being the Command object.
Each character has their own list of tick ID/Command object pairs. If a character is not being controller by the player, a script iterates through that list and executes each stored Command on the physics tick that matches the stored ID. The end result is that the characters repeat the player's stored inputs.
Playtest Problems and Solutions
I found several design issues while playtesting. In the next few sections I'll show those problems and explain my solutions.
Backpack Hanging
One issue was that characters could hang from their backpacks like so:
Falling off a ledge can result in the player's backpack hitting the ledge, leaving them hanging there.
Aside from looking janky, this was initially a serious exploit because players could actually jump in this state. It was possible to deliberately hang your backpack on a ledge and jump up to reach platforms that would normally be unreachable.
​
It would've been simple enough to check for these conditions and disallow the player from jumping, but what should actually happen here? I had a few options:
-
Disallow the player from moving altogether. They are truly stuck once they start hanging from their backpack, unless something happens to knock their backpack off the ledge.
-
Disallow the player from jumping but still allow them to move. This would've fixed the exploit but would've still been very janky looking; the player would be walking on air and sliding around on ledges.
-
Introduce a new mechanic to account for this situation.
​
When I run into a problem like this, I try to turn it into an opportunity and introduce a solution that has benefits beyond just solving the immediate problem. In this case, I had an idea for a mechanic that would not only solve this problem but introduce more depth to the game as a whole and facilitate the creation of more puzzles: what if the player falls out of their backpack when they start hanging from it?
The player falls out of their backpack shortly after they start hanging from it. Note that everything looks correct while rewinding as well.
As soon as the player starts hanging, the camera is strongly forced straight ahead to indicate loss of control (though the player can still move the camera a little bit). After a brief delay, the backpack is disconnected from the player and becomes a physics object. The player is also shown a "Pizza Dropped" UI message.
Even the replay character can still pick up their backpack, since Interact is a Command that's being saved by the replay system.
The player can pick their backpack back up by interacting with it, and in fact they need to do this otherwise they cannot finish the level (the goal is to deliver the pizzas in there, after all).
The player cannot finish the level if they don't have their backpack.
Seeing an opportunity to turn this into a meaningful mechanic, I also made it so that characters jump slightly higher if they aren't wearing their backpack. I also introduced a new "Pizza Magnet" object that prevents the player from jumping while wearing their backpack. Yes, it's ridiculous, but fits the tone of the game.
The pizza magnets on this platform prevent the player from jumping, and when the player falls and drops their backpack they're now able to jump.
This is a solution I was very happy with, since not only have I solved the original problem, I now have several more tools in my toolbox for making interesting content.
One interesting emergent property of this mechanic is that characters can pick up each other's backpacks. This is another quirk that I could use as the epiphany moment of a new puzzle.
Time Limit Difficulty
One big question: how long should the time limit be for each level? Some players will be vastly better at first-person platformers than others, which makes this tricky. For the initial build I simply timed how long it took me to finish the levels without trying to go particularly fast, added several seconds to that, used those numbers for the time limits and called it a day.
​
However, during playtesting I discovered that wasn't nearly enough for some players. I underestimated how quickly I finish the levels and how slow some players are in comparison.
This is how I play the first level. I finish with about 5 seconds to spare.
This is an approximation of how a lower-skilled player plays the first level. They don't quite finish in time.
When I play the game, I quickly perform my jumps without hesitating or pausing my forward movement at all. Lower-skilled players, on the other hand, tend to stop and back up to make sure they have enough runway before attempting a jump. From my observations, this is how lower-skilled players approached the jumps even when trying to go fast.
​
So here's a problem: it's not possible to make a time limit that works for both of these players. If I were to extend the time limit to be long enough for the lower-skilled player to comfortably finish the level, that would make the time limit utterly meaningless to the high-skilled player.
​
Granted, the speed-run element of this game is not the main attraction; the puzzle solving is the meat of the game, and so players who are low-skilled at platforming should still be able to complete the game so long as they're decent puzzle solvers. However, the time limit still enhances the experience by lighting a fire under players. I didn't want to lose the additional excitement and urgency that the time limit brings, and another good effect of the time limit is that it minimizes potential time-wasting in puzzles by forcing players to rewind often.
With this in mind, I settled on a simple, classic solution:
Players can select a difficulty on the title screen.
This difficulty selection screen asks players to gauge their own speed in similar games, and the time limits for each of the levels depend on which option the player selected. It's not a perfect solution, but it worked perfectly well for my purposes as a simple solution that allows players of all skill levels to feel a little bit of pressure to complete levels on time without excluding anyone from being able to complete the game (the easiest option is extremely relaxed).
I used Unity's ScriptableObjects to define each level's time limits for each difficulty option.