Stick 2D movement to the ground

Hi everyone

I’m trying to replicate the mechanic of a game i used to play when i was a child, but i’m having problems with the movement system.

In the game, the character never gets off the ground while moving, and rotates along the slopes, just like that:

7550203--933265--Movement_Example.gif

To move, i wrote the following code running in Update method:

void Walk()
    {
        float direction = Input.GetAxis("Horizontal");
        playerRb.velocity = new Vector2(direction * speed, playerRb.velocity.y);
    }

I also wrote a code to calculate the angle of the ground below the character and rotate it properly. Basically i’m using the angle difference between a Linecast point down in the front and back of the character (Gizmos of that can be seen in the Gif below)

void SlopeCheck()
    {
Vector3 frontPos = frontHit.point;
Vector3 backPos = backHit.point;
Vector3 lookDir = frontPos - backPos;
float SlopeAngle = Mathf.Atan2(lookDir.y, lookDir.x) * Mathf.Rad2Deg;

Quaternion charRotation = Quaternion.Euler(0, 0, SlopeAngle);
this.transform.rotation = charRotation;
}

7550203--933271--Bump.gif

7550203--933274--Weird_Fall.gif

The problem is that when i move fast from left to to right, the character bumps from the ground, and when he falls from a corner, it rotates very weirdly while in mid air, as in the GIFs above.

So how could I implement that mechanic properly? Couldn’t find any good tutorials on that, can anyone help me with an approach to solve this problems?

Thank you :smile:

For starters, if you’re using 2D physics, don’t modify the Transform. The Rigidbody2D is in control of the pose so use it’s API.

Total guess but I guess your Rigidbody2D here is a Dynamic rigidbody but you’re also donig Kinematic (explicit) pose changes (rotation). This is setting up two opposing systems. You’re rotating (via the Transform) manually so that’s Kinematic motion but I guess you’ve got a Dynamic body that’s trying to reproduce Dynamic motion with gravity, velocity etc. Those two are not going to work in harmony unless you’re careful.

What you’ve done, sort of works, but as you can see it fails. It means your controller model is too simplistic and you’ve found edge-cases where it doesn’t work.

Some things that can make it worse before you start is not understanding when physics runs (by default) which is during the fixed-update. You go and modify stuff per-frame expecting physics to be running per-frame when it isn’t. You can, of course, go into the 2D physics settings and change it to per-frame but you need to be aware of this and what the FixedUpdate means.

In the absolutely worst case, your Dynamic body (on the Box) should be set to not rotate on its own so lock the Z rotation on the Rigidbody2D. This means YOU must control the rotation and use velocity and gravity to do the rest. If you’ve not done this then it’ll be rotating itself because of its angle no slopes and/or corners touching the ground. I would suggest, in the very least, that you perform three linecasts, one from each lower corner and a mid-point to the ground. Using these, you can calculate the optimal angle so it doesn’t intersect. Also, often you have a rotate speed and move quickly but not instantly which stops it from being so jittery and actually damps the motion.

You can actually see this issue in your video when you move from one hump to another so the leading corner intersects because the central aligning isn’t optimal.

There ARE lots of example videos and tutorials on this subject so maybe you’re just looking for something too specific.

1 Like

Thanks for the answer! I’ve made some changes based on what you said, and it improved a lot. All physics related code are now in the FixedUpdate. For rotation, I’m using Rigidbody2D.SetRotation instead of changing the transform. The bumps mentioned before were been caused by the momentum of the movement, since I was only moving on the X axis, that was fixed by using the code below for movement:

void Walk()
    {
       
        if (direction != 0 && isGrounded)
        {
            boxCol.sharedMaterial = normalFriction;
            playerRb.MovePosition(transform.position + lookDir * direction * speed);
        }
        else
        {
            boxCol.sharedMaterial = fullFriction;
        }

    }

lookDir is the direction given by the two Raycasts pointing down (one in the front and one in the back of the character, as mentioned before), this way the character always moves in the direction he is rotated to, not only in the X axis.

I’m almost finishing this system, but I stepped on one last problem. When I’m on a edge, the character starts flicking, because the rotation is too extreme, and when he rotates the first time, the Raycasts rotate with him, recalculating the rotation angle, causing him to rotate again, and so on…

