Search Unity

Question Struggling with custom controller wall clipping

Discussion in 'Scripting' started by Tiernan98, Dec 15, 2020.

  1. Tiernan98

    Tiernan98

    Joined:
    Jul 11, 2017
    Posts:
    42
    Hi,

    I have been coding my own custom character controller like Unity's. I would like one that behaves differently, which is why I'm not using the built-in one.

    My problem is with my move loop. Every frame in Update, I take the input velocity and sweep a capsule through the scene with an amount of velocity * Time.deltaTime. Then there are velocity projection rules depending on what the character hit. The movement is exactly how I would like it and I am pleased with it.

    However, if I move along the side of some obstacles (never happens when running straight ahead), then the character can sometimes clip inside the obstacle. This is my code:
    Code (CSharp):
    1.  
    2.     private Vector3 MoveLoop(Vector3 remaining)
    3.     {
    4.         int loopCount = 0;
    5.         bool collided = true;
    6.         Vector3 result = transform.position;
    7.         Vector3 previousHitNormal = Vector3.zero;
    8.         Vector3 moveDir = remaining;
    9.  
    10.         while (collided && loopCount < MaxMoveLoops && remaining.sqrMagnitude > 0.0f)
    11.         {
    12.             Vector3 sweepAmount = remaining + remaining.normalized * CollisionOffset;
    13.             collided = CapsuleSweepSingle(result, sweepAmount, out RaycastHit closestHit);
    14.  
    15.             if (collided)
    16.             {
    17.                 Vector3 moveUsed = remaining.normalized * closestHit.distance;
    18.                 Vector3 moveAmount = moveUsed + closestHit.normal * CollisionOffset;
    19.                 result += moveAmount;
    20.                 remaining -= moveUsed;
    21.  
    22.                // Projection rules removed from here (not relevant)
    23.             {
    24.                 // Nothing in way of move, simply perform it
    25.                 result += remaining;
    26.                 remaining = Vector3.zero;
    27.             }
    28.  
    29.             loopCount++;
    30.         }
    31.  
    32.         return result;
    33.     }
    34.  
    35.     private bool CapsuleSweepSingle(Vector3 position, Vector3 amount, out RaycastHit hit)
    36.     {
    37.         Vector3 capsuleStart = position + _capsuleInfo.start;
    38.         Vector3 capsuleEnd = position + _capsuleInfo.end;
    39.  
    40.         return Physics.CapsuleCast(
    41.             capsuleStart,
    42.             capsuleEnd,
    43.             _capsule.radius,
    44.             amount.normalized,
    45.             out hit,
    46.             amount.magnitude,
    47.             _groundLayers.value,
    48.             QueryTriggerInteraction.Ignore);
    49.     }
    50.  
    I have commented out the projection rules because they are quite long and I have confirmed they are not causing the problem. Even if I break after 1 iteration, this problem can occur. It is a loop because the projection rules would change the remaining movement as needed until none is left or max loops reached.

    CollisionOffset is a very small constant though I have tried increasing it up to 0.001f with no luck. At first, I thought it might be that the sideways movement would let the character get too close and floating-point error would let it clip through, but if the character is being pushed out in the direction of the normal I don't see how this could be the case?

    Anyone with experience doing collision see any issues?
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,734
    It's not really going to be possible to reason about this complex setup in a vacuum. I would recommend simplifying your scene setup down to the barest minimum number of GameObjects that shows your problem off, then run it with some instrumentation.

    One good piece of instrumentation would be to understand what CapsuleCast is hitting as well as where it is hitting it. You can get this out of the
    hit
    object, printing out the names of the colliders involved.
     
  3. AbandonedCrypt

    AbandonedCrypt

    Joined:
    Apr 13, 2019
    Posts:
    73
    Maybe as a side-note: Try to be more accurately descriptive with your method and variable names, it makes lots of things easier in the long run, especially if you go on vacation for 2-3 weeks and come back to your own complex code.
     
  4. Tiernan98

    Tiernan98

    Joined:
    Jul 11, 2017
    Posts:
    42
    Give me an example of something you'd change. The "Collision Offset" is the collision offset. The "moveUsed" is how much of the move you have used. "closestHit" is the closest thing the sweep hit. I called them these things for the exact reason you pointed out.

    Yeah, sorry, I understand. Was trying to stop the post from being too long but I obviously didn't balance it well enough lol.

    I hate when I post about something and come up with a solution not long after; don't want to waste people's time. But I'd been struggling with it for a few days. It was in fact the side of the capsule getting too close to the collider, so I have an extra test, where I sweep out to the left and right side of the character after my initial sweep test and if they are too close then I use some good old trigonometry to push them back.

    Here is how I did it in case it helps anymore in the future:


    The capsule cast distance only has the collision offset added in the direction of movement, so it allows the side to overlap due to floating error. So when the character is moved by the desired distance, I send rays out on either side of the character to see if they are closer than the collision offset.

    If they are, don't move them out by the normal because they could get pushed into something else. Instead, calculate the angle theta at the apex (where the move extrapolated joins with the wall), which is the same as the angle between the ray direction and the wall-normal, then calculate the current distance from the apex and the distance from the apex if it was offset properly and push that character back along the direction of movement to this point.

    This is the function that calculates the required backwards movement (not including the raycasting):
    Code (CSharp):
    1.  
    2.     private float CorrectSidewaysOverlap(Vector3 wallNormal, Vector3 rayDirection, float hitDistance)
    3.     {
    4.         Vector3 wallToPlayer = -rayDirection;
    5.  
    6.         float apexAngle = Vector3.Angle(wallToPlayer, wallNormal);
    7.  
    8.         if (apexAngle == 0.0f)
    9.         {
    10.             return 0.0f;
    11.         }
    12.  
    13.         float distanceToApex = hitDistance / Mathf.Tan(apexAngle * Mathf.Deg2Rad);
    14.  
    15.         float desiredDistance = (CollisionOffset + _capsule.radius) / Mathf.Tan(apexAngle * Mathf.Deg2Rad);
    16.  
    17.         return desiredDistance - distanceToApex;
    18.     }
     
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,734
    It's fairly common actually, don't hate it, embrace it. I do that all the time. I start writing out longhand all the steps to reproduce and report the error and suddenly I realize "Oh yeah, I forgot that step..."
     
    Tiernan98 likes this.