AND THEN JACK WOKE UP

ROLES

Game Designer & Product Owner

 

YEAR

2020

 

TIME

7 Weeks

 

TEAM SIZE

9 People (3 Designers)

 

ENGINE

Unity 2019

CONCEPT

And Then Jack Woke Up is a narrative driven game that tells the story of Jack and how he starts to realize that his life might not be what it seems.


Our team worked to showcase our ability to portray emotions and an understanding of narrative driven gameplay.


CONTRIBUTION

PROJECT MANAGEMENT

Product Owner, Team Management and Communication


GAME DESIGN

Game Loop Design, Game Mechanics Design, Narrative Design & Level Design


C# SCRIPTING

Game System Implementation & UI Scripting


SOUND DESIGN

Sound Mixing & Gameplay Audio Scripting


GAME DESIGN

NARRATIVE DESIGN

During this project I worked as the lead narrative designer.


My goal when writing the narrative for this game was to tell a story that gets the player thinking about consciousness and game logic. I did this by creating a recognizable setting that would slowly change and become a bit meta.


I wanted the environment to tell some of the story, so I worked closely with both the other designers and the artists to weave the narrative into the level design and atmosphere of the game. As the story progresses the atmosphere becomes more glitchy and the level design starts to make less sense.


The two main narrative devices that I used were post-it notes with odd messages and dialogue through in-game objects.


An early draft of the story had a narrator, much like in The Stanley Parable, that would start talking to the main character. However, I realized that that didn’t feel right, because I wanted the player to feel unsure whether there actually was an outside world or not.


GAMEPLAY LOOP

To get a better sense of the narrative and gameplay as a whole I mapped out the entire gameplay loop.


I did this by creating a flowchart that took into consideration the narrative, the level design and the players' actions. This helped all the team members get a feeling for how the narrative worked together with the gameplay and what to look for when playtesting.


(Click the image for full size)

LEVEL DESIGN

I worked with the other designers to create the level design for our game. I mapped out the level layouts and playtested the flow of them.


It was important to me that the levels had a good flow but still felt like realistic spaces.


The game has two different levels - an apartment and an office. The player then loops through three different stages of these levels.


We wanted the player to become increasingly suspicious towards their environment as the game progressed so we knew we had to establish a normal and familiar setting at the beginning.

The player starts in a warm lit studio apartment that has a recognizable and nostalgic feel to it. Around the apartment are personal items and photos and the player can interact with some generic household items.
The player continues by walking out of the apartment towards the elevator and this is when the first sign of glitching behavior happens.

The elevator doors open to show the apartment again. Everything is the same as before, but this time there is a briefcase that can be picked up. After doing so, the player can continue into the elevator and ride it.

The first elevator experience works as an introduction to the looping of the game.
A visual glitch occurs and for some odd reason the doors open to show that the player is now at an office building later the same day.

We wanted to question the strange logic in games where players will load into areas or scenarios without context, but this time make the player character aware of this.
As the player arrives in the office space, we wanted to make it obvious which workstation belongs to the player character by framing it when they first enter.

We did this with lighting and by decorating it more than the other desks.
After interacting with the computer, the player gets prompted to use the coffee machine, which is the second glitch behavior that transitions the player home.
It is the same apartment as earlier, but later in the evening. The player gets the option to go to bed, and the next day (or loop) begins.
We wanted the player to become increasingly suspicious towards their environment as the game progressed so we knew we had to establish a normal and familiar setting at the beginning.

The player starts in a warm lit studio apartment that has a recognizable and nostalgic feel to it. Around the apartment are personal items and photos and the player can interact with some generic household items.
The player continues by walking out of the apartment towards the elevator and this is when the first sign of glitching behavior happens.

The elevator doors open to show the apartment again. Everything is the same as before, but this time there is a briefcase that can be picked up. After doing so, the player can continue into the elevator and ride it.

The first elevator experience works as an introduction to the looping of the game.
A visual glitch occurs and for some odd reason the doors open to show that the player is now at an office building later the same day.

We wanted to question the strange logic in games where players will load into areas or scenarios without context, but this time make the player character aware of this.
As the player arrives in the office space, we wanted to make it obvious which workstation belongs to the player character by framing it when they first enter.

We did this with lighting and by decorating it more than the other desks.
After interacting with the computer, the player gets prompted to use the coffee machine, which is the second glitch behavior that transitions the player home.
It is the same apartment as earlier, but later in the evening. The player gets the option to go to bed, and the next day (or loop) begins.
The second time the player wakes up, the apartment looks pretty much the same as the first time. However, this time the answering machine is behaving strangely. When the player interacts with it, they hear the front door opening. When they leave to go towards the elevator a new door has opened in the hallway.

It was important that the player instantly felt that something was off when they started this day.
When the player continues to the office for the second time, more narrative clues are visible around the lobby area.

The post-it notes works as breadcrumbs leading the player to areas of interest.
This time the office space is a lot different from the first time the player saw it. Post-it notes can be found everywhere and the NPCs are behaving strangely.

When the player interacts with the coffee machine a sound can be heard coming from the office space. When reentering the office space, the player will notice that the computer is back, and it triggers a bit of dialogue. After interacting with the computer, a visual glitch occurs, and the office is now empty.
The office now has one new room available. We wanted the feeling of being in a game to become obvious at this point, so we included some concept art and our actual SCRUM board.

