Teleport issue (ledge climbing)

i’m a newibe, and i’m doing a 2D platformer.
In pseudo code this what i’m trying to do :

1 - my player can hit a “climbable” wall ;
2 - then if he jumps as he is still in contact with the climbable wall, he would grabb the ledge and hold the position, grabbing the ledge ;
3 - then if he press Up, he would climb over the ledge
4 - and when it’s done, he would return to his initial condition, to idle, but at the top of the plateform he just climbed.

1 to 3 is ok, and 4 too but i have this problem : when the climb animation is done, before to go back to the idle position on top, the player is very shortly up in the air… as you could see on the screen capture :

for climbing, it’s juste an animation of the player going from down to up.
as his colliders stay down, i added an empty gameObject child to the player “NewPosition”
in the animation, the NewPosition is set at his feet in the idle animation and in the final image of the climb animation. then with an animation event, i use in the script this to teleport the player :

public GameObject Joueur;
public Transform NouvellePosition;
[...]

player.transform.position = NewPosition.transform.position;

it’s a bit abrupt, there’s is no transition…
but it should not have this “in the air” phase…
i don’t know what’s going on, i double checked my animation and animator of course.
Maybe the method i used isn’t very appropriate, i don’t know ?

thanks !

I suspect the teleport position is wrong for the capsule.
You should also remove any forces and set velocity to 0.

Ok ! thanks, i’ll check that.

i checked the position of my gameobject, wich seems correct.
but i sill have this “jump”

i tried to remove velocity with

GetComponent<Rigidbody2D>().velocity = Vector2.zero;

how could i remove forces now ?

That does “remove” the velocity, it sets the linear velocity to zero.

try disabling interpolation for your rigidbody and check if this resolves your problem.

The jump you mention is likely forces acting on something that penetrated it to push it apart, That’s why I said it may be the wrong position.

Try putting a break point in the code: https://docs.unity3d.com/ScriptReference/Debug.Break.html
Then using the > icon in Unity editor to step through the frames. You could call this in the routine just before you set the forces. Then you can each frame also print out forces, or all kinds of info and look at what is REALLY happening.

This will easily find the root problem for you. It could be for one frame, the capsule is inside another collider and is pushed out violently. Quite common cause of “jump” issues.

So use Debug.Break(); and Debug.Log() data like velocity, what’s going on etc etc.

1 Like

@Kreshi the interpolation is set to “none”. I also tried every interpolation configuration.

@Digital Ape :
i put a Debug.break() where there is the problem…
the game freezes when my player is in the air, but i don’t know why or what to do with that…
Plus now, i have a problem wit my collider… (i post in another topic for that issue)