This can be better vizualized in the GIF below, the green gizmos are the raycasts, and the red gizmo is the angle given by the two points.

7580653--939604--FlickeringOnEdge.gif

Any ideias on how could I avoid this? Or maybe other way of calculating rotation besides the two Raycasts?

This is a problem that occurs largely because you’re dealing with a static collision shape and it’s a constant one you see in all games. Interesting how many people want to have a look at it and get it absolutely perfect, the only way I could see it potentially getting solved is if you had some kind of adaptive collision shape that changes according to the ground. This would take a lot of coding for the effect you’re trying to introduce.

As for the actual issue you’re experiencing now well the problem itself seems to be that one raycast is snapping to the ground below and the other is trying to attach itself to the ground that the box is still on causing a conflict. A simple fix for this would be to have different colliders for each height of ground and do some if checks to have the box behave correctly dependent on what’s going on.

This is a very persistent issue though and sometimes you have to just live with it and make do by getting the behaviour to work in an acceptable fashion with some sensible level design.

At first I also though it was a collision problem, so I disabled the box collider of the character and tested, the result was the same. The cause of this is that the reference points to the raycasts are parented to the character, so when the character rotates, the raycasts direction rotate too, causing the character to recalculate its angle of rotation, causing another recalculation, and so on.

What I’m tryng to get is an alternative way to calculate the ground angle below the character besides using that two raycasts pointing down system. Or just some approach to avoid the flickering with this system.

I should be clearer with what I meant but it’s good that you’re working through it, what I mean is, even if you develop nearly ‘perfect’ raycasting, if you have awkward bumps and gaps within your terrain you will never get that 100% accurate sticking to the ground effect you want. I’ve had to point this out with people doing this kind of thing before and with building placement too and one person who was doing a car. When you create your character or shape, you must have it match with the surface you’re moving it along.

This is why for example you have ‘voxel’ based games where a character is a cube and everything they interact with are cubes because on each face you’ll be able to calculate everything perfectly. Same goes for these spherical world type games like mario galaxy, they simply made a straightforward sphere rather than do anything complicated and that’s how they get such perfect movement.

It’s more down to the design of your levels/characters than the code you’re writing, one thing I have seen though which you might be vaguely interested in is mesh/terrain deformation. If you’ve seen those really fancy snow and mud effects in simulator games with trucks or tanks driving over them the way they’ve done it is they’ve actually changed the mesh to match the shape of whatever is moving over it or in this case driving. This is a very advanced technique though and likely not necessarily what you’re looking for which is why I recommend you rely on sensible level design unless you’re completely obsessed with getting the effect you want.

To give you a more relatable example, you could create a character that has multiple bending and flexing parts, like a centipede and have the raycasts change individual parts as they’re going along. There’s also recent examples in unity where they’ve used inverse kinematics to walk over uneven terrain but this relies on good character design where what you’ve created makes sense to bend itself and adapt like a spider. You simply can’t for example have just a straight cube trying to go across extremely uneven terrain or surfaces.

I agree with the above and it’s true that your movement model has encountered terrain that it doesn’t deal with well so it’s either design your level differently or change the movement model for this edge-case. You’re likely to find more such as if you decide to move quicker or when you land if you were to add a jumping dynamic later.

Well for the two ray-casts that’s RaycastHit2D.normal. However you’re sampling two points, not even a center-point so you might get UP on the right-cast and LEFT on the left-cast; you need to know how to deal with these.

If it were me, I’d perform more ray-casts down from the base of the character and work out some average/weight distribution to rotate the character to try to balance them to be the same distance (possibly with a bias so that it cares more about the ends being closer). I used to do signal processing as a living so when I see stuff like this it reminds of lessons learned with Nyquist theorem. Not quite the same thing but if you treat your terrain as a continuously changing signal, your character is sampling that. Right now you’re performing two samples and it’s just not enough to reconstruct the terrain given such a sharp discontinuity. Best to read the normals from those hits in the very least and maybe, look at performing a configurable amount of them and some way to analyse (or just average) those normals.