top of page

Canyon Climber

Platform
Engine
Language
Development Time
Team Size

PC
Unreal Engine
C#
5 Weeks

17

The Game:

Itch.io:

"Canyon Climber" is an adrenaline-pumping mobile game where two players embark on an epic journey to conquer towering canyons.

Tethered together by a rope, they must coordinate their movements to ascend swiftly while dodging treacherous obstacles. 

With intuitive controls and breathtaking landscapes, players must rely on communication and teamwork to reach the summit in record time. 

Are you ready to take on the challenge and become the ultimate Canyon Climber?

My Role

I was Lead Programmer
My main responsibilities were:

  • Movement

  • Dynamic UI

  • Grabbing

  • Other

Cover.png

Movement

We get the joystick's horizontal orientation, and if the current player position is further along in the direction of the joystick's orientation then we apply force to the rigidbody in the joystick direction. We apply drag on the rigidbody depending on if it touches something in the ground LayerMask. We also control the speed by overriding the rigidbody velocity if the velocity magnitude exceeds the player's assigned speed.

Code

AddForceJoystickMovement.cs

using UnityEngine;

public class AddForceJoystickMovement : MonoBehaviour
{
    [
SerializeField] private float m_moveSpeed = 10, m_playerHeight = 2, m_groundDrag = 3;

    [SerializeField] private FixedJoystick m_joystick;
    [
SerializeField] private LayerMask m_whatIsGround;
    [
SerializeField] private Transform m_orientation;
    [
SerializeField] private PlayerStartingPosition m_playerStartingPosition;

 

    private Rigidbody2D m_rigidBody;
   
private Vector3 m_flatVelocity;
   
private Vector2 m_moveDirection;
   
private bool m_grounded;
   
private float m_oldCharacterPosition;
    
 
  public float MoveDirectionX { get; private set; }


    private void Start() //Assign the gameobject rigidbody to m_rigindBody, also freeze its rotation
    {
       
m_rigidBody = GetComponent<Rigidbody2D>();
       
m_rigidBody.freezeRotation = true;
        //m_oldCharacterPosition = new Vector2(-6, 0);
        m_oldCharacterPosition = m_playerStartingPosition.StartingPositionX;
    }

    private void Update()
    {

        //Look if player position is on something
        m_grounded = Physics.Raycast(transform.position, Vector3.down, m_playerHeight * 0.5f + 0.2f, m_whatIsGround);

        SpeedControl();

        if (m_grounded) // if not in the air then add drag
        {
           
m_rigidBody.drag = m_groundDrag;
        }
       
else //Remove drag if not on ground
        {
           
m_rigidBody.drag = 0;
        }
    }

    private void FixedUpdate() //Calls for the movement
    {
       
MovePlayer();
    }

    private void MovePlayer() //Movement from joystick converted to a vector 2 that is normalised and used to and force to player
    {
       
m_moveDirection = m_orientation.right * m_joystick.Horizontal;
       
MoveDirectionX = m_moveDirection.x;


        switch (MoveDirectionX)
        {
           
case > 0:
            {
               
if (gameObject.transform.position.x >= m_oldCharacterPosition)
                {
                   
m_rigidBody.AddForce(m_moveDirection.normalized * m_moveSpeed);
                }

                m_oldCharacterPosition = gameObject.transform.position.x;
               
break;
            }
           
case < 0:
            {
               
if (gameObject.transform.position.x <= m_oldCharacterPosition)
                {
                   
m_rigidBody.AddForce(m_moveDirection.normalized * m_moveSpeed);
                }

                m_oldCharacterPosition = gameObject.transform.position.x;
               
break;
            }
        }
    }

    private void SpeedControl()
    {

        //Create a vector for the flat velocity
        m_flatVelocity = new Vector3(m_rigidBody.velocity.x, 0f);

        //Check if player velocity is larger than move speed and reset it to normal speed
        if (m_flatVelocity.magnitude > m_moveSpeed)
        {
           
var m_limitedVelocity = m_flatVelocity.normalized * m_moveSpeed;
           
m_rigidBody.velocity = new Vector3(m_limitedVelocity.x, m_rigidBody.velocity.y);
        }
    }
}

Dynamic UI

