0
votes

I'm working on a 2D project in Unity.

The character controller is physics based, so I use rigidbody to move the player. Everything is working fine except when I try to apply a high speed movement to the character, like a dash.

This is how the code looks like.

I just check if the player is dashing, so I increase the Vector2 movement in a certain amount.

    private void DashMovement() {
        if (isDashing) {
            movement.x *= dashFactor;
        }
    }

I'm also calculating the ground angle, so I set the movement vector to follow the ground inclination.

     private void OnSlopeMovement() {
        if (isGrounded && !isJumping) {
            float moveDistance = Mathf.Abs(movement.x);
            float horizontalOnSlope = Mathf.Cos(groundAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(movement.x);
            float verticalOnSlope = Mathf.Sin(groundAngle * Mathf.Deg2Rad) * moveDistance;

            if (horizontalOnSlope != 0)
                movement.x = horizontalOnSlope;
            if (isGrounded && verticalOnSlope != 0)
                movement.y = verticalOnSlope;
        }

        SetMaxFallVelocity();
    }

So I set the rigidbody velocity for making it move.

    private void Move() {
        movement.x *= Time.fixedDeltaTime;
        if(isGrounded && !isJumping) movement.y *= Time.fixedDeltaTime;
        Vector3 targetVelocity = new Vector2(movement.x, movement.y);

        PlayerController.rb2d.velocity = Vector3.SmoothDamp(PlayerController.rb2d.velocity, targetVelocity, ref velocity, movementSmoothing);
    }

The problem appears when I apply a speed high enough. I understand this issue is because of physics. I think the ray that checks the ground and is used to calculate the groundAngle doesn't work fast enough to keep track of that movement, so I can not keep the player fixed on the ground.

I would like to find a solution without making the player kinematic, or stopping the dash on slopes.

This is how it looks ingame.

And this is how the rigidbody movement remain right over the ground, following the slopes angle.

EDIT:

This is how I get the ground angle:

private void GroundAngle() {
        Vector2 rayOrigin = feetCollider.bounds.center;
        rayOrigin.y += 0.1f;
        Vector2 rayDirection = (Input.GetAxisRaw("Horizontal") == 0) ? Vector2.right : new Vector2(Input.GetAxisRaw("Horizontal"), 0);

        int groundCollisions = Physics2D.RaycastNonAlloc(rayOrigin, Vector2.down, groundResults, Mathf.Infinity, groundMask);
        if (groundCollisions > 0) {
            groundAngle = Vector2.Angle(groundResults[0].normal, rayDirection) - 90f;
            //Debug.DrawRay(rayOrigin, Vector2.down, Color.green);
            if (groundAngle > 0 && !isDashing) {
                rayOrigin.x += Input.GetAxisRaw("Horizontal") * .125f;
                Physics2D.RaycastNonAlloc(rayOrigin, Vector2.down, groundResults, Mathf.Infinity, groundMask);
                groundAngle = Vector2.Angle(groundResults[0].normal, rayDirection) - 90f;
                //Debug.DrawRay(rayOrigin, Vector2.down, Color.blue);
            }
        } 
    }

Thanks to @Ruzhim for the help. I just post a first "solution" for the problem. According to Ruzhim advises, I've used him code this way.

private void SetPositionAfterTick() {
        if (isDashMovement) {
            Vector2 currentPosition = new Vector2(transform.position.x, transform.position.y);
            currentPosition.y = feetCollider.bounds.min.y;
            Vector2 feetPosAfterTick = currentPosition + PlayerController.rb2d.velocity * Time.deltaTime;

            float maxFloorCheckDist = .1f;

            RaycastHit2D groundCheckAfterTick = Physics2D.Raycast(feetPosAfterTick + Vector2.up * maxFloorCheckDist, Vector2.down, maxFloorCheckDist * 5f);

            if (groundCheckAfterTick) {
                Vector2 wantedFeetPosAfterTick = groundCheckAfterTick.point;

                if (wantedFeetPosAfterTick != feetPosAfterTick) {
                    //PlayerController.rb2d.transform.position = (wantedFeetPosAfterTick + new Vector2(0f, feetCollider.bounds.min.y - PlayerController.rb2d.position.y));
                    PlayerController.rb2d.velocity = Vector2.zero;
                }
            }
        }
    }

This is how it looks like.

This is good enough to continue polishing that mechanic. I still need to set the position in some way. The rigidbody's position calculation is not working as it is raised right now, as the condition (wantedFeetPosAfterTick != feetPosAfterTick) is always true, so the character goes throw the floor and fall.

As you can see, I also need to control the down slopes movement, as it uses the slopes movement sometimes, and dash straight forward others.

1
I personally think dash-into-a-ramp-to-fly looks like a fun gameplay mechanic but i'm an old tribes player ;)Ruzihm
This interaction is normal for that speed, the only way you could really manipulate the results when using physics based interactions, would be to add possibly some kind of physics material to your slope, but that would likely slow your guy down, or adjusting his mass? I tend to remove physics from my player movement and do my own calculations to "fake" physics, so a skill like this I would just project where he is moving to, create an effect to go over the region and move him there after the effect ends...AresCaelum
But as @Ruzihm said though, that looks like it could be a fun feature.AresCaelum
Yeah, I just thought on it the first time I saw it, but it is not in my opinion I kind of think that may happen for every single slope. Would be a nice mechanic on certain points and being intentionally to achieve something. And about your answer @AresCaelum, I just want to spend my possibilities just before changing my controller.Rubzero
What happens if you dash down a slope?Ruzihm

1 Answers

3
votes

This is how asker Rubzero implemented the below code to work for them:

private void SetPositionAfterTick() {
    if (isDashMovement) {
        Vector2 currentPosition = new Vector2(transform.position.x, transform.position.y);
        currentPosition.y = feetCollider.bounds.min.y;
        Vector2 feetPosAfterTick = currentPosition + PlayerController.rb2d.velocity * Time.deltaTime;

        float maxFloorCheckDist = .1f;

        RaycastHit2D groundCheckAfterTick = Physics2D.Raycast(feetPosAfterTick + Vector2.up * maxFloorCheckDist,
                Vector2.down, maxFloorCheckDist * 5f);

        if (groundCheckAfterTick) {
            Vector2 wantedFeetPosAfterTick = groundCheckAfterTick.point;

            if (wantedFeetPosAfterTick != feetPosAfterTick) {
                //PlayerController.rb2d.transform.position = (wantedFeetPosAfterTick + new Vector2(0f, feetCollider.bounds.min.y -
                        PlayerController.rb2d.position.y));
                PlayerController.rb2d.velocity = Vector2.zero;
            }
        }
    }
} 

