Wayback Machinekoobas.hobune.stream
May JUN Jul
Previous capture 13 Next capture
2021 2022 2023
1 capture
13 Jun 22 - 13 Jun 22
sparklines
Close Help
  • Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
  • Asset Store
  • Get Unity

UNITY ACCOUNT

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account
  • Blog
  • Forums
  • Answers
  • Evangelists
  • User Groups
  • Beta Program
  • Advisory Panel

Navigation

  • Home
  • Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
    • Blog
    • Forums
    • Answers
    • Evangelists
    • User Groups
    • Beta Program
    • Advisory Panel

Unity account

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account

Language

  • Chinese
  • Spanish
  • Japanese
  • Korean
  • Portuguese
  • Ask a question
  • Spaces
    • Default
    • Help Room
    • META
    • Moderators
    • Topics
    • Questions
    • Users
    • Badges
  • Home /
avatar image
0
Question by Burning-North · May 20, 2014 at 02:25 AM · c#collisionmovementphysics

FIXED: How can I stop a collider from tunneling?

NOTE: I found a solution after fumbling around for another day, and I've posted the answer below. I'm leaving my original question here in its entirety, but you'll probably only find the code at the bottom of any use.


So, I have a player object that uses a kinematic Rigidbody and a CapsuleCollider for movement, and so far I've been completely unable to prevent it from tunneling through my level geometry. The biggest problem cases are corners - or any place where the player can touch multiple colliders simultaneously - but quite often it'll just fall through the floor of its own accord.

Initially, I used only CapsuleCastAll for collision detection, but when that didn't work I added a kinematic Rigidbody component and tried SweepTestAll instead. Still no dice.

The thing is, I'm pretty sure I know what's going wrong. As I've been told, the value returned by RaycastHit.distance is lossy and prone to floating-point error. This means that even after you resolve a collision, you may still be intersecting with the other collider by some infinitesimal amount, which means the next sweep test will miss and you'll pass through it.

I tried to solve this problem by moving the player away by a tiny additional amount after resolving each collision. I started with a multiple of float.Epsilon, which is usually sufficient for this kind of thing. When that wasn't enough, I kept increasing it until I reached a (comparatively) huge value of 0.00001f, which still can't compensate for what looks like severe floating-point error in the sweep test.

Here's what I have at the moment. "playerVelocity" is a class member that stores the current velocity, and "playerRigidbody" is just a cached GetComponent call.

         // Calculate the intended movement and new position
         Vector3 move = playerVelocity * dTime;
         Vector3 newPos = playerRigidbody.position + move;
 
         // Sweep the rigid body
         RaycastHit[] allHits = playerRigidbody.SweepTestAll(move.normalized, move.magnitude);
 
         // Resolve all collisions
         foreach(RaycastHit hit in allHits)
         {
             // Use a small value to compensate for floating point error
             float collisionEpsilon = 0.00001f;
 
             // Calculate the penetration depth
             Vector3 penetration = move - (move.normalized * hit.distance);
 
             // Move the new position out of the collider along its surface normal
             newPos -= hit.normal * Vector3.Dot(penetration, hit.normal);
 
             // Move the new position away by a tiny amount in case of FP error
             newPos += hit.normal * collisionEpsilon;
 
             // Project the velocity onto the collision plane
             playerVelocity -= hit.normal * Vector3.Dot(playerVelocity, hit.normal);
         }
 
         // Move the player
         playerRigidbody.MovePosition(newPos);

I feel like there's something really simple and stupid that I've missed, but I'm just not seeing it. Even a huge epsilon value like 0.1f - which causes sickening judder - doesn't seem to be enough to keep the player collider out of tunneling distance, and eventually I always end up plummeting through the abyss anyway.

Removing my own movement code and just doing everything through Rigidbody.AddForce calls does conveniently solve the problem, but unfortunately this is a networked multiplayer game, and I need to write my own movement in order to support client-side prediction and all those other features that don't play nice with Unity's physics implementation.

I'm going to feel awful if I have to build my silly little game in Unreal just because of this minor shortcoming. It'd be like swatting a fruit fly with a hand grenade.

Anyone have any ideas? Am I just being really stupid and sleep-deprived? Thanks!


