Unity Bug: Rigidbody.SweepTestAll Occasionally Missing Colliders, Seemingly At Random.

I’ve got a game I’ve been working on for a few years now. I think it’s coming along very nicely, although I’ve had one persisting bug that I have not been able to reproduce on purpose. My world is generated through additively loaded scenes and assembled at runtime as workaround for no nested prefabs. It supports up to two players, networked or solo (using localClient), currently My characters move through the world by doing rigidbody.SweepTestAll and finding relevant collisions by object tag.

Occasionally, when testing the game, one of the characters will fall through the level when entering a new room. Both characters use the same scripts in an identical fashion to determine movement yet. More rarely, a character will be able to move through a wall. I say “more rarely”, but that’s generally because in my play testing I don’t spend my time walking into walls. After ages of this happening, it finally happened in a game in the editor, in solo mode (localClient, so everything all adjustments to the scene were possible). I played around with the scene bunch and could not get it to detect the collision it was missing. I tried resetting every component on the original and changing every variable. Eventually I just copy/pasted the scene object and the collision detection worked perfectly.

This makes me think it is a Unity bug. If the scene is the same for both characters, being all run in the one instance of Unity, then the collision detection should also be identical. The bug is always happening in a different scene and I can go multiple games without finding missed collision. I haven’t had it happen to the same object twice.

The objects I have seen the character clip through (almost exclusively the floor) all have the same components. A Transform, Cube (Mesh Filter), Box Collider and Mesh Renderer. They all have the same tag too, “Fixture”, so that’s not the issue.

The world, once assembled, it pretty large (as I understand it) with dimensions of about 20000 squared, with a player being 100x200x100. I don’t know what to do and would appreciate any and all help I can be given.

We need to see your collision code to be of any help, but I’ll assume what your collision system does is just does a sweeptestall from your current position outward the distance you want to move, then on hit you use that new hit distance as your new target position. Maybe you have some extra code in there so you slide across and what not.

I wanted to make a character controller that just used a capsulecast, or spherecasts, which from what I know are pretty much like sweeptests. I failed to rely on just capsulecasts or spherecasts because of a major flaw, if the cast starts inside a collider, even just a tiny tiny amount, it will not detect the collider. Combine that with floating point precision errors and youve got yourself a problem, and seeing how big you have everything, the farther from the origin of the world, the higher the float point precision error.
For you to only rarely see this issue is whats strange, since I think the issue was very much seen during my testings, however, that can depend on a lot of things. I think I saw the issue the most when I was walking against a wall at a small angle, so I was pretty much walking straight down a wall, almost hardly touching the wall. However, I think that was due to the code I put in trying to fix this issue, which worked except for cases such as walking against the wall at a small angle. The fix I tried was to just push back from the hit point a little bit, but it still had issues.

So, when you spawn players in, do they spawn almost perfectly on the floor, or in the air a bit? If they are too close to the floor, when they spawn sometimes float point errors might cause the cast to miss since it started inside the collider.
And not walking against walls I guess would reduce the issue, but I would think youd see it even just walking on floors unless if you put in code to try and fight this issue already.

So lets say your issue is indeed the one I explained, what is the fix? Well, unfortunately you will have to do proper collision detecting, or shall I say that would have been unfortunate. Recently, Unity released 2 new physics methods in 5.6 called ComputePenetration and ClosestPoint.
I think you can, or even must, just use ComputePenetration for collision code. I didnt try it yet, so I dont know how well it works.
Edit - Also, since those 2 methods are pretty new, expect bugs to show up.

My code is a little more complex because I’ve created a system for objects pushing other objects, to go along with my dungeon puzzle theme. The character does a sweeptest forward, with a magnitude of 20, and if the objects that are hit are not pushable it doesn’t move, it they are pushable it tries to push them and then moves accordingly.

It determines whether or not something is pushable by the object tag. All the objects I have fallen through have the tag “Fixture” and nowhere are they allowed to be pushed or moved through. The collider on my 100x200x100 character is set to 0.99x0.99x0.99 and the move magnitude of any object is 20. All fixtures have their colliders set to 1x1x1. This means that I should have a threshold of 1 between the character and the ground at any given time and a threshold of 0.5f between any wall. When I last had the error in the editor I moved the wall/floor in all directions by 5 and it had no impact on whether or not the character passed through. On the wall I was able to pass through from all directions. If it was a clipping issue, and especially given I had moved the wall, it should have only passed through from half the directions.

If the player is cleared to move it jumps forward 20. This is a small enough unit in my game that it still appears smooth, so I don’t need to use lerping.

