Category: Unity3D

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.

Advertisements

A Very Simple Movement Script In Unity

A good way to explain the basics of Unity scripting is to make a very simple script that simply takes the A and D or left and right arrow keys inputs and moves the gameobject with those values. Let’s take a look at the following example.


using UnityEngine;
using System.Collections;

namespace PlatformerExample.Scripts {

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

      private Rigidbody2D m_Rigidbody;
      private float horizontalInput;
      [SerializeField] private float movementSpeedMultiplier = 20f;

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

      // Update is called once per frame
      void Update ()
      {
         // Collect inputs here
         horizontalInput = Input.GetAxis("Horizontal");
      }

      // Do physics related operations here
      void FixedUpdate()
      {
         // Now the input values are used for altering the velocity of the rigidbody
         m_Rigidbody.velocity = new Vector3(
            horizontalInput * movementSpeedMultiplier,
            m_Rigidbody.velocity.y,
            0
         );
      }
   }
}

At the top of the file, we import our libraries. Most important of these is the UnityEngine -library. Without it, we don’t have access to Unity Engine’s various classes.

Next we have the line with RequireComponent. This is used to prevent situations in which there isn’t a RigidBody 2D -component attached to the gameobject. The Unity editor prevents this script from being attached to a gameobject without a RigidBody 2D -component.

In the Start() -method we assign a reference of the RigidBody 2D -component that is attached to the gameobject to the m_RigidBody variable.

In Update() -method, which is run every frame, we read the inputs. In this case, we want the horizontal inputs, in other words the inputs from whatever controls are bound to the horizontal axis. By default, these are A, D, left arrow and right arrow keys on the keyboard. We store the value of this axis to horizontalInput. The limits for this value are -1 and 1, so the values are always something from between. When left arrow is pressed down, the value is -1, and for right arrow it is 1.

We then apply this value to the velocity of the RigidBody in the FixedUpdate() -method. Unlike the Update() -method, FixedUpdate() isn’t dependent on the framerate, but rather operates based on real world time, which makes it ideal for physics related tasks. In the script, a new Vector3 is assigned on every loop to RigidBody.velocity. The Vector3 consists of 3 values: X, Y and Z. As this script is for 2D, we ignore the Z -axis, and assign a 0 to it. The X value is the horizontal input multiplied by the movement speed multiplier, which gives the movement the necessary speed increase. The Y value is the current Y-axis velocity. If that is not assigned to Y value, it causes a “floaty” effect. That is due to the effect of gravity being overwritten. So to retain the effect of gravity, we assign the current Y-axis velocity to the new Y-axis velocity.

That is the script “in a nutshell”.

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.