Out of the Abyss

 


Summary

Out of the Abyss is a First Person Shooter that features agile movement mechanics, explosive weaponry and fast-paced gameplay inspired by games like quake and doom. The core gameplay loop is designed to allow for short play sessions of compacted fun.
The player take on the role of an explorer from a distant world that finds themselves trapped in a dark, underground city. The only signs of life are the monsters lurking in the shadows.

In order to escape you must power up the towering elevator in the middle of the city. Venture into procedurally generated cave systems below the city and find the power generators, modify your weapon with modules, defeat hordes of monsters and survive the city!

The game was made in Unity 2018.1 and took 6 and a half weeks to complete plus two weeks of pre-production.
Our team was composed of:

  • 3 x designers
  • 2 x 2D artists
  • 4 x 3D artists

My role

My role in the project was main scripter as well as one of three game designers.
My main contributions was:

  • The procedural generation of caves
  • First person movement mechanics
  • Some weapon module behaviours
  • Many small things, bug fixes and testing

Procedural ”dungeon” generation

The biggest thing I did was the randomization of rooms and corridors for each cave the player entered, thereby making each playthrough have a different layout.To generate random layouts we created many different room prefabs that each had a number of exits and a dedicated collision box to prevent the rooms to be placed inside one another.
The algorithm used is fairly straigth forward:

  1. Randomly select a room prefab
  2. Loop through all of its exits
  3. Randomly select a room prefab to place by the exit
  4. Select one of the new room’s exits and rotate to match the old room
  5. Repeat for all the exits for a pre-determined number of iterations (that can be different for different caves)

Special cases are the first room that will always be one of the starter rooms, and the last room that always is one of the generator rooms.

Generate floor

using System.Collections.Generic;
using System.Collections;
using UnityEngine;

public class Floor : MonoBehaviour {
    private int iterations;
    private GameObject newRoomPrefab;
    private int newRoomPrefabId;
    private GameObject newRoomInstance;
    private Transform[] newRoomExits;
    private List<GameObject> rooms = new List<GameObject>();
    private List<Transform> pendingExits = new List<Transform>();

    private RoomPrefabStruct[] startPrefabs;
    private RoomPrefabStruct[] roomPrefabs;
    private RoomPrefabStruct[] corridorPrefabs;
    private RoomPrefabStruct[] generatorPrefabs;

    [SerializeField]
    private GameObject exitPrefab;
    private DungeonManager dungeonManager;
    private List<Vector3> posiblePositionsForStairs = new List<Vector3>();
    private Vector3 stairsPosition;
    private Vector3 generatorPosition;
    private int roomTries;
    private Transform exitToMatch;
    private HashSet<int> exclude = new HashSet<int>();
    private List<Transform> exitsWithoutConnections = new List<Transform>();
    private bool currentIsCorridor;
    private bool needGenerator;
    private bool corridor;
    public List<Vector3> blockerPositions = new List<Vector3>();
    public List<Quaternion> blockerRotations = new List<Quaternion>();
    public List<ChestStruct> chests = new List<ChestStruct>();
    [SerializeField]
    private GameObject blockerObject;
    private System.Random rand = new System.Random();

    public void InitValues(DungeonManager dungeonManager_in)
    {
        dungeonManager = dungeonManager_in;
        startPrefabs = dungeonManager.startPrefabs;
        roomPrefabs = dungeonManager.roomPrefabs;
        corridorPrefabs = dungeonManager.corridorPrefabs;
        generatorPrefabs = dungeonManager.generatorPrefabs;
        
        iterations = GameManager.Instance.currentDungeonIterations;
    }

