Canyon Climber
Platform
Engine
Language
Development Time
Team Size
PC
Unreal Engine
C#
5 Weeks
17
"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

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.Euler( 0, 0, 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;
}
}
}
}
}