here’s my code :

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class Joueur : MonoBehaviour
{
    // References
    public GameObject player;
    public Animator animator;
    public Rigidbody2D rigidbody2D;
    public Transform groundCheck;              
    public Transform ceilingCheck;             
    public LayerMask whatIsGround;             
    public Collider2D crouchDisableCollider;    // When player would crouch, the collider of the top of the body would be disabled.
    public Transform NewPosition;               // Child to the player, at his feet at idle position / at his hand in last frame of climbing ledge animation

    // variables
    public float speed = 40.0f;                
    public float crouchSpeed = 10.0f;             
    public float verticalJumpForce = 3400.0f;      
    private float horizontalInput = 0.0f;          
    const float groundedRadius = .2f;           // Radius of the overlap circle to determine if grounded
    const float ceilingRadius = .2f;            // Radius of the overlap circle to determine if the player can stand up
    private float fallingTime = 0f;             // used to kill the player if he's falling too long
    public float deadlyHeight = 3f;             // >the player dies

    // bools
    private bool facingRight = true;             
    public bool isGrounded = true;             
    public bool isWalking = false;
    public bool isJumping = false;
    public bool isCrouching = false;
    public bool canHang = false;                // when the player collides a climbable wall, allows to climb
    public bool isHanging = false;              // when the player is hanging to a ledge
    public bool isDead = false;

    //
    [Header("Events")]
    [Space]
    public UnityEvent OnLandEvent;
    [System.Serializable]
    public class BoolEvent : UnityEvent<bool> { }
    public BoolEvent OnCrouchEvent;
    private bool wasCrouching = false;


    //-------------------------------------------------------------------------------------------------//


    private void Awake()
    {
        rigidbody2D = GetComponent<Rigidbody2D>();

        if (OnLandEvent == null)
            OnLandEvent = new UnityEvent();

        if (OnCrouchEvent == null)
            OnCrouchEvent = new BoolEvent();
    }


    //-------------------------------------------------------------------------------------------------//


    void Update()  
    {
        if (isGrounded && !isDead)
        {
            // COMMANDES (Inputs)

            // BOUTON AVANCER
            horizontalInput = Input.GetAxisRaw("Horizontal") * speed;
            if (horizontalInput > 0 || horizontalInput < 0)
            {
                isWalking = true;
            }
            else if (horizontalInput == 0)
            {
                isWalking = false;
            }

            // BOUTON SE BAISSER
            if (Input.GetButtonDown("Crouch"))
            {
                isCrouching = true;
                speed = crouchSpeed;
            }
            else if (Input.GetButtonUp("Crouch"))
            {
                isCrouching = false;
                ResetSpeed();
            }


            // BOUTON SAUT
            if (Input.GetButtonDown("Jump"))
            {
                Jump();
            }


            // S'ACCROCHER A UN REBORD
            if (canHang && Input.GetButtonDown("Jump"))
            {
                Hang();
            }

        }


        if (!isDead)
        {
            // BOUTON ESCALADER UN REBORD
            if (isHanging && Input.GetButtonDown("Up"))
            {
                Climb();
            }


            // MORT : CHUTE MORTELLE
            //DeadlyFall();
        }


    }


    //-------------------------------------------------------------------------------------------------//


    void FixedUpdate()  // Moving a Rigidbody only in fixedUpdate
    {
        // GROUNDED
        bool wasGrounded = isGrounded;
        isGrounded = false;

        // On sait que le joueur touche le sol, "isGrounded", quand le gameObject groundCheck qui lui est parenté au niveau de ses pieds avec un collider, touche tout ce qui est identifié par calque comme étant le sol       
        Collider2D[] colliders = Physics2D.OverlapCircleAll(groundCheck.position, groundedRadius, whatIsGround);
        for (int i = 0; i < colliders.Length; i++)
        {
            if (colliders[i].gameObject != gameObject)
            {
                isGrounded = true;
                if (!wasGrounded)
                    OnLandEvent.Invoke();
            }
        }


        if (!isDead && isGrounded)
        {
            // AVANCER
            transform.Translate(horizontalInput * Time.deltaTime, 0, 0);
            animator.SetFloat("Mouvement", Mathf.Abs(horizontalInput));


            // DIRECTION
            if (horizontalInput > 0 && !facingRight)
            {
                Flip();
            }
            else if (horizontalInput < 0 && facingRight)
            {
                Flip();
            }


            // SE BAISSER
            if (isCrouching)
            {
                if (!wasCrouching)
                {
                    wasCrouching = true;
                    OnCrouchEvent.Invoke(true);
                }

                // Disabled the collider when crouching
                if (crouchDisableCollider != null)
                    crouchDisableCollider.enabled = false;
            }
            else
            {
                // enable the collider when done crouching
                if (crouchDisableCollider != null)
                    crouchDisableCollider.enabled = true;

                if (wasCrouching)
                {
                    wasCrouching = false;
                    OnCrouchEvent.Invoke(false);
                }
            }

            if (!isCrouching)
            {
                // if there is a ceiling, the player can't get up and stay crouched
                if (Physics2D.OverlapCircle(ceilingCheck.position, ceilingRadius, whatIsGround))
                {
                    isCrouching = true;
                }
            }
        }
    }


    //-------------------------------------------------------------------------------------------------//


    void Flip() // Fonction pour se retourner
    {
        // Switch the way the player is labelled as facing.
        facingRight = !facingRight;

        // flip the player by *-1
        Vector3 theScale = transform.localScale;
        theScale.x *= -1;
        transform.localScale = theScale;
    }


    //-------------------------------------------------------------------------------------------------//


    public void OnLanding() // Landing after a jump
    {
        isJumping = false;
        animator.SetBool("Saute", false);
    }


    //-------------------------------------------------------------------------------------------------//


    public void OnCrouching(bool isCrouching) // stay crouching
    {
        animator.SetBool("SeBaisse", isCrouching);
    }


    //-------------------------------------------------------------------------------------------------//


    private void ResetSpeed()
    {
        speed = 40.0f;
    }


    //-------------------------------------------------------------------------------------------------//

    private void Jump()
    {
        isJumping = true;
        rigidbody2D.AddForce(new Vector2(0f, verticalJumpForce)); // insuffle une force verticale au rigidbody du joueur
        animator.SetBool("Saute", true);
    }


    //-------------------------------------------------------------------------------------------------//

    private void Hang()
    {
        isJumping = false;
        isHanging = true;
        canHang = false;
        speed = 0f;
        verticalJumpForce = 0f;
        animator.SetBool("PeutSaccrocher", true);
        Hanged();
    }


    //-------------------------------------------------------------------------------------------------//

    private void Hanged()
    {
        speed = 0f;
        verticalJumpForce = 0f;
        animator.SetBool("EstAccroche", true);
    }


    //-------------------------------------------------------------------------------------------------//

    private void Climb()
    {
        speed = 0f;
        verticalJumpForce = 0f;
        canHang = false;
        isHanging = false;
        animator.SetBool("Monte", true);
    }


    //-------------------------------------------------------------------------------------------------//

    private void HasClimbed()
    {
        Debug.Log("Has Climbed");
        animator.SetBool("Monte", false);
        animator.SetBool("EstAccroche", false);
        animator.SetBool("PeutSaccrocher", false);
        player.transform.position = NewPosition.transform.position;
        Debug.Break();
        canHang = false;
       
    }



    //-------------------------------------------------------------------------------------------------//

    // COLLISIONS


    void OnCollisionEnter2D(Collision2D collision)
    {
        /// Pour se suspendre aux plateformes       
        if (collision.gameObject.tag == "MursGrimpables")
        {
            canHang = true;
            animator.SetBool("EnCollisionAvecUnMurGrimpable", true);

            //when hitting a wall, player's speed is 0, he stop walk against the wall. but if the wall is climbable, he can climb
            speed = 0f;

            // after a while his speed get back, he could walk further
            Invoke("ResetSpeed", 0.05f);
        }

        if (collision.gameObject.tag == "Murs")
        {
            animator.SetBool("EnCollisionAvecUnMur", true);

            // the same with a simple wall
            speed = 0f;

            //
            Invoke("ResetSpeed", 0.05f);
        }
    }

    void OnCollisionExit2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "MursGrimpables")
        {
            canHang = false;
            animator.SetBool("EnCollisionAvecUnMurGrimpable", false);
        }

        if (collision.gameObject.tag == "Murs")
        {
            animator.SetBool("EnCollisionAvecUnMur", false);
        }
    }

    void OnCollisionStay2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "EauMortelle") // player drowns if he touches deadly Water
        {
            speed = 0f;
            isDead = true;
            animator.SetBool("MortNoyade", true);
        }

        if (collision.gameObject.tag == "Sols" && isDead) // player dies if he falls from too high
        {
            animator.SetBool("ChuteMortelle", true);
        }

    }

    //-------------------------------------------------------------------------------------------------//

    // DEATHS

    void DeadlyFall()
    {
        if (isGrounded)
        {
            fallingTime = 0f;
        }

        if (!isGrounded)
        {
            fallingTime += Time.deltaTime;
        }

        if (fallingTime >= deadlyHeight)
        {
            isDead = true;
            Debug.Log("Chute Mortelle !");
        }
    }


    //-------------------------------------------------------------------------------------------------//

    private void Drowning()
    {
        animator.speed = 0f;
        speed = 0f;
        isDead = true;
    }


    //-------------------------------------------------------------------------------------------------//

}