EDIT #1: I woke up this morning and tried a different solution, just to make sure I'm not doing something terribly dumb with my collision resolution code. I completely removed both the Rigidbody and the CapsuleCollider, and ran the same code with a fake SphereCastAll instead:

         // Calculate the intended movement and new position
         Vector3 move = playerVelocity * dTime;
         Vector3 newPos = playerTransform.position + move;
 
         // Sweep a fake sphere
         RaycastHit[] allHits = Physics.SphereCastAll(playerTransform.position, 0.75f, move.normalized, move.magnitude);
 
         // Resolve all collisions
         foreach(RaycastHit hit in allHits)
         {
             // Use a small value to compensate for floating point error
             float collisionEpsilon = 0.00001f;
 
             // Calculate the penetration depth
             Vector3 penetration = move - (move.normalized * hit.distance);
 
             // Move the new position out of the collider along its surface normal
             newPos -= hit.normal * Vector3.Dot(penetration, hit.normal);
 
             // Move the new position away by a tiny amount in case of FP error
             newPos += hit.normal * collisionEpsilon;
 
             // Project the velocity onto the collision plane
             playerVelocity -= hit.normal * Vector3.Dot(playerVelocity, hit.normal);
         }
 
         // Move the player
         playerTransform.position = newPos;

To my surprise, this code is rock-solid. I haven't been able to break it all morning. Which is great, but I want to use a capsule for my player if possible. So I replaced SphereCastAll with CapsuleCastAll, and once again the player started passing through walls:

         // Fake capsule endpoints and radius
         Vector3 point1 = playerTransform.position + (Vector3.up * 0.35f);
         Vector3 point2 = playerTransform.position - (Vector3.up * 0.35f);
         float radius = 0.4f;
 
         // Sweep the fake capsule
         RaycastHit[] allHits = Physics.CapsuleCastAll(point1, point2, radius, move.normalized, move.magnitude);

Out of curiosity, I went back to my original Rigidbody.SweepTestAll code, only this time I used a SphereCollider instead of a capsule. Lo and behold, it ran perfectly.

So now I know that my collision code works fine, and the values returned by SweepTestAll are indeed correct when using a sphere collider. But somehow, using a capsule instead of a sphere breaks Unity's sweep test, and that's where my tunneling bug is coming from.

So my question is now this: why would a sweep test with a capsule collider return such a flaky result? So far as I can tell, this is an issue within Unity itself, but if anyone knows a way to code around it I'd be grateful.


EDIT #2: Upon further testing, I've discovered that SweepTestAll can still tunnel with a sphere collider, but it happens far less often, and generally only in certain areas. Nonetheless, I'm running out of ideas and I'm seriously considering building this game in a different engine. There has got to be something I'm missing, but I'm coming up empty. :(


EDIT #3:

Here's the code I'm currently using. I ditched SphereCastAll because I realized I may have been resolving collisions with occluded surfaces that shouldn't have needed resolving at all, and now I'm using a distance-based sequential approach:

     // Use a small value to compensate for floating point error
     float collisionEpsilon = 0.00001f;
 
     // Store the player's position and attempted movement
     Vector3 pos = playerTransform.position;
     Vector3 move = playerVelocity * Time.fixedDeltaTime;
 
     // Attempt to move as long as there is still distance to travel
     while(move.magnitude > collisionEpsilon)
     {
         // Store the result of the sphere cast
         RaycastHit hit;
 
         // If the sphere cast hits a collider...
         if(Physics.SphereCast(pos, 1.0f, move.normalized, out hit, move.magnitude))
         {
             // ...create a new vector to move to the contact point
             Vector3 contact = move.normalized * hit.distance;
 
             // Subtract this from the remaining distance to travel
             move -= contact;
 
             // Update the target position
             pos += contact;
 
             // Move away from the surface in case of FP error
             pos += hit.normal * collisionEpsilon;
 
             // Project the remaining movement onto the surface
             move -= hit.normal * Vector3.Dot(move, hit.normal);
 
             // Project velocity onto the surface
             playerVelocity -= hit.normal * Vector3.Dot(playerVelocity, hit.normal);
         }
         // If there's been no collision...
         else
         {
             // ...move the full distance
             pos += move;
 
             // The move is now complete
             move = Vector3.zero;
         }
     }
 
     // Apply the new position
     playerTransform.position = pos;

So far, this code works almost all the time, but it still tunnels on angled surfaces, and I have no clue what to do now. Any ideas are welcome at this point.

Comment
Add comment · Show 3
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image nickfourtimes · May 20, 2014 at 01:26 PM 0
Share

A long shot alongside everyone else's long shots: can you tweak the Skin Width on the Character Controller?

avatar image meat5000 ♦ · May 20, 2014 at 01:28 PM 0
Share

If you are using character controller and rigidbody, try turning off rigidbody gravity and making it kinematic.

avatar image Burning-North · May 20, 2014 at 02:43 PM 0
Share

Nope, not using a CharacterController, I'm afraid. Just a kinematic Rigidbody component so I can do everything myself.

2 Replies

· Add your reply
  • Sort: 
avatar image
2
Best Answer

Answer by Burning-North · May 21, 2014 at 11:19 PM

BOOYAH.