The player will be drawn towards the exit of the office by the sound of the elevator. The doors to the office has been replaced by the elevator doors. After walking inside the player transitions to the last stage of the game.
The second time the player wakes up, the apartment looks pretty much the same as the first time. However, this time the answering machine is behaving strangely. When the player interacts with it, they hear the front door opening. When they leave to go towards the elevator a new door has opened in the hallway.

It was important that the player instantly felt that something was off when they started this day.
When the player continues to the office for the second time, more narrative clues are visible around the lobby area.

The post-it notes works as breadcrumbs leading the player to areas of interest.
This time the office space is a lot different from the first time the player saw it. Post-it notes can be found everywhere and the NPCs are behaving strangely.

When the player interacts with the coffee machine a sound can be heard coming from the office space. When reentering the office space, the player will notice that the computer is back, and it triggers a bit of dialogue. After interacting with the computer, a visual glitch occurs, and the office is now empty.
The office now has one new room available. We wanted the feeling of being in a game to become obvious at this point, so we included some concept art and our actual SCRUM board.

The player will be drawn towards the exit of the office by the sound of the elevator. The doors to the office has been replaced by the elevator doors. After walking inside the player transitions to the last stage of the game.
This time, when arriving at the apartment, everything is drastically different. To make it even more clear that the player is inside a game world, we changed it to look like the blockout of the level. After leaving the elevator, the player is instructed to find the bed, which is missing.
The same door that was open the day before now reveals another answering machine with a message. As the player walk towards the end of the hallway, we wanted to guide them to a new area. We did this by directing the lighting and swapping the direction of signs. We wanted the intense feeling of the narrative to translate visually for this part of the game, so we added more dramatic lighting and visual distortion.

Instead of going left to the elevator, the player is led right to a door.
The door leads to another, emptier, version of the player characters apartment. The player can read some narrative documents on a laptop and is then presented with a choice.

They will have to either interact with the bed, like the narrative is guiding them to do, or with a door. The ending differs depending on this choice.
This time, when arriving at the apartment, everything is drastically different. To make it even more clear that the player is inside a game world, we changed it to look like the blockout of the level. After leaving the elevator, the player is instructed to find the bed, which is missing.
The same door that was open the day before now reveals another answering machine with a message. As the player walk towards the end of the hallway, we wanted to guide them to a new area. We did this by directing the lighting and swapping the direction of signs. We wanted the intense feeling of the narrative to translate visually for this part of the game, so we added more dramatic lighting and visual distortion.

Instead of going left to the elevator, the player is led right to a door.
The door leads to another, emptier, version of the player characters apartment. The player can read some narrative documents on a laptop and is then presented with a choice.

They will have to either interact with the bed, like the narrative is guiding them to do, or with a door. The ending differs depending on this choice.

SCRIPTING

ALARM CLOCK

I wanted the alarm clock to automatically play an alarm and then start the radio when the alarm was shut off.


I did the functionality and mixed and implemented the audio.


using System.Collections;
using UnityEngine;

[RequireComponent(typeof(AudioSource))]
public class AlarmClock : MonoBehaviour, IInteractable
{
    [SerializeField] private AudioSource audioSource = default;
    [SerializeField] private AudioSource buttonAudioSource = default;
    [SerializeField] private AudioClip alarmClip = default;
    [SerializeField] private AudioClip radioClip = default;

    [SerializeField] private string alarmInteractionText = "Stop alarm";
    [SerializeField] private string offInteractionText = "Turn off radio";
    [SerializeField] private string onInteractionText = "Turn on radio";

    [SerializeField] private bool canInteract = true;
    [SerializeField] private bool alarmIsOn = false;
    [SerializeField] private bool radioIsOn = false;

    [SerializeField] private float volumeFadeTime = 3.0f;
    
    private float fullVolume;
    private float noVolume = 0.0f;
    private float buttonTime;

    private void Start()
    {
        buttonTime = buttonAudioSource.clip.length;
        audioSource = GetComponent();
        fullVolume = audioSource.volume;
        audioSource.loop = true;

        if (alarmIsOn)
        {
            StartCoroutine(PlayAlarm());
        }
        else
        {
            audioSource.clip = radioClip;
        }
    }

    public GameObject GetGameObject()
    {
        return gameObject;
    }

    public string GetInteractText()
    {
        if (alarmIsOn)
        {
            return alarmInteractionText;
        }
        else
        {
            if (radioIsOn)
            {
                return offInteractionText;
            }
            else
            {
                return onInteractionText;
            }
        }
    }

    public void OnInteract(GameObject interactor)
    {
        buttonAudioSource.Play();

        if (alarmIsOn)
        {
            alarmIsOn = false;
            audioSource.Stop();
            audioSource.clip = radioClip;
            StartCoroutine(PlayRadio());
        }
        else
        {
            if (radioIsOn)
            {
                StartCoroutine(StopRadio());
            }
            else
            {
                StartCoroutine(PlayRadio());
            }
        }
    }

