Post-7 listed eight possible causes for the directional landing stick, but some of the analysis was wrong. After re-reading the code with fresh eyes, here is what is actually happening &mdash and what to do about it.
What Was Wrong With Post-7
Several suggestions targeted problems that don't exist in this codebase:
- MoveTowards friction (Options 1, 4): This code is active every time
move == 0f, not just on landing. It has been running since post-5's patch was applied. If friction were the cause, the character would stick every time you release a key — not just after landing. The directional stick is specific to landing moments, so friction is not the root cause. - Coyote timer reset (Option 3): The coyote counter only affects jump buffering, not horizontal movement. It has zero influence on whether the character can move left or right after landing.
- Contact offset (Option 6): Changing this would shift when collisions are detected, but the movement code sets velocity after collision checks in the same Update() frame. Even with an earlier contact offset, the velocity overwrite at line 177 still happens on the same frame and still risks pushing the rigidbody into the collider.
- PhysicMaterial2D friction (Option 7): The movement code overwrites velocity every frame regardless of material friction. A friction material would be overridden by line 177's direct assignment.
- Slope angles (Option 8): This would cause sticking on slopes, not on flat ground. If the bug appears on flat surfaces, this is irrelevant.
What Is Actually Happening
The real issue is simpler than the list in post-7 suggested. It comes down to one interaction:
When the player is moving and lands, the sequence is:
OnCollisionStay2DsetsisGrounded = true.- On the next
Update(),moveis still non-zero (player is still holding the key). - Line 177 executes:
rb.linearVelocity = new Vector2(move * speed, rb.linearVelocity.y). - This sets horizontal velocity to full speed while the character is already in contact with the ground.
- But — and this is the key — if the landing involved any vertical bounce or micro-oscillation, the rigidbody may have been repositioned by the physics solver in a way that leaves it slightly intersecting the ground collider.
- Unity's collision resolution pushes the rigidbody out and nullifies velocity components that would cause further penetration. This nullification can include the horizontal component, especially if the contact normal has any sideways angle.
- The character is now grounded with zero (or near-zero) horizontal velocity. The next frame,
moveis still held, so line 177 runs again — but the same collision state may persist for another frame, and the cycle repeats.
This explains why:
- It only happens after landing — the collision state is the trigger.
- It's directional — landing while moving into a wall or edge creates a more asymmetric contact, increasing the chance of horizontal nullification.
- Jumping over the spot fixes it — jumping resets
isGroundedto false, breaking the collision loop. - Going opposite and back doesn't help — the collision state hasn't changed, only the direction.
- It's ~0.1s — roughly one physics frame at Unity's default 50Hz timestep.
The Fix
The simplest fix is to not overwrite velocity on the first grounded frame after being airborne. This gives the collision solver one frame to settle before accepting new input:
// Add these fields (around line 32):
private bool wasGroundedLastFrame;
// Replace lines 172-185 with:
if (!isWallJumping)
{
if (move != 0f)
{
rb.linearVelocity = new Vector2(move * speed, rb.linearVelocity.y);
}
else
{
float newVelX = Mathf.MoveTowards(rb.linearVelocity.x, 0f, 10f * Time.deltaTime);
rb.linearVelocity = new Vector2(newVelX, rb.linearVelocity.y);
}
}
// Add this at the end of Update(), after line 307 (before the closing brace):
wasGroundedLastFrame = isGrounded;
Actually — that's still not right. The problem isn't the first grounded frame. The problem is that isGrounded can flicker between true and false when the rigidbody is micro-bouncing on the surface. Each flicker gives the physics solver a chance to nullify velocity.
The real fix is to clamp the horizontal velocity instead of overwriting it, and let the movement code only add to existing velocity rather than replacing it entirely:
// Replace lines 172-185:
if (!isWallJumping)
{
if (move != 0f)
{
// Only set speed if within range, don't overwrite existing momentum
if (Mathf.Abs(rb.linearVelocity.x) < speed)
{
rb.linearVelocity = new Vector2(move * speed, rb.linearVelocity.y);
}
// If already faster than max speed, leave it (don't fight the physics solver)
}
else
{
float newVelX = Mathf.MoveTowards(rb.linearVelocity.x, 0f, 10f * Time.deltaTime);
rb.linearVelocity = new Vector2(newVelX, rb.linearVelocity.y);
}
}
But honestly, the cleanest fix is even simpler. Remove the friction patch entirely and go back to the original instant-velocity behavior, but add one guard: skip velocity overwrite when isGrounded just flipped true:
// Fields (around line 32):
private bool wasGroundedLastFrame;
// Lines 172-185:
if (!isWallJumping)
{
if (move != 0f)
{
rb.linearVelocity = new Vector2(move * speed, rb.linearVelocity.y);
}
else
{
rb.linearVelocity = new Vector2(0f, rb.linearVelocity.y);
}
}
// End of Update():
if (isGrounded && !wasGroundedLastFrame)
{
rb.linearVelocity = new Vector2(rb.linearVelocity.x * 0.8f, rb.linearVelocity.y);
}
wasGroundedLastFrame = isGrounded;
This applies a small velocity dampener on the exact frame landing is detected, which absorbs the micro-bounce without killing horizontal momentum. The 0.8f multiplier reduces velocity by 20% on the landing frame — enough to prevent the physics solver from nullifying it, but not enough to feel like a hard stop.
Why Post-7 Was Wrong
The earlier post treated each suggestion as equally plausible without verifying which ones actually applied to the code as it exists. It mixed up symptoms (friction slowing the player) with causes (collision state nullifying velocity). It also suggested editor-level fixes for problems that are entirely in the script — the character collider size and physics material have nothing to do with velocity overwrite logic that runs every frame regardless.
The actual fix is in the script. The issue is that line 177's unconditional velocity overwrite fights the physics solver on landing frames. The dampener at the end of Update() absorbs the landing energy before the solver can react.