When the error happens it typically only happens to one object in the area. As there are very few floor objects in the game, about 4 per room and about 7 rooms on average, there aren’t many opportunities for it to fail.

    public void GetCheckDirection(string givenString) {
        string currentPushingObject = "character";
        NetworkInstanceId currentNetId = netId;
        Vector3 moveVector = Vector3.zero;
        if (optionsScript.menu == false) {
            if (givenString == "y") {
                moveVector = -transform.up;
                currentPushingObject = "gravity";
                currentNetId = gravityNetId;
            } else if (givenString == "x") {
                moveVector = Vector3.right * currentXAxis;
            } else if (givenString == "z") {
                moveVector = Vector3.forward * currentZAxis;
            }
        }
        List<Collider> blankList = new List<Collider>();
     
        int givenWeight = 0;
        CheckMovement(moveVector, "check", currentPushingObject, currentNetId, ref blankList, ref givenWeight);
    }

    public bool CheckMovement(Vector3 givenVector, string givenString, string pushingObject, NetworkInstanceId givenNetId, ref List<Collider> givenTestedObjects, ref int givenWeight) {
        if (pushingObject != "door") {
            allowedToMove = false;
        }
        if (givenVector != Vector3.zero) {
         
            bool belowThingsHit = true;
            givenVector = Vector3.Normalize(givenVector);
            Vector3 originalVector = givenVector;

            if (givenString == "check") {
                //Update Movement Vector To Use Ladders
                if (ladderScript.downDirections.Count > 0) {
                    givenVector = ladderScript.LadderMovement(givenVector);
                }
                //Verify Object Is On Another Object Before Moving XZ
                //Some Scenarios Don't Require Checking Objects Below
                bool checkBelow = true;
                if (ladderScript.downDirections.Count == 0 && givenVector == -Vector3.up) {
                    checkBelow = false;
                } else if (givenVector == Vector3.up) {
                    checkBelow = false;
                } else if (ladderScript.downDirections.Count != 0 && givenVector != -Vector3.up) {
                    checkBelow = false;
                } else if (checkBelow == true) {
                    //Baring Some Objects, Check Below For Another Object
                    if (ladderScript.downDirections.Count == 0 || givenVector != -Vector3.up) {
                        belowThingsHit = false;
                    }
                    RaycastHit[] belowThings = GetComponent<Rigidbody>().SweepTestAll(-transform.up, speed, QueryTriggerInteraction.Ignore);
                    foreach (RaycastHit item in belowThings) {
                        if (item.collider.tag != "Cube Wall" && item.collider.tag != "Character") {
                            if (checkBelow == true) {
                                belowThingsHit = true;
                                if (ladderScript.downDirections.Count != 0) {
                                    givenVector = originalVector;
                                }
                                break;
                            }
                        }
                    }
                }
            }
            if (belowThingsHit == true && givenVector != Vector3.zero) {
                //Check If Moving Will Result In A Collision, Or Possibly Push Cubes
                //bool hitSomething = false;
                //bool onlyPushableCube = gameObject.GetComponent<PushCube>().CheckHits(pushingObject, givenVector*speed, out hitSomething, ladderScript.downDirections.Count);
                //If No Collision In The Way, And Not Trying To Freefall Whilst On Ladder
                bool freeMove = ladderScript.downDirections.Count == 0 || originalVector != Vector3.down;
                bool freePush = playerabilities.pushCube == true && givenVector != Vector3.up;
                bool shouldMove = gameObject.GetComponent<PushCube>().CheckCubes(givenVector*speed, givenString, pushingObject, givenNetId, ref givenTestedObjects, ref givenWeight, freeMove, freePush);
                if (shouldMove) {
                    if (givenString == "check") {
                        allowedToMove = true;
                        allowedToMoveVector = givenVector;
                    } else if (givenString == "set") {
                        allowedToMove = true;
                        allowedToMoveVector = givenVector;
                    }
                    return true;
                } else if (givenString == "check" || givenString == "set") {
                    allowedToMove = false;
                }
            }
        }
        return false;
    }
    public bool CheckCubes(Vector3 moveVector, string givenString, string pushingObject, NetworkInstanceId givenNetId, ref List<Collider> testedObjects, ref int givenWeight, bool characterFreeMove = true, bool characterFreePush = true, int ladderDownCount = 0) {
        //Checking If All The Cubes Are Able To Move
        if (testedObjects.Count == 0) {
            if (gameObject.GetComponent<Collider>() != null) {
                testedObjects.Add(gameObject.GetComponent<Collider>());
            }
        }
        if (gameObject.tag == "Pushable Cube" && gameObject.GetComponent<XZNetworkedCube>().hasMovementPlan == true) {
            moveVector = Vector3.Normalize(moveVector) * gameObject.GetComponent<XZNetworkedCube>().GetSpeed();
        }
        hitThings = GetComponent<Rigidbody>().SweepTestAll(moveVector, moveVector.magnitude, QueryTriggerInteraction.Ignore);
        List<Collider> newObjects = new List<Collider>();
        bool hitSomething = false;
        bool allCubesCanMove = true;
        foreach (RaycastHit item in hitThings) {
            if (item.collider.tag == "Pushable Collider") {
                hitSomething = true;
                if (item.collider.transform.parent.GetComponent<XZNetworkedCube>().movingNetId != givenNetId.ToString()) {
                    allCubesCanMove = false;
                    break;
                }
            } else if (item.collider.tag == "Pushable Cube") {
                if (pushingObject == "door" || pushingObject == "character" || pushingObject == "gravity") {
                    if (testedObjects.Contains(item.collider) == false) {
                        testedObjects.Add(item.collider);
                        newObjects.Add(item.collider);
                        XZNetworkedCube movingScript = item.collider.GetComponent<XZNetworkedCube>();

                        if (pushingObject == "gravity" || movingScript.movingNetId != gravityNetId.ToString() || movingScript.hasMovementPlan != true) {
                            hitSomething = true;
                            bool cubeCanMove = movingScript.CheckMovement(moveVector, "pushCheck", pushingObject, givenNetId, ref testedObjects, ref givenWeight);
                            if (cubeCanMove == false) {
                                allCubesCanMove = false;
                                break;
                            } else {
                                givenWeight += movingScript.GetComponent<WeightCalculatorStorer>().weightCalculatorScript.weight;
                            }
                        }
                    }
                } else {
                    hitSomething = true;
                    allCubesCanMove = false;
                }
            } else if (item.collider.tag == "Character") {
                if (pushingObject == "door") {
                    if (testedObjects.Contains(item.collider) == false) {
                        testedObjects.Add(item.collider);
                        newObjects.Add(item.collider);
                        hitSomething = true;
                        XZNetworkedPlayer movingScript = item.collider.GetComponent<XZNetworkedPlayer>();
                        bool characterCanMove = movingScript.CheckMovement(moveVector, "pushCheck", "door", givenNetId, ref testedObjects, ref givenWeight);
                        if (characterCanMove == false) {
                            allCubesCanMove = false;
                            break;
                        } else {
                            givenWeight += movingScript.GetComponent<WeightCalculatorStorer>().weightCalculatorScript.weight;
                        }
                    }
                } else {
                    if (((pushingObject == "character" || pushingObject == "gravity") && (characterFreeMove == false || Vector3.Normalize(moveVector) != Vector3.down)) || (pushingObject != "character" && pushingObject != "gravity")) {
                        hitSomething = true;
                        allCubesCanMove = false;
                        break;
                    }
                }
            } else {
                if (item.collider.tag != "Bullet") {
                    if (pushingObject == "character") {
                        if (item.collider.tag != "Cube Wall" && (Vector3.Normalize(moveVector) != -Vector3.up || item.collider.tag != "Character" || ladderDownCount != 0)) {
                            hitSomething = true;
                            allCubesCanMove = false;
                            break;
                        }
                    } else if (pushingObject == "door") {
                        if (item.collider.tag != "Cube Wall" && item.collider.tag != "Player Wall") {
                            hitSomething = true;
                            allCubesCanMove = false;
                            break;
                        }
                    } else {
                        hitSomething = true;
                        allCubesCanMove = false;
                        break;
                    }
                }
            }
            if (((pushingObject == "door" && givenWeight > optionsScript.doorMaxWeight) || (pushingObject == "character" && givenWeight > optionsScript.characterMaxWeight)) && Vector3.Normalize(moveVector) != Vector3.down && allCubesCanMove == true) {
                allCubesCanMove = false;
                break;
            }
        }
        //If Cubes Can Move, And Ocassionaly Moves Them
        if (allCubesCanMove == true) {
            if (pushingObject != "character" || (pushingObject == "character" && ((hitSomething == false) || (characterFreePush == true && allCubesCanMove == true && hitSomething == true)))) {
                if (givenString == "set") {
                    testedObjects.Reverse();
                    foreach (Collider item in testedObjects) {
                        if (item.tag == "Pushable Cube") {
                            XZNetworkedCube movingScript = item.GetComponent<XZNetworkedCube>();
                            movingScript.SetMovingNetId(givenNetId);
                        }
                    }
                    foreach (Collider item in testedObjects) {
                        if (item.gameObject != gameObject) {
                            if (item.tag == "Pushable Cube") {
                                XZNetworkedCube movingScript = item.GetComponent<XZNetworkedCube>();
                                movingScript.MakeMovementPlan(moveVector, pushingObject, givenNetId, "pushed");
                            } else if (item.tag == "Character" && pushingObject == "door") {
                                XZNetworkedPlayer movingScript = item.GetComponent<XZNetworkedPlayer>();
                                movingScript.TryMoving(moveVector, pushingObject, givenNetId, "pushed");
                            }
                        }
                    }
                }
                return true;
            }
        }
        return false;
    }