    private IEnumerator PlayAlarm()
    {
        audioSource.volume = noVolume;
        audioSource.clip = alarmClip;
        audioSource.Play();

        while (audioSource.volume < fullVolume)
        {
            audioSource.volume += Time.deltaTime / volumeFadeTime;

            yield return null;

            if (audioSource.volume > fullVolume)
            {
                audioSource.volume = fullVolume;
            }
        }
        audioSource.volume = fullVolume;  
    }

    private IEnumerator PlayRadio()
    {
        yield return new WaitForSeconds(buttonTime);

        audioSource.mute = false;

        if (!audioSource.isPlaying)
        {
            audioSource.Play();
        }
        radioIsOn = true;
    }

    private IEnumerator StopRadio()
    {
        yield return new WaitForSeconds(buttonTime);

        audioSource.mute = true;
        radioIsOn = false;
    }

    public bool CanInteract()
    {
        return canInteract;
    }

    public void SetCanInteract(bool canInteract)
    {
        this.canInteract = canInteract;
    }
}
using System.Collections;
using UnityEngine;

[RequireComponent(typeof(AudioSource))]
public class AlarmClock : MonoBehaviour, IInteractable
{
    [SerializeField] private AudioSource audioSource = default;
    [SerializeField] private AudioSource buttonAudioSource = default;
    [SerializeField] private AudioClip alarmClip = default;
    [SerializeField] private AudioClip radioClip = default;

    [SerializeField] private string alarmInteractionText = "Stop alarm";
    [SerializeField] private string offInteractionText = "Turn off radio";
    [SerializeField] private string onInteractionText = "Turn on radio";

    [SerializeField] private bool canInteract = true;
    [SerializeField] private bool alarmIsOn = false;
    [SerializeField] private bool radioIsOn = false;

    [SerializeField] private float volumeFadeTime = 3.0f;
    
    private float fullVolume;
    private float noVolume = 0.0f;
    private float buttonTime;

    private void Start()
    {
        buttonTime = buttonAudioSource.clip.length;
        audioSource = GetComponent();
        fullVolume = audioSource.volume;
        audioSource.loop = true;

        if (alarmIsOn)
        {
            StartCoroutine(PlayAlarm());
        }
        else
        {
            audioSource.clip = radioClip;
        }
    }

    public GameObject GetGameObject()
    {
        return gameObject;
    }

    public string GetInteractText()
    {
        if (alarmIsOn)
        {
            return alarmInteractionText;
        }
        else
        {
            if (radioIsOn)
            {
                return offInteractionText;
            }
            else
            {
                return onInteractionText;
            }
        }
    }

    public void OnInteract(GameObject interactor)
    {
        buttonAudioSource.Play();

        if (alarmIsOn)
        {
            alarmIsOn = false;
            audioSource.Stop();
            audioSource.clip = radioClip;
            StartCoroutine(PlayRadio());
        }
        else
        {
            if (radioIsOn)
            {
                StartCoroutine(StopRadio());
            }
            else
            {
                StartCoroutine(PlayRadio());
            }
        }
    }

    private IEnumerator PlayAlarm()
    {
        audioSource.volume = noVolume;
        audioSource.clip = alarmClip;
        audioSource.Play();

        while (audioSource.volume < fullVolume)
        {
            audioSource.volume += Time.deltaTime / volumeFadeTime;

            yield return null;

            if (audioSource.volume > fullVolume)
            {
                audioSource.volume = fullVolume;
            }
        }
        audioSource.volume = fullVolume;  
    }

    private IEnumerator PlayRadio()
    {
        yield return new WaitForSeconds(buttonTime);

        audioSource.mute = false;

        if (!audioSource.isPlaying)
        {
            audioSource.Play();
        }
        radioIsOn = true;
    }

    private IEnumerator StopRadio()
    {
        yield return new WaitForSeconds(buttonTime);

        audioSource.mute = true;
        radioIsOn = false;
    }

    public bool CanInteract()
    {
        return canInteract;
    }

    public void SetCanInteract(bool canInteract)
    {
        this.canInteract = canInteract;
    }
}

MAIN MENU

I implemented the functionality for the main menu UI. Because the narrative is a bit meta, I really wanted to use aspects of the narrative for the menu.


One aspect that I'm very happy about is the animation when starting the game.


