Project Description

Dream Walker

TEAM
Deliverone FutureGames Game Project 1
Category

Team Projects

Year

2021

Context

FutureGames Game Project 2

Tools

Unity, C#

My Responsibilities

Pickup Interaction System, Input System, Settings Manager, Sfx Integration

Team Members

Artists
Alishia Nossborn
Gustav Nilsson
Simon Eliasson

Programmers
Oliver Lebert
Justus Hörberg
Sebastian Åkerlund

Designers
Simon Olivecrona
Jesper Westlund
Samuel Einheri
Hanna Högdin

Music
Jesper Westlund
Adam Wiger

Project Breakdown

Introduction

DreamWalker takes place within a nightmare. The player gets to explore within this dreamworld. Your aim as a DreamWalker is to prevent a girl from being trapped in her nightmare. Traverse different areas of her nightmare, solve puzzles, and manipulate your environment in this third-person point and click adventure. Do whatever it takes to fulfill your duty.

This was my second game project at FutureGames which I made along with 9 other super talented teammates. The project was four weeks long and it was super fun to make. Since I had worked on some tools before this project, I was familiar with custom inspectors which proved to be super useful under the project, more on that below.

Compared to my first FutureGames project I am actually impressed with myself over the amount I have learned in just a few months. My code is so much nicer and easier to read and I was able to make many advanced systems that are modular and easily scalable should, for instance, a new puzzle want to be added.

The Code

The code in this project is a lot more complicated than in my last project. The interaction system is easily the biggest part of the game since the game is so focused on puzzles. I made the foundations of it and most interactables that are used in the puzzles. My programming colleague Justus worked primarily on the movement/camera and Sebastian was a bit of a generalist. Notably, Sebastian made the outline shader when you look at interactable objects.

Going back to my main responsibility, the interaction system, let’s take a look at the class diagram, showing the relevant classes to the system in color and their immediate dependencies in gray:

DreamWalker Class Diagram

The Interaction System

The interaction system starts with interactables which are classes that derive from a main abstract Interactable class. They recieve both key down and key up events from the bound interact key or mouse button. Based on that input it will perform any logic specified in that derived class. It can also, in some cases trigger an output which is just a delegate that any external system(s) can subscribe to, to perform additional custom logic when that delegate gets called.

The InteractableRaycaster MonoBehaviour is where everything starts, its purpose is to shoot a ray (every frame) from the camera (in the camera’s forward direction) and handle any interactables that are hit. If an interactable is hit, the method “IsCurrentlyInteractable” (in the Interactable class) gets called which returns a boolean. This allows each type of interactable to have its own implementation for when it should be interactable and not. For example, this allows for a maximum range between the interactable and the player to be specified, which can be different for each instance of interactable, instead of being completely dependent on just the raycast range for more fine-tuning.

If the interactable is currently interactable it will be outlined and cached as the current interactable in range. The crosshair will also turn blue to indicate you are currently looking at an interactable object. When looking away from the interactable, the cached interactable will be set to null, the crosshair will revert and the outline will be reset.

When pressing or releasing the interact key the Interact method in the raycaster will be called. The reason for passing both down/up states into the Interact method is that some interactables may require this behavior. For instance, when implementing a button, it should turn on when key is pressed and off when released. While switches should only toggle the output on key down. To keep the code clean, I implemented a class attribute that can be added to Interactables. The attribute specifies which key states the interactables should be triggered on. The implementation looks like this:

// Global scope
[AttributeUsage(AttributeTargets.Class)]
public class InteractOn : Attribute
{
	private Interaction[] modes;

	public InteractOn(Interaction mode)
	{
		modes = new[] {mode};
	}

	public InteractOn(Interaction mode1, Interaction mode2)
	{
		modes = new[] {mode1, mode2};
	}

	public Interaction[] GetModes()
	{
		return modes;
	}
}

public enum Interaction
{
	KeyDown,
	KeyUp
}

