Unmoored

Duration:  7 Weeks

Team size: 10 People

Language: C#

Engine: Unity

My role: Gameplay programmer



Unmoored is a thriller story game where you play as the journalist Alexandra McCarter (Alex) as she travels to a deserted island located within the Bermuda triangle to investigate the disappearance of Dr. Stanley Moore.


Alex is a mysterious character who comes across as a lone wolf so as soon as she realizes she is not alone her immediate trust issues kicks in.


At the island she must discover the truth throughout clues and puzzles. The game was based and inspired on the feeling of suspicion which made it an interesting game to develop.

My role for this project was as a gameplay programmer and UI Programmer. The scripts I worked most with was PlayerController, PlayerCamera, Puzzle/WinPuzzle and PauseMenu.


This project was our first remote one since the quarantine. It was a big adjustment to work remote I personally found it harder to solve common issues in the beginning, but we quickly got used to working that way and managed to figure everything out on how to work remote.

This project was made during quarantine started and we worked from home. I do believe we did the best we could and that we had good teamwork during the circumstances. We were a team of ten. 3 programmers, 2 game designers, 3 artists (3D) and 2 artists (2D).


During this project we started to learn about scrum and agile working . We learned how to create and use a sprint log, backlog, and a burnout chart we also used planning poker. I noticed a significant difference when working more agile and that was how much easier it became to prioritize and get everything done in time for our weekly overview.


To work this way is something that I learned during this project and I have found it to be a helpful tool for both team projects but also for my own ones. We worked well as a team and had fun during developing this game we had great teamwork and communicated well.

The programming part. 

This project was quite large, and we had to create multiple scripts for this game. I worked mostly with the movement and setup for the jigsaw puzzle but I also worked with parts of the UI.

 

We pair programmed and worked a lot in each other’s scripts. Since the game was a thriller game based on the feeling of suspicion, we put lot of time into smooth gameplay and intriguing experience. It was important to offer the player a challenging yet fun experience.

PlayerController

We started off with way to complicated movement that had unnecessary functions such as crouching, sprinting, jumping and regular WASD movement.  


Eventually we realized that we did not need all the mechanics and that it would probably give it a much smoother experience to focus on the storyline with minimalistic movement, so we scrapped everything except the WASD movement. It made it much better and easier to focus on the gameplay.

PlayerCamera

This is the PlayerCamera script. It is a typical first-person camera where the player will always look to the same direction that the player is going. We wanted it to look a bit like you are moving in reality, so we added head bobbing to the player which is a separate script.

Puzzle

The puzzle base was set up so you could move the pieces and rotate them to fit in the right image tile. Later on, we connected it to the inventory system where the the puzzle pieces are to complete the jigsaw.

PauseMenu

This is the script for the pause menu that I helped set up for our 2D artist. It works as a basic pause menu where we also have an options/settings part to increase/decrease the volume and change the ouse senstivity.

      
      	
using System.Collections;
using UnityEngine;
using UnityEngine.Assertions.Must;
using UnityEngine.UI;