private void Start()
    {
        StartCoroutine(FadeIn());
    }

    private void Update()
    {
        if (hasStartedGame && player.PlayerMovement != null)
        {
            if (Input.GetKeyDown(KeyCode.Escape) && player.PlayerMovement.IsEnabled)
            {
                Debug.Log("pausing game");
                PauseGame();
            }
        }
    }

    public void BeginGame()
    {
        if (canOpen)
        {
            StartCoroutine(OpenDoor());
            hasStartedGame = true;
        }
    }

    public void ClickUIButton()
    {
        UISource.clip = UIClickClips[Random.Range(0, UIClickClips.Length)];
        UISource.Play();
    }

    IEnumerator FadeIn()
    {
        float fadeTime = 2.0f;
        float transparent = 0.0f;
        float opaque = 1.0f;

        for (float t = 0.0f; t < fadeTime; t += Time.deltaTime / fadeTime)
        {
            Color newColor = new Color(0, 0, 0, Mathf.Lerp(opaque, transparent, t));
            blackScreen.color = newColor;
            yield return null;
        }
    }

    IEnumerator FadeOutMusic()
    {
        MenuMusicSource.volume = 0.0f;
        float volumeFadeTime = 2.0f;
        float noVolume = 0.0f;
        float fullVolume = 0.1f;

        for (float t = 0.0f; t < volumeFadeTime; t+= Time.deltaTime / volumeFadeTime)
        {
            MenuMusicSource.volume = Mathf.Lerp(fullVolume, noVolume, t);
            yield return null;
        }
        MenuMusicSource.volume = 0.0f;
    }

    IEnumerator FadeInMusic()
    {
        MenuMusicSource.volume = 0.0f;
        float volumeFadeTime = 2.0f;
        float noVolume = 0.0f;
        float fullVolume = 0.1f;

        for (float t = 0.0f; t < volumeFadeTime; t += Time.deltaTime / volumeFadeTime)
        {
            MenuMusicSource.volume = Mathf.Lerp(noVolume, fullVolume, t);
            yield return null;
        }
        MenuMusicSource.volume = fullVolume;
    }

    IEnumerator OpenDoor()
    {
        door.SetBool("Open", true);
        handleSource.Play();
        doorAudioSource.PlayOneShot(doorOpen);
        mainMenuCamera.gameObject.SetActive(false);

        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;

        yield return new WaitForSeconds(0.3f);

        StartCoroutine(FadeOutMusic());
        cameraAnim.SetBool("InGame", true);
        blackScreenObject.GetComponentInChildren().raycastTarget = true;

        yield return new WaitForSeconds(1f);

        float fadeTime = 1.0f;
        float transparent = 0.0f;
        float opaque = 1.0f;

        for (float t = 0.0f; t < fadeTime; t += Time.deltaTime / fadeTime)
        {
            Color newColor = new Color(0, 0, 0, Mathf.Lerp(transparent, opaque, t));
            blackScreen.color = newColor;
            yield return null;
        }

        yield return new WaitForSeconds(2.0f);

        WorldSystem.LoadLevel(1, true);
        WorldSystem.LoadLevel(2, true);
        EnterDoor();
    }

    IEnumerator SetBlackScreenInactive()
    {
        yield return new WaitForSeconds(0.5f);
        blackScreenObject.SetActive(false);
    }

    private void EnterDoor()
    {
        menuCamera.GetComponent().enabled = false;
        menuCamera.GetComponent().enabled = false;
        playerUI.gameObject.SetActive(true);
        wakeUpCameraObj.SetActive(true);
        playerCamera.gameObject.SetActive(true);
        StartCoroutine(SetBlackScreenInactive());

        startButton.SetActive(false);
        continueButton.SetActive(true);
        foreach (GameObject item in canvasObjects)
        {
            item.SetActive(false);
        }
        canOpen = false;
    }

    public void UnPauseGame()
    {
        if (canOpen)
        {
            StartCoroutine(UnPause());
        }
    }

    IEnumerator UnPause()
    {
        door.SetBool("Open", true);
        handleSource.Play();
        doorAudioSource.PlayOneShot(doorOpen);

        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;

        yield return new WaitForSeconds(0.3f);

        StartCoroutine(FadeOutMusic());
        cameraAnim.SetBool("InGame", true);
        blackScreenObject.GetComponentInChildren().raycastTarget = true;

        yield return new WaitForSeconds(1f);

        float fadeTime = 1.0f;
        float transparent = 0.0f;
        float opaque = 1.0f;

        for (float t = 0.0f; t < fadeTime; t += Time.deltaTime / fadeTime)
        {
            Color newColor = new Color(0, 0, 0, Mathf.Lerp(transparent, opaque, t));
            blackScreen.color = newColor;
            yield return null;
        }

        menuCamera.GetComponent().enabled = false;
        menuCamera.GetComponent().enabled = false;
        playerUI.gameObject.SetActive(true);

        playerCamera.gameObject.GetComponent().enabled = true;
        playerCamera.enabled = true;

        StartCoroutine(SetBlackScreenInactive());
        imageAnim.SetBool("FromBlack", true);
        player.PlayerMovement.IsEnabled = true;
        player.AllowFootsteps = true;

        foreach (GameObject item in canvasObjects)
        {
            item.SetActive(false);
        }
        canOpen = false;

        yield return new WaitForSeconds(1f);
        imageAnim.SetBool("FromBlack", false);
    }

    public void PauseGame()
    {
        StartCoroutine(Pause());
    }

    IEnumerator Pause()
    {
        imageAnim.SetBool("ToBlack", true);
        player.PlayerMovement.IsEnabled = false;
        player.AllowFootsteps = false;

        yield return new WaitForSeconds(0.5f);
        playerCamera.gameObject.GetComponent().enabled = false;
        playerCamera.enabled = false;
        playerUI.gameObject.SetActive(false);

        menuCamera.GetComponent().enabled = true;
        menuCamera.GetComponent().enabled = true;

        blackScreenObject.SetActive(true);

        cameraAnim.SetBool("InGame", false);

        foreach (GameObject item in canvasObjects)
        {
            item.SetActive(true);
        }

        StartCoroutine(FadeInMusic());

        float fadeTime = 1.0f;
        float transparent = 0.0f;
        float opaque = 1.0f;

        for (float t = 0.0f; t < fadeTime; t += Time.deltaTime / fadeTime)
        {
            Color newColor = new Color(0, 0, 0, Mathf.Lerp(opaque, transparent, t));
            blackScreen.color = newColor;
            yield return null;
        }

        door.SetBool("Open", false);
        doorAudioSource.PlayOneShot(doorClose);

        yield return new WaitForSeconds(doorClose.length);

        blackScreenObject.GetComponentInChildren().raycastTarget = false;
        Cursor.lockState = CursorLockMode.Confined;
        Cursor.visible = true;

        canOpen = true;
    }
    
    public void ShowOptions()
    {
        cameraAnim.SetBool("InOptions", true);
    }

    public void HideOptions()
    {
        cameraAnim.SetBool("InOptions", false);
    }

    public void ShowCredits()
    {
        if (!elevatorDoorTransitioning)
        {
            StartCoroutine(OpenElevator());
        }
    }

    public void HideCredits()
    {
        if (!elevatorDoorTransitioning)
        {
            StartCoroutine(CloseElevator());
        }
    }

    IEnumerator OpenElevator()
    {
        elevator.SetBool("Open", true);
        elevatorAudioSource.PlayOneShot(elevatorDoorSound);
        elevatorDoorTransitioning = true;
        creditsReturnButton.interactable = false;
        cameraAnim.SetBool("InCredits", true);

        yield return new WaitForSeconds(3.5f);
        creditsReturnButton.interactable = true;
        elevatorDoorTransitioning = false;
    }

    IEnumerator CloseElevator()
    {
        elevatorDoorTransitioning = true;
        elevator.SetBool("Open", false);
        elevatorAudioSource.PlayOneShot(elevatorDoorSound);
        creditsReturnButton.interactable = false;

        yield return new WaitForSeconds(1.0f);
        cameraAnim.SetBool("InCredits", false);
        yield return new WaitForSeconds(0.5f);
        creditsReturnButton.interactable = true;
        elevatorDoorTransitioning = false;
    }