    public void GenerateRooms()
    {
        currentIsCorridor = false;
        corridor = true;

        needGenerator = false;
        Transform[] tmpTransform = new Transform[1];

        // Create start room
        int prefabId = (GameManager.Instance.currentDungeonId / 100) - 1;
        GameObject startRoom = Instantiate(startPrefabs[prefabId].prefab);
        Room startRoomComponent = startRoom.GetComponent<Room>();
            
        tmpTransform = startRoomComponent.GetExits();
        startRoomComponent.SetPrefabId(prefabId);
        startRoomComponent.SetLayermask(dungeonManager.prefabLayerMask);
        startRoomComponent.setRoomType(RoomTypeEnum.Start);
        rooms.Add(startRoom);

        // Fetch pending exits for first room
        for (int i = 0; i < tmpTransform.Length; i++)
        {
            pendingExits.Add(tmpTransform[i]);
        }

        for (int i = 0; i < pendingExits.Count; i++)
        {
            Transform temp = pendingExits[i];
            int randomIndex = Random.Range(i, pendingExits.Count);
            pendingExits[i] = pendingExits[randomIndex];
            pendingExits[randomIndex] = temp;
        }

        List<Transform> newExits = new List<Transform>();
        while (iterations > 0)
        {
            newExits = new List<Transform>();

            for (int i = 0; i < pendingExits.Count; i++)
            {
                roomTries = 0;

                var exclude = new HashSet<int>();

                if (!currentIsCorridor)
                {
                    RandomizeCorridorPrefab();
                    corridor = true;
                }
                else
                {
                    RandomizeRoomPrefab();
                    corridor = false;
                }
                
                bool overlappingRooms = InstantiateNewRoom(i);

                if (overlappingRooms)
                {
                    if (!exitsWithoutConnections.Contains(pendingExits[i]))
                    {
                        exitsWithoutConnections.Add(pendingExits[i]);
                    }
                    
                    continue;
                }

                for (int j = 0; j < newRoomExits.Length; j++)
                {
                    if (newRoomExits[j].position != exitToMatch.position)
                    {
                        newExits.Add(newRoomExits[j]);
                        if (iterations == 1) // Last iteration
                        {
                            exitsWithoutConnections.Add(newRoomExits[j]);
                        }
                    }
                }
            }

            pendingExits = newExits;

            if (currentIsCorridor)
            {
                currentIsCorridor = false;
            }
            else
            {
                currentIsCorridor = true;
            }
            iterations -= 1;
        }

        CountChests();

        UpdateWallColor();

        if (pendingExits.Count > 0)
        {
            SpawnBlockers(pendingExits[0]);
        }
    }

    private void UpdateWallColor()
    {
        Material wallMat = GameObject.Find("SM_CaveWall_03").GetComponent<Renderer>().sharedMaterial;

        if (GameManager.Instance.currentDungeonId == 100)
        {
            wallMat.color = Color.white;
        }
        else if (GameManager.Instance.currentDungeonId == 200)
        {
            wallMat.color = Color.red;
        }
        else if (GameManager.Instance.currentDungeonId == 300)
        {
            wallMat.color = Color.green;
        }
        else if (GameManager.Instance.currentDungeonId == 400)
        {
            wallMat.color = Color.blue;
        }

    }

    private void CountChests()
    {
        GameObject[] tmp = GameObject.FindGameObjectsWithTag("Chest");

        for (int i = 0; i < tmp.Length; i++)
        {
            ChestStruct tmpStruct = new ChestStruct();
            tmpStruct.opened = false;
            tmpStruct.position = tmp[i].transform.position;
            chests.Add(tmpStruct);
        }
    }

    private void SpawnBlockers(Transform lastExit)
    {
    if (!InstantiateEndRoom(lastExit, true))
    {
      if (needGenerator)
      {
        needGenerator = false;
      }
    }
    }

    private bool InstantiateEndRoom(Transform roomEntrance, bool overrideCheck)
    {
        int stepsFromSpawn = GameManager.Instance.currentDungeonIterations - (iterations - 1);

        newRoomPrefabId = (GameManager.Instance.currentDungeonId / 100) - 1;
        newRoomPrefab = generatorPrefabs[newRoomPrefabId].prefab;
        

        newRoomInstance = Instantiate(newRoomPrefab);

        rooms.Add(newRoomInstance);
        Room roomComponent = newRoomInstance.GetComponent<Room>();
        newRoomExits = roomComponent.GetExits();
        roomComponent.SetLayermask(dungeonManager.prefabLayerMask);
        roomComponent.SetPrefabId(newRoomPrefabId);
        roomComponent.SetStepsFromSpawn(stepsFromSpawn);
        roomComponent.setRoomType(RoomTypeEnum.Generator);

        int random = Random.Range(0, newRoomExits.Length);
        exitToMatch = newRoomExits[random];

        bool overlapping = MatchExits(roomEntrance, exitToMatch, RoomTypeEnum.Generator, overrideCheck);
        
        return overlapping;
    }

    private void RandomizeRoomPrefab()
    {
        newRoomPrefabId = GetRandomRoomPrefab();
        newRoomPrefab = roomPrefabs[newRoomPrefabId].prefab;
    }

    private void RandomizeCorridorPrefab()
    {
        newRoomPrefabId = Random.Range(0, corridorPrefabs.Length);
        newRoomPrefab = corridorPrefabs[newRoomPrefabId].prefab;
    }