// In InteractableToggle.cs
[InteractOn(Interaction.KeyDown)]
public class InteractableToggle : Interactable
{
	public bool poweredAtStart = false;

	protected override void Awake()
	{
		base.Awake();
			
		if(poweredAtStart)
			base.Interact();
	}
}

// In InteractableRaycaster.cs
public void Interact(Interaction currentInteraction)
{
	if (currentInteractableInRange == null)
		return;

	Attribute[] attributes = Attribute.GetCustomAttributes(currentInteractableInRange.GetType());

	foreach (Attribute attribute in attributes)
	{
		if (attribute is InteractOn response)
		{
			foreach (Interaction targetInteractionMode in response.GetModes())
			{
				if (targetInteractionMode == currentInteraction)
					currentInteractableInRange.Interact();
			}
			return;
		}
	}
	currentInteractableInRange.Interact();
}

This makes the code a lot cleaner, the InteractableToggle class is pretty much empty, instead of having a bool check to only toggle the output every other input (the solution I had before implementing the attribute).

There are a fair few implementations of Interactables. Here is a list of some of them:

  • InteractableToggle, used most often in DreamWalker, toggles on/off at key down.
  • InteractableMovable, used in the statue and pyramid puzzles. Specifies a starting state of an object, an array of possible states, and an index of a correct state. If the first state in the array is occupied by another movable, the current movable will try to move to the next, etc. If the movable is in the correct state it will give an output.
  • InteractableMoveToPlatform, the platforms that when clicked, the player will move to them. There are options to specify the arc in which the player travels (using bezier curves), as well as the speed using an animation curve.
  • InteractableTrigger, an interactable that cannot be clicked but will only trigger the output when the player walks into its trigger collider.
  • MovingPlatforms, the moving platforms just before the pyramid puzzle (made by Sebastian).

Some of these interactables make up the framework for the inputs in the puzzles, while others are more standalone (such as the MoveToPlatform). Now there’s only one key thing missing, what these inputs trigger. For puzzles, the PuzzleHandler lays the foundations of that. It’s quite simple, it takes an array of Interactables as input, and when they are all outputting true, the output will be triggered.

The output is quite hard to make correctly because it has to do a lot of things, for example, when completing the statue puzzle, a cutscene has to be triggered, a sound must play, the platforms you used to traverse to the island have to be disabled, the bridge to lead to the next part of the game must become interactable, etc. Other puzzles have completely other outputs that must be triggered. I realized that there were two ways to solve this, either I make a script for every puzzle that will do the things mentioned, which would get very messy very quickly. Or I make a generic system where all events can be customized completely in the inspector. This would mean that I wouldn’t have to do any additional coding, I can just give all the power to the designers to customize exactly what will happen when either a puzzle is completed or an interactable is triggered. I of course made the latter which I named the InteractableEventSystem.

This is how the inspector looks:
InteractableEventSystem

You attach either a puzzle handler, an interactable, or a UI button to the field, the event system will subscribe to a delegate in the relevant object that will be triggered when the interactable or puzzle handler’s output switches to true. The event system can also be customized to trigger as soon as it is enabled. The events will be executed from top to down (as configured in the inspector), you can add a delay to events and some events will wait until they have finished executing before running the next event. For example, when showing subtitles (makes it easy to make a subtitle sequence without having to think about delay etc) and waiting for the player to close the diary (in the beginning), etc.

Because of the complexity of the script and the number of things you can do with it, it had to have a custom inspector to make sure everything has the correct layout and that only relevant controls (based on the event type) are shown at one time. Luckily, I had worked a lot on a tool just before the project so I had even made an editor library that I could just import into the project. This enabled me to get the event system up and running in just a single day because I was still in the tool-making mindset.

The tool was really fun to make, and it was a joy to see how much it was used. There are over a hundred instances of the event system across all scenes in the game (including test scenes). I can just imagine how much time would’ve been lost during the production for both programmers and designers, if all of that would’ve been hardcoded.