And then Jack woke up
Game Info:
Roles: Game Programmer
Date: May – June 2020
Time: 7 Weeks
Team Size: 9 People (2 Programmers, 3 Designers, 3 3D artists, 1 2D artist)
Genre: Walking Simulator
Engine: Unity 3d
Version Control: Perforce
Code Language: C#
Jack lives in a nice apartment, has a mundane office job and hobbies. But Jack’s world is slowly crumbling: he’s waking up.
And Then Jack Woke Up is a narrative driven first-person game which takes the player through an ordinary day in the life of Jack. However, something is strange about Jack’s life. His furniture is changing, he is recieving strange messages on his answering machine…
… and what’s with all these post-it’s?
Challenge: Create a game based on a feeling (We chose suspicion)
My Contributions:
World transition system
For the game we wanted a seemless level loading.
The game shouldn’t have any loading screens and all levels should be able to get activated when wanted. Our game is very linear and therefore a wish to defining a sequence for the levels and to progress them without knowing the current level index was requested. Information about the current level for the player was also requested.
I created a system that loads up every level on game starts and then deactivates a root component in the scene. Designers could then specify the levels that would be loaded on game start. Since the player sometimes needed to change position when loading a level the functionality to move a Transform was added.
The system was hooked into Unity’s SceneManager and worked with it.
Code example - WorldSystem.cs
using System;
using UnityEngine;
public static class WorldSystem
{
private static readonly ISceneSystem sceneSystem = new SceneLoader();
private static SceneData[] originalSceneDatas;
public static event Action<SceneData> OnSceneLoaded = delegate { };
public static void Init(SceneData[] sceneDatas)
{
sceneSystem.ClearData();
sceneSystem.LoadScenes(sceneDatas);
originalSceneDatas = sceneDatas;
}
public static void LoadLevel(int id, bool additive = false)
{
SceneData loadedScene = sceneSystem.LoadScene(id, additive);
OnSceneLoaded.Invoke(loadedScene);
}
public static void LoadLevelAndMovePlayerTransform(int sceneId, Transform playerTransform, Vector3 newPosition)
{
LoadLevel(sceneId);
playerTransform.GetComponent<CharacterController>().enabled = false;
playerTransform.position = newPosition;
playerTransform.GetComponent<CharacterController>().enabled = true;
}
public static void LoadLevelAndMovePlayerTransform(int sceneId, Transform transform, Vector3 lookRotation, Vector3 newPosition)
{
LoadLevel(sceneId);
transform.GetComponent<CharacterController>().enabled = false;
transform.position = newPosition;
transform.GetComponent<CharacterController>().enabled = true;
transform.GetComponent<Player>().PlayerMovement.SetRotationExternal(lookRotation);
}
public static void ProgressLevelAndMovePlayerTransform(Transform transform, Vector3 newPosition)
{
ProgressLevel();
transform.GetComponent<CharacterController>().enabled = false;
transform.position = newPosition;
transform.GetComponent<CharacterController>().enabled = true;
}
public static void ProgressLevelAndMovePlayerTransform(Transform transform, Vector3 lookRotation, Vector3 newPosition)
{
ProgressLevel();
transform.GetComponent<CharacterController>().enabled = false;
transform.position = newPosition;
transform.GetComponent<CharacterController>().enabled = true;
transform.GetComponent<Player>().PlayerMovement.SetRotationExternal(lookRotation);
}
public static void ProgressLevel()
{
SceneData loadedScene = sceneSystem.ProgressLevel();
OnSceneLoaded.Invoke(loadedScene);
}
public static void UnloadScene(int id)
{
sceneSystem.UnloadScene(id);
}
public static void ResetWorld()
{
Init(originalSceneDatas);
}
}
Code example - SceneLoader.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneLoader : ISceneSystem
{
private const string sceneParentTag = "SceneParent";
private readonly Dictionary<int, SceneData> scenes;
private bool scenesLoaded = false;
private int currentLoadedScene = 2;
public event Action OnSceneLoaded;
public SceneLoader()
{
SceneManager.sceneLoaded += SceneLoaded;
scenes = new Dictionary<int, SceneData>();
}
public void LoadScenes(SceneData[] scenes)
{
if (scenesLoaded)
{
return;
}
foreach (SceneData scene in scenes)
{
this.scenes.Add(scene.SceneIndex, scene);
SceneManager.LoadScene(scene.SceneIndex, LoadSceneMode.Additive);
}
scenesLoaded = true;
}
public void ClearData()
{
for (int i = 0; i < SceneManager.sceneCount; i++)
{
Scene currentScene = SceneManager.GetSceneAt(i);
if (currentScene == SceneManager.GetActiveScene())
{
continue;
}
SceneManager.UnloadSceneAsync(currentScene);
}
scenes.Clear();
scenesLoaded = false;
}
private void SceneLoaded(Scene loadedScene, LoadSceneMode loadMode)
{
if(!scenes.ContainsKey(loadedScene.buildIndex))
{
return;
}
SceneData sceneData = scenes[loadedScene.buildIndex];
SetSceneParent(sceneData, loadedScene);
if(!sceneData.LoadOnStart)
{
sceneData.SceneParent.SetActive(false);
}
}
private void SetSceneParent(SceneData sceneData, Scene loadedScene)
{
foreach (GameObject rootObject in loadedScene.GetRootGameObjects())
{
if (rootObject.CompareTag(sceneParentTag))
{
sceneData.SceneParent = rootObject;
break;
}
}
}
public Dictionary<int, SceneData> GetScenes()
{
return scenes;
}
public SceneData LoadScene(int id, bool additive = false)
{
if (!additive)
{
scenes[currentLoadedScene].SceneParent.SetActive(false);
LightmapSettings.lightProbes = null;
}
scenes[id].SceneParent.SetActive(true);
currentLoadedScene = id;
return scenes[currentLoadedScene];
}
public SceneData ProgressLevel()
{
int id = currentLoadedScene + 1;
scenes[id].SceneParent.SetActive(true);
scenes[currentLoadedScene].SceneParent.SetActive(false);
currentLoadedScene = id;
return scenes[currentLoadedScene];
}
public void UnloadScene(int id)
{
scenes[id].SceneParent.SetActive(false);
}
}
Quest System
We wanted a system to keep track on the players progress. Objects that had the quest listener component on them chose whatever quests to listen to, this quest was then connected to a function on a chosen gameobject. This made it easy for designers to choose what every quest should do. Each quest could trigger several functions as well.
Since this system only triggered on event completion I ran into some problems when an item was loaded and the quest was already marked as complete. To solve this I added a boolean that was exposed so if wanted this item could trigger this function if the quest was already complete.

