Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

How do I slide player along a wall smoothly?

Discussion in 'Scripting' started by Marjo_Borestag, Nov 8, 2014.

  1. Marjo_Borestag

    Marjo_Borestag

    Joined:
    Aug 14, 2013
    Posts:
    14
    I'm trying to create sliding along a wall mechanic for my game and can't seem to work out the vector math needed. Basically I'd like smooth sliding across walls like that in games like: Quake, Half-Life 1, counter-strike etc. But my player slides weirdly and even starts bouncing in some cases. That happens because the distance from the wall becomes higher than threshold. The problem seems to be with sliding direction I set. This is 3D game by the way.

    Here's the code snippet, that makes the player slide and image of the problem and how I think the vector math is being worked out:

    Code (CSharp):
    1.  
    2.         bool canWalk = true;
    3.         Vector3 curPos = transform.position;
    4.         Vector3 nextPos = transform.position +  pos;//pos is for example transform.forward + transform.right
    5.         Vector3 dir = nextPos - curPos;
    6.         Vector3 direction  = dir / dir.magnitude;//setting the vector for direction
    7.        
    8.          Ray ray = new Ray (transform.renderer.bounds.center, distance);
    9.  
    10.         RaycastHit rInfo;
    11.  
    12.          if (Physics.Raycast (ray, out rInfo, movementMargin))
    13.            {
    14.                         canWalk = false;
    15.                        //if close enough to start sliding
    16.                         if(setSlidePos)
    17.                         {
    18.                        //Add the player direction and wall normal vectors
    19.                         normMov = (direction + rInfo.normal).normalized;
    20.                         }
    21.                 }
    22.                 else
    23.                         canWalk = true;
    Untitled.png

    P.S. Player gets moved using MoveRotation, MovePosition functions. For e.g. transform.position + transform.forward + transform.right gets passed into MovePosition.
     
    Last edited: Nov 8, 2014
  2. psycocrusher

    psycocrusher

    Joined:
    Jul 24, 2011
    Posts:
    71
    I don't know if this can help, but i use it on some of my games to move along a surface' s normals:
    Code (JavaScript):
    1. //Run Along Normals.
    2. function Update(){
    3.  
    4.     // This would cast rays only against colliders in layer 8 .
    5.     var layerMask8 = 1<<8;
    6.        
    7.     var hit : RaycastHit;
    8.            
    9.     // cast a ray to the right of the player object
    10.     if (Physics.Raycast (transform.position,transform.TransformDirection (Vector3.right),hit,30,layerMask8)) {
    11.    
    12.         // orient the Moving Object's Left direction to Match the Normals on his Right
    13.         var RunnerRotation = Quaternion.FromToRotation (Vector3.left, hit.normal);
    14.        
    15.         //Smooth rotation
    16.         transform.rotation = Quaternion.Slerp(transform.rotation, RunnerRotation,Time.deltaTime * 10);
    17.     }
    18. }
     
  3. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    I use this based on what I found from this Unity Answer http://answers.unity3d.com/questions/10323/calculating-a-movement-direction-that-is-a-tangent.html

    forceCross is going to be the new force that moves our player on collision. The hitInfoCapsuleCast is the hitInfo that we would get when a CapsuleCast we set up collides with something. We then will also shoot a Raycast at where the CapsuleCast hit and get our hitInfoRaycast (Reason for this below). The targetPositionLocal is how much we are moving in worldspace relative to our player. This is not the localTransform of the player, but I'm sure you can set it up to work with the local world of the player.
    With Vector3.Cross we are able to flip around the direction to be perpendicular to the hitInfo.normal

    Code (CSharp):
    1. if(Vector3.Angle(hitInfoCapsuleCast.normal, hitInfoRaycast.normal) > 5)
    2.                     {
    3.                         colliderNormal = hitInfoCapsuleCast.normal;
    4.                     }else{
    5.                         colliderNormal = hitInfoRaycast.normal;
    6.                     }
    7.  
    8.                     forceCross = Vector3.Cross(targetPositionLocal, colliderNormal);
    9.                     forceCross = Vector3.Cross(colliderNormal, forceCross);
    10.  
    11. transform.Translate(forceCross, relativeTo: Space.World);
    However, there is something big to keep in mind. I will copy and paste the comment I left in that Unity Answer page...

    "I have also had problems with CapsuleCast hitInfo.normal returning different normal values depending on the angle of impact. This page is what got me to realize why my normal alignment would be off when on a certain angle.

    http://answers.unity3d.com/questions/50825/raycasthitnormal-what-does-it-really-return.html

    To combat this, I just have a Raycast shoot from my CapsuleCast start position towards my CapsuleCast hitInfo.point and get the normal with the Raycast hitInfo instead of the CapsuleCast. This comment is for anyone who been having trouble with inaccurate normal values as I wasted hours for such a silly thing.

    I also notice that if you are using a mesh collider and you change the scale, the hitInfo.point will go to weird places on the collider and can give bad results. For example, I had a mesh collider scaled to be very tall, but when colliding with it, the hitInfo.point from my CapsuleCast point to the bottom of the collider, which was through the floor, so the raycast I shot out hit the floor and gave me the floors normal causing wrong results."

    A Raycast hitInfo.normal and a CapsuleCast hitInfo.normal give different results. The Raycast hitInfo.normal is constant no matter where you are hitting on the objects face and the CapsuleCast hitInfo.normal seems to give different results depending on where you are on the face. One of the more noticeable difference is that the CapsuleCast hitInfo.normal seems to wrap around corners of a mesh nice and smoothly while the Raycast hitinfo.normal is constant. We can use this to our advantage by having smooth curving around edges, which is very important as it helps avoid our object from penetrating into the mesh. So to do all this, we just compare the angle between the two hitinfo.normals and when the angle starts to go above 5, we can be confident enough to know that we are near an edge or corner of the object we are colliding with, so we will use the smooth hitInfo.normal the CapsuleCast provides.
     
    Last edited: Feb 4, 2015
  4. MrMartinlll

    MrMartinlll

    Joined:
    Nov 19, 2016
    Posts:
    3
    This seems to fix it without raycasting, just reacting to collisions and picking one direction around a particular obstacle. Idea is: When we hit an obstacle, pick a direction, perpependicular to the normal. Keep that direction until we clear the obstacle. Let this direction be dominant but also let the desired direction weigh in to make sure we stay in contact with the wall.

    ----------------------------------

    private void OnCollisionEnter(Collision other)
    {
    if (other.gameObject.name != "Ground")
    {
    ContactPoint contact = other.GetContact(0);
    Vector3 n = contact.normal;
    Vector3 option1 = new Vector3(n.z, 0, -n.x);
    Vector3 option2 = -option1;
    if ((option1 - lastFrameRequestedMovementDirection).sqrMagnitude < (option2 - lastFrameRequestedMovementDirection).sqrMagnitude)
    {
    currentMovementModifier = option1;
    }
    else
    {
    currentMovementModifier = option2;
    }
    }
    }

    private void OnCollisionExit(Collision other)
    {
    if (other.gameObject.name != "Ground")
    {
    currentMovementModifier = Vector3.zero;
    }
    }

    void Move(Vector3 movementDirection){
    movementDirection.y = 0;
    //modify for collision avoidance and such
    movementDirection = movementDirection.normalized + currentMovementModifier.normalized * 2;
    movementDirection = movementDirection.normalized * moveSpeed * Time.fixedDeltaTime;
    // Move the player to it's current position plus the movement.
    rigidBody.MovePosition(transform.position + movementDirection);

    }
     
    Seith likes this.
  5. v2-Ton-Studios

    v2-Ton-Studios

    Joined:
    Jul 18, 2012
    Posts:
    237
    I spent most of the weekend wrestling with a solution for this. Sadly none of the above worked for my particular situation. Nor any of the related posts across the web.

    Perhaps the below is of use to others.

    // Setup

    -- Realistic transform based movement. NOT RigidBody
    -- XZ movement over uneven terrain.
    -- A MoveController calls this code and makes use of its state like OpeningDirection and HasObstacle.

    // Collision Evaluation

    -- I use CheckSphere (still need to see the perf cost on that) to see if my character is first and foremost colliding with something.
    -- Then I try to find an "opening" for them to move to so they "glance" or "slide" off of collisions rather than getting stuck.

    Code (CSharp):
    1.  
    2. void evalCollision()
    3. {    
    4.     DoDetour = false;
    5.     OpeningDirection = _infinity;
    6.     bool sphereCheck = Physics.CheckSphere(_data.Position, _rangeToObstacle, GameManager.Instance.LayerMask_Characters);
    7.     if (sphereCheck)
    8.     {
    9.         HasObstacle = true;
    10.        
    11.         bool ahead = DetectObstacle();
    12.         if (ahead == false)
    13.         {
    14.             HasObstacle = false;
    15.         }
    16.         else if (OpeningDirection.x != Mathf.Infinity)
    17.         {
    18.             DoDetour = true;
    19.             Debug.Log($"Opening Direction: {OpeningDirection}");
    20.         }
    21.     }
    22.     else
    23.     {
    24.         DetectObstacle();
    25.         HasObstacle = false;
    26.     }    
    27. }
    28.  

    // Adjustment Properties

    -- These set how far "ahead" we look
    -- and at what angle we look relative to the character's current position
    -- For my character setup 2 units high 0.5f radius, I used a shorter look ahead 1.5f and a fairly wide range 27f

    You want to tune these so they create a "short cone" in front of the character such that they cannot slowly ease into and through a collision, but instead always "move away" from it at a pseudo reflect angle.

    Code (CSharp):
    1.  
    2. [SerializeField]
    3. float _openingRange = 1.5f;
    4. [SerializeField]
    5. float _openingAngle = 27f;
    6.  

    // Obstacle Detection Ahead

    -- Here we are doing the tests for obstacles AND looking for an opening
    -- The opening is use IFF there is no collision on the "opposite" look ahead.
    -- We look ahead directly, to-the-left and to-the-right.
    -- If we have a collision on the left, but not on the right then we have an opening, and vice versa.
    -- From that the actual opening direction is further-to-the-left or -right relative to the opening to ensure we are always "moving away" from the obstacle, never skimming into it, which can result in the character getting trapped inside the obstacle.

    Code (CSharp):
    1.  
    2. bool DetectObstacle()
    3. {
    4.     Vector3 forward = _data.MoveInput.normalized;
    5.     Vector3 right = Quaternion.Euler(0, _openingAngle, 0) * forward;
    6.     Vector3 left = Quaternion.Euler(0, -_openingAngle, 0) * forward;
    7.     Vector3 openingRight = Quaternion.Euler(0, _openingAngle * 2f, 0) * forward;
    8.     Vector3 openingLeft = Quaternion.Euler(0, -(_openingAngle * 2f), 0) * forward;
    9.     int forwardCollisions = Physics.RaycastNonAlloc(_rayStart, forward, forwardHits, _openingRange, GameManager.Instance.LayerMask_Characters);
    10.     int rightCollisions = Physics.RaycastNonAlloc(_rayStart, right, rightHits, _openingRange, GameManager.Instance.LayerMask_Characters);
    11.     int leftCollisions = Physics.RaycastNonAlloc(_rayStart, left, leftHits, _openingRange, GameManager.Instance.LayerMask_Characters);
    12.     bool forwardCollision = forwardCollisions > 0;
    13.     bool rightCollision = rightCollisions > 0;
    14.     bool leftCollision = leftCollisions > 0;
    15.     if (forwardCollision)
    16.     {
    17.         DbgX.DrawRay(_rayStart, forward * _openingRange, Color.red, 0f);
    18.     }
    19.     else
    20.     {
    21.         DbgX.DrawRay(_rayStart, forward * _openingRange, Color.green, 0f);
    22.     }
    23.     if (rightCollision)
    24.     {
    25.         DbgX.DrawRay(_rayStart, right * _openingRange, Color.red, 0f);
    26.     }
    27.     else
    28.     {
    29.         DbgX.DrawRay(_rayStart, right * _openingRange, Color.green, 0f);
    30.     }
    31.     if (leftCollision)
    32.     {
    33.         DbgX.DrawRay(_rayStart, left * _openingRange, Color.red, 0f);
    34.     }
    35.     else
    36.     {
    37.         DbgX.DrawRay(_rayStart, left * _openingRange, Color.green, 0f);
    38.     }
    39.     if (leftCollision && !rightCollision)
    40.     {
    41.         OpeningDirection = openingRight.normalized;
    42.         DbgX.DrawRay(_rayStart, openingRight * _openingRange, Color.cyan, 0f);
    43.     }
    44.     else if (!leftCollision && rightCollision)
    45.     {
    46.         OpeningDirection = openingLeft.normalized;
    47.         DbgX.DrawRay(_rayStart, openingLeft * _openingRange, Color.cyan, 0f);
    48.     }
    49.     return forwardCollision || rightCollision || leftCollision;
    50. }
     
    Last edited: Aug 3, 2023
  6. faUnity

    faUnity

    Joined:
    Aug 7, 2023
    Posts:
    14