private void Start()
    {
        StartCoroutine(FadeIn());
    }

    private void Update()
    {
        if (hasStartedGame && player.PlayerMovement != null)
        {
            if (Input.GetKeyDown(KeyCode.Escape) && player.PlayerMovement.IsEnabled)
            {
                Debug.Log("pausing game");
                PauseGame();
            }
        }
    }

    public void BeginGame()
    {
        if (canOpen)
        {
            StartCoroutine(OpenDoor());
            hasStartedGame = true;
        }
    }

    public void ClickUIButton()
    {
        UISource.clip = UIClickClips[Random.Range(0, UIClickClips.Length)];
        UISource.Play();
    }

    IEnumerator FadeIn()
    {
        float fadeTime = 2.0f;
        float transparent = 0.0f;
        float opaque = 1.0f;

        for (float t = 0.0f; t < fadeTime; t += Time.deltaTime / fadeTime)
        {
            Color newColor = new Color(0, 0, 0, Mathf.Lerp(opaque, transparent, t));
            blackScreen.color = newColor;
            yield return null;
        }
    }

    IEnumerator FadeOutMusic()
    {
        MenuMusicSource.volume = 0.0f;
        float volumeFadeTime = 2.0f;
        float noVolume = 0.0f;
        float fullVolume = 0.1f;

        for (float t = 0.0f; t < volumeFadeTime; t+= Time.deltaTime / volumeFadeTime)
        {
            MenuMusicSource.volume = Mathf.Lerp(fullVolume, noVolume, t);
            yield return null;
        }
        MenuMusicSource.volume = 0.0f;
    }

    IEnumerator FadeInMusic()
    {
        MenuMusicSource.volume = 0.0f;
        float volumeFadeTime = 2.0f;
        float noVolume = 0.0f;
        float fullVolume = 0.1f;

        for (float t = 0.0f; t < volumeFadeTime; t += Time.deltaTime / volumeFadeTime)
        {
            MenuMusicSource.volume = Mathf.Lerp(noVolume, fullVolume, t);
            yield return null;
        }
        MenuMusicSource.volume = fullVolume;
    }

    IEnumerator OpenDoor()
    {
        door.SetBool("Open", true);
        handleSource.Play();
        doorAudioSource.PlayOneShot(doorOpen);
        mainMenuCamera.gameObject.SetActive(false);

        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;

        yield return new WaitForSeconds(0.3f);

        StartCoroutine(FadeOutMusic());
        cameraAnim.SetBool("InGame", true);
        blackScreenObject.GetComponentInChildren().raycastTarget = true;

        yield return new WaitForSeconds(1f);

        float fadeTime = 1.0f;
        float transparent = 0.0f;
        float opaque = 1.0f;

        for (float t = 0.0f; t < fadeTime; t += Time.deltaTime / fadeTime)
        {
            Color newColor = new Color(0, 0, 0, Mathf.Lerp(transparent, opaque, t));
            blackScreen.color = newColor;
            yield return null;
        }

        yield return new WaitForSeconds(2.0f);

        WorldSystem.LoadLevel(1, true);
        WorldSystem.LoadLevel(2, true);
        EnterDoor();
    }

    IEnumerator SetBlackScreenInactive()
    {
        yield return new WaitForSeconds(0.5f);
        blackScreenObject.SetActive(false);
    }

    private void EnterDoor()
    {
        menuCamera.GetComponent().enabled = false;
        menuCamera.GetComponent().enabled = false;
        playerUI.gameObject.SetActive(true);
        wakeUpCameraObj.SetActive(true);
        playerCamera.gameObject.SetActive(true);
        StartCoroutine(SetBlackScreenInactive());

        startButton.SetActive(false);
        continueButton.SetActive(true);
        foreach (GameObject item in canvasObjects)
        {
            item.SetActive(false);
        }
        canOpen = false;
    }

    public void UnPauseGame()
    {
        if (canOpen)
        {
            StartCoroutine(UnPause());
        }
    }

    IEnumerator UnPause()
    {
        door.SetBool("Open", true);
        handleSource.Play();
        doorAudioSource.PlayOneShot(doorOpen);

        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;

        yield return new WaitForSeconds(0.3f);

        StartCoroutine(FadeOutMusic());
        cameraAnim.SetBool("InGame", true);
        blackScreenObject.GetComponentInChildren().raycastTarget = true;

        yield return new WaitForSeconds(1f);

        float fadeTime = 1.0f;
        float transparent = 0.0f;
        float opaque = 1.0f;

        for (float t = 0.0f; t < fadeTime; t += Time.deltaTime / fadeTime)
        {
            Color newColor = new Color(0, 0, 0, Mathf.Lerp(transparent, opaque, t));
            blackScreen.color = newColor;
            yield return null;
        }

        menuCamera.GetComponent().enabled = false;
        menuCamera.GetComponent().enabled = false;
        playerUI.gameObject.SetActive(true);

        playerCamera.gameObject.GetComponent().enabled = true;
        playerCamera.enabled = true;

        StartCoroutine(SetBlackScreenInactive());
        imageAnim.SetBool("FromBlack", true);
        player.PlayerMovement.IsEnabled = true;
        player.AllowFootsteps = true;

        foreach (GameObject item in canvasObjects)
        {
            item.SetActive(false);
        }
        canOpen = false;

        yield return new WaitForSeconds(1f);
        imageAnim.SetBool("FromBlack", false);
    }

    public void PauseGame()
    {
        StartCoroutine(Pause());
    }

    IEnumerator Pause()
    {
        imageAnim.SetBool("ToBlack", true);
        player.PlayerMovement.IsEnabled = false;
        player.AllowFootsteps = false;

        yield return new WaitForSeconds(0.5f);
        playerCamera.gameObject.GetComponent().enabled = false;
        playerCamera.enabled = false;
        playerUI.gameObject.SetActive(false);

        menuCamera.GetComponent().enabled = true;
        menuCamera.GetComponent().enabled = true;

        blackScreenObject.SetActive(true);

        cameraAnim.SetBool("InGame", false);

        foreach (GameObject item in canvasObjects)
        {
            item.SetActive(true);
        }

        StartCoroutine(FadeInMusic());

        float fadeTime = 1.0f;
        float transparent = 0.0f;
        float opaque = 1.0f;

        for (float t = 0.0f; t < fadeTime; t += Time.deltaTime / fadeTime)
        {
            Color newColor = new Color(0, 0, 0, Mathf.Lerp(opaque, transparent, t));
            blackScreen.color = newColor;
            yield return null;
        }

        door.SetBool("Open", false);
        doorAudioSource.PlayOneShot(doorClose);

        yield return new WaitForSeconds(doorClose.length);

        blackScreenObject.GetComponentInChildren().raycastTarget = false;
        Cursor.lockState = CursorLockMode.Confined;
        Cursor.visible = true;

        canOpen = true;
    }
    
    public void ShowOptions()
    {
        cameraAnim.SetBool("InOptions", true);
    }

    public void HideOptions()
    {
        cameraAnim.SetBool("InOptions", false);
    }

    public void ShowCredits()
    {
        if (!elevatorDoorTransitioning)
        {
            StartCoroutine(OpenElevator());
        }
    }

    public void HideCredits()
    {
        if (!elevatorDoorTransitioning)
        {
            StartCoroutine(CloseElevator());
        }
    }

    IEnumerator OpenElevator()
    {
        elevator.SetBool("Open", true);
        elevatorAudioSource.PlayOneShot(elevatorDoorSound);
        elevatorDoorTransitioning = true;
        creditsReturnButton.interactable = false;
        cameraAnim.SetBool("InCredits", true);

        yield return new WaitForSeconds(3.5f);
        creditsReturnButton.interactable = true;
        elevatorDoorTransitioning = false;
    }

    IEnumerator CloseElevator()
    {
        elevatorDoorTransitioning = true;
        elevator.SetBool("Open", false);
        elevatorAudioSource.PlayOneShot(elevatorDoorSound);
        creditsReturnButton.interactable = false;

        yield return new WaitForSeconds(1.0f);
        cameraAnim.SetBool("InCredits", false);
        yield return new WaitForSeconds(0.5f);
        creditsReturnButton.interactable = true;
        elevatorDoorTransitioning = false;
    }

