'RaycastHit2D normal value is not correct near the edge of a collider
I have a simple 2D scene with a character and 2 ground prefabs. One ground is flat and to the right of it is an angled one, which serves as a ramp. When moving from the player to the right, they get stuck on the transition to the second, angled ground platform.
I will attach the player controller script below, but basically it processes the y movement first, saving the ground normal if it collides with anything. Then it processes the x movement next, projecting the velocity into direction perpendicular to the normal if the player is grounded. The problem arises because the ground normal isn't correct and the player gets stuck.
I assured that the raycast is colliding with the second, ramped collider, not the first one. Both ground platforms have kinematic rigidbodies.
Stack Overflow is stingy about gif upload size, so here is a link to a video showing my bug: https://youtu.be/35AwyluX0Bo. The red line shows the currently cached ground normal and the blue line shows the attempted player velocity (gravity and x movement). Green lines/boxes are the borders for the 2D box colliders.
public class PlatformerCharacter : MonoBehaviour
{
public Rigidbody2D body_;
public ContactFilter2D filter_;
public CharacterKinematicData kinematicData_;
[Header("Collision Check Points")]
[Space]
public Transform groundCheckPoint_;
public Transform headCheckPoint_;
[Header("Input")]
[Space]
public PlayerActions playerActions_;
public GameEventListener jumpListener_;
private Vector2 lastGroundNormal_;
private Vector2 lastVelocity_;
public void JumpResponse()
{
if (kinematicData_.jumpsRemaining_ > 0)
{
kinematicData_.jumpsRemaining_--;
lastVelocity_ = Vector2.up * kinematicData_.initialJumpVelocity_;
}
}
private void FixedUpdate()
{
// input: start with current velocity
Vector2 vel = lastVelocity_;
// apply changes to the X and Y components of the velocity to predict where we are going
vel = ControlX(vel);
vel.y += (kinematicData_.gravity_ * Time.fixedDeltaTime);
Debug.DrawRay(groundCheckPoint_.position, vel * Time.fixedDeltaTime, Color.blue);
// cache the old position
Vector2 oldPosition = body_.position;
// move the y velocity first
MoveY(vel.y);
// then move the x velocity
MoveX(vel.x);
// set last velocity
lastVelocity_ = (body_.position - oldPosition) / Time.fixedDeltaTime;
// set new position
kinematicData_.position_ = body_.position;
kinematicData_.velocity_ = lastVelocity_;
Debug.DrawRay(groundCheckPoint_.position, lastGroundNormal_, Color.red);
}
private Vector2 ControlX(Vector2 curVel)
{
curVel.x = playerActions_.moveDirection_ * kinematicData_.maxMoveSpeed_ * (kinematicData_.isGrounded_ ? 1.0f : kinematicData_.airControlModifier_);
return curVel;
}
private void MoveY(float yVelocity)
{
if (CheckGround())
{
MoveYGrounded(yVelocity);
}
else
{
MoveYUngrounded(yVelocity);
}
}
private bool CheckGround()
{
RaycastHit2D hit = Physics2D.Raycast(groundCheckPoint_.position, Vector2.down, kinematicData_.skinWidth_, filter_.layerMask);
if (hit)
{
Debug.DrawLine(hit.normal + hit.point, hit.point, Color.green);
Ground(hit.normal);
}
else
Unground();
return hit;
}
private void MoveYGrounded(float yVelocity)
{
float yDistance = Mathf.Abs(yVelocity * Time.fixedDeltaTime);
Vector2 moveDirection = (Vector2.up * yVelocity).normalized;
if (yVelocity > 0) // moving up
{
RaycastHit2D hit = Physics2D.Raycast(headCheckPoint_.position, moveDirection, yDistance + kinematicData_.skinWidth_, filter_.layerMask);
if (hit)
{
if (hit.distance >= kinematicData_.skinWidth_)
{
Unground();
body_.position += (moveDirection * (hit.distance - kinematicData_.skinWidth_));
}
}
else // moving up and did not hit anything
{
Unground();
body_.position += (moveDirection * yDistance);
}
}
else // not moving up
{
RaycastHit2D hit = Physics2D.Raycast(groundCheckPoint_.position, moveDirection, yDistance + kinematicData_.skinWidth_, filter_.layerMask);
if (!hit) throw new System.Exception("could not find ground after checkGround returns true");
Debug.Log(hit.collider.gameObject.name);
lastGroundNormal_ = hit.normal;
if (hit.rigidbody)
{
Debug.Log("hit a rigidbody");
body_.position += hit.rigidbody.velocity * Time.fixedDeltaTime;
}
else
{
body_.position += (moveDirection * (hit.distance - kinematicData_.skinWidth_));
}
}
}
private void MoveYUngrounded(float yVelocity)
{
float yDistance = Mathf.Abs(yVelocity * Time.fixedDeltaTime);
Vector2 moveDirection = (Vector2.up * yVelocity).normalized;
if (yVelocity > 0) // moving up
{
RaycastHit2D hit = Physics2D.Raycast(headCheckPoint_.position, moveDirection, yDistance + kinematicData_.skinWidth_, filter_.layerMask);
if (hit)
{
Unground();
body_.position += (moveDirection * (hit.distance - kinematicData_.skinWidth_));
}
else // moving up and did not hit anything
{
Unground();
body_.position += (moveDirection * yDistance);
}
}
else // not moving up
{
RaycastHit2D hit = Physics2D.Raycast(groundCheckPoint_.position, moveDirection, yDistance + kinematicData_.skinWidth_, filter_.layerMask);
if (hit)
{
Ground(hit.normal);
body_.position += (moveDirection * (hit.distance - kinematicData_.skinWidth_));
// Could do something here with unmoved distance, like move downslope if the angle is too steep
}
else
{
body_.position += (moveDirection * yDistance);
}
}
}
private void MoveX(float xVelocity)
{
float xDistance = Mathf.Abs(xVelocity * Time.fixedDeltaTime);
Vector2 moveDirection = (Vector2.right * xVelocity).normalized;
if (CheckGround())
{
// project x velocity onto the slope of the ground
if (xVelocity >= 0)
{
moveDirection = PerpendicularClockwise(lastGroundNormal_).normalized;
}
else
{
moveDirection = PerpendicularCounterClockwise(lastGroundNormal_).normalized;
}
//xDistance += (Vector2.Dot(lastGroundNormal_, moveDirection) * moveDirection.x);
}
RaycastHit2D hit = Physics2D.Raycast(groundCheckPoint_.position, moveDirection, xDistance + kinematicData_.skinWidth_, filter_.layerMask);
if (hit)
{
body_.position += (moveDirection * hit.distance);
}
else
{
body_.position += (moveDirection * xDistance);
}
}
private void Ground(Vector2 groundNormal)
{
lastGroundNormal_ = groundNormal;
if (!kinematicData_.isGrounded_)
{
kinematicData_.isGrounded_ = true;
kinematicData_.jumpsRemaining_ = kinematicData_.maxJumps_;
}
}
private void Unground()
{
lastGroundNormal_ = new Vector2();
if (kinematicData_.isGrounded_)
{
kinematicData_.isGrounded_ = false;
kinematicData_.jumpsRemaining_ = 1;
}
}
private static Vector2 PerpendicularClockwise(Vector2 vector2)
{
return new Vector2(vector2.y, -vector2.x);
}
private static Vector2 PerpendicularCounterClockwise(Vector2 vector2)
{
return new Vector2(-vector2.y, vector2.x);
}
}
Thanks in advance for the help. I will be more than happy to update with more details if needed.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
