Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Slow collision detection at low frame rates

I'm experiencing an odd issue with my collision detection. I'm using the Update method to move the player (I don't want to use FixedUpdate because that creates an undesired weird movement). The fixed timestep is set at the default 0.02 (I tried playing with time setting but that didn't work either) . I set the collision detection of the rigidbodies of both objects to "continuous dynamic". Also, I set the target frame rate to 300 and that didn't change anything...

When the framerate is low or the device itself is slow, the collision detection doesn't always work. The player can easily fall through the object it's supposed to collide with, though sometimes it doesn't.

Please tell me what I can do to fix this because I've published a game and many users are reporting this (serious) bug. Thank you for your support.

This is what is supposed to happen:

enter image description here

This is what actually happens:

enter image description here

(as you can see, the cube gets out of the wall and to the other side)

I move the player when the user releases the mouse button:

Script 1:

public Script2 Jumper;
public float TimeToJump;

public void Update()
{
        if (Input.GetMouseButtonUp(0)) 
    {
            StartCoroutine (Delay (1f/50f)); //Don't mind the time.
    }
}

IEnumerator Delay(float waitTime) 
{
    yield return new WaitForSeconds (waitTime);
    if (Jumper != null) 
    {
        Jumper.SetVelocityToJump (gameObject, TimeToJump);
    }
}

Script 2 attached to player (cube):

public class Script2 : MonoBehaviour {

    GameObject target;
    private float timeToJump;
    public bool isJumping = false;

    public void SetVelocityToJump(GameObject goToJumpTo, float timeToJump)
    {
        StartCoroutine(jumpAndFollow(goToJumpTo, timeToJump));
        this.timeToJump = timeToJump;
        this.target = goToJumpTo;
    }

    private IEnumerator jumpAndFollow(GameObject goToJumpTo, float timeToJump)
    {
        var startPosition = transform.position;
        var targetTransform = goToJumpTo.transform;
        var lastTargetPosition = targetTransform.position;
        var initialVelocity = getInitialVelocity(lastTargetPosition - startPosition, timeToJump);

        var progress = 0f;
        while (progress < timeToJump)
        {
            progress += Time.deltaTime;
            if (targetTransform.position != lastTargetPosition)
            {
                lastTargetPosition = targetTransform.position;
                initialVelocity = getInitialVelocity(lastTargetPosition - startPosition, timeToJump);
            }

            float percentage = progress * 100 / timeToJump;  
            GetComponent<Rigidbody>().isKinematic = percentage < 100.0f;  

            transform.position = startPosition + (progress * initialVelocity) + (0.5f * Mathf.Pow(progress, 2) * _gravity);
            yield return null;
        }

        OnFinishJump (goToJumpTo, timeToJump);
    }


    private void OnFinishJump(GameObject target, float timeToJump)
    {
        if (stillJumping)
        {
            this.isJumping = false;
        }
    }

    private Vector3 getInitialVelocity(Vector3 toTarget, float timeToJump)
    {
        return (toTarget - (0.5f * Mathf.Pow(timeToJump, 2) * _gravity)) / timeToJump;
    }
}

The target of the cube is a child of the bigger cube (the wall).

If you require clarification, please leave a comment below. I might give the link to my game if you need more details.

Quote from here (found thanks to @Logman): "The problem exists even if you use continuous dynamic collision detection because fast moving objects can move so fast that they are too far apart from itself from one frame to the next immediate frame. It's like they teleported and no collision detection would ever be triggered because no collision existed, from each frame perspective, and thus from all calculations processed."

In my case, the cube is not going fast, but you get the concept.

like image 232
Dinovr Avatar asked Jun 12 '16 12:06

Dinovr


Video Answer


2 Answers

There are several issues with your code.

  1. You are asking a Coroutine to yield for 1/50th of a second. The minimum time a yield must occur for is one frame. If Time.deltaTime > 0.02f this is already one of the problems.
  2. You are using Coroutines and yield return null to compute physics calculations. Essentially, you're computing physics in Update(), which is only called once per frame (null is equivalent to new WaitForEndOfFrame(): as mentioned in (1), a running Coroutine cannot be yielding between frames). Under low frame-rate, the amount of motion an object undertook between two frames might exceed the collision range of the target trigger. Assuming linear, non-accelerating motion: ∆S = v∆t where v = velocity, ∆S is movement to cover in the current frame, ∆t is Time.deltaTime. As you can see, ∆S scales proportionally with ∆t.

  3. You have GetComponent<T>() calls inside loops. Always avoid doing this: store a reference as a member variable instead (initialise it in Start()).

My suggestion for the quickest working hack would be to not worry too much about "being clean", and instead create subroutines that you call from FixedUpdate(), and (create and) use member bools to conditionally test which subroutine to "execute" and which to "skip". You can also use member bools or enums as triggers to switch between various "states".

A better solution would be to let Unity handle the kinematics and you instead work with rigidbody mutators (and not transform.positions), but that may be totally unnecessary for an arcade situation, which yours might be. In that case stick to the hack above.

If you really want to control kinematics by hand, use an engine like SFML. A Particle System tutorial would be a good place to start.

like image 124
karnage Avatar answered Oct 20 '22 00:10

karnage


It's your float percentage, among other things.

"If isKinematic is enabled, Forces, collisions or joints will not affect the rigidbody anymore."

That's from the isKinematic page of Unity's documentation. You're setting it to true when progress hits 100. So at lower framerates, there'll be a sudden jump due to Time.deltaTime steps being a lot higher, progress is suddenly >= 100, isKinematic is set to true and the player is no longer affected by collisions.

I think you're going to have to rethink a lot of the code here and do some heavy optimisations. But the other posters have laid those out already, so I don't need to.

EDIT: Misunderstood the initial question, thought that it meant you were trying to detect collisions but your code wasn't always detecting them. Didn't realise it actually meant getting the collisions to occur in the first place.

like image 31
DisturbedNeo Avatar answered Oct 20 '22 02:10

DisturbedNeo