Code example - QuestSystem.cs
using System;
using System.Collections.Generic;
using UnityEngine;
public static class QuestSystem
{
private static Dictionary<int, bool> completedQuests = new Dictionary<int, bool>();
public static event Action<GameObject, int> OnQuestCompleted = delegate { };
public static void QuestCompleted(GameObject questItem, int questId)
{
completedQuests.Add(questId, true);
OnQuestCompleted.Invoke(questItem, questId);
}
public static bool IsQuestCompleted(int id)
{
return completedQuests.ContainsKey(id) && completedQuests[id];
}
public static void ResetQuests()
{
completedQuests.Clear();
}
}
Code example - QuestListener.cs
using System.Collections.Generic;
using UnityEngine;
public class QuestListener : MonoBehaviour
{
[SerializeField] private QuestData[] listenForQuests = default;
private Dictionary<int, QuestData> quests;
private void OnEnable()
{
QuestSystem.OnQuestCompleted += QuestComplete;
if (quests == null)
{
GenerateQuestsFromQuestData();
}
InvokeQuestsOnEnable();
}
private void OnDisable()
{
QuestSystem.OnQuestCompleted -= QuestComplete;
}
private void GenerateQuestsFromQuestData()
{
quests = new Dictionary<int, QuestData>();
foreach (var quest in listenForQuests)
{
quests.Add(quest.ListenForId, quest);
}
}
private void InvokeQuestsOnEnable()
{
foreach (var questIdDataPair in quests)
{
if (QuestSystem.IsQuestCompleted(questIdDataPair.Key))
{
if (questIdDataPair.Value.InvokeEventOnEnable)
{
questIdDataPair.Value.OnQuestCompleted.Invoke();
}
}
}
}
private void QuestComplete(GameObject interactor, int questId)
{
if(quests.ContainsKey(questId))
{
quests[questId].OnQuestCompleted.Invoke();
}
}
}
Dialogue System
Since our game was very narrator focused we wanted to add captions to make it easier for the player to follow along the game. The system should handle adding audiofiles easy and the captions should be on a timer.
Code example - DialogueSystem.cs
using TMPro;
using UnityEngine;
public class DialogueSystem
{
private readonly TMP_Text captionText;
private readonly AudioSource audioSource;
private bool displayingCaptions = false;
private float timeLeftOnCaption = 0.0f;
private DialogueCaptions currentCaptions;
private int currentCaptionIndex = 0;
public DialogueSystem(AudioSource audioSource, TMP_Text captionText)
{
this.audioSource = audioSource;
this.captionText = captionText;
}
public void RunUpdate()
{
if(!displayingCaptions)
{
return;
}
timeLeftOnCaption -= Time.deltaTime;
if(timeLeftOnCaption <= 0.0f)
{
FinishCaption();
}
}
private void FinishCaption()
{
currentCaptionIndex++;
if (CurrentCaptionsContainsMoreCaptions())
{
GetNextCaption();
}
else
{
ClearCaptionInformation();
}
}
private bool CurrentCaptionsContainsMoreCaptions()
{
return currentCaptions != null && currentCaptions.captions.Length > currentCaptionIndex;
}
private void GetNextCaption()
{
CaptionData current = currentCaptions.captions[currentCaptionIndex];
captionText.text = current.caption;
timeLeftOnCaption = current.displayTime;
}
private void ClearCaptionInformation()
{
if(audioSource.isPlaying)
{
audioSource.Stop();
}
captionText.text = string.Empty;
displayingCaptions = false;
currentCaptionIndex = 0;
audioSource.Stop();
}
public void PlayNarratorVoice(AudioClip clip, DialogueCaptions caption)
{
ClearCaptionInformation();
if (clip != null)
{
audioSource.clip = clip;
audioSource.Play();
}
currentCaptions = caption;
if(currentCaptions.captions.Length > 0)
{
CaptionData current = currentCaptions.captions[currentCaptionIndex];
captionText.text = current.caption;
timeLeftOnCaption = current.displayTime;
}
displayingCaptions = true;
}
public void StopCurrentDialogue()
{
ClearCaptionInformation();
}
}
Player Interaction System
One core part of the game was an interactive world. You should be able to interact with objects around. For this I created a system that highlighted interactable objects and that made it able for a player to interact with the highlighted object.
Code example - PlayerInteraction.cs
using System.Collections;
using TMPro;
using UnityEngine;
[System.Serializable]
public class PlayerInteraction
{
private float range;
private GameObject owner = default;
private GameObject interactionObjectUI;
private GameObject notInteractedUIObject;
private GameObject interactedUIObject;
private float switchIconTime;
private TMP_Text interactText;
private Camera camera;
private Player player;
private const string pressString = "Press '{0}' to {1}";
private const char interactionButton = 'E'; // If it becomes a setting then remove this and add a reference to the key
private RaycastHit[] raycastHits;
private Vector3 raycastVector;
private string lastInteractText;
private IInteractable currentInteractable;
private GameObject currentGameObject;
private GameObject lastGameObject;
private Coroutine switchBackRoutine;
private LayerMask noLayer = 0;
private LayerMask outlineLayer = 8;
public PlayerInteraction(float interactionRange, GameObject owner, GameObject interactionObjectUI, GameObject notInteractedObject, GameObject interactedObject, float switchIconTime)
{
range = interactionRange;
this.owner = owner;
player = owner.GetComponent<Player>();
this.interactionObjectUI = interactionObjectUI;
interactText = interactionObjectUI.GetComponentInChildren<TMP_Text>();
notInteractedUIObject = notInteractedObject;
interactedUIObject = interactedObject;
this.switchIconTime = switchIconTime;
notInteractedUIObject.SetActive(true);
interactedUIObject.SetActive(false);
camera = owner.GetComponentInChildren<Camera>();
raycastHits = new RaycastHit[20];
}
public void RunUpdate()
{
raycastVector.x = Screen.width * 0.5f;
raycastVector.y = Screen.height * 0.5f;
int hits = Physics.RaycastNonAlloc(camera.ScreenPointToRay(raycastVector), raycastHits, range);
if(hits > 0)
{
TryToSetInteractable();
}
else if(currentInteractable != null)
{
currentGameObject = currentInteractable.GetGameObject();
SetLayerRecursively(currentGameObject.transform, 0);
lastGameObject = null;
currentGameObject = null;
currentInteractable = null;
}
if(currentInteractable == null && interactionObjectUI.activeSelf || currentInteractable != null && !currentInteractable.CanInteract())
{
if (currentGameObject)
{
SetLayerRecursively(currentGameObject.transform, 0);
lastGameObject = null;
}
currentGameObject = null;
interactionObjectUI.SetActive(false);
}
}
private void TryToSetInteractable()
{
// Try getting an interactable on the hit
currentInteractable = TryGetInteractable();
// Make sure we have a current interactable
if (currentInteractable != null)
{
TryHighlightCurrentGameObject();
}
else if (currentGameObject != null)
{
ResetHighlightedObjects();
}
}
private void TryResettingLastGameObject()
{
// If the lastgameobject was not null and it's not the same as the current
// we need to set the outline on the last object to noLayer in order to
// not have two outlined objects
if (lastGameObject != null && lastGameObject != currentGameObject)
{
SetLayerRecursively(lastGameObject.transform, noLayer);
if (switchBackRoutine != null)
{
player.StopCoroutine(switchBackRoutine);
}
if (interactedUIObject.activeSelf)
{
ToggleInteractedIcon();
}
}
}
private void TryHighlightCurrentGameObject()
{
// Set currentGameObject
currentGameObject = currentInteractable.GetGameObject();
TryResettingLastGameObject();
// Make sure the interactable is currently accepting interaction and that it's a new game object
if (currentInteractable.CanInteract() && currentGameObject != lastGameObject)
{
// Highlight current gameobject
interactionObjectUI.SetActive(true);
SetLayerRecursively(currentGameObject.transform, outlineLayer);
lastGameObject = currentGameObject;
}
// Only update the text if it has changed
if (!currentInteractable.GetInteractText().Equals(lastInteractText))
{
interactText.text = string.Format(pressString, interactionButton, currentInteractable.GetInteractText());
lastInteractText = currentInteractable.GetInteractText();
}
}
private void ResetHighlightedObjects()
{
if (lastGameObject != null)
{
SetLayerRecursively(lastGameObject.transform, noLayer);
}
SetLayerRecursively(currentGameObject.transform, noLayer);
currentGameObject = null;
lastGameObject = null;
}
private void SetLayerRecursively(Transform transform, LayerMask layerMask)
{
transform.gameObject.layer = layerMask;
foreach (Transform child in transform)
{
if(child.childCount > 0)
{
SetLayerRecursively(child, layerMask);
}
}
}
private IInteractable TryGetInteractable()
{
Transform closestTransform = null;
float closestDistance = float.MaxValue;
for(int i = 0; i < raycastHits.Length; i++)
{
float distance = Vector3.Distance(camera.transform.position, raycastHits[i].point);
if (distance < closestDistance)
{
closestTransform = raycastHits[i].transform;
closestDistance = distance;
}
raycastHits[i] = default;
}
if(closestTransform == null)
{
return null;
}
return (IInteractable)closestTransform.GetComponent(typeof(IInteractable));
}
public void TryInteraction()
{
if(currentInteractable != null && currentInteractable.CanInteract())
{
currentInteractable.OnInteract(owner);
ToggleInteractedIcon();
switchBackRoutine = player.StartCoroutine(SwitchBackIcon());
}
}
private void ToggleInteractedIcon()
{
interactedUIObject.SetActive(!interactedUIObject.activeSelf);
notInteractedUIObject.SetActive(!notInteractedUIObject.activeSelf);
}
private IEnumerator SwitchBackIcon()
{
yield return new WaitForSeconds(switchIconTime);
ToggleInteractedIcon();
}
}