Hard to know what the issue is but I’ll share the internal C++ code here for the Slide method. In theory, you could implement this in C#, I don’t think I used any special sauce when I wrote it. Maybe it’ll help figure out how it actually works or implement your own.
Rigidbody2D::SlideResults Rigidbody2D::Slide(const Vector2f& velocity, const float deltaTime, const SlideMovement& slideMovement)
{
PROFILER_AUTO(gPhysics2DProfileRigidbody2D_Slide, this);
// Fetch configuration.
const int maxIterations = slideMovement.maxIterations;
const float surfaceSlideAngle = slideMovement.surfaceSlideAngle;
const float gravitySlipAngle = slideMovement.gravitySlipAngle;
const Vector2f surfaceUp = NormalizeSafe(slideMovement.surfaceUp, Vector2f::yAxis);
const Vector2f startPosition = slideMovement.startPosition;
const bool useStartPosition = slideMovement.useStartPosition;
const bool useNoMove = slideMovement.useNoMove;
const bool useSimulationMove = slideMovement.useSimulationMove;
const bool useAttachedTriggers = slideMovement.useAttachedTriggers;
const bool useCustomLayerMask = slideMovement.useLayerMask;
const UInt32 customLayerMask = slideMovement.layerMask.m_Bits;
Collider2D* selectedCollider = slideMovement.selectedCollider;
const Vector2f gravity = slideMovement.gravity;
Box2D::b2Vec2 surfaceAnchorDirection = PhysicsUtility2D::Tob2Vec2(slideMovement.surfaceAnchor);
const float surfaceAnchorDistance = surfaceAnchorDirection.Normalize();
const bool useSurfaceAnchor = surfaceAnchorDistance > 0.0f;
// Initial default results.
SlideResults results;
results.position = useStartPosition ? startPosition : GetPosition();
results.remainingVelocity = velocity;
results.slideHit.Reset();
results.surfaceHit.Reset();
// Validate explicit arguments.
RETURN_INVALID_ARG_VECTOR2(velocity, velocity, Slide, Rigidbody2D, results);
RETURN_INVALID_ARG_FLOAT(deltaTime, deltaTime, Slide, Rigidbody2D, results);
// Ignore very small/negative values or if the slide-count is bad or there's no body or physics-scene.
if (deltaTime < PHYSICS_2D_SMALL_RANGE_CLAMP ||
maxIterations < 1 ||
m_Body == NULL || m_PhysicsScene == NULL ||
(useSimulationMove && GetBodyType() == BodyType::kStaticBody))
return results;
// Ignore if we specify a collider and it's not attached to this rigidbody.
if (selectedCollider != NULL && selectedCollider->GetAttachedRigidbody() != this)
return results;
// Calculate the remaining distance and direction.
float remainingDistance = Magnitude(velocity * deltaTime);
// Calculate if we can gravity slide.
const bool canGravitySlip = remainingDistance < PHYSICS_2D_SMALL_RANGE_CLAMP;
// Calculate the direction we want to move.
Box2D::b2Vec2 direction = PhysicsUtility2D::Tob2Vec2(velocity);
direction.Normalize();
// Fetch the current body transform.
Box2D::b2Transform transform = m_Body->GetTransform();
// Set the start position if selected.
if (useStartPosition)
transform.p = PhysicsUtility2D::Tob2Vec2(startPosition);
// Get the surface offset.
const float surfaceOffset = box2d_b2_linearSlop * 2.0f;
// Get the collider(s) to use.
Collider2D::ColliderArray colliders(kMemTempAlloc);
if (selectedCollider == NULL)
{
GetAttachedColliders(colliders, useAttachedTriggers);
}
else
{
colliders.push_back(selectedCollider);
}
// Fetch the collider count.
const int colliderCount = colliders.size();
// Get the collider layer masks.
dynamic_array<UInt32> colliderLayerMasks(kMemTempAlloc);
// Does the contact filter override layer masks?
if (useCustomLayerMask)
{
// Yes, so set all the collider layer masks to use the fixed layer-mask.
for(int i = 0; i < colliderCount; ++i)
colliderLayerMasks.push_back(customLayerMask);
}
else
{
// No, so fetch all the collider layer masks from the layer collision matrix.
const Physics2DSettings& settings = GetPhysics2DSettings();
for (int i = 0; i < colliderCount; ++i)
colliderLayerMasks.push_back(settings.GetLayerCollisionMask(colliders[i]->GetGameObject().GetLayer()));
}
// Hit results.
RaycastHit2D& slideHit = results.slideHit;
RaycastHit2D& surfaceHit = results.surfaceHit;
// Reset the iteration count.
int iterationCount = 0;
// Iterate the slides.
do
{
// Only slide if there's some remaining distance.
if (remainingDistance > PHYSICS_2D_SMALL_RANGE_CLAMP)
{
// Find the slide contact.
if (FindSlideContact(this, direction, remainingDistance, slideHit, m_PhysicsScene, transform, colliders, colliderLayerMasks) > 0)
{
// Yes, so check if the surface is along the move direction.
// NOTE: We filter contacts by this so this should always be the case!
const Vector2f& surfaceNormal = slideHit.normal;
const Vector2f moveDirection = PhysicsUtility2D::ToVector2f(direction);
const float surfaceDot = Dot(moveDirection, surfaceNormal);
if (surfaceDot < PHYSICS_2D_SMALL_RANGE_CLAMP)
{
// Yes, so check if the surface angle is below the surface slide angle.
const float surfaceAngle = Degrees(Angle(surfaceNormal, surfaceUp)) - PHYSICS_2D_SMALL_RANGE_CLAMP;
if (surfaceAngle <= surfaceSlideAngle)
{
// Did we move a significant amount?
if (slideHit.distance > surfaceOffset)
{
// Yes, so calculate the distance we'd like to move.
const float distance = slideHit.distance - surfaceOffset;
// Move the transform by the hit distance.
transform.p += distance * direction;
// Reduce the remaining distance.
remainingDistance -= distance;
}
}
else
{
remainingDistance = 0.0f;
}
}
// Clamp the movement direction.
const Box2D::b2Vec2 hitNormal = PhysicsUtility2D::Tob2Vec2(slideHit.normal);
direction -= b2Dot(direction, hitNormal) * hitNormal;
}
else
{
// No hits so move the transform by the remaining move.
transform.p += remainingDistance * direction;
// No remaining distance so we're done.
remainingDistance = 0.0f;
}
}
// Are we using a surface anchor?
// NOTE: We always want to use the surface anchor, even if there's no remaining distance. We just won't do this if there's no delta-time.
if (useSurfaceAnchor)
{
// Yes, so find a contact for the surface anchor.
if (FindSlideContact(this, surfaceAnchorDirection, surfaceAnchorDistance, surfaceHit, m_PhysicsScene, transform, colliders, colliderLayerMasks) > 0)
{
// Did we find a surface anchor?
if (surfaceHit.distance > surfaceOffset)
{
// Yes, so calculate the distance to move to the surface anchor.
const float distance = surfaceHit.distance - surfaceOffset;
// Move the transform to the surface anchor.
transform.p += distance * surfaceAnchorDirection;
}
}
}
}
// Exit conditions.
while (
++iterationCount < maxIterations &&
remainingDistance > PHYSICS_2D_SMALL_RANGE_CLAMP &&
direction.LengthSquared() > (surfaceOffset * surfaceOffset));
// Are we using gravity?
Box2D::b2Vec2 gravityDirection = PhysicsUtility2D::Tob2Vec2(gravity);
const float gravityDistance = gravityDirection.Normalize() * deltaTime;
if (gravityDistance > box2d_b2_epsilon)
{
// Yes, so find a contact for the gravity movement.
if (FindSlideContact(this, gravityDirection, gravityDistance, surfaceHit, m_PhysicsScene, transform, colliders, colliderLayerMasks) > 0)
{
// Did we move a significant amount?
const float hitDistance = surfaceHit.distance - surfaceOffset;
if (hitDistance > surfaceOffset)
{
// Yes, so move the transform by the hit distance.
transform.p += hitDistance * gravityDirection;
}
// Can we gravity slip?
// NOTE: If the user requests movement then we won't slip.
if (canGravitySlip)
{
// Yes, so check if the surface is along the gravity direction.
// NOTE: We filter contacts by this so this should always be the case!
const Vector2f& surfaceNormal = surfaceHit.normal;
const float surfaceDot = Dot(gravity, surfaceNormal);
if (surfaceDot < PHYSICS_2D_SMALL_RANGE_CLAMP)
{
// Yes, so check if the surface angle is beyond the surface slip angle.
const float surfaceAngle = Degrees(Angle(surfaceNormal, surfaceUp)) + PHYSICS_2D_SMALL_RANGE_CLAMP;
if (surfaceAngle >= gravitySlipAngle)
{
// The surface slip angle has been exceeded to we need to slip with any remaining gravity distance.
const float remainingGravityDistance = gravityDistance - surfaceHit.distance;
// Clamp the slip direction.
const Box2D::b2Vec2 slipNormal = PhysicsUtility2D::Tob2Vec2(surfaceNormal);
gravityDirection -= b2Dot(gravityDirection, slipNormal) * slipNormal;
// Perform one slide (slope slip).
if (FindSlideContact(this, gravityDirection, remainingGravityDistance, slideHit, m_PhysicsScene, transform, colliders, colliderLayerMasks) > 0)
{
// Did we move a significant amount?
if (slideHit.distance > surfaceOffset)
{
// Yes, so calculate the distance we'd like to move.
const float distance = slideHit.distance - surfaceOffset;
// Move the transform by the hit distance.
transform.p += distance * gravityDirection;
}
}
else
{
// No hits so move the transform by the whole move.
transform.p += remainingGravityDistance * gravityDirection;
}
}
}
}
}
else
{
// No hits so move the transform by the whole move.
transform.p += gravityDistance * gravityDirection;
}
}
// Fetch the target position.
const Vector2f targetPosition = PhysicsUtility2D::ToVector2f(transform.p);
// If we're using "No Move" then we skip all movement options.
// NOTE: We'll simply return the target position.
if (!useNoMove)
{
// Are we using a simulation move?
if (useSimulationMove)
{
// Yes, so ask for a simulation move.
MovePosition(targetPosition);
}
else
{
// No, so fetch any interpolation pose.
RigidbodyInterpolationPose2D* pose = GetInterpolationPose();
if (pose != NULL && pose->enabled)
{
// Reset the pose.
ReadPose3D(pose->positionFrom, pose->rotationFrom);
pose->positionTo = pose->positionFrom;
pose->rotationTo = pose->rotationFrom;
// Flag as overridden.
pose->setupOverridden = true;
}
// Apply the move and wake the body.
m_Body->SetTransform(transform.p, transform.q.GetAngle());
m_Body->SetAwake(true);
// Update the transform pose.
Vector3f transformPosition;
Quaternionf transformRotation;
ReadPose3D(transformPosition, transformRotation);
GetComponent<Transform>().SetPositionAndRotationIgnoringSpecificSystems(transformPosition, transformRotation, GetPhysicsManager2D()->GetAllPhysicsTransformMasks());
}
}
// Return the results;
results.remainingVelocity = NormalizeSafe(PhysicsUtility2D::ToVector2f(direction), Vector2f::zero) * remainingDistance;
results.position = targetPosition;
results.iterationsUsed = iterationCount;
return results;
}
inline int FindSlideContact(
const Rigidbody2D* rigidbody,
const Box2D::b2Vec2& direction,
const float distance,
RaycastHit2D& closestHit,
PhysicsScene2D* physicsScene,
const Box2D::b2Transform& transform,
const Collider2D::ColliderArray& colliders,
const dynamic_array<UInt32>& colliderLayerMasks)
{
PROFILER_AUTO(gPhysics2DProfileRigidbody2D_SlideFindContact, rigidbody);
// Reset contact filter to filter by layer-mask (set-up later).
// NOTE: We don't want to hit triggers.
Collider2D::ContactFilter contactFilter;
contactFilter.m_UseTriggers = false;
contactFilter.m_UseLayerMask = true;
// Set-up to only see normals opposing our direction of motion.
const float normalAngle = Degrees(atan2(-direction.y, -direction.x));
contactFilter.SetNormalAngle(normalAngle - 89.9f, normalAngle + 89.9f);
const int colliderCount = colliders.size();
RaycastHit2DArray allHits(kMemTempAlloc);
// Iterate body colliders.
for (int i = 0; i < colliderCount; ++i)
{
const Collider2D* collider = colliders[i];
// Set-up a contact filter to filter by the layer-mask for this collider game-object layer.
contactFilter.m_LayerMask.m_Bits = colliderLayerMasks[i];
// Perform the cast and store the hit if we found one.
RaycastHit2D hit;
if (PhysicsQuery2D::ColliderCast(physicsScene, &transform, collider, PhysicsUtility2D::ToVector2f(direction), distance, contactFilter, true, &hit, 1) > 0)
{
// Only add as a hit if the collision isn't being ignored between the collider pair.
const Collider2D* hitCollider = reinterpret_cast<const Collider2D*>(Object::IDToPointer(hit.collider));
if (!physicsScene->GetIgnoreCollision(collider, hitCollider))
allHits.push_back(hit);
}
}
// Sort and find the closest hit.
const int resultCount = allHits.size();
if (resultCount > 0)
{
// Yes, so sort the hits by distance.
std::sort(allHits.begin(), allHits.end(), RaycastHit2D());
// Fetch the closest hit.
closestHit = allHits[0];
}
return resultCount;
}