WALK SOUND

I believe that the best sound design is when you do not pay attention to the sounds themselves, yet everything feels cohesive and natural.

 

It was important to me that there was variation in the sounds so that they didn’t feel repetitive. To get this variation the footstep system supports four different ground surfaces that use random pitch and volume.


using System.Collections;
using UnityEngine;

[RequireComponent(typeof(AudioSource))]
public class WalkSound : MonoBehaviour
{
    private enum GroundType
    {
        Wood,
        Tiles,
        Carpet,
        Stone,
    }

    [SerializeField] private AudioClip[] stepWood = default;
    [SerializeField] private AudioClip[] stepTile = default;
    [SerializeField] private AudioClip[] stepCarpet = default;
    [SerializeField] private AudioClip[] stepConcrete = default;

    [SerializeField] private CharacterController controller = default;
    [SerializeField] private Player player = default;
    [SerializeField] private float minDelayBetweenSteps = 1.5f;
    [SerializeField] private float maxDelayBetweenSteps = 0.5f;
    [SerializeField] private float volumeBase = 0.3f;
    [SerializeField] private float pitchBase = 1.0f;
    [SerializeField] private float footAudioRandomness = 0.1f;
    [SerializeField] private float soundEffectPitchRandomness = 0.05f;
    [SerializeField] private float lowestSpeedMinThreshold = 0.0f;
    [SerializeField] private float lowestSpeedMaxThreshold = 1.0f;
    [SerializeField] private float mediumSpeedMaxThreshold = 3.0f;
    [Header("Names")]
    [SerializeField] private string playerTag = "Player";
    [SerializeField] private string woodMaterialName = "Wood (Instance)";
    [SerializeField] private string tileMaterialName = "Tiles (Instance)";
    [SerializeField] private string carpetMaterialName = "Carpet (Instance)";
    [SerializeField] private string stoneMaterialName = "Stone (Instance)";

