- Home /
Ways to work around OnCollisionStay/Enter events firing one frame late?
I'm trying to make a rigidbody-based player movement script, and for the most part everything's worked well. Recently though, while trying to add code to avoid sliding down slopes below a walkable slope limit, I came across an issue with my ground check system; i.e., OnCollisionStay, which my ground check is reliant on, appears to fire one physics frame late from when it happens. Adding an OnCollisionEnter check has no effect, nor does any variant of the rigidbody collision detection setting.
Searching in general has turned up very few things, but one result of note was that it appears this behavior is intentional in PhysX:
"[...] frame is then getting lost because there is a general one frame delay from the moment a contact was discovered to the the moment an appropriate callback was called. This behaviour is by design in PhysX."
This is mind, I've been trying to find alternatives for finding ground for the rigidbody. The issue linked above suggests using an overlap function followed by a CapsuleCast. I've been trying something similar, but it always ends up reporting normals pointing directly up:
Vector3 rotatedCapsuleHalfHeight = transform.up * (collider.height / 2);
RaycastHit[] hits = Physics.CapsuleCastAll(transform.position - rotatedCapsuleHalfHeight,
transform.position + rotatedCapsuleHalfHeight, collider.radius - 0.01f, -transform.up,
0.0001f, collisionMask);
if (hits.Length > 0)
{
Vector3 normal;
// Find the contact normal that is closest to 'up', relative to the current direction of gravity; this signifies the most level ground
if (hits.Length == 1) normal = hits[0].normal;
else
{
float closestDot = 1;
normal = hits[0].normal;
foreach (RaycastHit hit in hits)
if (Vector3.Dot(hit.normal, -gravity.normalized) < closestDot) normal = hit.normal;
}
float angle = Vector3.Angle(-gravity.normalized, normal);
if (angle < slopeLimit - 0.01f) walkableGroundNormal = groundNormal = normal;
else if (angle < 89.9f) groundNormal = normal;
// IsGrounded is a shorthand property that returns true if groundNormal does not equal Vector3.zero
// groundNormal is set to Vector3.zero at the end of every FixedUpdate.
if (IsGrounded) lastGroundedTime = Time.time;
}
It's worth noting though that I've been trying to tackle this problem for a few days now, so I'm absolutely open to any workarounds to this, not just the CapsuleCast one.
Answer by LukeZaz · Aug 05, 2019 at 07:09 AM
Fixed my code. Turns out, CapsuleCast and SphereCast give RaycastHits whose normals are the inverse normals of the cast, not the normal of the hit as expected. I was able to repair this using a modified function I found here. (Thanks a ton, lordofduct!)
Note that there's still a couple bugs with my implementation that I'm ironing out (namely, non-MeshCollider normals still seem to be a tad messy), but I'll edit this post with any fixes as I find them.
Hit normal repair code:
// I use this as an extension to RaycastHit here, but obviously that's not necessary by any stretch.
public static Vector3 GetRepairedNormal(this RaycastHit hit)
{
if (hit.collider is MeshCollider)
{
Mesh mesh = (hit.collider as MeshCollider).sharedMesh;
// Get the vertices of the specific triangle that was hit
Vector3 vertex0 = mesh.vertices[mesh.triangles[hit.triangleIndex * 3]];
Vector3 vertex1 = mesh.vertices[mesh.triangles[hit.triangleIndex * 3 + 1]];
Vector3 vertex2 = mesh.vertices[mesh.triangles[hit.triangleIndex * 3 + 2]];
Vector3 normal = Vector3.Cross(vertex1 - vertex0, vertex2 - vertex1).normalized;
return hit.transform.TransformDirection(normal);
}
else
{
Vector3 point = hit.point + hit.normal * 0.01f;
hit.collider.Raycast(new Ray(point, -hit.normal), out hit, 0.011f);
return hit.normal;
}
}
Note that I have changed the ground check code in my script to happen at the start of every FixedUpdate, instead of the end as I had before. I also only reset my ground normals right before checking collision; otherwise, IsGrounded
would be false during Update calls.
Code for finding ground:
Vector3 rotatedCapsuleHalfHeight = transform.up * (collider.height / 2 - collider.radius);
// Note that the gravity variables used here are Vector3s that represent force AND direction.
RaycastHit[] hits = Physics.CapsuleCastAll(transform.position - rotatedCapsuleHalfHeight,
transform.position + rotatedCapsuleHalfHeight, collider.radius, baseGravity.normalized,
currentGravity.magnitude * Time.fixedDeltaTime, collisionMask);
if (hits.Length > 0)
{
Vector3 normal;
if (hits.Length == 1) normal = hits[0].GetRepairedNormal();
else
{
float closestDot = 1;
normal = hits[0].GetRepairedNormal();
foreach (Vector3 norm in hits.Select(x => x.GetRepairedNormal()))
if (Vector3.Dot(norm, -gravity.normalized) < closestDot) normal = norm;
}
float angle = Vector3.Angle(-gravity.normalized, normal);
if (angle < slopeLimit - 0.01f) walkableGroundNormal = groundNormal = normal;
else if (angle < 89.9f) groundNormal = normal;
// IsGrounded is a shorthand that returns true if groundNormal does not equal Vector3.zero
// groundNormal and walkableGroundNormal are both reset right before this collision check,
// which works ideally at the beginning of FixedUpdate, before code relying on ground checks.
if (IsGrounded) lastGroundedTime = Time.time;
}
Answer by xxmariofer · Aug 05, 2019 at 06:27 AM
well i think your issue is not exactly the same as reported there, Unity physics are completly independent from the main loop in unity, this means that per frame you can get multiple collision callbacks, or not a single. if yoou are not colliding frequently enought you can try using the property collision detection to Continues or continues dynamic and see if it helps, your rigidboy moves independant of the player thats being rendered. you can use the Window>Physics Debugger to see this, if you want to get collision messages before they really occur the only alternatives are using a child empty component with the same collider as the parent but triggered and a bit bigger so you get the trigger messages, or using raycast with a certain length
I'm aware. I'd already tried all collision detection settings – even continuous speculative, just in case – and none helped. It also isn't a rendering issue, as the player doesn't even have a renderer right now. I ended up fixing my code though, which I've posted as an answer above.
That all said, thanks for mentioning the Physics Debugger, I didn't know that was there!