I tested your code without animations (I guess HasClimbed() is called via an animation event? So I called HasClimbed just after Climb() for debugging purposes and making this work without animations) and the teleportation worked as expected.

I guess the issue you experience happens because of “NewPosition.transform.position” giving you back the wrong position (somewhere in the air).

Altogether I am not a huge fan of the choosen teleportation solution. I recommend using either a custom transition for climbing from A → to B (for example with the help of a coroutine) or using real root motion on your character (which moves the whole rigidbody as well and not only the sprite(s)). In both cases, you would need to set “rigidbody2D.isKinematic = true;” when you execute “Hang()” and reset it back to “rigidbody2D.isKinematic = false” when “HasClimbed()” has been called.

The problem I see with moving only the sprite(s) and then teleporting the physics object afterwards to the climbed position achieved by the sprite(s) animation is that you have to sync the sprite(s) position(s) back to the physics objects position after the teleportation. I guess you do this as well with the help of the animator but I am afraid you may run into 1 frame offset-issues (or other transition issues) where your character may snap from the wrong position to the correct one.

My debugging includes assumptions, if you want us to test your issue directly you would need to upload a small sample project so that we can have a look at the whole setup of your character, animations and everything else :).

Thanks a lot Kreshi !
Very kind of you.

You guessed right with HasClimbeb() called as a animation event.
And i suspect you’re right too that “NewPosition.transform.position” is giving me back the wrong position (in the air above the player) i’m looking for this solution for hours now ! :smile:

i agree that the teleportation solution is not the best idea… i think it’s not very elegant,leaving the collider back when the sprites of the player are moving forward etc. and it complicates the animator with a lot of conditions to manage… quite confusing sometimes. i followed a tutorial, wich seems to do the trick but…
i could try your suggestion of a “custom transition” or root motion.
Root Motion may be a better compromise maybe ? as i understand it it’s the quite the same method, but the rigidbody is moved all allong ? i would have to find a tutorial on it.

Root Motion moves the transform object as well yes (has to be enabled on the animator component).
In my implementation of climbing edges (3D) I use Root Motion + Custom Transition simultaneously and can weight how much each one is used for creating the Combined motion. However, I found that for the use-cases and animations I use, just using a Custom Transition works best (it just works - independently from the animation or climb height - out of the box).

