Eden Jeffrey

2024 / Yarn Theory

Description

“Space for Two is a 2 player Co-Op adventure game featuring Bubble the fish and Squeak the cat. Explore and scavenge alongside fast-paced mini-games, working together to find a new habitable home.”Space For Two

Space for Two was the winner of a “Finalist” award in the Rookies Awards 2022.

Additionally, Space For Two was featured prominently in the Games Academy EXPO 2024, winning the staff award “Holistic Harmony”, for the completeness of the Audio, Art and more.

Project Breakdown

Roles

[detailed description of roles]

Overview

Audio is essential for conveying the personalities of characters and environments in Space for Two. Designing audio to suit all elements of the game both tonally and aesthetically was a challenge. Through the consistent use of animals, toys, small mechanisms and tonal layers, the soundscape of Space for Two is satisfyingly cohesive throughout its sound effects, UI, and music.

Project Management

Spreadsheets! As far as I’m concerned, spreadsheets are mandatory for managing and tracking the many audio events and assets required for game audio development. Starting with a template provided by u/8ude, I adapted the below spreadsheet layout for listing and tracking the design / implementation status of all Wwise events required, as well as the audio assets needed for each event.

Jira and Confluence are powerful tools for Agile development, something I tried to be consistent and diligent with through the production of Space For Two. The workflow I personally found best was to use Google Sheets to store my event spreadsheet, with a linking document in Confluence. Any other documentation, I would create Confluences pages for so that all team members could easily reference and refer to if needed.

Jira tasks and stories were created accordingly, with some referencing multiple events from the asset spreadsheet, if the feature in development required them. This system allowed my own personal progress tracking to be as closely linked to the team management workflow as possible.

Technical Breakdown

I’ve linked the technical reel / breakdown of some of the features I’ve found most interesting to develop for Space For Two. These features range from linear sound design, to audio management systems in C++. Please watch the below video to find out more.

A slightly more detailed breakdown of the above video can be read below if preferred.

Ship Engine

To design the ship’s engine, I opted for the subtractive synthesiser “Vital”. Synthesis was suitable for the tonal qualities I wanted, as well as allowing for real-time modulation and iteration. In Wwise, events were created for playing and stopping both the synthesised engine, and thruster layers. Play and stop events incorporated both volume and pitch modulation.

This created smooth and believable transitions from ignition to shut down. Both engine and thruster implement “Load” and “RPM” RTPC’s, conveying speed and acceleration to the player. Blend containers were used to transition between engine RPM loops, as well as between on-load / off-load blend containers. Each RPM loop is pitch automated at its relative position in the RPM RTPC, creating a smooth blend between different RPM’s.

//Calculates a normalized RPM value, sets the rpm RPTC for the ship engine
void UShipAudioMethods::UpdateRpm(float Speed, float MaxSpeed, float Interp)
{
	//Convert speed of ship into RTPC scaled value, update RPM RTPC
	float NormalizedSpeed = Speed / MaxSpeed;
	float SpeedRTPCValue = (EngineLoad > 0.001) ? TransformLinearToSqrt(NormalizedSpeed, 0, 1, 0, RPMRTPCMaxValue, RPMCurve) : TransformLinearToSqrt(NormalizedSpeed, 0, 1, 0, RPMRTPCMaxValue, 0);
	//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Orange, FString::Printf(TEXT("SpeedRTPCValue: %f"), SpeedRTPCValue));
	UBPFL_AudioStatics::UpdateRTPC(EngineAkComponent, EngineRPMRTPC, SpeedRTPCValue, Interp);

}

//Sets the load RTPC for the ship engine, returns a new load value to be stored
void UShipAudioMethods::UpdateLoad(float Acceleration, float Interp)
{
	float NewLoad = FMath::Clamp(Acceleration, 0.001, 1);
	EngineLoad = NewLoad;
	//xGEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, FString::Printf(TEXT("Load: %f"), NewLoad));
	UBPFL_AudioStatics::UpdateRTPC(EngineAkComponent, EngineLoadRTPC, NewLoad, Interp);
	UBPFL_AudioStatics::UpdateRTPC(ThrusterAkComponent, ThrusterRTPC, NewLoad, Interp);
}