I managed to solve the problem with a little trial-and-error this afternoon, and I figured it would be best to post an answer to my own question instead of just deleting it. This solution goes back to the fake sphere cast I used earlier, but it's now far more robust and I haven't been able to break it after several hours of testing. As always, I'm sure there will be edge cases where it falls apart, but for my needs this is sufficient:

 // Use a small value to compensate for floating point error
 float collisionError = 0.001f;
 
 // Calculate the desired movement vector
 Vector3 move = playerVelocity * dTime;
 
 // Store the result of the collider cast
 RaycastHit hit;
 
 // Check continuously until the collision is resolved
 while(Physics.SphereCast(playerTransform.position, playerHitRadius,
                          move.normalized, out hit, move.magnitude))
 {
     // Calculate the vector toward the contact point
     Vector3 contact = move.normalized * hit.distance;
 
     // Calculate the penetration depth
     Vector3 penetration = move - contact;
 
     // Project the penetration vector onto the surface
     penetration -= hit.normal * Vector3.Dot(penetration, hit.normal);
 
     // Build the new movement vector
     move = contact + penetration;
 
     // Project the velocity onto the surface
     playerVelocity -= hit.normal * Vector3.Dot(playerVelocity, hit.normal);
 
     // Push the movement and velocity vectors slightly away from the surface
     move += hit.normal * collisionError;
     playerVelocity += hit.normal * collisionError;
 }
 
 // Update the player's position
 playerTransform.position += move;

It's not a very elegant solution, I'll admit. Especially in the last lines of the while() loop, where I'm deflecting the movement and velocity vectors away from the surface by a tiny amount to compensate for floating point error. This is necessary to prevent tunneling, but after the adjustment the vectors are no longer colinear. For my game this doesn't matter, but it'll likely cause issues with more realistic simulations.

Theoretically, this technique should work with capsules and/or Rigidbody.SweepTest(), but so far I've still encountered bugs using those versions. Frankly, my game works fine with just sphere collisions anyway, so I'll probably leave it at that.

Many thanks to everyone who's offered their thoughts (including those on Twitter)!

EDIT #1: Haha, there is an edge case that breaks my solution! However, it only happens when the player object falls into a V-shaped groove between two horizontal colliders. Since this only seems to happen on horizontal grooves and not in the corners of rooms, I can live with it. My level design isn't likely to have many of those, and if it does I'm sure I'll be able to duct-tape my way out of it. :)

Comment
Add comment · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image
0

Answer by torsigurdson · Sep 14, 2018 at 04:04 PM

And you set the rigidbody to continuous collision detection? Otherwise you might want to changed the fixed time step in the TimeManager.

Comment
Add comment · Show 2 · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image eses · Sep 14, 2018 at 04:08 PM 0
Share

@torsigurdson - This is a comment, not answer...?

avatar image torsigurdson · Sep 14, 2018 at 04:09 PM 0
Share

Oh, sorry, yes.

Your answer

Hint: You can notify a user about this post by typing @username

Up to 2 attachments (including images) can be used with a maximum of 524.3 kB each and 1.0 MB total.

Follow this Question

Answers Answers and Comments

22 People are following this question.

avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image

Related Questions

Making a bubble level (not a game but work tool) 1 Answer

Keep Horizontal Momentum after Jump 2 Answers

Player is jittery when it collides with wall (C# 3D) 0 Answers

Objects not colliding twice 0 Answers

RigidBody.MovePosition seems jerky in movement 0 Answers


Enterprise
Social Q&A

Social
Subscribe on YouTube social-youtube Follow on LinkedIn social-linkedin Follow on Twitter social-twitter Follow on Facebook social-facebook Follow on Instagram social-instagram

Footer

  • Purchase
    • Products
    • Subscription
    • Asset Store
    • Unity Gear
    • Resellers
  • Education
    • Students
    • Educators
    • Certification
    • Learn
    • Center of Excellence
  • Download
    • Unity
    • Beta Program
  • Unity Labs
    • Labs
    • Publications
  • Resources
    • Learn platform
    • Community
    • Documentation
    • Unity QA
    • FAQ
    • Services Status
    • Connect
  • About Unity
    • About Us
    • Blog
    • Events
    • Careers
    • Contact
    • Press
    • Partners
    • Affiliates
    • Security
Copyright © 2020 Unity Technologies
  • Legal
  • Privacy Policy
  • Cookies
  • Do Not Sell My Personal Information
  • Cookies Settings
"Unity", Unity logos, and other Unity trademarks are trademarks or registered trademarks of Unity Technologies or its affiliates in the U.S. and elsewhere (more info here). Other names or brands are trademarks of their respective owners.
  • Anonymous
  • Sign in
  • Create
  • Ask a question
  • Spaces
  • Default
  • Help Room
  • META
  • Moderators
  • Explore
  • Topics
  • Questions
  • Users
  • Badges