Perpetomb

Perpetomb

Khalid will die in five minutes.
Then, he will die again.
He is trapped in a time loop inside a pyramid.
Help him escape!

In Perpetomb, an ambitious teenage thief has to break out of a deadly time loop that keeps him trapped in the pyramid he just robbed.

In this 1st person escape-the-room adventure game, you, in the shoes of thief Khalid, solve the puzzles of the pyramid, explore the story of the pharaoh and manage information from previous lives.
Will you make it out of this ancient Egyptian prison with the treasure... and your life?

This game was made by a team of five people as a 2nd semester project for the Animation and Game study programme at the Darmstadt University of Applied Sciences.
The trailer for the game.

Information

Jonas Büttner Art direction Texturing & lighting Level design Concept art
Tobias Höhn Sound design SFX
Silvia Picazo Soriano VFX Modeling Cinematography
Oliver Rosengarth Game direction Game design Programming Writing
Saskia Schneising Management Modeling Animation UV mapping
Platform PC
Genre Adventure game 1st person
Mode Singleplayer
Engine Unity (C#)
Play Time 2-5 hours
Development April - July 2019
Links Discord Server Windows Download h_da Page
A showreel that summarizes my contributions to the project.

Game Design

For the original pitch, I came up with a basic game design idea:
You have inventory-based adventure game mechanics and you die every five minutes and go back in time, so you lose your inventory but keep your memory, so you have to use the five minutes to collect information and eventually learn how you can survive.

At the start of the concept phase, the whole team worked on deciding the setting, characters, story and art style.
Then, still early in the concept phase, we decided that it was time to create a first prototype, to see whether we can create puzzles that work with the setting and time loop mechanic, and how this mechanic feels and whether five minutes are the right amount of time.
The First Prototype
Jonas Büttner and I created the puzzles for the first room together.
First, we talked about what was important for the puzzles to work well with the time loop mechanic. Our ideas were:
  • If you escape the room and then die, you'll end up in the first room again and have to escape it again. This shouldn't become repetitive or take away time needed for other rooms.
    • Therefore, we decided to have a combination lock for the door.
  • Long linear chains of puzzles would be problematic, since the player would have to solve the entire chain within one loop, and if they were missing one part, they'd have to repeat everything leading up to it after death. Therefore, we instead wanted to have multiple small puzzle chains that can be solved in parallel.
    • Therefore, we decided to split the code for the door into multiple pieces and put each of them at the end of its own puzzle chain.
  • To make sure the time loops are necessary, we decided on having the same items being used for multiple different things. Not just that, but having some of these uses be mutually exclusive, so the player would have each of them in their own loop.
Then, we came up with the details of the puzzles via brainstorming and I organized them into a flowchart (see above on the left) as a reference for later programming. This first flowchart only really worked as a reference for me, who already knew what everything was supposed to mean.

Later, I analyzed the gameplay and puzzle structure of Chaos on Deponia, Virtue's Last Reward and The Witness for research. In the process, I developed my own system for visualizing puzzles. As a consequence, future flowcharts became more advanced, see for example the one above on the right.
Gameplay Decisions
At this point, we had not decided on the gameplay beyond the puzzles yet, so before being able to create the prototype in Unity, I had to come up with something that would at least work for making all puzzles playable.

I noticed that a certain item needs three separate interactions: the player can examine it, push it and take it.
These became the player's core actions.

Originally, we wanted the game to be in third person, as that would make the deaths clearer. We also decided on a freely movable camera, to allow for better exploration and puzzles that utilize perspective inspired by The Witness.
However, these perspective puzzles did not work well in third person and the character would also get in the way of important wall paintings and other objects, so I added a first person zoom mode for this prototype.
Later Developments
Later on, I thought about making the game entirely in first person, other than third person cutscenes, since the third person perspective did not seem to add anything to the gameplay, so I discussed this with the team and we decided to make the next prototype in first person.

We also worked together as a team to make the puzzles simpler and more story-based after feedback from the stakeholders.

For the other levels of the game, we used roughly the same methods as for the first room.
Jonas Büttner and I would brainstorm puzzles together based on the same principles, then I would create a flowchart and work on the puzzle logic while he would work on designing room layouts and props.
The flowcharts turned out to be helpful not just for describing the puzzle logic, but also for finding problems with it and fixing them.
  • Story Writing

    Because of the first person perspective, Khalid's character and emotions couldn't be shown visually outside of cutscenes, so the written dialogue became important.
    The basics of Khalid's characterization and story were developed in the whole group.
    To get it across within the dialogue, I first fleshed out the character in my head and then mentally put myself in his shoes to find out what he would think about and how he would feel about the events and objects in the game.

    In the following, I will show some examples for the main purposes of the dialogue.
    Who is Khalid?
    His general choice of words also shows that he is rather young.
    What Does He Do and Why?
    Khalid wants to steal the pharaoh's treasure to show that he is the master thief.
    Rather than dump information on the player, I wanted to show (and not tell) this through multiple pieces of dialogue.
    What Happened to Him?
    Since the game cuts from going into the pyramid right up to Khalid getting locked in the pyramid after stealing the treasure and being cursed, I was worried that the player might be confused about what happened in between.

    Also, it was important to get across to the player that Khalid is stuck in a time loop (rather than just respawning after death) and to have enough hints towards him being cursed for the player to figure it out, as this was never explicitly stated.

    Some of these things were done visually in cutscenes, but the dialogue also adds to it.
    How Does He Feel?
    In the beginning, Khalid feels excited and confident about his adventure and finally proving that he's the master thief.
    Later, he has to handle being stuck in the pyramid, dying over and over.
    Comic Relief
    The dialogue contains some jokes for comic relief.
    Puzzle Hints
    I also used the dialogue to assist the gameplay, such as giving hints for difficult puzzles.

    Programming

    For the first prototype mentioned above, I had to find a way to implement the entire large and complex set of puzzles in the flowchart from scratch.

    Hardcoding the puzzle logic in C# would have been inefficient and would have created a bottleneck, as that would mean that only I as the programmer could implement the puzzles or change some of their details.

    Therefore, I designed and programmed my own system for defining the puzzles inside the Unity inspector, inspired by the Unity Adventure Game tutorial.
    Actions
    The smallest unit in this system is an action: a single event that happens in the game, for example showing a set of dialogue, setting an ingame variable to a certain value or enabling or disabling a game object.

    Actions are rather basic, for example, taking an object into the inventory is not a single action but consists of three actions: disabling the object in the environment, enabling its counterpart in the inventory and playing a sound effect.
    For anything that cannot be constructed from these basic actions, there is also an ExecuteScript action that runs a C# script.

    An action consists of an enum for the type of action (such as SetActive) and a string array for its parameters (such as the name of the object and whether it should become active or inactive.) This structure was chosen so that actions could be easily set in the Unity inspector without use of custom editors.
    To perform the action, the game uses reflection to find a static function in the Action class with the same name as the enum value, converts the string array into the necessary parameters and calls this function.
    Variables
    Ingame variables are for storing information on the player's progress, for example whether the player has input the first part of the door code correctly or how many times the player has died.

    Looking at the puzzles we had at this point, I figured that all necessary values would be either integers or booleans, although the former could represent the latter.
    Therefore, I decided that ingame variables would be stored in a Dictionary<string, int> on a dedicated object called Memory, the string being the name and the int being the value.

    However, some variables needed to be reset after death (e.g. whether a certain object is currently on the floor), while some needed to not be reset (e.g. the number of deaths), so I created two seperate dictionaries on two separate objects, only one of them being in DontDestroyOnLoad in Unity.
    The game knows which one to use by the prefix of the variable name: t for temporary variables and g for permanent/global variables.
    Conditions
    Variables tie into conditions, which are strings representing logical expressions, such as (tGraveLight1=1)&(tHasArrow=1).

    These conditions, which can use conjunction, disjunction and negation, are evaluated using recursive regex pattern matching until one reaches the bottom level: the comparison of an ingame variable to an integer value, like tGraveLight=1 or gLoopCount>10, which maps to a boolean value.
    Interactions
    Actions and conditions together create interactions.
    An interaction contains unconditional actions, conditional actions and default actions.
    Unconditional actions are an array of actions that are always all executed whenever the interaction happens, before any of the other actions.
    Then, the game goes over the array of conditional actions, each consisting of a condition and an array of associated actions. If one of the conditions is found to be true, all associated actions are executed, and then the interaction ends - later conditions aren't checked.
    If none of the conditions are true, it executes the entire array of  default actions.
    Interactions can also be enabled or disabled, both through a default setting and through actions.

    Interactable objects have an interaction for each thing the player can do with them, being looking, touching and taking for environment objects.
    If one of these is disabled, the player cannot do it and the respective cursor does not light up upon hovering.
    Inventory objects can be looked at as well, but they can also be used on any other interactable object, so they have an array of interactions with an associated objects, as well as a default interaction for when the player uses it on an object for which nothing else has been defined (in this case, whether it is enabled is irrelevant.)
    There are also time-based interactions that happen automatically at a certain ingame time and custom interactions that can be called from anywhere, for example by UI buttons or other interactions.

    Originally, interactions could only be written within the Unity inspector, but this could cause problems when there were serialization issues and could be quite inefficient when interactions need to be transferred to other objects or sets of actions needed to be copypasted.
    As a solution, I wrote a MonoBehaviour that would, depending on its current setting, upon entering play mode or reloading the scene, either do nothing, export all interactions into text files or import all interactions from such text files into the current scene, based on the name of the object they belong to.
    This allowed for easy copypasting, backups and replacing of assets - the new object just needed to have the same name as its predecessor, and all interactions were automatically transferred. It also helped with version control, allowing for easier merging.
    Additionally, team members could now choose whether they wanted to write interactions in the inspector or in text files.
    Share by: