DEVLOG 2 – MONDAY 13TH FEB

To start off this week, I began consolidating the areas the player would be able to explore in my game. As I mentioned previously, I plan on creating two areas, the Caverns (Brown) and the Forest (Green). To help visualise the timeline of the game, I outlined the critical path. The Village (Light Blue) is the intended starting area of the player. From there, the player will set out to the Water Guardian’s Shrine in hopes of reaching out to them to try and cure the worlds blight. On their way to the shrine, players will pick up basic movement and combat knowledge. I also intend to include a Shrine in Room 7 as an extra means of healing if the player becomes too damaged, and to include more exploration. From Room 11, players can then travel to the Forest area, where they can explore and make their way to the Forest Guardian. I’ve also included a secret area in Room 11 where players can unlock a double jump, as reward for exploration, and to teach players about breakable walls. However, looking at this map, and considering what I’ve learned from my previous research, the critical path is too linear, and so I should think about adding more rooms for further exploration if time allows it.

I’ve also finished the bulk of the Caverns assets. These were a joy to work on and I can see the improvement from the first batch of assets I created, which means I may have to go back and update those so they fit better with the overall style of the environment, but for now I want to keep working on decorating the rooms I’ve got in Unity. I feel as though I may need to consider making more assets, however, as these may become too monotonous given the scope of the Caverns environment.

Player Movement has been an important aspect to get right, given the Metroidvania-like nature of my game. The script has also been through a few developments. For example, I had originally only defined the speed, maxMoveSpeed, and jumpingPower of the player. However, thanks to the help of some more tutorials, I’ve learned varying techniques I can use to improve the movement of my player.

When making a platformer, the jump of the player is paramount. The arc of the jump should be altered so that the player has a lot of airtime, but then falls back down to the ground immediately, creating a snappy and tight jump. Therefore, I defined the fallMultiplier, lowJumpFallMultiplier and the airLinearDrag, to create a tight jump movement.

Coyote Time is also a useful feature to implement into the player’s jump. Named after the iconic Looney Tunes duo, coyote time lets the player jump after a small time that they’ve left a platform. This makes the movement a little less punishing for the player, and overall helps the game to feel better and fairer.

Similarly, jump buffering also ensures a fairer and better feeling experience for the player. Before, the player would only be able to jump the moment after their character hit the ground again. However, with jump buffering, the game registers the jump input a few moments before the player hits the ground, and is able to register that input and jump again, even though the player hadn’t pressed that input the moment after they hit the ground.

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

public class PlayerMovement : MonoBehaviour
{
    private float horizontal;
    [SerializeField] private float speed = 8f;
    [SerializeField] private float maxMoveSpeed = 12f;
    [SerializeField] private float jumpingPower = 12f;
    [SerializeField] private float fallMultiplier = 8.5f;
    [SerializeField] private float lowJumpFallMultiplier = 4.5f;
    [SerializeField] private float airLinearDrag = 1.5f;

    [SerializeField] private float coyoteTime = 0.2f;
    private float coyoteTimeCounter;

    [SerializeField] private float jumpBufferTime = 0.2f;
    private float jumpBufferCounter;

    private bool isFacingRight = true;

    // Reference game objects and components in Unity
    [SerializeField] private Rigidbody2D rb;
    [SerializeField] private Transform groundCheck;
    [SerializeField] private LayerMask groundLayer;

    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        horizontal = Input.GetAxisRaw("Horizontal"); //Returns a value of -1, 0 or 1 depending on the direction of movement

        if (IsGrounded())
        {
            coyoteTimeCounter = coyoteTime;
        }
        else
        {
        coyoteTimeCounter -= Time.deltaTime;
        }

        if (Input.GetButtonDown("Jump"))
        {
            jumpBufferCounter = jumpBufferTime;
        }
        else
        {
        jumpBufferCounter -= Time.deltaTime;
        }

        if (jumpBufferCounter > 0f && coyoteTimeCounter > 0f)
        {
            rb.velocity = new Vector2(rb.velocity.x, jumpingPower);

            jumpBufferCounter = 0f;
        }
        else
        {
            ApplyAirLinearDrag();
            FallMultiplier();
        }

        if (Input.GetButtonUp("Jump") && rb.velocity.y > 0f)
        {
            // Lets player jump higher and longer by holding down button
            rb.velocity = new Vector2(rb.velocity.x, rb.velocity.y * 0.2f);

            coyoteTimeCounter = 0F;
        }

        Flip();
    }

    private void FixedUpdate()
    {
        rb.velocity = new Vector2(horizontal * speed, rb.velocity.y);
        if (Mathf.Abs(rb.velocity.x) > maxMoveSpeed) //If the player's speed exceeds that of the maxMoveSpeed
        {
            rb.velocity = new Vector2(Mathf.Sign(rb.velocity.x) * maxMoveSpeed, rb.velocity.y); //Clamp it
        }
}

    private bool IsGrounded()
    {
        return Physics2D.OverlapCircle(groundCheck.position, 0.2f, groundLayer); //(position of groundCheck, small radius, groundLayer), creates a small circle at the player's feet which when in contact with the ground, lets the player jump
    }

    private void Flip()
    {
        if (isFacingRight && horizontal < 0f || !isFacingRight && horizontal > 0f)
        {
            isFacingRight = !isFacingRight;
            Vector3 localScale = transform.localScale;
            localScale.x *= -1f;
            transform.localScale = localScale;
        }
    }

    private void ApplyAirLinearDrag()
    {
        rb.drag = airLinearDrag;
    }

private void FallMultiplier()
    {
        if (rb.velocity.y < 0)
        {
            rb.gravityScale = fallMultiplier;
        }
        else if (rb.velocity.y > 0 && Input.GetButtonDown("Jump"))
        {
            rb.gravityScale = lowJumpFallMultiplier;
        }
        else
        {
            rb.gravityScale = 1f;
        }
    }
}

In order for the player to move between levels, I had to create a level (or scene) manager. To do this. I utilised what’s called a scriptable object, which is a container that can store large amounts of data.

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

public class LevelMove : MonoBehaviour
{
public int sceneBuildIndex;

[SerializeField]
private GameObject player;

[SerializeField]
private LevelConnection connection;

[SerializeField]
private Transform spawnPoint;

private void Start()
{
    player = GameObject.FindWithTag("Player");
    if(connection == LevelConnection.ActiveConnection)
    {
       // FindObjectOfType<player>().transform.position = spawnPoint.position;
        player.transform.position = spawnPoint.position;

    }
}

private void OnTriggerEnter2D(Collider2D other)
{
    LevelConnection.ActiveConnection = connection;
    print("Trigger Entered");

    if(other.tag == "Player")
        {
            print("Switching scene to " + sceneBuildIndex);
            SceneManager.LoadScene(sceneBuildIndex, LoadSceneMode.Single);
            //Player.transform.position = spawnPoint.position;
        }
}

So that the player couldn’t see outside of the area I wanted them to see, I downloaded a plug-in for Unity called Cinemachine. This has allowed me to define the bounds for each individual area, so that the camera is unable to go beyond those bounds. Cinemachine also comes with a host of other features, such as being able to add dampening so the camera follows the player more smoothly, or changing the amount of what the player can see at a given time.

I defined the boundaries for each area by creating an empty gameObject, and giving it a Polygon Collider 2D. By editing this shape around the entire scene, I can define the space that the camera is able to move around in, and prevent it from leaving that area.

Leave a Comment

Your email address will not be published. Required fields are marked *