//!Function transforms Y value of X to its sqrt eqivalent.
// -------------------------------------------------------
///X = NormalizedSpeed
///X_min / Y_min = Minimum acceleration (almost always 0)
///x_max = Maximum normalized speed value to be interoplated (1 for full interp, if less than 1 the remainder will be linearly scaled)
///Y_max = Maximum RPM value
///Curvature = Intensity of sqrt curve (clamped 0-2)
//--------------------------------------------------------
double UShipAudioMethods::TransformLinearToSqrt(double X, double X_Min, double X_Max, double Y_Min, double Y_Max, double Curvature)
{
	//!Variables
	//Clamps curvature to reasonable values
	double M = FMath::Clamp(Curvature, 0, 2);
	//Clamps x_max / x_min to a 0-1 (range of normalized speed)
	X = FMath::Clamp(X, 0.01, 1);
	X_Max = FMath::Clamp(X_Max, 0.1, 1);
	X_Min = FMath::Clamp(X_Min, 0, 1);
	//ensures y_min is a positive value
	Y_Min = abs(Y_Min);

	//!Transform fucntion
	// Normalize x to the range [0, 1]
	double X_Norm = (X - X_Min) / (X_Max - X_Min);
	// Calculate the corresponding y value on the curve
	double Y = (Y_Max - Y_Min) * (M * sqrt(X_Norm) + (1 - M) * X_Norm) + Y_Min;
	return Y;
}

Variables and functions for all ship audio were created on a custom actor component “ShipAudioMethods”, allowing common behaviour to be shared between both types of ship in the game. Game parameter updates are handled in C++, with linear speed values from the ship transformed to a definable curve.

Trailer Cutscene

I took a linear approach to designing audio for this cinematic, incorporating both Foley performance and hand splicing. Selecting audio and synchronising the clothing and fabric movements to each character’s movements was something I ended up doing anyway to add embellishment; but the primary layers for the clothing Foley were several recorded performances of myself mimicking the animation with my coat. This process is not only fun, but in many cases far more time efficient, especially for longer performances.

Throughout Space for Two, I consistently selected satisfying tonal layers, along with small mechanisms and toys. This resulted in a distinctive and cohesive sound, shared between all aspects of the game. One of the pillars set out by the art team was to have a “crunchable”, visually edible art style. I tried to reflect this aesthetic in as many aspects of the sound design as possible. Through the use of bubbles, squeaks, pops, sloshes and crunches, the auditory identity of the game quickly became distinct.

For all the mechanical and sci-fi elements, I opted to select small mechanisms that as closely reflected the motions and movements of what was being displayed as possible. For example, rotations and continuous movements were often based on clockwork or winding toys and mechs. Clunks and impacts were created from things like car door locks and Nerf guns. Synthesised tones and layers were often created to replicate cats and animals, with further sounds based off of previous ones.

This methodology was used throughout Space For Two, and created a harmonious audio style, through this creative restriction of sample choice.

Ship Airlock Doors

For the airlock doors, I wanted to be able to have animation synced audio, without compromising on the design detail. Due to constraints of both the door movement mechanic, and Wwise itself, a creative solution was required. FMOD’s “Start Offset” parameter allows runtime modulation, enabling events to be shorted according to game values. Wwise on the other hand does not support this.

If anyone knows why this is not a thing in Wwise please let me know

My solution was to export multiple incrementally cropped versions of each sound, allowing them to be switched between. This switch was then bound to an RTPC, acting as a manual “Start Offset”. Although not the most performant, this solution was fast, effective, and still low cost relative to the scope of the project.

Stop Active Events System

Space for Two’s loading is done asynchronously, presenting an issue for destroying looping sound Ak Components. To tackle this, I created a system on the Audio Manager for tracking and destroying looping events during level transitions. When a looping event is posted, it is registered to an Array on the Audio Manager, using a custom Struct. When a transition is called, all events registered to the audio manager are stopped with a one-second fade out.

Relevant excerpts from the AudioManager class can be seen below:

AudioManager.h

UPROPERTY(BlueprintReadWrite, Category = "Event Management")
TArray<FEventID> ActiveEvents;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Event Management")
UAkAudioEvent* StopAllEventsOnComponentEvent;

UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "Audio|Custom")
void AddEventToActiveEvents(const FEventID EventID);

UFUNCTION(BlueprintCallable, BlueprintCosmetic, Category = "Audio|Custom")
void StopAllActiveEvents();

AudioManager.cpp

void AAudioManager::AddEventToActiveEvents(const FEventID EventID)
{
	ActiveEvents.Add(EventID);
}

void AAudioManager::StopAllActiveEvents()
{
	for (const auto& EventID : ActiveEvents)
	{
		if (IsValid(EventID.Event) && EventID.PlayingID != NULL)
		{
			if (UBPFL_AudioStatics::IsPlayingIDActive(EventID.Event->GetShortID(), EventID.PlayingID))
			{
				if (IsValid(EventID.Component))
				{
					EventID.Component->PostAkEvent(StopAllEventsOnComponentEvent);
				}
			}
		}
	}
	ActiveEvents.Empty();
}