    private float delayBetweenSteps;

    private AudioSource stepSource;
    private bool isWalking;
    private GroundType groundType;
    private bool isPlayingWalkSound;
    float horizontalSpeed;

    private Coroutine currentRoutine;

    private void Update()
    {

        GetVelocityAndSetDelay();

        if (controller.isGrounded)
        {
            if (horizontalSpeed > 0f)
            {
                isWalking = true;
                if (!isPlayingWalkSound)
                {
                    currentRoutine = StartCoroutine(PlayFootsteps());
                }

            }
            else
            {
                isWalking = false;
            }
        }
        else
        {
            isWalking = false;
        }

        if (!player.AllowFootsteps)
        {
            StopFootstepSound();
        }
    }

    private void Start()
    {
        stepSource = GetComponent();
    }

    private void GetVelocityAndSetDelay()
    {
        Vector3 horizontalVelocity = new Vector3(controller.velocity.x, 0, controller.velocity.z);

        horizontalSpeed = horizontalVelocity.magnitude;

        if (horizontalSpeed > lowestSpeedMinThreshold && horizontalSpeed < lowestSpeedMaxThreshold)
        {
            delayBetweenSteps = minDelayBetweenSteps;
        } 
        else if (horizontalSpeed < mediumSpeedMaxThreshold)
        {
            delayBetweenSteps = minDelayBetweenSteps / horizontalSpeed;
        }
        else
        {
            delayBetweenSteps = maxDelayBetweenSteps;
        }
    }

    private IEnumerator PlayFootsteps()
    {
        isPlayingWalkSound = true;
        while (isWalking && player.AllowFootsteps)
        {
            if (groundType == GroundType.Wood)
            {
                stepSource.clip = stepWood[Random.Range(0, stepWood.Length)];
            }
            else if (groundType == GroundType.Tiles)
            {
                stepSource.clip = stepTile[Random.Range(0, stepTile.Length)];
            }
            else if (groundType == GroundType.Carpet)
            {
                stepSource.clip = stepCarpet[Random.Range(0, stepCarpet.Length)];
            }
            else if (groundType == GroundType.Stone)
            {
                stepSource.clip = stepConcrete[Random.Range(0, stepConcrete.Length)];
            }

            stepSource.volume = Random.Range(volumeBase - footAudioRandomness, volumeBase + footAudioRandomness);
            stepSource.pitch = Random.Range(pitchBase - soundEffectPitchRandomness, pitchBase + soundEffectPitchRandomness);
            stepSource.Play();

            yield return new WaitForSeconds(delayBetweenSteps);
        }
        isPlayingWalkSound = false;
    }

    private void OnTriggerEnter(Collider c)
    {
        if (c.material != null && !c.gameObject.CompareTag(playerTag))
        {

            if (c.material.name.Equals(woodMaterialName))
            {
                groundType = GroundType.Wood;
            }
            else if (c.material.name.Equals(tileMaterialName))
            {
                groundType = GroundType.Tiles;
            }
            else if (c.material.name.Equals(carpetMaterialName))
            {
                groundType = GroundType.Carpet;
            }
            else if (c.material.name.Equals(stoneMaterialName))
            {
                groundType = GroundType.Stone;
            }
            else
            {
                Debug.Log("found material and it is: " + c.material.name);
            }
        }
        else if(c.material == null)
        {
            Debug.Log("material is null");
        }
    }

    public void StopFootstepSound()
    {
        if(currentRoutine != null)
        {
            StopCoroutine(currentRoutine);
        }

        stepSource.Stop();
        isPlayingWalkSound = false;
    }
}
using System.Collections;
using UnityEngine;

