Can someone explain how this Unity 2d collision detection works and why it fails sometimes?

520 Views Asked by At

I tried to adapt the 2d platformer character controller from this live session: https://www.youtube.com/watch?v=wGI2e3Dzk_w&list=PLX2vGYjWbI0SUWwVPCERK88Qw8hpjEGd8

Into a 2d top down character controller. It seemed to work but it is possible to move into colliders with some combination of keys pressed that I couldn't really find out, but it's easy to make it happen.

The thing is I don't understand how the collision detection is really working here so I don't know how to fix it. I appreciate if someone can explain how this works.

Thanks :)

This is how the player is set up: Player Inspector

PlayerControllerTopDown2D.cs:

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

public class PlayerControllerTopDown2D : PhysicsObject2D
{
    public float maxSpeed = 7;

    private SpriteRenderer spriteRenderer;
    private Animator animator;

    private bool facingUp, facingDown, facingLeft, facingRight;

    void Awake()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
        animator = GetComponent<Animator>();

        facingUp = true;
        facingDown = facingLeft = facingRight = false;
    }

    protected override void ComputeVelocity()
    {
        Vector2 move = Vector2.zero;

        move.x = Input.GetAxis("Horizontal");
        move.y = Input.GetAxis("Vertical");

        targetVelocity = move * maxSpeed;

        if (move.y > minMoveDistance && !facingUp)
        {
            clearOthersAndSet(0);
            // sprite rotation
        }

        if (move.y < -minMoveDistance && !facingDown)
        {
            clearOthersAndSet(1);
            // sprite rotation
        }

        if (move.x < -minMoveDistance && !facingLeft)
        {
            clearOthersAndSet(2);
            // sprite rotation
        }

        if (move.x > minMoveDistance && !facingRight)
        {
            clearOthersAndSet(3);
            // sprite rotation
        }


    }

    void clearOthersAndSet(int x)
    {
        switch (x)
        {
            case 0;
                facingUp = true;
                facingDown = facingLeft = facingRight = false;
                break;
            case 1:
                facingDown = true;
                facingUp = facingLeft = facingRight = false;
                break;
            case 2:
                facingLeft = true;
                facingUp = facingDown = facingRight = false;
                break;
            case 3:
                facingRight = true;
                facingUp = facingDown = facingLeft = false;
                break;
        }
    }
}

PhysicsObject2D.cs:

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

public class PhysicsObject2D : MonoBehaviour
{
    protected Rigidbody2D rb2d;
    protected Vector2 velocity;
    protected Vector2 targetVelocity;

    protected ContactFilter2D contactFilter;
    protected RaycastHit2D[] hitBuffer = new RaycastHit2D[16];
    protected List<RaycastHit2D> hitBufferList = new List<RaycastHit2D>(16);


    protected const float minMoveDistance = 0.001f;
    protected const float shellRadius = 0.01f;

    protected bool hitSomething = false;


    void OnEnable()
    {
        rb2d = GetComponent<Rigidbody2D>();
    }

    void Start()
    {
        contactFilter.useTriggers = false;
        int layerMask = Physics2D.GetLayerCollisionMask(gameObject.layer);
        contactFilter.SetLayerMask(layerMask);
        contactFilter.useLayerMask = true;
    }

    void Update()
    {
        targetVelocity = Vector2.zero;
        ComputeVelocity();
    }

    protected virtual void ComputeVelocity()
    {

    }

    void FixedUpdate()
    {
        if (hitSomething)
        {
            targetVelocity = -targetVelocity * 5;
            hitSomething = false;
        }

        velocity.x = targetVelocity.x;
        velocity.y = targetVelocity.y;

        Vector2 deltaPosition = velocity * Time.deltaTime;

        Vector2 move = Vector2.right * deltaPosition.x;

        Movement(move, false);

        move = Vector2.up * deltaPosition.y;

        Movement(move, true);
    }

    void Movement(Vector2 move, bool yMovement)
    {
        float distance = move.magnitude;

        if (distance > minMoveDistance)
        {
            int count = rb2d.Cast(move, contactFilter, hitBuffer, distance + shellRadius);

            if (count > 0)
                hitSomething = true;
            else
                hitSomething = false;

            hitBufferList.Clear();
            for (int i = 0; i < count; i++)
            {
                hitBufferList.Add(hitBuffer[i]);
            }

            for (int i = 0; i < hitBufferList.Count; i++)
            {
                float modifiedDistance = hitBufferList[i].distance - shellRadius;
                distance = modifiedDistance < distance ? modifiedDistance : distance;
            }
        }

        rb2d.position = rb2d.position + move.normalized * distance;
    }
}
3

There are 3 best solutions below

2
On

simplifying, unity checks for collision each frame in a synchronized way (for the sake of frame drop compensation), if your object is moving fast (covering a great distance in a short time), there's a chance of your object pass through a wall in that exactly time gap of a collision check and another. as well stated and tested on this thread.

if your object is passing through a object, the first thing you want to change is the collision detection mode, when the mode is set to discrete, you're saying that the object is checking for collision in a lower rate.enter image description here

and when you set it to continuous, the object checks for collision more frequently. enter image description here

so probably setting detection mode from "discrete" to continuous should be enough to solve your problem.

0
On

As Matheus suggested I changed the colliders mode to continuous, however the problem still happens. I found a way to make it happen for sure.

To make this work I removed line 38 of PhysicsObject2D.cs:

targetVelocity = -targetVelocity * 5;

See this image

In this position, I press left + down and the player moves up into the box collider. The player was not inside other colliders at the start.

When the bodies are overlaping it is then allowed to move freely inside but the movement is much slower and the directions are inverted, pressing up moves down, pressing the left arrow moves to the right.

1
On

1) Set Collision Detection Mode to "Continuous" for important entities like the player.

2) Use rb2d.MovePosition(); for movement.

3) Don't call rb2d.MovePosition() more than once in a frame.

these 3 combined should make your movement and collision detection work fine. I wont question the rest of your code, but here are my generalized suggestions

Vector2 MovementDirection; //Assuming this is assigned in Update() from Input
public float MaxSpeed;
void FixedUpdate() 
{
    Vector2 finalMoveDir = MovementDirection.normalized * MaxSpeed;
    //
    //any additional changes to the final direction should happen here
    //
    finalMoveDir *= Time.deltaTime;
    rb2d.MovePosition((Vector2)transform.position + finalMoveDir);
}