How to make player with controller script slip on a surface

I want to make the player slip when they step on the slippery surface (light blue colored). I first attempted it with a Physics Material 2D (w/ 0 friction and bounciness) but since I’m directly changing the player’s RigidBody velocity in the player controller script when they move, the surface has no effect on them, so the player simply walks like usual. I tried several other things but to no avail. Any way to make it work?

PlayerController.cs

// Class which handles player movement
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController : MonoBehaviour
{
    [Header("Game Object and Component References")]
    [Tooltip("The Input Manager component used to gather player input.")]
    public InputManager inputManager = null;
    [Tooltip("The Ground Check component used to check whether this player is grounded currently.")]
    public GroundCheck groundCheck = null;
    [Tooltip("The sprite renderer that represents the player.")]
    public SpriteRenderer spriteRenderer = null;
    [Tooltip("The health component attached to the player.")]
    public Health playerHealth;

    // The rigidbody used to move the player (necessary for this component, so not made public)
    private Rigidbody2D playerRigidbody = null;

    // Enum to help determine which direction the player is facing.
    public enum PlayerDirection
    {
        Right,
        Left
    }

    // Which way the player is facing right now
    public PlayerDirection facing
    {
        get
        {
            if (horizontalMovementInput > 0)
            {
                return PlayerDirection.Right;
            }
            else if (horizontalMovementInput < 0)
            {
                return PlayerDirection.Left;
            }
            else
            {
                if (spriteRenderer != null && spriteRenderer.flipX == true)
                    return PlayerDirection.Left;
                return PlayerDirection.Right;
            }
        }
    }

    // Whether this player is grounded false if no ground check component assigned
    public bool grounded
    {
        get
        {
            if (groundCheck != null)
            {
                return groundCheck.CheckGrounded();
            }
            else
            {
                return false;
            }
        }
    }

    // The horizontal movement input collected from the input manager
    public float horizontalMovementInput
    {
        get
        {
            if (inputManager != null)
                return inputManager.horizontalMovement;
            else
                return 0;
        }
    }
    // the jump input collected from the input manager
    public bool jumpInput
    {
        get
        {
            if (inputManager != null)
                return inputManager.jumpStarted;
            else
                return false;
        }
    }

    [Header("Movement Settings")]
    [Tooltip("The speed at which to move the player horizontally")]
    public float movementSpeed = 4.0f;

    [Header("Jump Settings")]
    [Tooltip("The force with which the player jumps.")]
    public float jumpPower = 10.0f;
    [Tooltip("The number of jumps that the player is alowed to make.")]
    public int allowedJumps = 1;
    [Tooltip("The duration that the player spends in the \"jump\" state")]
    public float jumpDuration = 0.1f;
    [Tooltip("The effect to spawn when the player jumps")]
    public GameObject jumpEffect = null;
    [Tooltip("Layers to pass through when moving upwards")]
    public List<string> passThroughLayers = new List<string>();

    // The number of times this player has jumped since being grounded
    private int timesJumped = 0;
    // Whether the player is in the middle of a jump right now
    private bool jumping = false;

    // Enum used for categorizing the player's state
    public enum PlayerState
    {
        Idle,
        Walk,
        Jump,
        Fall,
        Dead
    }

    [HideInInspector]
    // The player's current state (walking, idle, jumping, falling, or dead)
    public PlayerState state = PlayerState.Idle;

    private void Start()
    {
        SetupRigidbody();
        SetUpInputManager();
    }

    private void LateUpdate()
    {
        ProcessInput();
        HandleSpriteDirection();
        DetermineState();
    }

    private void ProcessInput()
    {
        HandleMovementInput();
        HandleJumpInput();
    }

    // Handles movement input
    private void HandleMovementInput()
    {
        Vector2 movementForce = Vector2.zero;
        if (Mathf.Abs(horizontalMovementInput) > 0 && state != PlayerState.Dead)
        {
            movementForce = horizontalMovementInput * movementSpeed * transform.right;
        }
        MovePlayer(movementForce);
    }

    // Moves the player with a specified force
    private void MovePlayer(Vector2 movementForce)
    {
        float horizontalVelocity = movementForce.x;
        float verticalVelocity = (grounded && !jumping) ? 0 : playerRigidbody.velocity.y;
        playerRigidbody.velocity = new Vector2(horizontalVelocity, verticalVelocity);

        if (playerRigidbody.velocity.y > 0)
        {
            foreach (string layerName in passThroughLayers)
            {
                Physics2D.IgnoreLayerCollision(LayerMask.NameToLayer("Player"), LayerMask.NameToLayer(layerName), true);
            } 
        }
        else
        {
            foreach (string layerName in passThroughLayers)
            {
                Physics2D.IgnoreLayerCollision(LayerMask.NameToLayer("Player"), LayerMask.NameToLayer(layerName), false);
            }
        }
    }

    // Handles jump input
    private void HandleJumpInput()
    {
        if (jumpInput)
        {
            StartCoroutine(Jump(1.0f));
        }
    }

    // Coroutine which causes the player to jump.
    private IEnumerator Jump(float powerMultiplier = 1.0f)
    {
        if (timesJumped < allowedJumps && state != PlayerState.Dead)
        {
            jumping = true;
            float time = 0;
            SpawnJumpEffect();
            playerRigidbody.velocity = new Vector2(playerRigidbody.velocity.x, 0);
            playerRigidbody.AddForce(jumpPower * powerMultiplier * transform.up, ForceMode2D.Impulse);
            timesJumped++;
            while (time < jumpDuration)
            {
                yield return null;
                time += Time.deltaTime;
            }
            jumping = false;
        }
    }

    // Spawns the effect that occurs when the player jumps
    private void SpawnJumpEffect()
    {
        if (jumpEffect != null)
        {
            Instantiate(jumpEffect, transform.position, transform.rotation, null);
        }
    }

    // Bounces the player upwards, refunding jumps.
    public void Bounce()
    {
        timesJumped = 0;
        if (inputManager.jumpHeld)
        {
            StartCoroutine(Jump(1.5f));
        }
        else
        {
            StartCoroutine(Jump(1.0f));
        }
    }

    // Determines which way the player should be facing, then makes them face in that direction
    private void HandleSpriteDirection()
    {
        if (spriteRenderer != null)
        {
            if (facing == PlayerDirection.Left)
            {
                spriteRenderer.flipX = true;
            }
            else
            {
                spriteRenderer.flipX = false;
            }
        }
    }

    // Gets and returns the player's current state
    private PlayerState GetState()
    {
        return state;
    }

    // Sets the player's current state
    private void SetState(PlayerState newState)
    {
        state = newState;
    }

    // Determines which state is appropriate for the player currently
    private void DetermineState()
    {
        if (playerHealth.currentHealth <= 0)
        {
            SetState(PlayerState.Dead);
        }
        else if (grounded)
        {
            if (Mathf.Abs(horizontalMovementInput) > 0)
            {
                SetState(PlayerState.Walk);
            }
            else
            {
                SetState(PlayerState.Idle);
            }
            if (!jumping)
            {
                timesJumped = 0;
            }
        }
        else
        {
            if (jumping)
            {
                SetState(PlayerState.Jump);
            }
            else
            {
                SetState(PlayerState.Fall);
            }
        }
    }

    // Sets up the player's rigidbody
    private void SetupRigidbody()
    {
        if (playerRigidbody == null)
        {
            playerRigidbody = GetComponent<Rigidbody2D>();
        }
    }

    // Gets the reference to the input manager
    private void SetUpInputManager()
    {
        inputManager = InputManager.instance;
        if (inputManager == null)
        {
            Debug.LogError("There is no InputManager set up in the scene for the PlayerController to read from");
        }
    }
}