Transforming stamina and grabbing-indicator UI elements onto the player position. Decreasing stamina bar according to owning player stamina.

Code

FollowPlayer.cs

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

using UnityEngine;
//using UnityEngine.UIElements;
using UnityEngine.UI;

public class FollowPlayer : MonoBehaviour
{
    [
SerializeField] private Transform m_lookAt;
    [
SerializeField] private GameObject m_staminaBar, m_grabSymbolObj;
    [
SerializeField] private Vector3 m_staminaOffset, m_grabSymbolOffset;
    [
SerializeField] private AudioSource m_audioStamina;
    
    [
SerializeField] private Image m_staminaCircle;
    [
SerializeField] private CanvasGroup m_grabSymbolCanvasGroup;
    [
SerializeField] private float m_fadeOutTime;

    
   
private Camera m_camera;
   
private GrabScriptNew m_grabScriptNew;
   
private PlayerManager m_playerManager;
   
private float m_staminaSoundValue = 0.3f;
   
private bool m_audioPlaying = false;


    
    
 
  private void Awake()
    {
       
m_playerManager = GetComponent<PlayerManager>();
       
m_grabScriptNew = GetComponent<GrabScriptNew>();
    }

    // Start is called before the first frame update
    private void Start()
    {
       
m_camera = Camera.main;
    }

    // Update is called once per frame
    private void Update()
    {
       
FollowPlayerPosition();

        switch (m_grabScriptNew.FreezePlayer) //If not frozen in place play UnGrabbed / If frozen in place play Grabbed
        {
           
case false:
               
UnGrabbed();
               
break;
           
case true:
               
Grabbed();
               
break;
        }
    }

    private void FollowPlayerPosition()
    {
       
Vector3 m_lookAtPosition = m_lookAt.position;


        // Get the Position
        Vector3 m_posStamina = m_camera.WorldToScreenPoint(m_lookAtPosition + m_staminaOffset);

 

        // Get the Position
        Vector3 m_posGrabSymbol = m_camera.WorldToScreenPoint(m_lookAtPosition + m_grabSymbolOffset);

 

        //If not in the right position then go to the correct position

        if (m_staminaBar.transform.position != m_posStamina) 
        {
           
m_staminaBar.transform.position = m_posStamina;
        }

 

        //If not in the right position then go to the correct position
        if (m_grabSymbolObj.transform.position != m_posGrabSymbol) 
        {
           
m_grabSymbolObj.transform.position = m_posGrabSymbol;
        }
    }

    private void Grabbed()
    {

        //Set active if stamina decreses while grabbed
        if (m_staminaCircle.fillAmount < 1)
        {
           
m_staminaBar.SetActive(true);
        }
       
else if (m_staminaCircle.fillAmount <= 0) //If stamina is 0 then UnGrab the Player
        {
           
m_grabScriptNew.UnGrab();
        }

        //Decrese stamina bar in line with the stamina
        m_staminaCircle.fillAmount = m_playerManager.GripStamina / m_playerManager.MaxGripStamina

        StaminaAudio();
        
       
m_grabSymbolObj.SetActive(true); //Turn in Grab Symbol on Player


        if (m_grabSymbolCanvasGroup.alpha > 0) //Decrese the Grab Symbol alpha while grabbed until 0
        {
           
m_grabSymbolCanvasGroup.alpha -= m_fadeOutTime * Time.deltaTime;
        }
    }
    
   
private void UnGrabbed()
    {

        //Sett Grab Symbol Off, Reset its Alpha
        m_grabSymbolObj.SetActive(false);
       
m_grabSymbolCanvasGroup.alpha = 1;

        //Remove the Stamina Circle if Stamina maxed
        if (m_staminaCircle.fillAmount >= 1)
        {
           
m_staminaBar.SetActive(false);
        }
       
else //Regain Stamina when not grabbed
        {
           
m_staminaCircle.fillAmount = m_playerManager.GripStamina / m_playerManager.MaxGripStamina;
        }

        StaminaAudio();
    }
    
   
private void StaminaAudio()
    {
       
if ((m_staminaCircle.fillAmount <= 0 || m_staminaCircle.fillAmount > m_staminaSoundValue) && m_audioPlaying)
        {
           
m_audioStamina.Stop();
           
m_audioPlaying = false;
        } 
       
else if (m_staminaCircle.fillAmount > 0 && m_staminaCircle.fillAmount <= m_staminaSoundValue && !m_audioPlaying)
        {
           
m_audioStamina.Play();
           
m_audioPlaying = true;
        } 
    }
}

