
ESCAPE 51
School Project
- Contributions:
- Game Design
- Seamless Scene Loading
- Object Pushing
- Explosion effects
- Collecting Gameplay Footage
- Simple Unity Animations
- Shader Programming (that unfortunately did not make the cut)
A stealth game where you play as an alien with the ability to switch limbs in order to escape Area 51. This was our final school project, in which I got to wear many hats, something that I find enjoyable despite the challenge of having to get into disciplines that I had no prior experience of (e.g. shader programming).
Seamless Scene Loading
This script enables dynamic, seamless scene transitions by leveraging Unity’s additive scene management. When the player enters a trigger, it loads the specified scenes and unloads unnecessary ones, ensuring smooth transitions without performance drops.
- Additive Loading: Efficiently loads scenes only when needed, avoiding duplicates.
- Scene Unloading: Unloads unneeded scenes to free up memory.
- Trigger-Based Activation: Automatically triggers loading and unloading when the player enters the collider.
- State Tracking: Ensures proper sequence of actions, enabling smooth animations or other post-loading actions.
using UnityEngine;
using UnityEngine.SceneManagement;
// Class handling async scene loading and unloading
public class SceneLoadTrigger : MonoBehaviour
{
[SerializeField] private SceneField[] _scenesToLoad;
[SerializeField] private SceneField[] _scenesToUnload;
bool isSceneLoaded;
bool loadCompleted;
bool unloadCompleted;
// Triggers scene loading and unloading when player enters the objects hitbox the script is attached
to
private void OnTriggerEnter(Collider col)
{
if(col.CompareTag("Player"))
{
LoadScenes();
UnloadScenes();
}
}
//Loads the scenes added to the list _scenesToLoad, adjusts bools accordingly
private void LoadScenes()
{
for(int i = 0; i < _scenesToLoad.Length; i++)
{
isSceneLoaded = false;
for(int j = 0; j < SceneManager.sceneCount; j++)
{
Scene loadedScene = SceneManager.GetSceneAt(j);
if(loadedScene.name == _scenesToLoad[i].SceneName)
{
isSceneLoaded = true;
break;
}
}
if (!isSceneLoaded)
{
SceneManager.LoadSceneAsync(_scenesToLoad[i],
LoadSceneMode.Additive);
}
}
loadCompleted = true;
}
//Unloads the scenes added to the list _scenesToUnload, adjusts bools
//accordingly
private void UnloadScenes()
{
for(int i = 0; i < _scenesToUnload.Length; i++)
{
for (int j = 0; j < SceneManager.sceneCount; j++)
{
Scene loadedScene = SceneManager.GetSceneAt(j);
if(loadedScene.name == _scenesToUnload[i].SceneName)
{
SceneManager.UnloadSceneAsync(_scenesToUnload[i]);
}
}
}
unloadCompleted = true;
}
//Bool to use in order to play animation when the load/unload is done
public bool Done()
{
bool done = loadCompleted && unloadCompleted;
loadCompleted = false;
unloadCompleted = false;
return (done);
}
}
Pushing Objects
This script system enables pushable object interactions and logic, ensuring smooth, direction-based movement while preventing overlapping or incorrect states. The InteractPushObject script handles push direction, blocking mechanics, and interaction toggling, while PushStop serves as a designated endpoint to manage stopping logic.
- Directional Setup: Automatically determines push direction based on object rotation.
- Collision Logic: Blocks opposite directions when objects encounter obstacles.
- Player Interaction: Ensures proper activation and deactivation of colliders for responsive gameplay.
- Push Termination: Triggers when an object encounters a “stop” zone.
- State Integration: Links to InteractPushObject for cohesive behavior.
using UnityEngine;
// Class handling push objects' interaction
public class InteractPushObject : MonoBehaviour, IInteract
{
private Transform _rotationParent;
private KeyCode _directionalKey;
private Vector3 _pushDirection;
private bool _blocked = false;
private bool _active = true;
private float _pushDistOffsetMultiplier = 0.7f; // Range of 0 to 1, 0 being halfway in the collider, 1 being at the edge of the collider
public Transform RotationParent { get { return _rotationParent; } }
public KeyCode DirectionalKey { get { return _directionalKey; } }
public Vector3 PushDirection { get { return _pushDirection; } }
public bool Blocked { get { return _blocked; } set { _blocked = value; } }
public bool Active { get { return _active; } set { _active = value; } }
// Checks which direction the push object should be pushed and updates variables accordingly
private void Start()
{
_rotationParent = transform.parent;
switch (_rotationParent.localRotation.eulerAngles.y)
{
case 0:
_directionalKey = KeyCode.W;
_pushDirection = _rotationParent.parent.forward;
break;
case 90:
_directionalKey = KeyCode.D;
_pushDirection = _rotationParent.parent.right;
break;
case 180:
_directionalKey = KeyCode.S;
_pushDirection = _rotationParent.parent.forward * (-1);
break;
case 270:
_directionalKey = KeyCode.A;
_pushDirection = _rotationParent.parent.right * (-1);
break;
}
}
public bool TriggerLogic(PlayerStateMachiene playerStateMachineScript)
{
playerStateMachineScript.ImportInfo = this;
return false;
}
public bool TriggerLogic()
{
Debug.Log("Error exception: State machine logic");
return false;
}
// Detects when an object enters the trigger on a pushable object,
// & what kind of object in order to handle accurate logic.
private void OnTriggerEnter(Collider col)
{
if (col.name == "PushStop")
{
col.GetComponent<PushStop>().InteractPushObject = this;
}
else if (!col.CompareTag("Player") && !col.CompareTag("PushThrough") && !col.CompareTag("CameraZone"))
ToggleKey(true, col.name); //Block interaction
}
private void OnTriggerExit(Collider col)
{
if (!col.CompareTag("Player") && !col.CompareTag("PushThrough") && !col.CompareTag("CameraZone"))
ToggleKey(false, col.name); //Unblock interaction
else if (!_active && col.CompareTag("Player"))
{
gameObject.tag = "Untagged";
transform.GetComponent<Collider>().enabled = false;
}
}
// Finds the opposite direction's transform for interaction
private Transform OppositeTransform()
{
switch (_directionalKey)
{
case KeyCode.W:
return transform.Find("../../S");
case KeyCode.D:
return transform.Find("../../A");
case KeyCode.S:
return transform.Find("../../W");
case KeyCode.A:
return transform.Find("../../D");
default:
return null;
}
}
// Toggles blocking status for opposite interaction
private void ToggleKey(bool block, string name)
{
Debug.Log("pushed into " + name);
OppositeTransform().GetComponentInChildren<InteractPushObject>().Blocked = block;
}
// Disables interaction on the opposite side and moves the object
public void DisableCollider()
{
InteractPushObject oppositeCollider = OppositeTransform().GetComponentInChildren<InteractPushObject>();
oppositeCollider.Blocked = true;
oppositeCollider.Active = false;
if (oppositeCollider.transform.Find("../RotationParent"))
{
MoveObject move = oppositeCollider.transform.Find("../RotationParent").GetComponent<MoveObject>();
move.Move();
}
}
// Re-enables interaction on the opposite side
public void ReEnableCollider()
{
InteractPushObject oppositeCollider = OppositeTransform().GetComponentInChildren<InteractPushObject>();
oppositeCollider.transform.GetComponent<Collider>().enabled = true;
oppositeCollider.Active = true;
oppositeCollider.tag = "Push";
if (oppositeCollider.transform.Find("../RotationParent"))
{
MoveObject move = oppositeCollider.transform.Find("../RotationParent").GetComponent<MoveObject>();
move.ResetMove();
}
}
// Returns true if the player is within the bounds of the colliders offset position
public bool PushPosition(Vector3 playerPosition)
{
switch (_directionalKey)
{
case KeyCode.W:
return playerPosition.z > transform.position.z - ((transform.lossyScale.z / 2) * _pushDistOffsetMultiplier);
case KeyCode.D:
return playerPosition.x > transform.position.x - ((transform.lossyScale.x / 2) * _pushDistOffsetMultiplier);
case KeyCode.S:
return playerPosition.z < transform.position.z + ((transform.lossyScale.z / 2) * _pushDistOffsetMultiplier);
case KeyCode.A:
return playerPosition.x < transform.position.x + ((transform.lossyScale.x / 2) * _pushDistOffsetMultiplier);
default:
return false;
}
}
}
Manages pushable objects’ movement and interaction, including direction assignment and collision handling.
using UnityEngine;
/* Use intructions:
*
* Name of object should be PushStop, (this is also changed in code as a failsafe)
* BoxCollider needs to be a trigger
* The object/s that this object represents, such as walls etc needs to on layer pushthrough
*/
// Class used to to disable the opposite collider when the associated objects collider collides with a push collider
[RequireComponent(typeof(BoxCollider))]
public class PushStop : MonoBehaviour
{
private Collider _pushObject;
private InteractPushObject _interactPushObject;
public InteractPushObject InteractPushObject { set { _interactPushObject = value; } }
private void Start()
{
name = "PushStop";
}
private void Update()
{ // If there is an active pushable object and a valid InteractPushObject reference,
// disable the opposite collider
if ((_pushObject != null) && (_interactPushObject != null))
{
_interactPushObject.DisableCollider();
}
}
private void OnTriggerEnter(Collider col)
{
if (col.name == "PushObject")
{
if (_interactPushObject != null) // Immediately disables the opposite collider
{ //if a valid InteractPushObject reference exists
_interactPushObject.DisableCollider();
}
else
_pushObject = col;
}
}
private void OnTriggerExit(Collider col)
{
if (col.name == "PushObject") // Clears the reference to the pushable object
{ // if the object is a valid push object.
_pushObject = null;
if (_interactPushObject != null)
{ // Re-enables the opposite collider and clears the InteractPushObject reference
_interactPushObject.ReEnableCollider();
_interactPushObject = null;
}
}
}
}
Serves as the endpoint for pushable objects, halting movement and enabling further logic when an object reaches its limit.
Explosion Effect
This script handles the activation of a door’s damage effect when the player enters a specific trigger area. Upon collision, the door is deactivated, a damaged door mesh is shown, and an explosion sound is played. Additionally, the script disables the collider to prevent the effect from triggering multiple times.

- Door Deactivation: Disables the door and activates a damaged door mesh for visual feedback.
- Audio Feedback: Plays a sound to enhance the impact of the event.
- Collider Management: Disables the collider after the event to prevent re-triggering.
- Camera Shake: The component Cinemachine Collision Impulse is used to add a camera shake effect to simulate the physical impact of the explosion. This enhances the player’s immersion by shaking the camera in response to the door’s destruction.
using UnityEngine;
// Class handling how the first cell door in section 1 breaks
public class DoorDamageScript : MonoBehaviour
{
[SerializeField] GameObject door;
[SerializeField] GameObject damagedDoorMesh;
private void OnTriggerEnter(Collider col)
{
if (col.CompareTag("Player"))
{
GetComponent<AudioSource>().Play();
door.SetActive(false);
damagedDoorMesh.SetActive(true);
GetComponent<BoxCollider>().enabled = false;
}
}
}