Retaken

A 2.5D side-scrolling platformer inspired by Celeste

5 min. read

Retaken: Made by Team Citrus

Retaken is a student project developed by a team of 4. The goal for the four of us who were working on this project was to strengthen our knowledge in the respective disciplines that we were studying. My goal as the gameplay programmer was to make a platforming game that is very easy to control and hard to master.

My Role

We wanted to make a game that is very easy to control, hard to master, yet fun to play. A lot of my time was dedicated to polishing the gameplay, but I also enjoyed helping out with the environment art and level design.

It’s funny to think that we overscoped the project heavily. We never really got the chance to include things such as enemies, bosses, and a 3-act structure to the game. We came into this project very motivated to make an awesome game - but due to the lack of time and overburden of assignments, we had to cut out many features that we didn’t want to cut out. It was a blast working on the game (and pulling a few all-nighters).

The Three C’s

70% of the development cycle was me trying to nail down the three C’s. Character, Camera, and Control. It was really fun tweaking values and getting the “feeling” right. For the most part, people really enjoyed controlling the character, but one thing I deprived was the camera. Because I had not realized how crucial the camera was, we had to adjust some of the level design to fit the new system late in development. It was also a surprise to learn that I would spend a lot of time adjusting the camera - but in the end, it was a good lesson to never forget about the 2nd C!

Movement

Character Class

This is the base Character class that every other Character will be inheriting from.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Character : MonoBehaviour
{
/// <summary>
/// </summary>
protected SpriteRenderer _sr;

/// <summary>
/// </summary>
protected void Awake()
{
_rb = GetComponent<Rigidbody>();
_c = GetComponent<CapsuleCollider>();
_sr = GetComponent<SpriteRenderer>();
_as = GetComponent<AudioSource>();
_a = GetComponent<Animator>();
_health = GetComponent<Health>();
_health.OnDeath.AddListener(Death);
_health.OnDamage.AddListener(Damaged);
_rb.useGravity = false;
}
}

IrisControl class

I am not proud of this script, but overall I learned how to structure my code more better for Unity. It would’ve been more wise to put the Move Input and Animator stuff into their own methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class IrisControl : Character
{
/// <summary>
/// </summary>
private void Update()
{
// Ignore Update when game is paused.
if (Time.timeScale == 0)
return;

// Get movement input
_moveInput = Input.GetAxis("Horizontal");

// Handle Look Direction
if (_moveInput < 0 && !_sr.flipX)
_sr.flipX = true;
else if (_moveInput > 0 && _sr.flipX)
_sr.flipX = false;

// Handle Player Mechanics
HandleWallCollision();
HandleJumpGravity();
HandleDashCooldown();
HandleDash();

// Handle Jump Input
if (Input.GetButtonDown("UseJump"))
TryPressJump();
else if (Input.GetButtonUp("UseJump"))
TryReleaseJump();

// Handle Fast Fall
if (Input.GetAxis("Vertical") > CONTROLLER_ANALOG_THRESHOLD)
{
if (!_alreadyAxisDown)
{
_alreadyAxisDown = true;
TryFastFall();
}
}
else
_alreadyAxisDown = false;

// Handle Dash Input
if (Input.GetButtonDown("UseDash"))
TryPressDash();

// Handle animator
_a.SetFloat("velocity", Mathf.Abs(_rb.velocity.x));
_a.SetBool("isJumping", !_touchingGround);
_a.SetBool("isDashing", _isDashing);

// Check Kill Y
if (transform.position.y <= KillY)
Death();
}
}

Camera

Player Camera class

I’m kinda glad I didn’t use Cinemachine, it was fun programming my own camera.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class PlayerCamera : MonoBehaviour
{
public const float CAMERA_DISTANCE_X = 22f;
public const float CAMERA_DISTANCE_Y = 10.5f;
public static PlayerCamera Instance { get; private set; }
private readonly float _deltaTolerance = 0.85f;
private readonly float _deltaThreshold = 3.8f;
public Vector3 CameraOffset;
public Vector3 DefaultOffset;
public GameObject Target;
public Vector3 BoundaryMin;
public Vector3 BoundaryMax;

/// <summary>
/// </summary>
private void Awake() => Instance = this;

/// <summary>
/// </summary>
private void LateUpdate ()
{
if (Target)
{
var camPos = transform.position;
camPos.z = Target.transform.position.z;
camPos += CameraOffset;

var targetPos = Target.transform.position;
targetPos.x = Mathf.Max(BoundaryMin.x + CameraOffset.x, Mathf.Min(BoundaryMax.x - CameraOffset.x, targetPos.x));
targetPos.y = Mathf.Max(BoundaryMin.y + CameraOffset.y, Mathf.Min(BoundaryMax.y - CameraOffset.y, targetPos.y));

var dir = (targetPos - camPos);
var speed = Mathf.Min(Mathf.Pow(Mathf.Max(dir.magnitude - _deltaTolerance, 0) / _deltaThreshold, 8f) * Time.fixedTime, 1f);
var aim = transform.position + dir * speed;
transform.position = Vector3.Lerp( transform.position, aim, 0.15f);
}
}
}