Code

GrabButtonColorFade.cs

using UnityEngine;

public class GrabButtonColorFade : MonoBehaviour
{
    //[SerializeField] private CanvasGroup m_grabButtonOnPlayerCanvasGroup;
    [SerializeField] private CanvasGroup m_grabButtonOnSideCanvasGroup;
    [
SerializeField] private GameObject m_grabButtGameObject;
    [
SerializeField] private CanvasGroup m_UnGrabCanvasGroup;
    [
SerializeField] private GameObject m_UnGrabGameObject;
    [
SerializeField] private float m_fadeOutTime;

    private GrabScriptNew m_grabScriptNew;
   
private bool m_showButton = false;

    
    

    private void Awake()
    {
       
m_grabScriptNew = GetComponent<GrabScriptNew>();
    }
    
   
private void Update()
    {

        //var alpha = m_grabButtonOnPlayerCanvasGroup.alpha;
        if (!m_grabScriptNew.FreezePlayer)
        {

            //m_grabButtonOnPlayerCanvasGroup.alpha = 0;
            m_grabButtonOnSideCanvasGroup.alpha = 1;
           
m_UnGrabCanvasGroup.alpha = 0;
            
           
m_grabButtGameObject.SetActive(true);
           
m_UnGrabGameObject.SetActive(false);

            
           
m_showButton = false;
        }
       
else if (m_grabScriptNew.FreezePlayer && m_UnGrabCanvasGroup.alpha <= 0)
        {
           
m_grabButtonOnSideCanvasGroup.alpha = 0;
           
m_UnGrabCanvasGroup.alpha = 1;
            
           
m_grabButtGameObject.SetActive(false);
           
m_UnGrabGameObject.SetActive(true);
            
        }

        else if (m_grabScriptNew.FreezePlayer && m_showButton == false)
        {

            //m_grabButtonOnPlayerCanvasGroup.alpha = 1;
            m_showButton = true;
        }

    }
}

Grabbing

Grabbing onto the background rocks by freezing the player's rigidbody position and rotation. Letting go of the rock by removing rigidbody constraints.

Code

GrabScriptNew.cs

using UnityEngine;

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;


public class GrabScriptNew : MonoBehaviour
{
    [
SerializeField] private Rigidbody2D m_player;
    [
SerializeField] private float m_letGoWait = 1;
    [
SerializeField] private AudioSource m_audioGrab, m_audioUnGrab;


    private PlayerManager m_playerManager;
   
private bool m_freezePlayer, m_haveGrabbed;
   
private float m_timeToHold;

