OnCollisionEnter event seems to be called late.

Hi people..

I was examining the physics in Unity and here is what I did for it:

  • Created a sphere and a cube.
  • Scaled the cube to make it act as a floor.
  • Placed the sphere above the floor(cube).
  • Attached a rigged body to the sphere.
  • Configured the gravity as (0,-200,0) from the Physics Manager.
  • Wrote a script which logs out when OnCollisionEnter event happens.
  • Attached the script to the sphere.
  • And finally pressed the Run button.

Sphere begins to fall down. When it hits the floor for the first time OnCollisionEnter event is not called. It is actually called on the next fixed frame. Please see the screenshots below.

Start

alt text

First hit

alt text

Next frame after the first hit

alt text

So why is this event called on the next frame? Am I missing something?

After writing a guess about this I have come to realise that this is the exact reason for the collider-based bug I was experiencing today on a project I’m working on right now.

At least in the editor the following is true:

Update() is called, and then the rendering after Update() has been applied is carried out. The new positions of your game objects are then reflected in both the Scene and Game views. However, to do anything based on the new collider positions as a result of changes in Update(), you have to yield and WaitForFixedUpdate() before the colliders will be picked up by Physics methods in their new positions.


Here is a test that you can run yourself to verify this:


Set up a scene as you had it before, only make the following changes:

  • Set the Sphere’s position to (0, 4, 0)
  • Set the Cube’s position to (0, 0, 0)
  • Set your Camera’s position to (0, 0, -10)
  • Disable Use Gravity on the Sphere’s Rigidbody component

Then add the following scripts:

Add this script (SphereBehaviour.cs) to your Sphere:

using UnityEngine;
using System.Collections;

public class SphereBehaviour : MonoBehaviour {
	
	int frameId = 0;

	void Update() {
		if (frameId == 0) {
			Debug.Log("First frame Update() on Sphere: Do nothing");
		} else if (frameId == 1) {
			Debug.Log("Second frame Update() on Sphere: Translate downwards");
			transform.Translate(0.0f, -4.0f, 0.0f);
		}
		frameId++;
	}
	
	void OnCollisionEnter(Collision collision) {
		Debug.Log("OnCollisionEnter() responded");
	}

}

Next add the following script (CubeBehaviour.cs) to your Cube object:

using UnityEngine;
using System.Collections;

public class CubeBehaviour : MonoBehaviour {
	
	[SerializeField] Transform sphereTransform;
	
	void LateUpdate() {
		Debug.Log("LateUpdate() on Cube Before fixed update:");
		OverlapSphere();
		StartCoroutine(_WaitForFixedUpdate());
	}
	
	IEnumerator _WaitForFixedUpdate() {
		yield return new WaitForFixedUpdate();
		Debug.Log("After fixed update on Cube:");
		OverlapSphere();
	}
	
	void OverlapSphere() {
		Collider[] hitColliders = Physics.OverlapSphere(transform.position, 0.5f);
		Debug.Log("Sphere position: " + sphereTransform.position + " / Hit collider count: " + hitColliders.Length);
	}
	
}

Don’t miss this step:

Now select the Cube object and then drag the Sphere object onto the Sphere Transform property in the Cube’s inspector. If you’re getting an error it’s because you haven’t done this step (and I forgot to add it in originally! Sorry!)


If you step through this scene frame-by-frame as you were doing before you will see the following occur:

Frame 1

In the first frame, we did nothing so that we can see how everything is laid out and we can get our first results from our first OverlapSphere(). Note that the Hit Collider Count in the shows it’s hitting 1 collider because it’s hitting the Cube game object’s collider (itself). Note also that the results after WaitForFixedUpdate() are not showing yet despite the rendering already being completed. This is our first sign that the rendering occurs before the physics is updated.

Frame 2

Note that I cleared the console before advancing the frame to reduce clutter.

After frame two has been rendered we finally have our first result from the _WaitForFixedUpdate() coroutine in CubeBehaviour. Notice how the position of the Sphere is still being shown to be in its original position. This code was executed before the Scene and Game views were rendered.

It is here that the Sphere has been translated downwards into its new position that should cause it to hit the Cube object. The new transform position is reflected in the LateUpdate() of CubeBehaviour. Game Objects’ positions have all been updated but we’re still only showing one hit from our OverlapSphere() in both cases.

Frame 3

Note again that I cleared the console before advancing the frame to reduce clutter.

After we have advanced to the third frame we finally have results that we would expect to have:

You can see at the very top of the Console output what we have been waiting for: OnCollisionEnter(). Physics has now finally noticed that the Sphere is colliding with the Cube object. Remember that we concluded in the last frame that the code in the coroutine after the yield to WaitForFixedUpdate() is executed before the rendering of this frame but after the frame has been advanced. In both cases Hit Collider Count is now returning 2, the value we would expect.

From this we can conclude that, at least in the editor, the frame you can see rendered in both the Scene and the Game views is half-way between being fully updated: Update(), LateUpdate() and any translations on game objects that were made during those methods have been applied, however collider positions have not yet been updated. Therefore if you want to test for collisions that may result from these translations manually using OverlapSphere() and similar Physics-related functions (including Raycasts!) you have to yield and WaitForFixedUpdate() before you will get the correct results.

If you are interested and want to test using the raycast as well you can add the following line to the end of the OverlapSphere() method in CubeBehaviour.cs:

Debug.Log("Raycast Count: " + Physics.RaycastAll(Camera.mainCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0.0f))).Length);

Hope that clears it up :slight_smile:

The higher your fps, the higher the accuracy will be.

Just lost a day on this as well.

And when OCE finally fires and you check a collision point, that collision point comes from the data you could see rendered 1 frame ago. In other words, Collision.contacts[0].point isn’t even on the object as you see it at all. Simply doing a test with Debug.DrawRay() from contacts[0].point will show this.

What’s more screwy is when you collide from the wrong side:

The green line is contacts[0].point. It is called in the frame you see rendered, but is a point on where the blue sphere was last frame. What’s more fun is the fact that last frame, the blue sphere had not collided at all. It was clearly not touching anything.

So colliding backwards is a little more derpy, but hey at least it called OCE on the right frame this time. Mmf. What a “feature”.

Here’s this too:

alt text