[RequireComponent(typeof(AudioSource))]
public class WalkSound : MonoBehaviour
{
    private enum GroundType
    {
        Wood,
        Tiles,
        Carpet,
        Stone,
    }

    [SerializeField] private AudioClip[] stepWood = default;
    [SerializeField] private AudioClip[] stepTile = default;
    [SerializeField] private AudioClip[] stepCarpet = default;
    [SerializeField] private AudioClip[] stepConcrete = default;

    [SerializeField] private CharacterController controller = default;
    [SerializeField] private Player player = default;
    [SerializeField] private float minDelayBetweenSteps = 1.5f;
    [SerializeField] private float maxDelayBetweenSteps = 0.5f;
    [SerializeField] private float volumeBase = 0.3f;
    [SerializeField] private float pitchBase = 1.0f;
    [SerializeField] private float footAudioRandomness = 0.1f;
    [SerializeField] private float soundEffectPitchRandomness = 0.05f;
    [SerializeField] private float lowestSpeedMinThreshold = 0.0f;
    [SerializeField] private float lowestSpeedMaxThreshold = 1.0f;
    [SerializeField] private float mediumSpeedMaxThreshold = 3.0f;
    [Header("Names")]
    [SerializeField] private string playerTag = "Player";
    [SerializeField] private string woodMaterialName = "Wood (Instance)";
    [SerializeField] private string tileMaterialName = "Tiles (Instance)";
    [SerializeField] private string carpetMaterialName = "Carpet (Instance)";
    [SerializeField] private string stoneMaterialName = "Stone (Instance)";

    private float delayBetweenSteps;

    private AudioSource stepSource;
    private bool isWalking;
    private GroundType groundType;
    private bool isPlayingWalkSound;
    float horizontalSpeed;

    private Coroutine currentRoutine;

    private void Update()
    {

        GetVelocityAndSetDelay();

        if (controller.isGrounded)
        {
            if (horizontalSpeed > 0f)
            {
                isWalking = true;
                if (!isPlayingWalkSound)
                {
                    currentRoutine = StartCoroutine(PlayFootsteps());
                }

            }
            else
            {
                isWalking = false;
            }
        }
        else
        {
            isWalking = false;
        }

        if (!player.AllowFootsteps)
        {
            StopFootstepSound();
        }
    }

    private void Start()
    {
        stepSource = GetComponent();
    }

    private void GetVelocityAndSetDelay()
    {
        Vector3 horizontalVelocity = new Vector3(controller.velocity.x, 0, controller.velocity.z);

        horizontalSpeed = horizontalVelocity.magnitude;

        if (horizontalSpeed > lowestSpeedMinThreshold && horizontalSpeed < lowestSpeedMaxThreshold)
        {
            delayBetweenSteps = minDelayBetweenSteps;
        } 
        else if (horizontalSpeed < mediumSpeedMaxThreshold)
        {
            delayBetweenSteps = minDelayBetweenSteps / horizontalSpeed;
        }
        else
        {
            delayBetweenSteps = maxDelayBetweenSteps;
        }
    }

    private IEnumerator PlayFootsteps()
    {
        isPlayingWalkSound = true;
        while (isWalking && player.AllowFootsteps)
        {
            if (groundType == GroundType.Wood)
            {
                stepSource.clip = stepWood[Random.Range(0, stepWood.Length)];
            }
            else if (groundType == GroundType.Tiles)
            {
                stepSource.clip = stepTile[Random.Range(0, stepTile.Length)];
            }
            else if (groundType == GroundType.Carpet)
            {
                stepSource.clip = stepCarpet[Random.Range(0, stepCarpet.Length)];
            }
            else if (groundType == GroundType.Stone)
            {
                stepSource.clip = stepConcrete[Random.Range(0, stepConcrete.Length)];
            }

            stepSource.volume = Random.Range(volumeBase - footAudioRandomness, volumeBase + footAudioRandomness);
            stepSource.pitch = Random.Range(pitchBase - soundEffectPitchRandomness, pitchBase + soundEffectPitchRandomness);
            stepSource.Play();

            yield return new WaitForSeconds(delayBetweenSteps);
        }
        isPlayingWalkSound = false;
    }

    private void OnTriggerEnter(Collider c)
    {
        if (c.material != null && !c.gameObject.CompareTag(playerTag))
        {

            if (c.material.name.Equals(woodMaterialName))
            {
                groundType = GroundType.Wood;
            }
            else if (c.material.name.Equals(tileMaterialName))
            {
                groundType = GroundType.Tiles;
            }
            else if (c.material.name.Equals(carpetMaterialName))
            {
                groundType = GroundType.Carpet;
            }
            else if (c.material.name.Equals(stoneMaterialName))
            {
                groundType = GroundType.Stone;
            }
            else
            {
                Debug.Log("found material and it is: " + c.material.name);
            }
        }
        else if(c.material == null)
        {
            Debug.Log("material is null");
        }
    }

    public void StopFootstepSound()
    {
        if(currentRoutine != null)
        {
            StopCoroutine(currentRoutine);
        }

        stepSource.Stop();
        isPlayingWalkSound = false;
    }
}