That would make sense, since the objects you can push would have moved out of your way to the point that you weren’t touching them anymore, whereas when colliding against a Fixture, maybe eventually you are going to move so close to one to the point that float point errors is going to kick you in the but and make it so the SweepTest thinks it is starting inside the Fixtures collider, therefore ignores it.

I dont understand this part. Regardless what size you set any colliders, the sweeptest is going to use that colliders size in its calculations. There are no offsets unless if after the sweeptest hit you manually subtract from the hit.distance, but that would not be 100% safe still, as I have tried that with my spherecasts / capsulecasts and failed. However, you say you move a fixed amount of 20, which tells me you are not even applying any offsets.

Your code is very confusing at first glance, so I didnt really dissect it since that might take all day ^^, so I apologize if your code explains a few important things.
The reason you might not be experiencing many issues is due the way you do things, which is even if you can move 10 out of the 20 magnitude you wanted to move, you just dont move at all. This would significantly reduce the amount of times you see the error. When I was working on using sphere/capsule casts for my collision, even if I can only move .1 out of whatever amount I wanted to move, I moved that .1, which means I kept staying as close to the walls as possible, which greatly increased the chances for errors to pop up. However, even if I put in code to try and avoid those errors, while they might work many of the time, theres that small chance it fails, which is just unacceptable.