    private bool InstantiateNewRoom(int i)
    {
        int stepsFromSpawn = GameManager.Instance.currentDungeonIterations - (iterations - 1);

        newRoomInstance = Instantiate(newRoomPrefab);
        
        rooms.Add(newRoomInstance);
        Room roomComponent = newRoomInstance.GetComponent<Room>();

        newRoomExits = roomComponent.GetExits();
        roomComponent.SetPrefabId(newRoomPrefabId);
        roomComponent.SetStepsFromSpawn(stepsFromSpawn);
        roomComponent.SetLayermask(dungeonManager.prefabLayerMask);

        RoomTypeEnum tmpType = RoomTypeEnum.Room;
        if (corridor)
        {
            tmpType = RoomTypeEnum.Corridor;
        }
        roomComponent.setRoomType(tmpType);

        int random = Random.Range(0, newRoomExits.Length);

        // Always use the first exit if prefab is corridor
        if (tmpType == RoomTypeEnum.Corridor)
        {
            random = 0;
        }

        exitToMatch = newRoomExits[random];

        bool overlapping = MatchExits(pendingExits[i], exitToMatch, tmpType, false);

        if (overlapping)
        {
            exclude.Add(newRoomPrefabId);
            roomTries++;
            if (roomTries < 3)
            {
                if (currentIsCorridor)
                {
                    RandomizeRoomPrefab();
                }
                else
                {
                    RandomizeCorridorPrefab();
                }
                
                overlapping = InstantiateNewRoom(i);
            }
        }

        if (newRoomPrefabId == roomPrefabs.Length - 2)
        {
            stairsPosition = newRoomInstance.transform.position;
        }

        return overlapping;
    }

    private int GetRandomRoomPrefab()
    {
        return range.ElementAt(index);*/

        int index = rand.Next(0, GameManager.Instance.availablePrefabs.Count);
        return GameManager.Instance.availablePrefabs[index];
    }

    private bool MatchExits(Transform oldExit, Transform newExit, RoomTypeEnum type, bool overrideCheck)
    {
        Transform newRoom = newExit.parent;
        Vector3 forwardVectorToMatch = -oldExit.transform.forward;

        Vector3 correctiveTranslation = oldExit.position - newExit.position;
        newRoom.Translate(correctiveTranslation);

        float correctiveRotation = Azimuth(forwardVectorToMatch) - Azimuth(newExit.forward);
        newRoom.RotateAround(newExit.position, new Vector3(0f, 1f, 0f), correctiveRotation);
        

        if (overrideCheck)
        {
            return false;
        }

        if (newRoom.GetComponent<Room>().CheckOverlap())
        {
            rooms.Remove(newRoomInstance);
            DestroyImmediate(newRoomInstance);
            return true;
        }
        else if (type == RoomTypeEnum.Room)
        {
            GameManager.Instance.availablePrefabs.Remove(newRoomPrefabId);
        }

        return false;
    }

    private float Azimuth(Vector3 vector) // Borrowed from tutorial
    {
        return Vector3.Angle(Vector3.forward, vector) * Mathf.Sign(vector.x);
    }

    public List<GameObject> GetRooms()
    {
        return rooms;
    }

    public Vector3 GetStairsPosition()
    {
        return stairsPosition;
    }

    public Vector3 GetGeneratorPosition()
    {
        return generatorPosition;
    }
}

 


 

Player Movement