[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{

    #region SerializedVariables
    [Header("Player movement")]

    [Tooltip("Adjusts the walk speed of the player")]
    [SerializeField] private float m_walkSpeed = 8f;

    //[Tooltip("Adjusts the run speed of the player")]
    //[SerializeField] private float m_runSpeed = 16f;

    [Tooltip("Adjusts the gravityscale")]
    [SerializeField] private float m_gravityScale = -20.0f;

    //[Tooltip("Adjusts the force of the players jump")]
    //[SerializeField] private float m_jumpHeight = 1.5f;

    [Tooltip("Adjusts the rotationspeed when rotating towards NPC")]
    [SerializeField, Range(1, 5)] private float m_rotationSpeed;

    [Header("Inventory")]
    public InventoryObject inventory;

    [Header("ObservableProperties")]
    [SerializeField] ObeservableBoolProperty inDialouge;
    [SerializeField] ObeservableBoolProperty inInventory;

    //[Header("Ground Check")]
    //[SerializeField] private float groundCheckRadius = 0.2f;
    //[SerializeField] private LayerMask groundLayerMask;

    [Header("References")]
    [SerializeField] private Transform m_robot;
    [SerializeField] Transform m_jigSawTransform;
    [SerializeField] float lerpMult;
    #endregion SerializedVariables

    #region PrivateVariables
    [HideInInspector]

    private Vector3 m_moveInput;
    private Vector3 m_moveDirection;

    private CharacterController m_controller;
    private float m_speed;
    private bool m_isGrounded;
    [SerializeField] private float m_runSpeed = 16f;
    private bool m_playerInteractWithRobot = false;
    private bool m_isPlaying = false;
    private bool m_inJigsawPos = false;
    private float t = 0;
    private Vector3 m_oldPos;
    private Quaternion m_oldRot;
    #endregion PrivateVariables


    private FadeBlack fading;
    private bool m_inTutorial;

    public float WalkSpeed { get => m_walkSpeed; set => m_walkSpeed = value; }

    private void OnEnable()
    {
        DialougeManager.StartPlayerRotation += RotatePlayer;
        DialougeManager.StopPlayerRotation += StopRotatePlayer;
        TornLetter.isPlaying += SetIsPlaying;
        TutorialTrigger.inTutorial += SetIsInTutorial;
    }

    private void OnDisable()
    {
        DialougeManager.StartPlayerRotation -= RotatePlayer;
        DialougeManager.StopPlayerRotation -= StopRotatePlayer;
        TornLetter.isPlaying -= SetIsPlaying;
    }

    private void SetIsPlaying(bool value)
    {
        m_isPlaying = value;
        m_oldPos = transform.position;
        m_oldRot = transform.rotation;
    }

    private void SetIsInTutorial(bool value)
    {
        m_inTutorial = value;
    }

    private void Awake()
    {
        fading = gameObject.GetComponent<FadeBlack>();
        m_controller = GetComponent<CharacterController>();
        m_speed = m_walkSpeed;
        WalkSpeed = m_speed;
    }

    private void Update()
    {
        if (m_playerInteractWithRobot)
        {
            RotatePlayerTowardsRobot();
        }

        if(!m_inTutorial)
        if (!inDialouge.Value && !inInventory.Value && !m_isPlaying)
        {
            m_isGrounded = m_controller.isGrounded;

            Moving();
            ApplyPhysics();
        }
        else if (m_isPlaying)
        {
            if (t < 1)
            {
                t += Time.deltaTime * lerpMult;
                transform.position = Vector3.Lerp(m_oldPos, m_jigSawTransform.position, t);
                transform.rotation = Quaternion.Lerp(m_oldRot, m_jigSawTransform.rotation, t);
            }
        }
    }

    public void Moving()
    {
        float horizontalAxis = Input.GetAxis("Horizontal");
        float verticalAxis = Input.GetAxis("Vertical");

        m_moveInput = transform.right * horizontalAxis + transform.forward * verticalAxis;
        m_controller.Move(m_moveInput * WalkSpeed * Time.deltaTime);

        //Stops moving when in air
        if (m_isGrounded) { m_moveDirection = m_moveInput; }

    }


    private void ApplyPhysics()
    {
        if (m_isGrounded && m_moveDirection.y < 0)
        {
            m_moveDirection.y = -500f;
        }

        m_moveDirection.y += m_gravityScale * Time.deltaTime;
        m_controller.Move(m_moveDirection * Time.deltaTime);
    }

    private void StopRotatePlayer()
    {
        m_playerInteractWithRobot = false;
    }

    private void RotatePlayer()
    {
        m_playerInteractWithRobot = true;
    }


    /// <summary>
    /// Rotates player camera towards the robot
    /// </summary>
    private void RotatePlayerTowardsRobot()
    {
        if (m_robot != null)
        {
            Vector3 directionToGo = (m_robot.position - transform.position).normalized;
            Quaternion rot = Quaternion.LookRotation(directionToGo);
            rot.x = 0;
            rot.z = 0;
            transform.rotation = Quaternion.Slerp(transform.rotation, rot, m_rotationSpeed * Time.deltaTime);
        }
    }
}

      	
      
    

      
      	
using UnityEngine;

public class PlayerCamera : MonoBehaviour
{
    [SerializeField] float m_minXRot = -90f;
    [SerializeField] float m_maxXRot = 90f;
    [SerializeField] ObservableFloatProperty m_mouseSensitivity;
    [SerializeField] ObeservableBoolProperty inDialouge;
    [SerializeField] ObeservableBoolProperty inInventory;
    [SerializeField] ObservableFloatProperty volumeProperty;
    [SerializeField] Transform m_jigsawTransform;
    Vector3 m_startPos;
    Quaternion m_startRot;
    bool m_isPlayingJigsaw = false;
    bool m_isInOriginLocation = true;

    float m_xRotation = 0f;
    float t = 0;
    float t2 = 0;
    [SerializeField] float m_camChangeSpeed;
    Bobbing bobbing;
    private bool m_inTutorial;

    public delegate void ShowTutorial();
    public static event ShowTutorial showTutorial;

    private void OnEnable()
    {
        TornLetter.isPlaying += SetIsPlaying;
        TutorialTrigger.inTutorial += SetIsInTutorial;
    }

    private void OnDisable()
    {
        TornLetter.isPlaying -= SetIsPlaying;
        TutorialTrigger.inTutorial -= SetIsInTutorial;
    }
    private void Start()
    {
        bobbing = GetComponent<Bobbing>();
        m_startPos = transform.localPosition;
        m_startRot = transform.localRotation;
        AudioListener.volume = volumeProperty.Value;
        if(m_mouseSensitivity.Value == 0)
        {
            m_mouseSensitivity.Value = 50;
        }
    }

    private void SetIsInTutorial(bool value)
    {
        m_inTutorial = value;
    }

    private void Update()
    {
        if(!m_inTutorial)
        if(!inDialouge.Value && !inInventory.Value && !m_isPlayingJigsaw && m_isInOriginLocation)
        {
            float mouseX = Input.GetAxis("Mouse X") * m_mouseSensitivity.Value * Time.deltaTime;
            float mouseY = Input.GetAxis("Mouse Y") * m_mouseSensitivity.Value * Time.deltaTime;

            m_xRotation -= mouseY;
            m_xRotation = Mathf.Clamp(m_xRotation, m_minXRot, m_maxXRot);

            transform.localRotation = Quaternion.Euler(m_xRotation, 0f, 0f);
            transform.parent.Rotate(Vector3.up, mouseX);
        }
        else if(m_isPlayingJigsaw && m_isInOriginLocation)
        {
            t += Time.deltaTime * m_camChangeSpeed;
            transform.localPosition = Vector3.Lerp(m_startPos, m_jigsawTransform.localPosition, t);
            transform.localRotation = Quaternion.Lerp(m_startRot, m_jigsawTransform.localRotation, t);
            if(t > 1)
            {
                m_isInOriginLocation = false;
                    showTutorial?.Invoke();
            }
        }
        else if(!m_isPlayingJigsaw && !m_isInOriginLocation)
        {
            t2 += Time.deltaTime * m_camChangeSpeed;
            transform.localPosition = Vector3.Lerp(m_jigsawTransform.localPosition, m_startPos, t2);
            transform.localRotation = Quaternion.Lerp(m_jigsawTransform.localRotation, m_startRot, t2);
            if (t2 > 1)
            {
                m_isInOriginLocation = true;
            }
        }
    }

    private void SetIsPlaying(bool value)
    {
        m_isPlayingJigsaw = value;
        if(value)
        {
            Cursor.lockState = CursorLockMode.Confined;
            Cursor.visible = true;
            bobbing.enableBobbing = false;
        }
        else if(!value)
        {
            Cursor.lockState = CursorLockMode.Locked;
            Cursor.visible = false;
            bobbing.enableBobbing = true;
        }
    }
}

      	
      
    

The EnemyController was my teammates script that I took some part in. The functions that i worked on was Attack, Damage and some of the checkAttackDistance.

      
      	
using System;
using UnityEngine;

public class Puzzle : MonoBehaviour
{
    [SerializeField] private GameObject m_correctBox;

    private bool moving;
    private bool finish;

    private float m_startPosY;
    private float m_startPosX;

    private Vector3 resetPosition;

    private void Start()
    {
        resetPosition = this.transform.position;
    }
    private void Update()
    {
        if(finish == false)
        {
            if (moving)
            {
                Vector3 mousePos;
                mousePos = Input.mousePosition;
                mousePos = Camera.main.ScreenToWorldPoint(mousePos);

                this.gameObject.transform.position = new Vector3(mousePos.x - m_startPosX, mousePos.y - m_startPosY, this.gameObject.transform.position.z);
            }
        }
       
    }

    private void OnMouseDown()
    {
        if(Input.GetMouseButtonDown(0))
        {
            Vector3 mousePos;
            mousePos = Input.mousePosition;
            mousePos = Camera.main.ScreenToWorldPoint(mousePos);

            m_startPosX = mousePos.x - this.transform.position.x;
            m_startPosY = mousePos.y - this.transform.position.y;

            moving = true;
        }
    }

    private void OnMouseUp()
    {
        moving = false;

        if(Mathf.Abs(this.transform.position.x - m_correctBox.transform.position.x) <= 0.5f &&
           Mathf.Abs(this.transform.position.y - m_correctBox.transform.position.y) <= 0.5f)
        {
            this.transform.position = new Vector3(m_correctBox.transform.position.x, m_correctBox.transform.position.y, m_correctBox.transform.position.z);
            finish = true;

            GameObject.Find("ItemsToHandle").GetComponent<WinPuzzle>().AddPieces();
        }
        else
        {
            this.transform.position = new Vector3(resetPosition.x, resetPosition.y, resetPosition.z);
        }
        
    }
}
      	
      
    

      
      	
using UnityEngine;
using UnityEngine.SceneManagement;


public class PauseMenu : MonoBehaviour
{
    public GameObject pauseMenuUi;
    [Tooltip("Where the Pause menu should be added")]
    [SerializeField] GameObject m_confirmationObject;
    [Tooltip("The parent object for the menus")]
    [SerializeField] GameObject m_MenuParent;
    [SerializeField] GameObject m_optionsParent;
    [SerializeField] ObeservableBoolProperty inDialouge;
    [SerializeField] ObeservableBoolProperty inInventory;

    public static bool isPaused = false;
    private bool m_isPlaying = false;

    public delegate void DropPiece();
    public static event DropPiece dropPiece;

    private void OnEnable()
    {
        TornLetter.isPlaying += SetIsPlaying;
       
    }

    private void OnDisable()
    {
        TornLetter.isPlaying -= SetIsPlaying;
    
    }

    private void SetIsPlaying(bool value)
    {
        m_isPlaying = value;
    }

    void Update()
    {
        if(!m_isPlaying)
        {
            if (Input.GetKeyDown(KeyCode.Escape))
            {
                if (isPaused)
                {
                    Resume();
                }
                else
                {
                    Pause();
                }
            }
        }
    }

    public void Resume()
    {
        CloseOptions();
        m_MenuParent.SetActive(false);
        m_confirmationObject.gameObject.SetActive(false);
        if (!inInventory.Value && !inDialouge.Value)
        {
            Cursor.visible = false;
            Cursor.lockState = CursorLockMode.Locked;
        }
        pauseMenuUi.SetActive(false);
        Time.timeScale = 1f;
        isPaused = false;


    }

    void Pause()
    {
        m_MenuParent.SetActive(true);
        Cursor.visible = true;
        Cursor.lockState = CursorLockMode.None;
        pauseMenuUi.SetActive(true);
        Time.timeScale = 0f;
        isPaused = true;
    }

    public void ActivateConfirmation()
    {
        m_MenuParent.SetActive(false);
        m_confirmationObject.SetActive(true);
    }

    public void DenyConfirmation()
    {
        m_confirmationObject.SetActive(false);
        m_MenuParent.SetActive(true);
    }

    public void OpenOptions()
    {
        m_MenuParent.SetActive(false);
        m_optionsParent.SetActive(true);
    }

    public void CloseOptions()
    {
        m_optionsParent.SetActive(false);
        m_MenuParent.SetActive(true);
    }

    public void LoadMenu()
    {
        Time.timeScale = 1f;
        SceneManager.LoadScene("MainMenu");
    }

    public void QuitGame()
    {
        Debug.Log("quitting game");
        Application.Quit();
    }
}

      	
      
    

      
      	
using UnityEngine;

public class Bobbing : MonoBehaviour
{
    [Header("Player movement")]

    [Tooltip("Adjusts the speed of the head bobbing")]
    [SerializeField] private float m_bobbingSpeed = 0.18f;
    [Tooltip("Adjusts the speed of the head bobbing while running")]
    [SerializeField] private float m_bobbingRunSpeed = 0.3f;
    [Tooltip("Adjusts the amount of how the head bobbing moves")]
    [SerializeField] private float m_bobbingAmount = 0.2f;
    [Tooltip("Adjusts the amount of how the head bobbing moves while running")]
    [SerializeField] private float m_bobbingAmountRunning = 0.2f;
    [Tooltip("Adjusts the height of the head bobbing")]
    [SerializeField] private float m_midpoint = 2.0f;
    private float m_timer = 0.0f;
    [HideInInspector]
    public bool enableBobbing = true;

    void Update()
    {
        if(enableBobbing)
        {
            float waveslice = 0.0f;
            float horizontal = Input.GetAxis("Horizontal");
            float vertical = Input.GetAxis("Vertical");

            Vector3 cSharpConversion = transform.localPosition;
            if (Mathf.Abs(horizontal) == 0 && Mathf.Abs(vertical) == 0)
            {
                m_timer = 0.0f;
            }
            else
            {
                waveslice = Mathf.Sin(m_timer);
                m_timer = m_timer + m_bobbingSpeed;
                if (m_timer > Mathf.PI * 2)
                {
                    m_timer = m_timer - (Mathf.PI * 2);
                }
            }
            if (waveslice != 0)
            {
                float translateChange = waveslice * m_bobbingAmount;
                float totalAxes = Mathf.Abs(horizontal) + Mathf.Abs(vertical);
                totalAxes = Mathf.Clamp(totalAxes, 0.0f, 1.0f);
                translateChange = totalAxes * translateChange;
                cSharpConversion.y = m_midpoint + translateChange;
            }
            else
            {
                cSharpConversion.y = m_midpoint;
            }

            if (Input.GetKey(KeyCode.LeftShift))
            {
                waveslice = Mathf.Sin(m_timer);
                m_timer = m_timer + m_bobbingRunSpeed;
                if (m_timer > Mathf.PI * 2)
                {
                    m_timer = m_timer - (Mathf.PI * 2);
                }

                float translateChange = waveslice * m_bobbingAmountRunning;
                float totalAxes = Mathf.Abs(horizontal) + Mathf.Abs(vertical);
                totalAxes = Mathf.Clamp(totalAxes, 0.0f, 1.0f);
                translateChange = totalAxes * translateChange;
                cSharpConversion.y = m_midpoint + translateChange;
            }
            transform.localPosition = cSharpConversion;
        }
    }



}