Usually you just either inhibit or greatly reduce input on a slippery surface:

  • inhibit : just do not allow left/right control when on the surface
  • greatly reduce : allow a fraction of the input to get through to change velocity.

I think I was trying for the second option here. Basically a frictionless surface, so the player will be in constant sliding motion but the input can allow for changes in magnitude and direction of velocity still.

I actually managed to make it work with the first option… by simply skipping ProcessInput() when the player moves and collides with this surface, that way it slides across the entire surface without any additional input. Though, I’d also like to know if there’s a way to do it with some input still…

I think you would simply need to change line 15 above, this line:

Instead of just taking h / v and stuffing it into the rigidbody, you would read out the velocity, then blend some part of the new velocity in, then assign it back.

This is just a scribble, not tested… do it only when on slickness:

const float snappinessWhenSkidding = 5.0f;

var tempVelocity = playerRigidbody.velocity;
tempVelocity = Mathf.Lerp( tempVelocity, new Vector2(horizontalVelocity, verticalVelocity), snappinessWhenSkidding * Time.deltaTime);
playerRigidbody.velocity = tempVelocity;

Try adding OnCollisionEnter2D.

Build a SlipperySurface tag or component.
And just put the SlipperySurface component on the surface you want to slide on. And add Collider2D (Box Collider 2d for example)

public class SlipperySurface : MonoBehaviour {}

In your controller (PlayerController) add:

private bool SlipperySurface = false;

Add:

private void OnCollisionEnter2D(Collision2D collision)
{
    if (collision.collider.GetComponent<SlipperySurface>() != null)
    {
        onSlipperySurface = true;
    }
}
private void OnCollisionExit2D(Collision2D collision)
{
    if (collision.collider.GetComponent<SlipperySurface>() != null)
    {
        onSlipperySurface = false;
    }
}

Change the MovePlayer method a bit

private void MovePlayer(Vector2 movementForce)
{
    if (onSlipperySurface)
    {
        playerRigidbody.AddForce(movementForce * 0.5f, ForceMode2D.Force);
        float maxSpeed = movementSpeed * 1.5f;
        if (playerRigidbody.velocity.magnitude > maxSpeed)
        {
            playerRigidbody.velocity = playerRigidbody.velocity.normalized * maxSpeed;
        }
    }
    else
    {
        float horizontalVelocity = movementForce.x;
        float verticalVelocity = (grounded && !jumping) ? 0 : playerRigidbody.velocity.y;
        playerRigidbody.velocity = new Vector2(horizontalVelocity, verticalVelocity);
    }
}

This does give the player a bit of inertia while on the surface.

Pretty cool effect to show slipping/skidding. :+1: Thank you!

This is what I was looking for. Now the player slides naturally on the frictionless surface without inputs being completely disabled. Thank you!