I still think the issue has to do with your SweepTest starting inside the Fixture collider, or your scene is just too big for physics to be accurate.
Is your rigidbody just a capsule? If so, why not give it a test? Before you do your sweeptest. do a OverlapCapsule test with the same size as your capsule collider, and see if it ever returns true. It might not be a 100% accurate test, since maybe the OverlapCapsule capsule size might be a tiny bit different than the sweeptests capsule or something, but might still be useful to try.

My rigidbody is not a capsule, it uses a single box collider (which you’d think would have less errors again than sphere or capsule). You are correct in your understanding that even if I can move any amount out of 20, unless I can move the full 20, I do not move. My game works on a grid, with every object in the world set to integer positions and scales that are multiples of 100 (ie. 100x100x100 or 200x300x100), with the player being the only object that moves on a smaller scale.

For the character to fall through the floor it must not hit the collider in its forward sweeptest, move forward INTO the collider, then not notice the collider in its downward sweeptest. This seems unlikely to me. When I had the error in the editor I tried moving the ground collider downwards by up to 10, and the character still fell through it. This makes me not think it’s an error to do with clipping into then through the floor. If the floor was moved further downwards, then the character should not have been able to move into it, as it was below him, but should not have fallen through it as the floor would still have been less than 20 below and therefore the character would not move at all.

I have rigged up some physics.OverlapBox debugging to log to console that I can turn on next time it happens, but I tried game after game for an hour and it did not occur. It’s really rare, but really annoying.

Then the issue might be in your code. You have so many ifs and elses and variables going on that something might happen.
One thing that makes me a little uncomfortable is how you set your moveVector to -transform.up and then later do -vector3.up compare checks. It might be possible that float point error caused -transform.up to not exactly be -vector3.up. You would need to do something like what this thread talks of
http://answers.unity3d.com/questions/395513/vector3-comparison-efficiency-and-float-precision.html
It seems unity does some of it behind the scenes, but its error threshold might be too small.
But that might not be the issue still.

If you want to blame the physics, which it really might be the physics, you are going to need to isolate just the physics part and do tests to see if the physics ever failed in your scene. There is no way unity is going to go bug hunting unless if you make a simple reproducible environment.

I used to think boxes were the simplest collider to calculate, but eventually found out that spheres are the simplest, and then capsules are second simplest. Box colliders are actually not that simple, unless if its a axis aligned box collider, then maybe.