In order to easier be able to launch the player I based the player movement on forces instead of direct translation. The class below handles the movement and animations of the weapon.

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class PlayerMovement : MonoBehaviour {
    private float moveSpeed = 6f;
    private float speedMultiplier = 1f;
    private float maxVelocityChange = 5f;
    private float jumpStrength = 8f;
    private Rigidbody rBody;
    private Vector3 moveDirection;
    private bool isGrounded = true;
    [SerializeField]
    private LayerMask layerMask;
    private Transform cameraTransform;
    private float currentRotation;
    private Vector2 lookVector;
    private float clampLow = -90f;
    private float clampHigh = 90f;
    [SerializeField]
    private Camera cam;
    private bool isPushed = false;
    private float pushTimer;
    private float airControl = 20f;
    private bool isJumping = false;
    private bool canMove = true;
    private float stepInterval = 0.4f;
    private float stepTimer = 0f;
    private bool leftFoot;
    private bool isPushingJump = false;
    private float camShakeTimer = 0f;
    [SerializeField]
    private AnimationCurve gunMoveCurve;
    [SerializeField]
    private GameObject gunObject;
    private float gunMoveTimer = 0f;
    private float camShakeAmount = 0.05f;

    private AudioSource audioSource;
    private GunAnimation gunAnim;
    public bool isFiring = false;
    public Image loadingScreen;

    private void Awake()
    {
        rBody = GetComponent<Rigidbody>();
        
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
        rBody.freezeRotation = true;
        audioSource = GetComponent<AudioSource>();
    }

    private void Start()
    {
        cameraTransform = cam.transform;

        GetComponent<PlayerRespawnLogic>().SetPosition();

        gunAnim = GetComponentInChildren<GunAnimation>();
    }

    private void Update ()
    {
        HandleAnimStates();
        HandleJumping();
        HandleMovement();        
        HandleLooking();
        HandlePushTimer();
        isPushingJump = false;
  }

    private void HandleAnimStates()
    {
        bool holdingAMoveKey = Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.D);

        if (isGrounded && holdingAMoveKey && !isFiring && !isJumping)
        {
            gunAnim.SetIsWalking();
        }

        if ((!holdingAMoveKey && 
            (Input.GetKeyUp(KeyCode.W) || Input.GetKeyUp(KeyCode.A) || Input.GetKeyUp(KeyCode.S) || Input.GetKeyUp(KeyCode.D)) &&
            !isFiring))
        {
            gunAnim.SetIsIdle();
        }
    }


    private void HandleCameraShake()
    {
        if (camShakeTimer > 0f)
        {
            cam.transform.localPosition = Random.insideUnitSphere * camShakeAmount;
            camShakeTimer -= Time.deltaTime;
            if (camShakeTimer < 0f)
            {
                camShakeTimer = 0f;
                cam.transform.localPosition = new Vector3(0f, 0.5f, 0f);
            }
        }
    }

    private void MovePlayer()
    {
        if (!canMove)
        {
            return;
        }

        Vector3 velocity = rBody.velocity;

        if (isGrounded && !isPushed)
        { 
            Vector3 velocityChange = moveDirection - velocity / 1.5f;

            velocityChange.x = Mathf.Clamp(velocityChange.x, -maxVelocityChange, maxVelocityChange);
            velocityChange.z = Mathf.Clamp(velocityChange.z, -maxVelocityChange, maxVelocityChange);

            velocityChange.y = 0f;

            rBody.AddForce(velocityChange * 100f, ForceMode.Acceleration);
        }
        else
        {
            Vector3 velocityChange = moveDirection - velocity / 1.5f;
      
            if (rBody.velocity.y > 16f)
            {
                velocityChange.y = 16f - rBody.velocity.y;
            }
            else
            {
                velocityChange.y = 0f;
            }

            rBody.AddForce(velocityChange * 100f, ForceMode.Acceleration);
        }

        if (!isGrounded)
        {
            // Extra gravity
            rBody.AddForce(Physics.gravity * Time.fixedDeltaTime * 1.2f, ForceMode.Impulse);
        }
    }

    private void FixedUpdate()
    {
        RaycastHit hitInfo;
        isGrounded = Physics.SphereCast(transform.position, 0.4f, Vector3.down, out hitInfo, 1.01f, layerMask);

        MovePlayer();
    }

    private Vector3 GetForwardTangent(Vector3 mDirection, Vector3 up)
    {
        Vector3 right = Vector3.Cross(up, mDirection);
        Vector3 forward = Vector3.Cross(right, up);
        Debug.DrawRay(rBody.position, forward, Color.blue, 1f);
        return forward;
    }

    private void HandlePushTimer()
    {
        if (pushTimer > 0f)
        {
            pushTimer -= Time.deltaTime;
        }
        else if (isPushed)
        {
            isPushed = false;
        }
    }

    private void HandleMovement()
    {
        moveDirection = new Vector3(
            Input.GetAxis("Horizontal"),
            0,
            Input.GetAxis("Vertical")
            );

        moveDirection = transform.TransformDirection(moveDirection);
        moveDirection *= moveSpeed;
    }

    private void HandleJumping()
    {
        if (isGrounded && Input.GetKey(KeyCode.Space) && !isJumping)
        {
            Vector3 jumpVector = new Vector3(moveDirection.x, 0f, moveDirection.z) * 2f + Vector3.up * jumpStrength;
            if (rBody.velocity.y < jumpVector.y)
            {
                rBody.AddForce(jumpVector, ForceMode.Impulse);
            }
            
            isJumping = true;
            isPushingJump = true;
            gunAnim.SetIsIdle();
        }
    }
    
    private void HandleLooking()
    {
        if (cam != null && canMove)
        {
            lookVector.x = Input.GetAxis("Mouse X") * 1.5f;
            lookVector.y += Input.GetAxis("Mouse Y") * 1.5f;

            lookVector.y = Mathf.Clamp(lookVector.y, clampLow, clampHigh);

            cameraTransform.localRotation = Quaternion.identity * Quaternion.Euler(-lookVector.y, 0f, 0f);

            transform.Rotate(Vector3.up, lookVector.x);
        }
    }

    public void SetIsPushed(bool pushed)
    {
        isPushed = pushed;
        pushTimer = 3f;
    }

    private void OnCollisionEnter(Collision collision)
    {
        isPushed = false;
        isJumping = false;
    }

    public void SetCanMove(bool move)
    {
        canMove = move;
    }

    public void ShakeCamera(float duration, float amount)
    {
        camShakeTimer = duration;
        camShakeAmount = amount;
    }
}