This is how it looks like.

This is good enough to continue polishing that mechanic. I still need to set the position in some way. The rigidbody's position calculation is not working as it is raised right now, as the condition (wantedFeetPosAfterTick != feetPosAfterTick) is always true, so the character goes throw the floor and fall.

As you can see, I need to control the down slopes movement, as it uses the slopes movement sometimes, and dash straight forward others.


I agree with AresCaelum; using physics to do slope movement is pretty much the opposite of what you want to be doing if you don't want to preserve momentum when you're done going up/down the slope. Specifically, your problem is here:

float moveDistance = Mathf.Abs(movement.x);
float horizontalOnSlope = Mathf.Cos(groundAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(movement.x);
float verticalOnSlope = Mathf.Sin(groundAngle * Mathf.Deg2Rad) * moveDistance;

This is a problem because the more the player moves horizontally in a frame, the more they will move vertically based on the slope of the ramp they are on. However, this assumption doesn't hold if they should only be traveling up the ramp during only part of the movement during the frame. So, you need a way to handle that situation.

One solution is to use a raycast from where the player would be then if it's above the floor, alter the vertical velocity so that it would place them at that floor's position instead.

First, determine if slope movement has occurred in a physics frame...

 private bool slopeMovementOccurred = false;

 void FixedUpdate() {
     slopeMovementOccurred = false;

     // ...
 }

 private void OnSlopeMovement() {
    if (isGrounded && !isJumping) {
        slopeMovementOccurred = true; 

        // ...
    }

    SetMaxFallVelocity();
}

... and if it has, determine where the player is going to be after the physics update. Then do a physics2d raycast from above that position (by some amount) downward (double the previous amount) to find where the player's position should be, and then change the rb2d.velocity such that it will place the player exactly at the height they should be at.

Assuming you can calculate some kind of Vector2 feetOffset that has the local position of the player's feet:

void FixedUpdate() {
    // ...

    StickToSlopeLanding();
}

void StickToSlopeLanding() {
    if (slopeMovementOccurred) {
        Vector2 curVelocity = PlayerController.rb2d.velocity;
        Vector2 feetPosAfterTick = PlayerController.transform.position 
                + PlayerController.feetOffset 
                + curVelocity * Time.deltaTime;

        float maxFloorCheckDist = 1.0f;

        // determine where the player should "land" after this frame
        RaycastHit2D groundCheckAfterTick = Physics2D.Raycast(
                feetPosAfterTick + Vector2.up * maxFloorCheckDist, 
                -Vector2.up, maxFloorCheckDist * 2f);

        if (groundCheckAfterTick.collider != null) {
            Vector2 wantedFeetPosAfterTick = groundCheckAfterTick.point;

            // if basic physics won't take them to landing position
            if (wantedFeetPosAfterTick != feetPosAfterTick) {
                Vector2 wantedVelocity = curVelocity 
                        + Vector2.up 
                        * ((wantedFeetPosAfterTick.y - feetPosAfterTick.y)
                            / Time.deltaTime);

                // adjust velocity so that physics will take them to landing position
                PlayerController.rb2d.velocity = wantedVelocity;

                // optionally, set a flag so that next frame 
                // it knows the player should be grounded
            }
        }
    }
} 

Hopefully this gets you towards a solution that will work.


Note: you may need to also move the rigidbody so that it doesn't try to clip through the corner at the top of the ramp, and you can determine where to put the rigidbody using another raycast, setting the velocity from that point to be horizontal:

void StickToSlopeLanding() {
    if (slopeMovementOccurred) {
        Vector2 curVelocity = PlayerController.rb2d.velocity;
        Vector2 feetPosAfterTick = PlayerController.transform.position 
                + PlayerController.feetOffset 
                + curVelocity * Time.deltaTime;

        float maxFloorCheckDist = 1.0f;

        // determine where the player should "land" after this frame
        RaycastHit2D groundCheckAfterTick = Physics2D.Raycast(
                feetPosAfterTick + Vector2.up * maxFloorCheckDist, 
                -Vector2.up, maxFloorCheckDist * 2f);

        if (groundCheckAfterTick.collider != null) {
            Vector2 wantedFeetPosAfterTick = groundCheckAfterTick.point;

            // if basic physics won't take them to landing position
            if (wantedFeetPosAfterTick != feetPosAfterTick) {

                // look for corner of ramp+landing. 
                // Offsets ensure we don't raycast from inside/above it
                float floorCheckOffsetHeight = 0.01f;
                float floorCheckOffsetWidth = 0.5f;
                RaycastHit2D rampCornerCheck = Physics2D.Raycast(
                        wantedFeetPosAfterTick 
                        - floorCheckOffsetHeight * Vector2.up 
                        - floorCheckOffsetWidth * Mathf.Sign(movement.x) * Vector2.right,
                        Mathf.Sign(movement.x) * Vector2.right);

                if (rampCornerCheck.collider != null) {
                    // put feet at x=corner position
                    Vector2 cornerPos = Vector2(rampCornerCheck.point.x, 
                            wantedFeetPosAfterTick.y);

                    PlayerController.rb2d.position = cornerPos 
                            - PlayerController.feetOffset;
                    // adjust velocity so that physics will take them from corner 
                    // to landing position
                    Vector2 wantedVelocity = (wantedFeetPosAfterTick - cornerPos) 
                            / Time.deltaTime;

                    PlayerController.rb2d.velocity = wantedVelocity;

                    // optionally, set a flag so that next frame 
                    // it knows the player should be grounded
                }
            }
        }
    }
}