Ok !
Then how do you do a custom transition ? i quickly looked on google but i’m not sure i found something apropriate here : i found tutorial about how to make a transition between two scenes, using the timeline. is that the same you recommand ?

By the way i export the scene i’m working on if you would like to have a look :slight_smile:
the link to download the package : Smash
i hope i did well (first time i export a package…)

Some names are in french, so if you need translation or explanation, i’ll be glad to help of course.
thanks !

i did another try with the teleport option :
instead of replacing a gameObject position for the player in the end, i tried something simpler and less “prettier” with a simple new Vector 2 :

private void Teleportation()
    {
        isHanging = false;
        if(facingRight)
        {
            player.transform.position = new Vector2(player.transform.position.x + 3.0f, player.transform.position.y + 19.8f);
        }
        else if (!facingRight)
        {
            player.transform.position = new Vector2(player.transform.position.x - 3.0f, player.transform.position.y + 19.8f);
        }    
    }

i had to add a condition depending of the player facing direction, and i simply applied numbers given by the comparaison of the position of two gameobjects : the first on the ground where the player standed before the jump, the other over the edge of the ledge where the player would stand in the end.

it works better than my previous attempts with a gameobject child of the player on his hands giving the NewPosition…
but it’s not perfect yet. i notice there’s a frame with the player in between the two position it’s so fast you could not see it every time, nor on this gif capture below maybe. i don’t know why and what it is… i’m looking for it.

and one other thing, a bit annoying, is since the camera is following the player, this abrupt transition gives an abrupt “jumpcut” feeling when the camera align its position to the player’s new position.
Maybe there is a way to smooth that ?

thanks :slight_smile:

Your code in the packege won’t compile because some scripts are missing.

That one frame issue you experience now is the one I was talking about earlier:

“The problem I see with moving only the sprite(s) and then teleporting the physics object afterwards to the climbed position achieved by the sprite(s) animation is that you have to sync the sprite(s) position(s) back to the physics objects position after the teleportation. I guess you do this as well with the help of the animator but I am afraid you may run into 1 frame offset-issues (or other transition issues) where your character may snap from the wrong position to the correct one.”

You won’t be able to resolve this issues and your camera issue with your current approach that easily I am afraid. Therefore I recommend the custom transition approach. A custom transition is in the most simplified way:

transform.position = Vector3.Lerp(startpos, endpos, elapsedTimeSinceStart / Duration);

You can for example write a coroutine where you move your player from bottom to top and then from top to a little bit forward so that he makes it over the edge and therefore on top of the platform. Just make sure your rigidbody2D.isKinematic is true when you climb and back to default when you reached the top.

I can’t help you with the whole implementation of a full character controller (no time for that xD). I recommend you search for good tutorials to understand why you are doing what :).

Kreshi, thanks ! :slight_smile:

sorry for the missing scripts, i must have missed something to the export of the package. I just avoid to add all the unused scripts i got for this working scene… but maybe i forgot some really used ! i could start again if you’re interested.

ok, i see the problems for this “Teleport” method…
At least im’ glad i get to the end of that option. i was not happy to stay stucked…

i will try your suggestion. i’m a bit afraid to not understand very well or at all the coroutine and all, but i have to try (as you see, i’m not very confortable with codes yet :smile: )

i have to stop for a week away from Unity.
i’ll be back and post some news ! :slight_smile:

“I can’t help you with the whole implementation of a full character controller (no time for that xD)”

Too bad for me ! :smile:
well, thank you for your advices, it’s already a lot for me !

1 Like

I see. There is a free 2D character controller in the asset store that looks pretty powerful judging from the marketing material - have a look at it, maybe it’s helpful for you :slight_smile: : 2D Flexible Platformer Controller

Oh ! how could have missed this one ?!
thanks, it will be very helpful in deed. Thanks

I had a look on this package, but it’s way too complex for a beginner like me to customize it (i tried with replacing xith my animations, even that is really complex with the project organisation) and read or edit the code is too complex too (there a tons of codes, called everywhere).
i think i’would stay with my little script to improve instead ! it would be less powerful but at least i could control things or understand more.

so next week i’ll begin with the coroutine and custom transition. we’ll see ! :wink:

I did a little improvement on the “teleport” option :
i create an intermediate animation between the end of the climbing animation and the return to the idle animation.
it’s just 1 frame, that copy the idle animation but with the collider on the top of the edge, with an animation event that call a function in my script that command the idle animation to be played and the teleportation thingy i posted earlier (in that order).
it works better : the litte “dropframe” i had is gone.

[EDIT]
and for the jumcut effect that annoys me :
i adjust the X and Y damping of my Virtual Camera (cinemachine), wich was set at 0.
it i thing it’s fine now !

next step, i would try the Kreshi’s suggestion with custom transition…!

Thanks ! :wink: