Tag: rigidbody

Simple Player Controller Script In Unity

Possibly the simplest game genre to make is the 2D platformer (by now somewhat infamous for that). Still, the character controller for the player character in a 2D platformer can be tricky to implement.

What makes this particular rather simple script be more complicated is the Finite State Machine that it implements. At this point, the state machine prevents player from jumping in mid-air, or basically prevents the player character from existing in multiple states simultaneously. The state machine however makes expansion of the character’s capabilities simple, if such is desired. For instance, if the player character should be able to slide, a slide state simply is made, and all slide related functionality is added to the state.

The Player Controller:

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

[RequireComponent(typeof(Rigidbody2D))]
public class SimplePlayerController : MonoBehaviour {

    public Rigidbody2D myRigidbody { get; private set; }
    [SerializeField] private LayerMask floorLayer;
    
    private IState currentState;
    private Dictionary<string, IState> states;
    public static string IdleState = "Idle";
    public static string RunState = "Run";
    public static string JumpState = "Jump";

    private float horizontalInput; // by default, A and D keys
    private float movement; // the actual movement, movementSpeed multiplied by horizontalInput
    public float movementSpeed = 20f; // the speed multiplier
    public float jumpVelocity = 30f; // the velocity value that is added to the current velocity vector when player jumps

	// Use this for initialization
	void Start () {
        myRigidbody = GetComponent<Rigidbody2D>();

        // instantiate dictionary and add states
        states = new Dictionary<string, IState>();
        states.Add(IdleState, new PlayerIdleState());
        states.Add(RunState, new PlayerRunState());
        states.Add(JumpState, new PlayerJumpState());

        if (floorLayer == 0) // if floor layer has not been set in the inspector, set it to layer named Floor
        {
            floorLayer = LayerMask.GetMask("Floor");
        }

        // set the current state to Idle
        currentState = states[IdleState];
        currentState.OnEnter(this);
	}
	
	// Update is called once per frame
	void Update () {
        horizontalInput = Input.GetAxis("Horizontal"); // collect horizontal Input
        if (currentState is PlayerRunState) // multiply movementSpeed with horizontalInput if Running
        {
            movement = movementSpeed * horizontalInput;
        }
        currentState.OnUpdate(this); // state update
	}

    void FixedUpdate()
    {
        myRigidbody.velocity = new Vector2(movement, myRigidbody.velocity.y);
    }

    public void Transition(string nextState)
    {
        IState endState = states[nextState];
        endState.OnEnter(this);
        currentState.OnExit(this);
        currentState = endState;
    }

    // check if player is on ground
    public bool IsGrounded()
    {
        // cast a 2D ray downwards with distance of 2f, accept collision only if the object is on floor layer
        if (Physics2D.Raycast(transform.position, Vector2.down, 2f, floorLayer)) 
        {
            Debug.Log("Is on ground");
            return true;
        }
        return false;
    }

    // called from outside the controller, from the jump state (when entered)
    public void Jump()
    {
        myRigidbody.velocity += new Vector2(0, jumpVelocity);
    }
}

The State interface that is implemented by all Player States:

public interface IState
{
    void OnUpdate(SimplePlayerController fsm);
    void OnEnter(SimplePlayerController fsm);
    void OnExit(SimplePlayerController fsm);
}

The Player Idle State:

using System;
using UnityEngine;

public class PlayerIdleState : IState
{
    public void OnUpdate(SimplePlayerController fsm)
    {
        if (Mathf.Abs(Input.GetAxis("Horizontal")) > 0.0001f)
        {
            fsm.Transition(SimplePlayerController.RunState);
        }
        if (Input.GetButtonDown("Jump") && fsm.IsGrounded())
        {
            fsm.Transition(SimplePlayerController.JumpState);
        }
    }

    public void OnEnter(SimplePlayerController fsm)
    {
        Debug.Log("Idle State Enter");
    }

    public void OnExit(SimplePlayerController fsm)
    {
        Debug.Log("Idle State Exit");
    }
}

The Player Run State:

using System;
using UnityEngine;

public class PlayerRunState : IState
{
    public void OnUpdate(SimplePlayerController fsm)
    {
        if (Mathf.Abs(Input.GetAxis("Horizontal")) < 0.0001f)
        {
            fsm.Transition(SimplePlayerController.IdleState);
        }
        if (Input.GetButtonDown("Jump") && fsm.IsGrounded())
        {
            fsm.Transition(SimplePlayerController.JumpState);
        }
    }

    public void OnEnter(SimplePlayerController fsm)
    {
        Debug.Log("Run State Enter");
    }

    public void OnExit(SimplePlayerController fsm)
    {
        Debug.Log("Run State Exit");
    }
}

The Player Jump State:

using System;
using UnityEngine;

public class PlayerJumpState : IState
{
    public void OnUpdate(SimplePlayerController fsm)
    {
        if (fsm.IsGrounded())
        {
            // transition back to idle when back on ground
            fsm.Transition(SimplePlayerController.IdleState);
        }
    }

    public void OnEnter(SimplePlayerController fsm)
    {
        fsm.Jump();
        Debug.Log("Jump State Enter");
    }

    public void OnExit(SimplePlayerController fsm)
    {
        Debug.Log("Jump State Exit");
    }
}

You can download all the code and a sample level from here.
All the amazing art that is in the package is done by Kenney. You can download his Platformer Asset Pack Redux from here.