    public bool HaveGrabbed
    { 
       
get { return m_haveGrabbed; }
       
set m_haveGrabbed = value; }
    }

    public bool FreezePlayer
    {
       
get { return m_freezePlayer; }
    }

    private void Awake()
    {

        //Get Component and set time after one grab on you can first let go
        m_playerManager = GetComponent<PlayerManager>();
       
m_timeToHold = m_letGoWait;
       
m_haveGrabbed = false;
    }

    private void Update()
    {

// If one is grabbed on and stamina runs out then they let go.

        if (m_playerManager.CanGrab == false || m_playerManager.IsTouchingGrabbableObj == false ||

            m_playerManager.GripStamina <= 0)
        {
            //m_player.constraints = RigidbodyConstraints2D.FreezeRotation;
            m_player.constraints = RigidbodyConstraints2D.None;
           
m_freezePlayer = false;
           
m_playerManager.IsGrabing = false;
        }

        if (m_playerManager.GripStamina <= 0) m_audioUnGrab.Play();

        if (m_freezePlayer && m_letGoWait > 0) //Time until one can let go after grabbing
        {
           
m_letGoWait -= Time.deltaTime;
        }
    }

    public void Grab()
    {

        //Check if one can grab
        if (m_playerManager.CanGrab && m_playerManager.IsTouchingGrabbableObj && m_freezePlayer == false
        {
           
m_haveGrabbed = true;
           
m_letGoWait = m_timeToHold;
           
m_player.constraints = RigidbodyConstraints2D.FreezePositionX | RigidbodyConstraints2D.FreezePositionY |                  RigidbodyConstraints2D.FreezeRotation;

            m_freezePlayer = true;
           
m_playerManager.IsGrabing = true;
           
m_audioGrab.Play();
        }
    }

 


    public void UnGrab()
    {

        //Check if one can grab
        if (m_playerManager.CanGrab && m_playerManager.IsTouchingGrabbableObj && m_freezePlayer && m_letGoWait <= 0
        {
           
m_haveGrabbed = true;
            //m_player.constraints = RigidbodyConstraints2D.FreezeRotation;
            m_player.constraints = RigidbodyConstraints2D.None;
           
m_freezePlayer = false;
           
m_playerManager.IsGrabing = false;
           
m_audioUnGrab.Play();
        }
    }


}

Miscellaneous

BirdRandomSounds: Play random sounds at random intervals.

​WindScript: Wind pushes the player into either left or right.

Code

WindScript.cs

using System;
using UnityEngine;
using Random = System.Random;

public class WindScript : MonoBehaviour
{
    [
SerializeField] private AreaEffector2D m_areaEffector2D;
    [
SerializeField] private AudioSource m_audioWind;
    [
SerializeField] private ParticleSystem m_WindParticles;
    [
SerializeField] private float m_activeTime = 3;
    

    private float m_time;
   
private readonly int m_rightDirection = 90, m_leftDirection = 270;
   
private readonly int m_particleLeftDirection = 180, m_particleRightDirection = 0;
   
private readonly Random m_randomDirection = new Random();
    

    private void OnEnable()
    {
       
m_time = m_activeTime;
       
var directionChoice = m_randomDirection.Next(1, 3);
       
m_audioWind.Play();
       
switch (directionChoice)
        {
           
case 1:
               
m_areaEffector2D.forceAngle = m_leftDirection;
               
Instantiate(m_WindParticles, transform.position, Quaternion.Euler00, m_particleRightDirection));
               
break;
           
case 2:
               
m_areaEffector2D.forceAngle = m_rightDirection;
               
Instantiate(m_WindParticles, transform.position, Quaternion.Euler( 0, 0, m_particleLeftDirection));
               
break;
        }

    }

    private void Update()
    {
       
if (m_time > 0)
        {
           
m_time -= Time.deltaTime;
        }
       
if (m_time <= 0)
        {
           
m_WindParticles.Stop();
           
m_audioWind.Stop();
           
gameObject.transform.position = new Vector3(1000, 1000, 3);
           
gameObject.SetActive(false);
        }
    }
}

BirdRandomSounds.cs

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

using UnityEngine;
using Random = System.Random;

public class BirdRandomSounds : MonoBehaviour
{
    [
SerializeField] private AudioSource m_audioBird1, m_audioBird2;
    [
SerializeField] private int m_minimTimeForSound = 20, m_maximumTimeForSound = 40;
    [
SerializeField] private float m_spawnTime;

    private readonly Random m_randomBirdSounds = new Random();
   
private bool m_randoDone = false;

    private void Update()
    {
       
if (gameObject.activeSelf)
        {
           
if (!m_randoDone)
            {

                int m_spawnTimeInt = m_randomBirdSounds.Next(m_minimTimeForSound, m_maximumTimeForSound);
               
m_spawnTime = (float)m_spawnTimeInt;
               
m_randoDone = true;
            }

            if (m_spawnTime > 0 && m_randoDone)
            {
               
m_spawnTime -= Time.deltaTime;
            }

            if (m_spawnTime <= 0 && m_randoDone)
            {
               
var m_BirdSoundsNr = m_randomBirdSounds.Next(1, 3);
               
switch (m_BirdSoundsNr)
                {
                   
case 1:
                       
m_audioBird1.Play();
                       
m_randoDone = false;
                       
break;
                   
case 2:
                       
m_audioBird2.Play();
                       
m_randoDone = false;
                       
break;
                }
            }
        }
    }
}

Contact me

070 415 20 61

Stockholm, Sweden

bottom of page